订单跟踪bug13
订单跟踪严重 Bug 分析
本文档对当前系统订单跟踪链路(WebSocket 订单管理器、K 线服务消息路由、Executor 追踪与 HTTP 兜底)进行静态分析与数据流梳理,归纳可能导致订单状态错误、永久阻塞或资源问题的严重 bug,并给出修复方向。
一、订单跟踪架构简述
sequenceDiagram
participant Exec as Executor
participant WSM as WebSocketOrderManager
participant Kline as RealtimeKlineService
participant WS as EnhancedWSManager
Exec->>WSM: track_order(oid, coin, timeout)
WSM->>WSM: _timeout_then_verify 线程启动
Exec->>WSM: wait_for_order(tracking)
WS-->>Kline: on_message(orderUpdates/userFills)
Kline->>WSM: handle_message(msg)
WSM->>WSM: _on_order_update / _on_user_fill
WSM->>WSM: _resolve(oid) -> result_event.set()
Note over WSM: 超时则 HTTP query_order_status 兜底
- 状态解析唯一入口:
_resolve()(src/trading/websocket_order_manager.py),锁内改status、pop(oid),锁外result_event.set()。 - 订单消息来源:K 线服务的
on_message根据channel in ("orderUpdates", "userFills")转发给_get_ws_order_manager().handle_message(msg);若 manager 未就绪则写入_order_msg_buffer,就绪后先回放 buffer 再处理当前消息。
二、严重 Bug 列表
1. 订单消息缓冲区满时丢弃最旧消息,导致 WS 终态丢失(严重)
- 位置:
src/services/realtime_kline_service_base.py第 193 行:self._order_msg_buffer: deque = deque(maxlen=500);第 627 行:manager 未就绪时append(msg)。 - 问题:当 WebSocket 订单管理器长时间未就绪(例如交易模块启动晚、初始化异常)时,订单消息会不断入队。
deque(maxlen=500)满后,新消息会挤掉最旧消息(FIFO)。被挤掉的若是某订单的filled/canceled/rejected更新,该订单在管理器内会一直保持PENDING,直到超时后 HTTP 兜底才能解析。 - 影响:订单可能被误判为超时(TIMEOUT)或长时间阻塞(最多
timeout_seconds + _HTTP_VERIFY_BUFFER),增加延迟与误报,在 HTTP 也失败时可能两轮重试后强制 TIMEOUT。 - 修复方向:
- 方案 A:缓冲区有界时改为丢弃新消息并打日志,保证已缓冲的“旧消息”不丢(适合“宁可漏新不可丢旧”的语义);
- 方案 B:不设 maxlen,改为按条数/字节上限告警并触发降级(例如只做 HTTP 轮询),避免静默丢消息;
- 同时建议:manager 未就绪时尽早告警并限制 buffer 写入量,避免长时间无 manager 仍持续缓冲。
2. orderUpdates 消息中 status 字段位置未兼容(高风险)
- 位置:
src/trading/websocket_order_manager.py第 241 行:status_str = (item.get("status") or "").lower();解析逻辑仅使用item顶层字段。 - 问题:当前仅从
item.get("status")读取。若 Hyperliquid 将订单状态放在item.order.status或仅在嵌套对象中提供,则status_str始终为空,filled/canceled/rejected 分支均不会进入,该订单不会通过 WebSocket 被解析,只能依赖超时 + HTTP。 - 影响:所有依赖 WS 实时终态的订单都会退化为“超时后 HTTP 才解析”,失去实时性并增加误判 TIMEOUT 的风险。
- 修复方向:
- 查阅 Hyperliquid 官方文档或实际抓包确认 orderUpdates 单条
data项的结构(顶层statusvsorder.status); - 在解析时兼容两种位置,例如:
status_str = (item.get("status") or (item.get("order") or {}).get("status") or "").lower(),并补充单测或集成测试覆盖不同结构。
- 查阅 Hyperliquid 官方文档或实际抓包确认 orderUpdates 单条
3. cancel_all_open_orders 中 oid 类型未统一(中高)
- 位置:
src/trading/websocket_order_manager.py第 163 行:oid = order.get("oid"),直接传给self._executor._cancel_order(coin, oid);Executor_cancel_order(src/trading/executor.py)签名为oid: int。 - 问题:若
get_open_orders()或底层 API 某次返回的oid为字符串(例如不同端点或版本),未转为int可能导致类型错误或底层 API 调用异常,后续循环中的挂单可能未被取消,留下孤儿单。 - 影响:启动时“清理残留挂单”不完整,可能影响后续策略或风控假设。
- 修复方向:与现有
_safe_int_oid一致,对oid做安全转换:oid = _safe_int_oid(order.get("oid")),若为None则跳过该条并打日志,再调用_cancel_order(coin, oid)。
4. 重连后 verify_pending_orders 无并发限制(中)
- 位置:
src/trading/websocket_order_manager.py第 148–152 行:verify_pending_orders对每个 pending oid 依次调用_resolve_via_http(oid),无并发上限。 - 问题:重连后若存在大量 PENDING 订单,会同时发起大量 HTTP 请求(每个 oid 一次
query_order_status),可能触发交易所或网关限流、连接耗尽或延迟激增。 - 影响:重连后的补查失败或变慢,部分订单可能仍依赖超时路径才能解析。
- 修复方向:对
_resolve_via_http(oid)做并发限制(例如线程池或信号量,如最多 3–5 个并发),或串行 + 小延迟,避免瞬时打满 HTTP。
三、已确认无问题的设计点
- channel 字段:Hyperliquid 文档确认 orderUpdates 推送使用
channel: "orderUpdates",与当前msg.get("channel")路由一致。 - oid 类型:Executor 侧
order_id已用int(raw_oid)转换;WS 解析使用_safe_int_oid,类型一致。 - 重复追踪:同一 oid 再次
track_order时,旧 tracking 被设为 CANCELED 并result_event.set(),wait_for_order(old)会正确返回 False。 - _resolve 幂等与引用:
_resolve在锁内pop(oid)后,在锁外对同一tracking引用做set()和日志,对象仍有效,无 use-after-free 问题。
四、建议修复优先级
| 优先级 | Bug | 原因 |
|---|---|---|
| P0 | 缓冲区满丢弃最旧消息 | 直接导致订单 WS 终态丢失,只能等超时/HTTP |
| P1 | orderUpdates status 字段位置 | 若实际在 order 下,所有订单都无法通过 WS 解析 |
| P2 | cancel_all_open_orders oid 类型 | 防御性统一,避免孤儿单与异常 |
| P3 | verify_pending_orders 并发限制 | 重连场景下的稳定性与限流风险 |
完成 P0/P1 后,建议在测试环境或小流量下验证:订单从挂出到 filled/canceled 的 WS 解析率、超时率,以及重连后 pending 订单的 HTTP 补查成功率。