订单跟踪系统BUG36
订单跟踪系统严重 Bug 因果链分析报告
分析日期:2026-02-23
项目路径:Trading-in-websocket
分析范围:订单跟踪系统(WebSocket 事件、并发控制、数据一致性)
因果链格式:输入 → 状态变化 → 调用路径 → 出错点 → 根因
概览
发现 5 个 CRITICAL/P0 级 + 3 个 HIGH 级 Bug,分布于 EventBus 隔离、死锁、竞态、数据错误等方面。
严重等级汇总
| Bug # | 标题 | 严重等级 | 核心后果 |
|---|---|---|---|
| B1 | 双 EventBus 隔离 | P0 / 静默失效 | WS 所有订单/成交事件从未被消费 |
| B2 | WebSocket 锁内网络 IO | CRITICAL / 死锁 | 消息处理线程完全停止 |
| B3 | Grace Timer 竞态 | CRITICAL / 二重结算 | 成交价错误 |
| B4 | Fill Price 为 0 被视作有效 | CRITICAL / 数据错误 | 0.0 价格传递至下游 |
| B5 | 监控线程异常被吞 | CRITICAL / 永久阻塞 | wait_for_order() 无限等待 |
| B6 | Grace Timer 内存泄漏 | HIGH | 长期运行内存溢出 |
| B7 | Fill 去重逻辑缺陷 | HIGH | 成交量重复累计 |
| B8 | 撤单后 TOCTOU 竞态 | HIGH | 成交量/价格数据错误 |
CRITICAL 级别 Bug
B1:双 EventBus 隔离 — WS 订单事件从未被消费(P0)
文件:src/utils/websocket/enhanced_ws_manager.py L306、src/trading/executor.py L89/L146
完整因果链
输入:
交易所 WebSocket 推送 orderUpdates / userFills 消息
预期状态变化路径:
WS 消息到达 → 解析 → 发布 OrderStatusEvent / OrderFilledEvent
→ WebSocketOrderManager 接收 → 更新 tracking._ws_status / 累计 fill
→ _resolve() → tracking.result_event.set()
实际调用路径(错误):
1. EnhancedWebSocketManager.__init__()
└─ self._event_bus = EventBus() # 内部新建实例 A
2. Executor.__init__()
└─ self._event_bus = EventBus() # 独立实例 B
└─ WebSocketOrderManager(event_bus=self._event_bus) # 订阅实例 B
3. WS 消息到达
└─ ws_trading_manager._cache_latest_data()
└─ _publish_order_status_events()
└─ self._event_bus.publish(OrderStatusEvent) # 发布到实例 A
4. WebSocketOrderManager._on_order_status_event()
└─ 订阅在实例 B,永远等不到实例 A 的事件
出错点:
enhanced_ws_manager.py L306:
self._event_bus = EventBus() # 每个 WS 实例固定创建新总线,无法注入
根因:
EnhancedWebSocketManager 不支持 event_bus 参数注入。
发布端(trading WS)与消费端(WebSocketOrderManager)使用两个
不同的 EventBus 实例,事件无法跨总线传递,WS 订单追踪路径
静默失效,系统退化为纯 HTTP 兜底(2s 早期检查 + 600s 超时)。
影响范围
orderUpdates→OrderStatusEvent:从未到达WebSocketOrderManageruserFills→OrderFilledEvent:从未到达WebSocketOrderManager- 5s 宽限期逻辑:永不触发
- 成交均价累计:永不执行(只能用 HTTP 兜底价)
PositionUpdatedEvent/BalanceChangedEvent:同样孤立
修复建议
为 EnhancedWebSocketManager 增加可选参数 event_bus: EventBus | None = None;在创建交易 WS 时传入 executor 的 _event_bus:
# enhanced_ws_manager.py
def __init__(self, ..., event_bus: EventBus | None = None):
self._event_bus = event_bus if event_bus is not None else EventBus()
# realtime_kline_service_base.py(创建 trading WS 处)
self.ws_trading_manager = EnhancedWebSocketManager(
...,
event_bus=self.executor._event_bus # 注入同一总线
)
B2:WebSocket 锁内做网络 IO — 系统性死锁(CRITICAL)
文件:src/utils/websocket/enhanced_ws_manager.py L977–988, L653–675
完整因果链
输入:
任意触发 add_subscriptions() 的操作(首次连接、重连、动态订阅)
同时 WebSocket 消息线程正在处理事件
状态变化:
系统进入两线程互相等待的死锁状态,所有 WS 消息处理停止
调用路径(死锁形成):
线程 A(业务线程):
add_subscriptions()
└─ with self.subscriptions_lock: # 持有锁 L
└─ self.ws.send(json.dumps(msg)) # 等待 WS 消息线程 ↓
线程 B(WebSocket 消息线程):
_on_message(raw_msg)
└─ _cache_latest_data()
└─ 发布 OrderFilledEvent
└─ executor 处理事件
└─ 调用 add_subscriptions()
└─ with self.subscriptions_lock: # 等待锁 L ↑
→ 永远等不到(被线程 A 持有)
出错点:
enhanced_ws_manager.py L977–988:
with self.subscriptions_lock:
...
self.ws.send(json.dumps(msg)) # 锁内做阻塞网络 IO
根因:
锁的粒度过大,"修改订阅列表"(内存操作,应在锁内)和
"发送订阅消息"(网络 IO,不应在锁内)混在同一个临界区。
WebSocket 消息线程在等待锁时无法处理任何消息,
而持锁方需要 WS 消息线程完成 send,形成经典死锁。
影响
- WebSocket 消息处理完全停止,无任何报错(静默卡死)
- 所有
OrderStatusEvent/OrderFilledEvent无法接收 - 系统实质性崩溃
修复建议
将网络 IO 移至锁外:
def add_subscriptions(self, subscriptions):
pending_sends = []
with self.subscriptions_lock: # 锁内只做内存操作
for subscription in subscriptions:
if sub_key in self.active_subscriptions:
continue
self.subscriptions.append(subscription)
pending_sends.append(subscription)
for subscription in pending_sends: # 锁外做网络 IO
msg = {"method": "subscribe", "subscription": subscription}
self.ws.send(json.dumps(msg))
B3:Grace Timer 竞态 — 订单二重结算(CRITICAL)
文件:src/trading/websocket_order_manager.py L329–350, L370–408, L223
完整因果链
输入:
一笔限价单成交,orderUpdates 先到,userFills 几乎同时到(< 5ms)
状态变化(出错):
T+0ms: OrderStatusEvent(status="filled") 到达
→ tracking._ws_status = FILLED
→ tracking.has_fill_price == False
→ 启动 5s Grace Timer(timer 线程 A)
T+50ms: OrderFilledEvent(fill_id="tid:xxx") 到达(线程 B)
→ _fill_ids.add("tid:xxx")
→ _accumulate_fill(tracking, px, qty) ← 第 1 次累计
→ tracking.has_fill_price = True
→ tracking._ws_status != None → should_resolve = True
→ 调用 _resolve(oid, tracking) ← 线程 B 执行
T+5000ms: Grace Timer 触发(线程 A)
→ 调用 _resolve(oid, tracking) ← 线程 A 执行
→ _resolve() 内部无幂等保护,二次进入成功
调用路径:
_on_order_status_event() [L329–350]
└─ timer = threading.Timer(5s, self._resolve, [oid, tracking])
└─ timer.start() ← timer 线程 A 启动
_on_order_filled_event() [L370–408] ← 与 timer 并行运行
└─ _accumulate_fill()
└─ _resolve(oid, tracking) ← 线程 B 同时调用
出错点:
websocket_order_manager.py L329–350 启动 timer
websocket_order_manager.py L403–408 并发调用 _resolve()
_resolve() 内部缺少幂等性检查(未检查 status != PENDING 时提前返回)
根因:
_resolve() 不是幂等的。虽有 tracking identity check(验证 tracking
是否被替换),但未检查 tracking.status 是否已完成。两个线程同时
通过 identity check,都执行状态写入和 fill 累计,导致 avg_price /
filled_size 被重复计算。
影响
- 成交均价错误(被重复累加)
- 下游仓位 PnL 计算基于错误数据
- 结算事件触发两次
修复建议
在 Grace Timer 的回调中增加状态检查:
def _on_grace_timeout(self, oid: int, tracking: OrderTracking):
with self._lock:
if tracking.status != OrderStatus.PENDING:
return # 已被正常路径结算,跳过
self._resolve(oid, tracking)
B4:Fill Price 为 0 被视作有效 — 成交价 0.0 传递至下游(CRITICAL)
文件:src/trading/websocket_order_manager.py L257–277、src/trading/executor.py L597–600
完整因果链
输入:
一笔订单通过 HTTP 兜底路径结算(B1 导致 WS 路径失效后的常态)
HTTP 查询超时或返回 avgPx=0,fallback_px 同样为 0
状态变化(错误路径):
_resolve(oid, tracking, status=FILLED, px=0.0, sz=X)
└─ 进入 HTTP/超时模式(status is not None)
└─ tracking.has_fill_price == False
└─ if px > 0: # False,跳过 avg_price 赋值
└─ if tracking.avg_price <= 0:
tracking.avg_price = tracking._fallback_px # fallback 也是 0
└─ tracking.has_fill_price = True # ← 无条件设为 True(BUG 位置 L270)
调用路径:
_resolve() [websocket_order_manager.py L257–277]
└─ tracking.has_fill_price = True # L270,不检查 avg_price > 0
executor.py [L597–600]
└─ if not tracking.has_fill_price:
self._backfill_order_price(...) # 被错误跳过!
else:
self._verify_fill_completeness(...) # 基于 0.0 价格执行
出错点:
websocket_order_manager.py L270:
tracking.has_fill_price = True # 无条件,不检查 avg_price > 0
根因:
has_fill_price 的语义应为"已有有效非零成交价",但实现为
"进入过成交价赋值分支"。当 px=0 且 fallback_px=0 时,
avg_price 仍为 0.0,但 has_fill_price 被标记为 True,
阻止了下游 HTTP 回填(_backfill_order_price)。
触发条件(与 B1 叠加必然触发):
B1 导致 WS 路径失效 → 退化为 HTTP 兜底
HTTP 查询超时 → px=0, fallback_px=0
→ has_fill_price=True 但 avg_price=0.0
→ executor 计算仓位成本 = 0.0
影响
- 仓位开仓成本记录为 0.0
- PnL 计算严重偏差(虚假盈利)
- 风险管理决策基于错误数据
修复建议
设置 has_fill_price=True 前,校验 avg_price > 0:
# websocket_order_manager.py L270 附近
if tracking.avg_price > 0:
tracking.has_fill_price = True
# 否则保持 has_fill_price=False,允许后续 HTTP 回填
B5:监控线程异常被吞 — 订单永久卡在 PENDING(CRITICAL)
文件:src/trading/websocket_order_manager.py L427–434
完整因果链
输入:
_monitor_order_inner() 执行期间发生任何未预期异常
(HTTP 超时、网络错误、数据解析失败等)
状态变化:
tracking.status 停留在 PENDING
tracking.result_event 永不被 set()
调用 wait_for_order() 的线程永久阻塞
调用路径(当前"修复"后仍存在的缺陷):
_monitor_order() [L427–434]
try:
_monitor_order_inner(oid, tracking)
except Exception as e:
logger.error(...)
self._resolve(oid, tracking, OrderStatus.TIMEOUT) # 若此处也抛异常
当 _resolve() 自身异常时:
└─ 异常被外层 try/except Exception 吞掉
└─ tracking.result_event.set() 从未被调用
└─ wait_for_order() 无限阻塞
出错点:
websocket_order_manager.py L434:
self._resolve(oid, tracking, OrderStatus.TIMEOUT)
# 无嵌套 try/except,_resolve 异常直接传播到外层被吞
根因:
错误恢复路径(exception handler 内的 _resolve 调用)本身没有异常保护。
若 _resolve() 因任何原因抛出异常,该异常被外层 except 捕获并记录,
但 result_event 永不被释放,导致上层调用永久阻塞。
影响
- 订单追踪线程永久挂起(未释放)
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 失败,强制释放 result_event: oid={oid} | {resolve_err}"
)
tracking.result_event.set() # 确保 wait_for_order() 不永久阻塞
HIGH 级别 Bug
B6:Grace Timer 内存泄漏(HIGH)
文件:src/trading/websocket_order_manager.py L249–251, L343–350
完整因果链
输入:
高频交易 / WebSocket 重连导致同一 oid 被多次 track_order()
状态变化:
Timer 对象积累,GC 无法及时回收(daemon 线程持有引用)
调用路径:
track_order(oid, ...) 第 1 次
└─ timer_1 = threading.Timer(5s, _resolve, [oid, tracking_1])
└─ tracking_1._grace_timer = timer_1
└─ timer_1.start()
track_order(oid, ...) 第 2 次(重连/重试)
└─ old._grace_timer.cancel() ← timer_1 被取消(标志位设置)
└─ _tracking[oid] = tracking_2
└─ timer_2 启动
timer_1 虽被 cancel(),其底层线程仍需等待超时才真正结束。
高频场景下,大量 cancel-but-not-joined 的定时器线程积累。
出错点:
websocket_order_manager.py L343–350:
timer = threading.Timer(...)
timer.start() # cancel() 只设置标志位,不中断线程
根因:
Python threading.Timer.cancel() 不终止线程,只设置标志位令回调不执行。
线程本身仍在 sleep,持有对象引用直到超时到期。高频交易时积累大量
后台 Timer 线程,导致内存和线程描述符泄漏。
修复建议
将宽限期逻辑改为使用 threading.Event 控制的单后台线程,或使用 concurrent.futures 的延迟任务,可被真正取消。
B7:Fill 去重逻辑缺陷 — 成交量重复累计(HIGH)
文件:src/trading/websocket_order_manager.py L110–115、src/utils/websocket/enhanced_ws_manager.py L1436–1464
完整因果链
输入:
WebSocket 重连后,交易所重新推送历史 userFills 事件
或同一 oid 被多次 track_order()(重连后的常见场景)
状态变化:
新 tracking 对象的 _fill_ids 为空集
历史 fill 被重新处理,累计至新 tracking 的 avg_price / filled_size
调用路径:
WS 重连
└─ verify_pending_orders()
└─ track_order(oid, ...) ← 创建 tracking_new,_fill_ids={}
WS 重新推送历史 fills
└─ _on_order_filled_event(event)
└─ fill_id = "tid:12345"
└─ if fill_id in tracking_new._fill_ids: # False(空集)
return
└─ tracking_new._fill_ids.add("tid:12345")
└─ _accumulate_fill(...) ← 重复累计!
出错点:
websocket_order_manager.py L110–115:
self._tracking[oid] = tracking # 新 tracking._fill_ids 为空集
# 旧 tracking 的 _fill_ids 未被继承
根因:
track_order 的重复追踪逻辑仅 cancel 了旧 timer,
未迁移 _fill_ids 去重集合。重连后旧 fill 被重新投递,
新 tracking 将其视为首次到达,导致均价和成交量双重错误。
修复建议
# websocket_order_manager.py L110–115 附近
if old:
tracking._fill_ids = old._fill_ids.copy() # 继承去重集合
self._tracking[oid] = tracking
B8:撤单后 TOCTOU 竞态 — 成交量/价格数据错误(HIGH)
文件:src/trading/executor.py L769–804, L916–930
完整因果链
输入:
部分成交后触发撤单(限价单部分成交后超时撤单)
状态变化:
撤单请求发出 → 在途 fill 落点在"撤单"和"查询"之间 → fill 被遗漏
或查询失败 → 降级返回 price=0.0
调用路径:
executor.py L916–930
self._cancel_order(alt_coin, order_id) # 步骤 1:撤单
# ← 此时间窗口(约 10–50ms)内新 fill 可能到达
actual_filled, fill_px = self._check_order_after_cancel(
alt_coin, order_id, alt_is_buy # 步骤 2:查询
)
_check_order_after_cancel() L769–804
except Exception:
actual = self._get_actual_position_size(...)
return actual, 0.0 # ← 降级返回 0.0
出错点:
executor.py L799–804:
return actual, 0.0 # 降级路径返回价格 0.0
根因:
撤单(HTTP)和查询(HTTP)是两次独立的网络请求,之间存在时间窗口。
交易所在此窗口内处理的 fill 无法被单次查询捕获(TOCTOU)。
降级路径使用仓位差值估算数量但硬编码价格 0.0,
触发上层错误的价格回填流程,并引入不一致的仓位数据。
修复建议
降级路径应返回 None 而非 0.0,让上层明确触发 HTTP 价格回填:
except Exception:
actual = self._get_actual_position_size(...)
return actual, None # 明确告知上层价格未知,触发回填
Bug 叠加效应分析
B1(EventBus 隔离)
↓ WS 路径失效,所有订单退化为 HTTP 兜底
↓
├─→ B4(Fill Price 为 0)
│ HTTP 超时时 px=0,has_fill_price 错误标记,跳过回填
│ → 成交价 0.0 传递到仓位系统 → PnL 计算基于错误开仓价
│
└─→ B5(监控线程异常被吞)
HTTP 查询失败 → _resolve 异常 → result_event 永不释放
→ wait_for_order() 永久阻塞 → 持仓操作停止响应
B2(WebSocket 死锁)
↓ 触发后系统完全丧失 WS 能力
↓ 叠加 B1,此时 HTTP 兜底也因线程阻塞而失效
↓
系统完全失去订单追踪能力(双重失效)
修复优先级与关键文件索引
立即处理(P0 / CRITICAL)
| 顺序 | Bug | 关键文件 | 关键行 | 修复要点 |
|---|---|---|---|---|
| 1 | B2 WebSocket 死锁 | enhanced_ws_manager.py |
L977–988 | 将 ws.send() 移至锁外 |
| 2 | B1 EventBus 隔离 | enhanced_ws_manager.py |
L306 | 支持 event_bus 参数注入 |
| B1 EventBus 隔离 | realtime_kline_service_base.py |
创建 trading WS 处 | 传入 executor._event_bus |
|
| 3 | B3 Grace Timer 竞态 | websocket_order_manager.py |
L329–350, L223 | _resolve() 检查 status != PENDING |
| 4 | B4 Fill Price 为 0 | websocket_order_manager.py |
L270, L277 | 设置 has_fill_price 前校验 avg_price > 0 |
| 5 | B5 监控线程异常 | websocket_order_manager.py |
L427–434 | 嵌套 try/except 保护 _resolve,兜底 result_event.set() |
短期处理(HIGH)
| 顺序 | Bug | 关键文件 | 关键行 | 修复要点 |
|---|---|---|---|---|
| 6 | B6 Timer 内存泄漏 | websocket_order_manager.py |
L343–350 | 改用可真正取消的延迟机制 |
| 7 | B7 Fill 去重缺陷 | websocket_order_manager.py |
L110–115 | 重复 track_order 时继承 old._fill_ids |
| 8 | B8 TOCTOU 竞态 | executor.py |
L799–804, L916–930 | 降级路径返回 None 而非 0.0 |
验证方法
| Bug | 验证方式 |
|---|---|
| B1 | 注入同一 event_bus 后,打印 _on_order_status_event 被调用次数,应与 WS 推送次数一致 |
| B2 | 在 add_subscriptions 调用期间并发推送 WS 消息,超时内未死锁即通过 |
| B3 | 模拟 orderUpdates 和 userFills 同时到达,确认 _resolve 只执行一次(fill_count == 1) |
| B4 | Mock HTTP 返回 px=0,确认 has_fill_price == False,_backfill_order_price 被调用 |
| B5 | 在 _resolve 内注入异常,确认 result_event 被 set(),wait_for_order() 正常返回 |
| B7 | 重连后推送旧 fill_id,确认 _fill_ids 去重生效,filled_size 不变 |