订单跟踪系统BUG34
订单跟踪系统严重 Bug 分析报告
分析日期:2026-02-22
项目路径:Trading-in-websocket
分析范围:订单跟踪系统(OrderFilledEvent、WebSocket消息处理、并发控制、数据一致性)
概览
发现 7 个严重 Bug 和 3 个设计问题,涉及竞态条件、死锁、内存泄漏、数据一致性等方面。
严重等级汇总
| Bug # | 标题 | 严重等级 | 核心影响 |
|---|---|---|---|
| BUG #1 | Grace Timer 竞态条件 | CRITICAL | 订单二重结算、金额错误 |
| BUG #2 | Timer 内存泄漏 | HIGH | 内存溢出、系统崩溃 |
| BUG #3 | Fill Price 为 0 被视作有效 | CRITICAL | 成交价 0.0 传递至下游 |
| BUG #4 | 监控线程异常被吞掉 | CRITICAL | 订单永久卡在 PENDING |
| BUG #5 | Fill 去重逻辑缺陷 | HIGH | 成交量重复累计 |
| BUG #6 | 撤单后的 TOCTOU 竞态 | HIGH | 成交量/价格数据错误 |
| BUG #7 | WebSocket 锁内做网络 IO | CRITICAL | 消息处理完全停止(死锁) |
CRITICAL 级别 Bug
BUG #1:Grace Timer 竞态条件 — 订单二重结算
文件:src/trading/websocket_order_manager.py
行号:329–350, 370–408
问题描述
当 OrderStatusEvent(orderUpdates)中 status="filled" 先到达,但对应的 OrderFilledEvent(userFills)尚未到达时,代码会启动一个 5 秒的宽限期定时器。
# 第 329–350 行
if status_str == "filled":
tracking._ws_status = OrderStatus.FILLED
tracking._fallback_px = event.price
tracking._fallback_sz = event.size
if tracking.has_fill_price:
should_resolve = True
else:
if tracking._grace_timer is None:
timer = threading.Timer(
self._FILL_GRACE_SEC, self._resolve, # 宽限期 5 秒
[oid, tracking]
)
timer.daemon = True
tracking._grace_timer = timer
timer.start()
竞态场景
- 宽限期定时器启动后,
OrderFilledEvent立即到达 _on_order_filled_event在第 403–408 行设置should_resolve = True,调用_resolve()- 宽限期定时器同时在后台触发
_resolve(oid, tracking) - 两个线程同时调用
_resolve(),导致二重结算
影响
- 成交价被重复累计
- 结算事件被触发两次
- 下游业务收到错误的成交数据
修复建议
在 _resolve() 中增加幂等性保护,或在定时器触发前检查 tracking.status 是否已经完成。
def _on_grace_timeout(self, oid, tracking):
if tracking.status != OrderStatus.PENDING:
return # 已被正常路径结算,跳过
self._resolve(oid, tracking)
BUG #3:Fill Price 为 0 被视作有效 — 成交价 0.0 传递至下游
文件:src/trading/websocket_order_manager.py
行号:257–277
问题描述
订单标记为 FILLED 时,代码会尝试填充价格信息,但逻辑存在缺陷:
# 第 257–277 行
if final_status == OrderStatus.FILLED:
if status is not None: # HTTP/超时模式
if not tracking.has_fill_price:
if px > 0:
tracking.avg_price = px
tracking.filled_size = sz if sz > 0 else tracking.filled_size
# ⚠️ BUG:即使 px=0,has_fill_price 仍被设为 True
if tracking.avg_price <= 0:
tracking.avg_price = tracking._fallback_px
tracking.has_fill_price = True # 第 270 行
else: # WS 模式
if not tracking.has_fill_price:
tracking.avg_price = tracking._fallback_px
tracking.filled_size = tracking._fallback_sz
tracking.has_fill_price = True # 第 277 行
问题路径
当 px=0 且 _fallback_px=0 时:
avg_price保持0.0- 但
has_fill_price被设置为True executor.py第 597–600 行检查has_fill_price,认为已有价格,跳过_backfill_order_price()- 成交价为
0.0的订单被传递给下游业务
# executor.py 第 597–600 行
if not tracking.has_fill_price:
self._backfill_order_price(order_result, coin) # 被错误跳过!
else:
self._verify_fill_completeness(order_result, tracking, coin)
影响
- 成本计算、风险评估等下游模块收到错误的
0.0价格 - 损失 HTTP 查询回填的机会
修复建议
设置 has_fill_price=True 前,校验 avg_price > 0:
if tracking.avg_price > 0:
tracking.has_fill_price = True
# 否则保持 has_fill_price=False,允许后续回填
BUG #4:监控线程异常被吞掉 — 订单永久卡在 PENDING
文件:src/trading/websocket_order_manager.py
行号:427–434
问题描述
虽然代码注释标注"Bug 4 修复",但该修复本身不完整:
def _monitor_order(self, oid: int, tracking: OrderTracking):
try:
self._monitor_order_inner(oid, tracking)
except Exception as e:
logger.error(f"监控线程异常: {tracking.coin} oid={oid} | {e}", exc_info=True)
self._resolve(oid, tracking, OrderStatus.TIMEOUT) # 第 434 行
问题点
- 若
_resolve()自身抛出异常,该异常被外层try/except吞掉,订单状态停留在 PENDING _monitor_order_inner中任何 HTTP 查询异常都会触发 TIMEOUT 标记(语义错误)wait_for_order()因此无限等待,线程永远不会释放
影响
- 订单在 PENDING 状态永久挂起
- 调用
wait_for_order()的线程无限阻塞 - 系统整体停滞
修复建议
def _monitor_order(self, oid: int, tracking: OrderTracking):
try:
self._monitor_order_inner(oid, tracking)
except Exception as e:
logger.error(f"监控线程异常: {tracking.coin} oid={oid} | {e}", exc_info=True)
try:
self._resolve(oid, tracking, OrderStatus.TIMEOUT)
except Exception as resolve_err:
logger.critical(f"_resolve 失败,强制设置结果: oid={oid} | {resolve_err}")
tracking._result.set_exception(resolve_err) # 确保 Future 被释放
BUG #7:WebSocket 订阅锁内做网络 IO — 系统性死锁
文件:src/utils/websocket/enhanced_ws_manager.py
行号:977–988, 653–675
问题描述
add_subscriptions() 方法在持有 subscriptions_lock 期间调用 ws.send()(网络 IO):
# 第 977–988 行
with self.subscriptions_lock: # 持有锁期间...
if sub_key in self.active_subscriptions:
skipped_count += 1
continue
self.subscriptions.append(subscription)
if self.state == ConnectionState.CONNECTED and self._is_connected():
msg = {"method": "subscribe", "subscription": subscription}
self.ws.send(json.dumps(msg)) # ⚠️ 在锁内做网络 IO!
死锁路径
WebSocket 消息线程
└─ _on_message() 被调用
└─ 发布 OrderFilledEvent
└─ executor.py 处理事件
└─ 调用 add_subscriptions()
└─ 等待 subscriptions_lock ← 被阻塞
add_subscriptions() 调用线程
└─ 持有 subscriptions_lock
└─ 调用 ws.send() ← 等待 WebSocket 消息线程
两个线程互相等待,产生系统性死锁,后续所有 WebSocket 消息无法处理。
影响
- WebSocket 消息处理完全停止
- 所有订单事件(OrderFilledEvent、OrderStatusEvent)无法接收
- 系统实质性崩溃
修复建议
将网络 IO 移到锁外执行:
def add_subscriptions(self, subscriptions):
pending_sends = []
with self.subscriptions_lock:
for subscription in subscriptions:
sub_key = ...
if sub_key in self.active_subscriptions:
continue
self.subscriptions.append(subscription)
pending_sends.append(subscription)
# 锁外执行网络 IO
for subscription in pending_sends:
msg = {"method": "subscribe", "subscription": subscription}
self.ws.send(json.dumps(msg))
HIGH 级别 Bug
BUG #2:Grace Timer 内存泄漏
文件:src/trading/websocket_order_manager.py
行号:249–251, 343–350
问题描述
定时器在多个场景下无法被正确清理:
- 场景一:OrderFilledEvent 在定时器触发前 0.1 秒到达,定时器继续在后台运行
- 场景二:
track_order()被重复调用时,旧 tracking 的定时器被 cancel,但新 tracking 的定时器若随后又被替换,则无法被 cancel
# 第 117–118 行:旧 tracking 的定时器被取消
if old._grace_timer:
old._grace_timer.cancel()
# 第 343–350 行:新 tracking 的定时器如果再次被替换,则不会被取消
timer = threading.Timer(self._FILL_GRACE_SEC, self._resolve, [oid, tracking])
timer.daemon = True
tracking._grace_timer = timer
timer.start()
影响
- 高频交易场景下大量定时器积累,导致内存泄漏
- 野生定时器可能访问已被清理的对象
BUG #5:Fill 去重逻辑缺陷 — 成交量重复累计
文件:src/utils/websocket/enhanced_ws_manager.py(1436–1464)
文件:src/trading/websocket_order_manager.py(389–392)
问题描述
去重基于 fill_id,但重复追踪同一订单时,新 tracking 对象的 _fill_ids 为空:
# websocket_order_manager.py 第 110–115 行:重复追踪
old = self._tracking.get(oid)
if old:
logger.warning(f"重复追踪 oid={oid}...")
old.status = OrderStatus.CANCELED
# ⚠️ 旧 tracking 的 _fill_ids 不被继承给新 tracking!
self._tracking[oid] = tracking # 新 tracking._fill_ids 为空集
影响
- WebSocket 重连时,旧 fills 被新 tracking 重新处理
- 成交价和成交量被重复累计,数据严重错误
修复建议
重复追踪时,将旧 tracking 的 _fill_ids 迁移至新 tracking:
if old:
tracking._fill_ids = old._fill_ids.copy() # 继承去重集合
self._tracking[oid] = tracking
BUG #6:撤单后的 TOCTOU 竞态 — 成交量/价格数据错误
文件:src/trading/executor.py
行号:769–804, 916–930
问题描述
撤单后查询订单状态存在时间窗口:
# 第 916–930 行
if result.leg_a.order_id:
self._cancel_order(alt_coin, result.leg_a.order_id) # 步骤 1:撤单
# ⚠️ 此时可能有新 fill 在途
actual_filled, fill_px = self._check_order_after_cancel( # 步骤 2:查询
alt_coin, result.leg_a.order_id, alt_is_buy
)
查询失败时的降级逻辑返回价格 0.0:
# 第 799–804 行
except Exception as e:
logger.warning(f"撤单后订单查询失败,降级到仓位查询: ...")
actual = self._get_actual_position_size(...)
return actual, 0.0 # ⚠️ 返回价格 0.0!
影响
- 撤单和查询之间的新 fill 可能被遗漏或重复计算
- 降级场景下价格为
0.0,触发上层的回填逻辑,但成本已增加
设计问题
问题 D:EventBus 同步分发中的并发修改风险
文件:src/events/event_bus.py
行号:89–107
EventBus 在锁外调用 handler,若 handler 内部触发取消订阅,存在并发修改 _subscribers 列表的风险。虽然复制了列表规避了 ConcurrentModificationException,但逻辑一致性无法保证。
问题 E:微仓位回滚时机过早
文件:src/trading/executor.py
行号:933–947
成交率 < 5% 时触发回滚,但此时 orderUpdates 事件可能尚未全部到达,导致:
- 基于不完整数据做出错误的回滚决策
- 回滚使用市价,与成交价偏差较大,增加实际损耗
问题 F:本地缓存从未被事件更新
文件:src/trading/executor.py
行号:91–100
_cached_positions、_cached_account_value 等缓存字段在 __init__ 中初始化,但未订阅对应的 PositionUpdatedEvent 等事件,导致:
- 缓存始终为初始空值
- 持仓查询返回过期数据,引发错误的风险管理决策
修复优先级
立即处理(生产环境)
- BUG #7 — WebSocket 死锁(
enhanced_ws_manager.py:977):锁内网络 IO 导致系统级死锁 - BUG #1 — Grace Timer 竞态(
websocket_order_manager.py:329):二重结算导致财务数据错误 - BUG #3 — Fill Price 为 0(
websocket_order_manager.py:270):错误成交价传递至下游 - BUG #4 — 超时异常被吞(
websocket_order_manager.py:434):订单永久挂起
短期处理
- BUG #2 — 内存泄漏(
websocket_order_manager.py:343):影响长期运行稳定性 - BUG #5 — Fill 去重缺陷(
enhanced_ws_manager.py:1436):高频交易时触发 - BUG #6 — TOCTOU 竞态(
executor.py:916):边界情况下数据错误
规划处理
- 问题 D — EventBus 并发修改风险
- 问题 E — 微仓位回滚时机
- 问题 F — 缓存一致性