Coverage for core/data_source/adapters/trade_adapter.py: 53.20%
359 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-13 18:58 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-13 18:58 +0000
1"""
2交易数据源适配器
3提供统一的交易接口,屏蔽具体券商SDK差异
4"""
6from datetime import date, datetime
7from decimal import Decimal
8from typing import Any, Dict, List, Optional
10from core.data_source.adapters.data_source_adapter import DataSourceAdapter
12# 市场类型限制配置
13MARKET_ORDER_TYPES = {
14 "US": ["LO", "MO", "LIT", "MIT", "TSLPAMT", "TSLPPCT"],
15 "HK": [
16 "LO",
17 "ELO",
18 "MO",
19 "AO",
20 "ALO",
21 "ODD",
22 "LIT",
23 "MIT",
24 "TSLPAMT",
25 "TSLPPCT",
26 "SLO",
27 ],
28 "CN": ["LO", "MO"],
29 "SG": ["LO", "MO"],
30 "CRYPTO": ["LO", "MO"],
31}
33# 订单类型必填字段配置
34ORDER_TYPE_REQUIRED_FIELDS = {
35 "LO": ["submitted_price"], # 限价单
36 "ELO": ["submitted_price"], # 增强限价单
37 "ALO": ["submitted_price"], # 竞价限价单
38 "ODD": ["submitted_price"], # 碎股单
39 "LIT": ["submitted_price", "trigger_price"], # 触价限价单
40 "MIT": ["trigger_price"], # 触价市价单
41 "TSLPAMT": ["trailing_amount"], # 跟踪止损限价单(跟踪金额)
42 "TSLPPCT": ["trailing_percent"], # 跟踪止损限价单(跟踪涨跌幅)
43 "MO": [], # 市价单
44 "AO": [], # 竞价市价单
45 "SLO": ["submitted_price"], # 特殊限价单
46}
48# 美股必填字段
49US_REQUIRED_FIELDS = ["outside_rth"]
51# GTD订单类型必填字段
52GTD_REQUIRED_FIELDS = ["expire_date"]
55class TradeDataSourceAdapter(DataSourceAdapter):
56 """交易数据源适配器"""
58 def is_available(self) -> bool:
59 """检查是否可用"""
60 return self._get_client() is not None
62 def validate_order_params(
63 self,
64 symbol: str,
65 order_type: str,
66 time_in_force: str,
67 submitted_price: Optional[Decimal] = None,
68 trigger_price: Optional[Decimal] = None,
69 trailing_amount: Optional[Decimal] = None,
70 trailing_percent: Optional[Decimal] = None,
71 expire_date: Optional[date] = None,
72 outside_rth: Optional[str] = None,
73 ) -> Dict[str, Any]:
74 """验证订单参数"""
75 errors = []
76 warnings = []
78 # 从symbol中提取市场类型
79 market = self._extract_market_from_symbol(symbol)
81 # 验证市场类型是否支持该订单类型
82 if market in MARKET_ORDER_TYPES:
83 if order_type not in MARKET_ORDER_TYPES[market]:
84 errors.append(f"市场 {market} 不支持订单类型 {order_type}")
85 else:
86 warnings.append(f"未知市场类型: {market}")
88 # 验证订单类型必填字段
89 required_fields = ORDER_TYPE_REQUIRED_FIELDS.get(order_type, [])
90 for field in required_fields:
91 if field == "submitted_price" and submitted_price is None:
92 errors.append(f"订单类型 {order_type} 需要填写委托价格")
93 elif field == "trigger_price" and trigger_price is None:
94 errors.append(f"订单类型 {order_type} 需要填写触发价格")
95 elif field == "trailing_amount" and trailing_amount is None:
96 errors.append(f"订单类型 {order_type} 需要填写跟踪金额")
97 elif field == "trailing_percent" and trailing_percent is None:
98 errors.append(f"订单类型 {order_type} 需要填写跟踪涨跌幅")
100 # 验证美股必填字段
101 if market == "US" and outside_rth is None:
102 errors.append("美股订单必须选择盘前盘后交易选项")
104 # 验证GTD订单类型必填字段
105 if time_in_force == "GTD" and expire_date is None:
106 errors.append("GTD订单类型必须填写过期日期")
108 return {
109 "valid": len(errors) == 0,
110 "errors": errors,
111 "warnings": warnings,
112 "market": market,
113 }
115 def _extract_market_from_symbol(self, symbol: str) -> str:
116 """从股票代码中提取市场类型"""
117 if "." in symbol:
118 market_suffix = symbol.split(".")[-1].upper()
119 market_mapping = {
120 "US": "US",
121 "HK": "HK",
122 "SZ": "CN",
123 "SH": "CN",
124 "SG": "SG",
125 }
126 return market_mapping.get(market_suffix, "UNKNOWN")
127 return "UNKNOWN"
129 def get_supported_order_types(self, market: str) -> List[str]:
130 """获取指定市场支持的订单类型"""
131 return MARKET_ORDER_TYPES.get(market, [])
133 def get_order_type_required_fields(self, order_type: str) -> List[str]:
134 """获取订单类型必填字段"""
135 return ORDER_TYPE_REQUIRED_FIELDS.get(order_type, [])
137 def submit_order(
138 self,
139 symbol: str,
140 order_type: str,
141 side: str,
142 submitted_quantity: Decimal,
143 time_in_force: str,
144 submitted_price: Optional[Decimal] = None,
145 trigger_price: Optional[Decimal] = None,
146 limit_offset: Optional[Decimal] = None,
147 trailing_amount: Optional[Decimal] = None,
148 trailing_percent: Optional[Decimal] = None,
149 expire_date: Optional[date] = None,
150 outside_rth: Optional[str] = None,
151 remark: Optional[str] = None,
152 ) -> Optional[Dict[str, Any]]:
153 """委托下单"""
154 client = self._get_client()
155 if not client:
156 return None
158 # 验证订单参数
159 validation = self.validate_order_params(
160 symbol=symbol,
161 order_type=order_type,
162 time_in_force=time_in_force,
163 submitted_price=submitted_price,
164 trigger_price=trigger_price,
165 trailing_amount=trailing_amount,
166 trailing_percent=trailing_percent,
167 expire_date=expire_date,
168 outside_rth=outside_rth,
169 )
171 if not validation["valid"]:
172 return {
173 "success": False,
174 "message": "订单参数验证失败",
175 "errors": validation["errors"],
176 "warnings": validation["warnings"],
177 }
179 try:
180 trade_ctx = client.get("trade_ctx")
181 if trade_ctx:
182 # 转换枚举值
183 order_type_enum = self._get_order_type_enum(order_type)
184 side_enum = self._get_order_side_enum(side)
185 time_in_force_enum = self._get_time_in_force_enum(time_in_force)
186 outside_rth_enum = (
187 self._get_outside_rth_enum(outside_rth) if outside_rth else None
188 )
190 # 处理时区转换
191 processed_expire_date = self._convert_expire_date_timezone(
192 symbol, expire_date
193 )
195 # 调用LongPort下单接口
196 result = trade_ctx.submit_order(
197 symbol=symbol,
198 order_type=order_type_enum,
199 side=side_enum,
200 submitted_quantity=submitted_quantity,
201 time_in_force=time_in_force_enum,
202 submitted_price=submitted_price,
203 trigger_price=trigger_price,
204 limit_offset=limit_offset,
205 trailing_amount=trailing_amount,
206 trailing_percent=trailing_percent,
207 expire_date=processed_expire_date,
208 outside_rth=outside_rth_enum,
209 remark=remark,
210 )
212 return self._serialize_submit_order_response(result)
213 except Exception as e:
214 print(f"❌ 委托下单失败: {e}")
215 return {"success": False, "message": f"委托下单失败: {e}", "error": str(e)}
217 return None
219 def replace_order(
220 self,
221 order_id: str,
222 quantity: Decimal,
223 price: Optional[Decimal] = None,
224 trigger_price: Optional[Decimal] = None,
225 remark: Optional[str] = None,
226 ) -> bool:
227 """改单"""
228 client = self._get_client()
229 if not client:
230 return False
232 try:
233 trade_ctx = client.get("trade_ctx")
234 if trade_ctx:
235 trade_ctx.replace_order(
236 order_id=order_id,
237 quantity=quantity,
238 price=price,
239 trigger_price=trigger_price,
240 remark=remark,
241 )
242 return True
243 except Exception as e:
244 print(f"❌ 改单失败: {e}")
246 return False
248 def cancel_order(self, order_id: str) -> bool:
249 """撤单"""
250 client = self._get_client()
251 if not client:
252 return False
254 try:
255 trade_ctx = client.get("trade_ctx")
256 if trade_ctx:
257 trade_ctx.cancel_order(order_id)
258 return True
259 except Exception as e:
260 print(f"❌ 撤单失败: {e}")
262 return False
264 def get_today_orders(
265 self,
266 symbol: Optional[str] = None,
267 status: Optional[List[str]] = None,
268 side: Optional[str] = None,
269 market: Optional[str] = None,
270 order_id: Optional[str] = None,
271 ) -> List[Dict[str, Any]]:
272 """获取当日订单"""
273 client = self._get_client()
274 if not client:
275 return []
277 try:
278 trade_ctx = client.get("trade_ctx")
279 if trade_ctx:
280 # 转换枚举值
281 status_enums = None
282 if status:
283 status_enums = [self._get_order_status_enum(s) for s in status]
285 side_enum = None
286 if side:
287 side_enum = self._get_order_side_enum(side)
289 market_enum = None
290 if market:
291 market_enum = self._get_market_enum(market)
293 orders = trade_ctx.today_orders(
294 symbol=symbol,
295 status=status_enums,
296 side=side_enum,
297 market=market_enum,
298 order_id=order_id,
299 )
301 return [self._serialize_order(order) for order in orders]
302 except Exception as e:
303 print(f"❌ 获取当日订单失败: {e}")
305 return []
307 def get_history_orders(
308 self,
309 symbol: Optional[str] = None,
310 status: Optional[List[str]] = None,
311 side: Optional[str] = None,
312 market: Optional[str] = None,
313 start_at: Optional[datetime] = None,
314 end_at: Optional[datetime] = None,
315 ) -> List[Dict[str, Any]]:
316 """获取历史订单"""
317 client = self._get_client()
318 if not client:
319 return []
321 try:
322 trade_ctx = client.get("trade_ctx")
323 if trade_ctx:
324 # 设置默认时间范围
325 processed_start_at, processed_end_at = self._set_default_time_range(
326 symbol or "", start_at, end_at
327 )
329 # 转换时区
330 processed_start_at = self._convert_datetime_timezone(
331 symbol or "", processed_start_at
332 )
333 processed_end_at = self._convert_datetime_timezone(
334 symbol or "", processed_end_at
335 )
337 # 转换枚举值
338 status_enums = None
339 if status:
340 status_enums = [self._get_order_status_enum(s) for s in status]
342 side_enum = None
343 if side:
344 side_enum = self._get_order_side_enum(side)
346 market_enum = None
347 if market:
348 market_enum = self._get_market_enum(market)
350 orders = trade_ctx.history_orders(
351 symbol=symbol,
352 status=status_enums,
353 side=side_enum,
354 market=market_enum,
355 start_at=processed_start_at,
356 end_at=processed_end_at,
357 )
359 return [self._serialize_order(order) for order in orders]
360 except Exception as e:
361 print(f"❌ 获取历史订单失败: {e}")
363 return []
365 def get_today_executions(
366 self, symbol: Optional[str] = None, order_id: Optional[str] = None
367 ) -> List[Dict[str, Any]]:
368 """获取当日成交明细"""
369 client = self._get_client()
370 if not client:
371 return []
373 try:
374 trade_ctx = client.get("trade_ctx")
375 if trade_ctx:
376 executions = trade_ctx.today_executions(
377 symbol=symbol, order_id=order_id
378 )
380 return [
381 self._serialize_execution(execution) for execution in executions
382 ]
383 except Exception as e:
384 print(f"❌ 获取当日成交明细失败: {e}")
386 return []
388 def get_history_executions(
389 self,
390 symbol: Optional[str] = None,
391 start_at: Optional[datetime] = None,
392 end_at: Optional[datetime] = None,
393 ) -> List[Dict[str, Any]]:
394 """获取历史成交明细"""
395 client = self._get_client()
396 if not client:
397 return []
399 try:
400 trade_ctx = client.get("trade_ctx")
401 if trade_ctx:
402 # 设置默认时间范围
403 processed_start_at, processed_end_at = self._set_default_time_range(
404 symbol or "", start_at, end_at
405 )
407 # 转换时区
408 processed_start_at = self._convert_datetime_timezone(
409 symbol or "", processed_start_at
410 )
411 processed_end_at = self._convert_datetime_timezone(
412 symbol or "", processed_end_at
413 )
415 executions = trade_ctx.history_executions(
416 symbol=symbol, start_at=processed_start_at, end_at=processed_end_at
417 )
419 return [
420 self._serialize_execution(execution) for execution in executions
421 ]
422 except Exception as e:
423 print(f"❌ 获取历史成交明细失败: {e}")
425 return []
427 def get_order_detail(self, order_id: str) -> Optional[Dict[str, Any]]:
428 """获取订单详情"""
429 client = self._get_client()
430 if not client:
431 return None
433 try:
434 trade_ctx = client.get("trade_ctx")
435 if trade_ctx:
436 order_detail = trade_ctx.order_detail(order_id)
437 return self._serialize_order_detail(order_detail)
438 except Exception as e:
439 print(f"❌ 获取订单详情失败: {e}")
441 return None
443 def get_account_balance(
444 self, currency: Optional[str] = None
445 ) -> List[Dict[str, Any]]:
446 """获取账户余额"""
447 client = self._get_client()
448 if not client:
449 return []
451 try:
452 trade_ctx = client.get("trade_ctx")
453 if trade_ctx:
454 balances = trade_ctx.account_balance(currency=currency)
455 serialized_balances = []
456 for balance in balances:
457 try:
458 serialized_balance = self._serialize_account_balance(balance)
459 if serialized_balance: # 确保序列化成功
460 serialized_balances.append(serialized_balance)
461 except Exception as e:
462 print(f"❌ 序列化单个账户余额失败: {e}")
463 continue
464 return serialized_balances
465 except Exception as e:
466 print(f"❌ 获取账户余额失败: {e}")
468 return []
470 def estimate_max_purchase_quantity(
471 self,
472 symbol: str,
473 order_type: str,
474 side: str,
475 price: Optional[Decimal] = None,
476 currency: Optional[str] = None,
477 order_id: Optional[str] = None,
478 fractional_shares: bool = False,
479 ) -> Optional[Dict[str, Any]]:
480 """估算最大购买数量"""
481 client = self._get_client()
482 if not client:
483 return None
485 try:
486 trade_ctx = client.get("trade_ctx")
487 if trade_ctx:
488 # 转换枚举值
489 order_type_enum = self._get_order_type_enum(order_type)
490 side_enum = self._get_order_side_enum(side)
492 result = trade_ctx.estimate_max_purchase_quantity(
493 symbol=symbol,
494 order_type=order_type_enum,
495 side=side_enum,
496 price=price,
497 currency=currency,
498 order_id=order_id,
499 fractional_shares=fractional_shares,
500 )
502 return self._serialize_max_purchase_quantity_response(result)
503 except Exception as e:
504 print(f"❌ 估算最大购买数量失败: {e}")
506 return None
508 # 序列化方法
509 def _serialize_submit_order_response(self, response) -> Dict[str, Any]:
510 """序列化下单响应"""
511 if not response:
512 return {}
514 try:
515 return {
516 "order_id": getattr(response, "order_id", ""),
517 "status": str(getattr(response, "status", "")),
518 "message": getattr(response, "message", ""),
519 }
520 except Exception as e:
521 print(f"❌ 序列化下单响应失败: {e}")
522 return {}
524 def _serialize_order(self, order) -> Dict[str, Any]:
525 """序列化订单信息"""
526 if not order:
527 return {}
529 try:
530 return {
531 "order_id": getattr(order, "order_id", ""),
532 "status": str(getattr(order, "status", "")),
533 "stock_name": getattr(order, "stock_name", ""),
534 "symbol": getattr(order, "symbol", ""),
535 "side": str(getattr(order, "side", "")),
536 "order_type": str(getattr(order, "order_type", "")),
537 "quantity": float(getattr(order, "quantity", 0)),
538 "executed_quantity": float(getattr(order, "executed_quantity", 0)),
539 "price": (
540 float(getattr(order, "price", 0))
541 if getattr(order, "price", None) is not None
542 else None
543 ),
544 "executed_price": (
545 float(getattr(order, "executed_price", 0))
546 if getattr(order, "executed_price", None) is not None
547 else None
548 ),
549 "submitted_at": getattr(order, "submitted_at", None),
550 "updated_at": getattr(order, "updated_at", None),
551 "last_done": (
552 float(getattr(order, "last_done", 0))
553 if getattr(order, "last_done", None) is not None
554 else None
555 ),
556 "trigger_price": (
557 float(getattr(order, "trigger_price", 0))
558 if getattr(order, "trigger_price", None) is not None
559 else None
560 ),
561 "msg": getattr(order, "msg", ""),
562 "tag": str(getattr(order, "tag", "")),
563 "time_in_force": str(getattr(order, "time_in_force", "")),
564 "expire_date": getattr(order, "expire_date", None),
565 "currency": getattr(order, "currency", ""),
566 "remark": getattr(order, "remark", ""),
567 }
568 except Exception as e:
569 print(f"❌ 序列化订单信息失败: {e}")
570 return {}
572 def _serialize_execution(self, execution) -> Dict[str, Any]:
573 """序列化成交明细"""
574 if not execution:
575 return {}
577 try:
578 return {
579 "order_id": getattr(execution, "order_id", ""),
580 "trade_id": getattr(execution, "trade_id", ""),
581 "symbol": getattr(execution, "symbol", ""),
582 "trade_done_at": getattr(execution, "trade_done_at", None),
583 "quantity": float(getattr(execution, "quantity", 0)),
584 "price": float(getattr(execution, "price", 0)),
585 }
586 except Exception as e:
587 print(f"❌ 序列化成交明细失败: {e}")
588 return {}
590 def _serialize_order_detail(self, order_detail) -> Dict[str, Any]:
591 """序列化订单详情"""
592 if not order_detail:
593 return {}
595 try:
596 # 序列化history字段
597 history = []
598 for hist_item in getattr(order_detail, "history", []):
599 history.append(
600 {
601 "status": str(getattr(hist_item, "status", "")),
602 "time": getattr(hist_item, "time", None),
603 "quantity": float(getattr(hist_item, "quantity", 0)),
604 "price": (
605 float(getattr(hist_item, "price", 0))
606 if getattr(hist_item, "price", None) is not None
607 else None
608 ),
609 "msg": getattr(hist_item, "msg", ""),
610 }
611 )
613 # 序列化charge_detail字段
614 charge_detail = {}
615 charge_detail_obj = getattr(order_detail, "charge_detail", None)
616 if charge_detail_obj:
617 charge_detail = {
618 "total_charges": float(
619 getattr(charge_detail_obj, "total_charges", 0)
620 ),
621 "currency": getattr(charge_detail_obj, "currency", ""),
622 "charges": [],
623 }
625 # 序列化charges列表
626 for charge in getattr(charge_detail_obj, "charges", []):
627 charge_detail["charges"].append(
628 {
629 "code": getattr(charge, "code", ""),
630 "name": getattr(charge, "name", ""),
631 "fees": [],
632 }
633 )
635 # 序列化fees列表
636 for fee in getattr(charge, "fees", []):
637 charge_detail["charges"][-1]["fees"].append(
638 {
639 "code": getattr(fee, "code", ""),
640 "name": getattr(fee, "name", ""),
641 "amount": float(getattr(fee, "amount", 0)),
642 "currency": getattr(fee, "currency", ""),
643 }
644 )
646 return {
647 "order_id": getattr(order_detail, "order_id", ""),
648 "status": str(getattr(order_detail, "status", "")),
649 "stock_name": getattr(order_detail, "stock_name", ""),
650 "symbol": getattr(order_detail, "symbol", ""),
651 "side": str(getattr(order_detail, "side", "")),
652 "order_type": str(getattr(order_detail, "order_type", "")),
653 "quantity": float(getattr(order_detail, "quantity", 0)),
654 "executed_quantity": float(
655 getattr(order_detail, "executed_quantity", 0)
656 ),
657 "price": (
658 float(getattr(order_detail, "price", 0))
659 if getattr(order_detail, "price", None) is not None
660 else None
661 ),
662 "executed_price": (
663 float(getattr(order_detail, "executed_price", 0))
664 if getattr(order_detail, "executed_price", None) is not None
665 else None
666 ),
667 "submitted_at": getattr(order_detail, "submitted_at", None),
668 "updated_at": getattr(order_detail, "updated_at", None),
669 "last_done": (
670 float(getattr(order_detail, "last_done", 0))
671 if getattr(order_detail, "last_done", None) is not None
672 else None
673 ),
674 "trigger_price": (
675 float(getattr(order_detail, "trigger_price", 0))
676 if getattr(order_detail, "trigger_price", None) is not None
677 else None
678 ),
679 "msg": getattr(order_detail, "msg", ""),
680 "tag": str(getattr(order_detail, "tag", "")),
681 "time_in_force": str(getattr(order_detail, "time_in_force", "")),
682 "expire_date": getattr(order_detail, "expire_date", None),
683 "currency": getattr(order_detail, "currency", ""),
684 "remark": getattr(order_detail, "remark", ""),
685 "free_status": str(getattr(order_detail, "free_status", "")),
686 "free_amount": (
687 float(getattr(order_detail, "free_amount", 0))
688 if getattr(order_detail, "free_amount", None) is not None
689 else None
690 ),
691 "free_currency": getattr(order_detail, "free_currency", ""),
692 "deductions_status": str(
693 getattr(order_detail, "deductions_status", "")
694 ),
695 "deductions_amount": (
696 float(getattr(order_detail, "deductions_amount", 0))
697 if getattr(order_detail, "deductions_amount", None) is not None
698 else None
699 ),
700 "deductions_currency": getattr(order_detail, "deductions_currency", ""),
701 "platform_deducted_status": str(
702 getattr(order_detail, "platform_deducted_status", "")
703 ),
704 "platform_deducted_amount": (
705 float(getattr(order_detail, "platform_deducted_amount", 0))
706 if getattr(order_detail, "platform_deducted_amount", None)
707 is not None
708 else None
709 ),
710 "platform_deducted_currency": getattr(
711 order_detail, "platform_deducted_currency", ""
712 ),
713 "history": history,
714 "charge_detail": charge_detail,
715 }
716 except Exception as e:
717 print(f"❌ 序列化订单详情失败: {e}")
718 return {}
720 def _serialize_account_balance(self, balance) -> Dict[str, Any]:
721 """序列化账户余额"""
722 if not balance:
723 return {}
725 try:
726 # 序列化cash_infos
727 cash_infos = []
728 cash_infos_raw = getattr(balance, "cash_infos", [])
729 if cash_infos_raw:
730 for cash_info in cash_infos_raw:
731 try:
732 cash_infos.append(
733 {
734 "currency": str(getattr(cash_info, "currency", "")),
735 "available_cash": float(
736 getattr(cash_info, "available_cash", 0)
737 ),
738 "frozen_cash": float(
739 getattr(cash_info, "frozen_cash", 0)
740 ),
741 "settling_cash": float(
742 getattr(cash_info, "settling_cash", 0)
743 ),
744 "currency_earning": float(
745 getattr(cash_info, "currency_earning", 0)
746 ),
747 }
748 )
749 except Exception as e:
750 print(f"❌ 序列化cash_info失败: {e}")
751 continue
753 # 序列化frozen_transaction_fees
754 frozen_fees = []
755 frozen_fees_raw = getattr(balance, "frozen_transaction_fees", [])
756 if frozen_fees_raw:
757 for fee in frozen_fees_raw:
758 try:
759 frozen_fees.append(
760 {
761 "currency": str(getattr(fee, "currency", "")),
762 "frozen_transaction_fee": float(
763 getattr(fee, "frozen_transaction_fee", 0)
764 ),
765 }
766 )
767 except Exception as e:
768 print(f"❌ 序列化frozen_fee失败: {e}")
769 continue
771 return {
772 "total_cash": float(getattr(balance, "total_cash", 0)),
773 "net_assets": float(getattr(balance, "net_assets", 0)),
774 "currency": str(getattr(balance, "currency", "")),
775 "max_finance_amount": float(getattr(balance, "max_finance_amount", 0)),
776 "remaining_finance_amount": float(
777 getattr(balance, "remaining_finance_amount", 0)
778 ),
779 "risk_level": int(getattr(balance, "risk_level", 0)),
780 "margin_call": float(getattr(balance, "margin_call", 0)),
781 "init_margin": float(getattr(balance, "init_margin", 0)),
782 "maintenance_margin": float(getattr(balance, "maintenance_margin", 0)),
783 "buy_power": float(getattr(balance, "buy_power", 0)),
784 "cash_infos": cash_infos,
785 "frozen_transaction_fees": frozen_fees,
786 }
787 except Exception as e:
788 print(f"❌ 序列化账户余额失败: {e}")
789 return {}
791 def _serialize_max_purchase_quantity_response(self, response) -> Dict[str, Any]:
792 """序列化最大购买数量响应"""
793 if not response:
794 return {}
796 try:
797 return {
798 "cash_max_qty": float(getattr(response, "cash_max_qty", 0)),
799 "margin_max_qty": float(getattr(response, "margin_max_qty", 0)),
800 }
801 except Exception as e:
802 print(f"❌ 序列化最大购买数量响应失败: {e}")
803 return {}
805 # 枚举转换辅助方法
806 def _get_order_type_enum(self, order_type: str):
807 """转换订单类型枚举"""
808 from longport.openapi import OrderType
810 # 处理常见的命名差异
811 order_type_mapping = {
812 "LO": "LO", # Limit Order
813 "MO": "MO", # Market Order
814 "ELO": "ELO", # Enhanced Limit Order
815 "AO": "AO", # At-auction Order
816 "ALO": "ALO", # At-auction Limit Order
817 "ODD": "ODD", # Odd Lots
818 "LIT": "LIT", # Limit If Touched
819 "MIT": "MIT", # Market If Touched
820 "TSLPAMT": "TSLPAMT", # Trailing Limit If Touched (Trailing Amount)
821 "TSLPPCT": "TSLPPCT", # Trailing Limit If Touched (Trailing Percent)
822 "TSMAMT": "TSMAMT", # Trailing Market If Touched (Trailing Amount)
823 "TSMPCT": "TSMPCT", # Trailing Market If Touched (Trailing Percent)
824 "SLO": "SLO", # Special Limit Order
825 }
827 mapped_type = order_type_mapping.get(order_type, order_type)
828 return getattr(OrderType, mapped_type)
830 def _get_order_side_enum(self, side: str):
831 """转换订单方向枚举"""
832 from longport.openapi import OrderSide
834 side_mapping = {"Buy": "Buy", "Sell": "Sell", "Unknown": "Unknown"}
836 mapped_side = side_mapping.get(side, side)
837 return getattr(OrderSide, mapped_side)
839 def _get_time_in_force_enum(self, time_in_force: str):
840 """转换订单有效期枚举"""
841 from longport.openapi import TimeInForceType
843 time_in_force_mapping = {
844 "Day": "Day",
845 "GTC": "GoodTilCanceled",
846 "GTD": "GoodTilDate",
847 "GoodTilCanceled": "GoodTilCanceled",
848 "GoodTilDate": "GoodTilDate",
849 "Unknown": "Unknown",
850 }
852 mapped_tif = time_in_force_mapping.get(time_in_force, time_in_force)
853 return getattr(TimeInForceType, mapped_tif)
855 def _get_order_status_enum(self, status: str):
856 """转换订单状态枚举"""
857 from longport.openapi import OrderStatus
859 status_mapping = {
860 "Unknown": "Unknown",
861 "NotReported": "NotReported",
862 "ReplacedNotReported": "ReplacedNotReported",
863 "ProtectedNotReported": "ProtectedNotReported",
864 "VarietiesNotReported": "VarietiesNotReported",
865 "Filled": "Filled",
866 "WaitToNew": "WaitToNew",
867 "New": "New",
868 "WaitToReplace": "WaitToReplace",
869 "PendingReplace": "PendingReplace",
870 "Replaced": "Replaced",
871 "PartialFilled": "PartialFilled",
872 "WaitToCancel": "WaitToCancel",
873 "PendingCancel": "PendingCancel",
874 "Rejected": "Rejected",
875 "Canceled": "Canceled",
876 "Expired": "Expired",
877 "PartialWithdrawal": "PartialWithdrawal",
878 }
880 mapped_status = status_mapping.get(status, status)
881 return getattr(OrderStatus, mapped_status)
883 def _convert_expire_date_timezone(
884 self, symbol: str, expire_date: Optional[date]
885 ) -> Optional[date]:
886 """根据市场类型转换过期日期的时区"""
887 if expire_date is None:
888 return None
890 market = self._extract_market_from_symbol(symbol)
892 if market == "US":
893 # 美股:前端输入的是美东时间日期,LongPort API期望的也是美东时间
894 # 不需要转换,直接返回
895 return expire_date
896 elif market == "HK":
897 # 港股:前端输入的是北京时间日期,LongPort API期望的是香港时间
898 # 香港时间和北京时间相同,不需要转换
899 return expire_date
900 else:
901 # 其他市场:直接返回
902 return expire_date
904 def _convert_datetime_timezone(
905 self, symbol: str, dt: Optional[datetime]
906 ) -> Optional[datetime]:
907 """根据市场类型转换日期时间的时区"""
908 if dt is None:
909 return None
911 market = self._extract_market_from_symbol(symbol)
913 if market == "US":
914 # 美股:前端输入的是美东时间,LongPort API期望的也是美东时间
915 # 不需要转换,直接返回
916 return dt
917 elif market == "HK":
918 # 港股:前端输入的是北京时间,LongPort API期望的是香港时间
919 # 香港时间和北京时间相同,不需要转换
920 return dt
921 else:
922 # 其他市场:直接返回
923 return dt
925 def _set_default_time_range(
926 self, symbol: str, start_at: Optional[datetime], end_at: Optional[datetime]
927 ) -> tuple[Optional[datetime], Optional[datetime]]:
928 """设置默认时间范围"""
929 if start_at is None and end_at is None:
930 # 如果没有指定时间范围,默认查询最近7天
931 from datetime import timedelta
933 end_at = datetime.now()
934 start_at = end_at - timedelta(days=7)
936 if start_at is not None:
937 # 开始时间默认设置为00:00:00
938 start_at = start_at.replace(hour=0, minute=0, second=0, microsecond=0)
940 if end_at is not None:
941 # 结束时间默认设置为23:59:00
942 end_at = end_at.replace(hour=23, minute=59, second=0, microsecond=0)
944 return start_at, end_at
946 def _get_outside_rth_enum(self, outside_rth: str):
947 """转换盘前盘后交易枚举"""
948 from longport.openapi import OutsideRTH
950 outside_rth_mapping = {
951 "RTH_ONLY": "RTHOnly",
952 "ANY_TIME": "AnyTime",
953 "OVERNIGHT": "Overnight",
954 "UnknownOutsideRth": "Unknown",
955 }
957 mapped_outside_rth = outside_rth_mapping.get(outside_rth, outside_rth)
958 return getattr(OutsideRTH, mapped_outside_rth)
960 def _get_market_enum(self, market: str):
961 """转换市场枚举"""
962 from longport.openapi import Market
964 market_mapping = {
965 "US": "US",
966 "HK": "HK",
967 "CN": "CN",
968 "SG": "SG",
969 "Crypto": "Crypto",
970 "Unknown": "Unknown",
971 }
973 mapped_market = market_mapping.get(market, market)
974 return getattr(Market, mapped_market)
976 def refresh_access_token(self) -> Optional[Dict[str, Any]]:
977 """刷新访问令牌"""
978 try:
979 client = self._get_client()
980 if not client:
981 return {"success": False, "message": "无法获取长桥客户端"}
983 # 获取Config对象
984 config = client.get("config")
985 if not config:
986 return {"success": False, "message": "无法获取长桥配置对象"}
988 # 设置过期时间为6个月后
989 from datetime import datetime, timedelta
991 expired_at = datetime.now() + timedelta(days=180) # 6个月 = 180天
993 # 调用LongPort SDK的refresh_access_token方法
994 new_token = config.refresh_access_token(expired_at)
995 if new_token:
996 return {
997 "success": True,
998 "message": "Token刷新成功",
999 "new_token": new_token,
1000 "expired_at": expired_at.isoformat(),
1001 }
1002 else:
1003 return {"success": False, "message": "Token刷新失败:未返回新token"}
1004 except Exception as e:
1005 return {"success": False, "message": f"Token刷新失败: {str(e)}"}