English 中文 Deutsch 日本語
preview
Автоматизация торговых стратегий на MQL5 (Часть 12): Реализация стратегии смягчения ордер-блоков (MOB)

Автоматизация торговых стратегий на MQL5 (Часть 12): Реализация стратегии смягчения ордер-блоков (MOB)

MetaTrader 5Трейдинг |
370 5
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В нашей предыдущей статье (Часть 11) мы разработали многоуровневую систему сеточной торговли на языке MetaQuotes Language 5 (MQL5), позволяющую извлекать выгоду из колебаний рынка. Теперь, в Части 12, мы сосредоточимся на реализации стратегии Смягчения ордер-блоков (Mitigation Order Blocks, MOB), концепции Smart Money, которая определяет ключевые ценовые зоны, в которых институциональные ордера смягчаются перед значительными изменениями на рынке. В статье рассмотрим следующие темы:

  1. План стратегии
  2. Реализация средствами MQL5
  3. Тестирование на истории
  4. Заключение

К концу настоящей статьи у вас будет полностью автоматизированная торговая система смягчения ордер-блоков, готовая к осуществлению торговли. Начнем!


План стратегии

Для реализации стратегии смягчения ордер-блоков мы разработаем автоматизированную систему, которая обнаруживает, проверяет и исполняет сделки на основе событий, связанных со смягчением ордер-блоков. Стратегия будет направлена на выявление институциональных ценовых зон, в которых ликвидность поглощается до продолжения тренда. Наша система будет включать точные условия для входа, размещения стоп-лосса и управления сделками для обеспечения эффективности и точности. Структурируем разработку следующим образом:

  • Идентификация блоков ордеров – система будет сканировать динамику цен за прошлые периоды для выявления бычьих и медвежьих блоков ордеров, отфильтровывая слабые зоны на основе волатильности, захвата ликвидности и ценового дисбаланса.
  • Подтверждение смягчения – мы запрограммируем условия, которые подтвердят действительное событие смягчения, гарантируя, что цена вернется к ордер-блоку и отреагирует сигналами отклонения, такими как тени свечей или импульсные сдвиги.
  • Подтверждение структуры рынка – советник проанализирует тренды на более высоких таймфреймах и колебания ликвидности, чтобы убедиться, что выявленные меры по смягчению последствий соответствуют более широкому рыночному потоку.
  • Правила исполнения сделок – как только смягчение будет подтверждено, система определит точные точки входа, динамически рассчитает уровни стоп-лосса на основе структуры ордер-блоков и установит целевые значения тейк-профита на основе параметров соотношения риска и прибыли.
  • Управление рисками и капиталом – Стратегия будет включать в себя определение размера позиции, защиту от просадки и стратегии выхода для эффективного управления торговыми рисками.

В двух словах, вот общая визуализация такой стратегии.

Mitigation Order Block


Реализация средствами MQL5

Чтобы создать программу на MQL5, откройте MetaEditor, перейдите в Навигатор, найдите папку «Индикаторы» (Indicators), перейдите на вкладку "Создать" (New) и следуйте инструкциям по созданию файла. Как только это будет сделано, в среде программирования нам нужно будет объявить некоторые глобальные переменные, которые будем использовать во всей программе.

//+------------------------------------------------------------------+
//|                        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>", которая предоставляет встроенные функции для управления торговыми операциями. Затем инициализируем торговый объект "obj_Trade", используя класс "CTrade", что позволяет советнику программно исполнять ордера на покупку и продажу. Такая настройка обеспечит эффективное выполнение торговых операций без необходимости ручного вмешательства. Затем мы можем предоставить некоторые входные данные, чтобы пользователь мог изменять поведение и управлять им из пользовательского интерфейса (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" идентифицирует сделки, а консолидация определяется с помощью "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.  Устанавливаем магическое число советника, используя метод "SetExpertMagicNumber", гарантируя, что все сделки, совершаемые нашим советником, будут помечены уникальным образом, что предотвратит конфликты с другими сделками. Этот шаг имеет решающее значение для отслеживания и управления только сделками, открытыми в соответствии с нашей стратегией. Как только инициализация завершена, мы возвращаем 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, возвращаемся досрочно, оптимизируя производительность за счет пропуска ненужных вычислений. Если это новый бар, продолжаем искать консолидацию.

