CCXT替换为Hyperliquid官方SDK存在的BUG3

Hyperliquid SDK K 线请求严重 BUG — 第六轮因果链分析(实时服务路径)

分析日期: 2026-02-15
涉及文件: hyperliquid_candles.py, kline_data_filler.py, realtime_kline_service_base.py
与 BUG5 的关系: BUG5 从数据自愈路径(启动修复)分析了同一根因;本轮从实时分析服务路径(30 线程运行时)分析其在生产主路径上的表现


概览

BUG5 揭示了 hyperliquid_candles.py 封装层的 limit→endTime 语义翻译错误。本轮分析聚焦于该 BUG 在实时分析主路径上的触发方式和影响范围。

关键发现:实时分析服务的 30 个工作线程在检测到数据间隙时,优先调用有缺陷的 fill_missing_data_precise(而非正确的 fill_missing_data),且全局互斥锁将所有数据补充请求串行化,在多间隙场景下导致数据静默截断 + 分析管线阻塞的复合故障。


BUG 表现:实时分析服务中的完整因果链

1. 输入 (Input)

WebSocket 推送 5m K 线消息,触发分析工作线程。工作线程从数据库查询三个周期(5m/1h/4h)的历史 K 线,validate_continuity() 检测到多段不连续的数据间隙(例如 WebSocket 短暂断连两次)。

典型触发场景(5m 周期):

数据库中的 K 线时间线:
  ┌──────────┐  ┌─ 间隙1 ─┐  ┌────────────────┐  ┌─ 间隙2 ─┐  ┌──────┐
  │ 00:00~02:00 │  02:05~02:25  │ 02:30~05:00     │  05:05~05:25  │ 05:30~ │
  │  连续数据   │  5个缺失点   │   连续数据       │  5个缺失点   │ 连续   │
  └──────────┘  └──────────┘  └────────────────┘  └──────────┘  └──────┘

validate_continuity() 返回:
  missing_timestamps = [02:05, 02:10, 02:15, 02:20, 02:25,
                        05:05, 05:10, 05:15, 05:20, 05:25]
  共 10 个缺失时间点,跨越 200 分钟

2. 状态变化 (State Change)

_fetch_and_validate_price_data()realtime_kline_service_base.py:990-1006)检测到数据不连续或不充足:

# realtime_kline_service_base.py:1003-1006
need_refill = (
    not base_continuous or not alt_continuous or    # validate_continuity 失败
    not base_sufficient or not alt_sufficient       # validate_window_length 失败
)

need_refill = True,进入数据补充分支。

关键路由决策realtime_kline_service_base.py:1017-1023):

if base_missing:                                     # missing_timestamps 非空
    base_filled = self.data_filler.fill_missing_data_precise(   # ← 优先走精准补充
        self.base_symbol, tf, base_missing
    )
else:                                                # missing_timestamps 为空
    base_filled = self.data_filler.fill_missing_data(           # ← 仅在无间隙时走范围补充
        self.base_symbol, tf, query_start_time, end_time
    )

路由逻辑的隐患:只要 validate_continuity() 检测到任何间隙(missing_timestamps 非空),就走 fill_missing_data_precise(有 BUG 的路径)。而 validate_continuity() 在正常运行中几乎总会检测到间隙(WebSocket 短暂断连、网络抖动),因此生产环境几乎总是走错误路径

3. 调用路径 (Call Path)

_analysis_worker()                                              [base.py:843]
  → _analyze_and_alert(symbol, timeframe, kline_time)           [base.py:1288]
    → _fetch_and_validate_price_data(symbol, timeframe)         [base.py:946]
      → validate_continuity(alt_klines, tf)                     [filler.py:113]
        → 返回 missing_timestamps (10个点, 跨越 200min)
      → fill_missing_data_precise(symbol, tf, missing_timestamps)  [filler.py:199]
        → 计算参数:
            min_ts     = 02:05
            max_ts     = 05:25
            start_time = 02:00  (min_ts - 5min)
            end_time   = 05:30  (max_ts + 5min) ← ✅ 正确,但仅用于日志!
            limit      = max(10 + 2, 10) = 12
            since      = 02:00 的毫秒时间戳

        → _fetch_ohlcv_with_retry(symbol, tf, since=since, limit=12)     [filler.py:94]
          → fetch_candles_with_retry(info, symbol, interval, since_ms, limit=12)
                                                                         [candles.py:139]
            → end_ms = since_ms + min(12, 1500) × 300000ms              [candles.py:151]
                     = 02:00 + 12 × 5min
                     = 02:00 + 60min
                     = 03:00                      ← ❌ 只请求到 03:00!

            → fetch_candles(info, symbol, interval, since_ms=02:00, end_ms=03:00)
              → with _candles_lock:               ← ❌ 全局锁在 API 调用期间持有
                  → info.candles_snapshot("BTC", "5m", 02:00ms, 03:00ms)

4. 出错点 (Error Point)

出错点 A — 请求范围截断(hyperliquid_candles.py:151):

end_ms = since_ms + min(limit, api_limit) * interval_ms
#      = 02:00    + 12 × 5min
#      = 03:00                    ← 实际需要 05:30,差了 150 分钟!

