全系统key配对升级不完全分析1
系统缺陷分析报告:全系统 Key 配对维度升级后状态
Context
本次升级将系统内所有内部状态 key 从单一 symbol: str 升级为 PairKey = tuple[str, str](symbol, base_symbol),解决了同一 symbol 与多个 base 配对时的数据混淆问题。核心修改涉及 6 个文件:models.py, strategy.py, position_manager.py, trade_repository.py, protocols.py, orchestrator.py。
分析目标:评估升级后系统是否仍存在遗漏,哪些地方未完全配对化。
发现的缺陷
🔴 P1(高优先级):query_avg_zscore_4h 缺少 base_symbol 过滤
文件:src/utils/database/timescaledb.py:793-816
问题代码:
def query_avg_zscore_4h(self, symbol: str, hours: int = 4) -> float | None:
results = self.client.execute_query(
"""
SELECT AVG(zscore_4h) as avg_zscore_4h
FROM analysis_results
WHERE symbol = %s -- ❌ 没有 base_symbol 过滤!
AND zscore_4h IS NOT NULL
AND analysis_time >= NOW() - make_interval(hours => %s);
""",
(symbol, hours)
)
影响:
- 如果
analysis_results表中同一 symbol(如 PURR)有对应多个 base(HYPE、BTC)的记录,查询会混合不同配对的 z-score 来求平均 avg_zscore_4h用于入场时PairTradeSignal的辅助参数(entry_avg_zscore_4h),混合数据会导致策略指标失真- 调用路径:
realtime_kline_service_base.py:1441→avg_zscore_4h→on_entry_signal:453
修复方案:在方法中增加 base_symbol: str = None 参数,SQL 增加 AND (base_symbol = %s OR %s IS NULL) 过滤。
🟡 P2(中优先级):_has_open_position Gate bypass 逻辑是 symbol 维度
文件:src/services/realtime_kline_service_base.py:1521-1531 和 :1547-1557
问题代码:
# 1521行
if self._has_open_position(symbol): # ← 只检查 symbol,不区分 base_symbol
# skip_gates=True 跑退场分析
# 1547行
def _has_open_position(self, symbol: str) -> bool:
if pm is not None and any(
p.symbol == symbol for p in pm.open_positions # ← symbol 维度
):
return True
场景:
- PURR/HYPE 有开仓
- PURR/BTC 分析触发,Gate 未通过
_has_open_position("PURR")因为 PURR/HYPE 有仓位返回 True- 系统为 PURR/BTC 运行 skip_gates exit_only 分析
on_exit_signal因为没有 PURR/BTC 持仓而跳过(返回 False,无实际操作)- 日志中会出现
退场信号但无活跃仓位: PURR|BTC的 debug 日志
当前影响:低(不会误操作),但会有日志噪音,且分析计算浪费资源。
修复方案:将 _has_open_position(symbol) 改为接受 base_symbol 参数,按配对维度判断。或把原方法保留(用于其他逻辑),增加 _has_open_pair(symbol, base_symbol) 专用方法用于 Gate bypass。
🟡 P3(中优先级):孤儿收纳后策略引擎无法为其产生退场信号
文件:src/trading/position_manager.py:644-703 (_adopt_residual_base_leg)
机制:Leg A 平仓成功、Leg B 失败时,系统将 Leg B 转为独立的 single 模式孤儿仓位:
orphan = PairPosition(
symbol=base_symbol, # 原 base_symbol 作为新仓位的 symbol
base_symbol="", # base_symbol 为空
pair_mode="single",
...
)
问题:策略引擎收到的 tick 来自服务层,走的是 (base_symbol, other_base) 的 pair key(比如 HYPE/BTC),而孤儿仓位的 pair key 是 (HYPE, "")。两个 key 不匹配,策略引擎不会为孤儿产生退场信号。
当前缓解措施:孤儿仓位由止损监控线程(固定止损/移动止损/超时)管理,不依赖策略信号退场。设计上属于有意为之,但策略层的均值回归退场逻辑对孤儿完全失效。
修复方案(可选):对孤儿收纳后,_position_sync 中调用 strategy.sync_position(base_symbol, "", ...) 同步孤儿状态,并确保服务层对该孤儿 symbol 的单模式分析也能触发退场。注意:这需要评估是否符合设计意图。
🟡 P4(中优先级):_has_open_position 数据库兜底查询缺少 base_symbol
文件:src/services/realtime_kline_service_base.py:1559+(_has_open_position 数据库兜底逻辑)
与 P2 类似,数据库兜底查询可能也只按 symbol 过滤,与 P2 一并修复。
🟢 P5(低优先级/设计确认项):_check_entry 冷却时间(cooldown_minutes)
文件:src/trading/strategy.py
状态:已确认 _last_trade_time: dict[PairKey, datetime] 是配对维度,冷却是 per-pair 的,无问题。
🟢 P6(低优先级):trade_repository.get_known_pair_relations 的 None 处理
文件:src/trading/trade_repository.py:298
return [(row["symbol"], row["base_symbol"]) for row in rows]
SQL 已有 WHERE base_symbol IS NOT NULL AND base_symbol != '' 过滤,实际不会返回 None,无问题。
正确工作的部分(确认)
| 组件 | 状态 |
|---|---|
strategy.py — 所有 9 个 dict 按 PairKey 维度 |
✅ 正确 |
position_manager.py — open/close/sync 按配对 |
✅ 正确 |
orchestrator.py — base_symbol 全链路传递 |
✅ 正确 |
orchestrator.process_analysis — 从 multi_period_result 取 base_symbol |
✅ 正确 |
analysis_core.analyze_multi_period — 返回 dict 含 base_symbol |
✅ 正确 |
信号去重 key = symbol:base_symbol:direction:timestamp |
✅ 已配对化 |
黑名单系统 _is_blacklisted(symbol, base_symbol) |
✅ 已配对化 |
仓位同步 sync_with_exchange 返回 list[PairKey] |
✅ 正确 |
止损监控 _close_with_retry 传 pos.base_symbol or "" |
✅ 正确 |
策略回调 on_position_closed/opened/rejected 带 base_symbol |
✅ 正确 |
优先修复建议
- P1 立即修复:
query_avg_zscore_4h增加base_symbol参数和过滤,调用方(realtime_kline_service_base.py:1441)传入当前配对的 base_symbol - P2/P4 近期修复:
_has_open_position增加配对维度版本,Gate bypass 使用 pair 精确匹配 - P3 评估:确认孤儿仓位仅靠止损监控退场是否为设计意图,如需策略退场则需额外工作
关键文件路径
src/utils/database/timescaledb.py—query_avg_zscore_4h方法(P1)src/services/realtime_kline_service_base.py—_has_open_position方法、Gate bypass 逻辑(P2/P4)src/trading/position_manager.py—_adopt_residual_base_leg孤儿收纳(P3)
验证方式
- 场景:配置两个配对(如 PURR/HYPE、PURR/BTC),观察
avg_zscore_4h日志打印是否混合 - 场景:PURR/HYPE 有仓位时,手动触发 PURR/BTC 的 Gate 失败,检查日志中是否出现无意义的孤立退场尝试
- 单元测试:为
query_avg_zscore_4h增加带base_symbol参数的测试用例