全系统key配对升级 bug6
PairKey 维度优化后的缺陷与不足分析
持久化分析报告。在全系统 Key 已改为
(symbol, base_symbol)配对维度的前提下,通过实际代码阅读,对已知问题进行确认,并补充新发现的缺陷。日期:2026-02-19
一、现状验证(已正确落地项)
| 模块 | 状态 |
|---|---|
策略层 strategy.py |
✅ 所有状态字典按 (symbol, base_symbol) 存储 |
仓位管理 position_manager.py |
✅ _pair_key / 开仓防重复均使用 PairKey |
黑名单缓存 realtime_kline_service_base.py |
✅ TTLCache key 为 (symbol, base_symbol) |
DB 索引 trading_signals |
✅ init_timescaledb.sql 已用 (symbol, base_symbol, signal_time DESC) |
DB 索引 pair_positions |
✅ init_timescaledb.sql 已用 (symbol, base_symbol, status) |
DB 压缩 analysis_results |
✅ compress_segmentby = 'symbol, base_symbol' |
| 单腿/孤儿仓位 | ✅ base_symbol="" 统一约定 |
二、缺陷与不足
缺陷 1:base_symbol 归一化不统一(高优先级)
已记录于 docs/pair-dimension-analysis.md,代码确认仍未修复。
位置一:src/trading/trade_repository.py:292
# 当前(有风险)
return [(row["symbol"], row["base_symbol"]) for row in rows]
# 应改为
return [(row["symbol"], row.get("base_symbol") or "") for row in rows]
位置二:src/services/realtime_kline_service_base.py:1324
# 当前(有风险)
self._blacklist_cache[(row['symbol'], row['base_symbol'])] = True
# 应改为
self._blacklist_cache[(row['symbol'], row.get('base_symbol') or '')] = True
风险:若数据库驱动在异常状态下返回 None,会产生 (sym, None) 与 (sym, "") 两个不同 Key,同一配对被双重追踪,黑名单失效或仓位管理混乱。
缺陷 2:trading_signals 压缩分段未同步为 PairKey(高优先级)⭐ 新发现
位置:database/init_timescaledb.sql:457-461
-- 当前(与索引不一致)
ALTER TABLE trading_signals SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'symbol' -- ← 仅 symbol,缺 base_symbol
);
对比:同文件的查询索引已是 PairKey 维度:
CREATE INDEX IF NOT EXISTS idx_trading_signals_pair
ON trading_signals (symbol, base_symbol, signal_time DESC); -- ← PairKey ✅
影响:
- TimescaleDB 按
compress_segmentby分段压缩,查询时优先利用分段减少扫描范围。 - 当前分段为
symbol,按(symbol, base_symbol)查询时需扫描同 symbol 下所有 base_symbol 的数据块,无法利用分段剪枝,压缩后查询性能下降。 - 迁移文件
20260219_fix_pairkey_indexes.sql也未修复此处,存量数据库同样受影响。
修复方案(新建迁移):
-- 新迁移文件:database/migrations/20260220_fix_trading_signals_compress.sql
SELECT remove_compression_policy('trading_signals', if_exists => TRUE);
ALTER TABLE trading_signals SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'symbol, base_symbol'
);
SELECT add_compression_policy(
'trading_signals',
INTERVAL '30 days',
if_not_exists => TRUE
);
同时更新 init_timescaledb.sql:460 为 'symbol, base_symbol'。
缺陷 3:analysis_results 旧 symbol-only 索引残留(中优先级)
已记录于 docs/pair-dimension-analysis.md,代码确认仍未修复。
位置:database/init_timescaledb.sql:141-156
-- 旧索引(仍存在,与 PairKey 架构不一致)
CREATE INDEX IF NOT EXISTS idx_analysis_symbol_time
ON analysis_results (symbol, analysis_time DESC); -- ← 仅 symbol
CREATE INDEX IF NOT EXISTS idx_analysis_kline_time
ON analysis_results (symbol, kline_time DESC); -- ← 仅 symbol
同文件 init_timescaledb.sql:200-201 已有正确的 PairKey 索引:
CREATE INDEX IF NOT EXISTS idx_analysis_results_symbol_timeframe
ON analysis_results (symbol, base_symbol, analysis_time DESC); -- ✅
迁移文件漏 DROP:20260219_fix_pairkey_indexes.sql 中已 DROP 了 trading_signals 和 pair_positions 的旧索引,但未 DROP analysis_results 的两个旧索引:
-- 迁移文件中缺失的部分:
DROP INDEX IF EXISTS idx_analysis_symbol_time;
DROP INDEX IF EXISTS idx_analysis_kline_time;
影响:存量数据库和新建数据库均保留两个无效旧索引,写入 analysis_results 时需额外维护,增加写入开销。
缺陷 4:recent_analysis 节流维度语义不清(中优先级)
已记录于 docs/pair-dimension-analysis.md,代码确认仍未添加注释。
位置:src/services/realtime_kline_service_base.py:885, 931, 946
task_key = (symbol, timeframe) # ← 非 PairKey 维度,不含 base_symbol
# ...
self.recent_analysis[task_key] = current_time
设计意图(正确,但无文档):同一 symbol+timeframe 只调度一次分析任务,任务内部 for base_sym in base_symbols 循环所有配对,以控制 CPU/DB 调用压力。
风险:维护者极易误以为是配对级节流,在排查问题或重构时可能误改 key 格式,破坏原有调度语义。
建议:在该行附近添加注释:
# recent_analysis key 为 (symbol, timeframe),不含 base_symbol。
# 设计意图:同一 K 线下只调度一次分析任务,任务内部对所有配对循环执行
# _analyze_and_alert,避免同 K 线重复触发多次 DB 查询。
# 若需按配对独立节流,需将 key 改为 (symbol, base_symbol, timeframe),
# 并评估 QPS/存储影响。
task_key = (symbol, timeframe)
缺陷 5:策略层入口缺少防御性归一化(低优先级)
已记录于 docs/pair-dimension-analysis.md,代码确认仍未修复。
位置:src/trading/strategy.py:220, 273, 292, 310, 322...(多处)
def sync_position(self, symbol, base_symbol, ...):
key = (symbol, base_symbol) # 若调用方传 None,得到 (sym, None)
def on_position_opened(self, symbol, base_symbol, ...):
key = (symbol, base_symbol) # 同上
def cleanup_pair(self, symbol, base_symbol):
key = (symbol, base_symbol) # 同上
而 position_manager.py 中有 or "" 保护:
key = (signal.symbol, signal.base_symbol or "") # ← 有防御
风险:若调用方误传 base_symbol=None,策略层 key 为 (sym, None),与仓位管理器 key (sym, "") 不一致,导致同一配对在两个模块被识别为不同 key,开仓/平仓回调错位。
修复建议(可选,防御深度):
# 在各对外接口入口统一处理
key = (symbol, base_symbol or "")
三、缺陷汇总表
| # | 优先级 | 问题描述 | 文件位置 | 文档记录 |
|---|---|---|---|---|
| 1 | 🔴 高 | base_symbol None 未归一化,Key 分裂风险 |
trade_repository.py:292, realtime_kline_service_base.py:1324 |
已记录 |
| 2 | 🔴 高 | trading_signals 压缩分段仍为单 symbol |
init_timescaledb.sql:460 + 迁移文件漏修 |
新发现 |
| 3 | 🟡 中 | analysis_results 旧索引残留,迁移未 DROP |
init_timescaledb.sql:142,155 + 迁移文件 |
已记录 |
| 4 | 🟡 中 | recent_analysis 节流维度无注释说明 |
realtime_kline_service_base.py:885 |
已记录 |
| 5 | 🟢 低 | strategy.py 各入口无防御归一化 |
strategy.py:220,273,292... |
已记录 |
四、修复建议(按优先级排序)
立即修复(高优先级)
base_symbol归一化:在trade_repository.py:292和realtime_kline_service_base.py:1324使用row.get("base_symbol") or ""。trading_signals压缩分段:新建迁移文件修复存量库,同时更新init_timescaledb.sql:460为'symbol, base_symbol'。
短期修复(中优先级)
- 删除遗留索引:在迁移文件或新建迁移中
DROP INDEX IF EXISTS idx_analysis_symbol_time; DROP INDEX IF EXISTS idx_analysis_kline_time;。 - 添加代码注释:在
realtime_kline_service_base.py:885附近说明(symbol, timeframe)节流的设计意图。
可选修复(低优先级)
- 策略入口防御:
strategy.py各公共方法改用key = (symbol, base_symbol or "")。
五、数据流与 Key 维度核对图(更新版)
WS/数据层(coin 维度,合理)
└─ "{coin}:{interval}" / "{coin}:l2Book"
↓
应用层(PairKey = (symbol, base_symbol))
├─ 策略 strategy.py ✅
├─ 仓位 position_manager.py ✅
├─ 黑名单缓存 ✅(但 DB 恢复时未归一化 ⚠️)
└─ 分析节流 recent_analysis((symbol, timeframe),有意设计,缺注释 ⚠️)
↓
数据库层
├─ analysis_results:PairKey 压缩 ✅,旧索引残留 ⚠️
├─ trading_signals:PairKey 索引 ✅,压缩分段未更新 🔴
├─ pair_positions:PairKey 索引 ✅,压缩 N/A
└─ trade_repository 查询:base_symbol 未归一化 🔴