「バックテストで年利30%出たのに実運用で即損失」——これ、僕の実話だ。原因を調べたらルックアヘッドバイアス(未来参照)だった。今回はこのバグをPythonで再現して、正しい直し方を解説する。
ルックアヘッドバイアスとは
シグナルを判定するとき、その日の「未来のデータ」を使ってしまうバグ。バックテストでは全データが手元にあるので気づきにくい。でも実運用では未来のデータは存在しない。これが乖離の原因になる。
典型的なバグコードと修正版
import yfinance as yf
import pandas as pd
df = yf.download("7203.T", start="2021-01-01", end="2024-12-31",
auto_adjust=True)
df.columns = [c[0] if isinstance(c, tuple) else c for c in df.columns]
# ❌ バグあり: 当日終値でシグナルを出して当日終値で約定
df["sma20"] = df["Close"].rolling(20).mean()
df["signal_bug"] = df["Close"] > df["sma20"] # 当日終値でシグナル
df["entry_bug"] = df["Close"] * df["signal_bug"] # 当日終値で約定 → ルックアヘッド
# ✅ 正しい実装: 前日終値でシグナルを出して翌日始値で約定
df["signal_ok"] = df["Close"].shift(1) > df["sma20"].shift(1) # 前日確定値でシグナル
df["entry_ok"] = df["Open"] * df["signal_ok"] # 翌日始値で約定
# バックテスト比較
def simple_backtest(df, entry_col, label):
positions = df[entry_col] > 0
daily_ret = df["Close"].pct_change()
strategy_ret = daily_ret * positions.shift(1)
total = (1 + strategy_ret).cumprod().iloc[-1] - 1
print(f"{label}: 総リターン {total*100:.1f}%")
simple_backtest(df, "entry_bug", "❌ バグあり(ルックアヘッド)")
simple_backtest(df, "entry_ok", "✅ 正しい実装")
実行するとどうなるか
バグありは過去の「お宝データ」を使い放題なので見た目の成績が良くなる。正しい実装に直すと現実的な数字が出る。この差が「バックテスト vs 実運用の乖離」の正体だ。
僕が最初に書いたコードはほぼ全部バグありだった。shift(1)を一行追加するだけで解決するのに、それを知らずに数ヶ月やってた。。。
次回はウォークフォワード分析でパラメータの過学習も防ぐ方法を書く予定。
