English Deutsch
preview
MQL5での取引戦略の自動化(第12回):Mitigation Order Blocks (MOB)戦略の実装

MQL5での取引戦略の自動化(第12回):Mitigation Order Blocks (MOB)戦略の実装

MetaTrader 5トレーディング | 1 7月 2025, 07:58
47 4
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第11回)では、MetaQuotes Language 5 (MQL5)を使用して、市場の変動を活かすマルチレベルグリッド取引システムを構築しました。今回の第12回では、Mitigation Order Blocks (MOB)戦略の実装に焦点を当てます。これはスマートマネーコンセプトの一つであり、大きな値動きの前に機関投資家の注文が調整(ミティゲーション)される主要な価格ゾーンを特定する手法です。以下のトピックについて説明します。

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

この記事を読み終える頃には、Mitigation Order Blocks戦略に基づいた完全自動の取引システムが完成し、すぐに運用を開始できる状態になっているはずです。それでは始めましょう。


戦略の設計図

Mitigation Order Blocks戦略を実装するにあたり、本システムでは注文ブロックの検出・検証・エントリー実行までを自動化します。この戦略は、トレンド継続の前に流動性が吸収される機関投資家の価格ゾーンを特定することに焦点を当てます。本システムでは、エントリー、損切り、取引管理の条件を正確に定義し、効率的かつ精度の高い執行を目指します。以下のステップで開発を進めていきます。

  • オーダーブロックの識別:システムは過去のプライスアクションをスキャンして、強気・弱気のオーダーブロックを検出します。ボラティリティ、流動性の急激な吸収、価格の不均衡に基づいて、弱いゾーンはフィルタリングされます。
  • ミティゲーションの検証:価格がオーダーブロックを再び訪れ、髭やモメンタムの反転などの拒否シグナルが現れることで、正当なミティゲーションイベントを確認する条件をプログラムに組み込みます。
  • 市場構造の確認:EAは上位足のトレンドや流動性のスイープを分析し、ミティゲーションが市場全体の流れと整合していることを確認します。
  • 取引実行ルール:ミティゲーションが確認されると、正確なエントリーポイントを定義し、オーダーブロックの構造に基づいて動的に損切りラインを計算し、リスクリワード比に基づいた利益確定目標を設定します。
  • リスク・資金管理:ポジションサイズ、ドローダウン保護、エグジット戦略を統合し、効果的なリスク管理をおこないます。

簡単に言えば、以下がこの戦略の全体像のイメージです。

Mitigation Order Block


MQL5での実装

MQL5でプログラムを作成するには、まずMetaEditorを開き、ナビゲータに移動して、インジケーターフォルダを見つけ、[新規]タブをクリックして、表示される手順に従ってファイルを作成します。ファイルが作成されたら、コーディング環境で、まずプログラム全体で使用するグローバル変数をいくつか宣言する必要があります。

//+------------------------------------------------------------------+
//|                        Copyright 2025, Forex Algo-Trader, Allan. |
//|                                 "https://t.me/Forex_Algo_Trader" |
//+------------------------------------------------------------------+
#property copyright "Forex Algo-Trader, Allan"
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property description "This EA trades based on Mitigation Order Blocks Strategy"
#property strict

//--- Include the trade library for managing positions
#include <Trade/Trade.mqh>
CTrade obj_Trade;

実装は、まず「#include <Trade/Trade.mqh>」を使用して取引ライブラリを読み込むことから始めます。このライブラリには、売買などの取引操作を管理するための組み込み関数が用意されています。次に、CTradeクラスを用いてobj_Tradeという取引オブジェクトを初期化します。これにより、エキスパートアドバイザー(EA)は売買注文をプログラムから自動的に実行できるようになります。この設定により、手動での操作を必要とせずに効率的な注文実行が可能となります。その後、ユーザーがユーザインタフェース(UI)から動作を変更・制御できるよう、外部入力パラメータを定義します。

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input double tradeLotSize = 0.01;           // Trade size for each position
input bool enableTrading = true;            // Toggle to allow or disable trading
input bool enableTrailingStop = true;       // Toggle to enable or disable trailing stop
input double trailingStopPoints = 30;       // Distance in points for trailing stop
input double minProfitToTrail = 50;         // Minimum profit in points before trailing starts (not used yet)
input int uniqueMagicNumber = 12345;        // Unique identifier for EA trades
input int consolidationBars = 7;            // Number of bars to check for consolidation
input double maxConsolidationSpread = 50;   // Maximum allowed spread in points for consolidation
input int barsToWaitAfterBreakout = 3;      // Bars to wait after breakout before checking impulse
input double impulseMultiplier = 1.0;       // Multiplier for detecting impulsive moves
input double stopLossDistance = 1500;       // Stop loss distance in points
input double takeProfitDistance = 1500;     // Take profit distance in points
input color bullishOrderBlockColor = clrGreen;    // Color for bullish order blocks
input color bearishOrderBlockColor = clrRed;     // Color for bearish order blocks
input color mitigatedOrderBlockColor = clrGray;  // Color for mitigated order blocks
input color labelTextColor = clrBlack;           // Color for text labels

