「バックテストでは素晴らしい成績なのに実際の取引では損失続き」——この現象の多くは過学習(オーバーフィッティング)が原因です。ウォークフォワード分析はこの問題を防ぐための強力な検証手法です。
過学習とは?
過学習とは、戦略のパラメーターを特定の過去データに最適化しすぎて、新しいデータには対応できなくなる現象です。
例えば「トヨタ株の2010〜2020年データでRSI=65、移動平均=47日が最良」と最適化しても、2021年以降の相場には当てはまらない可能性が高いです。
ウォークフォワード分析の基本概念
ウォークフォワード分析は、データを時系列順に複数のウィンドウに分割し、最適化(In-Sample)→検証(Out-of-Sample)を繰り返す手法です。
期間: 2015 2016 2017 2018 2019 2020 2021 2022
[────────────In-Sample────────────][Out-of-Sample] ← ウィンドウ1
[────────────In-Sample────────────][OOS] ← ウィンドウ2
[────────────In-Sample────────────][OOS]← ウィンドウ3
OOSの結果を組み合わせて最終評価
Pythonでウォークフォワード分析を実装する
import yfinance as yf
import pandas as pd
import numpy as np
from itertools import product
def ma_cross_strategy(df, short, long_):
"""移動平均クロス戦略のリターンを計算"""
df = df.copy()
df['MA_s'] = df['Close'].rolling(short).mean()
df['MA_l'] = df['Close'].rolling(long_).mean()
df['Signal'] = np.where(df['MA_s'] > df['MA_l'], 1, 0).shift(1)
df['Return'] = df['Close'].pct_change() * df['Signal']
return df['Return'].dropna()
def optimize_params(df, short_range, long_range):
"""In-Sampleでパラメーターを最適化"""
best_sharpe = -999
best_params = None
for short, long_ in product(short_range, long_range):
if short >= long_:
continue
returns = ma_cross_strategy(df, short, long_)
if len(returns) < 30:
continue
sharpe = returns.mean() / returns.std() * np.sqrt(252) if returns.std() != 0 else 0
if sharpe > best_sharpe:
best_sharpe = sharpe
best_params = (short, long_)
return best_params, best_sharpe
def walk_forward_analysis(df, is_window=504, oos_window=126, step=63):
"""
ウォークフォワード分析
Args:
df: 価格データ
is_window: In-Sampleウィンドウのサイズ(日数)
oos_window: Out-of-Sampleウィンドウのサイズ(日数)
step: ステップサイズ(日数)
"""
results = []
short_range = range(5, 50, 5)
long_range = range(20, 200, 10)
start = 0
while start + is_window + oos_window <= len(df):
# データを分割
is_data = df.iloc[start:start + is_window]
oos_data = df.iloc[start + is_window:start + is_window + oos_window]
# In-Sampleで最適化
best_params, is_sharpe = optimize_params(is_data, short_range, long_range)
if best_params is None:
start += step
continue
# Out-of-Sampleで検証
oos_returns = ma_cross_strategy(oos_data, best_params[0], best_params[1])
oos_sharpe = oos_returns.mean() / oos_returns.std() * np.sqrt(252) if oos_returns.std() != 0 else 0
oos_return = (1 + oos_returns).prod() - 1
results.append({
'IS_Start': is_data.index[0].date(),
'IS_End': is_data.index[-1].date(),
'OOS_Start': oos_data.index[0].date(),
'OOS_End': oos_data.index[-1].date(),
'Best_Params': best_params,
'IS_Sharpe': round(is_sharpe, 2),
'OOS_Sharpe': round(oos_sharpe, 2),
'OOS_Return': f"{oos_return:.1%}"
})
start += step
return pd.DataFrame(results)
# 実行
df = yf.download("7203.T", period="10y", progress=False)
wf_results = walk_forward_analysis(df)
print("=== ウォークフォワード分析結果 ===")
print(wf_results.to_string())
結果の解釈
# 総合パフォーマンス評価
print(f"\n=== 総合評価 ===")
print(f"検証ウィンドウ数: {len(wf_results)}")
print(f"OOS勝率(シャープ>0): {(wf_results['OOS_Sharpe'] > 0).mean():.1%}")
print(f"IS平均シャープレシオ: {wf_results['IS_Sharpe'].mean():.2f}")
print(f"OOS平均シャープレシオ: {wf_results['OOS_Sharpe'].mean():.2f}")
# 効率係数(OOS/IS比率):1に近いほど過学習が少ない
efficiency = wf_results['OOS_Sharpe'].mean() / wf_results['IS_Sharpe'].mean()
print(f"効率係数(OOS/IS): {efficiency:.2f}")
print(f"判定: {'良好(過学習少)' if efficiency > 0.5 else '要注意(過学習の可能性)'}")
アンカーウィンドウ vs ローリングウィンドウ
| 方式 | 概要 | 特徴 |
|---|---|---|
| アンカーウィンドウ | In-Sampleの開始点は固定し、終点を移動 | データが増えるほど安定。市場変化への適応が遅い |
| ローリングウィンドウ | In-Sampleの開始点・終点を同じ幅で移動 | 最新の市場環境に適応。古いデータを切り捨てる |
ウォークフォワード分析のポイント
- OOS/IS比率(効率係数)が0.5以上あれば良好
- 全OOSウィンドウの60%以上でプラスリターンが理想
- パラメーターが毎回大きく変わる場合は戦略が不安定
- OOSのシャープレシオがISより大幅に低い場合は過学習の可能性
まとめ
ウォークフォワード分析は単純なバックテストより手間がかかりますが、過学習を検出できる非常に重要な検証手法です。本番投入前に必ず実施することをオススメします。

