バックテスト年利25%→実運用で即損失。。。「過学習」の罠とPython対策【ウォークフォワード分析】

Python実装・コード

バックテスト年利25%→実運用で即損失。。。「過学習」の罠とその対策をPythonで解説

先日、やっとの思いで組んだ自動売買BOTを動かしてみたんですが、バックテストでは年利25%だったのに、最初の2週間でしっかり損失が出ました笑。子供が寝た深夜にPCを開いてその数字を見たときの絶望感、伝わりますかね。。。なぜこんなことが起きるのか調べていくうちに「過学習(オーバーフィッティング)」という概念に行き着いたので、今日はその話をします。

なぜ僕がバックテストの過学習を調べたか

そもそも、バックテストって「過去データで戦略を試す」ことですよね。で、パラメーターをいじりながら「あ、この移動平均の組み合わせだと年利30%になる!」とか「ここでRSI=70を使ったほうがいい!」とかを繰り返していくと、なんかすごいパフォーマンスの戦略ができあがるんです。

でも実際に動かすと全然ダメ。これ、過去データにあまりにも「最適化」されすぎてしまって、未来の新しいデータには全然通用しない状態になっているんです。機械学習でよく言われる過学習と全く同じ概念で、僕みたいな初心者がすごく陥りやすいトラップです。

過学習が起きる主な原因

原因は大きく3つあります。

① パラメーターをいじりすぎ
移動平均の期間とか、RSIの閾値とか、ひたすら最適値を探す「カーブフィッティング」をやりすぎると、その過去データ専用の戦略になってしまいます。

② サンプルが少ない
日本株の場合、年間の取引日数は約240日。短期間のデータだけでテストすると、たまたまその期間に有効だったパターンを拾ってしまいます。

③ インサンプル・アウトオブサンプルの区別がない
最適化に使ったデータと検証に使うデータが同じだと、当然よい結果が出やすくなります。でも実運用はその外側のデータで行われます。

対策① ウォークフォワード分析でPythonを使って検証する

一番効果的な対策が「ウォークフォワード分析」です。データを時系列に分割して、最適化期間(インサンプル)→検証期間(アウトオブサンプル)を繰り返す方法です。

実際にPythonで組んでみたコードを見てみましょう。シンプルな移動平均クロス戦略での例です。

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

# トヨタ自動車(7203)のデータを取得
ticker = "7203.T"
df = yf.download(ticker, start="2020-01-01", end="2025-12-31")
df = df[['Close']].copy()

def sma_cross_returns(data, fast, slow):
    data = data.copy()
    data['SMA_fast'] = data['Close'].rolling(fast).mean()
    data['SMA_slow'] = data['Close'].rolling(slow).mean()
    data['signal'] = np.where(data['SMA_fast'] > data['SMA_slow'], 1, -1)
    data['returns'] = data['Close'].pct_change()
    data['strategy_returns'] = data['signal'].shift(1) * data['returns']
    return data['strategy_returns'].sum()

def walk_forward_analysis(data, in_sample_days=252, out_sample_days=63):
    results = []
    fast_periods = [5, 10, 20, 25]
    slow_periods = [25, 50, 75, 100]
    start = 0
    while start + in_sample_days + out_sample_days <= len(data):
        in_sample = data.iloc[start:start + in_sample_days]
        out_sample = data.iloc[start + in_sample_days:start + in_sample_days + out_sample_days]
        best_return = -np.inf
        best_fast, best_slow = 5, 25
        for fast in fast_periods:
            for slow in slow_periods:
                if fast >= slow:
                    continue
                ret = sma_cross_returns(in_sample, fast, slow)
                if ret > best_return:
                    best_return = ret
                    best_fast, best_slow = fast, slow
        oos_return = sma_cross_returns(out_sample, best_fast, best_slow)
        results.append({
            'period_start': data.index[start + in_sample_days],
            'best_fast': best_fast,
            'best_slow': best_slow,
            'in_sample_return': best_return,
            'out_of_sample_return': oos_return
        })
        start += out_sample_days
    return pd.DataFrame(results)

results = walk_forward_analysis(df)
print(results)
print(f"インサンプル平均リターン: {results['in_sample_return'].mean():.4f}")
print(f"アウトオブサンプル平均リターン: {results['out_of_sample_return'].mean():.4f}")

対策② パラメーターを最初から絞る

もう一つの対策は、そもそもパラメーターの探索範囲を広げすぎないこと。「移動平均の期間は市場の構造から考えて20日か50日くらいが理にかなっている」という仮説を先に立てて、その2つだけで試す、みたいな発想です。

僕の場合、製造メーカー株だと月次・四半期サイクルで機関投資家が動くことが多いので、20日(1ヶ月)と60日(1四半期)の組み合わせを使うことが多いです。これは数値で探したんじゃなくて、「そういう市場構造があるから」という理由付けがある。これが重要です。

対策③ シャープレシオで判断、リターンだけ見ない

バックテストの結果を見るとき、リターン(利益率)だけを見てしまいがちですが、リスクに対してどれだけ稼いでいるかを示す「シャープレシオ」も必ず確認するようにしました。

def calculate_sharpe(returns_series, risk_free_rate=0.001):
    excess_returns = returns_series - risk_free_rate / 252
    sharpe = excess_returns.mean() / excess_returns.std() * np.sqrt(252)
    return sharpe
# 目安: 1.0以上で良好、2.0以上で優秀

シャープレシオが1.0以下の戦略は、バックテストでどんなにリターンが高くても実運用には向かないと思っています。

まとめ:バックテストは「証明」じゃなくて「仮説検証」

バックテストで高いリターンが出ても、それは過去に有効だったというだけで、未来が保証されるわけじゃない。これって頭ではわかっているつもりでも、実際に「年利25%」という数字を見てしまうと舞い上がってしまうんですよね笑。

ウォークフォワード分析を取り入れてから、戦略の「本当の実力」が見えるようになった気がします。インサンプルとアウトオブサンプルのリターン差が小さい戦略を選ぶ習慣をつけてから、少しだけ実運用が安定してきました。個人的には次のステップとして、モンテカルロシミュレーションを使ったドローダウン分析も試してみたいと思っています。同じ悩みを持っている方はぜひウォークフォワード分析を試してみてください。

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