WebSocket 独立频道迁移设计文档

WebSocket 独立频道迁移设计文档

版本: v1.0
日期: 2026-03-09
适用项目: Trading-in-websocket(全币种协整配对量化交易系统)


目录

  1. 背景与目标
  2. 技术调研
  3. 架构对比
  4. 迁移方案设计
  5. 文件改动清单
  6. 核心实现细节
  7. 数据结构变化
  8. 缓存策略
  9. 验证方案与结果
  10. 风险与注意事项

1. 背景与目标

1.1 现状(迁移前)

系统使用 webData2 聚合频道接收交易 WebSocket 数据。该频道将 clearinghouseStatespotStateopenOrders 等多类数据打包推送,存在以下问题:

问题 描述
冗余数据 webData2 推送了大量不需要的字段(如 openOrders),浪费带宽和解析开销
统一账户受限 统一账户(Portfolio Margin)的余额只能通过 REST API 轮询获取,无法利用 WS 实时推送
HIP-3 无法 per-DEX 订阅 webData2 不支持 dex 参数,无法订阅 HIP-3 DEX 的独立持仓推送
关注点耦合 持仓、余额、订单状态混在同一频道,解析逻辑复杂

1.2 目标

webData2 聚合频道完全替换为独立频道(clearinghouseStatespotState),实现:

维度 目标
关注点分离 每个频道只推送一类数据,解析逻辑清晰
统一账户 WS 缓存 spotState 独立频道使统一账户余额也能 WS 缓存优先、HTTP 降级
HIP-3 per-DEX 订阅 clearinghouseState 支持 dex 参数,实现逐 DEX 实时持仓推送
零冗余 只订阅需要的频道,去除 openOrders 等无用数据

2. 技术调研

2.1 Hyperliquid WS 频道体系

Hyperliquid 提供两类用户数据频道:

聚合频道(Legacy):
  webData2  →  打包推送 clearinghouseState + spotState + openOrders + ...

独立频道(Recommended):
  clearinghouseState  →  持仓 + 保证金 + withdrawable
  spotState           →  Spot 层余额(统一账户核心)
  orderUpdates        →  订单状态变更
  userFills           →  成交记录

2.2 独立频道特性

特性 webData2 独立频道
数据范围 全量打包 按类型拆分
HIP-3 per-DEX 不支持 clearinghouseState 支持 dex 参数
统一账户 spotState 包含但与 perp 混在一起 独立频道,结构清晰
推送频率 ~4.6-4.8s ~4.6-4.8s(实测一致)
数据嵌套 直接是 state 数据 外层多一层包装(需 unwrap)

2.3 主网验证结果(2026-03-09)

通过 src/scripts/verify_independent_ws_channels.py 同时订阅两种频道,实测:

  • clearinghouseState:独立频道与 webData2 数据完全一致(键、值均匹配)
  • spotState:独立频道包含零余额代币(webData2 会过滤掉),属于无害差异
  • 推送频率:独立频道与 webData2 推送比 = 1.00,完全同步
  • HIP-3 per-DEXclearinghouseStatedex 参数正常推送

结论:独立频道可以安全替代 webData2


3. 架构对比

3.1 迁移前(webData2 架构)

交易 WS ──→ webData2 ──→ _publish_user_events()
                          ├─ clearinghouseState → PositionUpdatedEvent + BalanceChangedEvent
                          └─ (spotState 数据被忽略)

统一账户余额 ──→ 定时 HTTP 轮询(无 WS 缓存)
HIP-3 持仓    ──→ 定时 HTTP 轮询(无 WS per-DEX 缓存)

3.2 迁移后(独立频道架构)

交易 WS ──→ clearinghouseState ──→ _publish_clearinghouse_events()
            │                       ├─ PositionUpdatedEvent(dex="")
            │                       └─ BalanceChangedEvent
            │
            ├─ clearinghouseState(dex=xyz) ──→ _publish_clearinghouse_events()
            │                                  └─ PositionUpdatedEvent(dex="xyz")
            │
            ├─ spotState ──→ _publish_spot_state_event()
            │                └─ SpotStateUpdatedEvent
            │
            ├─ orderUpdates ──→ _publish_order_status_events()
            │                   └─ OrderStatusEvent
            │
            └─ userFills ──→ _publish_fill_events()
                             └─ OrderFilledEvent

Executor 订阅:
  PositionUpdatedEvent  → 按 dex 字段分流(主 perp / HIP-3 per-DEX 缓存)
  BalanceChangedEvent   → 更新余额缓存(仅主 perp)
  SpotStateUpdatedEvent → 更新 Spot 余额缓存(统一账户核心)
  WebSocketReconnectedEvent(source="trading") → 清空全部缓存 + 补查订单

4. 迁移方案设计

4.1 迁移策略

