订单跟踪系统BUG30

订单跟踪系统 Bug 分析报告

分析日期:2026-02-22
分析范围:src/trading/websocket_order_manager.pysrc/trading/executor.pysrc/trading/position_manager.py


一、架构与数据流简述

订单跟踪核心在 src/trading/websocket_order_manager.py

  • 唯一解析路径_resolve() 在持锁内将订单从 PENDING 转为终态(FILLED/CANCELED/REJECTED/TIMEOUT),并调用 result_event.set()
  • WS 驱动_on_order_update(orderUpdates)、_on_user_fill(userFills)在锁内设置 tracking._ws_status 等,释放锁后调用 _resolve()
  • HTTP/超时驱动:监控线程或 shutdown 传入 (oid, tracking, status, px, sz)_resolve 内做 identity check(current is tracking)再解析。
track_order(oid) ──────────────────┐
                                   ▼
WS "orderUpdates" ──→ _on_order_update ──→ _resolve(oid, tracking)
WS "userFills"    ──→ _on_user_fill    ──→ _resolve(oid, tracking)
grace timer       ──────────────────────→ _resolve(oid, tracking)   ← Bug 1/2 修复点
_monitor_order    ──→ _http_check      ──→ _resolve(oid, tracking, status, px, sz)
shutdown          ──────────────────────→ _resolve(oid, tracking, CANCELED)

二、Bug 分类总览

# 严重程度 状态 位置 简述
1 🔴 严重 工作区已修复,未提交 websocket_order_manager.py WS 解析 TOCTOU 竞态
2 🔴 严重 工作区已修复,未提交 websocket_order_manager.py Grace Timer TOCTOU 竞态
3 🟠 严重 未修复 executor.py limit_close 超时路径仍用仓位查询而非订单查询
4 🟠 严重 未修复 websocket_order_manager.py Grace Timer 在 WS 重连时被反复重置
5 🟡 低 已知缺陷 websocket_order_manager.py _fallback_sz=0 时 filled_size 记录丢失
6 🟡 低 已知缺陷 position_manager.py PnL 计算未含资金费

三、严重 Bug 详细分析


Bug 1 — WS 解析路径的 TOCTOU 竞态(工作区已修复,未提交)

位置: websocket_order_manager.py_on_order_update(约 386 行)、_on_user_fill(约 430 行)

问题描述

WS 消息处理在 _lock 内设置好 tracking._ws_status释放锁,然后仅以 oid 调用 _resolve(oid)_resolve 内再次按 oid 查表,存在典型的 TOCTOU(time-of-check to time-of-use)竞态窗口。

时间线(有问题的旧代码):

[Thread A - WS]       [Thread B - 调用方]
with _lock:
  tracking._ws_status = FILLED
  has_fill_price = True
  should_resolve = True
# 释放锁 ←── 竞态窗口开始
                       track_order(oid) ← 替换 _tracking[oid]!
                         old.status = CANCELED
                         old.result_event.set()  ← 旧追踪被误设为 CANCELED
_resolve(oid)
  with _lock:
    tracking = _tracking.get(oid)  ← 拿到的是新 tracking(_ws_status=None)
    → 条件不满足,返回 False       ← 新追踪永远等到 TIMEOUT

后果

  1. 已成交订单被上报为 CANCELED:旧 tracking 的 result_eventtrack_order 替换时设置,调用方得到 CANCELED 结果。
  2. 新订单被上报为 TIMEOUT:新 tracking 的 _ws_status=None,WS 路径解析失败,只能等到 600 秒超时才被 HTTP 兜底。
  3. 两边结果都错:实际已成交的订单,没有一个追踪对象能正确得到 FILLED 终态。

受影响代码(旧版本)

# _on_order_update(旧)
if should_resolve:
    self._resolve(oid)          # ← 仅传 oid,存在竞态

# _on_user_fill(旧)
if should_resolve:
    self._resolve(oid)          # ← 同上

修复方案(已在工作区实现)

在锁内保留 tracking 引用,释放锁后传入 _resolve_resolve 内对 tracking 做 identity check:

# _on_order_update(新)
resolve_tracking = None
with self._lock:
    ...
    if tracking.has_fill_price:
        should_resolve = True
        resolve_tracking = tracking    # ← 保存引用
    ...

if should_resolve:
    self._resolve(oid, resolve_tracking)  # ← 传入引用

