Walk-forward 优化¶
Backtest.optimize() 在整个数据集上挑选最佳参数组合 —— 然后汇报样本内表现。这是拟合噪声的经典配方。看到的「最优 Sharpe 2.4」实盘几乎从不成立。
Backtest.walk_forward_optimize() 在滚动窗口内做同样的网格搜索,在策略从未调过参的数据上汇报表现:
train test
window 0: [─────────────────────][──────]
window 1: [─────────────────────][──────]
window 2: [─────────────────────][──────]
step
每个窗口对里,QTrade 在训练片段挑选最佳参数,并在紧接的测试片段上评估。测试片段的聚合结果是策略真实表现的诚实样本外(OoS)估计。
API¶
result = bt.walk_forward_optimize(
train_window=200,
test_window=50,
maximize='Sharpe Ratio',
step=50,
constraint=lambda p: p['n1'] < p['n2'],
n1=range(5, 30, 5),
n2=range(20, 60, 10),
)
必填:
train_window:每个训练片段的 bar 数。test_window:每个测试片段的 bar 数(紧跟训练片段之后)。maximize:在每个训练片段内最大化的calculate_stats()指标名。常见选择:Sharpe Ratio、Total Return [%]、Calmar Ratio。含义详见 统计指标词汇表。**params_grid:和optimize()同语法 —— 关键字参数,值是候选项的可迭代对象。
可选:
step:相邻训练起点之间推进多少 bar。默认test_window(测试窗口不重叠)。更小值产生重叠(相关)的测试窗口;更大值留下空隙。constraint:评估前应用到每个参数字典的过滤器(lambda p: bool)。用于跳过无意义组合(如快线 SMA 窗口 > 慢线窗口)。
解读结果¶
result 是一个 dict:
result['windows'] # list of per-window dicts
result['summary'] # aggregate OoS metrics
每个 window dict 包含:
键 |
值 |
|---|---|
|
训练片段的时间戳。 |
|
测试片段的时间戳。 |
|
在训练片段上最大化 |
|
用 |
|
测试片段跑出来的完整 stats dict。 |
|
测试片段的 |
summary 包含:
键 |
含义 |
|---|---|
|
评估的训练 / 测试对总数。 |
|
测试窗口的 |
|
正收益的测试窗口占比。 |
|
最差和最好的测试窗口。 |
典型工作流¶
from qtrade.backtest import Backtest
from qtrade.utils.stats import calculate_stats
# 1. Cheap in-sample search to confirm the strategy is even worth tuning.
best_params, best_stats, _ = bt.optimize(
maximize='Sharpe Ratio',
n1=range(5, 30, 5),
n2=range(20, 60, 10),
constraint=lambda p: p['n1'] < p['n2'],
)
print("In-sample Sharpe:", best_stats['Sharpe Ratio'])
# 2. Walk-forward to get the realistic number.
result = bt.walk_forward_optimize(
train_window=200,
test_window=50,
maximize='Sharpe Ratio',
n1=range(5, 30, 5),
n2=range(20, 60, 10),
constraint=lambda p: p['n1'] < p['n2'],
)
print("OoS hit rate:", result['summary']['hit_rate'])
print("OoS mean return:", result['summary']['mean_oos_return'], '%')
# 3. Inspect window-by-window for parameter stability.
for w in result['windows']:
print(
f"{w['test_start'].date()}–{w['test_end'].date()}: "
f"params={w['best_params']} "
f"oos_return={w['test_stats']['Total Return [%]']:.2f}%"
)
如果样本内 Sharpe 和样本外均值差距悬殊,你已经找到了过拟合。要相信样本外的数字。
如何选择窗口¶
合适的 train_window 和 test_window 取决于:
策略「记忆长度」:策略拟合一个参数需要多少 bar?20 bar 的 SMA 至少需要 ~50 bar 给系统留余地。200 bar 回看的模型需要大得多的 train_window。
市场 regime 长度:训练窗口太小只能看到一个 regime;太大跨越多个 regime(不同 regime 的最优解相互抵消)。日 bar 数据的话,6–12 个月通常是合适起点。
你能支撑的窗口数量:长窗口 = 窗口更少,但每次拟合数据更多;短窗口 = 窗口更多但每个噪声更大。至少 5–10 个窗口汇总统计才有意义。
经验法则:train_window ≈ 4–10 × test_window 是常见比例。
常见坑¶
「OoS 命中率是 0.5」¶
和抛硬币没区别。要么策略真的没有 edge,要么你的参数网格没覆盖有意义的空间。试试:
看哪些参数在每个窗口胜出 —— 如果窗口之间的最优参数差异巨大,策略不稳定。
扩大网格。如果最优值老是落在网格边缘,说明你探索的范围不够远。
试更长的
train_window。
「OoS 均值是正的但最差窗口惨不忍睹」¶
这是「大部分时间 work 但偶尔爆仓」类策略的典型特征(如马丁格尔式加仓、裸卖空波动率)。均值掩盖了尾部风险。
永远要看每个窗口的明细,不要只看 summary。
「Walk-forward Sharpe 比样本内 Sharpe 低很多」¶
正常现象。差距就是过拟合的程度。2:1 的比例(样本内 2.0 → OoS 1.0)尚可;5:1 说明你在拟合噪声。降低参数自由度(缩小网格),或者增加策略的归纳偏置(用 constraint、跨资产共享参数等)。
「Step 小于 test_window」¶
这会产生重叠的测试窗口。summary 指标会重复计算 bar 并夸大 n_windows。有时这是有意为之(如每周重训的自适应 walk-forward),但需要明白窗口在统计上不独立。
另请参考¶
统计指标词汇表 —— 选择合适的
maximize指标。快速开始 —— 基础 Backtest 工作流。
examples/portfolio_strategy.py—— 可运行的多资产 walk-forward 示例。