English Русский Deutsch
preview
MQL5での取引戦略の自動化(第6回):スマートマネートレーディングのためのオーダーブロック検出の習得

MQL5での取引戦略の自動化(第6回):スマートマネートレーディングのためのオーダーブロック検出の習得

MetaTrader 5トレーディング |
129 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第5回)では、移動平均のクロスオーバーとRSIフィルターを組み合わせた「Adaptive Crossover RSI Trading Suite Strategy」を開発し、高確率の取引機会を見つける方法を紹介しました。今回の第6回では、MetaQuotes Language 5 (MQL5)を使った純粋なプライスアクション分析に焦点を当て、自動オーダーブロック検出システムを構築します。これはスマートマネートレーディングで活用される強力なツールです。この戦略は、大口プレイヤーがポジションを蓄積・分配する重要なゾーンである「オーダーブロック」を特定し、トレーダーが潜在的な反転やトレンド継続を予測するのに役立ちます。

従来のインジケーターとは異なり、この手法は価格構造のみに依存し、過去の価格動向に基づいて強気・弱気のオーダーブロックを動的に検出します。システムはこれらのゾーンをチャート上に直接表示し、市場の状況と取引のセットアップを明確に示します。この記事では、オーダーブロックの定義からMQL5への実装、バックテストによる有効性の検証、パフォーマンス分析までのステップを順を追って説明します。以下の構成で進めていきます。

  1. 戦略の設計図
  2. MQL5での実装
  3. バックテスト
  4. 結論

この記事を読み終える頃には、オーダーブロック検出の自動化に関する確かな基盤が身につき、スマートマネーの概念を取引アルゴリズムに組み込むことができるようになるでしょう。それでは、始めましょう。


戦略の設計図

まずは、価格が明確なトレンド方向を持たずに一定の範囲内で推移する「レンジ(保合い)」相場を特定することから始めます。これをおこなうために、価格が大きくブレイクアウトしないエリアを市場からスキャンします。レンジからのブレイクアウトを検出したら、その範囲内でオーダーブロックが形成されるかどうかを評価します。検証プロセスでは、ブレイクアウト直前の3本のローソク足をチェックし、それらが勢いのある動きを示しているかを確認します。勢いのある動きがあれば、ブレイクアウト方向に基づいてオーダーブロックを強気(ブル)か弱気(ベア)に分類します。上方向へのブレイクアウトであれば強気のオーダーブロック、下方向であれば弱気のオーダーブロックと判断します。検証が完了したら、今後の参考のためにそのオーダーブロックをチャート上にプロットします。以下はその例です。

注文ブロックサンプル

直前の3本のローソク足に勢いのある動きが見られない場合は、オーダーブロックとしては認定せず、レンジ相場だけを描画します。これにより、弱いまたは重要度の低いゾーンを誤ってマーキングすることを防ぎます。有効なオーダーブロックをマーキングした後は、価格の動きを継続的に監視します。価格が以前に検証済みのオーダーブロックまで戻ってきた場合は、最初のブレイクアウト方向に沿った取引を実行し、トレンドの継続を期待します。ただし、もしオーダーブロックが直近の重要な価格ポイントを超えてしまった場合は、そのオーダーブロックを有効なリストから削除します。これにより、常に関連性が高く新鮮なゾーンのみで取引をおこなうことができます。このように構造化されたアプローチにより、弱いブレイクアウトを除外し、スマートマネーの動きに沿った高確率の取引セットアップに集中できるようになります。


MQL5での実装

オーダーブロックの識別をMQL5で実装するためには、処理全体で必要となるいくつかのグローバル変数を定義する必要があります。

#include <Trade/Trade.mqh>
CTrade obj_Trade;

// Struct to hold both the price and the index of the high or low
struct PriceIndex {
    double price;
    int index;
};

// Global variables to track the range and breakout state
PriceIndex highestHigh = {0, 0}; // Stores the highest high of the range
PriceIndex lowestLow = {0, 0};  // Stores the lowest low of the range
bool breakoutDetected = false;  // Tracks if a breakout has occurred

double impulseLow = 0.0;
double impulseHigh = 0.0;
int breakoutBarIndex = -1; // To track the bar at which breakout occurred
datetime breakoutTime = 0; // To store the breakout time

string totalOBs_names[];
datetime totalOBs_dates[];
bool totalOBs_is_signals[];

#define OB_Prefix "OB REC "
#define CLR_UP clrLime
#define CLR_DOWN clrRed

bool is_OB_UP = false;
bool is_OB_DOWN = false;

まず、Trade.mqhライブラリをインクルードし、CTradeオブジェクト(obj_Trade)を作成して、取引の実行を処理します。価格レベルとそれに対応するインデックスの両方を格納するためのPriceIndex構造体を定義します。これにより、レンジ相場内の最高値と最低値を追跡できるようになります。グローバル変数「highestHigh」と「lowestLow」はこれらのキーレベルを格納し、「breakoutDetected」フラグはブレイクアウトが発生したかどうかを示します。