采用 一步替换 策略(非渐进式),原因:

  1. 独立频道已通过主网实测验证,数据一致性有保障
  2. webData2 和独立频道不应同时订阅(避免重复事件)
  3. 改动范围可控(7 个文件),风险可接受

4.2 实施步骤

Step 1: 主网验证(已完成)
  └─ verify_independent_ws_channels.py 对比 webData2 vs 独立频道

Step 2: 核心替换(已完成)
  ├─ 新增 SpotStateUpdatedEvent 事件类
  ├─ WS 消息路由替换(enhanced_ws_manager.py)
  ├─ 订阅列表重构(realtime_kline_service_base.py)
  └─ 缓存体系重构(executor.py)

Step 3: 辅助更新(已完成)
  ├─ 事件导出(events/__init__.py)
  └─ 配置注释(config.py)

5. 文件改动清单

文件 改动类型 说明
src/events/trading_events.py 新增+修改 新增 SpotStateUpdatedEvent 事件类;PositionUpdatedEvent 新增 dex 字段
src/events/__init__.py 修改 导出 SpotStateUpdatedEvent;更新事件流文档
src/utils/websocket/enhanced_ws_manager.py 修改 替换 webData2 处理为 clearinghouseState/spotState 独立频道处理
src/services/realtime_kline_service_base.py 修改 订阅列表从 webData2 改为独立频道 + HIP-3 per-DEX
src/trading/executor.py 修改 缓存体系重构:per-DEX 缓存、spotState 缓存、统一账户 WS 优先
src/config.py 修改 注释更新
src/scripts/verify_independent_ws_channels.py 新增 主网验证脚本(对比两种频道数据一致性)

6. 核心实现细节

6.1 事件层:新增 SpotStateUpdatedEvent

@dataclass(kw_only=True)
class SpotStateUpdatedEvent(Event):
    """Spot 层余额更新事件(统一账户核心数据源)"""
    balances: List[Dict] = field(default_factory=list)

    def __post_init__(self):
        super().__post_init__()
        self.priority = EventPriority.HIGH

6.2 事件层:PositionUpdatedEvent 新增 dex 字段

class PositionUpdatedEvent(Event):
    positions: List[Dict] = field(default_factory=list)
    account_value: float = 0.0
    margin_summary: Dict = field(default_factory=dict)
    dex: str = ""  # 空字符串=主 perp,非空=HIP-3 DEX 名称

6.3 WS 消息路由(enhanced_ws_manager.py)

替换前(单个 webData2 处理函数):

elif channel == "webData2":
    self._publish_user_events(data)

替换后(两个独立频道处理函数):

elif channel == "clearinghouseState":
    self._publish_clearinghouse_events(data)
elif channel == "spotState":
    self._publish_spot_state_event(data)

6.4 独立频道数据嵌套处理

独立频道数据比 webData2 多一层包装,需要 unwrap:

# clearinghouseState 独立频道推送格式:
# {"dex": "", "user": "0x...", "clearinghouseState": {实际数据}}
state = data.get("clearinghouseState", data)
dex = data.get("dex", "")

# spotState 独立频道推送格式:
# {"user": "0x...", "spotState": {"balances": [...]}}
inner = data.get("spotState", {})
balances = inner.get("balances", [])

6.5 订阅列表重构(realtime_kline_service_base.py)

def _build_trading_subscriptions(self) -> List[Dict]:
    subs = [
        {"type": "orderUpdates",       "user": user_address},
        {"type": "userFills",          "user": user_address},
        {"type": "clearinghouseState", "user": user_address},       # 主 perp
        {"type": "spotState",          "user": user_address},       # Spot 层
    ]
    # HIP-3 per-DEX 订阅
    for dex in hip3_dexes:
        subs.append({"type": "clearinghouseState", "user": user_address, "dex": dex})
    return subs

6.6 Executor 事件处理分流

def _on_position_updated(self, event: PositionUpdatedEvent):
    with self._cache_lock:
        if not event.dex:
            # 主 perp → 更新主缓存
            self._cached_positions = event.positions
            self._cached_account_value = event.account_value
            self._position_cache_ts = event.timestamp.timestamp()
        else:
            # HIP-3 → 更新 per-DEX 缓存
            self._hip3_dex_cache[event.dex] = {
                "account_value": event.account_value,
                "positions": event.positions,
                "ts": event.timestamp.timestamp(),
            }

7. 数据结构变化

7.1 Executor 缓存字段

移除

字段 原用途
_cached_hip3_account_value: float HIP-3 总账户价值(HTTP 轮询)
_hip3_account_value_ts: float HIP-3 缓存时间戳

新增

字段 用途
_cached_spot_balances: list[dict] spotState WS 推送的 Spot 余额列表
_spot_cache_ts: float Spot 缓存时间戳
_hip3_dex_cache: dict[str, dict] per-DEX WS 缓存 {dex: {"account_value", "positions", "ts"}}

