全系统key配对升级 bug23

配对维度(PairKey)优化后系统缺陷分析报告

背景

本项目是一个 Hyperliquid 配对交易自动化系统,近期完成了全系统 Key 从单一 symbol 维度升级为配对维度(PairKey = tuple[str, str],即 (symbol, base_symbol))的大规模重构。本次分析目标是评估优化后系统是否仍存在缺陷与不足。

系统架构

  • models.pyPairKey 类型定义(全系统统一索引键)
  • strategy.py → 按 PairKey 维度管理 Adaptive Bollinger Z-Score 状态
  • position_manager.py → 按 PairKey 维度管理仓位生命周期
  • orchestrator.py → 信号编排、KillSwitch、配对限速器
  • executor.py → Hyperliquid SDK 封装、配对下单
  • risk_manager.py → 9 项风控检查 + 仓位缩放
  • trade_repository.py → TimescaleDB 数据持久化

发现的缺陷与不足(按优先级分级)


🔴 P0 - 严重缺陷(立即修复)

S1. KillSwitch 激活时配对限速器内存泄漏

  • 位置: orchestrator.py:695-709
  • 问题: KillSwitch 平仓循环执行后直接 continue,跳过了正常平仓流程中的 _pair_rate_limiters.pop()。每次 KillSwitch 激活,所有被平仓配对的 RateLimiter 对象均不会被清理,长期运行导致累积泄漏。
  • 影响: 内存泄漏,每次 KillSwitch 激活留下孤立 RateLimiter 对象。

S2. 冷却期内突破状态机失效(信号丢失)

  • 位置: strategy.py:620-636
  • 问题: 冷却期内 return None 直接退出,未更新 _prev_above_threshold[key]。冷却期中若 adaptive_z 回落后重新突破,prev_above 仍为 True(旧值),导致冷却期结束后第一个有效突破信号被误判为"非突破"而丢弃。
  • 时序示例:
    T1: adaptive_z 穿越阈值 → 入场,冷却开始,_prev_above=True
    T2-T10: 冷却期内,adaptive_z 回落至 -0.3,current_above=False
             → 但 return None,未执行 _prev_above[key] = False
    T11: 冷却期结束,adaptive_z 再次穿越:
         prev_above=True (旧), current_above=True → breakout=False ❌ 信号丢失!
    
  • 影响: 冷却期后第一个有效信号必然被拒,错过入场机会。

S3. Alt 腿消失时 Exit Price 链路断裂导致 PnL 永久丢失

  • 位置: position_manager.py:347-414
  • 问题: 当 alt 腿已从交易所消失时,exit_price 获取链路为:alt_current_priceget_all_mids() → 放弃(传 None)。若两者均失败,DB 中 alt_exit_price 写入 None,realized_pnl 计为 0,该笔交易盈亏永久丢失。
  • 影响: pair 模式下网络不稳定时,PnL 统计存在永久性漏记风险。

S4. 缓冲区灌入(prime_buffer)与实时 Tick 处理的并发竞态

  • 位置: strategy.py:166-209 + orchestrator.py:174
  • 问题: 初始化时 prime_buffer()std_window.clear() + ema=None,若与正在计算 adaptive_z 的 tick 处理线程发生锁间隙交叉,会导致 EMA 数据中途被清空,信号突然消失或 NoneType 异常。
  • 影响: 重启后初始化期间,信号计算不稳定,可能误触发/误拒信号。

🟠 P1 - 重要缺陷(本周修复)

A1. 孤儿仓位大小同步后未持久化到 DB

  • 位置: position_manager.py:745-769
  • 问题: already_exists=True 且大小需要同步时,在锁内修改 existing.alt_size 后直接 return,未调用 self._repo.save_position(existing)。下次重启从 DB 恢复时,仍使用旧 size,与交易所不一致。
  • 影响: 重启后孤儿仓位大小与交易所不符,影响风控和 PnL 计算。

