订单跟踪系统严重 Bug 分析 — 完整因果链

订单跟踪系统严重 Bug 分析 — 完整因果链

本文档基于当前代码分析,建立「输入 → 状态变化 → 调用路径 → 出错点 → 根因」的因果链,聚焦订单跟踪WebSocketOrderManager + EventBus + Executor)中的严重问题。


一、系统概览与正常因果链

1.1 输入来源

输入 来源 形态
限价单下单结果 Executor 下单 API 响应 OrderResult.order_id (int)、coin、timeout
订单状态流 交易 WS orderUpdates OrderStatusEvent(order_id=str, status, price, size)
成交流 交易 WS userFills OrderFilledEvent(order_id=str, filled_qty, filled_price, fill_id)
补查/超时 监控线程 + 重连回调 HTTP query_order_by_oid 响应

1.2 状态与调用路径(正常)

Executor._track_limit_order(order_result, coin, timeout)
  → order_result.order_id 存在且非 FILLED
  → WebSocketOrderManager.track_order(oid, coin, timeout)
      状态: _tracking[oid] = OrderTracking(PENDING), 启动 _monitor_order 线程
  → WebSocketOrderManager.wait_for_order(tracking)  阻塞在 result_event.wait()

并行:
  - EventBus: OrderStatusEvent → _on_order_status_event
  - EventBus: OrderFilledEvent → _on_order_filled_event
  - 监控线程: 2s 早期 HTTP → 等待剩余超时 → 超时后 HTTP 验证(两轮)

终态由唯一路径 _resolve(oid, tracking, status?, px?, sz?) 完成:
  - 在 _lock 内做 identity 检查、设 status、填 avg_price/filled_size、pop、cancel grace timer
  - 在 _lock 外 result_event.set() → wait_for_order 返回,调用方根据 tracking.status 判断 FILLED 与否

二、严重 Bug 因果链

Bug 1:WS 事件中 order_id 为浮点或非数字字符串时,订单永远不被 WS 解析

环节 内容
输入 交易所或中间层下发的 orderUpdates / userFills 中,oid 为浮点(如 1.0)或非数字字符串。
状态变化 EnhancedWebSocketManager._publish_order_status_events / _publish_fill_eventsorder_id=str(oid) 发布;WebSocketOrderManager._on_order_status_event / _on_order_filled_event_safe_int_oid(event.order_id) 解析。
调用路径 WS 消息 → _publish_* → EventBus.publish(OrderStatusEvent/OrderFilledEvent) → _on_order_status_event / _on_order_filled_eventoid = _safe_int_oid(event.order_id)
出错点 _safe_int_oid("1.0")_safe_int_oid("abc")ValueError/TypeError,返回 None,后续 if oid is None: return事件被静默丢弃
根因 只接受可被 int() 直接转换的字符串(整数形式);未对浮点字符串(如 "1.0")或异常格式做兼容(如先 float 再 int,或容错回退)。
后果 该订单在 WS 侧永远不会被 resolve,只能依赖监控线程超时后的 HTTP 兜底;若 HTTP 也失败或延迟,wait_for_order 会拖到超时或两轮重试后才返回。

代码位置
websocket_order_manager.py 第 55–62 行 _safe_int_oid;第 294–296、355–357 行两处 if oid is None: return


Bug 2:orderUpdates 仅处理 filled/canceled/rejected,中间状态导致长期 PENDING 直至超时

环节 内容
输入 Hyperliquid 推送的 orderUpdatesitem.status / order.status 为未显式处理的中间或别名状态(如 "partial""open" 变体、或未来新增状态)。
状态变化 _on_order_status_event 中仅对 "filled""canceled"/"margincanceled"、以及 "rejected" in status_str 做分支处理并可能设 _ws_status 或 resolve;其它 status 不写 _ws_status
调用路径 orderUpdates 消息 → _publish_order_status_events → OrderStatusEvent(status=status_str) → _on_order_status_event → 不进入任一已知分支。
出错点 订单的 _ws_status 始终为 None,WS 路径永远不会触发 resolve;监控线程只会在「早期 HTTP + 超时后两轮 HTTP」后按 TIMEOUT 或 HTTP 结果 resolve。
根因 状态机只对终态(filled/canceled/rejected)做映射,未定义「部分成交」或其它中间态的策略(例如视为仍 PENDING、或按交易所文档映射到某一终态)。
后果 订单在 WS 上已是部分成交或其它状态,但本地一直 PENDING,必须等到超时或 HTTP 查到终态才结束,延迟和用户感知差。

