当前系统订单跟踪存在哪些严重的bug9
订单跟踪严重 Bug 分析
一、订单跟踪数据流概览
sequenceDiagram
participant Exec as Executor
participant WSM as WebSocketOrderManager
participant WS as K线服务 on_message
participant API as 交易所 HTTP/WS
Exec->>WSM: track_order(oid, coin, timeout)
WSM->>WSM: _tracking[oid]=tracking, 启动 _timeout_then_verify 线程
Exec->>WSM: wait_for_order(tracking) 阻塞
Note over WS,API: 订单消息来自同一 WebSocket
API-->>WS: orderUpdates / userFills
WS->>WSM: handle_message(msg)
WSM->>WSM: _on_order_update 或 _on_user_fill
WSM->>WSM: 更新 tracking,_tracking.pop(oid),result_event.set()
WSM->>Exec: wait 返回,Executor 根据 status 继续
- 状态来源:
orderUpdates决定终态(filled/canceled/rejected),userFills提供真实成交价(加权累计)。 - 唤醒:只有
_on_order_update收到终态或_resolve_via_http查询到终态时才会result_event.set();超时线程在timeout_seconds后做一次 HTTP 兜底。
涉及核心文件:
- src/trading/websocket_order_manager.py — 追踪表、成交价缓存、超时与 HTTP 结算
- src/trading/executor.py —
_track_limit_order、query_order_status - src/services/realtime_kline_service_base.py — 订单消息路由与缓冲
二、严重 Bug 列表
1. 订单消息缓冲区溢出导致订单更新永久丢失(高)
位置:realtime_kline_service_base.py 第 193、616–626 行。
现象:当 _get_ws_order_manager() 返回 None 时(例如 orchestrator/executor 未就绪或异常),orderUpdates/userFills 被放入 _order_msg_buffer = deque(maxlen=50)。缓冲区满后,新消息会挤掉最旧的一条(deque 的默认行为),被挤掉的消息不会在任何地方再被处理。
后果:
- 若被丢的是某订单的
orderUpdates(如filled),该订单在_tracking中会一直保持 PENDING,直到超时线程做 HTTP 查询;若 HTTP 也失败或尚未执行,会误判为 TIMEOUT。 - 若被丢的是
userFills,真实成交价可能永远无法写入,只能退化为limitPx/origSz,影响风控与对账。
触发条件:在订单管理器可用之前,同一连接上已收到超过 50 条订单相关消息(例如重连后历史回放 + 新推送叠加,或启动顺序异常导致 manager 迟迟未就绪)。
2. 成交价缓存驱逐导致错误成交价或真实价丢失(高)
位置:websocket_order_manager.py 第 352–368 行 _cache_fill。
现象:_fill_prices 按“先入先出”保留最多 _MAX_FILL_CACHE(500)条。超出时用 del self._fill_prices[next(iter(self._fill_prices))] 删除插入顺序最早的一条。驱逐策略与“是否仍被追踪”无关,因此可能删掉当前仍在 _tracking 中、尚未收到 orderUpdates filled 的订单的 userFills。
后果:
- 当该订单后来收到
orderUpdates的filled时,_on_order_update里fill = self._fill_prices.pop(oid, None)得到None,会退化为limitPx/origSz,成交价/成交量不准确。 - 若
orderUpdates迟迟不到、仅依赖wait_for_order内轮询_fill_prices取价,被驱逐后轮询永远拿不到该 oid 的 fill,真实成交价永久丢失。
触发条件:短时间内大量不同 oid 的 userFills(例如多腿、多币种或高频)导致缓存超过 500 条,且部分订单尚未收到 orderUpdates 终态。
3. 同一 oid 重复 track 导致前一次 wait 永久阻塞(中高)
位置:websocket_order_manager.py 第 88–98 行 track_order。
现象:对同一 oid 再次调用 track_order 时,会直接 self._tracking[oid] = tracking 覆盖旧条目。旧的 OrderTracking 不再在 _tracking 中,因此:
- 之后所有
_on_order_update/_on_user_fill/_resolve_via_http都只更新新的 tracking; - 旧 tracking 的
result_event永远不会被 set。
后果:若有线程正在 wait_for_order(old_tracking),会永久阻塞。正常单笔限价流一般不会对同一 oid 重复 track,但在重试、重复下单或上层逻辑错误时可能发生。
4. 仅靠 orderUpdates 终态唤醒,WS 丢包或延迟会误判超时(中)
位置:整体设计——result_event.set() 仅在 websocket_order_manager.py 的 _on_order_update(终态)或 _resolve_via_http 中调用;_on_user_fill 从不 set event。
现象:若交易所只推了 userFills(或用户已实际全成),但 orderUpdates 的 filled 因网络/交易所延迟或丢包未到达,则:
- 超时前没有任何路径会 set
result_event; - 超时线程触发后依赖 HTTP 查询;若 HTTP 失败或超时,会将该订单标为 TIMEOUT 并 set event,即使交易所上已成交。
后果:业务层会认为“限价单超时”,可能触发撤单、补单或风控逻辑,与真实成交状态不一致。
5. wait_for_order 在锁外读取 tracking 与 fill 轮询的竞态(低~中)
位置:websocket_order_manager.py 第 100–120 行 wait_for_order。
现象:设计上“所有 tracking 状态变更在 _lock 内完成”,但 wait_for_order 在 result_event.wait() 返回后:
- 读取
tracking.status、tracking.has_fill_price未持锁; - 随后在轮询中持锁只对
_fill_prices.pop(tracking.oid, None)。
在 CPython 下简单属性读通常安全,但若与 bug 2(fill 被驱逐)叠加:_on_order_update 已把 tracking 置为 FILLED 并 pop 掉 fill,或 fill 已被驱逐,则这里可能拿到错误的 avg_price/filled_size 或拿不到真实价。
后果:主要放大 bug 2 的影响(错误成交价被上层使用),单独看为理论上的可见性/一致性风险。
三、修复建议摘要
| Bug | 建议方向 |
|---|---|
| 1 缓冲区溢出 | 增大 maxlen 或改为按 oid 去重/按订单维度的有界队列;manager 未就绪时考虑拒绝新下单或明确降级策略,避免静默丢消息。 |
| 2 成交价驱逐 | 驱逐时跳过仍在 _tracking 中的 oid;或按 oid 为键、仅对“已从 _tracking 移除”的 oid 做 LRU/条数上限。 |
| 3 重复 track | 同一 oid 已存在时:要么返回现有 tracking 并避免覆盖,要么先结束旧 tracking(如置为 CANCELED/REJECTED 并 set event)再写入新 tracking。 |
| 4 单一唤醒 | 在 userFills 累计达到订单规模(若能从上下文或 API 获知)或收到明确“全成”语义时,也可 set result_event;和/或加强超时路径下 HTTP 重试与日志,减少误判 TIMEOUT。 |
| 5 锁外读 | 在 wait_for_order 中,对 status/has_fill_price 的读取和后续 fill 轮询统一在一段持锁区域内完成,或复制出所需字段再释放锁,避免与驱逐/更新的竞态。 |
四、结论
当前订单跟踪最严重的问题集中在:消息在缓冲区溢出时被静默丢弃、成交价缓存驱逐与“是否仍被追踪”脱节、以及同一 oid 重复追踪导致旧 wait 永久阻塞。建议优先修复 1、2、3,再结合业务需求处理 4 和 5,并补充针对“订单管理器未就绪”和“大量 userFills”的单元/集成测试与监控。