A2. Leg B 预检查与实际下单之间的余额竞态(限价单模式)

  • 位置: executor.py:738-786
  • 问题: Leg B 余额预检查在 Leg A 下单之前执行,但限价单追踪最长可达 10 分钟,其间其他信号可能消耗可用余额。Leg A 成交后,Leg B 实际下单时余额已不足,导致 Leg A 成交但 Leg B 失败,产生单腿残留。
  • 影响: 并发信号场景下概率性产生孤儿单腿仓位。

A3. 并发平仓时 Exit Price 字段被覆盖

  • 位置: trade_repository.py:165-203
  • 问题: update_position_status() 使用参数化 UPDATE,先后两次分别更新 alt_exit_pricebase_exit_price。若两次 UPDATE 因网络延迟乱序抵达,后执行的 UPDATE 可能只更新部分字段,覆盖先执行的正确值。
  • 影响: 部分平仓记录中 base_exit_price 可能为 NULL,realized_pnl 计算不完整。

A4. 配对限速器字典(_pair_rate_limiters)无清理机制

  • 位置: orchestrator.py(全局)
  • 问题: 正常平仓流程会调用 _pair_rate_limiters.pop(),但 KillSwitch、异常路径、幽灵清理等非正常平仓路径均未清理。长期运行下,已关闭配对的 RateLimiter 不断累积。
  • 影响: 内存缓慢泄漏(A4 是 S1 的扩展,覆盖所有非正常路径)。

A5. 平仓失败时状态恢复的竞态条件

  • 位置: position_manager.py:288-310
  • 问题: 设置 position.status = CLOSING 后释放锁,执行平仓(耗时操作),异常时重置为 OPEN。在此期间 sync 线程可能已修改 position 对象,导致状态恢复基于过期数据。
  • 影响: 极端情况下仓位状态停滞在非法状态,需手动干预。

A6. 策略层异常被过宽 except 掩盖(状态不同步)

  • 位置: orchestrator.py:402-427
  • 问题: on_position_closed + _pair_rate_limiters.pop() 整体被 except Exception 捕获并记为"不影响平仓"。但若内部有真实 bug(KeyError 等),会导致策略层仓位状态未清除,下次 tick 仍按持仓处理。
  • 影响: 策略层状态不同步,影响下一轮信号判断。

🟡 P2 - 中等缺陷(计划修复)

M1. 信号去重字典内存缓慢增长

  • 位置: strategy.py:527-532
  • 问题: _signal_history 每 60 秒清理一次,高频交易下(多配对 × 多方向 × 秒级 tick)60 秒内可积累数万条过期条目,每次 _check_entry 遍历性能下降。
  • 影响: 长期运行下策略引擎处理延迟从 <1ms 升至 10ms+。

M2. 信号去重键精度不足(秒级精度)

  • 位置: strategy.py:520,键格式:f"{symbol}:{base_symbol}:{direction}:{int(kline_time.timestamp())}"
  • 问题: 同一秒内对相同配对产生两次不同市场状态的合法信号,会被误判为重复而拒绝第二个。
  • 影响: 高波动期间合法信号被错误过滤。

M3. DB 操作无异常捕获导致持久化中断

  • 位置: position_manager.py:175-178, orchestrator.py:541-561
  • 问题: save_position(), save_signal(), save_order() 调用无 try-catch,DB 连接异常会向上传播,中断整个开仓/平仓流程,导致交易所有仓位但 DB 无记录。
  • 影响: 重启后恢复流程误判仓位状态。

M4. Leg B 残留告警异常捕获范围太窄

  • 位置: position_manager.py:459-473
  • 问题: 仅捕获 OSError, ValueError,若 sender_colourful 抛出 RuntimeErrorTimeoutError,会传播到上层中断平仓流程,跳过必要的 DB 清理。
  • 影响: 告警失败可能导致平仓流程异常终止。

M5. 价格快照不一致性(多配对同步使用同一旧快照)

  • 位置: position_manager.py:1024-1040
  • 问题: sync_with_exchange() 用单次 get_all_mids() 快照批量更新所有仓位的 alt_current_price,但各配对赋值时间不同,产生时间不一致的混合价格数据,影响跨配对 PnL 比较。
  • 影响: 多配对场景下实时 PnL 计算略有偏差。

