双WebSocket架构设计bug1

Bug 3:限价单跟丢 — 孤儿仓位误报 + 开仓失败误判

一、现象还原

测试网告警序列(2026-02-22 22:30–22:40):

时间 告警 说明
22:30:00 📤 限价单已挂出: ICP 订单 48939226203,142.24 ICP @ 2.1078
~22:31:00 🎴 真孤儿仓位收纳: ICP/USDC:USDC 来源: sync,大小: 12.19,数据库无记录
~22:40:00 ❌ 限价单timeout: ICP 错误: timeout,订单 ID: 48939226203
~22:40:00 🚨 开仓失败: ICP/USDC:USDC|APE/USDC:USDC 订单状态: resting,Leg A 状态: resting

Hyperliquid 交易历史实际成交记录:

22:30:03  ICP Open Long  12.19 ICP
22:33:00  ICP Open Long  19.89 ICP
22:33:11  ICP Open Long   8.11 ICP
22:33:15  ICP Open Long   7.88 ICP
22:33:51  ICP Open Long   8.36 ICP
22:35:59  ICP Open Long   7.92 ICP
22:35:59  ICP Open Long   7.74 ICP
─────────────────────────────────
合计成交:72.09 ICP(成交率 50.7%)
22:40:03  订单 Canceled,剩余 70.15 取消

矛盾:72.09 ICP 实际成交,但系统报告「开仓失败」且产生「孤儿仓位」告警。


二、完整因果链

因果链 A:孤儿仓位误报(次要 Bug)

输入
  └─ 22:30:03 交易所出现 12.19 ICP 仓位(第一笔部分成交)

状态
  ├─ exchange_positions["ICP"] = {szi: 12.19}
  ├─ executor._ws_order_manager._tracking = {48939226203: OrderTracking(PENDING)}
  └─ position_manager._positions = {}          ← 内存为空(订单未完成)
     position_manager._opening_pairs = {("ICP/USDC:USDC","")}

调用路径
  sync_from_exchange()                          ← 定时任务触发(约每分钟一次)
    └─ memory_coins = {}                        ← _positions 为空
    └─ _collect_orphan_candidates(
           exchange_coins, memory_coins={}
       )
         for coin="ICP":
           "ICP" in memory_coins → False        ← 不在内存,继续
           # ❌ 没有检查 _ws_order_manager._tracking 是否有该币 PENDING 订单
           candidates["ICP"] = {szi:12.19}      ← 误判为孤儿
    └─ _detect_and_adopt_orphans(candidates)
         → _positions[("ICP/USDC:USDC","")] = PairPosition(size=12.19)
         → 写 DB,发告警

出错点
  position_manager.py:_collect_orphan_candidates()
  只检查 memory_coins,不检查 ws_order_manager 的活跃追踪状态

根因
  PositionManager 不感知 WebSocketOrderManager 的追踪状态。
  限价单 600s 等待窗口内,部分成交产生的交易所仓位对
  position_manager 完全不可见,被当作外部孤儿处理。

因果链 B:开仓失败误判(主要 Bug)

输入
  └─ 22:40:00 tracking.timeout_seconds=600 到期

第一段:超时触发
  _monitor_order_inner()
    → HTTP check → order 状态 "open"(订单还在挂)
    → _resolve(oid, tracking, TIMEOUT)
    → tracking.status = TIMEOUT,result_event.set()

  _track_limit_order()
    wait_for_order() 解阻塞 → 返回 False
    order_result.status 仍为 "resting"          ← 从未更新
    发告警:❌ 限价单timeout: ICP
    return False

