🏠
AI量化交易 · 第二十四章
目录
CHAPTER 24

Walk-Forward分析

滚动优化与样本外验证的正确打开方式

本章导航

  1. 学习目标
  2. 故事引入:训练室冠军 vs 实战高手
  3. 核心知识:Walk-Forward分析原理与实现
  4. AI实践:让AI设计Walk-Forward框架
  5. 动手练习:Walk-Forward回测代码
  6. 本章测验
  7. 下一章预告

学习目标

传统的回测方法是在整个历史数据上优化参数,然后用同一套数据评估表现——这就像让学生在考场上先看完答案再做题。Walk-Forward分析(WFA)则要求策略在从未见过的数据上证明自己,这才是真正接近实盘的回测方式。

本章结束后,你将能够:

故事引入:训练室冠军 vs 实战高手

想象一下两个拳击手。A选手每天在训练室里打沙袋,每个动作都经过精密计算,姿势完美无瑕。B选手每天和不同的对手实战,经常被揍得鼻青脸肿,但学会了如何应对各种突发情况。

比赛日到了。A选手面对的是一个风格完全陌生的对手——他的完美组合拳完全施展不开,三回合就被KO。B选手虽然动作不够标准,但凭借丰富的实战经验灵活应变,最终点数获胜。

传统回测就像A选手的训练方式——在已知数据上反复优化,追求完美的历史表现。Walk-Forward分析则像B选手的实战训练——在不断变化的环境中滚动学习,用未知数据检验真实能力。

核心知识:Walk-Forward分析原理与实现

Walk-Forward的核心思想

Walk-Forward分析模拟了真实交易中最常见的场景:定期用近期数据重新优化策略参数,然后在接下来的一段时间内使用这些参数交易,周而复始。

具体步骤如下:

  1. 划分窗口:将历史数据划分为多个连续的子区间。每个子区间包含一个训练集(In-Sample, IS)和一个测试集(Out-of-Sample, OOS)。
  2. 训练优化:在训练集上优化策略参数,找到最优参数组合。
  3. 测试评估:将最优参数应用到测试集上,记录策略表现。
  4. 滚动前进:将整个窗口向前移动一定步长,重复步骤2-3,直到覆盖全部历史数据。
  5. 汇总结果:将所有测试集的表现拼接起来,得到完整的Walk-Forward绩效曲线。