# _resolve 内(新)
else:  # 按引用模式(WS / HTTP / 超时)
    current = self._tracking.get(oid)
    if current is not tracking:           # ← identity check
        return False
    ...
    final_status = status if status is not None else tracking._ws_status

Bug 2 — 宽限期 Grace Timer 的 TOCTOU 竞态(工作区已修复,未提交)

位置: websocket_order_manager.py_on_order_update 中创建 Timer 处(约 367 行)

问题描述

orderUpdates "filled" 到达、has_fill_price=False 时,启动宽限期定时器等待 userFills:

# 旧代码(有问题)
timer = threading.Timer(
    self._FILL_GRACE_SEC, self._resolve, [oid]   # ← 5 秒后仅传 oid
)

5 秒后定时器触发,_resolve(oid) 同样按 oid 查表,存在与 Bug 1 完全相同的竞态窗口。

后果

若在 5 秒宽限期内同一 oidtrack_order() 替换:

  • 定时器触发时拿到新 tracking(_ws_status=None),解析条件不满足
  • 旧 tracking 已被替换时设为 CANCELED → 已成交订单被误报 CANCELED
  • 新 tracking 继续等待直到 600 秒超时 → 新追踪被误报 TIMEOUT

修复方案(已在工作区实现)

# 新代码
timer = threading.Timer(
    self._FILL_GRACE_SEC, self._resolve,
    [oid, tracking]    # ← 传入 tracking 引用,_resolve 内做 identity check
)

Bug 3 — limit_close 超时路径未使用 _check_order_after_cancel(未修复)

位置: executor.pylimit_close() 方法,约 1147 行(Leg A)和 1188 行(Leg B)

问题描述

limit_close 超时处理:下了限价平仓单 → 追踪超时 → 撤单 → 查仓位判断残余:

# limit_close 超时路径(当前代码)—— 存在 TOCTOU
remaining_size = self._get_actual_position_size(
    alt_coin, expected_side=position.alt_side, force_refresh=True
)

对比 limit_open 超时路径(已修复为查订单状态):

# limit_open 超时路径(已修复)—— 消除 TOCTOU
actual_filled, fill_px = self._check_order_after_cancel(
    alt_coin, result.leg_a.order_id, alt_side
)
# _check_order_after_cancel 内注释:
# "消除 TOCTOU 竞态:撤单后查订单状态(而非仓位),获取可靠的成交量"

竞态窗口

[撤单请求发出]
     │
     ├── 场景 A:交易所先处理 cancel → 后处理 fill
     │            仓位查询 = 原始持仓量(正常,会做市价补单)
     │
     └── 场景 B:交易所先处理 fill → 后处理 cancel(时间窗口内)
                  fill 执行后,仓位查询返回 0(误判为已全部平仓!)
                  ↓
                  result.leg_a.success = True(设置为已成交)
                  跳过市价补单
                  ↓
                  实际仓位残留在交易所,DB 标记为 CLOSED
                  ↓
                  数据不一致 + 资金风险

limit_open 的对比

limit_open 超时 limit_close 超时
撤单后如何判断成交量 _check_order_after_cancel(oid) → 查订单 _get_actual_position_size() → 查仓位
TOCTOU 风险 已消除(Bug E/F 修复注释) 仍存在
补充说明 需要精确 fill 量来计算 Leg B 数量 也需要精确量来避免漏平

后果

  • 持仓残留未平limit_close 以为平仓成功,跳过市价补单
  • DB 与实际不一致:仓位在 DB 中标记为 CLOSED,但交易所仍有头寸
  • PnL 计算错误:基于错误的退出假设计算盈亏
  • 后续信号被抑制has_position() 返回 False,策略不会为这对配对生成新信号,但交易所实际有敞口

修复建议

limit_close 超时路径改为与 limit_open 一致,使用 _check_order_after_cancel

# limit_close Leg A 超时(建议修改)
filled_size, fill_px = self._check_order_after_cancel(
    alt_coin, result.leg_a.order_id, alt_side
) if result.leg_a.order_id else (0.0, 0.0)

remaining_size = position.alt_size - filled_size  # 原始量 - 已成交量
if remaining_size > 0:
    # 市价补单
    ...
else:
    # 已全部成交
    result.leg_a.success = True
    result.leg_a.status = ORDER_STATUS_FILLED
    if fill_px > 0:
        result.leg_a.price = fill_px
    else:
        self._backfill_order_price(result.leg_a, alt_coin)

Bug 4 — Grace Timer 在 WS 重连时被反复重置(未修复)