インパルスを検証するために、「impulseLow」と「impulseHigh」という変数を導入します。これらはブレイクアウトの強さを判断するのに役立ちます。また、「breakoutBarIndex」はブレイクアウトが発生した正確なバー(ローソク足)を追跡し、「breakoutTime」はそのタイムスタンプを保持します。オーダーブロックの管理のために、「totalOBs_names」、「totalOBs_dates」、「totalOBs_is_signals」の3つのグローバル配列を維持します。これらの配列には、注文ブロック名、それぞれのタイムスタンプ、および有効な取引シグナルであるかどうかが格納されます。

オーダーブロックの名前には接頭辞として「OB_Prefix」を定義し、買い(ブル)のオーダーブロックにはライム色を示すCLR_UP、売り(ベア)のオーダーブロックには赤色を示すCLR_DOWNの色コードを割り当てます。さらに、最後に検出されたオーダーブロックが買いか売りかを判別するために、ブール変数のis_OB_UPとis_OB_DOWNを使って管理します。プログラムの初期化時には既存のオーダーブロックを追跡する必要はないため、初期状態は空のままにしておきます。したがって、オーダーブロックの管理・検出ロジックは、OnTickイベントハンドラの中で直接制御・実行していきます。

//+------------------------------------------------------------------+
//| Expert ontick function                                           |
//+------------------------------------------------------------------+
void OnTick() {
    static bool isNewBar = false;
    int currBars = iBars(_Symbol, _Period);
    static int prevBars = currBars;

    // Detect a new bar
    if (prevBars == currBars) {
        isNewBar = false;
    } else if (prevBars != currBars) {
        isNewBar = true;
        prevBars = currBars;
    }

    if (!isNewBar)
        return; // Process only on a new bar

    int rangeCandles = 7;         // Initial number of candles to check
    double maxDeviation = 50;     // Max deviation between highs and lows in points
    int startingIndex = 1;        // Starting index for the scan
    int waitBars = 3;

   //---

}

OnTickイベントハンドラ内では、まず「currBars」と「prevBars」を使って新しいバーが形成されたかどうかを検出します。新しいバーが現れた場合にのみ、フラグ変数isNewBarをtrueに設定し、新しいバーが確認できなければ処理を早期に終了します。次に、レンジを検出するために最低でも解析するローソク足の本数として「rangeCandles = 7」を設定します。これは直近7本のローソク足を対象にレンジの有無を判断するという意味です。また、maxDeviationは50ポイントに設定し、レンジ内の最高値と最安値の差がこの値以内であればレンジと見なします。スキャンの開始位置は「startingIndex = 1」とし、これは直近で確定した最新のローソク足からスキャンを始めることを示しています。さらに、オーダーブロックの検証をおこなうまでに待つべきバー数を「waitBars = 3」と設定し、これによって誤検知を減らす工夫をしています。これらの初期設定を終えた後、次のステップとしてレンジ相場の範囲を検出し、その価格帯を取得して、オーダーブロックの有効性を判定する準備をします。

// Check for consolidation or extend the range
if (!breakoutDetected) {
  if (highestHigh.price == 0 && lowestLow.price == 0) {
      // If range is not yet established, look for consolidation
      if (IsConsolidationEqualHighsAndLows(rangeCandles, maxDeviation, startingIndex)) {
          GetHighestHigh(rangeCandles, startingIndex, highestHigh);
          GetLowestLow(rangeCandles, startingIndex, lowestLow);

          Print("Consolidation range established: Highest High = ", highestHigh.price, 
                " at index ", highestHigh.index, 
                " and Lowest Low = ", lowestLow.price, 
                " at index ", lowestLow.index);
          
      }
  } else {
      // Extend the range if the current bar's prices remain within the range
      ExtendRangeIfWithinLimits();
  }
}

新しいバーが形成されるたびに、レンジが発生しているか、あるいは既存のレンジをブレイクアウトが起きていなければ拡張できるかを確認します。highestHigh.priceとlowestLow.priceの両方がゼロの場合は、まだレンジ相場が確立されていないことを意味します。この場合、IsConsolidationEqualHighsAndLows関数を呼び出して、直近のrangeCandles本のローソク足が許容されるmaxDeviationの範囲内でレンジを形成しているかどうかをチェックします。レンジが確認できたら、GetHighestHighとGetLowestLow関数を使って、そのレンジ内の正確な最高値と最安値を求め、それぞれの価格と該当するバーのインデックスを記録します。

すでにレンジが確立されている場合は、現在のバーが設定されたレンジの範囲内に収まっているかどうかをExtendRangeIfWithinLimits関数で確認します。この関数は、ブレイクアウトが起きていない限り、レンジを動的に調整し拡張する役割を担います。以下はカスタム関数のコードスニペットの実装です。