API 只请求了 [02:00, 03:00] 范围的 K 线数据。间隙 2(05:05~05:25)完全不在请求范围内

出错点 Bend_time 被丢弃(kline_data_filler.py:221-234):

# 正确的 end_time 只被用于日志打印,未传递给 API 调用
logger.debug(
    f"精准补充K线数据 | {symbol} @ {timeframe} | "
    f"缺失: {len(missing_timestamps)} | 拉取: {limit} 条 | "
    f"范围: {start_time.isoformat()} ~ {end_time.isoformat()}"  # ← end_time 仅在此出现
)

# API 调用只用了 since 和 limit,end_time 被完全忽略
ohlcv = self._fetch_ohlcv_with_retry(
    symbol, timeframe,
    since=since,                       # ← 只传了 since
    limit=min(limit, self.API_LIMIT),  # ← 只传了 limit,end_time 丢失!
)

出错点 C — 全局锁串行化(hyperliquid_candles.py:107-121):

with _candles_lock:                          # 获取全局锁
    if elapsed < min_interval:
        time.sleep(min_interval - elapsed)   # 锁内等待 2.5s
    raw = info.candles_snapshot(...)          # 锁内 HTTP 请求 1~30s
    globals()["_last_candles_request_time"] = time.monotonic()
                                             # 锁直到 HTTP 响应后才释放

5. 根因 (Root Cause)

三层接口语义不匹配

┌─────────────────────────────────────────────────────────────────┐
│  Hyperliquid SDK (底层)                                         │
│  candles_snapshot(name, interval, startTime, endTime)            │
│  语义: "返回 [startTime, endTime] 时间范围内的所有 K 线"          │
│  → 原生时间范围接口,无 limit 概念                                │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             ↓ 封装层引入 limit 概念
┌─────────────────────────────────────────────────────────────────┐
│  封装层 fetch_candles_with_retry (中间层)                        │
│  参数: (since_ms, limit)                                         │
│  转换: end_ms = since_ms + limit × interval_ms                   │
│  → 将 limit(数量) 翻译为 endTime(时间)                           │
│  → 这个翻译只在"连续请求 limit 根 K 线"时正确                     │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             ↓ 调用方传入错误的 limit
┌─────────────────────────────────────────────────────────────────┐
│  fill_missing_data_precise (调用方)                               │
│  计算: limit = len(missing_timestamps) + 2                        │
│  含义: "缺失点数量 + 2"                                          │
│  → 缺失点数量 ≠ 时间跨度!                                       │
│  → 10 个缺失点可能跨越 200 分钟(40 个 interval)                   │
│  → 但 limit 只有 12,覆盖 60 分钟                                │
│  → end_time 被正确计算但从未使用!                                │
└─────────────────────────────────────────────────────────────────┘

等式 len(missing_timestamps) ≈ time_span / interval 仅在单段连续间隙时成立

场景 缺失点数 时间跨度 limit (n+2) 覆盖范围 结果
单段连续间隙 02:05~02:25 5 25min 7 35min ✅ 足够
两段间隙 02:05~02:25 + 05:05~05:25 10 200min 12 60min ❌ 只覆盖 30%
三段间隙 分布在 4 小时内 15 240min 17 85min ❌ 只覆盖 35%

影响范围:实时服务 vs 数据自愈

维度 实时服务路径(本轮) 数据自愈路径(BUG5)
调用频率 每 5 分钟 × 每个 symbol — 高频主路径 仅启动时 — 低频
并发度 30 个工作线程并发执行 串行遍历 symbol 列表
入口函数 _fetch_and_validate_price_data() DataHealingOrchestrator.heal_and_prepare()
路由逻辑 if missing → precise(几乎总走错误路径) _fill_kline_gaps()
失败后果 数据不完整 → 分析结果不可靠 → 错误交易信号 数据不完整 → 可多轮修复
重试机制 冷却期 600s 内不重试 HEALING_MAX_ITERATIONS=3
全局锁影响 30 线程竞争 → 严重阻塞 单线程 → 影响有限

实时路径的影响更严重:数据自愈路径每轮修复部分间隙、多轮可能修完;但实时路径在冷却期(600s)内不会重试,一次补充失败就直接用不完整数据执行分析 → 输出不可靠的 Z-score → 可能触发错误交易信号。


全局锁在 30 线程场景下的具体影响

正常场景(API 响应 1s)

Worker-01: [获取锁][sleep 2.5s][HTTP 1.0s][释放锁]                    = 3.5s
Worker-02:                                        [获取锁][sleep 2.5s][HTTP 1.0s][释放锁]  = +3.5s
Worker-03:                                                                                  [获取锁]...
...
Worker-30:                                                                                  [等待 ~102s]

总延迟: 30 × 3.5s ≈ 105s
→ 最后一个线程的数据补充延迟超过 1.5 分钟
→ 分析队列积压 → 可能触发队列满丢弃

超时场景(API 响应 30s)