ここでは、プログラムの動作を設定するための入力パラメータを定義します。tradeLotSizeはポジションサイズを指定するもので、enableTradingとenableTrailingStopはそれぞれ売買の実行とトレーリングストップの有効・無効を制御します。さらに、trailingStopPointsとminProfitToTrailを使用することで、トレーリングストップの挙動をより細かく調整できます。uniqueMagicNumberは、EAが発注したポジションを識別するための一意の識別子です。レンジの検出にはconsolidationBarsとmaxConsolidationSpreadを用い、ブレイクアウトの判定にはbarsToWaitAfterBreakoutとimpulseMultiplierを使って価格の勢いとタイミングを評価します。リスク管理のために、stopLossDistanceとtakeProfitDistanceによって損切りと利確の距離を指定します。また、チャート表示の視認性を高めるために、bullishOrderBlockColor、bearishOrderBlockColor、mitigatedOrderBlockColor、labelTextColorを使って、各オーダーブロックやラベルの色を設定します。

最後に、システム全体の制御に必要なグローバル変数を定義していきます。

//--- Struct to store price and index for highs and lows
struct PriceAndIndex {
   double price;  // Price value
   int    index;  // Bar index where this price occurs
};

//--- Global variables for tracking market state
PriceAndIndex rangeHighestHigh = {0, 0};    // Highest high in the consolidation range
PriceAndIndex rangeLowestLow = {0, 0};      // Lowest low in the consolidation range
bool isBreakoutDetected = false;            // Flag for when a breakout occurs
double lastImpulseLow = 0.0;                // Low price after breakout for impulse check
double lastImpulseHigh = 0.0;               // High price after breakout for impulse check
int breakoutBarNumber = -1;                 // Bar index where breakout happened
datetime breakoutTimestamp = 0;             // Time of the breakout
string orderBlockNames[];                   // Array of order block object names
datetime orderBlockEndTimes[];              // Array of order block end times
bool orderBlockMitigatedStatus[];           // Array tracking if order blocks are mitigated
bool isBullishImpulse = false;              // Flag for bullish impulsive move
bool isBearishImpulse = false;              // Flag for bearish impulsive move

#define OB_Prefix "OB REC "     // Prefix for order block object names

まず、PriceAndIndexという構造体を定義します。これは、特定の価格値priceと、その価格が発生したバーのインデックスindexを格納するためのもので、指定された範囲内で特定の価格ポイントを追跡するのに役立ちます。グローバル変数群は、市場構造およびブレイクアウト検出に関する重要な要素を管理します。rangeHighestHighとrangeLowestLowは、それぞれ保ち合いレンジ内の最高値と最安値を格納し、オーダーブロックの可能性があるゾーンの境界を定義するために使用されます。isBreakoutDetectedは、ブレイクアウトが発生したかどうかを示すフラグとして機能し、lastImpulseLowとlastImpulseHighはブレイクアウト後に形成される最初の安値と高値を格納し、インパルスの確認に用いられます。

breakoutBarNumberにはブレイクアウトが発生したバーのインデックスを、breakoutTimestampにはその正確な時刻を記録します。配列であるorderBlockNames、orderBlockEndTimes、orderBlockMitigatedStatusは、それぞれオーダーブロックの識別、存続時間、ミティゲーション状態の追跡を担います。論理フラグであるisBullishImpulseとisBearishImpulseは、ブレイクアウトの動きが強気または弱気のインパルス(勢い)と見なせるかどうかを判定します。最後に、OB_Prefixは、オーダーブロックオブジェクトの命名時に使用されるマクロ#defineで定義された接頭辞であり、チャート上のオブジェクト表示に一貫性を持たせるために使われます。以上の変数が定義されたことで、いよいよプログラムロジックの実装を開始する準備が整いました。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   //--- Set the magic number for the trade object to identify EA trades
   obj_Trade.SetExpertMagicNumber(uniqueMagicNumber);
   return(INIT_SUCCEEDED);
}

ここでは、OnInitイベントハンドラ内でEAの初期化をおこないます。SetExpertMagicNumberメソッドを使用してマジックナンバーを設定し、EAが実行するすべての取引に一意のタグを付与することで、他の取引との競合を防止します。このステップは、本戦略によって発注されたポジションのみを追跡・管理するために非常に重要です。初期化が完了したら、INIT_SUCCEEDEDを返して、プログラムが正常に稼働できる状態にあることを示します。その後、メインのOnTickイベントハンドラへ進み、メインの制御ロジックを実装していきます。

//+------------------------------------------------------------------+
//| Expert OnTick function                                           |
//+------------------------------------------------------------------+
void OnTick() {

   //--- Check for a new bar to process logic only once per bar
   static bool isNewBar = false;
   int currentBarCount = iBars(_Symbol, _Period);
   static int previousBarCount = currentBarCount;
   if (previousBarCount == currentBarCount) {
      isNewBar = false;
   } else if (previousBarCount != currentBarCount) {
      isNewBar = true;
      previousBarCount = currentBarCount;
   }

   //--- Exit if not a new bar to avoid redundant processing
   if (!isNewBar)
      return;
   //---
}

毎ティックではなく毎バーごとにデータを処理するために、OnTick関数(新しいティックを受信するたびに実行される)内で、iBars関数を使ってチャート上のバーの総数を取得し、currentBarCountに格納します。次に、これをpreviousBarCountと比較し、両者が等しければisNewBarはfalseのままとなり、不要な処理を防ぎます。一方、新しいバーが検出された場合は、previousBarCountを更新し、isNewBarをtrueに設定することで、ストラテジーロジックの実行を許可します。最後に、isNewBarがfalseであれば早期にreturnし、無駄な計算を避け、パフォーマンスを最適化します。新しいバーである場合のみ、保ち合いの検出を続けます。

//--- Define the starting bar index for consolidation checks
int startBarIndex = 1;

