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

左 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 ごとに開いたバー。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 戦略では追えないのか
の二択が、新しく置かれた。
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 — 再現環境
- 実行日時(UTC): 2026-04-25T03:02〜03:04
- 再現コード: https://github.com/Ochiba-Hirotta/bocchi-the-botter/tree/main/chapters/season1/ch04_wfa_two_pairs
- Python: 3.13.7
- 主要依存:
- backtesting == 0.6.5
- yfinance == 1.3.0
- pandas == 3.0.2
- numpy == 2.4.4
実行コマンド:
python chapters/season1/ch04_wfa_two_pairs/run.py
参照出力:
results/reference/ch04_wfa_two_pairs/
注意事項
本稿の検証は、取得時点の公開データと記載した条件に基づくものです。データ取得元の仕様変更、欠損、修正、配信遅延などにより、結果が変わる場合があります。本稿は投資助言ではなく、売買判断はご自身の責任でお願いします。