代码位置
websocket_order_manager.py 第 309–344 行(仅 filled / canceled / rejected 分支);enhanced_ws_manager.py 第 1426–1430 行 status 来源。


Bug 3:重连补查二次重试对「已替换」的 tracking 调用 _resolve,导致旧 tracking 永远不 set(理论边界)

环节 内容
输入 交易 WS 重连 → _on_websocket_reconnected(source="trading")→ 启动线程执行 verify_pending_orders() 两次(中间 sleep WS_RECONNECT_VERIFY_DELAY)。
状态变化 第一次循环中,部分 oid 的 _http_check(oid) 返回 None(查询失败),被加入 retry_list;在 sleep 期间,同一 oid 被再次 track_order 替换,_tracking[oid] 指向新 tracking。
调用路径 第二次循环:for (oid, tracking) in retry_list: self._http_check(oid) is Noneself._resolve(oid, tracking, OrderStatus.TIMEOUT)。此时 tracking第一次快照里的旧对象
出错点 _resolve(oid, old_tracking, TIMEOUT)current = self._tracking.get(oid) tracking,current is not tracking → 返回 False不会old_tracking 执行 result_event.set()。若旧 tracking 之前从未被 resolve(例如从未收到 WS、且第一次 HTTP 也失败),则旧 tracking 的 wait 永远阻塞
根因 verify_pending_orders 的 retry 阶段使用的是「第一次构建的 (oid, tracking) 快照」,未在二次重试前重新从 _tracking 取当前 tracking;在「同一 oid 在补查期间被重复 track」的边界下,会对着已替换掉的旧对象做 resolve。
后果 在「重复追踪 + 重连补查」叠加的罕见场景下,旧调用方的 wait_for_order(old_tracking) 可能永远不返回(若该 old 从未被 set)。当前实现中重复追踪时会在 track_order 里对 old 做 result_event.set(),因此多数情况下旧线程已返回;仅当替换发生在 verify_pending_orders 第一次失败到第二次重试之间、且旧 tracking 从未被 set 时才会暴露。

代码位置
websocket_order_manager.py 第 134–161 行 verify_pending_orders(retry_list 与第二次 for 循环);第 231–235 行 _resolve 的 identity 检查。


Bug 4:HTTP 响应结构假设过强,嵌套变化会导致成交价/量为 0

环节 内容
输入 Executor.query_order_status(oid) 返回的 dict 来自 raw["order"](Hyperliquid 内层);当前假设该内层为 {"status": "...", "order": { "limitPx", "origSz", "sz", ... }}
状态变化 WebSocketOrderManager._http_check_parse_order_response(resp)resp.get("order", {})limitPx/origSz/sz,计算 total_szavg_px;若 API 改为扁平或嵌套层级变化,inner 为空或字段不在预期位置。
调用路径 监控线程 / 重连补查 → _http_check(oid)query_order_status(oid)_parse_order_response(resp)_resolve(oid, tracking, status, avg_px, total_sz)
出错点 resp["order"] 缺失或结构变化,inner = {},则 avg_px=0total_sz=0;对 FILLED 订单 resolve 时 tracking.filled_size 若原本为 0(未收齐 userFills),则成交价和成交量写回为 0,上游 OrderResult.price/size 错误。
根因 解析逻辑与「当前」Hyperliquid 响应格式强绑定,未做防御性解析或对缺失字段的明确降级(如从 resp 顶层再尝试取 limitPx/origSz/sz)。
后果 下游依赖 order_result.price / order_result.size 的仓位、风控、通知会得到错误数值;在 API 升级或不同账户/环境返回不同结构时易触发。

代码位置
websocket_order_manager.py 第 536–572 行 _parse_order_response(尤其 550–556 行);executor.py 第 1744–1747 行 query_order_status 解包逻辑。


Bug 5:宽限期定时器使用 orderUpdates 的 size 作为 fallback,语义与文档不一致

