WebSocket 与 REST API 数据结构校验问题报告

WebSocket 与 REST API 数据结构校验问题报告

生成时间:2026-02-22
分析范围:测试网模拟交易环境下,对所有 WebSocket 频道和 REST API 返回的数据结构与代码实际解析逻辑进行全量比对
验证依据:scripts/out/fill_responses_20260222_124558.json(测试网实测数据)


目录


总体结论

级别 数量 说明
严重 (Critical) 1 直接导致核心功能静默失效
重要 (High) 2 影响正确性,有兜底但效果降级
中等 (Moderate) 2 语义错误或设计不完整
低 (Low) 1 类型注解问题,运行时不崩溃
信息 (Info) 1 架构潜在隐患

最高优先级:BUG-1 必须立即修复。userFills 频道的所有成交价数据被完全丢弃,导致订单追踪系统无法获取真实成交均价,降级为依赖 orderUpdates 的限价兜底或 HTTP 补查,影响所有限价单的成交价精度。


数据结构对照验证表

频道 / 接口 字段路径 实际数据类型 代码解析方式 结论
userFills data dict {isSnapshot, user, fills:[]} raw if isinstance(raw, list) else [raw] 解析错误
userFills data.fills[*] dict {coin, px, sz, oid, tid, ...} 被外层 dict 包裹,oid 永远为 None 数据丢失
orderUpdates data list [{order, status, statusTimestamp}] raw if isinstance(raw, list) else [raw] ✅ 正确
orderUpdates data[*].status "minTradeNtlRejected" 等枚举 仅匹配 "rejected"/"filled"/"canceled" ⚠️ 未知状态被忽略
orderUpdates data[*].order.limitPx str "956.3" _pos_float(order.get("limitPx")) ✅ 正确
orderUpdates data[*].order.origSz str "0.001" _pos_float(order.get("origSz")) ✅ 正确
user data.fills[*] dict {coin, px, sz, oid, ...} 直接列表 data.get("fills", []) 直接迭代 ✅ 正确
user data.fills[*].oid int 48913699327 OrderFilledEvent(order_id=fill.get("oid","")) 类型不匹配
user data.assetPositions[*] {position:{...}, type:"oneWayPosition"} p.get("position", p) 提取内层 ✅ 正确
user data.marginSummary.totalNtlPos 总持仓名义价值(非未实现盈亏) 赋值给 unrealized_pnl 字段 语义错误
user data.withdrawable str 可用余额 float(data.get("withdrawable", 0)) ✅ 正确
candle data.s str "BTC"(币种代码) data.get('s')f"{coin}/USDC:USDC" ✅ 正确
candle data.i str "5m"/"1h"/"4h" data.get('i') ✅ 正确
candle data.t/o/h/l/c/v int/str 时间戳和价格 _safe_int / _safe_float 转换 ✅ 正确
l2Book data.levels [[{px,sz,n},...],[{px,sz,n},...]] levels[0]levels[1] 取 bids/asks ✅ 正确(注解错误)
l2Book data.coin str "HYPE" data.get("coin") ✅ 正确
HTTP query_order_by_oid 响应顶层 {"status":"order","order":{<inner>}} 解包 raw["order"] 返回内层 ✅ 正确
HTTP query_order_by_oid <inner>.avgPx str 成交均价 _pos_float(resp.get("avgPx")) ✅ 正确
HTTP query_order_by_oid <inner>.totalSz str 总成交量 _pos_float(resp.get("totalSz")) ✅ 正确
HTTP open_orders 响应 list [{coin, oid, limitPx, sz, ...}] order.get("coin"), order.get("oid") ✅ 正确
HTTP meta().universe assetInfo.marginMode str "strictIsolated" margin_mode == "strictIsolated" ✅ 正确

BUG-1(严重)userFills 频道数据解析完全失效

文件src/trading/websocket_order_manager.py,第 392–434 行
方法_on_user_fill()

问题描述

userFills 频道推送的 WebSocket 消息中,真实成交记录嵌套在 data.fills 数组下,而非 data 本身。当前代码将整个 data dict 包裹成单元素列表后迭代,导致永远无法取到 oid 字段,所有成交记录被静默丢弃。

实测数据结构(测试网验证)

