订单跟踪系统BUG27

订单跟踪严重 Bug 分析

1. 架构与数据流简述

订单跟踪核心在 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(oid)(仅传 oid,在 _resolve 内再次按 oid 查表)。
  • HTTP/超时驱动:监控线程或 shutdown 传入 (oid, tracking, status, px, sz)_resolve 内做 identity checkcurrent is tracking)再解析。

调用关系简要如下:

sequenceDiagram
  participant Exec as executor
  participant Mgr as WebSocketOrderManager
  participant WS as WS消息
  Exec->>Mgr: track_order(oid) -> wait_for_order(tracking)
  WS->>Mgr: handle_message(orderUpdates/userFills)
  Mgr->>Mgr: _on_* 内设 _ws_status,释放锁后 _resolve(oid)
  Mgr->>Mgr: _resolve(oid) 内 get(oid) 再解析

2. 严重 Bug 1:WS 解析的 TOCTOU 竞态(错误终态 + 可能误报超时)

位置src/trading/websocket_order_manager.py_on_order_update(约 374–376 行)、_on_user_fill(约 416–418 行)。

现象:在锁内对某个 tracking 设置好 _ws_status(如 FILLED)后释放锁,再调用 _resolve(oid)。在「释放锁」到「_resolve 内再次加锁并 self._tracking.get(oid)」之间,若其他线程对同一 oid 调用 track_order(oid),会替换掉当前条目。

后果

  1. 已成交订单被报成「取消」+「超时」

    • 替换时,旧 tracking 在 track_order 里被设为 CANCELEDresult_event.set(),对应调用方得到「取消」。
    • _resolve(oid) 随后拿到的是 tracking(_ws_status 仍为 None),解析条件不满足、返回 False,新 tracking 之后只会被超时路径解析为 TIMEOUT。
    • 订单实际已成交,但两方结果都错:一方 CANCELED,一方 TIMEOUT。
  2. 逻辑上「应被解析」的 tracking 未被解析

    • 设置 _ws_status 的是 tracking,但解析时按 oid 查表得到的是 tracking,旧 tracking 已不在表中且已被上面步骤标记为 CANCELED,因此 WS 路径永远不会用 FILLED 正确解析任一对象。

根因:WS 路径只用 oid 查表,没有像 HTTP/超时路径那样传入 tracking 引用并做 identity 校验,存在典型的 TOCTOU(time-of-check to time-of-use)窗口。

修复方向

  • WS 路径也改为「按引用解析」:在 _on_order_update / _on_user_fill 中,在持锁内得到并更新 tracking 后,在释放锁后调用 _resolve(oid, tracking=tracking)(不传 status,或约定 status=None 表示用 tracking._ws_status)。
  • 扩展 _resolve 语义:当 tracking is not Nonestatus is None 时,视为「WS 按引用解析」:在持锁内做 current = self._tracking.get(oid),仅当 current is tracking 时继续,终态用 tracking._ws_status(及现有 fallback 价格逻辑)。这样与现有 HTTP/超时路径的 identity check 一致,避免替换导致的错解析。

3. 严重 Bug 2:宽限期定时器只传 oid,存在相同竞态

位置src/trading/websocket_order_manager.py 约 359–361 行。

代码threading.Timer(self._FILL_GRACE_SEC, self._resolve, [oid]),即 5 秒后只带 oid 调用 _resolve(oid)

问题:定时器触发时再次按 oid 查表,若在此期间同一 oid 被 track_order 替换,会解析到新 tracking(通常 _ws_status 仍为 None),导致本次 _resolve 无效;本应在此定时器里被解析的「旧」tracking 已在替换时被标记 CANCELED 并 set_event,结果与 Bug 1 一致(已成交订单被报成取消/超时)。

修复方向:定时器回调改为携带当前 tracking 引用(例如 functools.partial(self._resolve, oid, tracking) 或等价 lambda),在 _resolve 中支持「按引用 + 使用 tracking._ws_status」的 WS 解析模式(与 Bug 1 的扩展一致),这样定时器触发时解析的是当时启动定时器的那个 tracking,不受后续替换影响。


4. 与现有设计的一致性

  • 文档注释(文件头)已说明:HTTP/超时路径用 identity check 防止 tracking 被 track_order() 替换;WS 路径当前是「仅 oid 查表」,未做同样保护。
  • 修复后:所有 调用 _resolve 的路径要么传 (oid, tracking, status, ...)(HTTP/超时),要么传 (oid, tracking)status=None(WS 按引用),统一依赖 identity 校验,避免替换导致的错解析与错误终态。

5. 其他已注意到的点(非本次「严重」结论)

  • 重复 track_order:代码已显式处理「同一 oid 重复追踪则旧追踪被替代」并打日志;在未替换的常态下,WS 解析竞态窗口较小,但一旦发生替换(如重试或误用),就会触发上述错误终态。
  • result_event 永久阻塞:当前逻辑下,替换时旧 tracking 会在 track_order 里被设 CANCELED 并 result_event.set(),因此不会出现「永远不 set」的阻塞;问题主要是错误终态(CANCELED/TIMEOUT 误报),而不是死锁。
  • verify_pending_orders / shutdown:前者按 oid 查表做 HTTP 补查,后者显式传 (oid, tracking, OrderStatus.CANCELED),仅 shutdown 与监控/超时路径一样是「按引用」解析,设计上合理;verify 若在重连后使用且极少与同一 oid 的 track_order 替换重叠,风险相对可控,可与上述两处修复一并考虑是否在补查时也改为按引用(若需要可单独列任务)。

6. 修复项汇总与建议顺序

序号 问题 位置 建议修复
1 WS 解析 TOCTOU:释放锁后 _resolve(oid) 按 oid 查表,可能解析到被替换后的新 tracking,导致已成交报成取消/超时 _on_order_update_on_user_fill 释放锁后调用 _resolve(oid, tracking=tracking);扩展 _resolve 支持「tracking 非空且 status 为 None」时按引用 + _ws_status 解析
2 宽限期定时器只传 oid,存在与 1 相同的替换竞态 创建 Timer 处 定时器回调传入当前 tracking,_resolve 支持按引用解析(同上)

建议先实现 _resolve 的「WS 按引用」扩展(一次改动、两处受益),再改 _on_order_update / _on_user_fill 与 Timer 的调用方式,并补单元/集成测试覆盖:同一 oid 先 WS filled 再 track_order 替换,或先 track_order 再 WS filled 再替换,验证终态与 result_event 行为正确。

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