B面#3)ORBを15分足へ戻したら、戻る場所がなかった

FX Bot開発ログ

「この一本だけ持っていく」と決めたのが、うれしかったらしい。
気づいたのは、三日あとだった。

Abstract

  • 出典: Trading Steady「Is the ORB Strategy Actually Profitable? 6 Month Results
  • 対象: OANDA practice / USDJPY / M5(bid・ask)の固定抽出から集約した、完全な15分足だけ
  • やったこと: 1時間足へ翻訳していたORBのレンジを、原文の9:30–9:45 ET(M15一本)へ差し戻し、bid/ask別のOHLCで約定を判定する事前固定ルールで検証
  • 結果: 事前に決めた主判定(決済後残高のリターンがプラス、かつ固定5区間中3区間以上で実現損益がプラス)は不成立
  • 非対象: 原文(EURUSD・原著者の環境)の再現・評価、以前の1時間足版との成績比較、実注文の約定再現、実運用の可否
指標主版
トレード数292
リターン-21.647934%
最大ドローダウン(決済後残高)-26.542576%
プロフィットファクター0.833976
勝率44.178082%
プラスの固定区間1 / 5

1. Introduction — 切符

あの夜は、判断を終えただけだと思ってた。

M5の正本から、9:30 ETの15分足を一本、m15_opening_rangeという変数に置いた。それだけ。うれしいとか、そういうのは、なかったはずだった。

三日たって、気づいたら、あの1時間足のコードを開いていた。

一本の意味が、途中で変わってた。置いたときは「持っていく」だった。三日のあいだに、「これがあれば、あの妥協を消せる」になってた。

前にORBを検証したとき、9:30の15分足が手元になかった。yfinanceの15分足は60日ぶんしか遡れない。だから9:00〜10:00の1時間足へ、レンジを翻訳した。レンジの時刻も、ATRの平均も、16:00の線も、最後はわたしが決めた。決めていたと分かって、怖くなった。そこまでが、この前までのこと。

今は、ある。9:30ちょうどに始まる、三本揃いを確認済みの15分足。

エディタで、1時間足のレンジ定義をコメントアウトする。その下に、M15の一行を書く。

これでやっと、本物のORBが走る。

そう思った。切符を手に入れた人みたいに。


2. Method — 戻った一本、残った条件

2.1 レンジだけは、戻る

いきなり全期間は走らせない。まず一日ぶん。

9:30–9:45 ETの完全なM15一本。そのbidのhighとlowを、レンジの上端と下端にする。前に「9:30から始まる足は、どこにもない」と残っていた場所に、今度はその足が入る。

9:30の足が、9:30のまま置けた。

そこだけは、ちゃんと戻った。

2.2 残った条件を、一つずつ見る

次の条件へ進んで、手が止まる。

通貨。原文はEURUSD。わたしのDBには、USD_JPYしかない。EURUSDを新しく取りにいくこともできるけど、それで増えるのは別のデータの別の検証で、この検証が原文になるわけじゃない。USDJPYのまま。……これは、戻らない。

ブレイクの確認。原文で確認できたのは「ブレイク確認後」まで。終値で確認して、次の足の始値で入る、と決めたのは前回のわたしだった。足を15分にしても、この決め方は要る。戻らない。

12:00 ET以降は建てない。残る。

ATR。原文では平滑の取り方を確認できなかった。14本の単純平均は、わたしが置いた数字。足はM15になっても、平均の取り方はわたしの判断のまま。残る。

16:00に閉じる線。原文にはなかった線。ここにも、そのまま残る。

一つ差し戻して、五つ残った。

項目1時間足版今回扱い
レンジ9:00–10:00 ETの1h一本9:30–9:45 ETのM15一本今回戻したもの
通貨USDJPYUSDJPY翻訳のまま残る
エントリー終値確認 → 次足始値bid終値確認 → 方向別の次足始値残って、判断が増える
ATR1hのTR 14本SMAM15のbid TR 14本SMA足は戻るが平滑判断は残る
12:00 / 16:00ありあり翻訳のまま残る
コスト固定スプレッド0.3銭bid/askの実データ。固定幅の追加なし方式を置き換える

2.3 細かいデータで、判断が増える

表のいちばん下の行が、思っていたのと逆だった。

貯めてきたデータには、bidとaskのOHLCが別々にある。前回みたいに「midに固定0.3銭」と仮置きしなくていい。仮定が一つ消える、と思ってた。

開いてみると、bidとaskのhigh/lowは同じ時刻とは限らないから、混ぜて作れる「正しいmidの高値・安値」がない。レンジもブレイクもATRも、どの系列で見るのか。買いはどっちの価格で約定して、売りはどっちで出るのか。

決める場所が、増えてた。

bidを判定の系列に固定する。レンジも、ブレイク確認も、ATRも、ぜんぶbid。約定だけ方向で分ける。買いはaskで入ってbidで出る。売りはbidで入ってaskで出る。固定スプレッドは、もう足さない。

細かくなったら、決めなくて済むと思ってた。逆だった。

書いた分岐の、真ん中はここ(orb_m15.py):