{
  "channel": "userFills",
  "data": {
    "isSnapshot": true,
    "user": "0x065a6505206d723440fc62a76cf58f5dda7c1132",
    "fills": [
      {
        "coin": "VINE",
        "px": "0.01804",
        "sz": "14288.0",
        "side": "A",
        "time": 1771726639209,
        "oid": 48913699327,
        "tid": 227144388046239,
        "fee": "0.038663",
        "closedPnl": "0.0",
        "feeToken": "USDC",
        "twapId": null
      }
    ]
  }
}

当前代码(错误)

# src/trading/websocket_order_manager.py:398-411
def _on_user_fill(self, message: dict):
    raw = message.get("data")
    # raw = {"isSnapshot": True, "user": "0x...", "fills": [...]}  ← dict,非 list
    if not raw:
        return
    fills = raw if isinstance(raw, list) else [raw]
    # fills = [{"isSnapshot": True, "user": "...", "fills": [...]}]  ← 外层 dict 被包裹
    for fill_data in fills:
        if not isinstance(fill_data, dict):
            continue
        oid = _safe_int_oid(fill_data.get("oid"))
        # fill_data.get("oid") = None  ← 外层 dict 无 oid 字段,永远返回 None
        if oid is None:
            continue  # ← 所有 fill 在此被丢弃

运行时验证

raw type: <class 'dict'>
fill_data.get("oid"): None
>>> BUG CONFIRMED: oid is None, fill is IGNORED

修复方案

# src/trading/websocket_order_manager.py:_on_user_fill()
def _on_user_fill(self, message: dict):
    raw = message.get("data")
    if not raw:
        return
    # 修复:处理嵌套结构 {"isSnapshot": ..., "fills": [...]}
    if isinstance(raw, dict):
        fills = raw.get("fills", [])
    elif isinstance(raw, list):
        fills = raw
    else:
        return
    for fill_data in fills:
        # ... 后续逻辑不变

影响范围

  • WebSocketOrderManager._on_user_fill() 完全失效
  • 所有限价单的成交均价无法通过 WS 实时累积
  • 系统降级为依赖 orderUpdates.order.limitPx 兜底(限价而非实际成交价)或 HTTP 补查
  • 加权均价计算逻辑(_accumulate_fill)从未被触发
  • tracking.fill_count 永远为 0,tracking.has_fill_price 永远为 False(直到 HTTP 兜底)

BUG-2(重要)orderUpdates 未识别的拒绝状态类型

文件src/trading/websocket_order_manager.py,第 378–387 行
方法_on_order_update()

问题描述

Hyperliquid 的 orderUpdates 推送包含多种语义为"拒绝"的 status 值(如 minTradeNtlRejected),但代码仅识别字面字符串 "rejected",其余拒绝类型均走 else: continue 分支被静默跳过,订单追踪无法及时解析。

实测数据(测试网验证)

{
  "channel": "orderUpdates",
  "data": [
    {
      "order": {
        "coin": "ETH",
        "side": "B",
        "limitPx": "956.3",
        "sz": "0.001",
        "oid": 48920367120,
        "origSz": "0.001"
      },
      "status": "minTradeNtlRejected",
      "statusTimestamp": 1771739133080
    }
  ]
}

状态映射分析

实际 status 值 代码处理结果 是否正确
"filled" FILLED → resolve
"canceled" CANCELED → resolve
"margincanceled" CANCELED → resolve
"rejected" REJECTED → resolve
"minTradeNtlRejected" else: continue被忽略
"resting" else: continue → 正常跳过 ✅(预期行为)
"open" / "pending" else: continue → 正常跳过 ✅(预期行为)

影响范围

  • 下单金额过小(低于交易所最低名义价值)时,订单追踪将等待全部超时周期(600 秒)才能通过 HTTP 兜底确认拒绝
  • 延迟 10 分钟才能释放追踪资源
  • 潜在存在的其他拒绝类型(如 "limitExceeded" 等)同样不被处理

修复方案

# src/trading/websocket_order_manager.py:_on_order_update()
elif "rejected" in status_str or status_str == "rejected":
    tracking._ws_status = OrderStatus.REJECTED
    should_resolve = True
    resolve_tracking = tracking

BUG-3(重要)OrderFilledEvent.order_id 类型不匹配

文件src/utils/websocket/enhanced_ws_manager.py,第 1414 行
方法_publish_user_events()

问题描述

OrderFilledEvent.order_id 字段类型注解为 str,但 user 频道推送的 fill 数据中 oid 字段为 int,代码未做类型转换,直接将 int 赋值给 str 字段。

代码对照

# src/events/trading_events.py:57
order_id: str = ""  # 类型注解为 str

