当前系统订单跟踪存在哪些严重的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-105verify_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

触发路径

  1. WebSocket 断线后触发重连
  2. executor._on_websocket_reconnected → 调用 ws_order_manager.verify_pending_orders()
  3. verify_pending_orders 对所有 PENDING 订单调用 _resolve_via_http
  4. Hyperliquid HTTP API 返回 {"status": "open", ...}(订单仍在等待成交,完全正常)
  5. _parse_order_response 误判为 TIMEOUT
  6. _resolve_via_httptracking.status = TIMEOUT,触发 result_event.set()
  7. wait_for_order 返回 False
  8. _track_limit_order 认为未成交 → limit_open 执行撤单
  9. 有效挂单被强制取消,错失成交机会

额外说明

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-1814get_available_balance 缓存命中条件)

根本原因

_cache_timestampPositionUpdatedEventBalanceChangedEvent 共享,但每个事件只更新了自己负责的字段,未同步更新另一个字段:

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 时:

  1. _cache_timestamp 被刷新为当前时间
  2. _cached_available_balance 仍为 0.0(初始值或上次重置后的值)
  3. 下次调用 get_available_balance() 时,缓存命中,返回 0.0
  4. _precheck_leg_b_conditions 中:available_balance (0.0) < required * 1.1 → 始终为真
  5. 所有 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:
        # ❌ 不执行平仓,直接在数据库关闭仓位记录
        # 交易所实仓仍在,但系统不再追踪!

触发场景

  1. WebSocket 断线 → _on_websocket_reconnected 清空缓存:_cached_positions = []
  2. HTTP 查询 get_account_state() 失败(网络问题)→ 返回空字典
  3. get_positions() 返回空列表(无异常,不触发 except 分支)
  4. alt_found = Falsealt_gone_from_exchange = True
  5. 系统认为仓位已被外部清掉,直接关闭数据库记录,不向交易所发送任何平仓指令
  6. 交易所仍保有实仓,成为系统未追踪的孤立仓位

修复方向

_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_pricefilled_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_priceposition.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-996recover_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 低(移至锁外) 启动阶段卡顿

Read more

跑步的技巧(滚动落地)

“滚动落地(rolling contact / rolling foot strike)”不是一种教条式的“脚法”,而是一种 让冲击沿着整只脚、整条后链逐级传递的落地机制。 它的核心不是“你先用哪儿着地”,而是: 你的脚落地之后,冲击是不是像轮子一样滚过去,而不是像锤子一样砸下去。 这就是滚动落地的本质。 一、什么叫“滚动落地”? 你可以把它理解成两种完全不同的落地方式: 1. 砸地(撞击式) 脚像锤子一样拍到地上: * 要么后跟先砸 * 要么前掌先戳 * 冲击集中在一个点 * 一个结构瞬间吃掉大部分载荷 结果就是: * 后跟砸 → 膝盖难受 * 前掌戳 → 前脚掌磨烂 * 都不是长跑友好模式 这叫 撞击式着地(impact strike)。 2. 滚地(滚动式) 脚像轮胎一样“滚”过地面: * 不是某一点硬砸 * 而是外侧中足先轻触 * 再向前滚到前掌 * 最后从大脚趾蹬离

By SHI XIAOLONG

AMI的优越性

世界模型(World Models)的具体例子 如下,我按类型分类,便于理解。每类都附带实际实现、演示效果和应用场景。 1. Yann LeCun / Meta 的 JEPA 系列(最直接对应“世界模型”概念) 这些是 LeCun 主张的非生成式抽象预测世界模型代表。 * I-JEPA(Image JEPA,2023) 输入一张图像,模型把不同区域(context 和 target)编码成抽象表示,然后预测 target 的表示(不在像素级别重建)。 例子:给定一张遮挡了部分物体的图片,模型能预测“被遮挡物体的大致位置和属性”,构建对物体持久性和空间关系的理解。 这是一个“原始世界模型”,能学习物理常识(如物体不会凭空消失)。 * V-JEPA / V-JEPA 2(Video JEPA,

By SHI XIAOLONG

什么是:“世界模型(World Models)”

世界模型(World Models) 是人工智能领域的一个核心概念,尤其在 Yann LeCun 等研究者推动的下一代 AI 架构中占据中心位置。它指的是 AI 系统在内部构建的对现实世界的抽象模拟或内部表示,让机器能够像人类或动物一样“理解”物理世界、预测未来、规划行动。 简单比喻 想象你闭上眼睛也能“看到”房间里的物体会如何移动、碰撞或掉落——这就是你大脑里的世界模型。AI 的世界模型就是类似的“数字孪生”(digital twin)或“内部模拟器”:它不是简单记住数据,而是学习世界的动态、因果关系和物理直觉(如重力、物体持久性、遮挡、因果等)。 为什么需要世界模型? 当前主流的大型语言模型(LLM) 擅长处理文本(统计模式预测),但存在根本局限: * 缺乏对物理世界的真正理解 → 容易“幻觉”、无法可靠规划。 * 样本效率低 → 人类/

By SHI XIAOLONG

K线周期可配置化设计方案

K线周期可配置化设计方案 1. 背景与目标 当前 Beta 套利策略的 K 线周期硬编码为 "1h",分散在多个文件中。需要: 1. 将 K 线周期从 1h 改为 2h 2. 提取为环境变量 BETA_ARB_KLINE_INTERVAL,使其可在 .env 中配置 2. 影响范围分析 2.1 需要修改的文件(共 6 个) 文件 硬编码位置 修改内容 src/trading/config.py BetaArbConfig dataclass 新增 kline_interval 字段,

By SHI XIAOLONG