if side == "long":
    entry_price = float(entry_bar["ask_open"])
    stop_loss = range_low
    initial_risk = entry_price - stop_loss
    take_profit = entry_price + RR * initial_risk
else:
    entry_price = float(entry_bar["bid_open"])
    stop_loss = float(range_bar["ask_high"])
    initial_risk = stop_loss - entry_price
    take_profit = entry_price - RR * initial_risk

決済側の価格、同一足の優先順、窓ぎわの隙間の扱いは、後ろに全部書く。

2.4 揃っている日を、自分で決める

もう一つ、先に決めることがあった。

一日を9:30から16:00まで15分足で追うには、開始時刻が27個、一度ずつ要る。レンジの一本だけじゃなくて、その後ろの26本も。

途中まであった一日を見つける。9:30はある。昼のどこかが、ない。

欠けた場所に前の足を写せば、追えたことにはできる。それは、この前一度やめたやり方だった。一本でも欠けた日は、その日ぜんぶを見送る。欠ける前までの取引も、しない。

920暦日のうち、27本揃った日が651日。揃わなかった日が269日。269日のほとんどは、必要な時刻に足が一本もない日だった。途中まであった日は、1日だけだった。全部を「欠損」とは呼ばない。分類は後ろに置く。

651日から、さらに減る。レンジ幅がATRの1.25倍に届かない日が341日。3.0倍を超えた日が8日。通ったのは302日。そこから、12:00に間に合わなかった7日と、ブレイクが出なかった3日が消えて、292回。

項目固定条件
入力固定範囲のM5抽出 → 完全なM15
ET [2024-01-06, 2026-07-14)、920暦日
有効な日9:30〜16:00 ET開始のM15が27本、一度ずつ揃う日
レンジ9:30–9:45 ETのbid high / low
ATRフィルタbidのTR 14本SMA、レンジ幅が1.25〜3.0倍
エントリー最初のbid終値ブレイク後、12:00 ET未満の次足始値
約定買いask→bid / 売りbid→ask
決済1.5R、同一足はSL優先、16:00 ETの始値で強制手仕舞い
サイズ初期残高100万円、リスク1%、証拠金率4%
コスト手数料0、通常スリッページ0、スワップ0、固定スプレッド追加なし
調整結果を見て条件を変えない

3. Results — 調整しない

3.1 数字

走らせる前に、入力のhashを、前に固定した抽出の値と照合した。一致。走らせる。

指標結果
トレード数292
最終残高783,520.656円
リターン-21.647934%
勝率44.178082%
最大ドローダウン(決済後残高)-26.542576%
プロフィットファクター0.833976
平均実現R-0.079005
プラスの固定区間1 / 5
主判定不成立

-21.65%。

もう一度見ても、マイナスだった。プラスの区間は、五つのうち一つ。事前に決めた判定は、不成立。

3.2 直す場所を探す

数字より先に、どこを直せば別の結果になるかを考えていた。

実行条件を見る。合ってる。テストを回す。通る。区間の境界。事前に置いた日付のまま。入力のhash。さっき照合した。……間違いは、見つからない。

1.25。3.0。1.5R。12:00。16:00。どれかを動かせば、別の数字が出る。

カーソルが、1.25の上にあった。

変更せず、閉じた。

3.3 内訳

買いが161回、売りが131回。閉じた理由は、16:00の強制手仕舞いが126回、損切りが121回、利確が45回。

決済理由の内訳

292回の決済理由。時間切れ(close_16)と損切り(sl)で大半を占め、利確(tp)は45回。

3.4 固定した5区間

窓は先に、184日ずつ五つに切ってあった。

区間取引数実現損益
159-11,931円
268+34,729円
364-46,990円
456-68,894円
545-123,393円
固定184日ごとの区間損益

920暦日窓を開始日から184日ずつ5区間に固定分割。プラスは区間2だけ。区間ごとの標本は45〜68本で、傾きの理由までは、ここからは言えない。

固定した五つの区間のうち、プラスは区間2だけだった。観測は、そこまで。


4. Discussion — 隣

4.1 この数字の場所

この-21.65%が判定しているのは、わたしが走らせる前に固定した条件の集合。原文の手法じゃない。EURUSDでもないし、原著者の環境でもない。

1時間足版とも、比べられない。データの取得元も、期間も、価格の系列も違う。15分足へ戻したから負けた、とも言えない。レンジと一緒に、約定の方式も、データの出どころも変わってる。

数えてみる。原文から確認して運んだ条件。前回から残った翻訳。今回増えた判断。それが一つの場所に集まって、この数字が出た。集めたのは、ぜんぶわたしだった。

原文の場所でもなくて、1時間足版の場所でもない。第三の条件の集合に、この結果は属してる。

4.2 上書き

保存しようとして、ディレクトリ名の入力欄で止まった。

三日前のわたしは、1時間足版を仮の姿だと思ってた。データがなかったせいの、代用品。だったら、15分足版で置き換えれば済む。同じ名前で保存し直せばいい。

orb_1h_translationのディレクトリが、画面の隅にある。82本の明細と、あのときの条件が、ぜんぶそこに残ってる。

