当前系统订单跟踪存在哪些严重的bug4
订单跟踪系统 Bug 分析报告
分析时间: 2026-02-21
分析范围:src/trading/websocket_order_manager.py,src/trading/executor.py,src/trading/position_manager.py,src/trading/trade_repository.py
Bug 总览
| # | 严重程度 | 文件 | 简述 |
|---|---|---|---|
| 1 | 🔴 致命 | websocket_order_manager.py |
verify_pending_orders 重连后将 "open" 订单错误标记为 TIMEOUT,导致有效挂单被撤销 |
| 2 | 🔴 致命 | executor.py |
缓存时间戳共享导致 get_available_balance 返回 0,阻断所有 pair 模式开仓 |
| 3 | 🔴 严重 | position_manager.py |
_execute_close 使用过期缓存误判"仓位消失",不执行实际平仓就关闭仓位记录 |
| 4 | 🟠 中等 | websocket_order_manager.py |
_on_user_fill 多次部分成交时价格被覆盖而非加权平均,导致成本价失真 |
| 5 | 🟠 中等 | position_manager.py |
recover_positions_from_db 在持有 _lock 期间执行网络 I/O,可能导致启动阶段死锁 |
Bug 1(🔴 致命)- verify_pending_orders 重连后错误撤销存活订单
问题位置
websocket_order_manager.py:274-290(_parse_order_response)websocket_order_manager.py:97-105(verify_pending_orders)
根本原因
_parse_order_response 对 Hyperliquid 返回的 "open" 状态(订单仍在挂单等待成交)映射为 OrderStatus.TIMEOUT:
@staticmethod
def _parse_order_response(resp) -> tuple[OrderStatus, float, float]:
raw_status = (resp.get("status") or "").lower()
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
# ❌ "open" 状态落入此分支
return OrderStatus.TIMEOUT, 0.0, 0.0
触发路径
- WebSocket 断线后触发重连
executor._on_websocket_reconnected→ 调用ws_order_manager.verify_pending_orders()verify_pending_orders对所有 PENDING 订单调用_resolve_via_http- Hyperliquid HTTP API 返回
{"status": "open", ...}(订单仍在等待成交,完全正常) _parse_order_response误判为TIMEOUT_resolve_via_http将tracking.status = TIMEOUT,触发result_event.set()wait_for_order返回False_track_limit_order认为未成交 →limit_open执行撤单- 有效挂单被强制取消,错失成交机会
额外说明
verify_pending_orders 不检查订单是否已到超时时间,对所有 PENDING 订单一律强制结算。即使订单还有 590 秒有效期,重连后一律被视为超时处理。
修复方向
_parse_order_response 增加 "open" 分支返回 PENDING(或专用状态),verify_pending_orders 收到 PENDING 结果时跳过,不关闭追踪。
Bug 2(🔴 致命)- 缓存时间戳共享导致可用余额返回 0,阻断所有开仓
问题位置
executor.py:1516-1526(_on_position_updated)executor.py:1808-1814(get_available_balance缓存命中条件)
根本原因
_cache_timestamp 被 PositionUpdatedEvent 和 BalanceChangedEvent 共享,但每个事件只更新了自己负责的字段,未同步更新另一个字段:
def _on_position_updated(self, event: PositionUpdatedEvent):
with self._cache_lock:
self._cached_positions = event.positions
self._cached_account_value = event.account_value
self._cache_timestamp = event.timestamp.timestamp()
# ❌ 缺失: _cached_available_balance 未更新
# 但 _cache_timestamp 已刷新!
def get_available_balance(self, force_refresh=False):
if not force_refresh:
with self._cache_lock:
age = time.time() - self._cache_timestamp
# PositionUpdated 刷新了时间戳 → age < ttl 成立
# _cache_timestamp > 0 成立
if age < self._cache_ttl and self._cache_timestamp > 0:
return self._cached_available_balance # ❌ 返回 0.0(初始值)
触发场景
系统收到 PositionUpdatedEvent(如持仓变化)但尚未收到 BalanceChangedEvent 时:
_cache_timestamp被刷新为当前时间_cached_available_balance仍为0.0(初始值或上次重置后的值)- 下次调用
get_available_balance()时,缓存命中,返回0.0 _precheck_leg_b_conditions中:available_balance (0.0) < required * 1.1→ 始终为真- 所有 pair 模式开仓请求被拒绝,告警"Leg B 余额不足"
修复方向
将余额缓存的时间戳与持仓缓存时间戳分开维护,或在缓存命中条件中加入余额有效性检查:
# 方案:分离时间戳
self._balance_cache_timestamp = 0.0
self._positions_cache_timestamp = 0.0
# 或:加入有效性标志
self._balance_cache_valid = False
Bug 3(🔴 严重)- 平仓前使用过期缓存误判"仓位消失",跳过实际平仓
问题位置
position_manager.py:347-368(_execute_close同步持仓段)
根本原因
_execute_close 在平仓前同步持仓大小时,使用默认的 get_positions()(不强制 HTTP 刷新),依赖可能过期的缓存:
def _execute_close(self, ...):
try:
# ❌ 未使用 force_refresh=True,可能返回旧缓存
exchange_positions = self._executor.get_positions()
alt_found = False
with self._lock:
for ex_pos in exchange_positions:
if coin == alt_coin and szi != 0:
alt_found = True
alt_gone_from_exchange = not alt_found # 缓存为空时必然 True
except (...) as e:
logger.warning("使用系统记录值")
# ← 异常时保持 alt_gone_from_exchange = False(安全)
if alt_gone_from_exchange:
# ❌ 不执行平仓,直接在数据库关闭仓位记录
# 交易所实仓仍在,但系统不再追踪!
触发场景
- WebSocket 断线 →
_on_websocket_reconnected清空缓存:_cached_positions = [] - HTTP 查询
get_account_state()失败(网络问题)→ 返回空字典 get_positions()返回空列表(无异常,不触发 except 分支)alt_found = False,alt_gone_from_exchange = True- 系统认为仓位已被外部清掉,直接关闭数据库记录,不向交易所发送任何平仓指令
- 交易所仍保有实仓,成为系统未追踪的孤立仓位
修复方向
在 _execute_close 中使用 force_refresh=True 强制从 HTTP 获取最新持仓:
exchange_positions = self._executor.get_positions(force_refresh=True)
Bug 4(🟠 中等)- _on_user_fill 多次部分成交时价格被覆盖,成本价失真
问题位置
websocket_order_manager.py:211-228(_on_user_fill)
根本原因
同一订单收到多条 userFills 消息(分批成交)时,avg_price 和 filled_size 每次都被直接覆盖,而非累加或加权平均:
def _on_user_fill(self, message):
for fill in fills:
with self._lock:
tracking = self._tracking.get(oid)
if tracking and tracking.status == OrderStatus.PENDING:
if px:
tracking.avg_price = float(px) # ❌ 覆盖,非加权平均
if sz:
tracking.filled_size = float(sz) # ❌ 覆盖,非累加
else:
# 已 resolve 订单的缓存也一样覆盖
self._fill_prices[oid] = (float(px), float(sz)) # ❌ 覆盖
后果
订单分批成交(如 10 次小批量成交)时:
avg_price记录最后一笔成交价,而非全量加权均价filled_size记录最后一笔成交量,而非累计成交量position.alt_entry_price和position.alt_size基于错误数据- PnL 统计失真,止损/止盈计算基础错误
说明
Hyperliquid GTC 限价单通常一次全量成交,多次部分成交概率较低,但流动性不足时仍可能触发。
修复方向
# 累加成交量,计算加权平均价
if px and sz:
old_sz = tracking.filled_size
new_sz = old_sz + float(sz)
tracking.avg_price = (tracking.avg_price * old_sz + float(px) * float(sz)) / new_sz
tracking.filled_size = new_sz
Bug 5(🟠 中等)- recover_positions_from_db 持锁期间执行网络 I/O
问题位置
position_manager.py:960-996(recover_positions_from_db锁内代码段)
根本原因
recover_positions_from_db 在持有 self._lock 的同时调用 get_all_mids(),后者可能发起 HTTP 网络请求(50~200ms):
with self._lock:
for row in rows:
# ... 内存操作 ...
self._positions[key] = position
# ❌ 在 _lock 内发起可能的网络请求
if recovered > 0:
all_mids = self._executor.get_all_mids()
# get_all_mids → get_all_mids_from_ws (内存) 或
# get_all_mids_from_api → retry_call → HTTP (50-200ms)
后果
系统重启的仓位恢复阶段,若 WebSocket L2 缓存为空(刚启动),get_all_mids() 降级为 HTTP 请求。HTTP 请求期间(含重试最多数秒):
- 所有需要
self._lock的操作全部阻塞:开仓、平仓、价格更新 - 若 HTTP 超时,系统在启动阶段长时间无响应
- 极端情况下(网络问题 + 多次重试),可能导致启动卡死数十秒
修复方向
将 get_all_mids() 调用移至 self._lock 外部,先获取数据再加锁写入:
# 锁外获取价格数据
all_mids = self._executor.get_all_mids() if recovered > 0 else {}
with self._lock:
# 只做纯内存操作
for pos in self._positions.values():
mid = all_mids.get(symbol_to_coin(pos.symbol), 0.0)
if mid > 0:
pos.alt_current_price = mid
优先修复计划
| 优先级 | Bug | 修复复杂度 | 风险 |
|---|---|---|---|
| P0 | Bug 1 - verify_pending_orders 撤销有效挂单 | 低(增加 "open" 分支) | 资金损失(错过成交机会) |
| P0 | Bug 2 - 缓存时间戳共享余额返回 0 | 中(分离时间戳) | 系统拒绝所有开仓 |
| P1 | Bug 3 - 平仓误判仓位消失 | 低(加 force_refresh=True) | 孤立仓位,暴露风险 |
| P2 | Bug 4 - 部分成交价格累加错误 | 中(加权平均逻辑) | 成本价失真,PnL 不准 |
| P2 | Bug 5 - 锁内网络 I/O | 低(移至锁外) | 启动阶段卡顿 |