//--- Check for consolidation or extend the existing range
if (!isBreakoutDetected) {
   if (rangeHighestHigh.price == 0 && rangeLowestLow.price == 0) {
      //--- Check if bars are in a tight consolidation range
      bool isConsolidated = true;
      for (int i = startBarIndex; i < startBarIndex + consolidationBars - 1; i++) {
         if (MathAbs(high(i) - high(i + 1)) > maxConsolidationSpread * Point()) {
            isConsolidated = false;
            break;
         }
         if (MathAbs(low(i) - low(i + 1)) > maxConsolidationSpread * Point()) {
            isConsolidated = false;
            break;
         }
      }
      if (isConsolidated) {
         //--- Find the highest high in the consolidation range
         rangeHighestHigh.price = high(startBarIndex);
         rangeHighestHigh.index = startBarIndex;
         for (int i = startBarIndex + 1; i < startBarIndex + consolidationBars; i++) {
            if (high(i) > rangeHighestHigh.price) {
               rangeHighestHigh.price = high(i);
               rangeHighestHigh.index = i;
            }
         }
         //--- Find the lowest low in the consolidation range
         rangeLowestLow.price = low(startBarIndex);
         rangeLowestLow.index = startBarIndex;
         for (int i = startBarIndex + 1; i < startBarIndex + consolidationBars; i++) {
            if (low(i) < rangeLowestLow.price) {
               rangeLowestLow.price = low(i);
               rangeLowestLow.index = i;
            }
         }
         //--- Log the established consolidation range
         Print("Consolidation range established: Highest High = ", rangeHighestHigh.price,
               " at index ", rangeHighestHigh.index,
               " and Lowest Low = ", rangeLowestLow.price,
               " at index ", rangeLowestLow.index);
      }
   } else {
      //--- Check if the current bar extends the existing range
      double currentHigh = high(1);
      double currentLow = low(1);
      if (currentHigh <= rangeHighestHigh.price && currentLow >= rangeLowestLow.price) {
         Print("Range extended: High = ", currentHigh, ", Low = ", currentLow);
      } else {
         Print("No extension: Bar outside range.");
      }
   }
}

ここでは、直近の価格変動を分析して保ち合いの範囲を定義・確立します。まず、保ち合い検出の開始点としてstartBarIndexを1に設定します。isBreakoutDetectedがfalse、つまりまだブレイクアウトが検出されていない場合、市場が狭い保ち合いフェーズにあるかどうかを判定します。具体的には、直近のconsolidationBars本のバーをループで調べ、MathAbs関数を使って連続する高値および安値の絶対差を測定します。この差がすべてmaxConsolidationSpread以内であれば、保ち合いが確認されます。

保ち合いが検出されたら、範囲内の最高値と最安値を決定します。rangeHighestHighとrangeLowestLowはstartBarIndexの高値と安値で初期化し、範囲内を走査して新たな最高値・最安値を見つけた際にそれらの値を更新します。これらが保ち合いの境界となります。

既に保ち合いの範囲が確立されている場合は、現在のバーが既存の範囲を拡張しているかを確認します。highとlow関数を用いて現在の高値currentHighと安値currentLowを取得し、rangeHighestHigh.priceおよびrangeLowestLow.priceと比較します。価格が範囲内に収まる場合は、Print関数でレンジ拡張のメッセージを出力します。そうでなければ拡張はなく、ブレイクアウトの可能性を示すメッセージを出力します。以下にカスタム価格関数を示します。

//+------------------------------------------------------------------+
//| Price data accessors                                                 |
//+------------------------------------------------------------------+
double high(int index) { return iHigh(_Symbol, _Period, index); }   //--- Get high price of a bar
double low(int index) { return iLow(_Symbol, _Period, index); }     //--- Get low price of a bar
double open(int index) { return iOpen(_Symbol, _Period, index); }   //--- Get open price of a bar
double close(int index) { return iClose(_Symbol, _Period, index); } //--- Get close price of a bar
datetime time(int index) { return iTime(_Symbol, _Period, index); } //--- Get time of a bar

これらのカスタム関数は価格データの取得を簡便にします。high関数はiHighを使って指定したindexのバーの高値を返し、low関数はiLowを呼び出して対応する安値を取得します。open関数はiOpenで始値を取得し、close関数はiCloseで終値を取得します。さらに、time関数はiTimeを用いて指定したバーのタイムスタンプを返します。プログラムを実行すると、以下の結果が得られます。

保ち合い確認

画像からわかるように、価格の範囲が確立され、その範囲内で価格が推移している間は、その範囲を拡張し続けます。そして、価格が範囲を突破した時点で拡張を停止します。そこで、確定した価格の遅れ範囲でのブレイクアウトを検出する必要があります。これを以下のロジックで実現します。

//--- Detect a breakout from the consolidation range
if (rangeHighestHigh.price > 0 && rangeLowestLow.price > 0) {
   double currentClosePrice = close(1);
   if (currentClosePrice > rangeHighestHigh.price) {
      Print("Upward breakout at ", currentClosePrice, " > ", rangeHighestHigh.price);
      isBreakoutDetected = true;
   } else if (currentClosePrice < rangeLowestLow.price) {
      Print("Downward breakout at ", currentClosePrice, " < ", rangeLowestLow.price);
      isBreakoutDetected = true;
   }
}

