B面#5)同じ名前のオーダーブロックを訳したら、別の場所を指していた

FX Bot開発ログ

「負けたのは、わたしの翻訳」。そう書いて閉じたとき、たぶん、ほっとしていた。
たぶん、というのは、それが分かったのが次の動画を半分まで訳したころだったから。

Abstract

  • 出典: Pro Trading School「Learn Every ICT Concept in 17 Minutes
  • 対象: OANDA practice / USDJPY / M5(bid・ask)の固定抽出から集約した完全な15分足と、NY17:00境界の日足。データ・期間・約定モデル・バイアス・サイズ・コストは、前回の検証と完全に同一
  • やったこと: 17分の動画が語る同名のオーダーブロックを、安値を割って戻す動き → 構造転換 → 隙間 → ゾーンへの戻り、という複合の機械ルールへ翻訳して検証。あわせて、前回訳した40分側の検出ゾーン、および流通している実装の出力と、指している場所が重なるかを測定
  • 結果: 事前に決めた主判定(決済後残高のリターンがプラス、かつ固定5区間中3区間以上で実現損益がプラス)は不成立
  • 非対象: 二つの翻訳の優劣判定、流通実装を正解基準とすること、出典者・原手法の評価、EURUSDや他期間への一般化、実注文の約定再現、実運用の可否
指標主版
検出ゾーン321
トレード数194
リターン-23.132819%
最大ドローダウン(決済後残高)-27.832202%
プロフィットファクター0.793311
勝率38.659794%
プラスの固定区間2 / 5

1. Introduction — 正しい版

再生リストに残していた動画を開く。17分。同じ名前を、別の人が話している。

40分のほうを訳したときは、余白を埋めるのに時間がかかった。今度は短い。要約みたいなものだろうし、たぶんすぐ終わる。

訳し比べれば、自分の翻訳のどこがズレていたか分かるはずだった。

そう思って、書き取りを始めた。


2. Method — 同じ名前の下で

2.1 最初の違いは、細部じゃなかった

17分のほうは、有効なオーダーブロックの条件を並べていく。その一番目に、こうある。

there must be an imbalance also known as a gap or inefficiency

隙間がなければならない。ローソクとローソクのあいだに、価格が通らなかった空白があること。

……40分のほうには、この条件がなかった。

もう一度、前に書いた凍結表を開いて確かめる。陰線。窓の中の最安値。高値の上抜けで有効化。隙間の話は、どこにも出てこない。

説明の順番が違うとか、言い方が違うとか、そういうことだと思っていた。条件そのものが違う。

隙間を機械の条件にする。三本並べて、後ろの足の安値が、前の足の高値より高いこと。等号は入れない。

2.2 探しても、ない

入る場所の話になる。ゾーンまで戻ってきて、そこで跳ね返されたら入る、と言っている。

具体的な価格は、言わない。

ゾーンの50%。半分まで引きつけて入る、というのを、どこかで読んだ。オーダーブロックの入り方として、当たり前みたいに書いてあった。

探す。ない。

戻して聞き直す。この人は、半分とも、中心とも、言っていない。

どこかで見た数字だから、どこかにはあると思っていた。ここには、ない。どこから来たのかも分からない。

分からないものは置けないので、捨てる。ゾーンの境界に指値を置く。戻ってきて最初に触るところ。

2.3 順番は、自分で組んだ

もうひとつ、決めることがあった。

安値を割ってから戻す動き——だましのブレイクの話は、動画の前半のセクションにある。構造転換のほうは、後半のオーダーブロックの条件として出てくる。

この二つが「まず安値を狩って、それから構造が変わって、そこにできたオーダーブロック」という一続きの流れとして説明されているわけではない。並んでいるだけ。

つなげたのは、わたし。

安値を割って終値で戻す。そこから12本以内に構造が変わる。そのあいだに隙間がある。その区間の最後の陰線を、オーダーブロックにする。

この順番は、17分の中にはない。

2.4 凍結