位置: websocket_order_manager.py_on_order_update 中的 grace timer 创建逻辑(约 365-373 行)

问题描述

orderUpdates "filled" 到达且 has_fill_price=False 时,代码会取消旧定时器并启动新的 5 秒倒计时:

if tracking._grace_timer:
    tracking._grace_timer.cancel()     # 取消旧定时器
timer = threading.Timer(self._FILL_GRACE_SEC, ...)
timer.start()                          # 重新开始 5 秒倒计时!

WS 重连后,交易所会重推当前订单状态。若订单已是 "filled" 状态,每次重连都会触发上述逻辑,重置 5 秒计时器。

时间线示例

T=0s   orderUpdates "filled" → grace timer 启动(T+5s 到期)
T=1s   WS 断连重连,交易所推送 "filled" → grace timer 重置(T+6s 到期)
T=3s   WS 再次断连重连 → grace timer 重置(T+8s 到期)
T=5s   WS 第三次断连重连 → grace timer 重置(T+10s 到期)
...
T=Ns   WS 稳定 → grace timer 到期,使用 fallback_px 解析(延迟 N+5s)

后果

  • 订单解析严重延迟:网络抖动导致 WS 频繁重连时,本应 5 秒内完成的解析被推迟至重连稳定后
  • 开/平仓流程阻塞wait_for_order(tracking) 无超时地阻塞,直到 grace timer 最终到期
  • 策略响应延迟:在订单等待期间,策略层无法进行下一步操作
  • 极端情况:若 WS 持续抖动,延迟可达数十秒,直至 _monitor_order 的 600 秒总超时触发

修复建议

在重复 "filled" 消息时,若 grace timer 已在运行,跳过重置:

if status_str == "filled":
    tracking._ws_status = OrderStatus.FILLED
    tracking._fallback_px = ...
    tracking._fallback_sz = ...
    if tracking.has_fill_price:
        should_resolve = True
        resolve_tracking = tracking
    else:
        # 仅在 grace timer 尚未启动时才创建(防止重连重置)
        if tracking._grace_timer is None:        # ← 新增幂等检查
            timer = threading.Timer(
                self._FILL_GRACE_SEC, self._resolve,
                [oid, tracking]
            )
            timer.daemon = True
            tracking._grace_timer = timer
            timer.start()
        # 若 timer 已在运行,更新 fallback 价格但不重置定时器

四、已知缺陷(低严重度)

缺陷 5 — _fallback_sz=0 时 filled_size 可能记录为 0

位置: websocket_order_manager.py:358-359

若 orderUpdates "filled" 消息中不含 totalSzorigSz,则 _fallback_sz=0。若 5 秒内无 userFills 且 HTTP 补查失败,filled_size 记录为 0。

缓解: _verify_fill_completeness 会 HTTP 补查并覆盖。仅在 HTTP 也失败时影响实际记录。


缺陷 6 — PnL 计算未含资金费

位置: position_manager.py:730_calculate_realized_pnl

代码注释已说明:「资金费因无 API 接口获取,暂不扣减」。日志标注 (不含资金费)。实际盈亏比系统记录偏高(持仓时间越长,偏差越大)。

PnL 计算当前包含三部分:

  1. alt_pnl:目标币种盈亏
  2. base_pnl:基准币种盈亏(Leg B 失败时用 mid_price 估算)
  3. trading_fee:开仓(maker) + 平仓(taker) 手续费估算

缺失的资金费在永续合约中可累积为显著偏差,尤其是持仓超过数小时的场景。


五、修复优先级与建议

优先级 Bug 建议行动 原因
P0 Bug 1 & 2(TOCTOU) 立即提交工作区修复 已修复,可直接提交
P1 Bug 3(limit_close 路径) 本迭代修复 资金风险:持仓可能残留未平
P1 Bug 4(grace timer 重置) 本迭代修复 网络抖动时策略响应严重延迟
P2 缺陷 5 & 6 监控告警 + 后续迭代 影响记录准确性,无直接资金风险

六、核心设计原则(修复后应遵循)

  1. 所有 _resolve 调用路径均传入 tracking 引用,依赖 identity check 防止替换竞态
  2. 撤单后查订单状态(非仓位),消除 fill/cancel 并发的 TOCTOU
  3. Grace Timer 创建使用幂等逻辑,防止 WS 重连触发重置
  4. _resolve 是唯一解析路径,任何设置终态的操作都必须通过它完成

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