//--- Reset state after a breakout is detected
if (isBreakoutDetected) {
   Print("Breakout detected. Resetting for the next range.");
   breakoutBarNumber = 1;
   breakoutTimestamp = TimeCurrent();
   lastImpulseHigh = rangeHighestHigh.price;
   lastImpulseLow = rangeLowestLow.price;

   isBreakoutDetected = false;
   rangeHighestHigh.price = 0;
   rangeHighestHigh.index = 0;
   rangeLowestLow.price = 0;
   rangeLowestLow.index = 0;
}

以前に特定された保ち合い範囲からのブレイクアウトを検出・処理するために、まずrangeHighestHigh.priceおよびrangeLowestLow.priceの値が有効かどうかを確認します。これにより、保ち合い範囲が正しく確立されているかを判断します。次に、close関数を用いて取得したcurrentClosePriceを範囲の上下限と比較します。終値がrangeHighestHigh.priceを上回る場合は、上方向のブレイクアウトと見なし、イベントをログに記録してisBreakoutDetectedをtrueに設定します。同様に、終値がrangeLowestLow.priceを下回る場合は、下方向のブレイクアウトとして検出し、同様にフラグを立てます。

ブレイクアウトが確認されたら、新たな保ち合いフェーズを追跡する準備として、必要な状態変数をリセットします。ブレイクアウトの発生をログに記録し、breakoutBarNumberを1として設定し、これをブレイクアウトの最初のバーとしてマークします。また、breakoutTimestampにはTimeCurrentを使用して、ブレイクアウトが発生した正確な時間を保存します。さらに、lastImpulseHighおよびlastImpulseLowを記録して、ブレイクアウト後の価格の動きを追跡します。最後に、isBreakoutDetectedをfalseに戻し、rangeHighestHigh.priceおよびrangeLowestLow.priceを0にリセットして、次の取引機会の検出に備えます。

ブレイクアウトが確認された場合は、その後のインパルスを待って検証し、チャート上にプロットします。

//--- Check for impulsive movement after breakout and create order blocks
if (breakoutBarNumber >= 0 && TimeCurrent() > breakoutTimestamp + barsToWaitAfterBreakout * PeriodSeconds()) {
   double impulseRange = lastImpulseHigh - lastImpulseLow;
   double impulseThresholdPrice = impulseRange * impulseMultiplier;
   isBullishImpulse = false;
   isBearishImpulse = false;
   for (int i = 1; i <= barsToWaitAfterBreakout; i++) {
      double closePrice = close(i);
      if (closePrice >= lastImpulseHigh + impulseThresholdPrice) {
         isBullishImpulse = true;
         Print("Impulsive upward move: ", closePrice, " >= ", lastImpulseHigh + impulseThresholdPrice);
         break;
      } else if (closePrice <= lastImpulseLow - impulseThresholdPrice) {
         isBearishImpulse = true;
         Print("Impulsive downward move: ", closePrice, " <= ", lastImpulseLow - impulseThresholdPrice);
         break;
      }
   }
   if (!isBullishImpulse && !isBearishImpulse) {
      Print("No impulsive movement detected.");
   }
   //---
}

ここでは、ブレイクアウト後の値動きを分析し、インパルスが発生したかどうかを判定します。これは有効なオーダーブロックを特定する上で重要なステップです。まずbreakoutBarNumberが有効であるか、そして現在時刻(TimeCurrentで取得)がbreakoutTimestampにbarsToWaitAfterBreakout × PeriodSecondsを加えた時刻を超えているかどうかを確認します。これにより、十分な待機時間が経過しているかどうかを判断します。次に、impulseRangeをlastImpulseHighからlastImpulseLowを引いた値として算出します。これはブレイクアウト後の価格の変動幅を示します。続いて、このimpulseRangeにimpulseMultiplierを掛けたものをimpulseThresholdPriceとして計算し、インパルスと見なすために必要な最小価格拡張幅を定義します。

その後、isBullishImpulseおよびisBearishImpulseをfalseに初期化し、直近のbarsToWaitAfterBreakout本分のバーを対象に価格動向を評価します。forループで各バーを処理し、close関数で終値を取得します。closePriceがlastImpulseHigh + impulseThresholdPrice以上であれば、インパルス的な上昇と見なしてisBullishImpulseをtrueに設定し、その旨をログに出力します。逆に、closePriceがlastImpulseLow - impulseThresholdPrice以下であれば、インパルス的な下落と判断し、isBearishImpulseをtrueに設定してログに記録します。どちらの条件も満たさない場合は、インパルスは検出されなかったというメッセージを出力します。このロジックによって、強いブレイクアウトの継続のみを有効なオーダーブロックとして扱い、今後の処理対象とします。それらをチャート上に可視化するために、次のロジックを用います。

bool isOrderBlockValid = isBearishImpulse || isBullishImpulse;