flowchart LR
    subgraph W1["窗口1"]
        IS1["训练集
2015-2017"] --> OOS1["测试集
2018"] end subgraph W2["窗口2"] IS2["训练集
2016-2018"] --> OOS2["测试集
2019"] end subgraph W3["窗口3"] IS3["训练集
2017-2019"] --> OOS3["测试集
2020"] end W1 --> W2 --> W3

窗口设计的核心参数

Walk-Forward的效果高度依赖于窗口设计。三个关键参数是:

Walk-Forward效率比率

如何衡量Walk-Forward的价值?一个关键指标是效率比率(Efficiency Ratio)

ER = Walk-Forward夏普 / 全样本最优夏普

效率比率越接近1,说明Walk-Forward的表现越接近上帝视角的全样本优化。通常ER > 0.5被认为是可接受的,ER < 0.2说明策略对参数变化极其敏感,可能不适合实盘。

import pandas as pd
import numpy as np
from datetime import datetime, timedelta

def walk_forward_analysis(price_df, strategy_func, param_grid,
                          train_size=504, test_size=252, step_size=252):
    """
    Walk-Forward分析框架
    price_df: DataFrame with datetime index and 'close' column
    strategy_func: function(params) -> returns_series
    param_grid: list of parameter dicts to optimize
    """
    n = len(price_df)
    results = []
    oos_returns = []
    
    start = 0
    while start + train_size + test_size <= n:
        # 划分训练集和测试集
        train_data = price_df.iloc[start:start+train_size]
        test_data = price_df.iloc[start+train_size:start+train_size+test_size]
        
        # 在训练集上优化参数
        best_sharpe = -np.inf
        best_params = None
        best_train_returns = None
        
        for params in param_grid:
            train_returns = strategy_func(train_data, params)
            if len(train_returns) < 10 or train_returns.std() == 0:
                continue
            sharpe = train_returns.mean() / train_returns.std() * np.sqrt(252)
            if sharpe > best_sharpe:
                best_sharpe = sharpe
                best_params = params
                best_train_returns = train_returns
        
        if best_params is None:
            start += step_size
            continue
        
        # 在测试集上评估最优参数
        test_returns = strategy_func(test_data, best_params)
        oos_returns.extend(test_returns.tolist())
        
        results.append({
            'train_start': train_data.index[0],
            'train_end': train_data.index[-1],
            'test_start': test_data.index[0],
            'test_end': test_data.index[-1],
            'best_params': best_params,
            'train_sharpe': best_sharpe,
            'test_sharpe': (test_returns.mean() / test_returns.std() * np.sqrt(252)
                            if test_returns.std() > 0 else 0)
        })
        
        start += step_size
    
    oos_series = pd.Series(oos_returns)
    overall_sharpe = (oos_series.mean() / oos_series.std() * np.sqrt(252)
                      if oos_series.std() > 0 else 0)
    
    return pd.DataFrame(results), oos_series, overall_sharpe

# 示例:简单的均线策略
param_grid = [
    {'short': 10, 'long': 60},
    {'short': 20, 'long': 120},
    {'short': 30, 'long': 180},
    {'short': 50, 'long': 200},
]

def ma_strategy(data, params):
    short_ma = data['close'].rolling(params['short']).mean()
    long_ma = data['close'].rolling(params['long']).mean()
    signal = (short_ma > long_ma).astype(int).diff().fillna(0)
    returns = signal.shift(1) * data['close'].pct_change()
    return returns.dropna()

# 使用模拟数据演示
np.random.seed(42)
dates = pd.date_range('2015-01-01', '2024-01-01', freq='B')
price = pd.DataFrame({
    'close': 100 * np.exp(np.cumsum(np.random.normal(0.0002, 0.01, len(dates))))
}, index=dates)

results_df, oos_rets, overall_sharpe = walk_forward_analysis(
    price, ma_strategy, param_grid, train_size=504, test_size=252, step_size=252
)

print("=== Walk-Forward结果 ===")
print(results_df[['train_start', 'test_start', 'train_sharpe', 'test_sharpe']].head())
print(f"
整体样本外夏普比率: {overall_sharpe:.2f}")

Walk-Forward vs 传统回测

为什么Walk-Forward比传统回测更接近实盘?

如果Walk-Forward结果远差于传统回测,说明策略高度依赖参数优化,实盘风险很大。

与AI一起练习

练习1:让AI优化你的窗口参数

我有一个双均线策略,想在A股沪深300上做Walk-Forward分析。
请帮我:
1. 讨论训练集长度选2年、3年、5年的利弊
2. 讨论测试集长度选6个月、1年、2年的利弊
3. 写代码比较不同窗口组合下的Walk-Forward效率比率
4. 给出推荐参数并解释原因

练习2:让AI分析Walk-Forward结果

我的策略Walk-Forward结果如下:
- 训练集夏普均值: 1.8
- 测试集夏普均值: 0.6
- 测试集夏普标准差: 0.9
- 效率比率: 0.33

请帮我分析:
1. 这个策略是否适合实盘?为什么?
2. 测试集夏普标准差很大说明什么?
3. 提出3个改进方案

练习3:让AI讨论锚点Walk-Forward

除了标准的滚动Walk-Forward,还有一种锚点Walk-Forward(训练集从固定起点开始,只扩展终点)。
请帮我:
1. 解释锚点Walk-Forward的原理和适用场景
2. 比较滚动WFA和锚点WFA的优缺点
3. 用Python实现锚点Walk-Forward并对比两种方法的结果

动手练习:完整的Walk-Forward框架

"""Walk-Forward分析完整框架"""
import pandas as pd
import numpy as np

class WalkForwardAnalyzer:
    def __init__(self, price_df, strategy_func, param_grid):
        self.price = price_df
        self.strategy = strategy_func
        self.params = param_grid
    
    def run(self, train_size=504, test_size=252, step_size=252):
        n = len(self.price)
        results = []
        all_oos_returns = []
        
        start = 0
        while start + train_size + test_size <= n:
            train = self.price.iloc[start:start+train_size]
            test = self.price.iloc[start+train_size:start+train_size+test_size]
            
            # 训练集优化
            best_sharpe = -np.inf
            best_p = None
            for p in self.params:
                r = self.strategy(train, p).dropna()
                if len(r) < 10 or r.std() == 0:
                    continue
                sharpe = r.mean() / r.std() * np.sqrt(252)
                if sharpe > best_sharpe:
                    best_sharpe = sharpe
                    best_p = p
            
            if best_p is None:
                start += step_size
                continue
            
            # 测试集评估
            test_r = self.strategy(test, best_p).dropna()
            all_oos_returns.extend(test_r.tolist())
            
            results.append({
                'period': len(results)+1,
                'train_sharpe': round(best_sharpe, 2),
                'test_sharpe': round(test_r.mean()/test_r.std()*np.sqrt(252), 2)
                        if test_r.std()>0 else 0,
                'best_params': str(best_p)
            })
            
            start += step_size
        
        self.results = pd.DataFrame(results)
        self.oos_returns = pd.Series(all_oos_returns)
        return self
    
    def summary(self):
        oos = self.oos_returns
        sharpe = oos.mean() / oos.std() * np.sqrt(252) if oos.std() > 0 else 0
        print("="*50)
        print("Walk-Forward分析报告")
        print("="*50)
        print(f"窗口数量: {len(self.results)}")
        print(f"训练集平均夏普: {self.results['train_sharpe'].mean():.2f}")
        print(f"测试集平均夏普: {self.results['test_sharpe'].mean():.2f}")
        print(f"整体样本外夏普: {sharpe:.2f}")
        print(f"测试集夏普标准差: {self.results['test_sharpe'].std():.2f}")
        print("="*50)

# 使用示例
# analyzer = WalkForwardAnalyzer(price_df, strategy, param_grid)
# analyzer.run(train_size=756, test_size=252, step_size=252)
# analyzer.summary()

本章测验

测测你的理解
1. Walk-Forward分析与传统全样本回测的核心区别是什么?
  • A. Walk-Forward计算更快
  • B. Walk-Forward在真正的未知数据上测试,模拟实盘滚动优化
  • C. Walk-Forward不需要优化参数
  • D. Walk-Forward只适用于机器学习策略
Walk-Forward的核心价值在于每个测试集(OOS)在优化时都是不可见的,这更接近真实交易中用历史数据优化、未来数据验证的场景。
2. 效率比率(ER)= Walk-Forward夏普 / 全样本最优夏普,ER接近0说明什么?
  • A. 策略对参数极其敏感,样本外表现远差于样本内,不适合实盘
  • B. 策略非常稳健
  • C. 计算有误
  • D. 需要更多数据
效率比率接近0意味着Walk-Forward夏普远差于全样本最优夏普,说明策略高度依赖参数优化,在未知数据上几乎失效。
3. 训练集长度设置太短会导致什么问题?
  • A. 参数不稳定,因为训练数据没有覆盖完整市场周期
  • B. 计算量太大
  • C. 过拟合风险降低
  • D. 测试集数据太多
训练集太短可能导致优化出的参数只适用于特定短期市场环境,一旦市场环境变化,参数就失效了。A股建议至少覆盖一个完整的牛熊周期(3-5年)。

下一章预告

Walk-Forward分析让我们学会了如何在滚动窗口中验证策略。但验证之后,你如何全面评价一个策略的好坏?年化收益和最大回撤就够了吗?

第25章将带你系统学习策略评价指标。从夏普比率、索提诺比率到卡玛比率、Omega比率,从收益分布的偏度峰度到最大回撤的恢复时间——你将学会用多维度的尺子衡量策略,而不是只看收益率这一个数字。

收益是策略的声音,风险是策略的沉默。只听得见声音的人,永远无法理解一首完整的乐曲。

觉得有收获?分享给同样对量化交易感兴趣的朋友

上一章
过拟合
当回测战神遇到实盘,为什么策略总会失效
下一章
策略评价指标
用多维度的尺子衡量策略好坏

如果这本书对你有所启发,欢迎支持我继续创作

赞赏二维码