//--- 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", переходим к оценке того, находится ли рынок в фазе жесткой консолидации. Выполняем перебор по последнему количеству баров "consolidationBars" с помощью функции MathAbs для измерения абсолютных различий между последовательными максимумами и минимумами. Если все различия остаются в пределах "maxConsolidationSpread", подтверждаем консолидацию.

Как только обнаруживается консолидация, мы определяем самый высокий максимум и самый низкий минимум в пределах диапазона. Инициализируем "rangeHighestHigh" и "rangeLowestLow" значениями high и low из "startBarIndex", затем перебираем диапазон, чтобы обновлять эти значения всякий раз, когда сталкиваемся с новым максимумом или минимумом. Эти значения определяют наши границы консолидации.

Если диапазон консолидации уже установлен, проверяем, расширяет ли текущий бар существующий диапазон. Мы извлекаем "currentHigh" и "currentLow", используя функции "high" и "low", и сравниваем их с "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 для возврата временной метки данного бара. После запуска программы получаем следующий результат.

Consolidation Confirmation

На изображении видно, что как только ценовой диапазон установлен и цена колеблется в пределах этого диапазона, мы расширяем его до тех пор, пока не достигнем предела диапазона. Итак, теперь нам нужно обнаружить прорыв в подтвержденном диапазоне ценового лага. Для этого мы используем следующую логику.

//--- 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", отражающую колебания цены после пробоя. Используя это, вычисляем "impulseThresholdPrice" путем умножения "impulseRange" на "impulseMultiplier", чтобы определить минимальный рост цены, необходимый для импульсивного движения.

Затем инициализируем "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" и динамического вычисления "blockEndTime" на основе PeriodSeconds, гарантируя, что прямоугольник остается видимым в пределах текущей области диаграммы.

Имя ордер-блока создается с использованием "OB_Prefix" и функции TimeToString, чтобы включить временную метку для уникальности. Цвет и метка определяются в зависимости от того, является ли импульс бычьим или медвежьим, с выбором "bullishOrderBlockColor" или "bearishOrderBlockColor" и присвоением соответствующей метки.

Затем проверяем наличие ордер-блока посредством ObjectFind. Если его не существует, используем функцию ObjectCreate, чтобы нарисовать прямоугольник (OBJ_RECTANGLE), представляющий собой ордер-блок, устанавливая его временные и ценовые границы посредством ObjectSetInteger и ObjectSetDouble. Прямоугольник заполняется (OBJPROP_FILL), применяется цвет (OBJPROP_COLOR), и он отображается на переднем плане (OBJPROP_BACK = false).