环节 内容
输入 orderUpdates 中 order.origSz / order.sz;文档写「order.sz / order.origSz → size(sz=当前剩余量,origSz=原始委托量)」;发布时 size=float(order.get("origSz") or order.get("sz") or 0)
状态变化 _on_order_status_event 在 status= filled 且 tracking.filled_size == 0 时启动 5s 宽限期 Timer,回调 _resolve(oid, tracking, None, event.price, event.size)
调用路径 OrderStatusEvent(price=limitPx, size=origSz or sz) → 宽限期到 → _resolve(..., px=event.price, sz=event.size);在 _resolve 中若 final_status==FILLED and tracking.filled_size==0,则用 px/szavg_pricefilled_size
出错点 filled 订单,WS 侧通常 sz=0origSz=委托量,故 event.size 多为 origSz(总成交量),用作 fallback 合理;若某端实现或将来改为「仅推送 sz」且 filled 时未送 origSz,则 event.size 可能为 0 或剩余量,fallback filled_size 错误
根因 依赖「orderUpdates 在 filled 时一定提供 origSz 或可推导的 size」的隐含约定,未在代码或文档中明确「宽限期 fallback 仅当 size 表示成交量时有效」,也未对 event.size<=0 做防护。
后果 在边缘实现或协议演变下,可能出现「WS 显示成交但本地 filled_size=0、仅用 limitPx 作价」或错误成交量,影响统计与风控。

代码位置
websocket_order_manager.py 第 321–328 行(宽限期 Timer 与 resolve 参数);第 250–255 行(fallback 填充);enhanced_ws_manager.py 第 1436–1438 行 OrderStatusEvent 的 price/size 来源。


Bug 6:userFills 快照被整体跳过,重连后可能漏掉断连期间的成交

环节 内容
输入 交易 WS 重连后,Hyperliquid 可能先发 userFillsisSnapshot: true(历史成交列表),再发增量。
状态变化 _publish_fill_eventsif data.get("isSnapshot"): return整条 snapshot 不发布任何 OrderFilledEvent
调用路径 重连 → userFills 消息 → _publish_fill_events → isSnapshot 为 True → 直接 return,不发布事件。
出错点 若订单在断连期间已成交,重连后只有 snapshot 包含这笔成交、且无后续增量,则 WebSocketOrderManager 永远收不到该笔 fill;仅当 orderUpdates 也推送了 filled 时,会走宽限期 5s 后用 limitPx/origSz 作 fallback resolve,真实成交价依赖 HTTP 或 fallback。若 orderUpdates 也因重连未补发,则只能靠 verify_pending_orders 的 HTTP 补查拿到终态。
根因 为避免「连接建立时历史成交重复触发」而跳过 snapshot,但未区分「连接建立时全量快照」与「重连后含断连期间成交的快照」,导致重连后的 snapshot 也被跳过。
后果 断连期间成交的订单,可能缺少 userFills 的精确成交价,只能依赖 orderUpdates 的 fallback 或 HTTP;在极端情况下若两者都缺失或延迟,会拖到超时或补查才结束。

代码位置
enhanced_ws_manager.py 第 1449–1450 行 if data or data.get("isSnapshot"): return;第 1442–1448 行注释说明 isSnapshot 用途。


三、因果链汇总表

Bug 输入 状态/路径 出错点 根因
1 WS oid 浮点/非数字 EventBus → _safe_int_oid → None 事件静默丢弃 只接受 int 可解析的 order_id
2 orderUpdates 未知 status 不进入 filled/canceled/rejected _ws_status 永不设 未处理中间/未知状态
3 重连补查 + 同 oid 重复 track retry_list 存旧 tracking,第二次 resolve(oid, old) identity 失败,old 不 set 重试未用当前 _tracking 引用
4 HTTP 响应结构变化 _parse_order_response 取 resp["order"] inner 空 → avg_px/total_sz=0 强依赖当前 API 嵌套结构
5 orderUpdates size 语义 宽限期 resolve(..., event.size) event.size 非成交量时错误 未明确并防护 size 语义
6 userFills isSnapshot 整条 snapshot return 断连期间成交可能无 fill 事件 快照一律跳过,未区分重连快照

四、建议修复优先级

  1. P0:Bug 1 — 在 _safe_int_oid 或事件处理侧兼容浮点/异常 order_id,避免整单 WS 路径失效。
  2. P0:Bug 4 — 对 _parse_order_response 做防御性解析(如兼容 resp 顶层字段),并对缺失 avg_px/total_sz 做明确降级或告警。
  3. P1:Bug 2 — 明确中间状态策略(如 partial → 仍 PENDING,或按交易所文档映射)。
  4. P1:Bug 6 — 区分「首次连接全量快照」与「重连快照」,或对重连后的 userFills 做一次按 oid 的补全而非整条跳过。
  5. P2:Bug 5 — 在宽限期 fallback 处校验 event.size 语义,或仅在 size>0 时写入 filled_size。
  6. P2:Bug 3 — 二次重试前从 _tracking 重新取当前 tracking 再 resolve,避免对已替换对象操作。

以上为当前系统订单跟踪的严重 bug 与完整因果链(输入 → 状态变化 → 调用路径 → 出错点 → 根因)。

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