当前系统订单跟踪存在哪些严重的bug
订单跟踪严重 Bug 分析
一、订单跟踪架构简述
sequenceDiagram
participant Exec as Executor
participant WSM as WebSocketOrderManager
participant Kline as RealtimeKlineService
participant WS as WebSocket
Exec->>WSM: track_order(oid, coin, timeout)
WSM->>WSM: _timeout_then_verify(oid) 后台线程
Exec->>WSM: wait_for_order(tracking) 阻塞
WS-->>Kline: on_message(orderUpdates/userFills)
Kline->>Kline: 先 is_duplicate(msg)
Kline->>WSM: handle_message(msg)
WSM->>WSM: _on_order_update / _on_user_fill
Note over WSM: 或超时后 _resolve_via_http(oid)
WSM->>Exec: result_event.set() + 从 _tracking 移除
核心文件:
- 追踪与状态机:
src/trading/websocket_order_manager.py - 消息入口与去重:
src/services/realtime_kline_service_base.py(on_message)、src/utils/message_deduplicator.py - 调用方:
src/trading/executor.py(_track_limit_order、limit_open、limit_close)
二、严重 Bug 列表
1. HTTP 验证覆盖 WS 已成交(竞态)
位置:src/trading/websocket_order_manager.py 中 _resolve_via_http(约 212–239 行)。
现象:超时线程在 time.sleep(timeout_seconds) 后调用 _resolve_via_http(oid)。若此时:
- HTTP 先拿到锁并查到「仍 open」或请求失败 → 解析为
TIMEOUT,然后tracking.status = status、result_event.set()、pop(oid); - 而 WS 的「filled」推送稍晚到达(网络延迟),
则会出现:HTTP 已把结果置为 TIMEOUT 并 pop,WS 再处理时_tracking.get(oid)为 None 被忽略。
结果是:订单实际已成交,但系统认为超时未成交。
影响:
- 限价开仓/平仓会误判「未成交」,触发撤单、市价补单或回滚,可能造成重复成交、错误回滚或仓位与系统状态不一致。
- 与设计意图「HTTP 兜底」相反,在 WS 延迟场景下 HTTP 反而覆盖了正确结果。
根因:在第二次持锁更新前,没有判断「若 WS 已把状态改为 FILLED/CANCELED/REJECTED 则不再用 HTTP 结果覆盖」。当前逻辑只检查 if not tracking,没有检查 tracking.status != PENDING。
2. 超时后未利用 30s 缓冲即唤醒
位置:同上;以及 wait_for_order 使用 timeout=tracking.timeout_seconds + 30(约 71 行)。
现象:wait_for_order 预留了「超时 + 30s」的等待时间,本意是给 HTTP 验证和可能的 WS 延迟留缓冲。但 _timeout_then_verify 在 sleep(timeout_seconds) 后立即调用 _resolve_via_http;一旦 HTTP 返回「open」或失败,立刻 result_event.set(),调用方被唤醒,没有用到这 30s。
影响:WS 若在超时后几秒内才送达「filled」,会被上述 Bug 1 的覆盖问题或「先 pop 再 WS 到达」的组合所掩盖,增加误判为超时的概率。
建议方向:当 HTTP 返回仍为 open/resting 时,不立即 set + pop,而是延后一段时间(例如 30s 内)再决定最终状态并 set + pop,或仅在「仍 PENDING」时更新,避免与 WS 的 FILLED 竞态。
3. orderUpdates 多条目时去重误伤
位置:src/utils/message_deduplicator.py 中 _generate_fingerprint 对 orderUpdates 仅用 data[0](约 105–111 行);src/services/realtime_kline_service_base.py 中先 is_duplicate(msg) 再路由订单(约 607–621 行)。
现象:
- 指纹为
order:{data[0].oid}:{data[0].status},只考虑第一条订单。 - 若同一连接上先后收到两条 orderUpdates:
- 消息 A:
data = [{ oid:1, status:"filled" }] - 消息 B:
data = [{ oid:1, status:"filled" }, { oid:2, status:"filled" }]
则 B 的指纹与 A 相同,整条消息 B 被判定为重复并丢弃,oid=2 的成交永远不会被处理。
- 消息 A:
影响:多单同一批推送时,非首单的订单状态永远不更新,对应 track_order 会一直等到 timeout_seconds+30 才由 HTTP 或超时结束,容易误判为超时或依赖 HTTP 且仍受 Bug 1 影响。
4. orderUpdates 仅用 limitPx/origSz,成交价/量可能不准
位置:src/trading/websocket_order_manager.py 中 _on_order_update 在 status_str == "filled" 时(约 143–145 行)用 order.get("limitPx")、order.get("origSz") 赋给 avg_price、filled_size。
现象:
limitPx/origSz是委托限价和委托量,不是实际成交均价和成交量。- 若
userFills晚于orderUpdates到达,_on_order_update会先 set + pop,后续_on_user_fill里_tracking.get(oid)为 None,无法用 userFills 的px/sz修正。
影响:
- 下游(如 PnL、仓位、配对价值)可能用错误的「成交价/量」,导致统计与风控偏差。
- 若交易所 API 提供真实成交价(如 order 对象内 avgPx/totalSz 或仅在 userFills),当前实现会忽略或无法用上。
5. _on_order_update 在锁外修改 tracking
位置:src/trading/websocket_order_manager.py 约 136–162 行:with self._lock: tracking = self._tracking.get(oid) 后释放锁,再执行 tracking.status = ...、tracking.avg_price = ...、tracking.result_event.set(),最后再 with self._lock: self._tracking.pop(oid, None)。
现象:
- 对
tracking的写发生在锁外,与_resolve_via_http、_timeout_then_verify的读/写存在竞态。 - 若 HTTP 在「WS 已改 status 但尚未 pop」的窗口内再次持锁并更新,仍可能出现状态被覆盖(与 Bug 1 叠加)。
影响:
- 数据竞争、理论上的状态不一致或重复 set/重复 pop(若逻辑再复杂化会更难保证正确性)。
- 建议:在持锁期间完成对
tracking的修改,或至少「先检查 status 再决定是否覆盖」且缩短锁外写范围。
6. "user" channel 被路由但未处理
位置:src/services/realtime_kline_service_base.py 约 612 行:if channel in ("orderUpdates", "user", "userFills") 会把 "user" 也交给 handle_message;src/trading/websocket_order_manager.py 的 handle_message 只处理 orderUpdates 和 userFills,没有对 "user" 的分支。
现象:
"user"消息会被转发到订单管理器但静默忽略。- 若 Hyperliquid 在
"user"中推送订单/成交相关数据,这些更新会丢失。
影响:取决于交易所是否把关键订单状态只放在 "user";若会,则存在漏单/漏状态风险。需根据交易所文档确认并决定是否实现 "user" 或从路由中移除。
三、修复优先级建议
| 优先级 | Bug | 建议 |
|---|---|---|
| P0 | HTTP 覆盖 WS 成交(#1) | 在 _resolve_via_http 第二次持锁后,若 tracking.status != PENDING 则不再用 HTTP 结果覆盖,直接 return。 |
| P0 | 多条目去重误伤(#3) | orderUpdates 指纹按每条 data[i] 的 (oid, status) 分别生成并参与去重,或对 orderUpdates/userFills 不做整包去重(仅对单条更新去重)。 |
| P1 | 30s 缓冲未用(#2) | HTTP 返回 open 时延后 set+pop(如 30s 后再结算),或仅在不覆盖非 PENDING 的前提下再 set+pop。 |
| P1 | 锁外修改 tracking(#5) | 将对 tracking 的写和 pop 放入同一锁内,或先原子地「取引用+判 PENDING+更新+pop」再 set event。 |
| P2 | 成交价/量用 limitPx/origSz(#4) | 优先使用 order 或 userFills 中的实际成交字段(如 avgPx/totalSz/px/sz),并考虑 orderUpdates 与 userFills 到达顺序(例如先不 pop,等 userFills 或短超时后再定稿价格)。 |
| P2 | "user" 未处理(#6) | 查交易所文档,若 "user" 含订单/成交则补充处理或从路由中移除并注明。 |
四、小结
- 最严重:HTTP 验证在 WS 延迟时把「已成交」覆盖成「超时」(#1),以及多订单一条消息时去重导致非首单永远不更新(#3)。
- 建议优先:在
_resolve_via_http中禁止覆盖非 PENDING 状态;修正 orderUpdates 的指纹策略或对订单类 channel 放宽/分条去重。 - 在此基础上再处理 30s 缓冲利用、锁粒度与成交价/量来源,可显著提升订单跟踪的可靠性与一致性。