当前系统订单跟踪存在哪些严重的bug11
订单跟踪严重 Bug 分析报告
1. 架构与数据流概览
订单跟踪涉及三处核心代码:
src/trading/websocket_order_manager.py:限价单状态追踪(orderUpdates / userFills → 统一_resolve())src/trading/executor.py:_track_limit_order()调用track_order+wait_for_order,并根据tracking.status更新OrderResultsrc/services/realtime_kline_service_base.py:on_message中按channel将 orderUpdates/userFills 路由到WebSocketOrderManager.handle_message()
sequenceDiagram
participant Exec as Executor
participant WSM as WebSocketOrderManager
participant WS as WebSocket/K线服务
Exec->>WSM: track_order(oid, coin, timeout)
WSM->>WSM: _timeout_then_verify 后台线程
Exec->>WSM: wait_for_order(tracking)
WS->>WSM: handle_message(orderUpdates/userFills)
WSM->>WSM: _resolve(oid)
WSM-->>Exec: result_event.set() -> 返回 FILLED/其他
2. 严重 Bug 列表
Bug 1:超时线程与 WS “filled” 的竞态 —— 已成交被误判为 TIMEOUT(高严重)
位置:websocket_order_manager.py 中 _timeout_then_verify(约 317–329 行)。
现象:HTTP 查询返回订单仍为 open 后,在“设置 _terminal_status = OrderStatus.TIMEOUT”与“调用 _resolve(oid)”之间存在时间窗。若此时 WebSocket 的 orderUpdates “filled” 到达,会先把同一 tracking 的 _terminal_status 设为 FILLED;若随后超时线程先进入 _resolve(),则读到的是自己刚写的 TIMEOUT(或已被 WS 改成 FILLED,取决于谁后写、谁先进 _resolve)。
更糟糕的场景:超时线程先执行完 with self._lock: current._terminal_status = TIMEOUT 并释放锁,然后再调用 _resolve(oid)。在这段“无锁”间隙内,WS 线程可能尚未被调度,超时线程接着在 _resolve() 里看到 _terminal_status == TIMEOUT,从而把实际已成交的订单结算为 TIMEOUT。下游 Executor 会认为“未成交”,可能触发撤单、部分成交分支或错误通知。
根因:在“HTTP 确认为 open → 强制超时”路径中,从设 TIMEOUT 到真正解析之间没有再次用 HTTP 确认是否已成交,竞态窗口内 WS 可能晚到或尚未被处理。
修复方向:在设置 _terminal_status = OrderStatus.TIMEOUT 之后、调用 _resolve(oid) 之前,再做一次 HTTP query_order_status(oid)。若结果为 filled/canceled/rejected,则按该终态更新 _terminal_status 及价格/数量后再 _resolve(oid);仅当第二次 HTTP 仍为 open 时才按 TIMEOUT 解析。这样可大幅缩小“误判为超时”的窗口。
Bug 2:订单管理器未就绪时消息只缓冲不丢弃,可能无限堆积(中高严重)
位置:realtime_kline_service_base.py 约 614–633 行。
现象:当 _get_ws_order_manager() 返回 None(例如交易未启用或 Orchestrator 启动失败/未就绪)时,orderUpdates/userFills 会被放入 _order_msg_buffer(无界 deque)。仅当 buf_size > 200 时打 ERROR 日志,不会丢弃消息。若订单管理器长期不可用(如配置错误或启动失败),缓冲区会持续增长,占用内存且重连/恢复后回放顺序与真实时间可能不符,影响状态一致性。
根因:缓冲区无上限、无淘汰策略,且“未就绪”场景未区分“暂时未就绪”与“永久不可用”。
修复方向:
- 为
_order_msg_buffer设置最大长度(例如 500),超出时丢弃最旧或最新消息并打告警;和/或 - 若检测到“订单管理器永久不可用”(例如交易已禁用),则不再缓冲、直接丢弃并打一次告警,避免无界增长。
Bug 3:重连后补查与超时线程的重复/竞态(中严重)
位置:websocket_order_manager.py 中 verify_pending_orders()(约 127–141 行)与 _timeout_then_verify / _resolve_via_http。
现象:WebSocket 重连后,Executor 收到 WebSocketReconnectedEvent 并调用 verify_pending_orders(),对当前所有 PENDING 的 oid 依次调用 _resolve_via_http(oid)。此时某些订单可能正由 _timeout_then_verify 在同一时刻进行 HTTP 验证或即将调用 _resolve()。两处可能对同一 oid 重复 HTTP 查询并都可能调用 _resolve(oid)。现有实现依赖 _resolve() 的幂等(第二次 get(oid) 为 None 即 return),因此不会“双写”状态,但会产生重复 HTTP 请求,并在高并发下放大“Bug 1”的竞态窗口(例如一个线程刚 HTTP 得到 open 并准备设 TIMEOUT,另一线程 HTTP 得到 filled 并 resolve,顺序不确定)。
修复方向:
- 在
_resolve_via_http或verify_pending_orders中,对“正在被超时线程验证”的 oid 做简单规避(例如用 set 记录“正在 HTTP 验证的 oid”,超时线程和重连补查共用一个锁或标记,避免对同一 oid 并发 HTTP);和/或 - 与 Bug 1 的修复结合:统一“最后一步前再查一次 HTTP”的策略,减少误判 TIMEOUT 的同时也减少重复解析。
Bug 4:重复追踪同一 oid 时旧追踪被 CANCELED,调用方仍可能用旧 tracking 判断(中严重)
位置:websocket_order_manager.py 约 95–111 行;executor.py 约 581–584 行。
现象:track_order(oid, ...) 若发现 _tracking 中已有同一 oid,会把旧 tracking 的 status 置为 CANCELED、cancel 其 _fill_timer 并 result_event.set(),然后放入新 tracking。若之前有调用方仍持有旧的 OrderTracking 引用并在 wait_for_order(old_tracking) 中阻塞,会被正确唤醒,且 old_tracking.status == OrderStatus.CANCELED,所以会返回 False,逻辑正确。
但若调用方在“重复追踪”发生前就拿到了 tracking,随后另一线程(或同一策略的另一次下单)对同一 oid 再次调用 track_order,则旧 tracking 被置为 CANCELED。若此时旧调用方尚未调用 wait_for_order(例如先做别的事再 wait),则会在 wait_for_order 里等到 event 被 set,得到 False。若业务上把“同一 oid 的第二次追踪”视为错误或未考虑,可能误以为“第一次下的单被取消”。
更严重的是:若交易所复用 oid(极少但理论上存在),或业务层误对同一 oid 调用两次 track_order,第二次会覆盖第一次的追踪,第一次的 wait_for_order 会因 CANCELED 而返回 False,可能被解释为“订单取消”而非“被新追踪替代”。
修复方向:
- 在
track_order中,对“重复 oid”打更明确的日志(例如 WARNING 并注明“旧追踪被终止,新追踪已注册”),便于区分“交易所取消”与“重复追踪覆盖”; - 若业务上不允许同一 oid 被重复追踪,可在 Executor 侧保证“同一 oid 只会被当前流程唯一 track”,避免重复调用。
Bug 5:宽限期 5 秒内只依赖 userFills,若 API 不推送或延迟则用挂单价(中低严重)
位置:websocket_order_manager.py 约 246–253 行;_resolve() 中 184–186 行。
现象:orderUpdates 收到 “filled” 后若尚无 userFills,会启动 5 秒定时器,到期后 _resolve(oid),此时若仍无 has_fill_price 则用 _fallback_px / _fallback_sz(来自 order 的 limitPx / origSz)作为成交价和数量。若 userFills 因 API 延迟或漏推而永远不到,则 5 秒后会用挂单价结算,可能和真实成交价不一致,影响 PnL 与统计。
修复方向:宽限期到期时若 not tracking.has_fill_price,在 _resolve() 内(或定时器回调里)先做一次 HTTP query_order_status(oid),若 API 返回 filled 且带 avgPx/totalSz,则用 HTTP 结果覆盖 _fallback 再解析,减少“仅用挂单价”的误用。
3. 其他观察(非严重但建议关注)
- orderUpdates 单条结构:当前代码假定
item.get("status")在顶层、item.get("order")为内层对象(含 oid、limitPx、origSz)。根据 Hyperliquid 文档,该结构正确;若未来 API 变更需同步调整解析逻辑。 - wait_for_order 最大等待:
max_wait = timeout_seconds + _HTTP_VERIFY_BUFFER(60s),不会永久阻塞;超时后返回tracking.status == OrderStatus.FILLED(通常为 False)。 - 重连事件与补查:
WebSocketReconnectedEvent触发后清缓存并调用verify_pending_orders(),逻辑正确;与超时线程的竞态见 Bug 3。
4. 修复优先级建议
| 优先级 | Bug | 建议 |
|---|---|---|
| P0 | Bug 1:超时与 WS 竞态导致误判 TIMEOUT | 在强制 TIMEOUT 前增加一次 HTTP 再确认,再调用 _resolve |
| P1 | Bug 2:订单消息缓冲无界 | 为 _order_msg_buffer 设上限或“不可用时丢弃”策略 |
| P2 | Bug 3:重连补查与超时线程重复/竞态 | 对“正在 HTTP 验证”的 oid 做去重或与 Bug 1 统一二次确认逻辑 |
| P2 | Bug 4:重复 oid 追踪语义 | 明确日志与业务约定,避免重复 track 同一 oid |
| P3 | Bug 5:宽限期后无 userFills 用挂单价 | 宽限期到期时可选 HTTP 回填 avgPx/totalSz 再 resolve |
以上为当前代码与流程下识别出的订单跟踪相关严重问题及修复方向;实现时需保持 _resolve() 幂等与锁约定,避免引入新的竞态。