手が、止まった。

4.3 名前

15分足版に、orb_m15_retranslationと名前を付けた。

failedとは付けない。負けたことは、名前にしない。原文版とも、完成版とも呼ばない。

1時間足版は、そのまま。292件の明細と、-21.65%の結果も、同じ場所に保存する。

ファイラーの中で、二つのディレクトリが並んだ。

少しのあいだ、それを見てた。

戻る場所はなかった。隣に置いて、先へ進む。


Appendix — 再現条件と技術的な境界

A. 入力と同一性

項目
source / environmentoanda_rest_v20 / practice
instrument / granularity / priceUSD_JPY / M5 / BAcomplete=1のみ
固定抽出範囲[2024-01-01T22:00:00Z, 2026-07-14T10:05:00Z)
M5行数188,981
M5抽出SHA-256f6d0e1cd1bd50ec11f7f3f0bd34e31b61a39970a687f3c5ac83682ae2ea1d512
complete M15 / 不完全bucket62,957 / 61

M15はchapters/season2/ch02_minute_data_dbの公開実装aggregate_m5_to_m15で集約した派生で、M5 SQLiteを正本のまま使う。完全再計算には同一の非公開SQLiteが必要。行単位データは同梱しない。

B. 記事窓とsession完全性

  • 取引窓: ET日付 [2024-01-06, 2026-07-14) の920暦日。ATRのwarm-upだけ窓前データを使用
  • 固定5区間(各184暦日、開始日を含み終了日を含まない): 境界は 2024-01-06 / 2024-07-08 / 2025-01-08 / 2025-07-11 / 2026-01-11 / 2026-07-14
  • 有効な日: 9:30〜16:00 ET開始のM15が27本、一度ずつ揃う日。一本でも欠ける日は、欠損前までの取引や保有継続をせず日全体を見送り
  • 内訳: 有効651日 / 見送り269日(必要時刻に0本: 268日、途中まで: 1日)
  • 市場休場かデータ欠損かは、この集計では個別に分類しない

C. ATR・シグナル・エントリー

  • ATR: bidのTrue Range(max(H−L, |H−前C|, |L−前C|))の14本単純平均。現在足を含む直近14本の完全M15。9:30足の確定時点で判定し、未来参照なし
  • フィルタ: 1.25 <= レンジ幅 / ATR <= 3.0。651日の内訳は、1.25未満341日 / 通過302日 / 3.0超8日
  • 通過302日のうち、12:00カットオフ7日 / ブレイクなし3日 / 取引292日
  • エントリー: 最初のbid終値ブレイク確認後の次足始値。11:45のエントリーは可、12:00以降は不可。1日最大1回

D. 方向別の約定と決済

  • 買い: askで建て、SL・TP・16:00決済はbidで判定。SLは9:30足のbid low
  • 売り: bidで建て、SL・TP・16:00決済はaskで判定。SLは9:30足のask high
  • TPは建値からの実行側初期リスクに対して1.5R。エントリーした足も判定対象
  • 各足の優先順: 不利な窓開き → 有利な窓開き → 同一足はSL優先 → 単独のSL/TP。ただし16:00 ET開始足は、この判定より先に始値で強制手仕舞い。窓開き決済は0件だった
  • コスト: 手数料0、通常スリッページ0、スワップ0。固定スプレッドは追加しない(bid/askの幅が価格に含まれる)
  • エントリー時の始値quote幅は最大0.096円。結果を見たあとの外れ値除外はしていない
  • 履歴のbid/ask OHLCによる約定モデルであり、実注文の約定再現ではない

E. 指標の定義と限界

  • リターン: 最終残高 ÷ 1,000,000 − 1。最大DDは初期残高と各取引の決済後残高から算出し、保有中の時価評価を含まない
  • PF: 総利益 ÷ 総損失の絶対値。実現R: 方向別の単位あたり損益 ÷ 初期リスク
  • サイズ: 初期残高100万円、1回の初期リスク1%、証拠金率4%
  • M15のOHLCから、足の中の価格経路、SL/TP到達時点のスプレッドは復元できない
  • この結果を、原文EURUSD、1時間足版、他の期間、ORB一般へ広げて読まない

F. 再現コードと確認

  • 再現コード: GitHub
  • 参照結果の再計算: python chapters/season2/ch03_orb_m15_retranslation/run.py --db "$SOURCE_DB" --private-output-dir ../../code/data/s2_3_orb_m15 --reference-dir results/reference/ch03_orb_m15_retranslation
  • 独立検証: 同chapterのverify.py(集計のみのrow-freeモードと、--trades付きの完全モード)
  • 図の再生成: 同chapterのfigures.py(row-freeの参照JSON/CSVから生成)
  • Python 3.13.14 / pandas 3.0.2。入力・コード・集約物・図のSHA-256はresults/reference/ch03_orb_m15_retranslation/hashes.json

この固定条件とデータ仕様に基づく検証です。データ仕様の変更・欠損・修正などで結果は変わりえます。投資助言ではなく、売買判断はご自身の責任で。