if (isOrderBlockValid) {
   datetime blockStartTime = iTime(_Symbol, _Period, consolidationBars + barsToWaitAfterBreakout + 1);
   double blockTopPrice = lastImpulseHigh;
   int visibleBarsOnChart = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
   datetime blockEndTime = blockStartTime + (visibleBarsOnChart / 1) * PeriodSeconds();
   double blockBottomPrice = lastImpulseLow;
   string orderBlockName = OB_Prefix + "(" + TimeToString(blockStartTime) + ")";
   color orderBlockColor = isBullishImpulse ? bullishOrderBlockColor : bearishOrderBlockColor;
   string orderBlockLabel = isBullishImpulse ? "Bullish OB" : "Bearish OB";

   if (ObjectFind(0, orderBlockName) < 0) {
      //--- Create a rectangle for the order block
      ObjectCreate(0, orderBlockName, OBJ_RECTANGLE, 0, blockStartTime, blockTopPrice, blockEndTime, blockBottomPrice);
      ObjectSetInteger(0, orderBlockName, OBJPROP_TIME, 0, blockStartTime);
      ObjectSetDouble(0, orderBlockName, OBJPROP_PRICE, 0, blockTopPrice);
      ObjectSetInteger(0, orderBlockName, OBJPROP_TIME, 1, blockEndTime);
      ObjectSetDouble(0, orderBlockName, OBJPROP_PRICE, 1, blockBottomPrice);
      ObjectSetInteger(0, orderBlockName, OBJPROP_FILL, true);
      ObjectSetInteger(0, orderBlockName, OBJPROP_COLOR, orderBlockColor);
      ObjectSetInteger(0, orderBlockName, OBJPROP_BACK, false);

      //--- Add a text label in the middle of the order block with dynamic font size
      datetime labelTime = blockStartTime + (blockEndTime - blockStartTime) / 2;
      double labelPrice = (blockTopPrice + blockBottomPrice) / 2;
      string labelObjectName = orderBlockName + orderBlockLabel;
      if (ObjectFind(0, labelObjectName) < 0) {
         ObjectCreate(0, labelObjectName, OBJ_TEXT, 0, labelTime, labelPrice);
         ObjectSetString(0, labelObjectName, OBJPROP_TEXT, orderBlockLabel);
         ObjectSetInteger(0, labelObjectName, OBJPROP_COLOR, labelTextColor);
         ObjectSetInteger(0, labelObjectName, OBJPROP_FONTSIZE, dynamicFontSize);
         ObjectSetInteger(0, labelObjectName, OBJPROP_ANCHOR, ANCHOR_CENTER);
      }
      ChartRedraw(0);

      //--- Store the order block details in arrays
      ArrayResize(orderBlockNames, ArraySize(orderBlockNames) + 1);
      orderBlockNames[ArraySize(orderBlockNames) - 1] = orderBlockName;
      ArrayResize(orderBlockEndTimes, ArraySize(orderBlockEndTimes) + 1);
      orderBlockEndTimes[ArraySize(orderBlockEndTimes) - 1] = blockEndTime;
      ArrayResize(orderBlockMitigatedStatus, ArraySize(orderBlockMitigatedStatus) + 1);
      orderBlockMitigatedStatus[ArraySize(orderBlockMitigatedStatus) - 1] = false;

      Print("Order Block created: ", orderBlockName);
   }
}

ここでは、インパルスが検出されたかどうかに基づいて、オーダーブロックを作成すべきかを判断します。まず、isOrderBlockValidを評価し、isBearishImpulseまたはisBullishImpulseのいずれかがtrueであるかを確認します。条件を満たしている場合、オーダーブロックの主要なパラメータを定義します。blockStartTimeはiTime関数を使用して、consolidationBars + barsToWaitAfterBreakout + 1のバーに対応する時間を取得します。これにより、検出された構造に合わせた開始時間が確保されます。blockTopPriceにはlastImpulseHighを、blockBottomPriceにはlastImpulseLowを設定し、これらがオーダーブロックの価格帯となります。続いて、ChartGetInteger関数で現在チャート上に表示されているバー数(visibleBarsOnChart)を取得し、PeriodSecondsをもとにblockEndTimeを動的に計算します。これにより、オーダーブロックの矩形がチャート上で視認できる範囲に収まるようになります。

オーダーブロックの名前は、OB_PrefixとTimeToString関数を組み合わせて、タイムスタンプ付きの一意な名前を生成します。インパルスが上昇か下降かに応じて、bullishOrderBlockColorまたはbearishOrderBlockColorを使用して色を設定し、ラベルもそれに応じた文字列を割り当てます。

次に、ObjectFind関数で同名のオーダーブロックが既に存在していないかどうかを確認し、存在しない場合はObjectCreate関数を使って矩形(OBJ_RECTANGLE)を作成します。その後、ObjectSetIntegerObjectSetDoubleを用いて矩形の時間軸・価格軸の範囲を設定します。矩形には塗りつぶし(OBJPROP_FILL)が有効化され、色(OBJPROP_COLOR)が適用され、前面に描画されないように(OBJPROP_BACK = false)設定します。

次に、より視覚的に分かりやすくするため、矩形内にラベルを作成します。ラベルの時間(labelTime)は、blockStartTimeとblockEndTimeの中間に設定され、価格(labelPrice)もblockTopPriceとblockBottomPriceの中間値に設定されます。ラベル名はorderBlockNameにorderBlockLabelを加えて一意な名前とします。そのラベルが存在していなければ、ObjectCreateを使ってテキストオブジェクト(OBJ_TEXT)を作成します。テキスト内容(OBJPROP_TEXT)、色(OBJPROP_COLOR)、フォントサイズ(OBJPROP_FONTSIZE)を設定し、OBJPROP_ANCHOR = ANCHOR_CENTERで中央揃えにします。ChartRedraw関数によって、これらのオブジェクトがすぐにチャート上に反映されます。。フォントサイズはチャートのスケールによって見やすさが大きく異なるため、次に示す方法で動的に計算します。

//--- Calculate dynamic font size based on chart scale (0 = zoomed out, 5 = zoomed in)
int chartScale = (int)ChartGetInteger(0, CHART_SCALE); // Scale ranges from 0 to 5
int dynamicFontSize = 8 + (chartScale * 2);           // Font size: 8 (min) to 18 (max)

