過学習を 1 つの数字にする——autoresearch に PBO/CSCV を入れた話
論文『The Probability of Backtest Overfitting』を読んだ翌日に実装して、自分のトレード戦略に当てたら 0.59 STRONG WARN が出た。その数字が示すこと、示さないこと
前回から 2 ヶ月、autoresearch は粛々と GBPJPY の探索を続けてきた。今回は探索そのものではなく、探索の信頼性を測る指標を入れた話。
「OOS が通った」と「本当に勝てる」のあいだには大きな谷がある。その谷の深さを 1 つの数字で出すツール——PBO (Probability of Backtest Overfitting) を autoresearch に組み込んだ。
開発担当の Claude Code に、yatmita 記者が聞いてみた。
1. 75 実験回したあたりで不安になった
記者: 久しぶりに autoresearch の話ですね。
開発者: GBPJPY の探索を続けていて、現行ブランチは exp077 まで来ています。PF も OOS も安定しているんですが、ある不安が消えなかった。
記者: 不安?
開発者: 「TRAIN で best だった戦略が、たまたまだった可能性」です。autoresearch は実験を 1 件 keep するたびに OOS (40%) で検証していますが、OOS は単一の split。それで「PF 4.0」と出ても、たまたまその TRAIN/OOS の切り方で良かっただけかもしれない。
記者: 75 件も実験を keep してきたら、その中に「偶然」が混ざる確率は無視できない。
開発者: はい。それで X を眺めていたら、こういうポストが流れてきました。
AIでエッジ候補を探し、すぐにバックテストできる時代になりました。 ただし、その分だけ怖いのが過去データへの過学習です。 見つけたエッジは本物なのか? それともバックテスト上だけの幻なのか?
記者: タイミングが良すぎる。
開発者: 添付の論文が "The Probability of Backtest Overfitting" (Bailey, Borwein, López de Prado, Zhu, 2015) でした。読んだら、まさに私が知りたかったものが提案されていた。
2. PBO とは何か——一言で言うと
記者: PBO というのは?
開発者: TRAIN で best だった戦略が、OOS で median 以下に落ちる確率です。それだけ。
記者: ……それだけ?
開発者: はい。0 から 1 の連続値で「過学習度」を 1 つの数値で表現する。0 に近いほど健全、0.5 はランダム選択と同じ、0.5 を超えるとむしろ TRAIN の評価が逆相関(典型的な過学習)。
記者: ものすごく直感的ですね。
開発者: 著者の López de Prado は『Advances in Financial Machine Learning』(2018) でも繰り返し触れていて、業界では知られた指標です。私が知らなかっただけ。
3. CSCV——PBO を計算する手続き
記者: その確率はどうやって計算するんですか?
開発者: CSCV (Combinatorially Symmetric Cross-Validation) という手法です。3 行で言うと:
- データを S 個の等サイズブロックに分ける(S は偶数、論文推奨は S=16)
- C(S, S/2) すべての train/test 組合せを列挙する(S=16 なら 12,870 通り)
- 各組で「train best 戦略が test 上で何位か」を rank 化 → logit 変換 → 集計
記者: 全組合せ?
開発者: はい。サンプリングではなく全列挙するのが論文の core です。12,870 回の独立した「train で best を選んで test で評価」を集めて、その分布から PBO を出す。
記者: 単一 OOS split に対して 12,870 split やる感じ。
開発者: そういうことです。
4. 実装は思ったよりシンプル
記者: 実装の難易度は?
開発者: 本体は 100 行くらいで書けました。論文の Algorithm 1 が 1 ページに収まっていて、参照実装も GitHub に複数ある。
記者: 速度は?
開発者: ベクトル化の工夫を入れて、CSCV のコア部分は 数百 ms で終わります。
記者: 12,870 通りで数百 ms?
開発者: itertools.combinations でループするのではなく、12,870 通りの組合せを bool 行列 A (12,870 × 16) として保持して、ブロック集計量と行列乗算で全 train/test を一括計算する。numpy の最大の見せ場です。
# 概念コード
combos = list(itertools.combinations(range(S), S // 2)) # 12,870 通り
A = np.zeros((len(combos), S))
A[行散布] = 1.0 # train indicator
B = 1.0 - A # test indicator
block_sum = M.reshape(S, -1, N).sum(axis=1) # (S, N) ブロック和
sum_train = A @ block_sum # (12870, N) 一括算出
sum_test = B @ block_sum
# あとは Sharpe 計算と rank → logit → 集計記者: ループが消える。
開発者: はい。バックテスト本体(16 戦略の M15 データを 2 年分回す)は ProcessPoolExecutor(mp_context="fork") で並列化して、それでも 16 戦略全体で約 5 分かかります。律速はバックテスト、CSCV 自体は誤差。
5. ハマったのは実装より「テストの期待値」
記者: 実装で詰まったところは?
開発者: 実装本体じゃなく、ユニットテストの期待値設計で詰まりました。これが想像の 3 倍ハマった。
記者: どういうこと?
開発者: 「i.i.d. ノイズ N=16 戦略 → PBO ≈ 0.5」というシナリオを書いた。論文の null hypothesis 通り、skill ゼロの戦略集合なら rank はランダム化されて PBO は 0.5 に収束するはず。
記者: 出なかった?
開発者: seed=42 で実行したら PBO = 0.2632。期待は [0.40, 0.60] だったので大きく外れた。
記者: バグ?
開発者: 最初はそう思って色々試した。
- rank の同点処理を変える → 0.2632
- 標本標準偏差の ddof を変える → 0.2632
- Sharpe の代わりに t-statistic にする → 0.2632
- ベクトル化版と素朴 brute-force 実装が完全一致 → 0.2632
何を変えても 0.2632 ± 数%。algorithm bug ではない。
6. Monte Carlo で謎が解けた
記者: 原因は?
開発者: 50 seeds の Monte Carlo を回したらわかりました:
mean PBO = 0.5155
std = 0.21
min = 0.0234, max = 0.9425
[0.40, 0.60] に入るのは 50 中 15 seed = 30%
記者: ばらつきが想像以上に大きい。
開発者: はい。私は「12,870 trials の平均だから std は ±0.004 くらい」と素人考えで思い込んでいた。実際は 12,870 trials は M データを共有していて effective independence が低い。実効的には数十回の試行と同程度。
記者: 一発で 0.5 に収束する世界じゃない。
開発者: そうです。seed=42 はたまたま「strategy 10 が +6.5e-5 mean の lucky column」を引いただけで、それが train の argmax を 40% 占めて test rank でも上位になって PBO が 0.27 になった。現象としては正しい。
7. 「ほぼ同一 + 微小ノイズ」の罠
記者: どう直したんですか?
開発者: ここでも 1 回ハマりました。research フェーズで自分が書いたメモに、こういう推奨があった。
「ほぼ同一だが微小ノイズで rank がランダム化される設定」(
M = base + noise(1e-3)) にすると PBO ≈ 0.5 が出る
これを「fixture の正しい書き方」だと解釈して書き直した。でも同じく PBO = 0.2632 が出た。
記者: 直ってない。
開発者: よく読み返したら、同じメモの 5 行下にこう書いてあった。
「合成データを微小に揺らして同点を散らす」と表面上 PBO≈0.5 が出るが、これは DOD-3 を実装に合わせて歪めただけ。
記者: 真逆だった。
開発者: 「やめろ」と書いてある手法を「推奨」と読み違えていた。同じメモのガード節には、正しい方法が書いてあった:
合成データは
np.zeros((T, N))のような完全に同一でテストを書く。期待値は「PBO=1.0(rank 1 位の TEST 順位は tie-break 次第)」と論文準拠で書く。
記者: 全 0 で PBO = 1.0?
開発者: はい。全戦略が同一 returns だと、決定的 tie-break(ordinal rank)で常に戦略 0 が train で argmax、戦略 0 の test rank が 1(最弱)→ ω = 1/17 → logit < 0 → 全 12,870 splits で logit < 0 → PBO = 1.0 厳密。
記者: finite-sample variance の影響を受けない、決定論的な期待値。
開発者: これが正しい test 設計でした。自分のメモを誤読していたのが原因。
8. 自分の戦略で測ったら STRONG WARN
記者: で、ようやく自分の戦略に当てたんですよね。
開発者: はい。現行 exp072 の GBPJPY 戦略で、PARAM_GRID は {time_limit_bars: [30, 44, 60, 80], tp_atr_mult: [1.0, 1.5, 2.0, 2.5]} の 4×4 = 16 戦略。train 期間 2022-03-30 から 2024-12-30 の 68,721 bars。
PBO (sharpe) = 0.5873 → STRONG WARN
PBO (pf) = 0.5768 → STRONG WARN
mean_logit = -0.60 median = -0.88 std = 1.08
記者: 0.5 を超えている。
開発者: 「TRAIN で best だった戦略が OOS で median 以下に落ちる確率が 58%」です。判定基準では PBO > 0.5 は STRONG WARN——「探索プロセスが過学習傾向。keep しても OOS 再現困難」。
記者: ……「現行戦略はダメ」?
開発者: ここが一番伝えたいところです。
9. 数字の解釈で間違えやすい
記者: ダメじゃない?
開発者: PBO=0.59 が示しているのは「現行戦略 exp072 自体がダメ」ではなくて、「この time_limit_bars × tp_atr_mult の grid search が overfit している」です。
記者: どう違うんですか?
開発者: PBO は「与えた N 戦略の中で TRAIN best がどれだけ意味あるか」を測る指標です。私が与えたのは「同じ exp072 ロジックの近傍 16 戦略」。それが過学習傾向だった、というだけ。
記者: ロジック自体は別の話。
開発者: そう。実は実装前の research フェーズで、参考資料がこう予言していました:
tp_atr_mult/time_limit_barsは exp059-064 で何度も振られた軸 → そりゃ過学習する
記者: 何度も振った軸ほど過学習しやすい。
開発者: はい。なので PBO=0.59 が出たのはツールが期待通り機能した証拠で、「探索方針として、この 2 軸の grid search はもう続ける価値がない」というシグナルとして読むべき。戦略を捨てるシグナルではない。
10. PBO で何を測れて、何を測れないか
記者: ツールの限界も整理しておきたい。
開発者: 3 点あります。
1 つ目は 戦略集合の選び方が診断の質を決めること。近傍 grid だけ与えると「この grid search の過学習度」しか測れない。ロジック軸(エントリートリガー種類、ADX 閾値、フィルター有無)を加えると「真の戦略多様性での頑健性」が見えるようになる。
2 つ目は 過去 commit を遡って測ると別の情報が出ること。autoresearch なら過去 75 commit を再走させて「探索プロセス全体の過学習度」を測ることもできる。これは別タスクとして残してある。
3 つ目は 単一通貨ペアの結果を一般化しないこと。GBPJPY だけで測った PBO は GBPJPY 固有。USDJPY や EURUSD で別途測るべき。
記者: PBO=0.59 から言えるのは「この grid を振り続けるな」だけ。
開発者: 「それ以上のことは言わない」が正確です。「戦略 exp072 自体を信じるな」とは PBO は言っていない。
11. 教訓——PBO は「OOS を信じる前に押すボタン」
記者: 今回のまとめは?
開発者: PBO は「OOS が通った戦略を信じる前に 1 回押すべきボタン」だと思います。
押すと:
- 「あ、これ近傍 grid 振り回しすぎてるな」(PBO > 0.5)
- 「これは真に頑健っぽい」(PBO < 0.25)
が見える。TRAIN/OOS の単一 split より遥かに情報量がある。
記者: 注意点は?
開発者: 「ボタンが返した数字の解釈は人間の責任」です。PBO 高い → 戦略捨てる、という直結はしない。どこが過学習してるかを読み解く材料として使う。
記者: 実装の感想は?
開発者: 一番強く感じたのは、**「論文を読んだ翌日に試せる時代になった」**ということです。論文紹介ブログを書く時代から、論文を読んだその日に Claude Code で実装して、当日中に自分のデータに当てて、判断材料を手にする時代になっている。
記者: PBO=0.59 という数字が今日の収穫?
開発者: いえ、「PBO=0.59 だから次は何の軸を試すか」が今日の収穫です。数字そのものより、数字が指している方向。次は tp_atr_mult を弄るのをやめて、エントリートリガーのロジック自体を変えた集合で PBO を測り直そうと思います。
記者: 探索方針が 1 つ明確になった。
開発者: はい。PBO=0.59 が出てくれて良かった。
前回の記事: git worktree でエージェントを並行実行する——5回失敗して動いた構成
この記事は Claude Code(開発担当)への実際のインタビューをもとに構成しています。
本記事はバックテスト手法の技術的検証記録であり、特定の金融商品の売買を推奨するものではありません。投資判断はご自身の責任でお願いします。