Автоматизация торговых стратегий на MQL5 (Часть 12): Реализация стратегии смягчения ордер-блоков (MOB)
Введение
В нашей предыдущей статье (Часть 11) мы разработали многоуровневую систему сеточной торговли на языке MetaQuotes Language 5 (MQL5), позволяющую извлекать выгоду из колебаний рынка. Теперь, в Части 12, мы сосредоточимся на реализации стратегии Смягчения ордер-блоков (Mitigation Order Blocks, MOB), концепции Smart Money, которая определяет ключевые ценовые зоны, в которых институциональные ордера смягчаются перед значительными изменениями на рынке. В статье рассмотрим следующие темы:
К концу настоящей статьи у вас будет полностью автоматизированная торговая система смягчения ордер-блоков, готовая к осуществлению торговли. Начнем!
План стратегии
Для реализации стратегии смягчения ордер-блоков мы разработаем автоматизированную систему, которая обнаруживает, проверяет и исполняет сделки на основе событий, связанных со смягчением ордер-блоков. Стратегия будет направлена на выявление институциональных ценовых зон, в которых ликвидность поглощается до продолжения тренда. Наша система будет включать точные условия для входа, размещения стоп-лосса и управления сделками для обеспечения эффективности и точности. Структурируем разработку следующим образом:
- Идентификация блоков ордеров – система будет сканировать динамику цен за прошлые периоды для выявления бычьих и медвежьих блоков ордеров, отфильтровывая слабые зоны на основе волатильности, захвата ликвидности и ценового дисбаланса.
- Подтверждение смягчения – мы запрограммируем условия, которые подтвердят действительное событие смягчения, гарантируя, что цена вернется к ордер-блоку и отреагирует сигналами отклонения, такими как тени свечей или импульсные сдвиги.
- Подтверждение структуры рынка – советник проанализирует тренды на более высоких таймфреймах и колебания ликвидности, чтобы убедиться, что выявленные меры по смягчению последствий соответствуют более широкому рыночному потоку.
- Правила исполнения сделок – как только смягчение будет подтверждено, система определит точные точки входа, динамически рассчитает уровни стоп-лосса на основе структуры ордер-блоков и установит целевые значения тейк-профита на основе параметров соотношения риска и прибыли.
- Управление рисками и капиталом – Стратегия будет включать в себя определение размера позиции, защиту от просадки и стратегии выхода для эффективного управления торговыми рисками.
В двух словах, вот общая визуализация такой стратегии.

Реализация средствами 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 для возврата временной метки данного бара. После запуска программы получаем следующий результат.

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

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

Наконец, как только блоки оказываются за пределами допустимого диапазона, удаляем их из массивов хранения.
//--- 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" для динамической настройки уровней стоп-лосса для активных позиций. Мы начинаем с расчета "buyStopLoss" и "sellStopLoss", используя текущие цены bid/ask и указанные "trailingPoints". Далее мы перебираем все открытые позиции в цикле, фильтруя их по символу и магическому числу (если они указаны). Если у позиции на покупку действующий уровень стоп-лосса выше цены входа, и он либо превышает текущий стоп-лосс, либо не установлен, мы обновляем его. Аналогично, для позиций на продажу мы должны убедиться, что новый стоп-лосс находится ниже цены входа, прежде чем изменять его.
Затем вызываем функцию внутри обработчика событий OnTick для обработки на каждом тике, а не на каждом баре, на этот раз для проверки цены в режиме реального времени следующим образом.
//--- Apply trailing stop to open positions if enabled if (enableTrailingStop) { applyTrailingStop(trailingStopPoints, obj_Trade, uniqueMagicNumber); }
После компиляции и запуска программы получаем следующий результат.

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

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

Заключение
В заключение отметим, что мы успешно реализовали стратегию Смягчения ордер-блоков (Mitigation Order Blocks, MOB) на языке MQL5, позволяющую точно определять, визуализировать и автоматизировать торговлю на основе концепций smart money. Благодаря интеграции проверки пробоя, распознавания импульсивных движений и исполнения сделок на основе смягчения, наша система эффективно распознает и обрабатывает ордер-блоки, адаптируясь к динамике рынка. Кроме того, мы внедрили трейлинг-стопы и механизмы управления рисками для оптимизации торговых показателей и повышения надежности.
Отказ от ответственности: Содержание настоящей статьи предназначено только для целей обучения. Торговля сопряжена со значительным финансовым риском, а рыночные условия могут быть непредсказуемыми. Перед началом использования в реальных условиях необходимы тщательное тестирование на истории и управление рисками.
Используя эти методы, вы можете усовершенствовать свои алгоритмические торговые стратегии и повысить эффективность торговли на основе блоков ордеров. Продолжайте тестировать, оптимизировать и адаптировать свой подход для достижения долгосрочного успеха. Желаем удачи!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17547
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Автоматизация торговых стратегий на MQL5 (Часть 4): Построение многоуровневой системы зонального восстановления
Создание самооптимизирующихся советников на MQL5 (Часть 5): Самоадаптирующиеся торговые правила
Нейросети в трейдинге: Спайково-семантический подход к пространственно-временной идентификации (S3CE-Net)
Нейросети в трейдинге: Обучение глубоких спайкинговых моделей (Окончание)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Спасибо, Аллан, очень понравилось визуальное сопровождение, изменение цвета при уменьшении и работа с массивами. Спасибо, что поделились
Спасибо за добрый отзыв. Пожалуйста.
Вы вообще читали статью? Потому что мы уверены, что в статье вы найдете ответы на все вопросы.