第二段:limit_open 超时处理
  leg_a_filled = False
  → _cancel_order("ICP", 48939226203)           ← 撤单,70.15 剩余取消
  → _check_order_after_cancel(
        coin="ICP", oid=48939226203, expected_side="buy"
    )

    # Hyperliquid query_order_by_oid 原始响应:
    # raw = {
    #   "status": "order",
    #   "order": {
    #     "order": {"sz": "70.15", "limitPx": "2.1078", ...},  ← sz=剩余量
    #     "status": "canceled",
    #     "statusTimestamp": 1740232803000
    #   }
    # }
    # query_order_status() 解包后返回 raw["order"]:
    # resp = {"order":{sz:"70.15",...}, "status":"canceled", ...}

    raw_status = "canceled"
    total_sz = _safe_pos_float(
        resp.get("totalSz")     → None(字段不存在)
        or resp.get("total_sz") → None
    ) = 0.0
    avg_px = 0.0

    if total_sz > 0: ...    ← False,跳过
    if total_sz > 0: ...    ← False,跳过

    # ❌ 关键出错点(executor.py:797-798):
    logger.info("撤单后订单未成交")
    return 0.0, 0.0         ← try 块内提前 return,降级路径被绕过!

  except:                   ← 无异常,不触发
    ...
  # 降级路径(_get_actual_position_size)永远不执行

  actual_filled = 0.0
  → result.leg_a.success = False
  → result.leg_a.error_message = "限价单超时未成交(600s)"
  → return result(leg_a.status 仍为 "resting")

第三段:_open_position_inner 报失败
  order_result.leg_a.success = False
  → 发告警:🚨 开仓失败,订单状态: resting
  → 保存 ERROR 状态仓位到 DB,返回 None

出错点
  executor.py:797-798,_check_order_after_cancel()

根因
  Hyperliquid HTTP 订单查询(query_order_by_oid)对已撤销/部分成交订单的
  响应结构中:
    sz      = 剩余未成交量(70.15),非累计成交量
    totalSz = 字段不存在(totalSz 仅出现在 WS orderUpdates 推送消息中)
    avgPx   = 不存在(或 0)

  代码将 totalSz=0 等价于"无成交",并在 try 块内 return 0.0,0.0,
  绕过了 except 块后的降级路径 _get_actual_position_size()。
  72.09 ICP 的真实成交被完全忽略。

两个 Bug 的关系

Hyperliquid HTTP 订单查询
  canceled 响应 = {sz:剩余量}  ≠  WS 推送 {totalSz:成交量}
                      │
          ┌───────────┴───────────────┐
          │ Bug A(次要)              │ Bug B(主要)
          │ _collect_orphan_          │ _check_order_after_cancel
          │   candidates()            │ totalSz=0 → return 0.0,0.0
          │ 不感知活跃限价单追踪        │ 降级路径被 try 块绕过
          ▼                           ▼
  部分成交 → 孤儿告警              72.09 成交 → 开仓失败告警
  _positions 写入错误记录          leg_a.status 永久 resting

三、修复方案

Fix 1(主要)— src/trading/executor.py

位置_check_order_after_cancel(),第 769–804 行

改动:移除 try 块内的 return 0.0, 0.0,改为 fall-through 到仓位降级查询。

# 修复前
if total_sz > 0: return total_sz, avg_px
if total_sz > 0: return total_sz, avg_px
logger.info(f"撤单后订单未成交: {coin} oid={oid} status={raw_status}")
return 0.0, 0.0                      # ← 问题所在

# 修复后
avg_px_hint = 0.0
try:
    resp = self.query_order_status(oid)
    if isinstance(resp, dict):
        raw_status = (resp.get("status") or "").lower()
        total_sz = self._safe_pos_float(resp.get("totalSz") or resp.get("total_sz"))
        avg_px   = self._safe_pos_float(resp.get("avgPx")   or resp.get("avg_px"))
        if total_sz > 0:
            logger.info(
                f"撤单后订单成交: {coin} oid={oid} | "
                f"status={raw_status} sz={total_sz} px={avg_px}"
            )
            return total_sz, avg_px
        # totalSz 缺失(Hyperliquid canceled 订单 HTTP 响应无此字段)
        # 不能据此判断无成交,降级仓位查询
        avg_px_hint = avg_px
        logger.info(
            f"撤单后订单 totalSz 缺失({raw_status}),降级仓位查询: "
            f"{coin} oid={oid}"
        )
