7.14 回测验证:用历史数据检验你的策略
验证系统建好了,但它到底有没有效?这一篇讲怎么做一次可信的回测。
一、回测要回答的三个问题
| 问题 | 验证方法 |
|---|---|
| 方向判断准不准? | 命中率统计 |
| 判断有区分度吗? | 分层检验 |
| 长期能盈利吗? | ROI 与最大回撤 |
三个都要看。只看命中率会被误导。
二、四项基础指标
| 指标 | 计算 | 为什么重要 |
|---|---|---|
| 命中率 | 命中场次 / 总场次 | 基础能力 |
| ROI | 总预期收益 / 总投入 | 是否真的能盈利 |
| 最大回撤 | 资产曲线最大跌幅 | 风险承受度 |
| 平均数据数值 | 命中场次的平均数据数值 | 判断是否只押低赔方向 |
import numpy as np
import pandas as pd
def backtest_score(df, stake=1.0):
"""回测核心指标计算
df 需包含:hit(0/1)、odds(选择方向时数据数值)
"""
d = df.dropna(subset=['hit', 'odds']).copy()
if d.empty:
return None
n = len(d)
hits = int(d['hit'].sum())
hit_rate = hits / n
# 每场数据偏差:命中赚 (数据数值-1)×初始配置,未命中亏初始配置
d['pnl'] = np.where(d['hit'] == 1, (d['odds'] - 1) * stake, -stake)
total_pnl = float(d['pnl'].sum())
total_stake = n * stake
roi = total_pnl / total_stake
# 资产曲线与最大回撤
curve = d['pnl'].cumsum()
running_max = curve.cummax()
drawdown = curve - running_max
max_dd = float(drawdown.min())
return {
'场次': n,
'命中数': hits,
'命中率': round(hit_rate, 4),
'总数据偏差': round(total_pnl, 2),
'ROI': round(roi, 4),
'最大回撤': round(max_dd, 2),
'平均数据数值': round(float(d['odds'].mean()), 3),
'命中平均数据数值': round(float(d[d['hit'] == 1]['odds'].mean()), 3) if hits else None,
}
三、分层回测(比命中率更有综合性价比)
把预测按置信度分层,看命中率是否单调变化。
def layered_backtest(df, confidence_col='confidence', n_layers=4):
"""按置信度分层,检查命中率是否随置信度上升
如果高置信度层的命中率并不比低置信度层高,
说明你的「置信度」没有意义——这是很常见的发现。
"""
d = df.dropna(subset=[confidence_col, 'hit']).copy()
if len(d) < n_layers * 20:
return '样本不足'
d['layer'] = pd.qcut(d[confidence_col], q=n_layers, labels=False, duplicates='drop')
out = d.groupby('layer').agg(
场次=('hit', 'size'),
命中率=('hit', 'mean'),
).round(4)
out.index.name = '置信度分层(0=最低)'
return out
解读
| 结果 | 结论 |
|---|---|
| 命中率随置信度单调上升 | ✅ 置信度评分有意义 |
| 各层命中率接近 | ❌ 置信度没有区分能力,需要重新设计 |
| 高置信层反而更低 | ❌ 置信度算反了,或存在严重问题 |
四、回测的五个致命错误
错误一:数据穿越
用了未来信息。详见 7.10。
自查:回测结果好得不像真的,几乎一定是穿越。
错误二:用事后数据数值
回测时用了赛后的收盘数据数值,而真实场景下你只能在赛前拿到某个时点的数据数值。
正确做法:固定用某个赛前时点(如赛前 6 小时)的数据数值。
错误三:忽略实际可执行性
有些数据数值在现实中拿不到(限额、下架)。回测时必须考虑可执行性。
错误四:只看命中率
| 策略 | 命中率 | 平均数据数值 | ROI |
|---|---|---|---|
| A | 65% | 1.35 | −12% |
| B | 42% | 3.20 | +18% |
A 命中率高但亏损,B 命中率低但盈利。这就是为什么必须看 ROI。
错误五:样本量不足
| 样本量 | 可信度 |
|---|---|
| < 100 场 | 几乎无参考综合性价比 |
| 100 ~ 300 场 | 初步参考 |
| 300 ~ 1000 场 | 较为可信 |
| > 1000 场 | 具备统计意义 |
五、走步式回测(最可信的方式)
def walk_forward_backtest(df, build_model_fn, window_weeks=8):
"""走步式回测:模拟真实使用场景
用前 N 周数据建模,预测接下来 1 周,然后窗口前移。
每一步都只用「当时可得」的数据。
"""
df = df.sort_values('date').reset_index(drop=True)
weeks = sorted(df['week'].unique())
records = []
for i in range(window_weeks, len(weeks)):
train = df[df['week'].isin(weeks[i - window_weeks:i])]
test = df[df['week'] == weeks[i]]
if len(train) 0.5) == (row['outcome'] == 1)),
'odds': row['odds'],
'confidence': float(preds[k]),
})
return pd.DataFrame(records)
六、一份可信回测报告应该包含什么
| 项 | 说明 |
|---|---|
| 时间范围 | 明确起止日期 |
| 样本量 | 总场次数 |
| 覆盖赛事 | 哪些联赛/赛事 |
| 命中率 | 整体 + 分层 |
| ROI | 含计算口径说明 |
| 最大回撤 | 风险指标 |
| 基线对比 | 与简单策略的对比 |
| 失败样本 | 如实列出未命中的场次 |
最后一条最重要。
只展示成功案例的回测报告没有意义。可回溯的完整记录,是唯一能建立长期信任的东西。
这也是球小策坚持「历史记录不可删除」的原因(见 1.6)。
七、回测之后
拿到回测结果后,通常有三种结论:
| 结论 | 下一步 |
|---|---|
| 效果显著优于基线 | 小规模上线,持续监控 |
| 略优于基线 | 检查是否值得投入(考虑成本与精力) |
| 不如基线或与基线持平 | 从头检查特征设计,或接受「市场是有效的」 |
第三种结论是最常见也最诚实的结果。能接受它,才能做出真正有综合性价比的东西。
下一步该看什么
- 常见问题与支持 → 8.2 常见问题 FAQ
- 术语查询 → 8.4 足球数据术语表
足球赛事前瞻 | AI 智能分析 | 足球数据解读 - 球小策