订单跟踪系统BUG32
订单跟踪系统 Bug 分析报告(第二期)
分析日期:2026-02-22
分析范围:src/trading/websocket_order_manager.py、src/trading/position_manager.py、src/trading/trade_repository.py
严重 Bug 清单(按优先级排列)
🔴 P0 致命 Bug
BUG 1: exit_price 每次 save_position() 被重置为 0
文件: src/trading/trade_repository.py
每次调用 save_position() 时,alt_exit_price 和 base_exit_price 都硬编码初始为 0,且 ON CONFLICT DO UPDATE 子句中没有更新这两个字段。平仓后若任何代码再次调用 save_position(),exit_price 会被覆盖为 0,导致 PnL 计算永久错误。
根因:
-- ON CONFLICT 子句缺少以下两个字段的更新
-- alt_exit_price = EXCLUDED.alt_exit_price
-- base_exit_price = EXCLUDED.base_exit_price
影响:平仓收益统计完全失真,exit_price 丢失。
BUG 2: 竞态导致订单状态重复解析
文件: src/trading/websocket_order_manager.py:228-288
_resolve() 在删除 _tracking[oid] 后,grace timer 线程仍可能持有旧的 tracking 引用并尝试二次解析。identity check 虽然有保护,但若 grace timer 在 timer.start() 之前被抢占,整个解析链会出现不可预期的状态跳变,可能导致订单成交被漏记。
竞态场景:
Thread A: _on_order_update() 设置 _ws_status=FILLED,检查 has_fill_price=False
Thread B: _on_user_fill() 接收 fill 数据,调用 _resolve() 成功,弹出 _tracking[oid]
Thread A: timer.start() 执行,2秒后 _resolve() 因 identity check 失败返回 False
结果:订单已从 _tracking 移除,但部分 fill 数据未被累计
影响:订单成交价格错误,持仓入场均价失真。
BUG 3: PnL 计算精度缺失
文件: src/trading/position_manager.py:732-825
_calculate_realized_pnl() 中,若 alt_exit_price <= 0 或 alt_entry_price <= 0,alt_pnl 直接保持为 0.0 而非抛出异常或阻断计算。最终 PnL = 0 + base_pnl - fee,统计数据严重失真。
问题代码:
alt_pnl = 0.0
if alt_exit_price <= 0 or position.alt_entry_price <= 0:
logger.warning(...) # 只记录警告,不阻断
if alt_exit_price > 0 and position.alt_entry_price > 0:
alt_pnl = (alt_exit_price - position.alt_entry_price) * position.alt_size
# alt_pnl 仍为 0.0,但不会报错
影响:realized_pnl 严重低估,财务统计完全错误。
🟠 P1 高危 Bug
BUG 4: Grace Timer 竞态 — 成交价格可能错误
文件: src/trading/websocket_order_manager.py:354-364
竞态场景:
Thread A: 设置 _ws_status=FILLED,检查 has_fill_price=False,准备启动 timer
Thread B: 同时收到 fill 数据,调用 _resolve() 成功,弹出 _tracking[oid]
Thread A: timer.start() 执行,2秒后 _resolve() identity check 失败,timer 未被清理
问题代码:
if tracking._grace_timer is None:
timer = threading.Timer(...)
tracking._grace_timer = timer
timer.start() # ← timer 启动后 tracking 可能已被从 _tracking 移除
影响:grace timer 游离,后续同 oid 的新订单可能被错误关联,成交价格累计错误。
BUG 5: _http_check 中 _http_busy set 可能永久泄露
文件: src/trading/websocket_order_manager.py:522-559
若 _resolve() 内部抛出未预期异常,finally 块虽会执行 _http_busy.discard(oid),但 _resolve() 已经部分修改了状态,tracking 与实际 DB 状态出现不一致。
影响:
- tracking 状态与 DB 不一致
- 若异常路径绕过
finally,oid 永久被标记为 http_busy,无法再触发 HTTP 补查
BUG 6: 成交去重逻辑不完整
文件: src/trading/websocket_order_manager.py:401-412
_fill_key() 依赖 tid 或内容哈希(px + sz + time)。若两个 WebSocket 连接推送完全相同的成交数据,第二笔会被误判为重复并 continue 跳过,不执行 _accumulate_fill()。
问题代码:
fid = _fill_key(fill_data)
if fid in tracking._fill_ids:
continue # ← 跳过,不累计成交
tracking._fill_ids.add(fid)
self._accumulate_fill(tracking, fill_px, fill_sz)
影响:成交累计金额偏低,avg_fill_price 偏差,导致持仓均价计算错误。
BUG 7: 孤儿仓位收纳 — 内存与 DB 修改不原子
文件: src/trading/position_manager.py:882-924
锁内修改 existing.alt_size,锁外才执行 _repo.save_position()。若持久化失败,内存已更新但 DB 未更新,下次重启后数据回滚,仓位大小与实际不符。
问题流程:
with self._lock:
existing.alt_size = actual_size # ← 内存已修改
# 锁已释放,其他线程可能再次修改 existing
if already_exists:
self._repo.save_position(size_sync_pos) # ← 若此处失败,内存与DB不一致
影响:重启恢复时仓位大小错误,可能引发多次重复建仓或错误平仓。
BUG 8: verify_pending_orders 持有过期 tracking 引用
文件: src/trading/websocket_order_manager.py:156-185
with self._lock:
pending = [(oid, t) for oid, t in self._tracking.items() ...]
# 锁释放后,t 可能已被 _resolve() 从 _tracking 中移除
# _http_check() 重新从 _tracking.get(oid) 获取,得到 None 或新对象
# identity check 触发 "stale" 返回
影响:挂起中的订单可能永远不被 resolve,长期处于 PENDING 状态,占用追踪资源。
BUG 9: cancel_all_open_orders 单个失败中断全局清理
文件: src/trading/websocket_order_manager.py:187-204
_cancel_order() 对某个订单失败时,外层 except 直接捕获并退出循环,后续挂单无法继续清理。
问题代码:
for order in open_orders:
self._executor._cancel_order(coin, oid) # ← 若此处异常
# ↓ 以下所有订单均不被处理
except Exception as e:
logger.error(f"启动清理挂单失败: {e}")
# 整个函数退出
影响:系统启动时留有残余挂单,干扰后续策略执行,可能引发意外成交。
BUG 10: close_position 平仓禁用配置热更新竞态
文件: src/trading/position_manager.py:263-293
TOCTOU(Time-of-Check to Time-of-Use)竞态:
1. 检查 is_close_disabled() → False(此时未禁用)
2. 配置被热更新为 close_disabled=True
3. 继续执行,触发实际平仓操作
影响:被禁止平仓的标的意外被平仓,违反风控规则。
🟡 P2 中危 Bug
BUG 11: sync_with_exchange 锁外获取数据 + 锁内修改不一致
文件: src/trading/position_manager.py:1174-1176
exchange_positions 在锁外获取,但 pos.alt_size 在锁内修改,DB 同步在锁外执行。期间 pos.alt_size 可能被其他线程再次更改,导致入库数据陈旧。
影响:仓位同步数据有窗口期,监控面板展示的仓位大小可能与实际不符。
BUG 12: peak_pnl_pct 重复赋值掩盖潜在逻辑错误
文件: src/trading/position_manager.py:1057-1058
_load_position_from_db_row() 内已赋值,外部又覆盖一次。若将来两处取值来源不同,会产生静默的数据不一致。
影响:当前逻辑正确,但代码可读性差,隐藏未来维护风险。
优先修复建议
| 优先级 | Bug | 核心风险 |
|---|---|---|
| 🔴 P0 立即 | BUG 1:exit_price 被覆盖 | PnL 数据永久丢失 |
| 🔴 P0 立即 | BUG 3:PnL 计算为 0 | 财务统计完全错误 |
| 🔴 P0 立即 | BUG 2:竞态重复解析 | 订单成交漏记 |
| 🟠 P1 本周 | BUG 4:Grace Timer 竞态 | 成交价格累计错误 |
| 🟠 P1 本周 | BUG 5:_http_busy 泄露 | 订单永久不被补查 |
| 🟠 P1 本周 | BUG 6:成交去重不完整 | 持仓均价偏差 |
| 🟠 P1 本周 | BUG 7:孤儿仓位不原子 | 重启后仓位数据错误 |
| 🟠 P1 本周 | BUG 8:过期 tracking 引用 | 订单永久 PENDING |
| 🟠 P1 本周 | BUG 9:cancel 异常中断循环 | 启动时残余挂单 |
| 🟠 P1 本周 | BUG 10:平仓配置竞态 | 意外触发平仓 |
| 🟡 P2 计划 | BUG 11:仓位同步窗口期 | 监控数据陈旧 |
| 🟡 P2 计划 | BUG 12:重复赋值 | 代码维护风险 |
修复思路概要
BUG 1 修复
在 ON CONFLICT DO UPDATE 中补充:
alt_exit_price = EXCLUDED.alt_exit_price,
base_exit_price = EXCLUDED.base_exit_price
BUG 2 & 4 修复
在 _resolve() 中增加 grace timer 取消的双重保护,并确保 timer 回调在检查 identity 之前先持有锁。
BUG 3 修复
当 exit_price 或 entry_price 无效时,应抛出异常或返回 None 以阻断 PnL 写入,而非静默置为 0。
BUG 9 修复
将 cancel_all_open_orders 中的异常捕获移入循环内部,确保单个取消失败不中断其他订单的清理。