最後に、オーダーブロックの情報を配列に保存します。orderBlockNamesには作成したオーダーブロックのオブジェクト名を、orderBlockEndTimesにはオーダーブロックの有効期限(終了時刻)を、orderBlockMitigatedStatusにはそのオーダーブロックがすでにミティゲート(価格が再訪して反応)されたかどうかの状態を格納します。これらの配列はArrayResize関数を用いて動的にサイズ調整をおこない、新しいオーダーブロックを柔軟に追加できるようにしています。その後、オーダーブロックが正常に作成されたことを示す確認メッセージをPrint関数で出力します。最後に、今後の処理に備えて、ブレイクアウト検出に関連する変数をリセットし、システムが次の検出機会に備えられるようにします。

//--- Reset breakout tracking variables
breakoutBarNumber = -1;
breakoutTimestamp = 0;
lastImpulseHigh = 0;
lastImpulseLow = 0;
isBullishImpulse = false;
isBearishImpulse = false;

プログラムをコンパイルし実行すると、以上のロジックに基づいた動作が確認できます。

確認済みのOB

画像から確認できるように、インパルス的なブレイクアウトによって形成されたオーダーブロックが検出・ラベリングされました。ここからは、チャート内に表示されているセットアップを継続的に監視・管理しつつ、ミティゲーション(価格がオーダーブロックに戻って反応する現象)が発生したオーダーブロックの検証プロセスに進みます。

//--- Process existing order blocks for mitigation and trading
for (int j = ArraySize(orderBlockNames) - 1; j >= 0; j--) {
   string currentOrderBlockName = orderBlockNames[j];
   bool doesOrderBlockExist = false;

   //--- Retrieve order block properties
   double orderBlockHigh = ObjectGetDouble(0, currentOrderBlockName, OBJPROP_PRICE, 0);
   double orderBlockLow = ObjectGetDouble(0, currentOrderBlockName, OBJPROP_PRICE, 1);
   datetime orderBlockStartTime = (datetime)ObjectGetInteger(0, currentOrderBlockName, OBJPROP_TIME, 0);
   datetime orderBlockEndTime = (datetime)ObjectGetInteger(0, currentOrderBlockName, OBJPROP_TIME, 1);
   color orderBlockCurrentColor = (color)ObjectGetInteger(0, currentOrderBlockName, OBJPROP_COLOR);

   //--- Check if the order block is still valid (not expired)
   if (time(1) < orderBlockEndTime) {
      doesOrderBlockExist = true;
   }
   //---
}

orderBlockNames配列を逆順でループ処理し、それぞれのオーダーブロックについてミティゲーションの判定とトレード処理をおこないます。現在処理中のオーダーブロック名はcurrentOrderBlockNameとして保持されます。各オーダーブロックの詳細情報を取得するために、ObjectGetDoubleおよびObjectGetIntegerを使用して、orderBlockHigh、orderBlockLow、orderBlockStartTime、orderBlockEndTime、そしてorderBlockCurrentColorなどのプロパティを取得します。

その後、そのオーダーブロックがまだ有効かどうかを検証します。これには、time(1)(カスタム関数で取得した現在のバーの時間)とorderBlockEndTimeを比較し、現在の時間がオーダーブロックの有効範囲内にあるかどうかを確認します。もし有効であれば、doesOrderBlockExistをtrueに設定し、オーダーブロックが依然として有効であるとみなして処理を継続します。以降、そのブロックに対して取引処理を進めていきます。

//--- Get current market prices
double currentAskPrice = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits);
double currentBidPrice = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits);

//--- Check for mitigation and execute trades if trading is enabled
if (enableTrading && orderBlockCurrentColor == bullishOrderBlockColor && close(1) < orderBlockLow && !orderBlockMitigatedStatus[j]) {
   //--- Sell trade when price breaks below a bullish order block
   double entryPrice = currentBidPrice;
   double stopLossPrice = entryPrice + stopLossDistance * _Point;
   double takeProfitPrice = entryPrice - takeProfitDistance * _Point;
   obj_Trade.Sell(tradeLotSize, _Symbol, entryPrice, stopLossPrice, takeProfitPrice);
   orderBlockMitigatedStatus[j] = true;
   ObjectSetInteger(0, currentOrderBlockName, OBJPROP_COLOR, mitigatedOrderBlockColor);
   string blockDescription = "Bullish Order Block";
   string textObjectName = currentOrderBlockName + blockDescription;
   if (ObjectFind(0, textObjectName) >= 0) {
      ObjectSetString(0, textObjectName, OBJPROP_TEXT, "Mitigated " + blockDescription);
   }
   Print("Sell trade entered upon mitigation of bullish OB: ", currentOrderBlockName);
} else if (enableTrading && orderBlockCurrentColor == bearishOrderBlockColor && close(1) > orderBlockHigh && !orderBlockMitigatedStatus[j]) {
   //--- Buy trade when price breaks above a bearish order block
   double entryPrice = currentAskPrice;
   double stopLossPrice = entryPrice - stopLossDistance * _Point;
   double takeProfitPrice = entryPrice + takeProfitDistance * _Point;
   obj_Trade.Buy(tradeLotSize, _Symbol, entryPrice, stopLossPrice, takeProfitPrice);
   orderBlockMitigatedStatus[j] = true;
   ObjectSetInteger(0, currentOrderBlockName, OBJPROP_COLOR, mitigatedOrderBlockColor);
   string blockDescription = "Bearish Order Block";
   string textObjectName = currentOrderBlockName + blockDescription;
   if (ObjectFind(0, textObjectName) >= 0) {
      ObjectSetString(0, textObjectName, OBJPROP_TEXT, "Mitigated " + blockDescription);
   }
   Print("Buy trade entered upon mitigation of bearish OB: ", currentOrderBlockName);
}