// Function to detect consolidation where both highs and lows are nearly equal
bool IsConsolidationEqualHighsAndLows(int rangeCandles, double maxDeviation, int startingIndex) {
    // Loop through the last `rangeCandles` to check if highs and lows are nearly equal
    for (int i = startingIndex; i < startingIndex + rangeCandles - 1; i++) {
        // Compare the high of the current candle with the next one
        if (MathAbs(high(i) - high(i + 1)) > maxDeviation * Point()) {
            return false; // If the high difference is greater than allowed, it's not a consolidation
        }
        
        // Compare the low of the current candle with the next one
        if (MathAbs(low(i) - low(i + 1)) > maxDeviation * Point()) {
            return false; // If the low difference is greater than allowed, it's not a consolidation
        }
    }

    // If both highs and lows are nearly equal, it's a consolidation range
    return true;
}

IsConsolidationEqualHighsAndLowsというブール型関数を定義します。この関数は、直近のrangeCandles本のローソク足の高値と安値が、指定されたmaxDeviation内でほぼ等しいかどうかを検証することで、レンジを検出する役割を担います。処理は、startingIndexから始めて各バーを順にループしながら、隣り合うローソク足同士の高値と安値を比較する形でおこないます。

forループ内ではMathAbs関数を使って、現在のバーの高値(high(i))と次のバーの高値の絶対差を計算します。この差が、ポイント単位に変換したmaxDeviation(Point)を超えた場合、すぐにfalseを返し、高値が十分に近くないためレンジとは認められないと判断します。同様に、安値同士(low(i)とlow(i + 1))もMathAbsを用いて差をチェックし、許容範囲内かどうかを確認します。いずれかの比較で許容範囲を超えた場合は、早期にfalseを返して関数を終了します。すべての高値と安値の差が許容範囲内であれば、trueを返し、有効なレンジ相場であると判定します。次に定義する関数は、レンジ内の最高値と最安値を取得する処理を担当します。

// Function to get the highest high and its index in the last `rangeCandles` candles, starting from `startingIndex`
void GetHighestHigh(int rangeCandles, int startingIndex, PriceIndex &highestHighRef) {
    highestHighRef.price = high(startingIndex); // Start by assuming the first candle's high is the highest
    highestHighRef.index = startingIndex;       // The index of the highest high (starting with the `startingIndex`)

    // Loop through the candles and find the highest high and its index
    for (int i = startingIndex + 1; i < startingIndex + rangeCandles; i++) {
        if (high(i) > highestHighRef.price) {
            highestHighRef.price = high(i); // Update highest high
            highestHighRef.index = i;       // Update index of highest high
        }
    }
}

// Function to get the lowest low and its index in the last `rangeCandles` candles, starting from `startingIndex`
void GetLowestLow(int rangeCandles, int startingIndex, PriceIndex &lowestLowRef) {
    lowestLowRef.price = low(startingIndex); // Start by assuming the first candle's low is the lowest
    lowestLowRef.index = startingIndex;      // The index of the lowest low (starting with the `startingIndex`)

    // Loop through the candles and find the lowest low and its index
    for (int i = startingIndex + 1; i < startingIndex + rangeCandles; i++) {
        if (low(i) < lowestLowRef.price) {
            lowestLowRef.price = low(i); // Update lowest low
            lowestLowRef.index = i;      // Update index of lowest low
        }
    }
}

GetHighestHigh関数は、直近のrangeCandles本のローソク足のうち、startingIndexから開始して最高値(high)が最も高いバーの価格とそのインデックスを特定する役割を持ちます。まず、highestHighRef.priceにレンジ内の最初のローソク足の高値(high(startingIndex))を設定し、highestHighRef.indexにもstartingIndexを設定します。次に、指定された範囲内の残りのローソク足を順に調べ、現在の最高値highestHighRef.priceより高い価格があれば、highestHighRef.priceとhighestHighRef.indexの両方を更新します。この関数は、レンジ相場の上限を決定するのに役立ちます。

同様に、GetLowestLow関数は同じ範囲内で最も安い安値(low)とそのインデックスを見つける役割を果たします。lowestLowRef.priceにレンジ内の最初のローソク足の安値(low(startingIndex))、lowestLowRef.indexにstartingIndexを設定します。ループを通じて、もし現在のlowestLowRef.priceよりも低い価格があれば、両方の値を更新します。この関数は、レンジ相場の下限を決定します。最後に、レンジを拡張する関数があります。

// Function to extend the range if the latest bar remains within the range limits
void ExtendRangeIfWithinLimits() {
    double currentHigh = high(1); // Get the high of the latest closed bar
    double currentLow = low(1);   // Get the low of the latest closed bar

    if (currentHigh <= highestHigh.price && currentLow >= lowestLow.price) {
        // Extend the range if the current bar is within the established range
        Print("Range extended: Including candle with High = ", currentHigh, " and Low = ", currentLow);
    } else {
        Print("No extension possible. The current bar is outside the range.");
    }
}

