全系统key配对升级 bug23
配对维度(PairKey)优化后系统缺陷分析报告
背景
本项目是一个 Hyperliquid 配对交易自动化系统,近期完成了全系统 Key 从单一 symbol 维度升级为配对维度(PairKey = tuple[str, str],即 (symbol, base_symbol))的大规模重构。本次分析目标是评估优化后系统是否仍存在缺陷与不足。
系统架构:
models.py→PairKey类型定义(全系统统一索引键)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_price→get_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_price和base_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抛出RuntimeError或TimeoutError,会传播到上层中断平仓流程,跳过必要的 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.0和base_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(字段覆盖),导致账户状态不可信。
验证方法
- S1/A4 验证:触发 KillSwitch,检查
_pair_rate_limitersdict 长度是否归零 - S2 验证:模拟冷却期 + 信号回落 + 再次突破,检查信号是否正确产生
- S3 验证:模拟
get_all_mids()失败,检查 DB 中alt_exit_price是否有兜底值 - S4 验证:并发运行
prime_buffer()和process_tick(),检查是否有 NoneType 异常 - A1 验证:触发孤儿大小同步后重启,检查 DB 中
alt_size是否更新 - A3 验证:并发调用两次
update_position_status(),检查base_exit_price是否保留