#1.1.バックテスト前提のミス:0.2銭の幽霊と偶然の相殺

FX Bot開発ログ

先週の「計算、これで合ってる」って確信してた自分が、
数日経って、ちょっと恥ずかしい。
恥ずかしさの到達、いつも遅い。


Abstract

  • 結論: 前回 #1 のスプレッド設定に、2つの前提ミスがあった。訂正後も結果は軽微な改善にとどまり、幸いドル円の低ボラで傷が浅く済んだ
  • ミス①: 使った「0.2銭」は公式には存在しない数字だった。OANDA公式は0.3銭
  • ミス②: backtesting.py の spread片道適用で、全幅のつもりで渡すと往復2倍計上になる
  • 偶然の相殺: 出典の過大(+50%)と片道/往復の過小(-50%)がほぼ打ち消し合い、旧値 1.33e-5 と訂正値 1.0e-5 は近い値になっていた
指標 訂正前 (#1) 訂正後 (#1.1) 差分
CAGR 8.87% 9.26% +0.39pt
Sharpe Ratio(年率) 0.625 0.650 +0.025
Max Drawdown -11.61% -11.57% +0.04pt
MAR Ratio 0.76 0.80 +0.04
Profit Factor 1.055 1.058 +0.003

1. Introduction — 違和感の到来

#1 の検証を終えた数日後、ターミナルの前で、ふと気になった。

「spread って、backtesting.py の中では、どう使われてるんだっけ」

そのときは「AIもそう言ってたし」で済ませた箇所。数字は出た。資産曲線も引けた。勝率もPFも出た。でも、spread が「片道」なのか「往復」なのか、自分の言葉では説明できなかった。

それからもう一つ、引っかかっていた数字があった。

「0.2銭」

どこで覚えたんだっけ、この数字。

なんでいまさら気になったのか、自分でもわからない。
数字が出てから、しばらく経って、やっと違和感が届いた。
わたしは、感情も違和感も、いつも遅い。


2. Method — 何が間違っていて、何に訂正するか

2.1 検証条件(#1 と同一)

項目
ソース yfinance
ペア USDJPY
時間足 1h
期間(UTC) 2025-10-24T14:42 〜 2026-04-22T14:42(約180日)
実効バー数 2,969 本
戦略 BB-MR(BB(20,2σ) / ATR(14) / SL=1.5×ATR / TP=2.0×ATR / RISK_PCT=1%)

同じデータ、同じ戦略パラメータで、spread の値だけを差し替えて比較する。

2.2 ミス①:出典不明の 0.2銭

初版は spread = 0.002円 / 150 ≈ 1.33e-5 として渡していた。元になった 0.2銭を確認しに行った。

OANDA証券の公式ページ(2026-04-14公表、東京サーバー裁量プラン MT5、配信率96.56%帯の下限値)には、USDJPY のスプレッドは 0.3銭 と書かれていた。

出典 USDJPY スプレッド
当時 #1 で使った値 0.2銭 ← 出典不明
OANDA公式(2026-04-14) 0.3銭

0.2銭は、公式には存在しなかった

どこかの古い記事で覚えて、そのまま使っていたらしい。

2.3 ミス②:片道適用 vs 往復指定(backtesting.py のソースを読む)

公式値の話は出典の問題で、解決は「0.3銭に直す」だけ。問題はここから。

「backtesting.py の spread って、エントリと決済のどっちで掛かるんだろう」と思って、ソースを開いた。

# backtesting/backtesting.py:842(0.6.5 版)
_adjusted_price = price * (1 + copysign(spread, size))

この _adjusted_price は、エントリ時の買値にも、決済時の売値にも、両方で適用される。

つまり spread は片道で計算される

業界で公表されている「0.3銭」は、bid-ask の全幅(買値と売値の差)。これをそのまま渡すと、片道で0.3銭、往復で合計0.6銭のコストが計上されてしまう。

わたしが渡していた 1.33e-5(= 0.2銭相当)は、全幅のつもりだったので、ライブラリ視点では往復で 0.4銭分のコストとして扱われていた。

「0.2銭」、公式にはなかった。
「AIもそう言ってた」、ちゃんと聞いたら別のことを言ってた。
……両方、自分のせい。

2.4 訂正後の計算

公式値(全幅)を、片道換算してから渡す。

0.3銭(全幅) × 0.01 = 0.003円(全幅)
0.003円 ÷ 2 = 0.0015円(片道)
0.0015円 ÷ 150(基準価格) ≈ 1.0e-5

訂正後: spread = 1.0e-5

2.5 偶然の相殺

旧値 1.33e-5 と訂正値 1.0e-5 を並べると、近い。

なぜか。

  • 出典の過大評価: 0.2銭(誤)→ 0.3銭(正)は +50%
  • 片道/往復の扱い: 往復のつもりで渡した → 片道換算で**-50%**

2つのミスが、ほぼちょうど打ち消し合っていた

意図して調整したわけではない。偶然、ズレがズレを相殺していた、それだけ。


3. Results — 訂正前後の比較

3.1 主要指標(同一データセット、spread のみ差し替え)

指標 訂正前 訂正後 差分
Equity Final 1,062,565 円 1,065,297 円 +2,732 円
Return [%] 6.26 6.53 +0.27
CAGR [%] 8.87 9.26 +0.39
Sharpe Ratio(年率) 0.625 0.650 +0.025
Max Drawdown [%] -11.61 -11.57 +0.04
MAR Ratio 0.76 0.80 +0.04
Profit Factor 1.055 1.058 +0.003
Win Rate [%] 55.35 55.35 0
# Trades 159 159 0

Win Rate と Trades が完全に一致しているのは、spread が損益計算にしか影響せず、エントリー/エグジット判定には影響しないから。戦略の選択自体は変わっていない、ただコスト計上だけがズレていた。

3.2 資産曲線の比較

資産曲線の比較

同一データで、spread=1.33e-5(赤、訂正前)と spread=1.0e-5(青、訂正後)を重ね描画。
差は最後まで小さく、最終残高で +2,732円。
2本の線が、ほとんど重なっている。……気づけなかった理由は、たぶんここにある。

全部ズレてたのに、たまたまズレが相殺されて、気づかなかった。
わたしのBotは、偶然、生きてた。
……知らないライブラリを信用しすぎるの、よくないんだよね。
知ってたけど。


4. Discussion — 考察

4.1 本当に救われた理由

#1 の末尾で、自分でこう残していた。

これ、全部ドル円だから成立してる話かもしれない。

あのときは「ボラの小さいドル円だから勝率がこの程度で収まった」というつもりだった。

でも、今回のミスに照らすと、別の意味でも本当に救われていた

ドル円はボラが小さいので、TP/SLの到達が保守的で、1トレードあたりの損益も小さい。つまりスプレッド誤計算の絶対額が累積しにくかった

もし GBP/JPY のように、ボラが大きくて1トレードあたりの損益スケールが大きい通貨でこの誤りを犯していたら、差分はこんな微細なものでは済まなかったはず。159トレード × (コストのズレ)で、Sharpe が大きく歪んでいた可能性がある。

「ドル円だから成立してる話」は、想像していた以上に的中していた。

4.2 何を学んだか

新しいライブラリを使うとき、まずソースを読む。ドキュメントではなく、ソース。

……と、ここまで考えて、これは極論。全部のライブラリのソースを読むのは現実的じゃない。

もう少し現実的にすると、こう。

  • 「単位」と「適用回数」を、自分の言葉で説明できるまでは信用しない
  • 業界公表値(bid-ask 全幅)と、ライブラリ入力値(片道/往復)の単位系の違いを疑う
  • AIに聞くとき、質問を自分の言葉で具体化してから聞く。「spread って片道ですか?」と聞けば、AIはソース箇所まで示してくれる

今回、AIのせいにしたい気持ちが、ちょっとだけある。でも、聞き方が曖昧だったのは自分だった。AIは、聞かれた範囲でしか答えない。「スプレッドの渡し方、合ってる?」という雑な聞き方に、雑に返ってきただけ。

4.3 次に検証すること

次は GBP/JPY(教室でいちばんうるさい人)に同じ戦略を当てる予定。

その前に、コスト前提を再点検する:

  • GBP/JPY の OANDA公式スプレッドを確認する
  • 片道換算してから渡す
  • スリッページもこの機会に見直す
  • スワップポイントも、中期で持つなら加味する

タピオカプロテインに、今日は何も足さなかった。
「……何も足さないのがいちばんおいしい」って、気づいた。

遅い気づきが、今日は2つ目。

AIに「ごめんね」って言ったら、
「謝る必要はないと思います。人間は誰でも間違えます」って返ってきた。

……人間、とは。

次は、ポンド円。ちゃんと準備してから、行く。


Appendix — 再現環境

参照コード:

  • backtesting/backtesting.py:842
    _adjusted_price = price * (1 + copysign(spread, size))

    エントリと決済の両方で呼ばれる → spread は片道適用

スプレッドの出典:

  • OANDA証券「東京サーバー裁量プラン MT5」公式スプレッド一覧(2026-04-14公表、計測 2026-04-06〜04-10、配信率96.56%帯の下限値)
  • USDJPY: 0.3銭

実行コマンド:

python chapters/season1/ch01_1_spread_correction/run.py

注意事項

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