ここでは、ExtendRangeIfWithinLimits関数が、新しいバーが既存のレンジ相場内に収まっている限り、以前に特定されたレンジ相場が有効であり続けることを保証します。まず、「high(1)」関数と「low(1)」関数を使用して、直近で確定したローソク足の高値と安値を取得します。次に、currentHighがhighestHigh.price以下であり、かつcurrentLowがlowestLow.price以上であるかどうかを確認します。両方の条件が満たされていれば、レンジは拡張され、新しいローソク足が既存の範囲内に含まれていることを示す確認メッセージが出力されます。

それ以外の場合、新しいローソク足が設定されたレンジ外に移動すると、拡張はおこなわれず、レンジを拡張できないことを示すメッセージが出力されます。この関数は、有効な統合ゾーンを維持する上で重要な役割を果たし、市場が事前定義された範囲内に留まっている場合に不要なブレイクアウトの検出を防止します。

バーの価格データを取得する役割を担う定義済み関数も使用しました。以下にコードスニペットを示します。

//--- One-line functions to access price data
double high(int index) { return iHigh(_Symbol, _Period, index); }
double low(int index) { return iLow(_Symbol, _Period, index); }
double open(int index) { return iOpen(_Symbol, _Period, index); }
double close(int index) { return iClose(_Symbol, _Period, index); }
datetime time(int index) { return iTime(_Symbol, _Period, index); }