Worker-01: [获取锁][sleep 2.5s][━━━━━ HTTP 30s ━━━━━][释放锁]
Worker-02~30:                  [━━━━━━━ 全部阻塞等待锁 ━━━━━━━]

一次超时 → 所有 29 个工作线程阻塞 32.5s
→ 分析队列 30000 容量可能被耗尽
→ K 线消息被丢弃: "分析队列已满,跳过分析"
→ 系统监控告警: "分析队列使用率过高"

与 BUG #1 的复合效应

Worker-05 检测到 BTC 存在两段间隙:
  → fill_missing_data_precise(missing=10个点, 跨度200min)
  → API 只请求 60min 范围 (BUG #1)
  → 获取锁 + sleep + HTTP = 3.5s (BUG #2)
  → 返回不完整数据
  → 用不完整数据计算 Z-score → 结果不可靠
  → 冷却期 600s → 10 分钟内不会重试

Worker-06~30 在等待 Worker-05 释放锁...
  → 各自的数据补充操作依次排队
  → 大量分析任务延迟 → 队列积压

→ 结果: 数据截断(BUG#1) + 线程阻塞(BUG#2) 叠加
  → 不完整数据被用于分析 AND 分析本身被延迟
  → 双重可靠性降级

修复方案

修复 1:fill_missing_data_precise 使用时间范围替代 limit

# kline_data_filler.py — fill_missing_data_precise

# 修复前 (当前代码):
since = int(start_time.timestamp() * 1000)
ohlcv = self._fetch_ohlcv_with_retry(
    symbol, timeframe,
    since=since,
    limit=min(limit, self.API_LIMIT),  # ← limit 语义错误
)

# 修复后: 直接用已正确计算的 start_time/end_time,调用 range 版本
since_ms = int(start_time.timestamp() * 1000)
until_ms = int(end_time.timestamp() * 1000)
ohlcv = fetch_candles_range_with_retry(
    self._info, symbol, timeframe,
    since_ms, until_ms,
    api_limit=self.API_LIMIT,
    request_interval=self.API_REQUEST_INTERVAL,
)

fill_missing_data 已经正确使用 fetch_candles_range_with_retry + since_ms/until_ms,不受此 BUG 影响。修复只需将 fill_missing_data_precise 对齐到同一模式。

修复 2:全局锁缩小到仅覆盖限流逻辑

# hyperliquid_candles.py — fetch_candles

# 修复前: 锁覆盖 sleep + HTTP 请求
with _candles_lock:
    if elapsed < min_interval:
        time.sleep(min_interval - elapsed)    # 锁内 sleep
    raw = info.candles_snapshot(...)           # 锁内 HTTP
    globals()["_last_candles_request_time"] = time.monotonic()

# 修复后: 锁仅覆盖限流检查和时间戳更新
with _candles_lock:
    now = time.monotonic()
    elapsed = now - _last_candles_request_time
    if elapsed < min_interval:
        time.sleep(min_interval - elapsed)
    globals()["_last_candles_request_time"] = time.monotonic()
# HTTP 请求在锁外执行
try:
    raw = info.candles_snapshot(coin, interval, start_time_ms, end_time_ms)
except KeyError as e:
    logger.warning(
        f"交易所 meta 中无此 coin | symbol={symbol} coin={coin} | {e}"
    )
    raise
if not raw:
    return []
return [_candle_to_row(c) for c in raw]

与前五轮 BUG 的关系

轮次 发现 层级 路径 状态
第一轮 (BUG1.md) _find_kline_gaps 概念混淆 业务层 数据自愈 ✅ 已修复
第二轮 (BUG2.md) 间隔硬编码 / 无新鲜度检查 业务层 数据自愈 ✅ 已修复
第三轮 (BUG3.md) SQL 参数化 / 窗口边界 业务层 数据自愈 ✅ 已修复
第四轮 (BUG4.md) timeline 无限制 / cointegration 硬编码 业务层 数据自愈 ❌ 待修复
第五轮 (BUG5.md) limit→endTime 语义错误 + 全局锁 + 分页 +1ms 基础设施层 数据自愈路径 ❌ 待修复
第六轮 (本轮) 同一根因在实时分析主路径上的表现 基础设施层 实时分析路径 待修复

BUG5 与 BUG6 共享同一根因hyperliquid_candles.py 封装层的 limit→endTime 语义错误),但通过不同的调用路径触发,影响范围和严重程度不同。BUG5 影响启动时的一次性数据自愈;BUG6 影响运行时的持续数据补充,对分析结果可靠性和交易信号正确性有直接影响。


优先级排序

优先级 问题 严重性 触发频率 影响
P0 fill_missing_data_precise 请求范围截断 致命 🔴🔴 每次检测到多段间隙 K 线静默截断 → 分析不可靠 → 错误交易信号
P1 全局锁串行化 30 线程 API 调用 严重 🔴 多线程并发补充时 分析管线阻塞 → 队列积压/溢出 → 消息丢弃

修复 P0 后,fill_missing_data_precise 改用 fetch_candles_range_with_retry,在多段间隙场景下可能产生多次分页请求。此时 P1(全局锁)的影响会被放大,因此建议 P0 和 P1 一起修复。

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