バックテストの過学習(オーバーフィッティング)を防ぐ方法|Walk-Forward Validation Python実装

Python実装・コード

先日、自分で作ったBOTを本番稼働させてみたんですが、バックテストでは年利20%以上出てたのに、実際の運用では最初の3週間でじわじわ損失が出てきてちょっと焦りました笑。「あれ、なんで?」と思って調べていたら「過学習(オーバーフィッティング)」という問題にぶつかって、これはブログに書かないといけないなと思いました。

バックテストが「嘘をつく」理由

なぜ僕がこれを調べたかというと、バックテストで良い結果が出たBOTを実運用したら全然ダメだったからです。最初は「相場が変わったのかな」と思っていたんですが、原因は相場じゃなくて、僕の戦略が過去データに”ハマりすぎていた”ことでした。

過学習(オーバーフィッティング)とは、モデルが訓練データのパターンを覚えすぎてしまい、新しいデータに対してはうまく機能しない状態のことです。トレーディングで言うと、「過去の株価のクセを丸暗記しただけで、未来には使えないBOT」を作ってしまうイメージです。

具体的にはこんな症状が出ます:

  • バックテストでシャープレシオ2.0以上なのに、フォワードテストでは0.3以下
  • 最適化したパラメータが、テスト期間を少し変えると全然違う値になる
  • バックテスト中の最大ドローダウンが5%なのに、実運用では初月で10%ドローダウン

心当たり、ありませんか?僕はめちゃくちゃありました。。。

過学習が起きやすいケース3つ

① パラメータを最適化しすぎる

「移動平均の期間を何日にすればいいか」を何十通りも試して、一番良かった値を使う。これは過学習の典型です。たまたまその期間のデータにフィットしただけで、未来では意味がありません。

② テストデータが少なすぎる

1年分のデータでバックテストして「完璧だ!」と思っても、そのデータが強気相場だけだったりすると、相場が変わった瞬間に壊れます。日本株で言うと、2023〜2024年の上昇相場だけでテストした戦略は2025年以降に苦労するケースが多いです。

③ インジケーターを重ねすぎる

RSI、MACD、ボリンジャーバンド、一目均衡表…全部使えば精度が上がりそうな気がしますが、インジケーターを増やせば増やすほど過学習リスクが上がります。変数が多いほど、過去データへの「こじつけ」が起きやすくなるからです。

PythonでWalk-Forward Validationを実装する

過学習を防ぐ最も実践的な方法が「ウォークフォワード検証(Walk-Forward Validation)」です。これは、訓練期間とテスト期間をずらしながら複数回バックテストすることで、戦略がどんな相場環境でも機能するかを確認する手法です。

以下のコードは日本株(yfinance経由)で簡易的なウォークフォワード検証を行う例です。

import yfinance as yf
import pandas as pd
import numpy as np

def walk_forward_validation(ticker, start, end, train_months=12, test_months=3):
    """
    ウォークフォワード検証
    ticker: 銘柄コード(例: "7203.T" = トヨタ)
    train_months: 訓練期間(月数)
    test_months: テスト期間(月数)
    """
    df = yf.download(ticker, start=start, end=end, auto_adjust=True)
    df = df[['Close']].dropna()

    results = []
    start_idx = 0

    while True:
        train_end = start_idx + train_months * 21
        test_end = train_end + test_months * 21

        if test_end > len(df):
            break

        train_data = df.iloc[start_idx:train_end]
        test_data = df.iloc[train_end:test_end]

        best_short, best_long, best_return = 5, 20, -999
        for short_window in [5, 10, 15]:
            for long_window in [20, 30, 50]:
                if short_window >= long_window:
                    continue
                ret = simulate_ma_cross(train_data, short_window, long_window)
                if ret > best_return:
                    best_return = ret
                    best_short, best_long = short_window, long_window

        test_return = simulate_ma_cross(test_data, best_short, best_long)
        results.append({
            'train_start': df.index[start_idx].date(),
            'test_start': df.index[train_end].date(),
            'test_end': df.index[min(test_end-1, len(df)-1)].date(),
            'best_short': best_short,
            'best_long': best_long,
            'train_return': best_return,
            'test_return': test_return
        })
        start_idx += test_months * 21

    return pd.DataFrame(results)


def simulate_ma_cross(data, short_window, long_window):
    """移動平均クロス戦略のシミュレーション(簡易版)"""
    close = data['Close']
    short_ma = close.rolling(short_window).mean()
    long_ma = close.rolling(long_window).mean()
    signal = (short_ma > long_ma).astype(int).diff()
    buy_dates = data.index[signal == 1]
    sell_dates = data.index[signal == -1]
    total_return = 0
    for buy in buy_dates:
        next_sells = sell_dates[sell_dates > buy]
        if len(next_sells) == 0:
            continue
        sell = next_sells[0]
        buy_price = data.loc[buy, 'Close']
        sell_price = data.loc[sell, 'Close']
        total_return += (sell_price - buy_price) / buy_price
    return total_return


# 実行例:トヨタ自動車でウォークフォワード検証
if __name__ == "__main__":
    results = walk_forward_validation(
        ticker="7203.T",
        start="2021-01-01",
        end="2025-12-31",
        train_months=12,
        test_months=3
    )
    print(results.to_string())
    print(f"\n訓練平均リターン: {results['train_return'].mean():.2%}")
    print(f"テスト平均リターン: {results['test_return'].mean():.2%}")
    print(f"訓練→テストの劣化率: {(results['test_return'].mean() / results['train_return'].mean()):.1%}")

このコードを動かすと、「訓練データでは良かったのに、テストデータでは半分以下のリターン」という結果が出ることがあります。そうなったら、その戦略は過学習している可能性が高いです。

過学習を防ぐための実践的な3つのルール

ルール1:パラメータ数を最小限にする

シンプルな戦略ほど汎化しやすいです。「動く移動平均2本だけ」「RSI閾値1つだけ」というシンプルな戦略でも、ウォークフォワード検証で安定したリターンが出るなら、それは本物の戦略である可能性が高いです。

ルール2:アウトオブサンプルのデータを必ず残す

全データの20〜30%は「絶対に最適化に使わない」テスト用データとして封印しておいてください。最後の最後に一度だけこのデータで検証する、という使い方です。このデータで良い結果が出たときだけ、実運用を検討するといいと思います。

ルール3:バックテストと実運用の乖離を記録する

実運用を始めたら、毎月バックテスト時のシャープレシオと実績を比較するクセをつけてみてください。大きく乖離してきたら、相場環境が変わったか過学習かのどちらかです。早めに気づけると損失を最小化できます。

まとめ

バックテストが良くても実運用で損するのは、多くの場合「過学習」が原因です。ウォークフォワード検証を取り入れるだけで、戦略の信頼性はかなり上がります。

個人的には、このコードを使ってトヨタと日立でテストしてみたら、訓練データとテストデータの乖離が大きくて「あ、やっぱり自分の戦略は過学習してたんだな」と納得できました。次は相場レジームを分けてウォークフォワードをやってみる予定なので、また記事にします。ぜひ試してみてください。

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