# src/utils/websocket/enhanced_ws_manager.py:1414
event = OrderFilledEvent(
    ...
    order_id=fill.get("oid", ""),  # oid 实际为 int: 48913699327
    ...
)

实测数据

{"oid": 48913699327, "tid": 227144388046239, ...}

oidintfill.get("oid", "") 返回 int,但 order_id 字段期望 str。Python 不在赋值时强制检查,但任何做字符串处理的下游消费者(如 str.startswith()re.match()、字典键拼接等)都会在运行时抛 TypeError

修复方案

order_id=str(fill.get("oid", "")),

BUG-4(中等)BalanceChangedEvent.unrealized_pnl 语义错误

文件src/utils/websocket/enhanced_ws_manager.py,第 1439 行
方法_publish_user_events()

问题描述

将 Hyperliquid marginSummary.totalNtlPos 字段赋值给 BalanceChangedEvent.unrealized_pnl,但两者语义不同:

字段 语义
totalNtlPos 总持仓名义价值(Total Notional Position),即各仓位绝对值加总的敞口规模
unrealized_pnl(期望) 未实现盈亏,即持仓浮动盈亏

当前代码

# src/utils/websocket/enhanced_ws_manager.py:1432-1441
event = BalanceChangedEvent(
    timestamp=datetime.now(),
    source="websocket",
    available_balance=float(data.get("withdrawable", 0)),
    total_balance=float(margin.get("accountValue", 0)),
    margin_used=float(margin.get("totalMarginUsed", 0)),
    unrealized_pnl=float(margin.get("totalNtlPos", 0)),  # ← 实为总持仓规模,非盈亏
)

影响范围

当前 executor._on_balance_changed() 仅使用 event.available_balance 更新缓存,unrealized_pnl 字段未被直接消费,因此不影响现有交易逻辑。但如果未来风险监控或 PnL 报告逻辑依赖此字段,将得到错误数据。

Hyperliquid marginSummary 字段说明

{
  "accountValue": "50000.25",     // 账户总权益 = 保证金 + 未实现盈亏
  "totalMarginUsed": "10000.0",   // 已用保证金
  "totalNtlPos": "40000.25",      // 总持仓名义价值(敞口规模)
  "totalRawUsd": "9800.0"         // 原始 USD 保证金
}

未实现盈亏不在 marginSummary 中直接提供,可由 accountValue - totalRawUsd 近似计算,或从 assetPositions 各仓位汇总。

修复建议

# 方案1:使用语义更准确的字段名,或直接标注含义
unrealized_pnl=float(margin.get("totalNtlPos", 0)),  # 注意:此为总持仓规模非盈亏

# 方案2:改用 BalanceChangedEvent 的字段存放正确含义
# 需同步修改事件定义,增加 total_notional_position 字段

BUG-5(中等)OrderFilledEvent 缺少 fill_id 元数据

文件src/utils/websocket/enhanced_ws_manager.py,第 1411–1427 行
方法_publish_user_events()

问题描述

设计文档(docs/OrderFilledEvent接入订单追踪设计.md)明确要求:

发布端和消费端必须调用同一函数 _make_fill_id(fill) 生成 key,消费端强依赖 metadata["fill_id"],不自行构造备用 key。

但实际发布代码未设置 metadata["fill_id"]

# src/utils/websocket/enhanced_ws_manager.py:1411-1427
event = OrderFilledEvent(
    timestamp=datetime.now(),
    source="websocket",
    order_id=fill.get("oid", ""),
    client_order_id=fill.get("cloid"),
    symbol=fill.get("coin", ""),
    side=fill.get("side", ""),
    filled_qty=float(fill.get("sz", 0)),
    filled_price=float(fill.get("px", 0)),
    fee=float(fill.get("fee", 0)),
    closed_pnl=...,
    # ← 缺少 metadata={"fill_id": _fill_key(fill)}
)

现状与风险

当前 WebSocketOrderManager 未订阅 OrderFilledEvent(通过 EventBus),而是直接通过 handle_message() 接收 userFills 原始消息,因此目前不会因缺少 fill_id 崩溃。

但设计文档的 Phase 2(接入 EventBus 驱动的消费者)若被实现,消费者访问 event.metadata["fill_id"] 将抛 KeyError

修复方案