7.2 事件新增

事件 来源频道 用途
SpotStateUpdatedEvent spotState 独立频道 Spot 层余额更新,统一账户核心

7.3 事件修改

事件 变更 说明
PositionUpdatedEvent 新增 dex: str = "" 字段 区分主 perp 和 HIP-3 DEX 持仓

8. 缓存策略

8.1 统一账户余额查询(get_account_value / get_available_balance)

迁移前: HTTP 轮询 → 解析 spotState → 返回
迁移后: spotState WS 缓存 → (TTL 有效 && WS 在线) → 返回
                            → (TTL 过期 || WS 断连) → HTTP 降级

8.2 普通账户余额/持仓查询

迁移前: webData2 WS 缓存 → (TTL 有效 && WS 在线) → 返回
迁移后: clearinghouseState WS 缓存 → (TTL 有效 && WS 在线) → 返回
        (逻辑不变,数据源从 webData2 切换到独立频道)

8.3 HIP-3 持仓/价值查询

迁移前: HTTP 轮询(每次 get_account_value 调用时查所有 DEX)
迁移后: per-DEX WS 缓存 → (所有 DEX 缓存有效 && WS 在线) → 返回
                         → (任一 DEX 过期 || WS 断连) → HTTP 逐 DEX 降级

8.4 缓存失效场景

场景 行为
下单/平仓成功 _invalidate_position_cache() 清零 position_cache_tsspot_cache_tship3_dex_cache
交易 WS 重连 _on_websocket_reconnected() 清空全部缓存字段 + 补查未完成订单
行情 WS 重连 忽略(行情重连不影响交易缓存)

9. 验证方案与结果

9.1 验证脚本

src/scripts/verify_independent_ws_channels.py 同时订阅 webData2 + 独立频道,进行:

  1. 数据结构对比:键集合、值精度、嵌套层级
  2. 数值一致性对比:accountValue、withdrawable、持仓 szi/entryPx
  3. 推送频率对比:平均间隔、推送比

用法:

# 基础验证(60 秒)
python -m src.scripts.verify_independent_ws_channels --duration 60

# 包含 HIP-3 DEX 验证
python -m src.scripts.verify_independent_ws_channels --hip3-dex --duration 120

# 导出原始数据
python -m src.scripts.verify_independent_ws_channels --export-raw --duration 60

9.2 主网验证结果

clearinghouseState:
  ✅ 所有键完全一致
  ✅ 所有数值完全匹配
  ✅ 推送频率一致(ratio=1.00, avg≈4.7s)

spotState:
  ✅ 核心数据一致(balances 结构、coin/total/hold 值)
  ℹ️ 独立频道包含零余额代币(webData2 过滤掉了),无害差异
  ℹ️ 独立频道多出 isPortfolioMargin 和 tokenToAvailableAfterMaintenance 字段
  ✅ 推送频率一致

HIP-3 per-DEX clearinghouseState:
  ✅ 带 dex 参数的订阅正常推送
  ✅ 数据结构与主 perp 一致

9.3 编译验证

所有改动文件均通过 py_compile 验证:

python -m py_compile src/events/trading_events.py      # OK
python -m py_compile src/events/__init__.py             # OK
python -m py_compile src/utils/websocket/enhanced_ws_manager.py  # OK
python -m py_compile src/services/realtime_kline_service_base.py # OK
python -m py_compile src/trading/executor.py             # OK

10. 风险与注意事项

10.1 已知风险

风险 等级 缓解措施
独立频道 API 未来变更 Hyperliquid 官方推荐使用独立频道,webData2 是 legacy
HIP-3 DEX 数量增长导致订阅数增加 WS 1000 订阅上限远大于当前需求;仅订阅 cointegrated_pairs 中的 DEX
WS 断连期间数据丢失 已有断连检测 + HTTP 降级机制;重连后清空缓存 + 补查订单

10.2 注意事项

  1. 数据嵌套差异:独立频道数据多一层包装(data.clearinghouseState / data.spotState),解析时需要 unwrap
  2. spotState 零余额代币:独立频道会推送零余额代币,计算 sum(total) 时不会影响结果(值为 0)
  3. HIP-3 BalanceChangedEvent:仅主 perp 发布 BalanceChangedEvent,HIP-3 DEX 的 withdrawable 不直接使用(通过 _hip3_dex_cacheaccount_value 代替)
  4. 验证脚本保留verify_independent_ws_channels.py 保留在 src/scripts/ 中,可用于未来回归验证

10.3 回滚方案

若需回滚到 webData2

  1. 订阅列表恢复为 {"type": "webData2", "user": user_address}
  2. _cache_latest_data 恢复 webData2 分支
  3. 恢复 _publish_user_events() 函数
  4. Executor 恢复旧缓存字段

由于改动集中在 7 个文件,git revert 即可完成回滚。

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