全系统key配对升级 bug21
— 10 min read
配对维度优化后系统缺陷分析报告
分析时间:2026-02-20
代码基准:HEAD(requirements update 6 debug 18)
分析范围:全系统 Key 迁移至 PairKey = tuple[str, str] 后的最新状态
已确认修复的问题 ✅
| 项目 |
修复内容 |
位置 |
| 启动SQL时间边界 |
已添加 >= NOW() - INTERVAL '30 days' |
orchestrator.py:140-152 |
| KillSwitch平仓 |
stop_loss_monitor 中已加入 KillSwitch 检查并遍历平仓 |
orchestrator.py:692-702 |
| stop()主动平仓 |
添加 close_positions_on_stop 参数支持主动平仓 |
orchestrator.py:239-283 |
| 数据库索引 |
全系统 symbol → (symbol, base_symbol) 配对索引迁移 |
20260219_fix_pairkey_indexes.sql |
| 统计表扩展 |
daily_trading_stats 添加 symbol/base_symbol 列 |
20260220_bugfix_batch.sql |
| 移动止损分离 |
update_trailing_peak 与 check_trailing_stop 已拆分 |
risk_manager.py |
仍存在的缺陷
🔴 严重缺陷(直接影响资金安全/核心功能)
缺陷 S1:KillSwitch 平仓无异常保护
缺陷 S2:冷却期结束后突破信号可能丢失
- 位置:
src/trading/strategy.py:638-641
- 问题: 冷却期结束时,同一 tick 内同时清除
_last_trade_time 和 _prev_above_threshold(重置为 False)。若当前 tick 已在阈值上方,由于 prev=False、cur=True 满足"突破"条件,但状态重置操作与检测同步执行,实际行为取决于代码执行顺序——可能在冷却期内已出现过的突破状态被消费掉,导致信号丢失。
- 影响: 止损冷却结束后,若行情继续极端,合法的再入场信号可能被误跳过。
- 建议: 冷却期结束时仅清除
_last_trade_time,在下一个 tick 时再允许突破检测(使用 _cooling_just_ended 标志位延迟一拍)。
缺陷 S3:Base 腿 PnL 费用计算使用入场价而非出场价
缺陷 S4:Alt 腿消失时 Base 腿可能变成幽灵仓位
- 位置:
src/trading/position_manager.py:347-414
- 问题: 当
alt_gone_from_exchange=True 时,系统直接计算 PnL 并标记仓位关闭。但若此时 base 腿仍在交易所存在(base_size > 0),且 _adopt_residual_base_leg() 未被调用(或调用时机不对),该 base 腿会成为系统完全不知晓的幽灵仓位,持续占用资金但不受任何止损管理。
- 建议: 在标记仓位关闭前,强制检查 base_size,若 > 0 则必须先收纳或平仓。
🟠 重要缺陷(影响系统鲁棒性/数据完整性)
缺陷 A1:孤儿仓位 peak_pnl_pct 未初始化导致移动止损失效
- 位置:
src/trading/position_manager.py:1253-1268
- 问题: 孤儿单腿仓位创建时,
peak_pnl_pct 保持默认值 0.0。首次 tick 更新时,若当前 unrealized_pnl_pct 为负数,而 peak_pnl_pct=0 使得 drawdown_from_peak = 0 - current < 0,移动止损逻辑判断为未达到止损阈值,实际上这个"峰值"是虚假的。
- 建议: 孤儿仓位创建后,第一次进入止损检查前,先用当前 unrealized_pnl_pct 初始化 peak_pnl_pct(
peak_pnl_pct = max(0.0, current_pnl_pct))。
缺陷 A2:孤儿配对检测依赖方向推断而非数据库确认
- 位置:
src/trading/position_manager.py:1219-1223
- 问题: 孤儿检测通过
(alt_szi > 0) == (base_szi > 0) 来判断是否为 pair 模式——即"两个仓位方向相同"则认为是原始配对。但这是推断,不是确认:两个不相关的独立仓位碰巧方向相同时,会被误识别为配对,创建错误的 PairPosition。
- 建议: 孤儿恢复应优先查询数据库中的已知配对记录(
open_pair_positions 表),只在数据库无记录时才使用推断逻辑,并在日志中明确标注"推断配对"。
缺陷 A3:on_position_closed 未清理 _last_adaptive_z
- 位置:
src/trading/strategy.py:310-325
- 问题: 平仓回调清理了
_positions、_exit_pending、_prev_above_threshold,但未清理 _last_adaptive_z[pair_key]。
- 影响: 长期运行后,已关闭的配对仍在字典中占用内存(内存缓慢泄漏);重新开仓同一配对时,日志中可能显示上一次平仓时的旧 adaptive_z 值,造成监控混淆。
- 建议: 在
on_position_closed 中添加 self._last_adaptive_z.pop(pair_key, None)。
缺陷 A4:配对级限速器字典无清理机制(潜在内存泄漏)
- 位置:
src/trading/orchestrator.py:547-552
- 问题:
_pair_rate_limiters: dict[PairKey, RateLimiter] 采用惰性创建,每次新配对首次信号时创建限速器,但从不删除。若配对长期不活跃或被下架,对应的限速器仍留在字典中。
- 影响: 配对数量多且频繁变动时,内存占用持续增长。
- 建议: 在仓位关闭后(
on_position_closed 触发时),检查该 pair_key 是否还有活跃仓位,若无则移除限速器。
缺陷 A5:Exit Price 覆盖逻辑设计脆弱
- 位置:
src/trading/trade_repository.py:122-125
- 问题:
save_position() INSERT 时 exit_price 传入 0,ON CONFLICT 使用 COALESCE(NULLIF(..., 0), old_value) 保留非零值。
- 并发风险: 若
update_position_status() 与 save_position() 并发执行,两次写入可能以不确定顺序落库,realized_pnl 可能被后执行的 INSERT 覆盖。
- 设计脆弱: 依赖"0 作为哨兵值"来区分"未设置"和"真实值为0",属于反模式。
- 建议:
save_position() INSERT 时不包含 exit_price/realized_pnl 字段;exit_price 仅通过专用的 update_position_status() 方法写入。
缺陷 A6:止损 check 与 peak update 调用顺序有隐患
- 位置:
src/trading/risk_manager.py:250-264 与 orchestrator.py:710-719
- 问题:
check_trailing_stop() 依赖调用方保证先调用 update_trailing_peak()。如果调用顺序被误改(先 check 后 update),移动止损会用上一 tick 的峰值判断本 tick 数据,产生漏报。
- 建议: 将
update_trailing_peak() 合并入 check_trailing_stop() 内部,对外提供单一入口,消除调用顺序依赖。
🟡 中等缺陷(影响精度/可维护性)
缺陷 M1:资金费率未计入 PnL(已知设计局限)
- 位置:
src/trading/position_manager.py:629,685
- 问题: 代码注释明确说明"暂不扣减资金费",但也没有在日志中输出估算的资金费金额。
realized_pnl 实际上是"PnL - 交易手续费",资金费可能为正(做空时收取)或负(做多时支付),在极端行情下资金费率可达 0.1%~1%/8h,忽略会使实际收益偏差较大。
- 建议: 调用 Hyperliquid API 的
userFunding 接口,或至少在日志中估算资金费(资金费率 × 名义价值 × 持仓小时数 / 8)。
缺陷 M2:峰值权益文件写入无原子性保护
- 位置:
src/trading/risk_manager.py:48(_save_peak_equity)
- 问题: 直接写入
.peak_equity 文件,多线程环境下若同时触发写入,可能写入不完整数据(部分字节),下次启动时解析失败。
- 建议: 采用原子写(先写
.peak_equity.tmp,完成后 rename),Python 3.3+ 的 os.replace() 保证原子性。
缺陷 M3:_compute_adaptive_z 含隐式副作用
- 位置:
src/trading/strategy.py:369-384(方法本身),src/trading/strategy.py:450(调用处)
- 问题: 方法名暗示纯计算,但调用方需要手动执行
bl.last_std = computed_std 才能完成状态更新——这是 Command-Query Separation 违反,容易被遗漏。
- 建议: 要么在方法内部直接更新
bl.last_std(并明确命名为 _update_and_compute_adaptive_z),要么完全不更新,让返回值由调用方决定如何使用。
缺陷 M4:信号去重字典清理策略低效
- 位置:
src/trading/strategy.py:533-537
- 问题: 去重字典仅在条目数 > 500 时触发一次清理(删除超过 60s 的条目)。在低频场景下,字典长期不触发清理;在高频场景下,500 的阈值可能过小。
- 建议: 改为定时清理(如每 60s 一次,与去重窗口一致),避免依赖条目数触发。
缺陷 M5:check_max_drawdown 初始化逻辑有漏洞
- 位置:
src/trading/risk_manager.py:319-337
- 问题: 若
peak_equity <= 0 and account_value <= 0 时直接跳过检查(返回 True,即允许)。但在初始化阶段或账户异常情况下,account_value 可能真的为 0,此时应该拒绝开仓而不是放行。
- 建议:
peak_equity <= 0 时应先用当前 account_value 初始化 peak_equity;若 account_value 也 <= 0,应记录 warning 并返回 False(拒绝)。
🟢 轻微缺陷(扩展性/规范性)
缺陷 L1:coin_to_symbol 硬编码 USDC 计价币种
- 位置:
src/trading/models.py:210-218
- 问题: 函数固定拼接
/USDC:USDC 后缀,限制系统扩展至 USDT、BTC 等其他计价币种。
- 建议: 添加
quote_asset: str = "USDC" 参数。
缺陷 L2:PositionStatus 枚举缺少中间状态
- 位置:
src/trading/models.py:33-40
- 问题: 枚举仅有 OPEN / CLOSED / ERROR,缺少
PENDING_CLOSE 状态用于区分"正在执行平仓中"与"已完成平仓"的过渡状态,导致平仓重试逻辑中状态判断不精确。
缺陷 L3:datetime 时区不统一
- 位置:
src/trading/risk_manager.py:266-282(处理 naive datetime)、position_manager.py 中多处
- 问题: 部分代码使用 UTC aware datetime,部分使用 naive datetime,需要在比较时做兼容处理,增加代码复杂度并存在隐患。
- 建议: 在
models.py 中统一规定所有 datetime 字段为 UTC aware,在数据入口处强制转换。
缺陷汇总表
| 优先级 |
编号 |
缺陷描述 |
核心文件 |
行号 |
| 🔴 严重 |
S1 |
KillSwitch 平仓无异常保护 |
orchestrator.py |
692-698 |
| 🔴 严重 |
S2 |
冷却期结束时突破信号可能丢失 |
strategy.py |
638-641 |
| 🔴 严重 |
S3 |
Base 腿费用用入场价而非出场价 |
position_manager.py |
677-678 |
| 🔴 严重 |
S4 |
Alt 腿消失时 Base 腿变幽灵仓位 |
position_manager.py |
347-414 |
| 🟠 重要 |
A1 |
孤儿仓位 peak_pnl_pct 未初始化 |
position_manager.py |
1253-1268 |
| 🟠 重要 |
A2 |
孤儿配对检测逻辑依赖推断不严谨 |
position_manager.py |
1219-1223 |
| 🟠 重要 |
A3 |
on_position_closed 未清理 _last_adaptive_z |
strategy.py |
310-325 |
| 🟠 重要 |
A4 |
配对级限速器无清理机制(内存泄漏) |
orchestrator.py |
547-552 |
| 🟠 重要 |
A5 |
Exit Price 并发写入冲突风险 |
trade_repository.py |
122-125 |
| 🟠 重要 |
A6 |
止损 check/update 调用顺序有隐患 |
risk_manager.py |
250-264 |
| 🟡 中等 |
M1 |
资金费率完全未计入 PnL |
position_manager.py |
629,685 |
| 🟡 中等 |
M2 |
峰值权益文件写入非原子性 |
risk_manager.py |
48 |
| 🟡 中等 |
M3 |
_compute_adaptive_z 含隐式副作用 |
strategy.py |
369-384 |
| 🟡 中等 |
M4 |
信号去重字典清理策略低效 |
strategy.py |
533-537 |
| 🟡 中等 |
M5 |
check_max_drawdown 初始化允许异常放行 |
risk_manager.py |
319-337 |
| 🟢 轻微 |
L1 |
coin_to_symbol 硬编码 USDC |
models.py |
210-218 |
| 🟢 轻微 |
L2 |
PositionStatus 缺少 PENDING_CLOSE 状态 |
models.py |
33-40 |
| 🟢 轻微 |
L3 |
datetime 时区不统一 |
risk_manager.py |
266-282 |
合计:4 个严重 + 6 个重要 + 5 个中等 + 3 个轻微 = 18 个待处理缺陷
修复优先级建议
| 优先级 |
编号 |
理由 |
| P0(立即修复) |
S1、S2、S3、S4 |
直接影响资金安全和核心功能正确性 |
| P1(本周修复) |
A1、A2、A5 |
影响仓位数据完整性和止损功能 |
| P2(计划修复) |
A3、A4、A6、M2、M5 |
鲁棒性和数据准确性提升 |
| P3(优化改进) |
M1、M3、M4、L1、L2、L3 |
精度、可维护性、扩展性 |