Затем создаем метку внутри ордер-блока для лучшей визуализации. Время метки ("labelTime") устанавливается в средней точке "blockStartTime" и "blockEndTime", в то время как "labelPrice" рассчитывается как средняя точка "blockTopPrice" и "blockBottomPrice". Генерируем уникальное название метки, добавляя "orderBlockLabel" к "orderBlockName". Если метка не существует, создаем текстовый объект (OBJ_TEXT) с помощью "ObjectCreate", задавая текстовое содержимое (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 для размещения новых записей, обеспечивая сохранение гибкости управления ордер-блоками. Для указания на успешное создание ордер-блока будет выведено подтверждающее сообщение. Наконец, нам просто нужно сбросить переменные отслеживания пробоев.

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

После компиляции и запуска программы получаем следующий результат.

Confirmed OBs

На изображении видно, что мы подтвердили и отметили ордер-блоки, которые являются результатом импульсивных движений пробоя. Итак, теперь нам просто нужно приступить к проверке смягченных ордер-блоков посредством непрерывного управления настройками в пределах границ графика.

//--- 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)" (полученное с помощью функции "time") с "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,  проверяя, что и "currentAskPrice" и "currentBidPrice" приведены к соответствующему количеству знаков после запятой с помощью _Digits. Это гарантирует точность при размещении сделок. Далее проверяем, активна ли функция "enableTrading" и выполнено ли условие смягчения ордер-блоков. Смягчение происходит, когда цена пробивает блок ордеров, что указывает на сбой в его структуре удержания.

В отношении бычьих ордер-блоков проверяем, опустилась ли цена "close" предыдущего бара (полученная с помощью функции "close") ниже "orderBlockLow", и гарантируем, что в отношении этого ордер-блока смягчение ещё не произведено ("orderBlockMitigatedStatus[j] == false"). Если эти условия выполняются, размещаем сделку на продажу, используя функцию "Sell" объекта "obj_Trade". Сделка совершается по цене "currentBidPrice", при этом стоп-лосс ("stopLossPrice") устанавливается выше цены входа с помощью "stopLossDistance * _Point", а тейк-профит ("takeProfitPrice") устанавливается ниже цены входа с помощью "takeProfitDistance * _Point".

После исполнения сделки ордер-блок помечается как смягченный путем обновления значения "orderBlockMitigatedStatus[j]" до true, а его цвет изменяется с помощью ObjectSetInteger,  чтобы указать на его смягченное состояние. Если для этого блока ордеров существует текстовая метка (проверенная с помощью ObjectFind), обновляем ее с помощью ObjectSetString,  чтобы она отображала «Смягченный бычий ордер-блок" (Mitigated Bullish Order Block). Оператор Print регистрирует исполнение сделки для отслеживания и отладки.

Для медвежьих ордер-блоков процесс аналогичен. Проверяем, поднялась ли цена "close" выше "orderBlockHigh", что указывает на прорыв медвежьего ордер-блока. Если условия выполнены, с помощью функции "Buy" размещается сделка на покупку, используя "currentAskPrice" в качестве цены входа. "stopLossPrice" расположена ниже цены входа, а "takeProfitPrice" - выше нее, что обеспечивает надлежащее управление рисками. После размещения сделки на покупку обновляем "orderBlockMitigatedStatus[j]", меняем цвет блока ордеров, используя ObjectSetInteger, и изменяем текстовую метку (если она найдена), чтобы она отображала «Смягченный медвежий ордер-блок» (Mitigated Bearish Order Block). Наконец, оператор "Print" регистрирует исполнение сделки на покупку для целей отслеживания. Вот что у нас получилось.

Mitigated & Traded 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 для подтверждения очистки. Вот пример подтверждения очистки.

Blocks Cleanup

На изображении видно, что мы успешно выполнили очистку блоков. Теперь нам просто нужно добавить логику трейлинг-стопа, а для этого мы используем функцию, которая все инкапсулирует.

//+------------------------------------------------------------------+
//| 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" для динамической настройки уровней стоп-лосса для активных позиций. Мы начинаем с расчета "buyStopLoss" и "sellStopLoss", используя текущие цены bid/ask и указанные "trailingPoints". Далее мы перебираем все открытые позиции в цикле, фильтруя их по символу и магическому числу (если они указаны). Если у позиции на покупку действующий уровень стоп-лосса выше цены входа, и он либо превышает текущий стоп-лосс, либо не установлен, мы обновляем его. Аналогично, для позиций на продажу мы должны убедиться, что новый стоп-лосс находится ниже цены входа, прежде чем изменять его.

Затем вызываем функцию внутри обработчика событий  OnTick для обработки на каждом тике, а не на каждом баре, на этот раз для проверки цены в режиме реального времени следующим образом.

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

После компиляции и запуска программы получаем следующий результат.

MOB GIF

На изображении видно, что программа определяет и проверяет все условия входа и, если они подтверждены, открывает соответствующую позицию с соответствующими параметрами входа, тем самым достигая нашей цели. Осталось провести повторное тестирование программы, и это будет выполнено в следующем разделе.


Тестирование на истории

После тщательного тестирования на истории мы получили следующие результаты.

График тестирования на истории:

График

Отчет о тестировании на истории:

REPORT


Заключение

В заключение отметим, что мы успешно реализовали стратегию Смягчения ордер-блоков (Mitigation Order Blocks, MOB) на языке MQL5, позволяющую точно определять, визуализировать и автоматизировать торговлю на основе концепций smart money. Благодаря интеграции проверки пробоя, распознавания импульсивных движений и исполнения сделок на основе смягчения, наша система эффективно распознает и обрабатывает ордер-блоки, адаптируясь к динамике рынка. Кроме того, мы внедрили трейлинг-стопы и механизмы управления рисками для оптимизации торговых показателей и повышения надежности.

Отказ от ответственности: Содержание настоящей статьи предназначено только для целей обучения. Торговля сопряжена со значительным финансовым риском, а рыночные условия могут быть непредсказуемыми. Перед началом использования в реальных условиях необходимы тщательное тестирование на истории и управление рисками.

Используя эти методы, вы можете усовершенствовать свои алгоритмические торговые стратегии и повысить эффективность торговли на основе блоков ордеров. Продолжайте тестировать, оптимизировать и адаптировать свой подход для достижения долгосрочного успеха. Желаем удачи!

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17547

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (5)
linfo2
linfo2 | 28 мар. 2025 в 04:15
Спасибо, Аллан, очень понравилось визуальное сопровождение, изменение цвета при уменьшении и работа с массивами. Спасибо, что поделились
Allan Munene Mutiiria
Allan Munene Mutiiria | 28 мар. 2025 в 10:56
linfo2 #:
Спасибо, Аллан, очень понравилось визуальное сопровождение, изменение цвета при уменьшении и работа с массивами. Спасибо, что поделились

Спасибо за добрый отзыв. Пожалуйста.

davesarge1
davesarge1 | 12 апр. 2025 в 13:08
Не принимает сделки в тестере стратегий. Настройки по умолчанию для всех пар. В журнале нет сообщений об ошибках. Сообщения в журнале присутствуют: "Нет расширения: Бар вне диапазона" и "Импульсивное движение не обнаружено".


Allan Munene Mutiiria
Allan Munene Mutiiria | 14 апр. 2025 в 13:47
davesarge1 тестере стратегий. Настройки по умолчанию для всех пар. В журнале нет сообщений об ошибках. Сообщения в журнале присутствуют: "Нет расширения: Бар вне диапазона" и "Импульсивное движение не обнаружено".


Вы вообще читали статью? Потому что мы уверены, что в статье вы найдете ответы на все вопросы.

Bao Thuan Thai
Bao Thuan Thai | 1 авг. 2025 в 23:35
Большое спасибо. Очень ценю это! Я попробую
Автоматизация торговых стратегий на MQL5 (Часть 4): Построение многоуровневой системы зонального восстановления Автоматизация торговых стратегий на MQL5 (Часть 4): Построение многоуровневой системы зонального восстановления
В этой статье мы разработаем многоуровневую систему зонального восстановления в MQL5, которая использует RSI для генерации торговых сигналов. Каждый сигнал динамически добавляется в массив, что позволяет системе одновременно управлять несколькими сигналами в рамках логики зонального восстановления. Данный подход демонстрирует эффективную обработку сложных сценариев управления торговлей, сохраняя при этом масштабируемый и надежный дизайн кода.
Создание самооптимизирующихся советников на MQL5 (Часть 5): Самоадаптирующиеся торговые правила Создание самооптимизирующихся советников на MQL5 (Часть 5): Самоадаптирующиеся торговые правила
Правилам безопасного использования индикатора не всегда легко следовать. Спокойные рыночные условия могут неожиданно приводить к появлению на индикаторе значений, которые не будут считаться торговым сигналом, что приведет к упущенным возможностям для алгоритмических трейдеров. В статье рассматривается потенциальное решение проблемы, а также создание торговых приложений, способных адаптировать свои торговые правила к имеющимся рыночным данным.
Нейросети в трейдинге: Спайково-семантический подход к пространственно-временной идентификации (S3CE-Net) Нейросети в трейдинге: Спайково-семантический подход к пространственно-временной идентификации (S3CE-Net)
Приглашаем к знакомству с фреймворком S3CE-Net и его механизмами SSAM и STFS, которые точно обрабатывают спайковые события с учётом каузальности. Модель лёгкая, параллельная и умеет выявлять сложные связи во времени и пространстве.
Нейросети в трейдинге: Обучение глубоких спайкинговых моделей (Окончание) Нейросети в трейдинге: Обучение глубоких спайкинговых моделей (Окончание)
В данной статье показана практическая реализация фреймворка SEW ResNet средствами MQL5 с акцентом на прикладное применение в торговле. Двойной Bottleneck даёт возможность одновременно анализировать унитарные потоки и межканальные зависимости, не теряя градиентов при обучении. Спайковые активации с адаптивными порогами и гейты повышают устойчивость к шуму и чувствительность к новизне рынка. В тексте приведены детали реализации и результаты тестов.