M6. 孤儿仓位收纳后的原子性缺失

  • 位置: position_manager.py:745-795
  • 问题: 锁内检查/写入 _positions,锁外执行 save_position(),两者之间 sync 线程可能删除该仓位,导致 DB 中保存已过时的孤儿记录,重启时再次出现。
  • 影响: 偶发性幽灵仓位需手动清理。

🟢 P3 - 轻微问题(优化改进)

L1. base_symbol 类型混合(None vs "")

  • 位置: models.py:76-96, orchestrator.py:159
  • 问题: PairTradeSignal.base_symbol 可能为 None,而 PairKey 定义为 tuple[str, str],代码中有隐式 or "" 转换,不一致。
  • 建议: PairTradeSignal.base_symbol 统一声明为 str = "",禁止 None。

L2. pair_mode 判断方式不统一

  • 位置: 多处混用 config.pair_mode == "pair"if not base_symbol
  • 建议: 在 config 上增加 @property is_pair_mode: bool,全系统统一使用。

L3. entry_adaptive_z 重启恢复硬编码为 0.0

  • 位置: orchestrator.py:137-190
  • 问题: 重启恢复仓位时 pos.entry_adaptive_z = 0.0,影响移动止损和盈利追踪的准确性。
  • 建议: 在 trade_repository 中持久化 entry_adaptive_z 字段,重启时从 DB 恢复。

L4. PairPosition 模型缺少内存中的 exit_price 字段

  • 位置: models.py
  • 问题: alt_exit_price / base_exit_price 只在 DB 层存在,内存模型无对应字段,导致平仓过程中 exit_price 无法在内存链路中传递和验证。
  • 建议: PairPosition 增加 alt_exit_price: float = 0.0base_exit_price: float = 0.0 字段。

L5. 孤儿配对推断方向判断不够稳健

  • 位置: position_manager.py:1229-1262
  • 问题: 仅根据 alt_szi > 0 判断方向,未验证 base_szi 是否确实反向,若 API 延迟导致两腿都为正,配对被错误跳过,永远无法被收纳。
  • 建议: 增加 base_szi 方向一致性验证,对不符合配对模式的情况单独记录并告警。

缺陷分布热力图

模块 P0 P1 P2 P3
orchestrator.py S1 A4, A5, A6
strategy.py S2, S4 M1, M2
position_manager.py S3 A1, A5 M3, M4, M5, M6 L5
executor.py A2
trade_repository.py A3 M3 L3
models.py L1, L2, L4

整体评估

配对维度(PairKey)优化的架构设计是正确的,统一了全系统的状态索引,消除了单 symbol 维度下的配对混淆问题。但在实现层面,本次分析发现了 20 个待修复缺陷

  • 4 个 P0 严重缺陷:主要集中在内存泄漏、信号状态机失效、竞态条件
  • 6 个 P1 重要缺陷:主要集中在并发安全、数据持久化原子性
  • 6 个 P2 中等缺陷:主要集中在异常处理补全、性能优化
  • 5 个 P3 轻微缺陷:主要集中在代码规范统一、模型完善

最高风险组合:在并发信号 + 网络不稳定 + KillSwitch 激活的极端场景下,可能同时触发 S1(内存泄漏) + S3(PnL丢失) + A3(字段覆盖),导致账户状态不可信。


验证方法

  1. S1/A4 验证:触发 KillSwitch,检查 _pair_rate_limiters dict 长度是否归零
  2. S2 验证:模拟冷却期 + 信号回落 + 再次突破,检查信号是否正确产生
  3. S3 验证:模拟 get_all_mids() 失败,检查 DB 中 alt_exit_price 是否有兜底值
  4. S4 验证:并发运行 prime_buffer()process_tick(),检查是否有 NoneType 异常
  5. A1 验证:触发孤儿大小同步后重启,检查 DB 中 alt_size 是否更新
  6. A3 验证:并发调用两次 update_position_status(),检查 base_exit_price 是否保留

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