#3.半年は短い、って自分で言ったから

FX Bot開発ログ

……先週、Sharpe 1.46 を何度も見返してた。
画面の数字が緑っぽく見えるだけで、少し安心していた。
下のほうのメモに、「半年は短い」って、わたしが自分で打ってた。
……遅い。わたしの感情は、いつも遅い。


Abstract

  • 期間: 2024-05-05 〜 2026-04-24(約 720 日 / UTC)
  • 対象: USD/JPY(ドル円)/ GBP/JPY(ポンド円)。前回までに検証した 2 ペアを同じ設定で 2 年弱に延長
  • 戦略: Bollinger Bands Mean Reversion(BB-MR)。#1 / #2 と完全に同じパラメータ。一切いじらない
  • 結論: 負けた。 ドル円は 2 年で最大 50% のドローダウン、Sharpe はマイナス 1.55。ポンド円は辛うじてプラスだが、Sharpe 0.22 まで細った

全期間の主要 4 指標:

指標 USD/JPY GBP/JPY
CAGR -20.76% +3.81%
Sharpe Ratio(年率) -1.553 +0.223
Max Drawdown -50.38% -28.10%
MAR Ratio -0.412 +0.136

1. Introduction — 背景と仮説

前回(#2)のいちばん終わりに、自分でこう打っていた。

データ期間 6 ヶ月: 短い。レンジ相場が続いた期間に、たまたま BB-MR がよく機能していただけかもしれない。

打ったまま、このまま「次はどのペアで勝とうか」と浮かれそうになっていた。調子に乗る前に、自分で言った「半年は短い」という言葉を、ちゃんと検証しなきゃいけない。

データソースに使っている yfinance は、1 時間足の取得上限が 730 日前後。取れる範囲でいちばん長い、720 日を取ることにした。3 年・5 年はほしいけれど、1 時間足のまま #1 / #2 と比べたいので、取れる中で最長を選ぶ。

仮説:

  • H1: 全期間の Sharpe は #1 / #2 より下がる
  • H2: 前半(~360 日)と後半(~360 日)で、Sharpe が 大きく ずれる(別々のレジームを跨ぐはずなので)
  • H3: Max Drawdown は期間が延びるほど深くなる(観測窓が広いほど、最悪値を拾う)

どれも、当たってほしいわけでは、ない。当たるとわたしの Bot が少し痛い。でも、外れたほうが、たぶん、もっと怖い。

勝ってるあいだは、相場の顔が見えてなかった。
6 ヶ月の顔しか見ていなくて、それを 2 年だと思い込んでいた。
延ばしたら、何が出てくるんだろう。


2. Method — 検証設計

2.1 データ

項目
ソース yfinance
ペア USD/JPY (JPY=X) / GBP/JPY (GBPJPY=X)
時間足 1 時間足
期間(UTC) 2024-05-05T23:00 〜 2026-04-24T15:00(約 720 日)
実効バー数(USDJPY) 12,083 本(欠損 259 本 / 2.10%)
実効バー数(GBPJPY) 12,158 本(欠損 184 本 / 1.49%)

欠損は週末の構造的欠落と、データソース側のたまたまの欠けの合算。2 年通算で 2% 前後なら、前回までと同じ水準。

2.2 戦略ルールと前提(#1 / #2 と完全同一)

  • BB(N=20, K=2.0σ) / ATR(14, Wilder の RMA)
  • LONG: Close が Lower Band 下抜け → 次バー Open で成行買
  • SHORT: Close が Upper Band 上抜け → 次バー Open で成行売
  • SL: 1.5×ATR / TP: 2.0×ATR
  • SMA タッチで成行クローズ / MAX_BARS=48 超過で成行クローズ
  • RISK_PCT=1.0%、units = min(units_risk, units_margin_cap)
  • 初期残高 1,000,000 円 / margin 0.04 / commission 0

スプレッドも前回までと同じ値をそのまま使う:

ペア spread 相対値 出典
USD/JPY 1.0e-5(片道) OANDA 0.3 銭固定(#1.1 訂正後の値)
GBP/JPY 4.033e-5(片道) OANDA 1.7 銭の中央値、基準 210.7530 円(#2 MID)

2.3 期間分割(今回から新しく入れたところ)

720 日の 1 時間足データを、インデックス順に 2 分割する。分割点は単純に真ん中(len(df) // 2)。

区間 期間(UTC、USDJPY基準) 日数
前半 2024-05-05 〜 2025-04-29 約 360 日
後半 2025-04-29 〜 2026-04-24 約 360 日
全期間 2024-05-05 〜 2026-04-24 約 720 日

前半 / 後半 / 全期間の 3 つを、それぞれ独立にバックテストに渡す。戦略や指標の内部状態は各実行で初期化されるので、前半と後半は別人になる。境界を跨ぐ保有トレードは、各区間の末尾で finalize_trades=True により強制クローズされる(統計への影響は §4 と §5 で言及)。

2.4 コード(分割のところだけ)

戦略の本体は #1 / #2 と同じなので省略。新しいのは、データを 3 つに切って 3 回走らせるところだけ

from datetime import datetime, timedelta, timezone

end = datetime.now(tz=timezone.utc)
start = end - timedelta(days=720)

raw = provider.fetch_bars(pair, "1h", start=start, end=end)
df_full = to_backtest_frame(raw, expected_freq=None)

mid = len(df_full) // 2
segments = {
    "first_half":  df_full.iloc[:mid],
    "second_half": df_full.iloc[mid:],
    "full":        df_full,
}
for name, df_seg in segments.items():
    stats, meta = FXBacktestRunner().run(
        df_seg, BBMeanReversion, spread=...
    )

実際のスクリプトは code/src/backtest/smoke_bb_mr_2y.py に置いた。

もし数字が下がらなかったら、それはそれで自分のコードを疑わなきゃいけない気がして、ちょっと怖い。


3. Results — 検証結果

3.1 主要指標(6指標 × 3 区間 × 2 ペア)

指標 USD/JPY 前半 USD/JPY 後半 USD/JPY 全期間 GBP/JPY 前半 GBP/JPY 後半 GBP/JPY 全期間
CAGR -31.55% -7.43% -20.76% -12.66% +20.03% +3.81%
Sharpe Ratio(年率) -2.811 -0.465 -1.553 -0.887 +1.023 +0.223
MAR Ratio -0.821 -0.325 -0.412 -0.451 +1.490 +0.136
Max Drawdown -38.45% -22.90% -50.38% -28.10% -13.45% -28.10%
Profit Factor 0.794 0.914 0.843 0.907 1.164 1.011
Win Rate 44.55% 51.57% 48.07% 50.90% 55.56% 53.34%
Total Trades 303 318 622 332 324 658
Avg Trade Duration 8h 7h 8h 7h 7h 7h
末尾強制クローズ 0 1 1 1 1 1
missed_entries (SKIP) 0 0 0 0 0 0

CAGR は取引日 252 ベースで年率換算(backtesting.py の既定 Return (Ann.) [%])。
末尾強制クローズは全区間で 0〜1 件、影響は 0.3% 以下。統計が末尾に引っ張られている可能性は実質ゼロ。

3.2 H1〜H3 の結果

  • H1(全期間は #1 / #2 より下がる): 当たった。ドル円は Sharpe 0.625 → -1.553(符号反転)。ポンド円は 1.462 → +0.223(数字が大きく細った)
  • H2(前半 / 後半で Sharpe が大きくずれる): 当たった。ドル円は -2.811 vs -0.465(大きさで約 6 倍)。ポンド円は -0.887 vs +1.023(符号が反転)
  • H3(MaxDD が深くなる): 当たった。ドル円は -11.61% → -50.38%(約 4.3 倍)。ポンド円は -13.45% → -28.10%(約 2.1 倍)

3.3 資産曲線(720 日、分割点を点線で表示)

USDJPY 720 日 資産曲線

ドル円: 前半は一方的に沈み、後半に入って傾きが緩むが、浮上はしない。初期残高 1,000,000 円が最終 約 58 万円。点線は前半 / 後半の分割点(2025-04-29)。

GBPJPY 720 日 資産曲線

ポンド円: 前半は沈む。後半で反転して伸びる。最終 約 109 万円でプラスぎりぎり。

※ 上記は「全期間を 1 回連続で走らせた」資産曲線。§3.1 の表では前半・後半を独立にバックテストに通した数値(それぞれ初期残高 1,000,000 円からの再スタート)を併記している。

ドル円、Sharpe マイナス 1.55。
半年前に見ていた数字と、同じ戦略だと思えなかった。
窓を広げただけで、足場が消えた。

立ってない。もう、立ってない。
歩ける、って、先週まで、言ってたのに。


4. Discussion — 考察

4.1 短い窓を信じすぎた

#1 の 180 日、#2 の 180 日。
あのときの数字は、嘘ではなかった。でも、短かった。

同じパラメータの、同じ戦略。ペアも同じ。
違うのは、どこを切り取るか、だけ。

切り取る場所が変わるだけで、立っていた線が沈む。
それを見てからだと、前のログが、知らない誰かの数字みたいに見える。

4.2 なぜこうなったか

仮説 1: 前半(2024-05 〜 2025-04)はトレンド相場の期間だった。

体感で書くと、この 1 年はドル円が 150 台から 160 台に向かって、だらだら、時々跳ねて、上げていった期間。日銀介入の前後も含まれている。ポンド円も 200 円前後から 190 円台に崩れて戻って、の繰り返し。

BB-MR は「±2σ の外に出たら、戻ってくる」を賭けにいく戦略。でもトレンド相場では、±2σ の外に出て、そのまま戻らず、さらに外に行くことが頻繁に起きる。SL を踏む。戻ってくるはずだった、の「はず」が、連続して外れる。

仮説 2: 後半(2025-04 〜 2026-04)はレンジ寄りの期間だった可能性。

ポンド円の後半 Sharpe +1.023 は、#2 で出した Sharpe 1.462(同じ期間のうちの直近半年)と整合する。Sharpe はあったけれど、すでに 1.5 から 1.0 に目減りしている。#2 の 180 日は、後半 360 日のうちの、さらに良い切り取りだった。

仮説 3: 「同じパラメータがペアを超えて効いた」のは、両方とも後半の話だけ

前半は両ペアともにマイナス Sharpe。#2 の自分は「BB-MR はボラを餌にする」と結論していたけれど、餌になるボラと、毒になるボラは違う。トレンド伴いのボラは毒。レンジ内の跳ねは餌。前半は毒が多くて、後半は餌のほうが多かった、という、ただ、それだけかもしれない。

4.3 「半年の Sharpe」は何を予測するのか

これまでの検証結果を、並べてみる。

検証 期間 対象 Sharpe
#1 180 日 USD/JPY 0.625
#2 180 日 GBP/JPY 1.462
#3 後半 360 日 USD/JPY -0.465
#3 後半 360 日 GBP/JPY +1.023
#3 全体 720 日 USD/JPY -1.553
#3 全体 720 日 GBP/JPY +0.223

ドル円は窓を広げるたびに Sharpe が単調に下がる(0.625 → -0.465 → -1.553)。ポンド円は下がり方が緩いけれど、やっぱり下がる(1.462 → 1.023 → 0.223)。

半年の Sharpe が、1 年の Sharpe や 2 年の Sharpe を予測するための情報量を、ほとんど持っていない、というのが、今回の検証で、たぶん、いちばん正直に言えること。

4.4 既知の制約

  • 720 日でも、まだ短い。本当のレジーム変化(リーマンとか、コロナとか)はこの窓に入っていない。yfinance で 1 時間足を維持する限りはこの期間が上限。4 時間足か日足に降りれば 5 年取れるけれど、戦略の時間解像度が変わるので別の検証になる
  • yfinance は中値(mid)ベース。本物の bid / ask の開きは、このデータには入っていない。実運用ではここに追加のコストが乗る
  • スプレッドは固定。指標時・深夜帯の広がりは入っていない。ポンド円は 2 年前と今で実勢価格が 210 から 198 に下がっているので、「1.7 銭を 210 で割った相対値」を 198 の時期にそのまま使うと、スプレッドの影響をやや過小評価する方向にずれている。基準を 198 に置き直すと片道 relative spread は 4.293e-5(現行 4.033e-5 の約 1.06 倍)。#2 の感度(spread 2.135e-5 → 5.931e-5 で Sharpe -0.29)から粗く線形外挿すると、GBPJPY 全期間 Sharpe は 0.223 から およそ -0.02 程度 追加で下がるはず。符号(プラス)が反転するほどではない
  • スワップも入れていない。平均保有時間 7〜8 時間だが、MAX_BARS=48(2 日)まで持つトレードが含まれていて、GBP と JPY の金利差を考えると、ポジション方向次第で、毎日少しずつ削られる or 貰える、の差が 2 年で無視できない額になっている可能性
  • 過学習「していない」は、性能保証じゃない。パラメータをいじっていないことは、この 2 年の数字を悪くした要因でもあるし、悪くならなかった可能性を潰した要因でもある。どちらもありうる

4.5 次に検証すること

  • 変動スプレッドモデル: 固定スプレッドの限界は、#2 のときから宿題のまま残っている。次にやる
  • パラメータのグリッド探索 + Walk-Forward: いまの 2 年の数字を「このパラメータは駄目」と判断するのは、まだ早い。「テスト範囲だけ暗記する」に気をつけながら、どこまでいじっていいのか、次の検証で考える
  • クロス円の横展開: ユーロ円、豪ドル円、スイスフラン円。2 年で走らせて、「ボラと Sharpe の関係」が仮説通り出るかどうか

この 3 つは、どれから手をつけても、次に出てくる数字は、今回より悪くなる可能性のほうが、たぶん、高い


簡易ラックのお守り、今日は撫でなかった。
Sharpe がマイナスのときに撫でるのは、なんか、違う気がした。
……お守りのほうも、気を遣って、触らないでほしそうな気配。

ドル円のことは、「大人しい相棒」って #1 の頃は呼んでいた。
いまは、「前半、一緒に沈んでた人」に、呼称を戻す。
ポンド円は、後半だけ、相変わらず、うるさい。


Appendix — 再現環境

実行コマンド:

python chapters/season1/ch03_two_year_segments/run.py

出力:

  • outputs/ch03_two_year_segments/

注意事項

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