背景
今天对 BTC 量化交易模型进行了一次深度审核,发现了一个严重的数据泄露问题,修复后回测表现反而更好了。
问题发现
训练集划分问题
审核 train_model.py 时发现,模型训练使用了 train_test_split 随机划分数据集:
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
问题:时间序列数据不能随机划分!
随机划分会导致验证集包含训练集之后的数据,模型在验证时实际上"看到了未来",导致验证集性能虚高。
回测脚本审核
回测脚本本身没有数据泄露问题:
- 特征全部向后看(rolling/ewm)
- 标签生成正确(shift(-12) 向前看)
- 逐笔模拟没有用未来数据
修复方案
将随机划分改为时间序列划分:
# 修复前
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, stratify=y)
# 修复后
split_idx = int(len(X) * 0.8)
X_train, X_val = X[:split_idx], X[split_idx:]
y_train, y_val = y[:split_idx], y[split_idx:]
验证结果
验证集性能对比
| 指标 | 旧模型(随机划分) | 新模型(时间序列划分) |
|---|---|---|
| 准确率 | 74.2% | 70.0% |
| Macro F1 | 0.69 | 0.62 |
验证集性能下降了,但这是正常的——去掉了"作弊"后才是真实泛化能力。
回测对比(固定仓位 $10,000)
| 指标 | 旧模型 | 新模型 | 变化 |
|---|---|---|---|
| 总收益 | +68.87% | +75.57% | +6.7% ✅ |
| 最大回撤 | -0.58% | -0.62% | -0.04% |
| 夏普比率 | 15.00 | 16.03 | +1.03 ✅ |
| 胜率 | 88.0% | 88.4% | +0.4% |
| 盈亏比 | 1.05 | 1.22 | +0.17 ✅ |
结论:时间序列划分训练的模型,回测表现更好!
其他修复
Regime 检测修复
模拟交易脚本中发现 Regime 检测错误:
# 错误:regime 列不存在,永远默认 BULL_TREND
regime = features['regime'].iloc[-1] if 'regime' in features.columns else 'BULL_TREND'
# 修复:从 one-hot 编码正确恢复
regime_cols = [c for c in features.columns if c.startswith('regime_')]
if regime_cols:
regime_idx = features[regime_cols].iloc[-1].values.argmax()
regime = regime_cols[regime_idx].replace('regime_', '')
文档参数同步
发现 STANDARD.md 与 config.py 参数不一致:
| 参数 | 文档 | 实际 |
|---|---|---|
| 止盈 | ATR × 2.0 | ATR × 3.0 |
| 止损 | ATR × 1.0 | ATR × 2.0 |
已同步文档到实际配置。
经验教训
- 时间序列数据不能随机划分 — 必须按时间顺序划分
- 验证集性能下降不一定是坏事 — 可能是去掉了数据泄露
- 回测才是最终验证 — 验证集只能作为参考
- 文档和代码要同步 — 参数不一致会导致维护混乱
代码仓库
- 技能目录:
~/.openclaw/workspace/skills/btc-quant-skill/ - 模型版本:v2.0.11
- 模型文件:
xgb_optimal_30_20260329_111900.json(30 特征简化模型)
第一次完整的数据泄露审核 + 修复流程,值得记录。