隙間を判定しているのは、ここ(ict_ob.py):

for center in range(first_center, mss_index):
    before = frame.iloc[center - 1]
    after = frame.iloc[center + 1]
    if side == "long":
        if float(after["bid_low"]) > float(before["bid_high"]):
            return True

40分のほうの検出器には、これに当たる関数がない。

項目固定条件
データ・期間・区間・バイアス・サイズ・コスト前回の検証と完全に同一
検出安値を割って終値で戻す → 12本以内に構造転換 → その区間に隙間が1本以上 → 区間内の最後のbid陰線をオーダーブロックとする(買い側。売りは鏡像)
ゾーンオーダーブロック足の全レンジ(ヒゲ込み)
エントリーゾーン境界への指値(買いはbid_high)。50%中点は不採用。構造転換を確認した足の次から有効
損切り / 利確買い: オーダーブロック足のbid_lowそのもの / 確定済み直近スイング高値(遡り400本)。売りは鏡像
約定・ゾーン運用前回と同一(bid判定系列、方向別quote、同一足はSL優先、待機ゾーン1・置換式、強制手仕舞いなし)
調整結果を見て条件を変えない

同じにできるところは、全部同じにした。違うのは検出の条件と、狙う場所の定義だけ。

条件ごとの絞り込み

図1. 条件ごとの絞り込み。安値を割って戻した3,455回のうち、12本以内に構造が変わったのが768回。そこから隙間なしで259回、反対色の足が見つからずに184回が落ちる。残った325のうち、構造転換を確認した足の時点でバイアスが揃わない4回を見送り、321ゾーンが残る。

走らせる。


3. Results — 重ならない

3.1 数字

入力のhashを固定値と照合する。一致。二回走らせて、結果のhashも一致。

指標結果
検出ゾーン321
トレード数194
最終残高768,671.812円
リターン-23.132819%
勝率38.659794%
最大ドローダウン(決済後残高)-27.832202%
プロフィットファクター0.793311
平均実現R-0.124158
プラスの固定区間2 / 5
主判定不成立

-23.13%。プラスは五つのうち二つ。主判定は、不成立。

同じ結論だ。

3.2 途中の形が違う

前に走らせたほうと、並べてみる。

40分の翻訳17分の翻訳
検出ゾーン1,121321
決済534194
勝率11.235955%38.659794%
決済理由SL 470 / 窓開きSL 4 / TP 60SL 116 / 窓開きSL 3 / TP 75

決済の回数は3分の1近くまで減っている。なのに、利確に届いた回数だけ増えている。60回が、75回。

結論は同じところに着いたのに、途中がぜんぜん違う。

どちらが正しい翻訳なのか、と一瞬考えて、その問いの立て方が変だと気づく。

3.3 固定した5区間

区間取引数実現損益
119-115,976円
249+15,212円
354+71,596円
437-97,484円
535-104,676円
固定184日ごとの区間損益

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

プラスは区間2と3。事前に決めた「3区間以上」には届かない。観測は、そこまで。

3.4 同じ場所を指しているか

先に、重なりの数え方を決めておく。

価格の範囲が交わっていて、有効だった時間も交わっているとき、その二つは重なったことにする。買いと売りが一致しているかは問わない。約定まで行かなかったゾーンも、全部数に入れる。

40分の翻訳が見つけた1,121のうち、17分の翻訳と重なったのは、46。

17分の翻訳が見つけた321のうち、40分の翻訳と重なったのは、45。

4%と、14%。

三つの定義のゾーン数と重なり

図3. 三つの定義を二つずつ突き合わせ、それぞれが自分の検出ゾーンのうちどれだけ相手と重なったかを示す。母数が違うので、同じ組でも向きによって割合が変わる。流通実装の27件は期間中の全検出数ではなく、関数を通したあとに残った行(Appendix E)。

3.5 ゼロ

もうひとつ、突き合わせる先がある。

オーダーブロックを検出するライブラリが公開されていて、そこそこ使われている。デフォルトのまま、同じ15分足を通す。

