全系统key配对升级 bug17
配对维度优化后 — 缺陷与隐患分析报告
分析范围:全系统 Key 改为配对维度
(symbol, base_symbol)后的当前状态
日期:2026-02-20
一、已正确落地的部分(简要)
- 内存状态:
PositionManager._positions、AdaptiveBollingerStrategy._baselines/_positions/_last_adaptive_z等均按PairKey = (symbol, base_symbol)存储。 - 配置与风控:
get_strategy_params(symbol, base_symbol)、is_symbol_allowed/is_close_disabled支持币种级与配对级。 - 持久化:
trading_signals、pair_positions、analysis_results、symbol_blacklist均含base_symbol;迁移脚本已把索引/压缩分段改为 PairKey。 - 策略与编排:入场/退场、止损、仓位同步、信号去重键
symbol:base_symbol:direction:ts均按配对维度处理。 - 分析层:黑名单、新币黑名单的 key 均为
(symbol, base_symbol);分析结果写入带base_symbol;历史 z4h 灌入按(symbol, base_symbol)分组。
二、缺陷(建议修复)
缺陷 1:新币黑名单与取消订阅的维度不一致【高优先级 — 运行时 Bug】
位置:src/services/realtime_kline_service_base.py(约 1116–1148 行)
现象:
new_coin_blacklist 按配对存储 pair_key = (symbol, base_symbol)。但"数据不足"时对**整个币种(coin)**取消 K 线订阅:
coin = symbol.split('/')[0]
subscriptions_to_remove = [
{"type": "candle", "coin": coin, "interval": "5m"},
...
]
self.ws_manager.remove_subscriptions(subscriptions_to_remove)
问题:
例如仅 PURR|HYPE 因 4H 数据不足被加入 new_coin_blacklist,却会取消整个 PURR 的订阅,导致 PURR|BTC 等其它配对也不再收到 K 线,被错误地"连带"停掉。
建议:
- 方案 A(推荐):取消订阅前判断:仅当该
symbol在_pair_cache中的所有 base 对应的配对都已出现在new_coin_blacklist时,才取消该 symbol 的订阅;否则只把当前配对加入黑名单,不取消订阅。 - 方案 B:不在此处取消订阅,仅做"该配对不分析";若需节省资源,再单独做"按 symbol 聚合、全部配对都不可用再退订"的逻辑。
缺陷 2:数据自愈仅针对默认 base,多 base 配对未覆盖【中优先级】
位置:src/services/realtime_kline_service_base.py(约 1717–1722 行)
现象:
自愈配对列表为:
heal_pairs = [
(s, self.base_symbol) for s in self.symbols
if s != self.base_symbol and (s, self.base_symbol) in pairs_with_data
]
即只自愈 (symbol, self.base_symbol),未使用 _pair_cache 中的多 base。
问题:
当同一 alt 对应多个 base(如 PURR|HYPE、PURR|BTC)时,只有 (PURR, self.base_symbol) 会参与自愈;若默认 base 为 HYPE,则 PURR|BTC 缺数时不会被自愈。
建议:
从 _pair_cache 构建需自愈的配对列表,例如:
heal_pairs = [
(s, b)
for s in self.symbols
for b in (self._pair_cache.get(s) or [self.base_symbol])
if s != b and (s, b) in pairs_with_data
]
(并去重、控制并发/超时,避免一次自愈过多配对)。
缺陷 3:base_symbol 空值规范化存在三种混用模式【中优先级】
现象:代码中存在三种不统一的处理方式:
| 位置 | 模式 | 风险 |
|---|---|---|
strategy.py(9 处) |
base_symbol = base_symbol or "" 局部赋值 |
局部规范化 ✅ |
position_manager.py、orchestrator.py |
pos.base_symbol or "" 内联使用 |
部分覆盖 ⚠️ |
trade_repository.py(save_signal、save_position) |
直接使用,未规范化 | 可能写入 NULL ❌ |
问题:
若某条调用路径中 base_symbol 为 None(而非 ""),会导致:
- PairKey 不匹配:
("PURR", None) ≠ ("PURR", "")→ 字典 key 查找失败,状态悄然丢失 - 数据库写入 NULL 值,后续查询行为不一致
建议:
在 models.py 中定义统一的规范化 helper,全系统调用:
def normalize_base_symbol(base_symbol: str | None) -> str:
return base_symbol or ""
缺陷 4:数据库 base_symbol 字段存在历史 NULL 值风险【中优先级】
位置:src/trading/trade_repository.py — save_signal、save_position
现象:
持久化时直接传入 Python 对象中的 base_symbol,未做 or "" 规范化,若对象中为 None 则数据库存入 NULL。
建议:
- 先查验现有数据:
SELECT COUNT(*) FROM pair_positions WHERE base_symbol IS NULL; SELECT COUNT(*) FROM trading_signals WHERE base_symbol IS NULL; SELECT COUNT(*) FROM analysis_results WHERE base_symbol IS NULL; - 存在 NULL 时清理:
UPDATE pair_positions SET base_symbol = '' WHERE base_symbol IS NULL; UPDATE trading_signals SET base_symbol = '' WHERE base_symbol IS NULL; UPDATE analysis_results SET base_symbol = '' WHERE base_symbol IS NULL; - 加约束防止新写入 NULL:
ALTER TABLE pair_positions ALTER COLUMN base_symbol SET NOT NULL; ALTER TABLE trading_signals ALTER COLUMN base_symbol SET NOT NULL; ALTER TABLE analysis_results ALTER COLUMN base_symbol SET NOT NULL;
三、不足与改进建议(非致命)
不足 5:get_positions_by_symbols 仅按 symbol 过滤,语义模糊
位置:src/trading/trade_repository.py(约 257–262 行)
现状:
WHERE symbol = ANY(%s),返回该 symbol 下所有 base_symbol 的仓位。
当前唯一调用方 PositionManager._build_orphan_positions 在内存中再按 (symbol, base_symbol) 分组,逻辑正确,无立即风险。
建议:
保持现状;若未来有"按配对查询仓位"的接口需求,新增 get_open_positions_by_pairs(pairs: list[PairKey]) 而非复用现有方法,以避免语义混淆。
不足 6:每日统计无配对维度
位置:src/trading/trade_repository.py — update_daily_stats
现状:daily_trading_stats(stat_date, network, ...) 按"日+网络"全局汇总,无 (symbol, base_symbol)。
建议:
若产品需要"按配对看每日表现",可后续增加按 PairKey 的统计表或字段;当前若仅需全局视角,可保持不变。
不足 7:历史已压缩 Chunk 仍为旧分段方式
现状:
新数据已按 (symbol, base_symbol) 分段压缩;已存在的压缩 chunk 仍是按旧分段(如仅 symbol)压缩的,不会自动重写。
建议:
若需对历史数据也按 PairKey 分段压缩,可在低峰期对相关 hypertable 执行 decompress_chunk + 再压缩(评估 I/O 与锁影响);否则接受"历史 chunk 旧分段,新数据新分段"的混合状态,并在文档中注明。
不足 8:入队节流维度为 (symbol, timeframe),不含 base(有意设计)
位置:realtime_kline_service_base.py(约 885 行注释)
现状:
同一根 K 线只调度一次分析任务,任务内部对该 symbol 的所有 base 循环执行 _analyze_and_alert。节流 key 为 (symbol, timeframe),不按 base 分开。
结论:
这是有意设计(一根 K 线驱动多配对分析),与配对维度不冲突;仅需在文档中明确"节流按 symbol+timeframe,分析按 (symbol, base_symbol)",防止后续开发者误解。
四、优先级汇总
| 编号 | 类型 | 描述 | 优先级 |
|---|---|---|---|
| 缺陷 1 | Bug | 新币黑名单按配对,但取消订阅按币种,导致其它配对被连带停数据 | 🔴 高 |
| 缺陷 2 | Bug | 数据自愈只考虑 (symbol, self.base_symbol),多 base 配对未自愈 |
🟡 中 |
| 缺陷 3 | 隐患 | base_symbol 空值规范化三种模式混用,潜在 PairKey 失配 |
🟡 中 |
| 缺陷 4 | 隐患 | 数据库历史 NULL 值风险,持久化未规范化 | 🟡 中 |
| 不足 5 | 改进 | 仓库接口 get_positions_by_symbols 语义不够精确 |
🟢 低 |
| 不足 6 | 改进 | 每日统计无配对维度 | 🟢 低 |
| 不足 7 | 改进 | 历史压缩 chunk 仍为旧分段 | 🟢 低 |
| 不足 8 | 文档 | 入队节流维度说明缺失 | 🟢 低/文档 |
建议行动顺序:
- 立即修复:缺陷 1(订阅取消逻辑改为配对聚合后再退订)
- 近期修复:缺陷 2(自愈从
_pair_cache构建多 base 配对列表)+ 缺陷 3(统一normalize_base_symbolhelper)+ 缺陷 4(数据库 NULL 清理与约束) - 技术债积累处理:不足 5–8 按需迭代