バックテストの最大の罠は**過学習(オーバーフィッティング)**です。過去データで最適化したパラメータが未来では全く機能しない状態です。Hermes Agent ではこれを防ぐために ウォークフォワード検証 と VIX レジームフィルター を導入しています。
過学習の問題
グリッドサーチで最適化すると、過去データへの「暗記」が起きます。
過去3年データで最適化:
RSI_ob=62, SMA20/50, RR=2.5 → SQN=3.2, WR=68% ← 過去データでは優秀
でも未来(直近1年)では:
同じパラメータ → SQN=0.3, WR=41% ← 使い物にならない
「過去に当てはまる」と「未来でも機能する」は全く別の話です。
ウォークフォワード検証の仕組み
データを時系列で分割して「最適化期間(IS)」と「検証期間(OOS)」を分けます。
全データ: ─────────────────────────────────────────
IS (最適化) OOS (検証)
├──────────────────┤├──────────────┤
2年 1年
複数ウィンドウで繰り返す:
Window 1: IS[0-24ヶ月] → OOS[24-36ヶ月]
Window 2: IS[6-30ヶ月] → OOS[30-42ヶ月]
Window 3: IS[12-36ヶ月] → OOS[36-48ヶ月]
IS で最良パラメータを選び、OOS でそのパラメータを未来として検証します。OOS で崩れるなら過学習です。
# python/backtest/walkforward.py(概略)
def run_walkforward(df: pd.DataFrame, is_months=24, oos_months=12, step_months=6):
results = []
for start in range(0, total_months - is_months - oos_months, step_months):
is_data = df[start : start + is_months]
oos_data = df[start + is_months : start + is_months + oos_months]
# IS で最良パラメータを見つける
best_params = optimize_params(is_data)
# OOS でそのパラメータを検証
oos_result = backtest(oos_data, best_params)
results.append({
"window": start,
"is_sqn": best_params["sqn"],
"oos_sqn": oos_result["sqn"],
"oos_wr": oos_result["win_rate"],
})
return results
IS vs OOS の比較
OOS SQN が IS SQN の 50% 以上 を維持できているパラメータのみを採用します。
# 採用基準
is_sqn = result["is_sqn"]
oos_sqn = result["oos_sqn"]
degradation = (is_sqn - oos_sqn) / is_sqn
if degradation > 0.5:
print(f"❌ 過学習: IS={is_sqn:.2f} → OOS={oos_sqn:.2f} ({degradation*100:.0f}% 劣化)")
else:
print(f"✅ 採用: IS={is_sqn:.2f} → OOS={oos_sqn:.2f}")
実際に採用したパラメータの例:
| Symbol | Engine | IS SQN | OOS SQN | 劣化率 |
|---|---|---|---|---|
| GBP_USD | structural_htf | 4.8 | 3.9 | 19% ✅ |
| META | structural | 2.3 | 1.9 | 17% ✅ |
| BTC/USD | rebound | 2.8 | 0.9 | 68% ❌ → 不採用 |
VIX レジームフィルター
ウォークフォワードで選んだパラメータでも、相場のレジーム(状態) が変わると機能しなくなります。VIX(恐怖指数)が高い時期(market stress)は戦略の有効性が下がることが多いため、フィルターを設けました。
# python/backtest/optimize.py(VIX フィルター)
def apply_vix_filter(df: pd.DataFrame, vix_df: pd.DataFrame, threshold=25) -> pd.DataFrame:
"""VIX が threshold を超えている期間のシグナルをフィルタリング"""
merged = df.merge(vix_df[["date", "vix_close"]], on="date", how="left")
# VIX が高い期間は HOLD とする
merged.loc[merged["vix_close"] > threshold, "signal"] = 0
return merged
バックテスト上でも、VIX フィルターを適用したほうが多くの銘柄でリスク調整後リターンが向上しました。
自動最適化パイプライン
# run_fx_optimize.sh(概略)
#!/usr/bin/env bash
# Step 1: データ取得
python fetch_oanda_data.py --symbol EUR_USD --granularity H1
# Step 2: グリッドサーチ最適化(IS)
python optimize.py --symbol EUR_USD --timeframe H1 --save-csv
# Step 3: ウォークフォワード検証
python walkforward.py --symbol EUR_USD --is-months 24 --oos-months 12
# Step 4: 採用基準チェック(OOS SQN >= 1.0)
python apply_best_params.py --symbol EUR_USD --min-oos-sqn 1.0
# Step 5: HTML レポート生成
python generate_report.py --symbol EUR_USD
このパイプラインを新しいシンボルを追加するたびに実行し、採用基準を通ったパラメータのみを strategy-params.json に追加します。
Dollar SQN vs R-Multiple SQN
最初は Dollar SQN(損益額ベース)を使っていましたが、アカウントサイズに依存してしまう問題がありました。
Dollar SQN: SQN = (avg_profit_$) / std_profit_$ × √n
R-Multiple SQN: SQN = (avg_R) / std_R × √n
R(リスク倍数)は「実際の損益 ÷ 想定リスク(エントリーからSLまで)」です。ポジションサイズに依存しないため、銘柄をまたいだ比較が正確にできます。
まとめ
| 手法 | 目的 |
|---|---|
| グリッドサーチ | 最良パラメータの探索 |
| ウォークフォワード | 過学習の検出・排除 |
| VIX フィルター | レジーム変化への対応 |
| R-Multiple SQN | 銘柄横断の公平な比較 |
バックテストで「完璧な結果」が出るほど過学習を疑うべきです。OOS で SQN が半分以下になったパラメータは実戦では機能しません。厳しい検証を通過したパラメータだけを本番に使うことが、システムの長期的な有効性を保つ鍵です。