订单跟踪bug14
订单跟踪系统 Bug 分析报告(第二轮)
分析日期: 2026-02-21
涉及文件:
src/trading/websocket_order_manager.pysrc/services/realtime_kline_service_base.pysrc/utils/websocket/enhanced_ws_manager.pysrc/trading/executor.pysrc/events/event_bus.py
BUG 1(致命):重连事件同步调用 HTTP 补查,阻塞 WebSocket 线程
位置: enhanced_ws_manager.py:567-586 → executor.py:1554-1569
调用链
_on_open(WS线程)
→ EventBus.publish(WebSocketReconnectedEvent) [同步阻塞]
→ executor._on_websocket_reconnected
→ verify_pending_orders()
→ _resolve_via_http(oid) [HTTP 调用,每次 5-15s]
问题描述
_on_open 在 WebSocket 线程中运行。EventBus.publish 是同步调用(event_bus.py:99-107),若存在多个 pending 订单,verify_pending_orders 会在 WS 线程上连续发出 HTTP 请求,阻塞 ws_ready_event.set()(位于 _on_open 最后一行)。
与此同时,_connect 主线程正在等待:
# enhanced_ws_manager.py:540-541
if not self.ws_ready_event.wait(timeout=self.timeout):
raise TimeoutError(...)
风险
若 HTTP 补查总耗时超过 WS_TIMEOUT,_connect 抛出 TimeoutError,触发再次重连,再次触发 _on_open,再次补查,形成死循环。极端情况下服务永远无法成功重连。
复现条件
- WebSocket 断连时存在 ≥1 个 PENDING 订单
- HTTP 查询耗时 × 订单数 >
WS_TIMEOUT(默认约 30s)
修复思路
在 _on_open 中将事件 publish 移至锁外,或让 _on_websocket_reconnected 在独立线程中执行 verify_pending_orders,避免阻塞 WS 线程。
BUG 2(致命):wait_for_order 可能在订单实际成交后返回 False
位置: websocket_order_manager.py:124-128 与 _timeout_then_verify:320-383
问题代码
def wait_for_order(self, tracking: OrderTracking) -> bool:
max_wait = tracking.timeout_seconds + self._HTTP_VERIFY_BUFFER # +90s
tracking.result_event.wait(timeout=max_wait)
return tracking.status == OrderStatus.FILLED
问题描述
_timeout_then_verify 超时后的最长执行路径:
等待 timeout_seconds
+ 首次 HTTP 查询耗时(网络慢时可达 30s+)
+ HTTP_RETRY_DELAY(15s 固定等待)
+ 二次 HTTP 查询耗时(网络慢时可达 30s+)
若两次 HTTP 各耗时 45s,额外耗时约 105s,超过 _HTTP_VERIFY_BUFFER = 90s 的缓冲。
wait_for_order 的等待超时先行返回,此时 tracking.status 仍为 PENDING,函数返回 False。
风险
调用方收到 False 后认为订单失败,可能执行补偿操作(如重新下单)。而后台 _timeout_then_verify 线程随后通过 HTTP 确认订单已成交并 resolve,导致:
- 重复开仓:同一方向下了两笔订单
- 仓位误判:调用方记录订单失败,实际仓位已建立
修复思路
增大 _HTTP_VERIFY_BUFFER(如 180s),或在 wait_for_order 返回后二次确认 tracking.status(重新等待短暂时间),或通过回调而非阻塞等待的方式通知调用方最终结果。
BUG 3(严重):_http_querying 标志误返回 "resolved",可能导致订单永久 PENDING
位置: websocket_order_manager.py:412-414
问题代码
def _resolve_via_http(self, oid: int) -> str:
with self._lock:
...
if tracking._http_querying:
return "resolved" # ❌ 语义错误:查询进行中 ≠ 已解析
tracking._http_querying = True
危险并发场景
时刻 T1: verify_pending_orders(重连触发)开始查询 oid=X
→ _http_querying = True
时刻 T2: _timeout_then_verify(订单已超时)调用 _resolve_via_http(X)
→ 见到 _http_querying = True
→ 返回 "resolved"
→ 超时线程判断订单已处理,退出
时刻 T3: verify_pending_orders 的 HTTP 查询失败(网络异常)
→ 不解析,返回 "error",verify_pending_orders 忽略返回值
结果: 超时线程已退出,WS 消息不再到来,订单永久 PENDING
影响
- 订单资金永久占用,无法释放
wait_for_order调用方永远阻塞(直到max_wait超时返回False)
修复思路
将 _http_querying = True 时的返回值改为 "pending"(新增状态),让 _timeout_then_verify 等待一段时间后再重试,而非直接退出。或使用更健壮的锁机制(如 asyncio.Event)等待并发查询完成。
BUG 4(严重):当前订单消息无异常隔离,异常时静默丢失
位置: realtime_kline_service_base.py:619-625
问题代码
if mgr is not None:
# 缓冲消息有异常隔离 ✅
while self._order_msg_buffer:
buffered = self._order_msg_buffer.popleft()
try:
mgr.handle_message(buffered)
except Exception as e:
self.logger.error(f"回放缓冲订单消息失败: {e}", exc_info=True)
mgr.handle_message(msg) # ❌ 当前消息无 try/except 保护
问题描述
缓冲区中的历史消息有逐条 try/except 保护(异常隔离),但当前最新消息 mgr.handle_message(msg) 没有异常保护。
若 _on_order_update 或 _on_user_fill 因以下原因抛出异常:
- 消息字段格式异常(API 变更)
- 内部逻辑错误
异常会传播到 on_message 的外层 except,当前订单消息被日志记录后永久丢失。
风险
orderUpdates "filled"消息丢失 → 订单不进入宽限期等待 → 超时后强制 TIMEOUTuserFills消息丢失 → 成交价格无法记录,退化使用兜底价或 HTTP 补查价
修复思路
对 mgr.handle_message(msg) 包裹与缓冲消息相同的 try/except 异常隔离。
BUG 5(严重):_parse_order_response 未处理 "triggered" 状态
位置: websocket_order_manager.py:471-482
问题代码
@staticmethod
def _parse_order_response(resp: dict | None) -> tuple[OrderStatus | None, float, float]:
...
if raw_status == "filled":
return OrderStatus.FILLED, avg_px, total_sz
if raw_status in ("canceled", "margincanceled"):
return OrderStatus.CANCELED, 0.0, 0.0
if raw_status == "rejected":
return OrderStatus.REJECTED, 0.0, 0.0
if raw_status == "open":
return OrderStatus.PENDING, 0.0, 0.0
# ❌ 缺少: "triggered"(条件单触发转市价单)
logger.warning(f"HTTP 订单状态未识别: raw_status={raw_status!r},视为查询失败")
return None, 0.0, 0.0
问题描述
Hyperliquid 的止损单/止盈单触发时,订单状态会短暂变为 "triggered",表示条件已满足、正在以市价执行。此状态未被处理,_parse_order_response 返回 (None, 0.0, 0.0),被视为 HTTP 查询失败。
后续影响链
HTTP 返回 "triggered"
→ _parse_order_response 返回 None(视为 error)
→ _timeout_then_verify 触发 15s 重试
→ 二轮查询订单可能已 filled/canceled
(若二轮仍为 triggered)→ 强制 TIMEOUT
→ tracking 以 TIMEOUT 状态结算,丢失真实成交信息
→ 策略认为止损未触发,可能不更新仓位
修复思路
在 _parse_order_response 中增加对 "triggered" 状态的处理,视为 PENDING(订单仍在处理中),避免误判为查询失败。
BUG 6(中等):_connect 过早清空 active_subscriptions
位置: enhanced_ws_manager.py:514-517
问题代码
def _connect(self):
# 主线程执行(连接建立前)
with self.subscriptions_lock:
subscriptions_to_use = list(self.subscriptions)
self.active_subscriptions.clear() # ← 过早清空!
# ... WS 线程启动,_on_open 稍后才真正订阅并填充 active_subscriptions
问题描述
active_subscriptions 在 _connect 中被清空,但实际的订阅发送和 active_subscriptions 填充发生在 WS 线程的 _on_open 中。在这段窗口期内,若 add_subscriptions 被并发调用:
- 检查
if sub_key in self.active_subscriptions→ 空字典,总是 False - 认为是新订阅,添加到
self.subscriptions列表 _on_open完成后,active_subscriptions以subscriptions_to_use(旧快照)填充- 窗口期添加的订阅在
active_subscriptions中不存在 - 下次
add_subscriptions调用时再次被视为新订阅,发送重复订阅请求
风险
服务端收到重复订阅请求(通常返回 subscriptionResponse 或静默忽略,但可能增加服务端状态管理负担),以及 active_subscriptions 状态不准确导致去重失效。
修复思路
在 _on_open 中(WS 线程)统一管理 active_subscriptions 的清空和填充,_connect 主线程不操作 active_subscriptions,仅传递订阅列表快照。
总结
| # | 位置 | 严重程度 | 核心风险 |
|---|---|---|---|
| 1 | _on_open 同步 HTTP 补查 |
🔴 致命 | 重连死循环,服务永远无法恢复 |
| 2 | wait_for_order 超时竞态 |
🔴 致命 | 误判订单失败导致重复开仓 |
| 3 | _http_querying 误返回 "resolved" |
🟠 严重 | 订单永久 PENDING,资金占用 |
| 4 | 当前订单消息无异常隔离 | 🟠 严重 | 订单更新静默丢失 |
| 5 | 缺少 "triggered" 状态处理 | 🟠 严重 | 止损/止盈订单以 TIMEOUT 结算 |
| 6 | 过早清空订阅状态 | 🟡 中等 | 重复订阅,状态不准确 |
优先修复建议:BUG 1 和 BUG 2 直接影响系统稳定性和资金安全,应首先处理。