订单跟踪系统bug分析24
订单跟踪系统严重 Bug 分析报告
分析日期: 2026-02-21
分析范围:websocket_order_manager.py,executor.py,position_manager.py,realtime_kline_service_base.py
Bug 1 [严重] userFills 重复累计 → 成交价/数量错误
位置: websocket_order_manager.py:559-569 _accumulate_fill
问题: _accumulate_fill 没有任何 fill 级别的去重机制。它简单地将每笔 fill 的 px * sz 累加计算加权平均价。在以下场景会出错:
- WS 重连后交易所重播消息:如果
MessageDeduplicator的 TTL 已过期,同一笔 fill 会被重复计入 - 缓冲区回放时序问题:
_order_msg_buffer中的userFills消息在回放时,如果新的 WS 连接同时发来了相同的消息
后果: filled_size 膨胀(如实际成交 1.0 变成 2.0),avg_price 虽然可能不变(同价),但后续 position_manager 使用错误的 filled_size 会导致仓位记录不准确,PnL 计算出错。
# 当前代码 - 无 fill 去重
@staticmethod
def _accumulate_fill(tracking, fill_px, fill_sz):
old_sz = tracking.filled_size
new_sz = old_sz + fill_sz # 重复 fill 直接累加!
...
建议: 在 OrderTracking 中增加已处理的 fill ID 集合(cloid 或 tid),避免重复累计。
Bug 2 [严重] WS 重连期间丢失的 userFills 导致成交价不完整
位置: websocket_order_manager.py:356-388 + executor.py:584-592
问题: 订单可能有多笔部分成交(如 3 笔 partial fills),WS 重连期间可能只收到其中 1-2 笔。_accumulate_fill 基于收到的 fills 计算加权平均价,但不验证累计量是否等于实际总成交量。
时间线:
- 订单在交易所分 3 笔成交($100 × 0.5 + $101 × 0.3 + $99 × 0.2)
- WS 收到第 1 笔 fill → avg_price = $100, size = 0.5
- WS 断连
- 第 2、3 笔 fill 丢失
- WS 重连,
orderUpdates"filled" 到达 _finish被调用:has_fill_price = True(因为收到了第 1 笔),使用不完整的 avg_price
后果: _track_limit_order 中 tracking.has_fill_price = True → 跳过 HTTP _backfill_order_price → 最终使用的成交价只基于部分 fills,PnL 计算不准确。
# executor.py:591 - has_fill_price=True 时跳过 HTTP 回填
if not tracking.has_fill_price:
self._backfill_order_price(order_result, coin) # 被跳过!
建议: 在 _finish/_finish_direct 解析时比较 tracking.filled_size 与 _fallback_sz(来自 orderUpdates 的 totalSz),如果差异超过阈值,强制触发 HTTP 回填。
Bug 3 [严重] 限价单超时撤单的 TOCTOU 竞态窗口
位置: executor.py:819-851 (limit_open 超时处理)
问题: 超时 → 撤单 → 查询持仓,这三步之间存在时间窗口:
t0: track_order 返回 TIMEOUT
t1: _cancel_order(oid) ← 撤单请求发出
t2: 交易所处理撤单(可能失败)
t3: _get_actual_position_size() ← 查询持仓
...
t4: 交易所在 t3 之后才完成订单匹配 ← 竞态!
场景: 撤单和查询之间,订单在交易所端刚好成交。_get_actual_position_size 返回 0(撤单前的缓存或查询时延),代码判断"无成交"返回失败。但实际上订单已成交,产生了一个无人管理的孤儿仓位。
缓解因素: sync_with_exchange 定期扫描会最终发现孤儿仓位,但有时间窗口(取决于同步频率),这段时间仓位无止损保护。
建议: 撤单后增加短暂延迟(如 500ms),或增加二次确认查询。
Bug 4 [高] 部分成交无最小阈值 → 微型仓位
位置: executor.py:830-843
问题: 限价单超时后,接受任意数量的部分成交,注释明确写着"无成交率限制":
if actual_filled > 0:
# 🆕 有部分成交 - 无条件接受
result.leg_a.success = True
如果只成交了 0.01%(如 $100 仓位只成交了 $0.01),Leg B 会按比例计算出同样微小的数量。最终创建的配对仓位:
- 名义价值极小,手续费占比极高
- 可能低于交易所最小订单量限制(
round_size截断为 0 → Leg B 失败 → 回滚) - 即使不回滚,止损/平仓操作也可能因金额过小而失败
建议: 增加最小成交率阈值(如 10%),低于阈值视为未成交。
Bug 5 [高] _order_msg_buffer 无大小限制 → 内存泄漏
位置: realtime_kline_service_base.py:193 + 640
问题:
self._order_msg_buffer: deque = deque() # 无 maxlen!
如果 _get_ws_order_manager() 始终返回 None(交易模块初始化失败),每条 orderUpdates/userFills 消息都会被追加到缓冲区,永不释放。代码在 100/1000 条时输出警告/错误日志,但不做截断或丢弃。
长期运行下,这会导致内存持续增长。
建议: 使用 deque(maxlen=1000) 或在超过阈值时主动清理。
Bug 6 [中] _finish 中 fallback 价格可能为 0
位置: websocket_order_manager.py:225-230
问题: 当 orderUpdates 的 avgPx 和 limitPx 都为空/0 时,_fallback_px 为 0。如果此时 userFills 也未到达(has_fill_price = False),订单会以 avg_price = 0 完成解析:
if tracking.status == OrderStatus.FILLED and not tracking.has_fill_price:
tracking.avg_price = tracking._fallback_px # 可能为 0!
tracking.filled_size = tracking._fallback_sz # 可能为 0!
随后 _track_limit_order 检查:
if tracking.avg_price > 0:
order_result.price = tracking.avg_price # 跳过!
order_result.price 保持为限价(_place_limit_order 中设置的)。但如果实际成交价与限价偏差较大,PnL 计算会不准确。
Bug 7 [中] verify_pending_orders 对同一 oid 的并发 HTTP 查询
位置: websocket_order_manager.py:149-184 + executor.py:1631-1641
问题: _on_websocket_reconnected 启动双轮验证线程:
def _double_verify():
self._ws_order_manager.verify_pending_orders()
time.sleep(5)
self._ws_order_manager.verify_pending_orders()
同时,每个订单的 _monitor_order 线程也可能在执行 _http_check。虽然 _http_busy set 提供了并发保护,但 verify_pending_orders 遍历所有 pending 订单并逐个调用 _http_check,每次间隔 1s。如果某个 oid 在第一轮验证时返回 "busy"(monitor 线程正在查询),它会加入 retry_list。3s 后重试时 monitor 线程可能已完成查询并解析了该订单,此时 _http_check 返回 "resolved"。这不会造成错误,但增加了不必要的 HTTP 调用压力。
已修复的 Bug(本次 diff 中)
| Bug | 位置 | 问题 | 修复 |
|---|---|---|---|
_finish_direct 覆盖 WS 真实价格 |
websocket_order_manager.py:258 |
HTTP px 无条件覆盖 userFills 价格 | 增加 if not tracking.has_fill_price 检查 |
_get_actual_position_size 用缓存数据 |
executor.py:665 |
撤单后用缓存仓位量(可能过期) | 增加 force_refresh 参数 |
_close_ops 类型注解不匹配 |
position_manager.py:1134 |
7 元素注解,实际 8 元素 | 修正为 8 元素 |
closed_pairs 重复添加 |
position_manager.py:1263 |
base_leg_lost 平仓后重复追加 | 增加 if lost_key not in closed_pairs |
_order_msg_buffer 迭代竞态 |
realtime_kline_service_base.py:621 |
while buffer + popleft 非原子 |
改为 try/except IndexError |
leg_b_error 引用不存在的属性 |
executor.py:948 |
result.leg_b_error 不存在 → AttributeError |
先保存 leg_b_error 再置 None |
优先修复建议
- [紧急] Bug 2 - 增加
filled_size与totalSz的一致性校验,不一致时强制 HTTP 回填 - [紧急] Bug 1 - 在
_accumulate_fill中增加 fill 级别去重(按 trade ID) - [高] Bug 4 - 增加最小成交率阈值
- [高] Bug 5 - 给
_order_msg_buffer加上maxlen - [中] Bug 3 - 撤单后增加延迟或二次确认