当前系统订单跟踪存在哪些严重的bug10
订单跟踪系统 Bug 分析报告(第二期)
分析日期: 2026-02-21
涉及文件:
src/trading/executor.pysrc/trading/websocket_order_manager.pysrc/trading/position_manager.pysrc/utils/websocket/enhanced_ws_manager.py
🔴 严重 Bug(可能造成资金损失)
Bug 1:_publish_orderbook_event L2 订单簿数据完全解析错误
文件: src/utils/websocket/enhanced_ws_manager.py:1364-1365
问题代码:
# 当前代码(错误)
bids=levels[0][1] if len(levels) > 0 and len(levels[0]) > 1 else [],
asks=levels[0][0] if len(levels) > 0 and len(levels[0]) > 0 else [],
Hyperliquid L2Book 实际格式:
{
"channel": "l2Book",
"data": {
"coin": "BTC",
"levels": [
[{"px": "50000", "sz": "1", "n": 5}, ...], // index 0 = bids
[{"px": "50001", "sz": "0.5", "n": 3}, ...] // index 1 = asks
]
}
}
正确写法:
bids=levels[0] if len(levels) > 0 else [], # 全部买单列表
asks=levels[1] if len(levels) > 1 else [], # 全部卖单列表
现象分析:
levels[0][1]→ 取的是第二档买单的单个 dict,而非全部 bids 列表levels[0][0]→ 取的是最优买单的单个 dict,而非全部 asks 列表
后果: 所有 OrderBookUpdatedEvent 订阅者收到的 bids/asks 数据对调且截断为单档,是完全错误的数据。
注意: 交易关键路径(
executor.py中通过latest_data缓存直接读取 raw 消息计算 mid price)走的是独立代码路径,不受此 Bug 影响。受影响范围为所有OrderBookUpdatedEvent的下游订阅者。
Bug 2:get_available_balance WS 断连时不回退 HTTP,返回过期缓存
文件: src/trading/executor.py:1839-1844
问题代码:
# get_account_value —— 有断连检测 ✅
if age < self._cache_ttl and self._position_cache_ts > 0:
if self._is_ws_disconnected():
logger.debug("WS 断连,跳过账户价值缓存,降级 HTTP 查询")
else:
return self._cached_account_value
# get_available_balance —— 缺少断连检测 ❌
if age < self._cache_ttl and self._balance_cache_ts > 0:
logger.debug(f"✅ 可用余额缓存命中 | balance=${self._cached_available_balance:.2f} | age={age:.1f}s")
return self._cached_available_balance # ← WS 断连时仍返回过期数据!
调用链:
limit_open
└── _precheck_leg_b_conditions
└── get_available_balance() ← 返回断连前的旧余额
└── get_available_balance(force_refresh=True) ← Leg B 余额复检
└── 同样缺少断连检测
后果: WS 断连(重连中)期间:
- Leg B 余额预检查使用过期余额 → 可能在余额不足时仍通过验证,导致 Leg B 下单失败
- Leg B 余额复检使用
force_refresh=True可绕过缓存,但若缓存命中(TTL 未过期)仍返回旧值 get_account_value和get_available_balance行为不一致,逻辑上是遗漏
Bug 3:Leg B 定价使用的 all_mids 在 Leg A 追踪开始前预取,价格严重过期
文件: src/trading/executor.py:734, 860-864
问题代码:
def limit_open(self, signal, alt_size, base_size=0.0):
# ① Leg A 下单"之前"预取价格
all_mids = self.get_all_mids() if self._config.pair_mode == "pair" and base_size > 0 else {}
# ② Leg A 限价单追踪(最长可达 limit_order_timeout 秒,默认 600s)
leg_a_filled = self._track_limit_order(result.leg_a, alt_coin, timeout)
# ③ 追踪结束后,仍使用 ① 时预取的价格
base_price = all_mids.get(base_coin, 0.0) if all_mids else 0.0
if base_price <= 0: # 只有缓存返回 0 才重新获取
base_price = self.get_all_mids().get(base_coin, 0.0)
后果: 在 Leg A 追踪的数分钟内,若 base_coin 价格发生较大波动:
- Leg B 数量 =
leg_a_value / base_price(旧)→ 数量计算偏差 - 实际对冲名义价值与 Leg A 不等,配对比例失衡
- 市场剧烈波动时(如大行情),偏差可超过 5%-10%
触发频率: 每次 Leg A 未能即时成交(需要等待追踪)的限价开仓均受影响。
🟠 中等 Bug(影响数据准确性)
Bug 4:_on_order_update 无 userFills 时使用 limitPx 替代实际成交价
文件: src/trading/websocket_order_manager.py:188-203
问题代码:
if status_str == "filled":
fill = self._fill_prices.pop(oid, None)
if fill and fill[0] > 0:
# 路径 A: userFills 已到达 → 使用真实成交价 ✅
tracking.avg_price = fill[0]
tracking.filled_size = fill[1] if fill[1] > 0 else float(order.get("origSz", 0))
tracking.has_fill_price = True
elif tracking.has_fill_price:
# 路径 B: fill 已在 tracking 中积累 → 跳过(正确)✅
pass
else:
# 路径 C: 无任何 userFills 信息 → 回退到挂单价 ❌
tracking.avg_price = float(order.get("limitPx", 0)) # 挂单价而非成交价
tracking.filled_size = float(order.get("origSz", 0)) # 原始委托量
wait_for_order 轮询窗口:
# 仅轮询 5 次 × 0.2s = 最多等待 1 秒
for _ in range(self._FILL_POLL_ROUNDS): # FILL_POLL_ROUNDS = 5
fill = self._fill_prices.pop(tracking.oid, None)
...
time.sleep(self._FILL_POLL_INTERVAL) # FILL_POLL_INTERVAL = 0.2
后果:
- 若 userFills 推送延迟 > 1 秒,实际成交价永久丢失
tracking.avg_price = limitPx(挂单价),若有价格改善则 PnL 计算偏低- 此价格传递到
OrderResult.price→position.alt_entry_price→ 影响持仓盈亏记录
Bug 5:_backfill_order_price 中 "0" 字符串的 truthy 陷阱
文件: src/trading/executor.py:681-686
问题代码:
avg_px = resp.get("avgPx") or resp.get("avg_px")
total_sz = resp.get("totalSz") or resp.get("total_sz")
if avg_px: # ← "0" 字符串是 truthy!
order_result.price = float(avg_px) # → 0.0,价格被错误覆盖
if total_sz:
order_result.size = float(total_sz)
Python 行为说明:
# 数字 0 是 falsy,字符串 "0" 是 truthy!
bool(0) # False
bool("0") # True ← 陷阱所在
对比: 同项目 websocket_order_manager.py:324 正确处理了此问题:
# 正确写法 ✅
avg_px = float(resp.get("avgPx") or resp.get("avg_px") or 0)
后果: 若 HTTP 订单查询 API 返回 "avgPx": "0"(字符串形式的零),order_result.price 会被设为 0.0,后续基于此价格的 PnL 计算会产生极大偏差。
Bug 6:wait_for_order +30s buffer 可能不足,引发已成交订单被撤单
文件: src/trading/websocket_order_manager.py:102
时序分析:
时间线:
t=0 track_order() 启动追踪,启动 _timeout_then_verify 线程
t=timeout _timeout_then_verify 超时,开始 HTTP 查询(可能含重试)
t=timeout+30 wait_for_order() 超时返回 False ← 此时 HTTP 查询可能尚未完成
t=timeout+30 调用方执行 _cancel_order() ← 撤单请求发出
t=timeout+35 _resolve_via_http 返回: 订单已成交 ← 但撤单已在途
竞态结果:
- 若撤单请求先到达交易所 → 已成交的订单被撤掉(交易所拒绝,返回错误)
- 若成交确认先处理 → 撤单被拒,系统降级查询实际持仓(正确兜底)
- 极端情况(网络慢 + 重试):两个请求同时在途,结果不确定
根本原因: _resolve_via_http 内部调用 query_order_status,该函数通过 retry_call 可能重试多次,实际耗时可超过 30 秒。
🟡 逻辑缺陷(低频但需关注)
Bug 7:Leg A 回滚失败时产生无风控的隐形持仓
文件: src/trading/executor.py:629-649 + src/trading/position_manager.py
场景:
1. Leg A 成交
2. Leg B 下单失败 → 触发 _rollback_leg_a()
3. _rollback_leg_a() 也失败(网络异常等)
4. 代码依然执行: result.leg_a.success = False
5. _open_position_inner 因 leg_a.success=False 不存储仓位
结果:
- 交易所:Leg A 仓位真实存在(回滚失败)
- 系统内存:无该仓位记录
- 数据库:无该仓位记录
- 该仓位无止损/超时/移动止损保护
发现时机: 仅在下次 sync_with_exchange 的孤儿检测时发现,此时价格可能已大幅不利变动。Lark 告警会发出,但已错过最佳处置窗口。
汇总表
| 编号 | 文件 | 行号 | 危险级别 | 核心影响 |
|---|---|---|---|---|
| Bug 1 | enhanced_ws_manager.py |
1364-1365 | 🔴 严重 | OrderBookUpdatedEvent bids/asks 数据完全错误 |
| Bug 2 | executor.py |
1839-1844 | 🔴 严重 | WS 断连期间余额检查使用过期数据 |
| Bug 3 | executor.py |
734, 860-864 | 🔴 严重 | Leg B 对冲数量因价格过期而失准 |
| Bug 4 | websocket_order_manager.py |
197-199 | 🟠 中等 | userFills 延迟时成交价丢失,使用挂单价替代 |
| Bug 5 | executor.py |
681-686 | 🟠 中等 | "0" 字符串 truthy 陷阱,成交价可被覆盖为 0 |
| Bug 6 | websocket_order_manager.py |
102 | 🟠 中等 | 超时 buffer 不足,已成交订单可能被撤单 |
| Bug 7 | executor.py / position_manager.py |
629+ | 🟡 低频 | 回滚失败后产生无风控隐形持仓 |
修复优先级建议
优先级 P0(立即修复,影响资金安全):
→ Bug 2: 为 get_available_balance 补充 WS 断连检测
→ Bug 3: Leg B 下单前重新获取实时价格,不使用预取的 all_mids
优先级 P1(尽快修复,影响数据正确性):
→ Bug 1: 修正 _publish_orderbook_event 的 levels 索引
→ Bug 4: 延长 userFills 等待窗口,或在 wait_for_order 后异步补填价格
优先级 P2(计划修复):
→ Bug 5: 参照 _safe_positive_float 重写 _backfill_order_price 的价格解析
→ Bug 6: 增大 buffer 或改用超时事件链式通知
→ Bug 7: 在 _rollback_leg_a 失败时主动写入"孤儿仓位"记录