40分の翻訳とは、15。

17分の翻訳とは、0。

一件も重ならない。

数える手が、止まった。


4. Discussion — 答え合わせ

4.1 母集団が違う

0という数字を見たあとで、そのライブラリが何を返しているのかを確かめた。

返ってきた27件は、期間中に見つけた全部じゃない。一度検出したあと、反対側へ抜けたものを消して、最後に残った行だった。

スイングの取り方も違う。デフォルトは前後50本を見る。真ん中の足がスイングかどうかを、後ろの50本まで見てから決める。全部終わったあとで振り返って貼るラベルで、その時刻に手元で使える判定ではない。

だから0%は、このライブラリが間違っていることを示さない。示せるようにできていない。

4.2 答え合わせ

答え合わせをしにいったつもりだった。

相手は、全部見終わってから貼るラベルだった。

4.3 名前

三つ並んでいる。

40分の人が話したものを、わたしが訳したもの。17分の人が話したものを、わたしが訳したもの。誰かが実装して公開しているもの。

全部、オーダーブロックという同じ名前で呼ばれている。そして、ほとんど同じ場所を指していない。

「負けたのは、わたしの翻訳」と書いたとき、少しほっとしていた。

あれは、引き受けられたからじゃなかった。正しい版がどこかにあると、まだ思っていたからだ。自分のがズレているなら、ズレていない本物がどこかにあるはずで、それを見つければいい。そういう場所を、外に探していた。

名前は、そこにあるものを保証しない。

1,121と321を、重ねてみる。

重なっていたのは、46だけだった。次は、そこを見に行く。


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
採用した日足 / 不採用656 / 1
取引窓ET [2024-01-06, 2026-07-14) の920暦日、5区間×184日

日足バイアス、スイングの確定遅れ(前後2本・2本あとに確定)、ポジションサイズ(初期残高100万円、リスク1%、証拠金率4%)、コスト(手数料0、通常スリッページ0、スワップ0)は前回と同一。ただし手数料等を0にしても無コストではない——買いはask、売りはbidで約定させているため、bid/ask差は入力価格の側にコストとして含まれている。完全再計算には同一の非公開SQLiteが必要で、行単位データは同梱しない。

B. 翻訳した条件(買い側。売りは鏡像)

  • 安値の狩り: bid_low が確定済み直近スイング安値を下回り、かつ bid_close がその水準より上で終わること
  • 構造転換: 上記の足から12本以内に、bid_close が直前に確定していたスイング高値を上抜けること。超えたら破棄
  • 隙間: 上記の区間内に、bid_low[t+1] > bid_high[t-1] を満たす三本並びが1本以上あること(厳密不等号)。なければ破棄
  • オーダーブロック: 安値の狩り以降・構造転換の確認足より前にある、最後のbid陰線。なければ破棄
  • ゾーン: そのローソクの [bid_low, bid_high](ヒゲ込みの全レンジ)
  • エントリー: ゾーン境界(買いはbid_high)への指値。構造転換を確認した足の次の足から有効
  • 損切り: オーダーブロック足の bid_low そのもの(売りは ask_high そのもの)
  • 利確: 確定済みスイング高値のうち、エントリー価格より上にあるものの直近(売りはエントリー価格より下にある直近スイング安値)。探索範囲はエントリー足を含まない直前400本の完全15分足。条件を満たす水準がなければ見送り
  • 同一足の扱い: 指値が足の途中で成立した場合、その足での利確は無効とし、次の足から有効にする。損切りは悲観側として、成立した足から判定する。始値の時点ですでに指値条件を満たしていた場合だけ、その足の利確も有効とする
  • 「安値の狩り → 構造転換 → 隙間 → 戻り」という一続きの構成は、出典の中で結合して語られているものではなく、本検証が組んだ順序である

