バックテストの過学習を防ぐウォークフォワード分析をPythonで実装する方法【日本株対応】

「バックテストで年利30%出たのに実運用で2週間で損失」——これ、僕の実話です。原因はパラメータの過学習でした。今回はそれを防ぐウォークフォワード分析をPythonで実装します。

過学習とウォークフォワード分析とは

過学習(オーバーフィッティング)とは、過去データに最適化しすぎて未来のデータに対応できない状態。ウォークフォワード分析は、データを時系列に沿って複数の窓に分け「最適化→検証→次の窓へ」を繰り返す手法。

実装コード

import pandas as pd
import numpy as np
import yfinance as yf
from itertools import product

def download_data(ticker="7203.T", start="2019-01-01", end="2024-12-31"):
    df = yf.download(ticker, start=start, end=end, auto_adjust=True)
    df.columns = [c[0] if isinstance(c, tuple) else c for c in df.columns]
    return df

def sma_strategy(df, fast, slow):
    """シンプルな移動平均クロス戦略"""
    df = df.copy()
    df["sma_fast"] = df["Close"].rolling(fast).mean()
    df["sma_slow"] = df["Close"].rolling(slow).mean()
    df["signal"] = (df["sma_fast"].shift(1) > df["sma_slow"].shift(1)).astype(int)
    df["ret"] = df["Close"].pct_change()
    df["strat_ret"] = df["ret"] * df["signal"]
    total_ret = (1 + df["strat_ret"].dropna()).prod() - 1
    return total_ret

def walk_forward(df, train_months=12, test_months=3,
                 fast_range=range(5,21,5), slow_range=range(20,61,10)):
    """ウォークフォワード分析"""
    results = []
    dates = pd.date_range(df.index[0], df.index[-1], freq="MS")

    for i in range(0, len(dates) - train_months - test_months, test_months):
        train_end = dates[i + train_months]
        test_end = dates[i + train_months + test_months]

        train = df[dates[i]:train_end]
        test = df[train_end:test_end]

        # インサンプルで最適化
        best_ret, best_params = -np.inf, None
        for fast, slow in product(fast_range, slow_range):
            if fast >= slow:
                continue
            ret = sma_strategy(train, fast, slow)
            if ret > best_ret:
                best_ret, best_params = ret, (fast, slow)

        # アウトオブサンプルで検証
        if best_params:
            oos_ret = sma_strategy(test, *best_params)
            results.append({
                "期間": f"{dates[i].date()}~{test_end.date()}",
                "最適パラメータ": best_params,
                "IS_リターン": f"{best_ret:.1%}",
                "OOS_リターン": f"{oos_ret:.1%}"
            })

    return pd.DataFrame(results)

# 実行
df = download_data()
result = walk_forward(df)
print(result.to_string(index=False))

インサンプル(IS)でいい数字が出てもアウトオブサンプル(OOS)が悪ければ過学習。ISとOOSのリターンが近いパラメータが「頑健な戦略」の目安になる。次は日本製造メーカー株でこの分析を走らせてみたいと思っている。

タイトルとURLをコピーしました