订单跟踪系统BUG29
订单跟踪系统 Bug 分析报告
分析日期:2026-02-22
分析范围:src/trading/websocket_order_manager.py、src/trading/executor.py、src/trading/position_manager.py
一、架构与数据流简述
订单跟踪核心在 src/trading/websocket_order_manager.py:
- 唯一解析路径:
_resolve()在持锁内将订单从 PENDING 转为终态(FILLED/CANCELED/REJECTED/TIMEOUT),并调用result_event.set()。 - WS 驱动:
_on_order_update(orderUpdates)、_on_user_fill(userFills)在锁内设置tracking._ws_status等,释放锁后调用_resolve()。 - HTTP/超时驱动:监控线程或 shutdown 传入
(oid, tracking, status, px, sz),_resolve内做 identity check(current is tracking)再解析。
track_order(oid) ──────────────────┐
▼
WS "orderUpdates" ──→ _on_order_update ──→ _resolve(oid, tracking)
WS "userFills" ──→ _on_user_fill ──→ _resolve(oid, tracking)
grace timer ──────────────────────→ _resolve(oid, tracking) ← Bug 1/2 修复点
_monitor_order ──→ _http_check ──→ _resolve(oid, tracking, status, px, sz)
shutdown ──────────────────────→ _resolve(oid, tracking, CANCELED)
二、Bug 分类总览
| # | 严重程度 | 状态 | 位置 | 简述 |
|---|---|---|---|---|
| 1 | 🔴 严重 | 工作区已修复,未提交 | websocket_order_manager.py |
WS 解析 TOCTOU 竞态 |
| 2 | 🔴 严重 | 工作区已修复,未提交 | websocket_order_manager.py |
Grace Timer TOCTOU 竞态 |
| 3 | 🟠 严重 | 未修复 | executor.py |
limit_close 超时路径仍用仓位查询而非订单查询 |
| 4 | 🟠 严重 | 未修复 | websocket_order_manager.py |
Grace Timer 在 WS 重连时被反复重置 |
| 5 | 🟡 低 | 已知缺陷 | websocket_order_manager.py |
_fallback_sz=0 时 filled_size 记录丢失 |
| 6 | 🟡 低 | 已知缺陷 | position_manager.py |
PnL 计算未含资金费 |
三、严重 Bug 详细分析
Bug 1 — WS 解析路径的 TOCTOU 竞态(工作区已修复,未提交)
位置: websocket_order_manager.py,_on_order_update(约 386 行)、_on_user_fill(约 430 行)
问题描述
WS 消息处理在 _lock 内设置好 tracking._ws_status 后释放锁,然后仅以 oid 调用 _resolve(oid)。_resolve 内再次按 oid 查表,存在典型的 TOCTOU(time-of-check to time-of-use)竞态窗口。
时间线(有问题的旧代码):
[Thread A - WS] [Thread B - 调用方]
with _lock:
tracking._ws_status = FILLED
has_fill_price = True
should_resolve = True
# 释放锁 ←── 竞态窗口开始
track_order(oid) ← 替换 _tracking[oid]!
old.status = CANCELED
old.result_event.set() ← 旧追踪被误设为 CANCELED
_resolve(oid)
with _lock:
tracking = _tracking.get(oid) ← 拿到的是新 tracking(_ws_status=None)
→ 条件不满足,返回 False ← 新追踪永远等到 TIMEOUT
后果
- 已成交订单被上报为 CANCELED:旧 tracking 的
result_event被track_order替换时设置,调用方得到CANCELED结果。 - 新订单被上报为 TIMEOUT:新 tracking 的
_ws_status=None,WS 路径解析失败,只能等到 600 秒超时才被 HTTP 兜底。 - 两边结果都错:实际已成交的订单,没有一个追踪对象能正确得到 FILLED 终态。
受影响代码(旧版本)
# _on_order_update(旧)
if should_resolve:
self._resolve(oid) # ← 仅传 oid,存在竞态
# _on_user_fill(旧)
if should_resolve:
self._resolve(oid) # ← 同上
修复方案(已在工作区实现)
在锁内保留 tracking 引用,释放锁后传入 _resolve;_resolve 内对 tracking 做 identity check:
# _on_order_update(新)
resolve_tracking = None
with self._lock:
...
if tracking.has_fill_price:
should_resolve = True
resolve_tracking = tracking # ← 保存引用
...
if should_resolve:
self._resolve(oid, resolve_tracking) # ← 传入引用
# _resolve 内(新)
else: # 按引用模式(WS / HTTP / 超时)
current = self._tracking.get(oid)
if current is not tracking: # ← identity check
return False
...
final_status = status if status is not None else tracking._ws_status
Bug 2 — 宽限期 Grace Timer 的 TOCTOU 竞态(工作区已修复,未提交)
位置: websocket_order_manager.py,_on_order_update 中创建 Timer 处(约 367 行)
问题描述
orderUpdates "filled" 到达、has_fill_price=False 时,启动宽限期定时器等待 userFills:
# 旧代码(有问题)
timer = threading.Timer(
self._FILL_GRACE_SEC, self._resolve, [oid] # ← 5 秒后仅传 oid
)
5 秒后定时器触发,_resolve(oid) 同样按 oid 查表,存在与 Bug 1 完全相同的竞态窗口。
后果
若在 5 秒宽限期内同一 oid 被 track_order() 替换:
- 定时器触发时拿到新 tracking(
_ws_status=None),解析条件不满足 - 旧 tracking 已被替换时设为 CANCELED → 已成交订单被误报 CANCELED
- 新 tracking 继续等待直到 600 秒超时 → 新追踪被误报 TIMEOUT
修复方案(已在工作区实现)
# 新代码
timer = threading.Timer(
self._FILL_GRACE_SEC, self._resolve,
[oid, tracking] # ← 传入 tracking 引用,_resolve 内做 identity check
)
Bug 3 — limit_close 超时路径未使用 _check_order_after_cancel(未修复)
位置: executor.py,limit_close() 方法,约 1144 行(Leg A)和 1185 行(Leg B)
问题描述
limit_close 超时处理:下了限价平仓单 → 追踪超时 → 撤单 → 查仓位判断残余:
# limit_close 超时路径(当前代码)—— 存在 TOCTOU
remaining_size = self._get_actual_position_size(
alt_coin, expected_side=position.alt_side, force_refresh=True
)
对比 limit_open 超时路径(已修复为查订单状态):
# limit_open 超时路径(已修复)—— 消除 TOCTOU
actual_filled, fill_px = self._check_order_after_cancel(
alt_coin, result.leg_a.order_id, alt_side
)
# _check_order_after_cancel 内注释:
# "消除 TOCTOU 竞态:撤单后查订单状态(而非仓位),获取可靠的成交量"
竞态窗口
[撤单请求发出]
│
├── 场景 A:交易所先处理 cancel → 后处理 fill
│ 仓位查询 = 原始持仓量(正常,会做市价补单)
│
└── 场景 B:交易所先处理 fill → 后处理 cancel(时间窗口内)
fill 执行后,仓位查询返回 0(误判为已全部平仓!)
↓
result.leg_a.success = True(设置为已成交)
跳过市价补单
↓
实际仓位残留在交易所,DB 标记为 CLOSED
↓
数据不一致 + 资金风险
与 limit_open 的对比
limit_open 超时 |
limit_close 超时 |
|
|---|---|---|
| 撤单后如何判断成交量 | _check_order_after_cancel(oid) → 查订单 |
_get_actual_position_size() → 查仓位 |
| TOCTOU 风险 | 已消除(Bug E/F 修复注释) | 仍存在 |
| 补充说明 | 需要精确 fill 量来计算 Leg B 数量 | 也需要精确量来避免漏平 |
后果
- 持仓残留未平:
limit_close以为平仓成功,跳过市价补单 - DB 与实际不一致:仓位在 DB 中标记为 CLOSED,但交易所仍有头寸
- PnL 计算错误:基于错误的退出假设计算盈亏
- 后续信号被抑制:
has_position()返回 False,策略不会为这对配对生成新信号,但交易所实际有敞口
修复建议
将 limit_close 超时路径改为与 limit_open 一致,使用 _check_order_after_cancel:
# limit_close Leg A 超时(建议修改)
filled_size, fill_px = self._check_order_after_cancel(
alt_coin, result.leg_a.order_id, alt_side
) if result.leg_a.order_id else (0.0, 0.0)
remaining_size = position.alt_size - filled_size # 原始量 - 已成交量
if remaining_size > 0:
# 市价补单
...
else:
# 已全部成交
result.leg_a.success = True
result.leg_a.status = ORDER_STATUS_FILLED
if fill_px > 0:
result.leg_a.price = fill_px
else:
self._backfill_order_price(result.leg_a, alt_coin)
Bug 4 — Grace Timer 在 WS 重连时被反复重置(未修复)
位置: websocket_order_manager.py,_on_order_update 中的 grace timer 创建逻辑
问题描述
当 orderUpdates "filled" 到达且 has_fill_price=False 时,代码会取消旧定时器并启动新的 5 秒倒计时:
if tracking._grace_timer:
tracking._grace_timer.cancel() # 取消旧定时器
timer = threading.Timer(self._FILL_GRACE_SEC, ...)
timer.start() # 重新开始 5 秒倒计时!
WS 重连后,交易所会重推当前订单状态。若订单已是 "filled" 状态,每次重连都会触发上述逻辑,重置 5 秒计时器。
时间线示例
T=0s orderUpdates "filled" → grace timer 启动(T+5s 到期)
T=1s WS 断连重连,交易所推送 "filled" → grace timer 重置(T+6s 到期)
T=3s WS 再次断连重连 → grace timer 重置(T+8s 到期)
T=5s WS 第三次断连重连 → grace timer 重置(T+10s 到期)
...
T=Ns WS 稳定 → grace timer 到期,使用 fallback_px 解析(延迟 N+5s)
后果
- 订单解析严重延迟:网络抖动导致 WS 频繁重连时,本应 5 秒内完成的解析被推迟至重连稳定后
- 开/平仓流程阻塞:
wait_for_order(tracking)无超时地阻塞,直到 grace timer 最终到期 - 策略响应延迟:在订单等待期间,策略层无法进行下一步操作
- 极端情况:若 WS 持续抖动,延迟可达数十秒,直至
_monitor_order的 600 秒总超时触发
修复建议
在重复 "filled" 消息时,若 grace timer 已在运行,跳过重置:
if status_str == "filled":
tracking._ws_status = OrderStatus.FILLED
tracking._fallback_px = ...
tracking._fallback_sz = ...
if tracking.has_fill_price:
should_resolve = True
resolve_tracking = tracking
else:
# 仅在 grace timer 尚未启动时才创建(防止重连重置)
if tracking._grace_timer is None: # ← 新增幂等检查
timer = threading.Timer(
self._FILL_GRACE_SEC, self._resolve,
[oid, tracking]
)
timer.daemon = True
tracking._grace_timer = timer
timer.start()
# 若 timer 已在运行,更新 fallback 价格但不重置定时器
四、已知缺陷(低严重度)
缺陷 5 — _fallback_sz=0 时 filled_size 可能记录为 0
位置: websocket_order_manager.py:357-359
若 orderUpdates "filled" 消息中不含 totalSz 或 origSz,则 _fallback_sz=0。若 5 秒内无 userFills 且 HTTP 补查失败,filled_size 记录为 0。
缓解: _verify_fill_completeness 会 HTTP 补查并覆盖。仅在 HTTP 也失败时影响实际记录。
缺陷 6 — PnL 计算未含资金费
位置: position_manager.py:731(_calculate_realized_pnl)
代码注释已说明:「资金费因无 API 接口获取,暂不扣减」。实际盈亏比系统记录偏高(持仓时间越长,偏差越大)。
五、修复优先级与建议
| 优先级 | Bug | 建议行动 | 原因 |
|---|---|---|---|
| P0 | Bug 1 & 2(TOCTOU) | 立即提交工作区修复 | 已修复,可直接提交 |
| P1 | Bug 3(limit_close 路径) | 本迭代修复 | 资金风险:持仓可能残留未平 |
| P1 | Bug 4(grace timer 重置) | 本迭代修复 | 网络抖动时策略响应严重延迟 |
| P2 | 缺陷 5 & 6 | 监控告警 + 后续迭代 | 影响记录准确性,无直接资金风险 |
六、核心设计原则(修复后应遵循)
- 所有
_resolve调用路径均传入 tracking 引用,依赖 identity check 防止替换竞态 - 撤单后查订单状态(非仓位),消除 fill/cancel 并发的 TOCTOU
- Grace Timer 创建使用幂等逻辑,防止 WS 重连触发重置
_resolve是唯一解析路径,任何设置终态的操作都必须通过它完成