これらの1行関数「high」「low」「open」「close」「time」は、過去のバーの価格および時間データを取得するためのシンプルなラッパー関数です。各関数は、それぞれ対応するMQL5の組み込み関数((iHighiLowiOpeniCloseiTime)を呼び出して、指定された「index」に対する値を取得します。「high」関数は特定のバーの高値を返し、「low」関数は安値を返します。同様に、「open」は始値を取得し、「close」は終値を取得します。「time」関数はそのバーのタイムスタンプを返します。これらの関数を使うことで、コードの可読性を高め、プログラム全体で過去データへのアクセスをよりクリーンかつ構造的におこなえるようにしています。

これらの関数を用意することで、次のコードスニペットのように、レンジ相場が形成された際のブレイクアウトをチェックできるようになります。

// Check for breakout if a consolidation range is established
if (highestHigh.price > 0 && lowestLow.price > 0) {
  breakoutDetected = CheckRangeBreak(highestHigh, lowestLow);
}

ここでは、レンジ相場が形成されている場合に、カスタム関数「CheckRangeBreak」を使ってレンジブレイクアウトをチェックし、その結果を「breakoutDetected」変数に保存します。関数の実装は以下の通りです。

// Function to check for range breaks
bool CheckRangeBreak(PriceIndex &highestHighRef, PriceIndex &lowestLowRef) {
    double closingPrice = close(1); // Get the closing price of the current candle

    if (closingPrice > highestHighRef.price) {
        Print("Range break upwards detected. Closing price ", closingPrice, " is above the highest high: ", highestHighRef.price);
        return true; // Breakout detected
    } else if (closingPrice < lowestLowRef.price) {
        Print("Range break downwards detected. Closing price ", closingPrice, " is below the lowest low: ", lowestLowRef.price);
        return true; // Breakout detected
    }
    return false; // No breakout
}

ブール型型のCheckRangeBreak関数では、現在のローソク足のclosingPrice(終値)を、highestHighRef.price(最高値の参照)およびlowestLowRef.price(最安値の参照)と比較します。closingPriceがhighestHighRef.priceよりも高ければ、上方向へのブレイクアウトを検出します。逆に、closingPriceがlowestLowRef.priceよりも低ければ、下方向へのブレイクアウトと判断します。どちらかの条件が満たされた場合、trueを返し、ブレイクアウトの方向を出力します。どちらの条件も満たさない場合は、falseを返します。

この変数を使うことで、ブレイクアウトを検出し、次の可能性のあるレンジ相場に備えてレンジ相場の状態をリセットする処理を以下のように実装できます。

// Reset state after breakout
if (breakoutDetected) {
  Print("Breakout detected. Resetting for the next range.");
  
  breakoutBarIndex = 1; // Use the current bar's index (index 1 refers to the most recent completed bar)
  breakoutTime = TimeCurrent();
  impulseHigh = highestHigh.price;
  impulseLow = lowestLow.price;
  
  breakoutDetected = false;
  highestHigh.price = 0; 
  highestHigh.index = 0;

  lowestLow.price = 0; 
  lowestLow.index = 0;
}

ブレイクアウトが検出された後は、次のレンジに備えて状態をリセットします。まず、breakoutBarIndexを1に設定し、直近で確定したバーを参照します。また、TimeCurrent関数を使ってbreakoutTimeを現在の時刻に更新します。impulseHighとimpulseLowには、直前のレンジでのhighestHigh.price(最高値)およびlowestLow.price(最安値)をそれぞれ設定します。その後、breakoutDetectedをfalseに設定し、highestHighとlowestLowの価格およびインデックスの両方を0にリセットします。これにより、次のレンジ検出の準備が整います。この状態で、今度はインパルスに基づく有効なオーダーブロックの検出に進むことができます。

if (breakoutBarIndex >= 0 && TimeCurrent() > breakoutTime + waitBars * PeriodSeconds()) {
  DetectImpulsiveMovement(impulseHigh,impulseLow,waitBars,1);
   
   bool is_OB_Valid = is_OB_DOWN || is_OB_UP;
            
   datetime time1 = iTime(_Symbol,_Period,rangeCandles+waitBars+1);
   double price1 = impulseHigh;
   
   int visibleBars = (int)ChartGetInteger(0,CHART_VISIBLE_BARS);
   datetime time2 = is_OB_Valid ? time1 + (visibleBars/1)*PeriodSeconds() : time(waitBars+1);
   
   double price2 = impulseLow;
   string obNAME = OB_Prefix+"("+TimeToString(time1)+")";
   color obClr = clrBlack;
   
   if (is_OB_Valid){obClr = is_OB_UP ? CLR_UP : CLR_DOWN;}
   else if (!is_OB_Valid){obClr = clrBlue;}
   
   string obText = "";
   
   if (is_OB_Valid){obText = is_OB_UP ? "Bullish Order Block"+ShortToString(0x2BED) : "Bearish Order Block"+ShortToString(0x2BEF);}
   else if (!is_OB_Valid){obText = "Range";}

   //---

}

ここではまず、「breakoutBarIndex」が0以上であること、かつ現在時刻が「breakoutTime」よりも後であり、その差が「waitBars × PeriodSeconds関数による1バーの秒数」で計算された待機期間を超えているかをチェックします。この条件が満たされた場合、関数「DetectImpulsiveMovement」を呼び出して、インパルス的な市場の動きを検出します。この関数にはimpulseHigh、impulseLow、waitBars、および固定値の1を引数として渡します。

次に、オーダーブロックを検証するために、is_OB_DOWNまたはis_OB_UPのいずれかがtrueであるかを確認し、その結果をis_OB_Validに格納します。バーの時刻は、銘柄と時間足における特定バーの時間を返すiTime関数を使って取得し、time1に保存します。このバーの価格はimpulseHighに保存され、後の計算で使用されます。続いて、ChartGetInteger関数を使って、チャート上に表示されているバーの数を取得します。この関数にはCHART_VISIBLE_BARSというパラメータを使います。次に、time2を計算します。これはオーダーブロックが有効かどうかに応じて変化します。is_OB_Validがtrueの場合、time2はtime1から表示バー数分の時間(バー数 × PeriodSeconds)を加えた時刻になります。無効な場合は、time(waitBars+1)によって次のバーの時間を使います。この分岐は三項演算子を使っておこないます。

price2にはimpulseLowが設定されます。その後、OB_PrefixにTimeToString関数で整形した時間を付加して、オーダーブロックの名前を生成します。オーダーブロックの色は、デフォルトではobClrにより黒色に設定されます。ただし、オーダーブロックが有効な場合は、上昇方向ならCLR_UP、下降方向ならCLR_DOWNに色が変更されます。無効な場合は青色に設定されます。

オーダーブロックのテキスト(obText)は、方向に応じて設定されます。オーダーブロックが有効な場合は、「Bullish Order Block」または「Bearish Order Block」というラベルが付き、それぞれに対応するUnicode記号(上昇で0x2BED、下降で0x2BEF)をShortToString関数で変換して表示します。有効でない場合は「Range」というラベルになります。これらのUnicode記号は以下の通りです。

Unicode記号

インパルス的な動きを検知する関数は以下の通りです。

// Function to detect impulsive movement after breakout
void DetectImpulsiveMovement(double breakoutHigh, double breakoutLow, int impulseBars, double impulseThreshold) {
    double range = breakoutHigh - breakoutLow;         // Calculate the breakout range
    double impulseThresholdPrice = range * impulseThreshold; // Threshold for impulsive move

    // Check for the price movement in the next `impulseBars` bars after breakout
    for (int i = 1; i <= impulseBars; i++) {
        double closePrice = close(i); // Get the close price of the bar

        // Check if the price moves significantly beyond the breakout high
        if (closePrice >= breakoutHigh + impulseThresholdPrice) {
            is_OB_UP = true;
            Print("Impulsive upward movement detected: Close Price = ", closePrice, 
                  ", Threshold = ", breakoutHigh + impulseThresholdPrice);
            return;
        }
        // Check if the price moves significantly below the breakout low
        else if (closePrice <= breakoutLow - impulseThresholdPrice) {
            is_OB_DOWN = true;
            Print("Impulsive downward movement detected: Close Price = ", closePrice, 
                  ", Threshold = ", breakoutLow - impulseThresholdPrice);
            return;
        }
    }

    // If no impulsive movement is detected
    is_OB_UP = false;
    is_OB_DOWN = false;
    Print("No impulsive movement detected after breakout.");
}

この関数内では、ブレイクアウト後に価格がインパルス的に動いたかどうかを検出するため、まずrangeをbreakoutHighからbreakoutLowを引くことで計算します。次に、impulseThresholdPriceを、rangeにimpulseThresholdを掛けて算出します。これは、どれだけ価格が動けばインパルスと見なすかを定義するものです。続いて、forループを使って、次のimpulseBars本分のバーにわたって価格の動きをチェックします。

各バーについては、close(i)関数を使ってclosePrice(終値)を取得します。この関数は、i番目のバーの終値を返します。終値がbreakoutHighよりもimpulseThresholdPrice以上高ければ、インパルス的な上昇と判断し、is_OB_UPをtrueに設定して、検出したことを出力します。同様に、終値がbreakoutLowよりもimpulseThresholdPrice以上低ければ、インパルス的な下落と判断し、is_OB_DOWNをtrueに設定し、結果を出力します。

すべてのバーをチェックしてもインパルス的な動きが検出されなかった場合は、is_OB_UPとis_OB_DOWNの両方をfalseに設定し、インパルス的な動きが検出されなかったことを出力します。この処理の後は、チャート上にレンジとオーダーブロックを描画する準備が整います。

if (!is_OB_Valid){
   if (ObjectFind(0,obNAME) < 0){
      CreateRec(obNAME,time1,price1,time2,price2,obClr,obText);
   }
}
else if (is_OB_Valid){
   if (ObjectFind(0,obNAME) < 0){
      CreateRec(obNAME,time1,price1,time2,price2,obClr,obText);
      
      Print("Old ArraySize = ",ArraySize(totalOBs_names));
      ArrayResize(totalOBs_names,ArraySize(totalOBs_names)+1);
      Print("New ArraySize = ",ArraySize(totalOBs_names));
      totalOBs_names[ArraySize(totalOBs_names)-1] = obNAME;
      ArrayPrint(totalOBs_names);
      
      Print("Old ArraySize = ",ArraySize(totalOBs_dates));
      ArrayResize(totalOBs_dates,ArraySize(totalOBs_dates)+1);
      Print("New ArraySize = ",ArraySize(totalOBs_dates));
      totalOBs_dates[ArraySize(totalOBs_dates)-1] = time2;
      ArrayPrint(totalOBs_dates);
      
      Print("Old ArraySize = ",ArraySize(totalOBs_is_signals));
      ArrayResize(totalOBs_is_signals,ArraySize(totalOBs_is_signals)+1);
      Print("New ArraySize = ",ArraySize(totalOBs_is_signals));
      totalOBs_is_signals[ArraySize(totalOBs_is_signals)-1] = false;
      ArrayPrint(totalOBs_is_signals);
      
   }
}

breakoutBarIndex = -1; // Use the current bar's index (index 1 refers to the most recent completed bar)
breakoutTime = 0;
impulseHigh = 0;
impulseLow = 0;
is_OB_UP = false;
is_OB_DOWN = false;

ここでは、まずオーダーブロックが有効(is_OB_Valid)かどうかを確認します。オーダーブロックが無効な場合は、「obNAME」という名前のオブジェクトがすでにチャート上に存在するかどうかをObjectFind関数で確認します。この関数が負の値を返した場合(=オブジェクトが存在しない場合)、CreateRec関数を呼び出して、指定されたパラメータ(時間、価格、色、テキストなど)を使ってオーダーブロックをチャート上に作成します。

オーダーブロックが有効な場合も、同様にオブジェクトが存在するかを確認し、存在しない場合にはそれを作成します。その後、オーダーブロックのデータ管理をおこないます。具体的には、ArrayResize関数を使ってtotalOBs_names(オーダーブロックの名前を格納)、totalOBs_dates(オーダーブロックのタイムスタンプを格納)、totalOBs_is_signals(各オーダーブロックが有効なシグナルかどうか(初期値はfalse))の3つの配列のサイズを調整します。配列のサイズを変更した後、ArraySize関数で古いサイズと新しいサイズを出力し、ArrayPrint関数を使って配列の中身を表示します。最後に、ブレイクアウトの状態をリセットします。具体的には、breakoutBarIndexを-1に設定し、breakoutTime、impulseHigh、impulseLowをすべて0にリセットし、オーダーブロック方向のフラグis_OB_UPおよびis_OB_DOWNをfalseに設定します。

なお、チャート上にテキスト付きの長方形を作成するために、カスタム関数CreateRecを使用しています。その実装は以下の通りです。

void CreateRec(string objName,datetime time1,double price1,
               datetime time2,double price2,color clr,string txt){
   if (ObjectFind(0,objName) < 0){
      ObjectCreate(0,objName,OBJ_RECTANGLE,0,time1,price1,time2,price2);
      
      Print("SUCCESS CREATING OBJECT >",objName,"< WITH"," T1: ",time1,", P1: ",price1,
            ", T2: ",time2,", P2: ",price2);
      
      ObjectSetInteger(0,objName,OBJPROP_TIME,0,time1);
      ObjectSetDouble(0,objName,OBJPROP_PRICE,0,price1);
      ObjectSetInteger(0,objName,OBJPROP_TIME,1,time2);
      ObjectSetDouble(0,objName,OBJPROP_PRICE,1,price2);
      ObjectSetInteger(0,objName,OBJPROP_FILL,true);
      ObjectSetInteger(0,objName,OBJPROP_COLOR,clr);
      ObjectSetInteger(0,objName,OBJPROP_BACK,false);

    // Calculate the center position of the rectangle
    datetime midTime = time1 + (time2 - time1) / 2;
    double midPrice = (price1 + price2) / 2;

    // Create a descriptive text label centered in the rectangle
    string description = txt;
    string textObjName = objName + description; // Unique name for the text object
    if (ObjectFind(0, textObjName) < 0) {
        ObjectCreate(0, textObjName, OBJ_TEXT, 0, midTime, midPrice);
        ObjectSetString(0, textObjName, OBJPROP_TEXT, description);
        ObjectSetInteger(0, textObjName, OBJPROP_COLOR, clrBlack);
        ObjectSetInteger(0, textObjName, OBJPROP_FONTSIZE, 15);
        ObjectSetInteger(0, textObjName, OBJPROP_ANCHOR, ANCHOR_CENTER);

        Print("SUCCESS CREATING LABEL >", textObjName, "< WITH TEXT: ", description);
    }

      ChartRedraw(0);
   }
}

定義したCreateRec関数では、まずObjectFind関数を使って、指定されたオブジェクト名(objName)のオブジェクトがすでに存在するかを確認します。存在しない場合は、ObjectCreate関数を使用して、指定された時間と価格の位置に長方形を作成します。オブジェクトの種類はOBJ_RECTANGLEで定義され、色や塗りつぶし、表示設定などのプロパティはObjectSetIntegerObjectSetDouble関数で設定します。その後、長方形の中央位置を計算し、その中央にラベルを作成します。ラベルもObjectCreate関数で生成し、種類はOBJ_TEXTです。ラベルにはテキスト、色、サイズ、アンカー(表示位置)などのプロパティを設定します。最後に、ChartRedraw関数を呼び出してチャートを更新します。すでに同名のオブジェクトやラベルが存在する場合には、何もせずスキップします。

これでオーダーブロックの描画が完了したので、次は、価格がオーダーブロックに再接触したかどうかを判断し、その範囲をブレイクする動きがあればポジションをエントリーするといったロジックへ進む準備が整います。

for (int j=ArraySize(totalOBs_names)-1; j>=0; j--){
   string obNAME = totalOBs_names[j];
   bool obExist = false;
   //Print("name = ",fvgNAME," >",ArraySize(totalFVGs)," >",j);
   //ArrayPrint(totalFVGs);
   //ArrayPrint(barTIMES);
   double obHigh = ObjectGetDouble(0,obNAME,OBJPROP_PRICE,0);
   double obLow = ObjectGetDouble(0,obNAME,OBJPROP_PRICE,1);
   datetime objTime1 = (datetime)ObjectGetInteger(0,obNAME,OBJPROP_TIME,0);
   datetime objTime2 = (datetime)ObjectGetInteger(0,obNAME,OBJPROP_TIME,1);
   color obColor = (color)ObjectGetInteger(0,obNAME,OBJPROP_COLOR);
   
   if (time(1) < objTime2){
      //Print("FOUND: ",obNAME," @ bar ",j,", H: ",obHigh,", L: ",obLow);
      obExist = true;
   }    
   
   double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
   double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);
   
   if (obColor == CLR_UP && Ask > obHigh && close(1) > obHigh && open(1) < obHigh && !totalOBs_is_signals[j]){
      Print("BUY SIGNAL For (",obNAME,") Now @ ",Ask);
      double sl = Bid - 1500*_Point;
      double tp = Bid + 1500*_Point;
      obj_Trade.Buy(0.01,_Symbol,Ask,sl,tp);
      totalOBs_is_signals[j] = true;
      ArrayPrint(totalOBs_names,_Digits," [< >] ");
      ArrayPrint(totalOBs_is_signals,_Digits," [< >] ");
   }
   else if (obColor == CLR_DOWN && Bid < obLow && close(1) < obLow && open(1) > obLow && !totalOBs_is_signals[j]){
      Print("SELL SIGNAL For (",obNAME,") Now @ ",Bid);
      double sl = Ask  + 1500*_Point;
      double tp = Ask - 1500*_Point;
      obj_Trade.Sell(0.01,_Symbol,Bid,sl,tp);
      totalOBs_is_signals[j] = true;
      ArrayPrint(totalOBs_names,_Digits," [< >] ");
      ArrayPrint(totalOBs_is_signals,_Digits," [< >] ");
   }
   
   if (obExist == false){
      bool removeName = ArrayRemove(totalOBs_names,0,1);
      bool removeTime = ArrayRemove(totalOBs_dates,0,1);
      bool remove_isSignal = ArrayRemove(totalOBs_is_signals,0,1);
      if (removeName && removeTime && remove_isSignal){
         Print("Success removing the OB DATA from arrays. New Data as below:");
         Print("Total Sizes => OBs: ",ArraySize(totalOBs_names),", TIMEs: ",ArraySize(totalOBs_dates),", SIGNALs: ",ArraySize(totalOBs_is_signals));
         ArrayPrint(totalOBs_names);
         ArrayPrint(totalOBs_dates);
         ArrayPrint(totalOBs_is_signals);
      }
   }   
}

