全系统key配对升级 bug10
系统缺陷分析报告:配对维度 Key 优化后的状态
Context
系统已完成全局 Key 改为配对维度(PairKey = (symbol, base_symbol))的优化。
本报告分析优化后的代码中仍然存在的缺陷和不足,按严重程度分级。
🔴 高严重性缺陷
缺陷 1:symbol_blacklist 未做大写转换(config.py:425-432)
问题:黑名单解析时没有调用 .upper(),但检查时使用的是已大写的 coin 名称,导致大小写不一致时黑名单完全失效。
# load_trading_config() 中(config.py 425-432)
symbol_blacklist = {
s.strip() # ❌ 未调用 .upper()
for s in blacklist_raw.split(",")
if s.strip()
}
# is_symbol_allowed() 中(config.py 253-254)
alt = symbol.split('/')[0] # alt = "PURR"(来自交易所,天然大写)
# 检查:alt in self.symbol_blacklist
# 若用户配置 TRADING_SYMBOL_BLACKLIST=purr,则 "PURR" in {"purr"} = False
对比:_parse_close_disabled_symbols、_load_symbol_strategy_overrides、_load_pair_strategy_overrides 都正确地做了 .upper()。
位置:src/trading/config.py 第 425-432 行
影响:用户输入小写 coin 名称时黑名单静默失效,无任何错误提示
缺陷 2:监控线程在策略引擎初始化之前启动(orchestrator.py:128-144)
问题:止损监控线程和仓位同步线程在 AdaptiveBollingerStrategy 初始化之前启动,期间 self._strategy = None。
# orchestrator.py start() 中
# 1. 启动监控线程(self._strategy = None)
self._stop_loss_thread.start() # 第128行
self._sync_thread.start() # 第136行
# 2. 才初始化策略引擎
self._strategy = AdaptiveBollingerStrategy(self._config) # 第144行
# 3. 再同步仓位到策略
for pos in self._position_manager.open_positions:
self._strategy.sync_position(...) # 第181行
风险场景:若在步骤 1-3 之间,同步线程发现幽灵仓位并关闭,策略引擎 on_position_closed 由于 self._strategy is None 跳过。此后策略引擎不会获得该仓位关闭的通知,状态不一致。
虽然 closed_pairs 中的仓位不会出现在 open_positions(已删除),所以步骤 3 的 sync_position 不会重新注册它——结果实际上是 OK 的。但这是一个脆弱的初始化顺序,存在逻辑上的竞态窗口。
位置:src/trading/orchestrator.py 第 128-144 行
建议修复:先初始化策略引擎,再启动监控线程
🟡 中等严重性缺陷
缺陷 3:_close_ops 告警丢失 base_symbol(position_manager.py:906-910)
问题:同步关闭仓位时,_close_ops 元组中没有存储 base_symbol,导致告警中看不到配对信息。
# position_manager.py 第906-910行
_close_ops.append((
pos.position_id, now, symbol,
pos.direction, pos.alt_entry_price, pos.alt_size,
# ❌ 缺少 pos.base_symbol
))
# 告警循环(第963行)
for _, _, symbol, direction, entry_price, size in _close_ops:
sender_colourful(
title=f"仓位对账: {symbol}", # ❌ 只有 symbol,无 base_symbol
...
)
影响:PURR|HYPE 配对仓位被自动关闭时,运营只能看到 "仓位对账: PURR/USDC:USDC",不知道是哪个配对被关闭,排查困难。
缺陷 4:_close_with_retry 告警不含 base_symbol(orchestrator.py:703-710)
# orchestrator.py 第703-710行
sender_colourful(
title=f"🚨 平仓重试全部失败: {symbol_to_coin(pos.symbol)}",
content=(
f"**币种**: {pos.symbol}\n"
f"**方向**: {pos.direction}\n"
# ❌ 缺少 pos.base_symbol
...
),
)
缺陷 5:on_entry_signal 可被外部调用绕过 base_symbol 非空验证
问题:process_analysis 强制要求 base_symbol 非空(第296-299行),但 on_entry_signal 是公开方法,可直接被外部调用,构造 base_symbol="" 的信号,导致 key 为 (symbol, "") 的仓位与配对维度设计不符。
# orchestrator.py process_analysis(第296-299行)
base_symbol = multi_period_result.get("base_symbol", "")
if not base_symbol:
logger.error(f"process_analysis: base_symbol 缺失,跳过本次 tick | {symbol}")
return False # ✅ 有验证
# 但 on_entry_signal 无此验证(第425行)
def on_entry_signal(self, symbol, multi_period_result, ...):
signal = PairTradeSignal(
base_symbol=multi_period_result.get("base_symbol", ""), # ❌ 可为空
...
)
缺陷 6:_signal_history 信号去重键设计与注释不符(strategy.py:542)
问题:代码注释说 "同一配对+方向+K线时间内不重复入场",但实现用的是 tick 处理时间(秒级) 而非 kline_time:
# strategy.py 第542行
signal_key = f"{symbol}:{base_symbol}:{entry_signal.direction}:{int(timestamp.timestamp())}"
# ^^^ tick处理时间,非kline_time
实际效果:同一 K 线在不同 tick 时间到来时会产生不同的 signal_key,去重几乎没有实际效果(真正的防重依赖 _prev_above_threshold 的"非首次突破"逻辑)。
位置:src/trading/strategy.py 第 542 行
缺陷 7:_load_pair_strategy_overrides 回退逻辑只考虑 alt 资产(config.py:393-408)
问题:配对级参数的回退来源只检查 alt 资产(如 PURR)的币种级覆盖,完全忽略 base 资产(如 HYPE)的币种级覆盖:
# config.py 第393-408行
alt_asset = parts[0]
if alt_asset in symbol_overrides: # 只检查 alt
base_cfg = {symbol_overrides[alt_asset]}
else:
base_cfg = global_cfg # ❌ 未考虑 base 资产的覆盖
用户配置了 TRADING_STRATEGY_OVERRIDE_SYMBOLS=HYPE 后,PURR|HYPE 配对不会继承 HYPE 的参数。
🟢 低严重性 / 信息完整性问题
缺陷 8:any_ready 属性读取未加锁(strategy.py:139-145)
@property
def any_ready(self) -> bool:
for key, bl in self._baselines.items(): # ❌ 无 self._lock
...
实际上只在 start() 日志中使用一次,此时后台线程尚未运行,风险极低。但从代码规范角度,应与其他只读操作保持一致。
缺陷 9:on_exit_signal 使用 O(n) 遍历而非 O(1) dict 查找(orchestrator.py:558-561)
# orchestrator.py 第558-561行
if (symbol, base_symbol) not in {
(p.symbol, p.base_symbol or "") for p in self._position_manager.open_positions
}:
open_positions 返回的是副本列表,查找复杂度为 O(n)。PositionManager._positions 是 dict,应暴露 has_position(symbol, base_symbol) 方法实现 O(1) 查找。
缺陷 10:孤儿仓位均值回归退出被永久禁用
设计意图但存在风险:孤儿仓位(base_symbol="",entry_adaptive_z=0.0)在策略层永远不会产生退出信号:
# strategy.py 第587-589行
baseline = tracker.entry_adaptive_z
if abs(baseline) < 1e-9:
return None # 孤儿仓位直接跳过
孤儿仓位只能靠固定止损 / 移动止损 / 持仓超时退出,这是设计意图。但持仓超时默认 72h,在市场不利时风险敞口较大。
缺陷 11:_calculate_pnl_pct 相对名义价值而非保证金(risk_manager.py:199)
止损百分比 stop_loss_pct=0.03 是相对名义价值,不是保证金。在 3 倍杠杆下,名义价值的 3% = 保证金的 9% 亏损。这可能与用户直觉不符(用户可能认为 3% 是保证金止损),需文档明确。
关键文件位置速查
| 缺陷编号 | 文件 | 行号 |
|---|---|---|
| 1(黑名单大小写) | src/trading/config.py |
425-432 |
| 2(线程顺序) | src/trading/orchestrator.py |
128-144 |
| 3(close_ops 缺 base_symbol) | src/trading/position_manager.py |
906-910, 963 |
| 4(retry 告警缺 base_symbol) | src/trading/orchestrator.py |
703-710 |
| 5(on_entry_signal 验证) | src/trading/orchestrator.py |
425, 461 |
| 6(signal_history 键) | src/trading/strategy.py |
542 |
| 7(pair 参数回退) | src/trading/config.py |
393-408 |
| 8(any_ready 无锁) | src/trading/strategy.py |
139-145 |
| 9(O(n) 查找) | src/trading/orchestrator.py |
558-561 |
| 10(孤儿退出) | src/trading/strategy.py |
587-589 |
| 11(PnL 计算语义) | src/trading/risk_manager.py |
199 |