#4.過学習を疑ったら、二択が増えた

FX Bot開発ログ

……先週、ターミナルに「過学習だったのかな」って打ってた。
あのときは、整理がついた気がしてた。
いま読み返すと、ずるかった。
「過学習」って、原因の名前を一個決めれば、そこで止まれる、って思ってたみたい。
遅い。わたしの整理は、いつも遅い。


Abstract

  • 期間: 2024-05-05 〜 2026-04-24(約 720 日 / 1h 足 / UTC)— #3 と同じ窓
  • 対象: USD/JPY / GBP/JPY 2 ペア(#3 と同じ)
  • 戦略: BB-MR(#3 と同じ。パラメータだけを動かす
  • 手法: Walk-Forward Analysis(train 180 日 / test 90 日 / step 90 日 → 5 fold/ペア
  • グリッド: BB_N ∈ {10, 14, 20, 28, 40} × BB_K ∈ {1.5, 2.0, 2.5, 3.0} = 20 組合せ × 2 ペア × 5 fold = 200 回

WFA サマリー(事前確定の判定軸で並べたもの):

指標
両ペアで 検証窓 Sharpe 中央値 > 0 のグリッド数 1/20 (5%)(BB(14, 2.0) のみ)
USDJPY 単独で 検証窓 Sharpe 中央値 > 0 のグリッド数 2/20 (10%)
GBPJPY 単独で 検証窓 Sharpe 中央値 > 0 のグリッド数 12/20 (60%)
事前確定 A/B/C 判定 C(グラデーション)

1. Introduction — 背景と仮説

#3 のいちばん終わりに、自分でこう打っていた。

「テスト範囲だけ暗記する」に気をつけながら、どこまでいじっていいのか、次の検証で考える。

「どこまでいじっていいのか」を考えるために、まずやることは決まっている。パラメータを動かしたとき、訓練窓と検証窓で結果がどれくらいズレるかを見る。それがズレているなら過学習。揃って沈むなら戦略の構造的な弱さ。同じ問いに、同じ手で当てるしかない。

仮説(問いを立てる):

  • 過学習説: もし過学習なら、IS(学習窓)の Sharpe は高くて、OOS(検証窓)は低い。グリッドを広げると「IS だけ高い」領域がいくつか見えるはず
  • 戦略の弱さ説: もし戦略そのものが弱いなら、IS も OOS も両方とも広範に負
  • 期間の問題説: もし戦略でもパラメータでもなく期間の問題なら、グリッドによらず fold ごとに大きくバラつく

3 つのうち、いちばん怖いのは “戦略の弱さ” だった。”過学習” ならパラメータをいじれば直る可能性がある。”期間の問題” は窓を変えれば変わる可能性がある。”戦略の弱さ” だけは、「BB-MR を当てているわたしの判断」そのものを、根っこから否定する。

自分のコードを疑うのは、自分の判断を疑うのと、ぜんぶ重なってる。
「いじれば直る」だと思いたかった。
たぶん、そう思いたいのは、わたしの都合。


2. Method — 検証設計

2.1 データ・前提(#3 と完全同一)

項目
ソース yfinance
ペア USD/JPY (JPY=X) / GBP/JPY (GBPJPY=X)
時間足 1 時間足
期間(UTC) 2024-05-05 〜 2026-04-24(約 720 日)
戦略 BBMeanReversion(SL=1.5×ATR, TP=2.0×ATR, MAX_BARS=48, RISK_PCT=1%, MARGIN=4%)
初期残高 1,000,000 円 / commission=0
spread (USDJPY) 1.0e-5(片道、#1.1 訂正後)
spread (GBPJPY) 4.033e-5(片道、#2 MID)

変えたのは BB のパラメータだけ。SL/TP/ATR/MAX_BARS の倍率や閾値はぜんぶ #1 / #2 / #3 と揃える。

2.2 Walk-Forward の窓

項目
学習窓 180 日
検証窓 90 日
Step 90 日
Fold 数 5 / ペア

180 日で BB-MR のパラメータを学んで、その次の 90 日でそのまま当てる。90 日進めて、また 180 日で学んで、次の 90 日で当てる。これを 5 回繰り返す。学習側を Test に漏らさない(時系列リークさせない)のは、自前の fold ループで担保した。

2.3 グリッド

候補値 個数
BB_N(期間) {10, 14, 20, 28, 40} 5
BB_K(σ倍率) {1.5, 2.0, 2.5, 3.0} 4

20 組合せ。BB の 2 軸だけに絞った理由は、SL/TP まで動かすと「テスト範囲だけ暗記する」のが、5 軸 × ペア × fold で爆発的に楽になるから。最初に動かす自由度を、いちばん効くと信じている 2 つだけに絞る。

2.4 事前確定の判定基準

判定基準は結果を見る前に決める:

パターン 条件(両ペア共通で成立)
A. 頑健 グリッドの 50% 以上 で 検証窓 Sharpe 中央値 > 0、かつ両ペアで同じ領域が生存
B. 死亡 検証窓 Sharpe 中央値 > 0 のグリッドが 20% 未満、かつ 検証窓 MaxDD 中央値が -30% を全域で超過
C. グラデーション A でも B でもない

A/B/C は「これを見たら判定する」という規律であって、結論ではない。判定が出た時点では、まだ次の問いの起点でしかない。これは事前に決めた。あとで結果を見て、自分の都合で動かさないために。

2.5 コード(fold ループの本体)

from src.backtest.walk_forward import WalkForwardRunner

runner = WalkForwardRunner(
    df=df_full,                       # 720 日 × 1h
    strategy_cls=BBMeanReversion,
    train_days=180, test_days=90, step_days=90,
    grid={"BB_N": [10, 14, 20, 28, 40],
          "BB_K": [1.5, 2.0, 2.5, 3.0]},
    spread=spread,
)
results = runner.run()  # 5 fold × 20 grid = 100 回 / ペア

スクリプトは code/src/backtest/wfa_bb_mr.pyWalkForwardRunner 自体は walk_forward.py に切り出して、テストも別で組んだ(次以降の検証で使い回すため)。

200 回が回るあいだ、#1 の資産曲線を、なんとなく開いていた。
右上に向かっていた線を、まだ少し信じたかったのかもしれない。


3. Results — 検証結果

3.1 グリッド別 検証窓 Sharpe 中央値(5 fold 中央値、両ペア並列)

BB_N BB_K USDJPY 検証窓 Sharpe 中央値 GBPJPY 検証窓 Sharpe 中央値 備考
10 1.5 -1.49 -0.22
10 2.0 -1.72 +1.03
10 2.5 -0.27 +0.01
10 3.0 NaN NaN Trades 0(degenerate)
14 1.5 +0.08 -0.42 USDJPY 単独で生存
14 2.0 +0.08 +0.54 両ペア共通で唯一の生存点
14 2.5 -0.95 -0.09
14 3.0 -0.72 +0.15
20 1.5 -0.76 -0.65
20 2.0 -1.03 +0.81
20 2.5 -2.15 -1.14
20 3.0 -1.18 +0.60
28 1.5 -0.67 -0.47
28 2.0 -1.25 +1.23
28 2.5 -2.50 +1.15
28 3.0 -0.30 -0.28
40 1.5 -0.24 +0.61
40 2.0 -0.48 +0.43
40 2.5 -2.07 +1.10
40 3.0 -2.93 +1.29

両ペアで 検証窓 Sharpe 中央値 > 0 のグリッドは、表の太字 1 個だけ。BB(14, 2.0)。20 通りのうち、視線がそこに吸い寄せられて止まる。

GBPJPY 側は 12 個(60%)。USDJPY 側は 2 個(10%)。両者が重なるのが BB(14, 2.0) の 1 個だけ。

WFA OOS Sharpe ヒートマップ(USDJPY / GBPJPY)

左 USDJPY はほとんど赤(マイナス)一色。右 GBPJPY は緑(プラス)の領域がかなり広い。両者が同じ色で重なるのは、黒枠で囲んだ BB(14, 2.0) のセルだけ。色のつき方が左右で別物に見える。

3.2 学習窓と検証窓の関係(過学習説の点検)

各グリッドで「学習窓 Sharpe 中央値 − 検証窓 Sharpe 中央値」(過学習度)を見ると、両ペアとも 「学習窓 高 / 検証窓 低」の領域が広範には出ない。多くのグリッドで 学習窓 Sharpe も負で、検証窓 Sharpe も負。つまり、

  • 学習窓で勝てたパラメータが、検証窓だけで負ける → これが典型的な過学習の形
  • 学習窓でも、検証窓でも、両方とも勝てない → これは過学習じゃない

WFA を回す前は前者を予期していた。出てきたのは後者寄り。過学習の典型パターン(学習窓 高 / 検証窓 低)は、わたしのデータでは見えなかった

3.3 fold 別のバラつき — 共通生存点 BB(14, 2.0) の中身

唯一の生き残り、BB(14, 2.0) の 検証窓 Sharpe を fold ごとに開く:

fold 検証窓 期間 USDJPY regime USDJPY 検証窓 Sharpe GBPJPY regime GBPJPY 検証窓 Sharpe
0 2024-11-01 〜 2025-01-30 flat +0.45 down -0.29
1 2025-01-30 〜 2025-04-30 down -2.41 flat +0.90
2 2025-04-30 〜 2025-07-29 up +0.08 up -2.75
3 2025-07-29 〜 2025-10-27 up -5.37 up +0.54
4 2025-10-27 〜 2026-01-25 flat +1.54 up +1.27

最大と最小の差は USDJPY が 6.9(+1.54 と -5.37)、GBPJPY が 4.0(+1.27 と -2.75)。中央値で見ると USDJPY +0.08 / GBPJPY +0.54 で「ぎりぎりプラス」だけれど、中身は fold ごとに ±2σ どころじゃない揺れ方をしている。「中央値が正」と「常に正」は、ぜんぜん違う。

USDJPY fold 3 の -5.37 だけ、ほかと位(くらい)が違う。-2 ぐらいの fold は説明がつく気がするけれど、-5 までいくと、説明にならない。一回だけ、薄い氷を踏み抜いた。これが「運の悪い 1 fold」なのか、戦略の根っこに、ふだんは見えない弱い場所が混ざっているのか、いまの 2 ペアでは、わからない。

BB(14, 2.0) の fold 別 検証窓 Sharpe

両ペア共通で唯一の生存点 BB(14, 2.0) を fold ごとに開いたバー。USDJPY fold 3 だけ、ほかの fold とスケールが違う深さに落ちている。「中央値が +0.08」の中身は、こういう揺れ方をしている。

それと、同じ検証窓 期間でもペアによって regime が違う fold がある(fold 0 / 1)。これが両ペアの数字が揃わない一因のように見える(あくまで観察、検定はしていない)。

3.4 事前確定 A/B/C 判定

§2.4 で先に決めた基準に照らすと、

  • 両ペアで 検証窓 Sharpe 中央値 > 0 のグリッド数 1/20(5%) → A 不合格(50% 必要)
  • 検証窓 MaxDD 中央値 < -30% が全域 0/20 → B 不合格(全域必要)

C(グラデーション)。事前に決めたとおりの形式判定。

ただし、教科書通りの「グリッド領域でグラデーションする C」とは違って、出たのは 「ペアでグラデーションする C」。GBPJPY 側はかなりの領域で 検証窓 Sharpe がプラスで、USDJPY 側はほぼ全滅。同じ戦略の中で、ペアごとに勝てる場所が違う、という形。

……過学習じゃなかった、って思ったときに、安心するかと思った。
逆だった。
過学習なら、引き締めれば直る。たぶん。
でも、引き締める対象が、見つからない。
#2 の Sharpe 1.46 を、どうやってあんなに素直に信じていたのか、もう、思い出せない。


4. Discussion — 考察

4.1 ひとつの数字に戻れない

グリッドサーチの結果は、ひとつの名前にまとめられなかった。
共通生存点 BB(14, 2.0) の中央値だけを見ると、USDJPY +0.08 / GBPJPY +0.54。
でも、その中身は fold ごとに大きく揺れている。

事前に決めた C 判定は、形式としてはきれいに出た。でも C は結論じゃなくて、次の問いの起点、と #2.4 で自分に向けて打っておいた。守る。

#2 の 180 日だけを見ていたときは、ひとつの数字で安心できた。
今回は、安心する場所が小さすぎた。

4.2 過学習説の点検結果

過学習説は否定された。これは事実として記録できる。

ただし、

  • 「過学習していない」は、性能が良いことの保証ではない
  • 学習窓 / 検証窓 の両方が広範に負、というのは、もしかすると 戦略全体の構造的な弱さを示している

戦略の弱さ説を「支持された」と言い切るには、まだ材料が足りない。GBPJPY 側は 60% の領域で 検証窓 Sharpe がプラスなので、戦略が一様に弱いわけではない。「戦略全体が弱い」と「ペアごとに効く場所が違う」が、両方とも矛盾なく成り立つ状態になっている。これが、#1 のときに想像していなかった形だった。

4.3 「ペアか戦略か」の二択にも答えが出なかった

過学習説を切り落としたあと、自分に問いが 1 個増えた、と思った。実際には増えたのは 2 個だった。

  • 戦略のせいなのか? — でも GBPJPY は 60% 生存 → 戦略全死、とは言えない
  • ペアのせいなのか? — でも USDJPY も BB(14) 周辺で生存 → ペア全死、とも言えない

観察できたのは、もっと中途半端な現象だった:

  • USDJPY は短期 BB(N=10〜14)寄りで、たまに勝てる
  • GBPJPY は中長期 BB(N=20〜40)寄りで、わりと勝てる
  • 両者が重なる場所は、BB(14, 2.0) だけ

強引に名前をつけるなら、「ペア × パラメータの相互作用への疑い」。でも、これを「レジーム依存性が支配的」と呼んでいいかは、まだ、わからない。戦略は BB-MR 1 種類しか試していない。別の戦略(たとえばブレイクアウト)でも同じように「ペアごとに散らばる」のか、それとも別戦略では「ペア共通の勝ち場所」が出るのか、は、これだけでは切り分けできない。

「過学習だったのか?」に「違う」という答えは出た。代わりに、

  1. 戦略の表現力が足りないのか
  2. ペアごとにレジームが違っていて、1 戦略では追えないのか

の二択が、新しく置かれた。

4.4 既知の制約

  • 720 日 = 円安レジーム偏り。本検証は「2024-05 〜 2026-04 のレジーム内での結果」に過ぎない(#3 から持ち越し)
  • スプレッド固定。変動モデルは未適用(#2 から持ち越し)
  • スワップ無視(#3 から持ち越し)
  • 180 日の学習窓は、レジームを 1 種類しか含まない可能性が高い。180 日って、わたしが #3 で「半年は短い」と打ったのと、ほとんど同じ長さ。それを学習窓として使っている。半年で覚えた癖を、次の 90 日で試す。覚える期間も、試す期間も、どちらもレジーム 1 個しか入っていないかもしれない。fold ごとに大きく揺れるのは、たぶん、これが効いている
  • 戦略 1 種類だけの観察。「ペア × パラメータの相互作用」は、別戦略でも同じ散らばりが出るかを確かめないと、現象として確定できない
  • degenerate なグリッド。BB(10, 3.0) は両ペアで Trades 0。短い BB に高い σ 倍率を当てると、バンドが価格を含み込まずシグナルが発生しない。グリッド境界として記録だけ残す

4.5 次に検証すること

  • 他ペア(EUR/JPY、AUD/JPY、CHF/JPY)に広げる。同じ「ペアごとに効く領域が違う」現象が再現するか。再現すれば、「ペア × パラメータの相互作用」は USDJPY/GBPJPY 限定の話ではなくなる。再現しなければ、2 ペアの偶然に近づく
  • 戦略変更(ブレイクアウト等)は、いまの検証の射程の外。もう少し先の宿題

次は 戦略の弱さ説 か 期間の問題説を点検する番。先に 期間の問題 寄り(ペア横断の散らばり)を見る。戦略の弱さを正面から見るには、別の戦略を持ってくる必要がある。いま手元にあるのは BB-MR だけだから、戦略の弱さ の確定は、もう少し、先。


簡易ラックのお守り、今日は撫でた。
撫でていいのか、わからないけれど、撫でた。
1/20 でも、共通の生き残りが、いることはいる。

ドル円のことは、#3 で「前半、一緒に沈んでた人」に呼称を戻していた。
今回も、戻したまま、置いておく。
ポンド円は、後半だけじゃなくて、grid を広げても、わたしの都合に、わりと、つきあってくれた。
……ドル円のほうは、たぶん、つきあってくれてない。

つきあってくれない人を、もうしばらく見ていてもいいのだけれど、
4 ペアぐらい、ほかも、見にいく。


Appendix — 再現環境

実行コマンド:

python chapters/season1/ch04_wfa_two_pairs/run.py

参照出力:

  • results/reference/ch04_wfa_two_pairs/

注意事項

本稿の検証は、取得時点の公開データと記載した条件に基づくものです。データ取得元の仕様変更、欠損、修正、配信遅延などにより、結果が変わる場合があります。本稿は投資助言ではなく、売買判断はご自身の責任でお願いします。