ここでは、totalOBs_names配列をループして、各オーダーブロック(obNAME)を処理します。まず、ObjectGetDouble関数とObjectGetInteger関数を使って、オーダーブロックの高値・安値の価格、タイムスタンプ、および色を取得します。その後、現在の時刻がオーダーブロックの終了時間よりも前かどうかを確認します。この時間条件が満たされている場合は、オーダーブロックの色と価格の条件に基づいて、買いまたは売りシグナルのチェックに進みます。条件が満たされていれば、obj_Trade.Buyまたはobj_Trade.Sell関数を使って取引を実行し、そのオーダーブロックがすでにシグナルとして機能したことを示すため、totalOBs_is_signals配列を更新します。こうすることで、価格がリトレースして再び同じオーダーブロックに触れても二重で取引がおこなわれないようにします。

一方で、オーダーブロックが時間条件を満たさない(=すでに終了している)場合には、ArrayRemove関数を使って、totalOBs_names、totalOBs_dates、totalOBs_is_signalsの各配列からそのオブジェクトを削除します。削除が成功した場合、更新された配列のサイズと内容を出力します。これが私たちが現在達成したマイルストーンです。

注文ブロックが検証されました

画像から、オーダーブロックが検出され、取引が実行されていることがわかります。これにより、私たちの目的は達成されました。これについては次のセクションで扱います。