except Exception as e:
    logger.warning(f"撤单后订单查询失败,降级到仓位查询: {coin} oid={oid} | {e}")

# 降级:查仓位(totalSz 缺失 或 查询异常 均走此路径)
actual = self._get_actual_position_size(
    coin, expected_side=expected_side, force_refresh=True
)
return actual, avg_px_hint

关键变化

  • 引入 avg_px_hint 保留 HTTP 中可能存在的均价(供后续回填)
  • 移除 return 0.0, 0.0totalSz=0 时 fall-through 到仓位查询
  • 降级路径(_get_actual_position_size)现在对「totalSz 缺失」和「查询异常」均可达

Fix 2(次要)— src/trading/websocket_order_manager.py

位置WebSocketOrderManager 类末尾新增方法

def get_pending_coins(self) -> set[str]:
    """返回当前有 PENDING 限价单追踪的币种集合(供孤儿检测排除用)"""
    with self._lock:
        return {
            t.coin
            for t in self._tracking.values()
            if t.status == OrderStatus.PENDING
        }

Fix 3(次要)— src/trading/position_manager.py

修改点 A_collect_orphan_candidates()(第 1485 行),增加参数并跳过有活跃追踪的币种

def _collect_orphan_candidates(
    self,
    exchange_coins: dict,
    memory_coins: set[str],
    pending_order_coins: set[str] | None = None,   # 新增
) -> dict:
    candidates = {}
    for coin, ex_pos in exchange_coins.items():
        if coin in memory_coins:
            continue
        # 跳过有活跃 PENDING 限价单的币种,防止部分成交误判为孤儿
        if pending_order_coins and coin in pending_order_coins:
            logger.debug(f"孤儿检测跳过 {coin}:存在活跃限价单追踪")
            continue
        if self._config.is_close_disabled(coin_to_symbol(coin)):
            continue
        szi = float(ex_pos.get("szi", 0))
        if szi == 0:
            continue
        candidates[coin] = ex_pos
    return candidates

修改点 Brecover_from_exchange()sync_from_exchange() 中的两处调用,
分别在第 1069–1070 行和第 1229–1230 行,更新为:

# 获取活跃限价单追踪的币种(如果 ws_order_manager 已初始化)
_ws_mgr = getattr(self._executor, "_ws_order_manager", None)
pending_order_coins = _ws_mgr.get_pending_coins() if _ws_mgr else set()

orphan_candidates = self._collect_orphan_candidates(
    exchange_coins, memory_coins, pending_order_coins
)

四、修改文件清单

文件 函数 行号(约) 改动类型
src/trading/executor.py _check_order_after_cancel 769–804 逻辑修改
src/trading/websocket_order_manager.py 新增 get_pending_coins 类末尾 新增方法
src/trading/position_manager.py _collect_orphan_candidates 1485–1501 签名+逻辑
src/trading/position_manager.py recover_from_exchange 调用点 ~1069 更新调用
src/trading/position_manager.py sync_from_exchange 调用点 ~1229 更新调用

五、验证方法

  1. Fix 1 验证:测试网下一笔大限价单,临时调低 limit_order_timeout(如 30s),
    等待部分成交后触发超时撤单,确认日志出现「降级仓位查询」而非「订单未成交」,
    且不再产生「开仓失败」告警。

  2. Fix 2/3 验证:下一笔限价单并等待部分成交,在 600s 等待窗口内观察
    sync_from_exchange 是否还触发「孤儿仓位收纳」告警。

  3. 回归检查:确认真实孤儿仓位(无追踪订单、无 DB 记录)仍能被正确收纳,
    get_pending_coins() 返回空集合时行为与修改前完全一致。

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