# 在 enhanced_ws_manager.py 顶部或工具模块中定义(与 websocket_order_manager._fill_key 统一)
def _make_fill_id(fill: dict) -> str:
    tid = fill.get("tid")
    if tid is not None:
        return f"tid:{tid}"
    raw = f"{fill.get('oid')}:{fill.get('px')}:{fill.get('sz')}:{fill.get('time')}"
    return f"hash:{hashlib.md5(raw.encode()).hexdigest()}"

# 发布时附加
event = OrderFilledEvent(
    ...
    metadata={"fill_id": _make_fill_id(fill)},
)

BUG-6(低)OrderBookUpdatedEvent 类型注解与实际数据格式不符

文件src/events/trading_events.py,第 128–129 行

问题描述

事件字段注解为 List[List[float]],但实际 Hyperliquid L2 数据格式是 List[Dict]

# src/events/trading_events.py:128-129
bids: List[List[float]] = field(default_factory=list)  # 注解: [[price, size], ...]
asks: List[List[float]] = field(default_factory=list)

实际数据格式

{
  "channel": "l2Book",
  "data": {
    "coin": "HYPE",
    "levels": [
      [{"px": "5.0", "sz": "100", "n": 1}, ...],
      [{"px": "5.1", "sz": "50",  "n": 1}, ...]
    ]
  }
}

代码实际使用方式

executor.py 中使用 dict 访问方式,与实际格式一致:

best_bid = float(bids[0]["px"]) if bids else 0.0  # ← 正确:dict 方式
best_ask = float(asks[0]["px"]) if asks else 0.0

影响范围

Python 不在运行时检查类型注解,不会崩溃。但注解误导后续开发者,若新代码按 bids[0][0](列表索引)访问价格,将在运行时抛 TypeError

修复建议

from typing import TypedDict

class L2Level(TypedDict):
    px: str
    sz: str
    n: int

bids: List[L2Level] = field(default_factory=list)
asks: List[L2Level] = field(default_factory=list)

INFO-1 WS 连接事件 source 固定为 "websocket"

文件src/utils/websocket/enhanced_ws_manager.py,第 636–648 行

现状

EnhancedWebSocketManager 发布的所有连接类事件(WebSocketConnectedEventWebSocketDisconnectedEventWebSocketReconnectedEvent)统一使用 source="websocket"

event = WebSocketReconnectedEvent(
    timestamp=datetime.now(),
    source="websocket",  # 固定值,无法区分 market/trading WS
    downtime_seconds=downtime,
)

双 WS 架构背景

设计文档(docs/双WebSocket架构设计.md)描述了 market WS(source="market")和 trading WS(source="trading")的分离架构,其中 Executor 应仅响应来自 trading WS 的重连事件触发补查。

当前实现状态

当前 realtime_kline_service_base.py 只初始化一个 ws_manager,所有订阅(K线 + L2 + 订单)共用同一 WebSocket 连接。双 WS 架构尚未在基类层面实现,因此 executor._on_websocket_reconnected() 中无 source 过滤逻辑也不会造成问题。

潜在风险

若未来真正实现双 WS 时,未能正确设置 source 参数,会导致:

  • market WS 重连也触发 verify_pending_orders()(双轮 HTTP 补查),产生不必要的 API 调用
  • 无法通过 event.source 区分事件来源,事件过滤逻辑需要重构

优先修复建议

立即修复(影响当前生产功能)

编号 文件 行号 修复说明
BUG-1 src/trading/websocket_order_manager.py 398–402 fills = raw if isinstance(raw, list) else [raw] 改为从 raw.get("fills", []) 提取
BUG-2 src/trading/websocket_order_manager.py 378 elif status_str == "rejected": 改为 elif "rejected" in status_str:
BUG-3 src/utils/websocket/enhanced_ws_manager.py 1414 fill.get("oid", "") 改为 str(fill.get("oid", ""))

建议修复(语义正确性)

编号 文件 行号 修复说明
BUG-4 src/utils/websocket/enhanced_ws_manager.py 1439 确认 totalNtlPos 的语义,或使用正确的未实现盈亏字段
BUG-5 src/utils/websocket/enhanced_ws_manager.py 1411–1427 发布 OrderFilledEvent 时附加 metadata={"fill_id": ...}

低优先级(代码规范)

编号 文件 行号 修复说明
BUG-6 src/events/trading_events.py 128–129 修正 bids/asks 类型注解为 List[L2Level]
INFO-1 src/utils/websocket/enhanced_ws_manager.py 636–648 为双 WS 架构实现时预留 source_name 参数

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