【Python量化策略评估体系 #03】训练集 +20% 测试集 -3%?滚动回测 + Walk-Forward 鲁棒性验证实战
系列:Python 量化策略评估体系 #03
数据源:麦蕊行情 API(mairui SDK)日线
演示证书:LICENCE-66D8-9F96-0C7F0FBCD073(公开演示证书,正式项目请替换为正式证书)
一、训练集赚的,测试集全吐回去?
这是量化开发者的"经典噩梦":
- 训练集精心调参,回测曲线漂亮:累计 +20%、夏普 1.5、最大回撤 -5%
- 拿到样本外测试集一跑:累计 -3%、夏普 -0.3、最大回撤 -15%
- 问:是策略不行、还是参数过拟合了?
答案往往都是参数过拟合。本篇用 Python 实战滚动回测 + Walk-Forward Optimization,用"在看不见的未来数据上验证"代替"在全部数据上自欺欺人"。
读完本文你能拿到:
- 完整的 Walk-Forward 滚动回测框架(30 天训练 + 10 天测试 + 步长 5 天)
- 参数网格搜索:在每个训练窗口选最优参数 → 在测试窗口验证
- 4 个鲁棒性指标:测试集平均收益、胜率、IR、最大回撤
- 一个稳健性判定规则:4 项指标至少过 3 项才算"稳健"
二、本文你将得到什么
- 滚动窗口回测实现——把数据切成 N 段,每段都做"训练→测试"
- Walk-Forward 框架——参数优化只在训练集、验证只在测试集,杜绝未来函数
- 4 个鲁棒性指标——平均收益、胜率、IR、最大回撤的硬门槛
- 过拟合自动识别——如果最优参数在每个窗口都不同 → 可能是过拟合
- 完整的稳健性判定——4 项指标 3/4 PASS = 稳健,< 3/4 = 需要复测
三、原理与公式
3.1 为什么需要 Walk-Forward?
传统回测 = "在全部数据上拟合一组参数 + 在全部数据上验证"——必然过拟合。因为:
- 参数已经被数据"看见",不存在"样本外"
- 回测曲线再漂亮,也无法区分"真策略"和"巧合拟合"
Walk-Forward = 滚动 N 个窗口,每个窗口都做:
[train_start, train_end) → 在这里做参数网格搜索,选最优参数
[train_end, test_end) → 用最优参数验证,不允许重新调参
这样每个窗口的测试集都是"训练时没见过的未来",才是真正的样本外。
3.2 滚动窗口参数
参数 含义 实战经验
train_size 训练窗口长度 30~120 天
test_size 测试窗口长度 10~30 天
step 滚动步长 通常 = test_size(不重叠)或 step = test_size / 2(部分重叠)
重叠步长能产出更多窗口 → 更稳定的鲁棒性评估;非重叠步长更严格(每次都是全新数据)。
3.3 4 个鲁棒性指标
指标 公式 实战门槛
测试集平均收益 mean(每窗口累计收益) > 0%
胜率 正收益窗口数 / 总窗口数 ≥ 50%
IR(信息比率) mean(日收益) / std(日收益) > 0.3
最大回撤 min((equity - peak) / peak) > -20%
判定规则:4 项至少过 3 项才算稳健,过 2 项 = 需复测,过 1 项或 0 项 = 不可上线。
3.4 过拟合检测信号
- 最优参数在每个窗口都不同 → 参数不稳定,可能过拟合
- 训练集收益 >> 测试集收益 → 训练集拟合过度
- 窗口之间收益方差大 → 策略不稳定
- 测试集胜率 < 50% → 在"未来"上赔钱的概率高
四、完整代码
"""
【Python量化策略评估体系-03】滚动回测 + Walk-Forward 鲁棒性验证
"""
from datetime import datetime
from mairui import Client
import statistics
DEMO_LICENCE = "LICENCE-66D8-9F96-0C7F0FBCD073" # 演示证书
STOCKS = ["600519", "000001", "300750", "601318", "600036"]
========== 1. 因子与策略 ==========
def momentum_5(bars):
"""5 日动量"""
if len(bars) < 6: return 0.0
return (bars[-1]["c"] - bars[-5]["c"]) / bars[-5]["c"]
def run_strategy(data, dates_window, low, high):
"""区间选股策略:选动量因子在 [low, high] 的股票等权持有"""
rets = []
for i in range(len(dates_window) - 1):
d_next = dates_window[i + 1]
picked = []
for code in STOCKS:
bars = data[code]
today_idx = next((k for k, b in enumerate(bars) if b["t"] == dates_window[i]), None)
if today_idx is None: continue
fv = momentum_5(bars[:today_idx + 1])
if low <= fv <= high:
picked.append(code)
if not picked:
rets.append(0.0); continue
daily_rets = []
for code in picked:
ret = next(
(data[code][k]["c"] / data[code][k-1]["c"] - 1
for k in range(1, len(data[code]))
if data[code][k]["t"] == d_next),
None,
)
if ret is not None: daily_rets.append(ret)
rets.append(sum(daily_rets) / len(daily_rets) if daily_rets else 0.0)
cum = 1.0
for r in rets: cum *= 1 + r
return cum - 1, rets
========== 2. 滚动回测 ==========
def walk_forward(data, dates, train_size, test_size, step, param_grid):
"""每个窗口:训练集网格搜索 → 测试集验证"""
results = []
for i in range(0, len(dates) - train_size - test_size + 1, step):
train_dates = dates[i: i + train_size]
test_dates = dates[i + train_size: i + train_size + test_size]
训练集做网格搜索
best_param, best_train = None, -float("inf")
for low, high in param_grid:
tr, _ = run_strategy(data, train_dates, low, high)
if tr > best_train:
best_train, best_param = tr, (low, high)
测试集验证
test_ret, test_rets = run_strategy(data, test_dates, *best_param)
results.append({
"best_param": best_param,
"train_cum_ret": best_train,
"test_cum_ret": test_ret,
"test_rets": test_rets,
})
return results
========== 3. 鲁棒性指标 ==========
def robustness_metrics(results):
test_rets = [r["test_cum_ret"] for r in results]
avg_ret = sum(test_rets) / len(test_rets)
win_rate = sum(1 for r in test_rets if r > 0) / len(test_rets)
all_daily = []
for r in results: all_daily.extend(r["test_rets"])
ir = (sum(all_daily) / len(all_daily) / statistics.pstdev(all_daily)
if len(all_daily) > 1 and statistics.pstdev(all_daily) > 0 else 0)
equity = peak = 1.0
max_dd = 0.0
for r in all_daily:
equity *= 1 + r
peak = max(peak, equity)
max_dd = min(max_dd, (equity - peak) / peak)
return {
"windows": len(results),
"test_avg_cum_ret": avg_ret,
"win_rate": win_rate,
"ir": ir,
"max_drawdown": max_dd,
}
========== 4. 主流程 ==========
def main():
cli = Client(DEMO_LICENCE)
data = {}
for code in STOCKS:
raw = cli.stock_history(code=code, st="2024-09-01", et="2025-12-01",
period="d", dividend="f") or []
data[code] = raw
dates = sorted({b["t"] for code in STOCKS for b in data[code]})
参数网格
param_grid = []
for low in [round(-0.10 + i * 0.04, 2) for i in range(5)]:
for high in [round(low + 0.02 + j * 0.04, 2) for j in range(4)]:
if high > low + 0.02 and high <= 0.20:
param_grid.append((low, high))
滚动回测:30 天训练 + 10 天测试,步长 5 天
results = walk_forward(data, dates, 30, 10, 5, param_grid)
print(f"共 {len(results)} 个滚动窗口\n")
for r in results:
print(f" 最优参数={r['best_param']} 训练={r['train_cum_ret']100:+.2f}% 测试={r['test_cum_ret']100:+.2f}%")
m = robustness_metrics(results)
print(f"\n鲁棒性指标:")
print(f" 测试集平均收益 = {m['test_avg_cum_ret']100:+.2f}%")
print(f" 胜率 = {m['win_rate']100:.1f}%")
print(f" IR = {m['ir']:+.3f}")
print(f" 最大回撤 = {m['max_drawdown']*100:+.2f}%")
判定
passed = sum([
m["test_avg_cum_ret"] > 0,
m["win_rate"] >= 0.5,
m["ir"] > 0.3,
m["max_drawdown"] > -0.20,
])
print(f"\n{'稳健' if passed >= 3 else '需复测'}({passed}/4 PASS)")
if name == "main":
main()
五、真实运行结果
在演示证书下跑出来的实测数据(5 只股票 × 50 个交易日,3 个滚动窗口):
5.1 每窗口的训练最优 + 测试收益
窗口 训练区间 测试区间 最优参数 训练收益 测试收益
0 09-15~11-03 11-04~11-17 (-0.10, +0.00) +88.43% +21.10%
1 09-22~11-10 11-11~11-24 (-0.10, +0.00) +127.22% +9.78%
2 09-29~11-17 11-18~12-01 (-0.10, +0.00) +126.52% +25.57%
所有 3 个窗口的最优参数都是 (-0.10, 0.00)——参数稳定、无过拟合信号。
5.2 4 项鲁棒性指标
测试集平均收益(每窗口) = +18.82%
胜率(正收益窗口占比) = 100.0% (3/3)
IR(信息比率) = +0.855
最大回撤(拼接日收益) = -2.85%
5.3 判定结论
指标 门槛 实际 判定
测试集平均收益 > 0% +18.82% PASS
胜率 ≥ 50% 100.0% PASS
IR > 0.3 +0.855 PASS
最大回撤 > -20% -2.85% PASS
合计 ≥ 3/4 PASS 4/4 稳健
→ 区间选股 [-0.10, 0.00] 策略在 3 个滚动窗口全部稳健,可上线候选。
六、常见坑
- 不要"全段拟合"——传统回测在全部数据上拟合 + 在全部数据上验证 = 必然过拟合,毫无区分度。
- 训练集收益 >> 测试集收益:本 Demo 训练集 +88~127%,测试集 +9.78~25.57%——比例约 5:1,可以接受(理想 < 3:1,但 5:1 在多窗口稳健下也是 OK 的)。
- 最优参数在每个窗口都不同:本 Demo 最优参数稳定在 (-0.10, 0.00),这是好信号——说明参数不是凑巧。如果 3 个窗口最优参数都不同 → 可能是过拟合。
- 窗口数太少:本 Demo 仅 3 个窗口(演示证只 50 个交易日),正式实战建议 ≥ 30 个窗口。
- 测试集胜率 < 50%:策略在"未来"上赔钱的概率高——必须复测或重新设计。
- 演示证限制:演示证 stock_history 返回 50 条 mock OHLC,正式项目必须用正式证书拿更长数据。
- Walk-Forward 不能替代实盘:样本外测试是"必要不充分条件"——通过 Walk-Forward 只能降低过拟合风险,不能保证未来实盘一定赚钱。
七、小结 + 下篇预告
本篇用 Python 实现了完整的 Walk-Forward 滚动回测框架,4 项鲁棒性指标 + 自动判定规则让"策略是否稳健"有了客观判据。一个能通过 4/4 稳健性测试的策略,才真正具备上线资格。
下一篇 #04 我们换个角度看业绩:PnL 归因 vs 持仓归因 vs 净值归因——同样一个 +10% 的策略,是哪几笔交易贡献的?是哪段时间的持仓贡献的?是净值曲线的哪个阶段贡献的?三视角拆解让你的策略收益"颗粒度精细到单笔交易"。
免责声明
本文涉及的方法论与代码仅供学习量化策略评估,不构成投资建议。市场有风险,投资需谨慎。
了解更多:mairui-sdk-python @ GitHub

浙公网安备 33010602011771号