全系统Key改为配对维度bug2
配对维度优化后系统缺陷分析报告
分析日期:2026-02-19
分析范围:全系统 Key 改为 (symbol, base_symbol) 配对维度后的代码质量审查
结论:配对维度架构整体一致,发现 3 个真实缺陷,1 个设计改进建议
整体评估
| 维度 | 状态 | 说明 |
|---|---|---|
| 配对 Key 架构一致性 | ✅ 强一致 | (symbol, base_symbol) 贯穿全系统所有模块 |
| 策略参数三层优先级(查询) | ✅ 正确 | pair > symbol > global 在 get_strategy_params 中实现 |
| 数据库映射 | ✅ 一致 | SQL WHERE 子句与内存 key 格式完全对应 |
| 并发安全 | ⚠️ 部分缺陷 | 一处状态恢复未持锁(设计不对称) |
| 配置构建 | ❌ 功能缺陷 | 配对级参数构建时跳过了币种级回退 |
| 信号就绪判断 | ⚠️ 阈值偏低 | 25% 填充率不满足统计可靠性要求 |
Bug #1 — _execute_close 正常失败路径状态恢复无锁
文件:src/trading/position_manager.py
行号:527(出错点),对比 280(正确加锁)和 287(正确加锁)
严重性:🟡 中(设计违规,Python GIL 下实际触发概率低)
完整因果链
输入
└── 止损监控 / 信号处理 → 调用 close_position(symbol, base_symbol)
状态变化
└── pm.py:272 with self._lock:
pm.py:280 position.status = CLOSING ✅ 在锁内设置
调用路径
└── pm.py:283 return self._execute_close(key, position, ...)
└── pm.py:411 market_close(position) → 交易所返回 success=False
└── pm.py:526 else: ← Leg A 失败分支
pm.py:527 position.status = PositionStatus.OPEN ❌ 无锁!
出错点
├── 成功路径 pm.py:494: with self._lock: self._positions.pop() ✅
├── 异常路径 pm.py:285: with self._lock: position.status = OPEN ✅
└── 正常失败 pm.py:527: position.status = PositionStatus.OPEN(无锁) ❌
根因
└── _execute_close 在锁外运行(锁在 close_position 入口归还)。
三条"结束平仓"路径中:成功路径在锁内 pop,异常路径在锁内恢复状态,
但正常失败路径(Leg A 失败,正常 return None)直接修改属性,未持锁。
两条"恢复为 OPEN"路径行为不对称,违反设计约定。
并发场景分析
| 时序场景 | 实际行为 | 是否有害 |
|---|---|---|
| T1 在执行 line 527 之前,T2 调用 close_position | T2 进锁看到 CLOSING → return None(短暂屏蔽) | 低风险 |
| T1 执行 line 527 后,T2 调用 close_position | T2 进锁看到 OPEN → 正常执行 | 无害 |
修复方案:
# position_manager.py line 526-527
else:
with self._lock: # ← 新增
position.status = PositionStatus.OPEN
Bug #2 — 配对级参数构建不继承币种级,直接回退全局默认
文件:src/trading/config.py
行号:362-386(函数定义),481(调用点)
严重性:🔴 高(静默功能缺陷,参数错误但无报错)
完整因果链
输入(用户预期:PURR|HYPE 继承 PURR 的 EMA=72,只调整阈值)
├── TRADING_STRATEGY_OVERRIDE_SYMBOLS=PURR
├── TRADING_STRATEGY_PURR_EMA_SPAN=72
├── TRADING_STRATEGY_OVERRIDE_PAIRS=PURR__HYPE
└── TRADING_STRATEGY_PURR__HYPE_ADAPTIVE_THRESHOLD=2.0
状态变化(config.py:480-481)
├── 第1步: symbol_overrides = _load_symbol_strategy_overrides(global_cfg)
│ → {"PURR": StrategyParams(ema_span=72, ...)} ✅
└── 第2步: pair_overrides = _load_pair_strategy_overrides(global_cfg)
↑ 只传入 global_cfg,不传入第1步的结果!
调用路径(config.py:362-386)
└── def _load_pair_strategy_overrides(global_cfg: dict): ← 签名缺少 symbol_overrides
for pair in ["PURR__HYPE"]:
overrides["PURR|HYPE"] = _build_strategy_params(
"TRADING_STRATEGY_PURR__HYPE_",
global_cfg ← 回退来源是全局配置
)
出错点(_build_strategy_params,config.py:311-340)
└── os.getenv("TRADING_STRATEGY_PURR__HYPE_EMA_SPAN") → None(未设置)
return global_cfg["ema_span"] ← 返回 36(全局默认)❌
期望返回: 72(PURR 币种级)
根因
└── _load_pair_strategy_overrides 函数签名只接收 global_cfg 一个参数(line 362-363)。
load_trading_config(line 481)调用时未传入已构建的 symbol_strategy_overrides。
三层优先级在【查询时】(get_strategy_params)实现正确,
但在【构建时】(_load_pair_strategy_overrides)只有两层(pair_env > global),
静默跳过了 symbol 层。系统不会产生任何报错或警告。
数字验证
| 配置层 | 键 | ema_span 实际值 |
期望值 |
|---|---|---|---|
| 全局默认 | — | 36 | — |
| 币种级 | PURR | 72 | 72 ✅ |
| 配对级 | PURR|HYPE | 36(错误) | 72 ❌ |
修复方案:
# config.py — load_trading_config 内部(约 line 480-481)
_symbol_overrides = _load_symbol_strategy_overrides(_global_strategy_cfg)
config = TradingConfig(
...
symbol_strategy_overrides=_symbol_overrides,
pair_strategy_overrides=_load_pair_strategy_overrides(
_global_strategy_cfg,
_symbol_overrides, # ← 新增:传入币种级参数
),
)
# config.py — _load_pair_strategy_overrides 函数签名和内部逻辑(约 line 362)
def _load_pair_strategy_overrides(
global_cfg: dict,
symbol_overrides: dict[str, StrategyParams], # ← 新增参数
) -> dict[str, StrategyParams]:
...
for pair in pairs:
alt_asset = pair.split("__")[0]
# 若存在币种级覆盖,以其为基础;否则回退全局
if alt_asset in symbol_overrides:
sym_p = symbol_overrides[alt_asset]
base_cfg = {
"ema_span": sym_p.ema_span,
"std_window": sym_p.std_window,
"adaptive_threshold": sym_p.adaptive_threshold,
"min_zscore_abs": sym_p.min_zscore_abs,
"reversion_factor": sym_p.reversion_factor,
"cooldown_minutes": sym_p.cooldown_minutes,
"stop_loss_pct": sym_p.stop_loss_pct,
"max_hold_hours": sym_p.max_hold_hours,
}
else:
base_cfg = global_cfg
pair_key = pair.replace("__", "|", 1)
overrides[pair_key] = _build_strategy_params(
f"TRADING_STRATEGY_{pair}_", base_cfg # ← 使用正确的回退源
)
验证:
# 单元测试断言
config = load_trading_config() # 使用上述环境变量
params = config.get_strategy_params("PURR/USDC:USDC", "HYPE/USDC:USDC")
assert params.ema_span == 72 # 继承自 PURR 币种级
assert params.adaptive_threshold == 2.0 # 来自 PURR|HYPE 配对级覆盖
Bug #3 — is_ready 缓冲区就绪阈值过低(25%)
文件:src/trading/strategy.py
行号:134(is_ready),386(_compute_adaptive_z),141(any_ready)
严重性:🟡 中(早期信号质量差,服务重启后窗口期内可能误开仓)
完整因果链
输入
└── 服务重启,PURR|HYPE 配对开始逐条接收 K 线(std_window=72 by default)
状态变化(每条新 K 线)
└── _process_tick_unlocked() → 更新 Welford 统计量
bl.std_window: 1 → 2 → ... → 18 条(第 18 条时触发就绪)
调用路径(第 18 条 K 线)
├── strategy.py:447 if not self.is_ready(symbol, base_symbol): return None
│ └── strategy.py:134 min_required = max(10, 72 // 4) = 18
│ len(bl.std_window) = 18 >= 18 → True ← 仅 25% 就认为就绪 ❌
│
└── strategy.py:457 adaptive_z = _compute_adaptive_z(bl, z4h)
└── strategy.py:386 if n < max(10, maxlen // 4): return None
← 同样的 25% 阈值,通过检查
└── strategy.py:388 variance = bl.welford_m2 / (n-1) = m2 / 17
← 自由度仅 17,样本量远低于统计可靠基准(n=30)
└── strategy.py:392 return (z4h - ema) / std
← std 基于不稳定方差,可能偏小 → adaptive_z 被放大 → 误触阈值 → 虚假开仓信号
出错点
└── strategy.py:134(is_ready)和 strategy.py:386(_compute_adaptive_z)
均使用 `max(10, bl.std_window.maxlen // 4)` 作为最小样本要求
根因
└── 25% 阈值(maxlen // 4)是武断设定,缺乏统计依据。
Welford 增量方差在小样本时误差大:
- std_window=72,25%=18 点:自由度 17,方差估计标准误 ≈ σ²·√(2/17) ≈ 34% 的相对误差
- std_window=72,50%=36 点:自由度 35,相对误差降至 ≈ 24%
早期 std 偏低 → adaptive_z 被放大 → 误触入场阈值 → 虚假开仓。
数量化对比
| 参数 | 当前(25%=// 4) |
建议(50%=// 2) |
|---|---|---|
std_window=72 就绪需 |
18 点,自由度 17 | 36 点,自由度 35 |
std_window=36 就绪需 |
10 点(被 max 截断) | 18 点,自由度 17 |
| 方差估计相对误差(std_window=72) | ~34% | ~24% |
| 服务重启后不可靠窗口 | 前 18 条 K 线 | 前 36 条 K 线 |
修复方案:
# strategy.py line 134(is_ready)
min_required = max(10, bl.std_window.maxlen // 2) # 25% → 50%
# strategy.py line 141(any_ready,同步修改)
min_required = max(10, bl.std_window.maxlen // 2)
# strategy.py line 386(_compute_adaptive_z,同步修改)
if n < max(10, bl.std_window.maxlen // 2):
return None
三处需同步修改,保持一致。
设计改进建议(非 Bug)
配对缓存无定期刷新(realtime_kline_service_base.py)
影响:服务运行中若 DB 新增配对,需重启才能生效
建议:在现有健康监控循环中,每 5 分钟调用一次 _load_pair_cache()
优先级:🟠 低(运维便利性,不影响已有配对)
修复优先级
| 优先级 | Bug | 原因 |
|---|---|---|
| 🔴 立即 | Bug #2(配置级联) | 静默功能错误,影响所有配置了配对级覆盖的用户,且无任何报警 |
| 🟡 近期 | Bug #3(阈值偏低) | 影响重启后的早期信号质量,与风控关联 |
| 🟢 日后 | Bug #1(无锁) | 设计不对称,Python GIL 下实际触发概率低 |