mock_trading_normalize.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. """
  2. 妙想模拟交易 — 委托列表字段规范化
  3. 上游 `/api/claw/mockTrading/orders` 返回的字段名与取值在不同版本间可能不一致:
  4. - 买卖方向可能是数值(如 1=买入、2=卖出)而非字符串 buy/sell
  5. - 委托状态多为数值枚举,需映射为前端可用的 pending/done 等
  6. - 代码、名称、委托号、时间等可能存在 snake_case 或其它别名
  7. 此处集中做兼容解析,供 simulation_service 与 Agent 工具共用。
  8. """
  9. from __future__ import annotations
  10. from typing import Any, Mapping, Optional
  11. def extract_orders_dicts(data: Any) -> list[dict[str, Any]]:
  12. """从妙想 data 中取出委托对象列表(兼容 list、或 { rows/list/... } 包裹)。"""
  13. if not isinstance(data, Mapping):
  14. return []
  15. def coerce_dict_list(raw: Any) -> list[dict[str, Any]]:
  16. if raw is None:
  17. return []
  18. if isinstance(raw, list):
  19. return [x for x in raw if isinstance(x, dict)]
  20. if isinstance(raw, dict):
  21. for inner_key in ("rows", "records", "list", "items", "data"):
  22. inner = raw.get(inner_key)
  23. if isinstance(inner, list):
  24. return [x for x in inner if isinstance(x, dict)]
  25. return []
  26. # 单一列表字段优先(避免把多个片段重复拼接)
  27. for key in ("orders", "orderList", "list", "entrustList"):
  28. chunk = coerce_dict_list(data.get(key))
  29. if chunk:
  30. return chunk
  31. # 当日 + 历史分段返回时合并(仅当顶层未给出统一 orders)
  32. merged: list[dict[str, Any]] = []
  33. for key in ("todayOrders", "today_order_list", "historyOrders", "hisOrders"):
  34. merged.extend(coerce_dict_list(data.get(key)))
  35. return merged
  36. def _first(d: Mapping[str, Any], keys: tuple[str, ...]) -> Any:
  37. """从左到右取第一个非空值(None / 空字符串跳过)。"""
  38. for k in keys:
  39. if k not in d:
  40. continue
  41. v = d[k]
  42. if v is None or v == "":
  43. continue
  44. return v
  45. return None
  46. def _nested_stock(order: Mapping[str, Any]) -> Mapping[str, Any]:
  47. """部分响应把证券信息放在子对象里。"""
  48. for k in ("stock", "security", "stockInfo"):
  49. sub = order.get(k)
  50. if isinstance(sub, Mapping):
  51. return sub
  52. return {}
  53. def parse_trade_type(order: Mapping[str, Any]) -> str:
  54. """解析为 'buy' 或 'sell'(妙想常见:数值 1=买入,2=卖出)。"""
  55. # 关键:不能用「第一个非空字段」—— tradeType/trade_type 常为委托类别(如 5),
  56. # 真正的买卖方向在 orderDrt/orderBs 等字段,必须逐个尝试直到解析成功。
  57. dir_keys = (
  58. "orderDrt",
  59. "orderBs",
  60. "orderDirection",
  61. "bsFlag",
  62. "bsType",
  63. "entrustBs",
  64. "mmlx",
  65. "direction",
  66. "side",
  67. "tradeType",
  68. "trade_type",
  69. )
  70. nested = _nested_stock(order)
  71. def norm(val: Any) -> Optional[str]:
  72. if val is None:
  73. return None
  74. if isinstance(val, bool):
  75. return None
  76. if isinstance(val, (int, float)):
  77. iv = int(val)
  78. # 东方财富系常见:1 买入,2 卖出
  79. if iv == 1:
  80. return "buy"
  81. if iv == 2:
  82. return "sell"
  83. return None
  84. s = str(val).strip().lower()
  85. if s in ("buy", "b", "1", "买入", "买"):
  86. return "buy"
  87. if s in ("sell", "s", "2", "卖出", "卖"):
  88. return "sell"
  89. return None
  90. for src in (order, nested):
  91. for k in dir_keys:
  92. if k not in src:
  93. continue
  94. val = src[k]
  95. if val is None or val == "":
  96. continue
  97. parsed = norm(val)
  98. if parsed:
  99. return parsed
  100. # type:部分接口表示买卖;也可能是限价/市价等业务类型,故仅在可识别时采用
  101. for src in (order, nested):
  102. t_raw = src.get("type")
  103. parsed_t = norm(t_raw)
  104. if parsed_t:
  105. return parsed_t
  106. return ""
  107. def parse_order_status(order: Mapping[str, Any]) -> tuple[str, str]:
  108. """
  109. 返回 (canonical_status, chinese_label)。
  110. canonical 供前端撤单逻辑使用:pending / done / part_deal / canceled / unknown
  111. """
  112. nested = _nested_stock(order)
  113. status_keys = (
  114. "status",
  115. "order_status",
  116. "orderStatus",
  117. "entrustStatus",
  118. "wtStatus",
  119. "dealStatus",
  120. )
  121. def to_canonical_and_label(val: Any) -> tuple[str, str]:
  122. if val is None:
  123. return "unknown", "未知"
  124. if isinstance(val, str):
  125. sl = val.strip().lower()
  126. known = {
  127. "pending": ("pending", "未成交"),
  128. "unfilled": ("pending", "未成交"),
  129. "open": ("pending", "未成交"),
  130. "done": ("done", "已成交"),
  131. "filled": ("done", "已成交"),
  132. "success": ("done", "已成交"),
  133. "part_deal": ("part_deal", "部分成交"),
  134. "partial": ("part_deal", "部分成交"),
  135. "canceled": ("canceled", "已撤销"),
  136. "cancelled": ("canceled", "已撤销"),
  137. "withdrawn": ("canceled", "已撤销"),
  138. }
  139. if sl in known:
  140. return known[sl]
  141. # 已是中文等情况:当作未知但保留原文展示
  142. if sl.isdigit():
  143. return to_canonical_and_label(int(sl))
  144. return "unknown", str(val)
  145. try:
  146. iv = int(val)
  147. except (TypeError, ValueError):
  148. return "unknown", str(val)
  149. # 常见券商委托状态码(保守映射,未覆盖的显示「状态{n}」)
  150. mapping: dict[int, tuple[str, str]] = {
  151. 0: ("pending", "待报/待成交"),
  152. 1: ("pending", "未成交"),
  153. 2: ("part_deal", "部分成交"),
  154. 3: ("done", "已成交"),
  155. 4: ("part_deal", "部成部撤"),
  156. 5: ("canceled", "已撤销"),
  157. 6: ("canceled", "已撤"),
  158. 7: ("done", "成交"),
  159. 8: ("canceled", "废单"),
  160. }
  161. if iv in mapping:
  162. return mapping[iv]
  163. return "unknown", f"状态{iv}"
  164. for src in (order, nested):
  165. for k in status_keys:
  166. if k not in src:
  167. continue
  168. val = src[k]
  169. if val is None or val == "":
  170. continue
  171. return to_canonical_and_label(val)
  172. return to_canonical_and_label(None)
  173. def normalize_mock_order_row(order: Mapping[str, Any]) -> dict[str, Any]:
  174. """将单条原始委托转为前端/路由使用的统一结构。"""
  175. nested = _nested_stock(order)
  176. order_id = _first(
  177. order,
  178. (
  179. "orderId",
  180. "order_id",
  181. "orderNo",
  182. "orderNO",
  183. "wtOrderId",
  184. "entrustId",
  185. "wth",
  186. "wtbh",
  187. "contractId",
  188. "id",
  189. ),
  190. )
  191. stock_code = _first(
  192. order,
  193. ("stockCode", "stock_code", "securityCode", "zqdm", "scode", "code", "stockcode"),
  194. )
  195. if stock_code is None:
  196. stock_code = _first(nested, ("stockCode", "stock_code", "securityCode", "zqdm", "code"))
  197. stock_name = _first(order, ("stockName", "stock_name", "name", "sname", "securityName"))
  198. if stock_name is None:
  199. stock_name = _first(nested, ("stockName", "stock_name", "name"))
  200. price_raw = _first(
  201. order,
  202. ("price", "wtjg", "orderPrice", "order_price", "entrustPrice", "limitPrice", "wtPrice"),
  203. )
  204. try:
  205. price = float(price_raw) if price_raw is not None else 0.0
  206. except (TypeError, ValueError):
  207. price = 0.0
  208. qty_raw = _first(
  209. order,
  210. (
  211. "quantity",
  212. "wtVolume",
  213. "wtVol",
  214. "wtvol",
  215. "orderQty",
  216. "order_qty",
  217. "orderVolume",
  218. "vol",
  219. "volume",
  220. "stockNum",
  221. "entrustAmount",
  222. ),
  223. )
  224. try:
  225. quantity = int(qty_raw) if qty_raw is not None else 0
  226. except (TypeError, ValueError):
  227. quantity = 0
  228. create_time = _first(
  229. order,
  230. (
  231. "createTime",
  232. "create_time",
  233. "orderTime",
  234. "order_time",
  235. "entrustTime",
  236. "reportTime",
  237. "tradeTime",
  238. "wtTime",
  239. "report_time",
  240. ),
  241. )
  242. if create_time is None:
  243. create_time = _first(nested, ("createTime", "create_time"))
  244. trade_type = parse_trade_type(order)
  245. status, status_text = parse_order_status(order)
  246. return {
  247. "order_id": str(order_id) if order_id is not None else "",
  248. "stock_code": str(stock_code) if stock_code is not None else "",
  249. "stock_name": str(stock_name) if stock_name is not None else "",
  250. "trade_type": trade_type,
  251. "price": price,
  252. "quantity": quantity,
  253. "status": status,
  254. "status_text": status_text,
  255. "create_time": str(create_time) if create_time is not None else "",
  256. }