まず、SymbolInfoDouble関数を使って現在のマーケット価格を取得し、_Digitsを用いてcurrentAskPriceとcurrentBidPriceを小数点以下の桁数に合わせて正規化します。これは取引執行時の精度を確保するために重要です。続いて、enableTradingが有効かつ、特定のオーダーブロックがまだミティゲートされていない状態で、かつ価格がそのオーダーブロックをブレイクした場合に、取引を実行します。これは、価格がオーダーブロックの支持・抵抗の構造を崩したことを意味します。

強気オーダーブロックに対しては、直近バーの終値(close関数で取得)がorderBlockLowを下回り、かつそのオーダーブロックがまだミティゲートされていない(orderBlockMitigatedStatus[j]==false)ことを確認します。これらの条件を満たした場合、obj_TradeオブジェクトのSell関数を使って売り注文を実行します。注文はcurrentBidPriceでおこない、ストップロス価格(stopLossPrice)はエントリー価格の上にstopLossDistance*_Pointだけ離して設定し、テイクプロフィット価格(takeProfitPrice)はエントリー価格の下にtakeProfitDistance_Pointだけ離して設定します。

注文が実行されたら、orderBlockMitigatedStatus[j]をtrueに更新し、そのオーダーブロックがミティゲート済みであることを示します。さらに、ObjectSetInteger関数を用いてオーダーブロックの色をミティゲート済みの色に変更します。該当オーダーブロックのテキストラベルが存在下場合(ObjectFindで確認)、ObjectSetString関数を使ってラベルを「Mitigated Bullish Order Block」に更新します。最後に、Print関数で取引実行の記録をログに残します。

弱気オーダーブロックに対しても同様に、直近バーの終値がorderBlockHighを上回ったかどうかをチェックし、ベアオーダーブロックのブレイクを確認します。条件が満たされた場合、obj_TradeのBuy関数を使って買い注文をおこない、注文価格はcurrentAskPriceです。ストップロス価格(stopLossPrice)はエントリー価格の下に、テイクプロフィット価格(takeProfitPrice)はエントリー価格の上にそれぞれ設定し、適切なリスク管理を確保します。買い注文後、orderBlockMitigatedStatus[j]を更新し、ObjectSetIntegerでオーダーブロックの色を変更します。該当ラベルがあれば「Mitigated Bearish Order Block」にテキストを更新します。最後にPrint関数で買い注文の実行をログに記録します。これが私たちの実現した内容です。

ミティゲーションおよび取引されたOB

最後に、オーダーブロックがチャートの範囲外に出たら、保存用の配列からそれらを削除します。

//--- Remove expired order blocks from arrays
if (!doesOrderBlockExist) {
   bool removedName = ArrayRemove(orderBlockNames, j, 1);
   bool removedTime = ArrayRemove(orderBlockEndTimes, j, 1);
   bool removedStatus = ArrayRemove(orderBlockMitigatedStatus, j, 1);
   if (removedName && removedTime && removedStatus) {
      Print("Success removing OB DATA from arrays at index ", j);
   }
}

オーダーブロックが存在しなくなった場合、ArrayRemove関数を使って、それぞれの配列からオーダーブロック名、終了時間、ミティゲーション状態を削除します。すべての削除が成功した場合、Print文でクリーンアップ完了をログに記録します。以下はクリーンアップ確認の例です。

ブロックのクリーンアップ

画像からわかるように、オーダーブロックのクリーンアップが正常におこなわれています。次に、トレーリングストップのロジックを追加する必要がありますが、そのために、すべてをまとめて処理する関数を用意します。

//+------------------------------------------------------------------+
//| Trailing stop function                                           |
//+------------------------------------------------------------------+
void applyTrailingStop(double trailingPoints, CTrade &trade_object, int magicNo = 0) {
   //--- Calculate trailing stop levels based on current market prices
   double buyStopLoss = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - trailingPoints * _Point, _Digits);
   double sellStopLoss = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + trailingPoints * _Point, _Digits);
   
   //--- Loop through all open positions
   for (int i = PositionsTotal() - 1; i >= 0; i--) {
      ulong ticket = PositionGetTicket(i);
      if (ticket > 0) {
         if (PositionGetString(POSITION_SYMBOL) == _Symbol && 
             (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)) {
            //--- Adjust stop loss for buy positions
            if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && 
                buyStopLoss > PositionGetDouble(POSITION_PRICE_OPEN) && 
                (buyStopLoss > PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0)) {
               trade_object.PositionModify(ticket, buyStopLoss, PositionGetDouble(POSITION_TP));
            } 
            //--- Adjust stop loss for sell positions
            else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && 
                       sellStopLoss < PositionGetDouble(POSITION_PRICE_OPEN) && 
                       (sellStopLoss < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0)) {
               trade_object.PositionModify(ticket, sellStopLoss, PositionGetDouble(POSITION_TP));
            }
         }
      }
   }
}

ここでは、稼働中のポジションに対してストップロスレベルを動的に調整するapplyTrailingStop関数を定義します。まず、現在のBid/Ask価格と指定されたtrailingPointsを用いてbuyStopLossとsellStopLossを計算します。次に、すべてのポジションをループ処理し、銘柄およびマジックナンバー(指定されている場合)でフィルタリングします。買いポジションの場合、エントリー価格より上で有効なストップロスレベルがあり、それが現在のストップロスよりも有利、もしくは未設定であれば、ストップロスを更新します。同様に売りポジションに対しては、新しいストップロスがエントリー価格より下であることを確認した上で修正します。