C. 検出とゾーンの内訳

  • 検出: 安値の狩り 3,455(うち新しい狩りへ置き換え 2,056) → 12本以内に構造転換せず 631 / 構造転換 768 → 隙間なし 259 → 反対色の足が見つからず 184(買い側で陰線なし 118 / 売り側で陽線なし 66) → 残り 325 のうち、構造転換を確認した足の時点でバイアスが条件と揃わず見送り 4 → ゾーン 321
  • ゾーンの方向: 買い 210 / 売り 111
  • ゾーンのその後: 有効化 321 → 消費 270(利確水準が見つからず見送り 4 / 証拠金上限で見送り 72 / 約定 194)、無効化 5、置き換え 46
  • 決済の方向: 買い 109 / 売り 85(ゾーンの方向とは母数が異なる。約定まで進んだものだけの内訳)
  • 決済理由: SL 116 / 窓開きSL 3 / TP 75

D. 重なりの定義と全組合せ

  • 価格は閉区間、有効時間は半開区間。両方が交差したときだけ重なったとする
  • 買い・売りの一致は要求しない(事前に凍結していないため、方向一致率を後から主指標にしない)
  • 約定まで行かなかったゾーン、利確水準なしで見送ったゾーン、即座に置き換えられたゾーンも、すべて母集団に含める
  • 消費・無効化の足内時刻はOHLCから復元できないため、双方に同じ規則を当て、その足の終わりまで有効として扱う
組合せ左の重複 / 総数割合右の重複 / 総数割合
40分の翻訳 → 17分の翻訳46 / 1,1214.103479%45 / 32114.018692%
40分の翻訳 → 流通実装15 / 1,1211.338091%10 / 2737.037037%
17分の翻訳 → 流通実装0 / 3210%0 / 270%

左右で件数が違うのは、一つのゾーンが相手側の二つと重なる組があるため。重複ペアはそれぞれ46 / 15 / 0。

E. 流通実装の詳細と制約

  • smartmoneyconcepts==0.0.27、パラメータはデフォルトのまま(swing_length=50 等を明示指定していない)
  • 同じ完全15分足のbid OHLCVを入力。約定と損益は計算していない(ゾーンの比較のみ)
  • オーダーブロック: raw 27行、記事窓内27(買い 23 / 売り 4)。うち反対側へ抜けたもの 2、データ終端まで残ったもの 25
  • 流動性: raw 156行、記事窓内155(買い 77 / 売り 78)。関数出力の監査だけに使い、重なり率には含めない
  • この27件は期間中の全検出数ではなく、関数を通したあとに残った行である
  • デフォルトのスイング判定は前後50本の中央窓を使う。全標本を見たあとの回顧的なラベルであり、その時点で利用可能な非先読みの検出時刻ではない
  • したがって、この比較は「流通している実装の最終出力との突き合わせ」であって、正解との照合ではない

F. 指標の定義と限界

  • リターン: 最終残高 ÷ 1,000,000 − 1。最大DDは初期残高と各取引の決済後残高から算出し、保有中の時価評価を含まない
  • PF: 総利益 ÷ 総損失の絶対値。実現R: 方向別の単位あたり損益 ÷ 初期リスク
  • 主判定は決済194件(30件以上)を前提に判定した
  • 15分足のOHLCから足の中の価格経路は復元できない。同一足でSL優先とするのは悲観側の仮定
  • ここから言えないこと: 二つの翻訳の優劣、流通実装を正解基準とすること、「オーダーブロック一般の一致率」、原手法や出典者の評価、実運用可能性、EURUSDや他期間への一般化

G. 再現コードと確認

  • 再現コード: GitHub
  • 参照結果の再計算: python chapters/season2/ch04_ict_order_blocks/compare.py --db "$SOURCE_DB" --repeat 2(検出・約定・比較まで二度実行し、結果hashの一致を要求)
  • 独立検証: 同chapterのverify.py。集計のみのrow-freeモードと、トレード明細から主要値を再計算する完全モード
  • 条件・version・hashはresults/reference/ch04_ict_order_blocks/manifest.jsonに固定。テストは全135件PASS

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