バックテストと最適化

徹底的なバックテストの結果、次の結果が得られました。

バックテストグラフ

グラフ

バックテストレポート

レポート

こちらは、2024年の1年間にわたるストラテジーのバックテスト全体を紹介する動画形式の資料です。


結論

本記事では、オーダーブロック検出を活用したスマートマネー取引戦略のための、洗練されたMQL5 EA開発プロセスを紹介しました。ダイナミックなレンジ分析、プライスアクション、リアルタイムのブレイクアウト検出などのツールを取り入れることで、重要なサポート・レジスタンスレベルを特定し、実行可能なトレードシグナルを生成し、正確に注文管理をおこなうプログラムを構築しました。

免責条項:本記事は教育目的で提供されています。取引には重大な経済的リスクが伴い、市場の動きは非常に予測困難です。本記事で紹介した戦略は構造的なアプローチを提供するものですが、将来の利益を保証するものではありません。実際の取引をおこなう前に、適切なテストとリスク管理が不可欠です。

これらの手法を応用することで、より効果的なトレーディングシステムを構築し、市場分析のアプローチを洗練させ、アルゴリズム取引を次のレベルへと引き上げることが可能になります。取引の成功をお祈りします。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/17135

添付されたファイル |
ORDER_BLOCKS_EA.mq5 (33.68 KB)
JSONをマスターする:MQL5で独自のJSONリーダーをゼロから作成する JSONをマスターする:MQL5で独自のJSONリーダーをゼロから作成する
オブジェクトと配列の処理、エラーチェック、シリアル化を備えたMQL5でカスタムJSONパーサーを作成する手順をステップバイステップで説明します。MetaTrader5でJSONを処理するためのこの柔軟なソリューションを使用して、取引ロジックと構造化データを橋渡しするための実用的な洞察を得ることができます。
プライスアクション分析ツールキットの開発(第12回):External Flow (III)トレンドマップ プライスアクション分析ツールキットの開発(第12回):External Flow (III)トレンドマップ
市場の流れは、ブル(買い手)とベア(売り手)の力関係によって決まります。市場が反応する特定の水準には、そうした力が作用しています。中でも、フィボナッチとVWAPの水準は、市場の動きに強い影響を与える傾向があります。この記事では、VWAPとフィボナッチ水準に基づいたシグナル生成の戦略を一緒に探っていきましょう。
知っておくべきMQL5ウィザードのテクニック(第54回):SACとテンソルのハイブリッドによる強化学習 知っておくべきMQL5ウィザードのテクニック(第54回):SACとテンソルのハイブリッドによる強化学習
Soft Actor Critic (SAC)は、以前の記事で紹介した強化学習アルゴリズムです。その際には、効率的にネットワークを学習させる手法としてPythonやONNXの活用についても触れました。今回は、このアルゴリズムを改めて取り上げ、Pythonでよく使われるテンソルや計算グラフを活用することを目的としています。
MQL5入門(第12回):初心者のためのカスタムインジケーター作成ガイド MQL5入門(第12回):初心者のためのカスタムインジケーター作成ガイド
MQL5でカスタムインジケーターを構築する方法を学びます。プロジェクトベースのアプローチを採用します。この初心者向けガイドでは、インジケーターバッファ、プロパティ、トレンドの視覚化について解説し、段階的に学習を進めることができます。