この関数はOnTickイベントハンドラ内で呼び出され、今回のように毎ティックで処理することでリアルタイムの価格チェックをおこないます。

//--- Apply trailing stop to open positions if enabled
if (enableTrailingStop) {
   applyTrailingStop(trailingStopPoints, obj_Trade, uniqueMagicNumber);
}

プログラムをコンパイルして実行すると、次の結果が得られます。

MOB GIF

可視化からわかるように、プログラムはすべてのエントリー条件を正確に特定・検証し、条件が満たされた場合にはそれぞれのパラメーターでポジションをオープンしています。これで、狙い通りの動作が実現できていることがわかります。残された作業はプログラムのバックテストであり、それについては次のセクションで取り扱います。


バックテスト

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

バックテストグラフ

グラフ

バックテストレポート

レポート


結論

結論として、今回MQL5で Mitigation Order Blocks (MOB)戦略を実装し、スマートマネーの概念に基づいた正確な検出、可視化、自動売買を可能にしました。ブレイクアウトの検証、インパルスの認識、ミティゲーションに基づくトレード実行を統合することで、市場の動向に適応しつつオーダーブロックを効果的に特定・処理するシステムを構築しました。さらに、トレーリングストップやリスク管理の仕組みも組み込み、トレードパフォーマンスの最適化と堅牢性の向上を図っています。

免責条項:本記事は教育目的のみを意図したものです。取引には大きな財務リスクが伴い、市場の状況は予測できない場合があります。実際の運用前には十分なバックテストとリスク管理が不可欠です。

これらの手法を活用することで、アルゴリズム取引戦略を洗練させ、オーダーブロックに基づく取引効率を向上させることが可能です。引き続きテストと最適化を重ね、長期的な成功を目指してください。ご健闘をお祈りします。

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

添付されたファイル |
最後のコメント | ディスカッションに移動 (4)
linfo2
linfo2 | 28 3月 2025 において 04:15
ありがとう、アラン、うまくまとまっていて、ビジュアルと、緩和されたときの色の変化、そしてアレイの扱い方がとても気に入っている。シェアしてくれてありがとう。
Allan Munene Mutiiria
Allan Munene Mutiiria | 28 3月 2025 において 10:56
linfo2 #:
ありがとう、アラン、うまくまとまっていて、ビジュアルと、緩和されたときの色の変化、そしてアレイの扱い方がとても気に入っている。シェアしてくれてありがとう。

親切なフィードバックをありがとう。どういたしまして。

davesarge1
davesarge1 | 12 4月 2025 において 13:08
ストラテジーテスターで 取引していない。ジャーナルにエラーメッセージがない。 ジャーナルにメッセージがある:Bar outside range "と "No impulsive movement detected"。


Allan Munene Mutiiria
Allan Munene Mutiiria | 14 4月 2025 において 13:47
davesarge1 ストラテジーテスターで 取引していない。ジャーナルにエラーメッセージがない。 ジャーナルにメッセージがある:Bar outside range "と "No impulsive movement detected"。


記事を読みましたか?なぜなら、この記事があなたの答えをすべて教えてくれるからです。

MQL5で自己最適化エキスパートアドバイザーを構築する(第6回):自己適応型取引ルール(II) MQL5で自己最適化エキスパートアドバイザーを構築する(第6回):自己適応型取引ルール(II)
本記事では、より良い売買シグナルを得るために、RSIのレベルと期間を最適化する方法を探ります。最適なRSI値を推定する手法や、グリッドサーチと統計モデルを用いた期間選定の自動化について紹介します。最後に、Pythonによる分析を活用しながら、MQL5でソリューションを実装します。私たちのアプローチは、複雑になりがちな問題をシンプルに解決することを目指した、実用的かつ分かりやすいものです。
データサイエンスとML(第35回):MQL5でのNumPy活用術 - 少ないコードで複雑なアルゴリズムを構築する技法 データサイエンスとML(第35回):MQL5でのNumPy活用術 - 少ないコードで複雑なアルゴリズムを構築する技法
NumPyライブラリは、Pythonプログラミング言語においてほぼすべての機械学習アルゴリズムの中核を支えています。本記事では、高度なモデルやアルゴリズムの構築を支援するために、複雑なコードをまとめたモジュールを実装していきます。
MQL5入門(第14回):初心者のためのカスタムインジケーター作成ガイド(III) MQL5入門(第14回):初心者のためのカスタムインジケーター作成ガイド(III)
MQL5でチャートオブジェクトを使ってハーモニックパターンインジケーターを構築する方法を学びましょう。スイングポイントの検出、フィボナッチリトレースメントの適用、そしてパターン認識の自動化について解説します。
プライスアクション分析ツールキットの開発(第18回):クォーターズ理論の紹介(III) - Quarters Board プライスアクション分析ツールキットの開発(第18回):クォーターズ理論の紹介(III) - Quarters Board
この記事では、元のQuarters Scriptを改良し、「Quarters Board」というツールを導入しています。これにより、コードを編集し直すことなく、チャート上でクォーターレベルを直接オン・オフできるようになります。特定のレベルを簡単に有効化・無効化できるほか、エキスパートアドバイザー(EA)はトレンド方向に関するコメントも提供し、市場の動きをより理解しやすくします。