IMM Beta 导致 Adaptive Z 虚假入场信号 — BUG 分析报告
IMM Beta 导致 Adaptive Z 虚假入场信号 — BUG 分析报告
日期: 2026-03-19
严重程度: P0(已造成实际错误交易)
影响范围: 所有使用 IMM Kalman β 的交易配对
状态: 待修复
1. 事件摘要
2026-03-19 14:25:05,IMM 改造后的新代码对 ASTER/USDC:USDC|BTC/USDC:USDC 配对发出了错误的 long 入场信号,导致以 $0.68924 买入 29.0 ASTER。该信号在老代码(无 IMM 改造)中不存在。仓位于 33 秒后被均值回归退场机制平仓,亏损 $0.01。
1.1 对比日志(同一时刻 14:25:05,同一 pair,同一 z4h)
| 指标 | 新代码(IMM) | 老代码(无 IMM) |
|---|---|---|
| z4h | -1.5430 | -1.5430 |
| ema | 1.4326 | -1.1029 |
| raw_std | 1.0235 | 1.8705 |
| reg_std | 1.0666 | 1.8944 |
| adaptive_z | -2.7898 (139%thresh) | -0.2323 (12%thresh) |
| 触发入场 | ✅ 是(错误) | ❌ 否(正确) |
1.2 关键观察
- z4h 相同:当前时刻的 z4h 计算结果在两个版本中一致(-1.5430)
- ema 差距巨大:1.4326 vs -1.1029,差距 2.54(方向都反了,一正一负)
- std 差距巨大:1.0235 vs 1.8705,新代码的 std 仅为老代码的 55%
- adaptive_z 公式:
az = (z4h - ema) / reg_std- 新代码:(-1.5430 - 1.4326) / 1.0666 = -2.7898 → 触发入场
- 老代码:(-1.5430 - (-1.1029)) / 1.8944 = -0.2323 → 不触发
1.3 事件时间线
14:25:05 新代码 tick(candle) → az=-2.7898 (139%thresh) → 入场信号触发
14:25:05 新代码 买入 ASTER 29.0 @ $0.68924
14:25:07 订单成交
14:25:29 配对关系缓存重新加载(可能是服务组件重启)
14:25:35 仓位从 DB 恢复同步(entry_adaptive_z=-2.7898)
14:25:38 退场信号触发(az=-0.2333 已远超回归目标 -0.6974)
14:25:40 平仓成交 @ $0.68926,PnL=-$0.01,持仓 33 秒
2. 根因分析
2.1 BUG 定位
文件: src/utils/analysis/analysis_core.py
行号: 566-568(calculate_cointegration_params_dual_window 函数)
_n_updates = kalman_state_out.get('n_updates', 0) if kalman_state_out else 0
_kalman_warm = kalman_result is not None and _n_updates >= IMM_KALMAN_WARMUP
beta_for_spread = kalman_result['beta'] if _kalman_warm else beta_ols # ← BUG
IMM Kalman 热身完成后(n_updates >= 20),z4h 的 spread 计算从 OLS β 切换为 Kalman β。这一改动破坏了 z4h 时间序列的平稳性。
2.2 问题传播链
┌─────────────────────────────────────────────────────────────────────┐
│ IMM Kalman β 时变演化(如 0.9 → 0.5 → 0.64) │
│ ↓ │
│ 每次 tick 用当前 Kalman β 计算 spread → 计算 z4h │
│ ↓ │
│ z4h 时间序列引入人为水平漂移(β 变化 → spread 结构变化 → z4h 偏移) │
│ ↓ │
│ z4h 写入 analysis_results 表(DB 数据被"污染") │
│ ↓ │
│ strategy prime_buffer 从 DB 加载历史 z4h → EMA/STD 反映污染值 │
│ ↓ │
│ 实时 z4h(当前 Kalman β 恰好≈OLS β)回到"正常"值 │
│ ↓ │
│ adaptive_z = (正常z4h - 漂移ema) / 偏小std → 虚假大信号 │
└─────────────────────────────────────────────────────────────────────┘
2.3 为什么 EMA 能漂移 2.5?
策略引擎的 EMA 和 STD 跟踪的是 z4h 的历史值(存储在 SymbolBaseline.std_window 中,maxlen=72)。这些历史值有两个来源:
- prime_buffer:服务启动时从
analysis_results表加载最近 144 条 z4h - process_tick:实时运行时每根新 K 线追加 z4h
两个来源的 z4h 都使用了 Kalman β 计算,因此整个 buffer 都被污染。
单次 z4h 偏移量级估算
Δz4h ≈ (β_ols - β_kalman) × (log_base[-1] - mean(log_base)) / std_spread
典型值(ASTER/BTC):
- Δβ = 0.26(OLS β=0.90, Kalman β=0.64)
- BTC 趋势 3-5%: |log_base[-1] - mean(log_base)| ≈ 0.015-0.025
- spread_std ≈ 0.02-0.08
→ 单次 Δz4h ≈ 0.05 ~ 0.5(取决于行情)
单次偏移 0.05~0.5 看似不大,但:
- Kalman β 是时变的:在不同历史时段,β 偏差可能更极端(P_β=0.098 → β 的 95% CI 跨度 ±0.62)
- 偏移方向可持续:当 BTC 处于单边趋势时,Δz4h 方向一致,累积效应放大
- DB 历史数据跨度长:prime_buffer 加载 30 天内的数据,覆盖 Kalman β 演化全程
- EMA 收敛到偏移均值:如果历史 z4h 平均值偏移 +2.5,EMA 就会收敛到 +2.5
2.4 为什么当前 z4h 在两个版本中相同?
z4h 在 30 点窗口内做归一化:
z4h = (spread[-1] - mean(spread[:-1])) / std(spread[:-1])
所有 30 个点使用同一个 β(当前 Kalman β=0.6438)。归一化会吸收 β 的大部分影响——改变 β 相当于对 spread 做一个近似线性变换,mean/std 会相应调整,z4h 变化不大。
但不同时间点的 z4h 使用不同的 β(Kalman β 时变),这导致 z4h 序列不可比较。策略层的 EMA/STD 假设 z4h 是平稳序列,这一假设被破坏。
3. 影响范围
3.1 直接影响
- 所有 Kalman 热身完成(n_updates >= 20)的交易配对,其 z4h 均受 Kalman β 影响
- z4h 写入
analysis_results表,DB 中 IMM 部署后的 z4h 数据均被污染 - strategy 的
prime_buffer加载这些数据,EMA/STD 偏移
3.2 信号影响模式
| 场景 | adaptive_z 表现 | 后果 |
|---|---|---|
| Kalman β 曾大幅偏离 → 近期回归 | EMA 滞后于实际 z4h → az 虚假放大 | 虚假入场信号 |
| Kalman β 稳定但与 OLS β 不同 | z4h 持续偏移但 EMA 跟上 → az 正常 | 信号正常但回归目标偏移 |
| Kalman β 突然跳变 | z4h 突变 → az 尖峰 | 突发虚假信号或错失真信号 |
3.3 受影响代码路径
calculate_cointegration_params_dual_window() ← β 替换点
→ spread 使用 Kalman β
→ calculate_zscore_ols() 使用该 spread
→ z4h 被 Kalman β 影响
→ 写入 analysis_results 表
→ 传给 strategy.process_tick()
→ 更新 EMA/STD buffer
→ 计算 adaptive_z
→ 产生虚假入场/退场信号
4. 修复方案
4.1 核心修复:z4h spread 始终使用 OLS β
文件: src/utils/analysis/analysis_core.py
将第 568-569 行:
beta_for_spread = kalman_result['beta'] if _kalman_warm else beta_ols
alpha_for_spread = kalman_result['alpha'] if (_kalman_warm and use_alpha) else alpha
改为:
beta_for_spread = beta_ols
alpha_for_spread = alpha
原则:
- z4h/spread → 始终用 OLS β(保证时间序列平稳性,供策略层 EMA/STD 消费)
- Kalman β → 仅作为
kalman_beta字段输出,供交易执行层用作对冲比率
4.2 修复后的过渡期处理
修复代码后,DB 中仍存有被污染的历史 z4h 数据。strategy 的 prime_buffer 会加载这些数据。
自然恢复:buffer maxlen=72,每 5 分钟更新一次,约 6 小时后 buffer 完全被新数据替换,EMA 自动修正。
加速恢复(可选):
- 重启服务后手动清理
analysis_results中 IMM 部署后的 z4h 数据 - 或临时增大
min_required(prime_buffer 最小就绪条件),强制等更多正确数据积累后再产生信号
4.3 关于保留 Kalman β 用于 spread 的讨论
原设计意图是用 Kalman β(更精确的时变对冲比率)替代 OLS β 来构建 spread,提升 z4h 对均值回归的灵敏度。这一思路在单次 z4h 计算内是合理的——Kalman β 可能更好地捕捉当前市场结构。
但问题在于策略层的 adaptive z-score 模型假设 z4h 是平稳序列:
adaptive_z = (z4h - EMA(z4h)) / STD(z4h)
Kalman β 的时变性破坏了 z4h 的平稳性。每个历史 z4h 用不同的 β 计算,它们不具备统计可比性。EMA/STD 在不可比序列上计算的结果没有意义。
如果未来想让 z4h 使用时变 β,需要同时修改策略层:
- 对 β 变化做 z4h 校准(补偿 β 变化引起的 z4h 水平漂移)
- 或在 β 发生显著变化时重置 EMA/STD buffer
- 或使用 β-invariant 的 z4h 定义
在当前策略架构下,z4h 必须使用稳定的 β。
5. 验证方法
5.1 修复前验证
对比同一 pair 在修复前后的 z4h / ema / adaptive_z:
# 在 calculate_cointegration_params_dual_window 中临时加日志:
logger.info(
f"[β对比] {pair_name} | "
f"OLS β={beta_ols:.4f} Kalman β={kalman_result['beta']:.4f} "
f"Δ={kalman_result['beta'] - beta_ols:+.4f} | "
f"z4h_OLS={z4h_with_ols:.4f} z4h_Kalman={z4h_with_kalman:.4f} "
f"Δz4h={z4h_with_kalman - z4h_with_ols:+.4f}"
)
5.2 修复后验证
- 部署修复代码后,观察 ASTER/BTC 配对的 tick 日志
- 确认 ema 逐步从 1.4326 向负值回归(与老代码趋同)
- 确认不再产生类似的虚假入场信号
- 6 小时后确认 EMA 已完全修正
5.3 回归测试
确认以下功能不受影响:
- Kalman β 仍正常输出(
kalman_beta字段) - Gate3 体制检测(
kalman_regime_score)仍正常工作 - Kalman state 持久化/恢复仍正常
- 对冲比率(hedge ratio)仍使用 Kalman β
6. 经验教训
- 信号输入的平稳性是策略层的前提:任何改变 z4h 计算方式的改动,必须验证 z4h 时间序列的平稳性不被破坏
- 时变参数不能直接用于构建被统计模型消费的时序:Kalman β 用于 z4h → EMA → adaptive_z 的链条中,时变性在每个环节都被放大
- DB 写入的 z4h 有长期记忆:prime_buffer 从 DB 加载历史数据,任何计算方式的变更都会在 DB 中留下不一致的数据
- 双版本对比是发现此类 BUG 的有效手段:本次 BUG 正是通过同时运行新旧代码对比发现的