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

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

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

Введение

В нашей предыдущей статье (Часть 34) мы разработали систему пробоя линии тренда на языке MetaQuotes Language 5 (MQL5). Система определяла линии тренда поддержки и сопротивления по свинговым точкам и подтверждала их с помощью коэффициента детерминации R² как меры качества аппроксимации, после чего открывала сделки на пробое и динамически отображала объекты на графике. В Части 35 мы создадим торговую систему на основе брейкер-блоков, которая определяет диапазоны консолидации, использует свинговые точки для подтверждения брейкер-блоков и торговли на ретестах с настраиваемыми параметрами риска и визуальной индикацией на графике. Мы рассмотрим следующие темы:

  1. Принцип работы стратегии на основе брейкер-блоков
  2. Реализация на MQL5
  3. Бэктестирование
  4. Заключение

В результате у вас будет готовая стратегия MQL5 для торговли на ретестах брейкер-блоков, готовая к дальнейшей настройке. Приступим!


Принцип работы стратегии на основе брейкер-блоков

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

Медвежий брейкер-блок:

МЕДВЕЖИЙ БРЕЙКЕР-БЛОК

Бычий брейкер-блок:

БЫЧИЙ БРЕЙКЕР-БЛОК

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

СХЕМА СИСТЕМЫ НА ОСНОВЕ БРЕЙКЕР-БЛОКОВ


Реализация на MQL5

Чтобы создать программу на MQL5, откройте MetaEditor, затем панель «Навигатор», найдите папку Experts, нажмите «Создать» и следуйте указаниям мастера. После создания файла объявим входные параметры и глобальные переменные, которые будут использоваться во всей программе.

//+------------------------------------------------------------------+
//|                                    Breaker Block Strategy EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   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 strict

#include <Trade/Trade.mqh>                         //--- Подключаем торговую библиотеку для управления позициями
CTrade obj_Trade;                                  //--- Создаём торговый объект для выполнения ордеров

Начнём с подключения торговой библиотеки директивой «#include <Trade/Trade.mqh>». Она предоставляет встроенные средства для выполнения торговых операций. Затем создадим объект «obj_Trade» класса CTrade, чтобы советник мог программно открывать сделки на покупку и продажу. Благодаря этому торговые операции будут выполняться автоматически, без ручного вмешательства. Далее добавим входные параметры, с помощью которых пользователь сможет управлять поведением программы через пользовательский интерфейс (UI).

//+------------------------------------------------------------------+
//| Входные параметры |
//+------------------------------------------------------------------+
input double tradeLotSize = 0.01;                  // Объём каждой позиции в лотах
input bool   enableTrading = true;                 // Включение или отключение автоматической торговли
input bool   enableTrailingStop = true;            // Включение или отключение трейлинг-стопа
input double trailingStopPoints = 30;              // Расстояние трейлинг-стопа в пунктах
input double minProfitToTrail = 50;                // Минимальная прибыль в пунктах для запуска трейлинг-стопа
input int    uniqueMagicNumber = 12345;            // Уникальный идентификатор сделок советника
input int    consolidationBars = 7;                // Количество баров для поиска диапазона консолидации
input double maxConsolidationSpread = 50;          // Максимально допустимый разброс цен в пунктах для консолидации
input int    barsToWaitAfterBreakout = 3;          // Количество баров после пробоя до проверки импульса
input double impulseMultiplier = 1.0;              // Множитель для обнаружения импульсного движения цены
input double stopLossDistance = 1500;              // Расстояние Stop Loss от точки входа в пунктах
input double takeProfitDistance = 1500;            // Расстояние Take Profit от точки входа в пунктах
input double moveAwayDistance = 50;                // Расстояние отхода цены после аннулирования, в пунктах
input color  bullishColor = clrGreen;              // Основной цвет бычьих ордер-блоков и брейкер-блоков
input color  bearishColor = clrRed;                // Основной цвет медвежьих ордер-блоков и брейкер-блоков
input color  labelTextColor = clrBlack;            // Цвет текстовых подписей блоков
input bool   enableSwingValidation = true;         // Включение проверки аннулирования блока по свинговым точкам
input bool   showSwingPoints = true;               // Отображение подписей свинговых точек при включённой проверке
input color  swingLabelColor = clrWhite;           // Цвет подписей свинговых точек
input int    swingFontSize = 10;                   // Размер шрифта подписей свинговых точек

Здесь мы объявляем входные параметры, определяющие поведение программы. Параметр «tradeLotSize» задаёт объём позиции. «enableTrading» и «enableTrailingStop» включают автоматическую торговлю и трейлинг-стоп, а «trailingStopPoints» и «minProfitToTrail» уточняют логику его работы. «uniqueMagicNumber» служит идентификатором сделок, а «consolidationBars» и «maxConsolidationSpread» используются для поиска консолидации. Назначение остальных параметров понятно из их названий и комментариев. Текстовые метки в коде сохранены на английском для единообразия объектов. В завершение объявим глобальные переменные для общего управления состоянием системы.

//+------------------------------------------------------------------+
//| Структура для цены и индекса                                     |
//+------------------------------------------------------------------+
struct PriceAndIndex {                             //--- Определяем структуру для хранения цены и индекса
   double price;                                   //--- Сохраняем значение цены — максимум или минимум
   int    index;                                   //--- Сохраняем индекс бара, соответствующий цене
};

//+------------------------------------------------------------------+
//| Глобальные переменные для отслеживания состояния рынка           |
//+------------------------------------------------------------------+
PriceAndIndex rangeHighestHigh = {0, 0};           //--- Отслеживаем наивысший максимум диапазона консолидации
PriceAndIndex rangeLowestLow = {0, 0};             //--- Отслеживаем наименьший минимум диапазона консолидации
bool   isBreakoutDetected = false;                 //--- Флаг обнаружения пробоя
double lastImpulseLow = 0.0;                       //--- Сохраняем минимальную цену после пробоя для проверки импульса
double lastImpulseHigh = 0.0;                      //--- Сохраняем максимальную цену после пробоя для проверки импульса
int    breakoutBarNumber = -1;                     //--- Сохраняем индекс бара пробоя
datetime breakoutTimestamp = 0;                    //--- Сохраняем время пробоя
string   blockNames[];                             //--- Сохраняем имена объектов блоков
datetime blockEndTimes[];                          //--- Сохраняем время окончания блоков
bool     invalidatedStatus[];                      //--- Отслеживаем статус аннулирования блоков
string   blockTypes[];                             //--- Отслеживаем типы блоков (OB/BB, бычий/медвежий)
bool     movedAwayStatus[];                        //--- Отслеживаем, отошла ли цена после аннулирования
bool     retestedStatus[];                         //--- Отслеживаем, прошёл ли блок ретест
string   blockLabels[];                            //--- Сохраняем имена объектов подписей блоков
datetime creationTimes[];                          //--- Сохраняем время создания блоков
datetime invalidationTimes[];                      //--- Сохраняем время аннулирования блоков
double   invalidationSwings[];                     //--- Сохраняем свинговый максимум или минимум при аннулировании
bool     isBullishImpulse = false;                 //--- Флаг бычьего импульсного движения
bool     isBearishImpulse = false;                 //--- Флаг медвежьего импульсного движения
#define OB_Prefix "OB REC "                        //--- Задаём префикс имён ордер-блоков

Здесь мы создаём структуры данных и переменные для отслеживания состояния системы. Сначала определяем структуру «PriceAndIndex», в которой хранятся значение цены — максимум или минимум — и соответствующий индекс бара. Это позволяет точно фиксировать границы диапазона консолидации. Затем инициализируем глобальные переменные. Экземпляры «rangeHighestHigh» и «rangeLowestLow» сохраняют наивысший максимум и наименьший минимум диапазона. Флаг «isBreakoutDetected» отмечает факт пробоя. Переменные «lastImpulseLow» и «lastImpulseHigh» хранят цены для последующей проверки импульса, а «breakoutBarNumber» и «breakoutTimestamp» — индекс и время пробоя. Массивы «blockNames», «blockEndTimes», «invalidatedStatus», «blockTypes», «movedAwayStatus», «retestedStatus», «blockLabels», «creationTimes», «invalidationTimes» и «invalidationSwings» используются для сопровождения объектов блоков, времени окончания блоков, статуса аннулирования, типа, ретеста и связанных свинговых точек.

Наконец, с помощью директивы #define задаём «OB_Prefix» со значением «OB REC », чтобы единообразно именовать объекты ордер-блоков. Теперь создадим вспомогательные функции для затемнения аннулированных ордер-блоков и обработчиков событий.

//+------------------------------------------------------------------+
//| Затемнение цвета с заданным коэффициентом                        |
//+------------------------------------------------------------------+
color DarkenColor(color colorValue, double factor = 0.8) {
   int red = int((colorValue & 0xFF) * factor);          //--- Извлекаем и затемняем красную составляющую
   int green = int(((colorValue >> 8) & 0xFF) * factor); //--- Извлекаем и затемняем зелёную составляющую
   int blue = int(((colorValue >> 16) & 0xFF) * factor); //--- Извлекаем и затемняем синюю составляющую
   return (color)(red | (green << 8) | (blue << 16));    //--- Объединяем компоненты в единое значение цвета
}

//+------------------------------------------------------------------+
//| Функции доступа к ценовым данным                                 |
//+------------------------------------------------------------------+
double high(int index) {
   return iHigh(_Symbol, _Period, index);         //--- Возвращаем максимум для заданного индекса
}
double low(int index) {
   return iLow(_Symbol, _Period, index);          //--- Возвращаем минимум для заданного индекса
}
double close(int index) {
   return iClose(_Symbol, _Period, index);        //--- Возвращаем цену закрытия для заданного индекса
}
datetime time(int index) {
   return iTime(_Symbol, _Period, index);         //--- Возвращаем время для заданного индекса
}


//+------------------------------------------------------------------+
//| Функция инициализации советника                                  |
//+------------------------------------------------------------------+
int OnInit() {
   obj_Trade.SetExpertMagicNumber(uniqueMagicNumber); //--- Задаём магический номер для идентификации сделок
   return(INIT_SUCCEEDED);                            //--- Возвращаем успешный результат инициализации
}

//+------------------------------------------------------------------+
//| Функция деинициализации советника                                |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ObjectsDeleteAll(0, OB_Prefix);                //--- Удаляем все объекты с префиксом OB
   ChartRedraw(0);                                //--- Перерисовываем график после удаления объектов
}

Продолжим вспомогательными функциями. Функция «DarkenColor» принимает цвет и необязательный коэффициент, по умолчанию равный 0,8. С помощью побитовых операций она извлекает красную, зелёную и синюю составляющие, умножает каждую на коэффициент, а затем объединяет компоненты с помощью побитовых сдвигов. В результате возвращается более тёмный цвет, позволяющий визуально отличать аннулированные блоки.

Далее создаём функции доступа «high», «low», «close» и «time». Для указанного индекса бара они возвращают максимум (iHigh), минимум («iLow»), цену закрытия («iClose») и время (iTime) на текущем символе и таймфрейме. В функции OnInit передаём «uniqueMagicNumber» методу «SetExpertMagicNumber» объекта «obj_Trade» и возвращаем INIT_SUCCEEDED, подтверждая успешную инициализацию. В функции OnDeinit вызываем ObjectsDeleteAll, чтобы удалить с графика все объекты с префиксом «OB_Prefix», а затем ChartRedraw для обновления графика. Теперь можно перейти к основному обработчику событий OnTick.

//+------------------------------------------------------------------+
//| Функция обработки тика советника                                 |
//+------------------------------------------------------------------+
void OnTick() {
   static bool isNewBar = false;                  //--- Отслеживаем состояние нового бара
   int currentBarCount = iBars(_Symbol, _Period); //--- Получаем текущее количество баров
   static int previousBarCount = currentBarCount; //--- Сохраняем предыдущее количество баров
   if (previousBarCount == currentBarCount) {     //--- Проверяем отсутствие нового бара
      isNewBar = false;                           //--- Задаём флаг отсутствия нового бара
   } else {                                       //--- Обнаружен новый бар
      isNewBar = true;                            //--- Задаём флаг нового бара
      previousBarCount = currentBarCount;         //--- Обновляем предыдущее количество баров
   }
   if (!isNewBar) return;                         //--- Выходим, если новый бар не появился
   int startBarIndex = 1;                         //--- Задаём начальный индекс анализа
   int chartScale = (int)ChartGetInteger(0, CHART_SCALE); //--- Получаем масштаб графика
   int dynamicFontSize = 8 + (chartScale * 2);    //--- Рассчитываем динамический размер шрифта
   if (!isBreakoutDetected) {                     //--- Проверяем отсутствие обнаруженного пробоя
      if (rangeHighestHigh.price == 0 && rangeLowestLow.price == 0) { //--- Проверяем задан ли диапазон
         bool isConsolidated = true;              //--- Предполагаем наличие консолидации
         for (int i = startBarIndex; i < startBarIndex + consolidationBars - 1; i++) { //--- Перебираем бары консолидации
            if (MathAbs(high(i) - high(i + 1)) > maxConsolidationSpread * Point() || MathAbs(low(i) - low(i + 1)) > maxConsolidationSpread * Point()) { //--- Проверяем допустимый разброс
               isConsolidated = false;            //--- Отмечаем как отсутствие консолидации
               break;                             //--- Выходим из цикла
            }
         }
         if (isConsolidated) {                    //--- Подтверждаем наличие консолидации
            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;    //--- Обновляем индекс максимума
               }
            }
            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;       //--- Обновляем индекс минимума
               }
            }
            Print("Consolidation range established: Highest High = ", rangeHighestHigh.price, " at index ", rangeHighestHigh.index, " and Lowest Low = ", rangeLowestLow.price, " at index ", rangeLowestLow.index); //--- Выводим в журнал данные диапазона
         }
      } else {                                    //--- Диапазон уже задан
         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."); //--- Выводим в журнал, что бар вышел за пределы диапазона
         }
      }
   }
}

Сначала реализуем поиск диапазонов консолидации. В функции OnTick сравниваем текущее количество баров, полученное через iBars, со статической переменной «previousBarCount». При появлении нового бара устанавливаем «isNewBar» в true и обновляем счётчик; в противном случае завершаем обработку. Затем получаем масштаб графика через ChartGetInteger с параметром CHART_SCALE и рассчитываем «dynamicFontSize» как 8 плюс удвоенное значение масштаба. Если пробой ещё не обнаружен и диапазон не задан, перебираем «consolidationBars», начиная с индекса 1. С помощью MathAbs проверяем, что разница между максимумами и минимумами соседних баров не превышает «maxConsolidationSpread * Point()». При превышении порога «isConsolidated» устанавливается в false.

Если консолидация подтверждена, записываем максимум и минимум бара «startBarIndex» в «rangeHighestHigh.price» и «rangeLowestLow.price», а затем перебираем «consolidationBars» и находим наивысший максимум и наименьший минимум вместе с их индексами. Информация о диапазоне выводится через Print. Если диапазон уже существует, проверяем, находятся ли максимум и минимум текущего бара внутри его границ. Теперь эти значения можно использовать для обнаружения пробоя, построения ордер-блоков и их дальнейшего сопровождения. Сначала ордер-блок должен быть аннулирован, и только после этого он может стать брейкер-блоком.

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;               //--- Задаём флаг пробоя
   }
}
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;                   //--- Очищаем индекс минимума
}
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."); //--- Выводим в журнал отсутствие импульса
   }
   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 blockName = OB_Prefix + "(" + TimeToString(blockStartTime) + ")"; //--- Формируем имя блока
      color blockColor = isBullishImpulse ? bullishColor : bearishColor; //--- Задаём цвет блока
      string blockLabel = isBullishImpulse ? "Bullish Order Block" : "Bearish Order Block"; //--- Задаём подпись блока
      string blockType = isBullishImpulse ? "OB-bullish" : "OB-bearish"; //--- Задаём тип блока
      if (ObjectFind(0, blockName) < 0) {      //--- Проверяем существование блока
         ObjectCreate(0, blockName, OBJ_RECTANGLE, 0, blockStartTime, blockTopPrice, blockEndTime, blockBottomPrice); //--- Создаём прямоугольник блока
         ObjectSetInteger(0, blockName, OBJPROP_TIME, 0, blockStartTime); //--- Задаём время начала
         ObjectSetDouble(0, blockName, OBJPROP_PRICE, 0, blockTopPrice); //--- Задаём верхнюю цену
         ObjectSetInteger(0, blockName, OBJPROP_TIME, 1, blockEndTime); //--- Задаём время окончания
         ObjectSetDouble(0, blockName, OBJPROP_PRICE, 1, blockBottomPrice); //--- Задаём нижнюю цену
         ObjectSetInteger(0, blockName, OBJPROP_FILL, true); //--- Включаем заливку
         ObjectSetInteger(0, blockName, OBJPROP_COLOR, blockColor); //--- Задаём цвет блока
         ObjectSetInteger(0, blockName, OBJPROP_BACK, false); //--- Задаём отображение на переднем плане
         datetime labelTime = blockStartTime + (blockEndTime - blockStartTime) / 2; //--- Рассчитываем время подписи
         double labelPrice = (blockTopPrice + blockBottomPrice) / 2; //--- Рассчитываем цену подписи
         string labelObjectName = blockName + " Label"; //--- Формируем имя подписи
         ObjectCreate(0, labelObjectName, OBJ_TEXT, 0, labelTime, labelPrice); //--- Создаём подпись
         ObjectSetString(0, labelObjectName, OBJPROP_TEXT, blockLabel); //--- Задаём текст подписи
         ObjectSetInteger(0, labelObjectName, OBJPROP_COLOR, labelTextColor); //--- Задаём цвет подписи
         ObjectSetInteger(0, labelObjectName, OBJPROP_FONTSIZE, dynamicFontSize); //--- Задаём размер шрифта подписи
         ObjectSetInteger(0, labelObjectName, OBJPROP_ANCHOR, ANCHOR_CENTER); //--- Задаём точку привязки подписи
         ChartRedraw(0);                       //--- Перерисовываем график
         ArrayResize(blockNames, ArraySize(blockNames) + 1); //--- Изменяем размер массива имён блоков
         blockNames[ArraySize(blockNames) - 1] = blockName; //--- Добавляем имя блока
         ArrayResize(blockEndTimes, ArraySize(blockEndTimes) + 1); //--- Изменяем размер массива времени окончания
         blockEndTimes[ArraySize(blockEndTimes) - 1] = blockEndTime; //--- Добавляем время окончания
         ArrayResize(invalidatedStatus, ArraySize(invalidatedStatus) + 1); //--- Изменяем размер массива статусов аннулирования
         invalidatedStatus[ArraySize(invalidatedStatus) - 1] = false; //--- Устанавливаем статус аннулирования
         ArrayResize(blockTypes, ArraySize(blockTypes) + 1); //--- Изменяем размер массива типов блоков
         blockTypes[ArraySize(blockTypes) - 1] = blockType; //--- Добавляем тип блока
         ArrayResize(movedAwayStatus, ArraySize(movedAwayStatus) + 1); //--- Изменяем размер массива статусов отхода цены
         movedAwayStatus[ArraySize(movedAwayStatus) - 1] = false; //--- Устанавливаем статус отхода цены
         ArrayResize(retestedStatus, ArraySize(retestedStatus) + 1); //--- Изменяем размер массива статусов ретеста
         retestedStatus[ArraySize(retestedStatus) - 1] = false; //--- Устанавливаем статус ретеста
         ArrayResize(blockLabels, ArraySize(blockLabels) + 1); //--- Изменяем размер массива подписей блоков
         blockLabels[ArraySize(blockLabels) - 1] = labelObjectName; //--- Добавляем имя подписи
         ArrayResize(creationTimes, ArraySize(creationTimes) + 1); //--- Изменяем размер массива времени создания
         creationTimes[ArraySize(creationTimes) - 1] = time(1); //--- Задаём время создания
         ArrayResize(invalidationTimes, ArraySize(invalidationTimes) + 1); //--- Изменяем размер массива времени аннулирования
         invalidationTimes[ArraySize(invalidationTimes) - 1] = 0; //--- Сохраняем время аннулирования
         ArrayResize(invalidationSwings, ArraySize(invalidationSwings) + 1); //--- Изменяем размер массива свинговых точек аннулирования
         invalidationSwings[ArraySize(invalidationSwings) - 1] = 0.0; //--- Сохраняем свинговую точку аннулирования
         Print("Order Block created: ", blockName); //--- Выводим в журнал создание блока
      }
   }
   breakoutBarNumber = -1;                     //--- Сбрасываем бар пробоя
   breakoutTimestamp = 0;                      //--- Сбрасываем время пробоя
   lastImpulseHigh = 0;                        //--- Сбрасываем максимум импульса
   lastImpulseLow = 0;                         //--- Сбрасываем минимум импульса
   isBullishImpulse = false;                   //--- Сбрасываем бычий импульс
   isBearishImpulse = false;                   //--- Сбрасываем медвежий импульс
}

Здесь реализована логика обнаружения пробоя и создания ордер-блока. Сначала проверяем, заданы ли обе границы диапазона, и получаем цену закрытия текущего бара через «close(1)». Если она выше «rangeHighestHigh.price», фиксируем пробой вверх; если ниже «rangeLowestLow.price» — пробой вниз. В обоих случаях устанавливаем «isBreakoutDetected» в true. После обнаружения пробоя записываем индекс бара и текущее время через TimeCurrent, сохраняем «lastImpulseHigh» и «lastImpulseLow», а затем очищаем границы диапазона и сбрасываем флаг пробоя.

Когда «breakoutBarNumber» неотрицателен и с момента пробоя прошло заданное число периодов, вычисляем диапазон импульса и порог «impulseRange * impulseMultiplier». Затем в пределах «barsToWaitAfterBreakout» проверяем цены закрытия. Закрытие выше «lastImpulseHigh + impulseThresholdPrice» подтверждает бычий импульс, а закрытие ниже «lastImpulseLow - impulseThresholdPrice» — медвежий.

Если импульс не найден, выводим соответствующее сообщение. При подтверждённом бычьем или медвежьем импульсе создаём прямоугольный ордер-блок через ObjectCreate (OBJ_RECTANGLE). Время начала берём из iTime, верхнюю и нижнюю границы — из «lastImpulseHigh» и «lastImpulseLow», а время окончания рассчитываем по количеству видимых баров через ChartGetInteger(CHART_VISIBLE_BARS). Затем задаём заливку и цвет блока, создаём центральную подпись и обновляем массивы состояния. В конце сбрасываем переменные пробоя. Таким образом система обнаруживает пробои и визуализирует соответствующие ордер-блоки. После компиляции получаем следующий результат.

ПЕРВИЧНОЕ ОБНАРУЖЕНИЕ ОРДЕР-БЛОКОВ

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

for (int j = ArraySize(blockNames) - 1; j >= 0; j--) { //--- Перебираем блоки
   string currentBlockName = blockNames[j];    //--- Получаем имя текущего блока
   bool doesBlockExist = false;                //--- Инициализируем флаг существования блока
   double blockHigh = ObjectGetDouble(0, currentBlockName, OBJPROP_PRICE, 0); //--- Получаем верхнюю границу блока
   double blockLow = ObjectGetDouble(0, currentBlockName, OBJPROP_PRICE, 1); //--- Получаем нижнюю границу блока
   datetime blockStartTime = (datetime)ObjectGetInteger(0, currentBlockName, OBJPROP_TIME, 0); //--- Получаем начало блока
   datetime blockEndTime = (datetime)ObjectGetInteger(0, currentBlockName, OBJPROP_TIME, 1); //--- Получаем окончание блока
   color blockCurrentColor = (color)ObjectGetInteger(0, currentBlockName, OBJPROP_COLOR); //--- Получаем цвет блока
   if (time(1) < blockEndTime) {               //--- Проверяем, остаётся ли блок актуальным
      doesBlockExist = true;                   //--- Задаём флаг существования блока
   }
   if (StringFind(blockTypes[j], "OB-") == 0 && !invalidatedStatus[j]) { //--- Проверяем действующий ордер-блок
      bool invalidated = false;                //--- Инициализируем флаг аннулирования
      string newBlockType = "";                //--- Инициализируем новый тип блока
      color invalidatedColor = clrNONE;        //--- Инициализируем цвет аннулированного блока
      string newLabel = "";                    //--- Инициализируем новую подпись
      bool isForBullishBB = false;             //--- Инициализируем флаг бычьего брейкер-блока
      double breakPrice = 0.0;                 //--- Инициализируем цену пробоя
      int arrowCode = 0;                       //--- Инициализируем код стрелки
      int anchor = 0;                          //--- Инициализируем точку привязки
      if (blockTypes[j] == "OB-bearish" && close(1) > blockHigh) { //--- Проверяем аннулирование медвежьего блока
         isForBullishBB = true;                //--- Задаём бычий брейкер-блок
         breakPrice = blockHigh;               //--- Задаём цену пробоя
         arrowCode = 233;                      //--- Задаём стрелку вверх
         anchor = ANCHOR_BOTTOM;               //--- Задаём нижнюю точку привязки
         newBlockType = "Invalidated-bearish"; //--- Устанавливаем тип аннулированного блока
         invalidatedColor = DarkenColor(bearishColor); //--- Затемняем медвежий цвет
         newLabel = "Invalidated Bearish Order Block"; //--- Устанавливаем подпись аннулированного блока
      } else if (blockTypes[j] == "OB-bullish" && close(1) < blockLow) { //--- Проверяем аннулирование бычьего блока
         isForBullishBB = false;               //--- Задаём медвежий брейкер-блок
         breakPrice = blockLow;                //--- Задаём цену пробоя
         arrowCode = 234;                      //--- Задаём стрелку вниз
         anchor = ANCHOR_TOP;                  //--- Задаём верхнюю точку привязки
         newBlockType = "Invalidated-bullish"; //--- Устанавливаем тип аннулированного блока
         invalidatedColor = DarkenColor(bullishColor); //--- Затемняем бычий цвет
         newLabel = "Invalidated Bullish Order Block"; //--- Устанавливаем подпись аннулированного блока
      } else {                                 //--- Аннулирование не произошло
         continue;                             //--- Переходим к следующему блоку
      }
      bool validSwingForInvalidation = true;   //--- Предполагаем наличие допустимой свинговой точки
      int swingShift = -1;                     //--- Инициализируем сдвиг свинговой точки
      double swingPrice = 0.0;                 //--- Инициализируем цену свинговой точки
      if (enableSwingValidation) {             //--- Проверяем, включена ли валидация по свинговым точкам
         int creationShift = iBarShift(_Symbol, _Period, creationTimes[j], false); //--- Получаем сдвиг бара создания
         if (creationShift > 1) {              //--- Проверяем, достаточно ли баров
            double extreme = isForBullishBB ? blockLow : blockHigh; //--- Задаём экстремальную цену
            bool isBearishOB = isForBullishBB; //--- Задаём флаг медвежьего ордер-блока
            if (isBearishOB) {                 //--- Обрабатываем медвежий ордер-блок
               double minLow = extreme;        //--- Инициализируем минимальное значение
               for (int k = creationShift - 1; k > 1; k--) { //--- Находим более низкий минимум
                  if (low(k) < minLow) {       //--- Проверяем более низкий минимум
                     minLow = low(k);          //--- Обновляем минимальное значение
                     swingShift = k;           //--- Обновляем сдвиг свинговой точки
                  }
               }
               validSwingForInvalidation = minLow < extreme; //--- Проверяем свинговую точку
               swingPrice = minLow;            //--- Задаём цену свинговой точки
            } else {                           //--- Обрабатываем бычий ордер-блок
               double maxHigh = extreme;       //--- Инициализируем максимальное значение
               for (int k = creationShift - 1; k > 1; k--) { //--- Находим более высокий максимум
                  if (high(k) > maxHigh) {     //--- Проверяем более высокий максимум
                     maxHigh = high(k);        //--- Обновляем максимальное значение
                     swingShift = k;           //--- Обновляем сдвиг свинговой точки
                  }
               }
               validSwingForInvalidation = maxHigh > extreme; //--- Проверяем свинговую точку
               swingPrice = maxHigh;           //--- Задаём цену свинговой точки
            }
         } else {                              //--- Недостаточно баров
            validSwingForInvalidation = false; //--- Считаем свинговую точку недействительной
         }
      }
      if (validSwingForInvalidation) {        //--- Подтверждаем проверку свинговой точки
         invalidated = true;                  //--- Устанавливаем флаг аннулирования
      }
      if (invalidated) {                      //--- Обрабатываем аннулирование
         ObjectSetInteger(0, currentBlockName, OBJPROP_COLOR, invalidatedColor); //--- Обновляем цвет блока
         ObjectDelete(0, blockLabels[j]);     //--- Удаляем старую подпись
         datetime labelTime = blockStartTime + (blockEndTime - blockStartTime) / 2; //--- Рассчитываем время новой подписи
         double labelPrice = (blockHigh + blockLow) / 2; //--- Рассчитываем цену новой подписи
         string newLabelObjectName = currentBlockName + " Label"; //--- Формируем имя новой подписи
         ObjectCreate(0, newLabelObjectName, OBJ_TEXT, 0, labelTime, labelPrice); //--- Создаём новую подпись
         ObjectSetString(0, newLabelObjectName, OBJPROP_TEXT, newLabel); //--- Задаём текст подписи
         ObjectSetInteger(0, newLabelObjectName, OBJPROP_COLOR, labelTextColor); //--- Задаём цвет подписи
         ObjectSetInteger(0, newLabelObjectName, OBJPROP_FONTSIZE, dynamicFontSize); //--- Задаём размер шрифта подписи
         ObjectSetInteger(0, newLabelObjectName, OBJPROP_ANCHOR, ANCHOR_CENTER); //--- Задаём точку привязки подписи
         string arrowName = currentBlockName + "_break_arrow"; //--- Формируем имя стрелки
         if (ObjectFind(0, arrowName) < 0) {  //--- Проверяем существование стрелки
            ObjectCreate(0, arrowName, OBJ_ARROW, 0, time(1), breakPrice); //--- Создаём стрелку пробоя
            ObjectSetInteger(0, arrowName, OBJPROP_ARROWCODE, arrowCode); //--- Задаём код стрелки
            ObjectSetInteger(0, arrowName, OBJPROP_ANCHOR, anchor); //--- Задаём точку привязки стрелки
            ObjectSetInteger(0, arrowName, OBJPROP_COLOR, invalidatedColor); //--- Задаём цвет стрелки
         }
         if (enableSwingValidation && showSwingPoints && swingShift > 0) { //--- Проверяем отображение свинговых точек
            string swingLabelName = currentBlockName + "_invalid_swing"; //--- Формируем имя подписи свинговой точки
            if (ObjectFind(0, swingLabelName) < 0) { //--- Проверяем существование подписи свинговой точки
               datetime swingTime = time(swingShift); //--- Получаем время свинговой точки
               ObjectCreate(0, swingLabelName, OBJ_TEXT, 0, swingTime, swingPrice); //--- Создаём подпись свинговой точки
               string swingText = isForBullishBB ? "LL" : "HH"; //--- Задаём текст свинговой точки
               ObjectSetString(0, swingLabelName, OBJPROP_TEXT, swingText); //--- Задаём текст подписи свинговой точки
               ObjectSetInteger(0, swingLabelName, OBJPROP_COLOR, swingLabelColor); //--- Задаём цвет подписи свинговой точки
               ObjectSetInteger(0, swingLabelName, OBJPROP_FONTSIZE, swingFontSize); //--- Задаём размер шрифта подписи свинговой точки
               ObjectSetInteger(0, swingLabelName, OBJPROP_ANCHOR, isForBullishBB ? ANCHOR_LEFT_UPPER : ANCHOR_LEFT_LOWER); //--- Задаём точку привязки подписи свинговой точки
            }
         }
         ChartRedraw(0);                       //--- Перерисовываем график
         invalidatedStatus[j] = true;          //--- Устанавливаем статус аннулирования
         blockTypes[j] = newBlockType;         //--- Обновляем тип блока
         movedAwayStatus[j] = false;           //--- Сбрасываем статус отхода цены
         retestedStatus[j] = false;            //--- Сбрасываем статус ретеста
         blockLabels[j] = newLabelObjectName;  //--- Обновляем имя подписи
         invalidationTimes[j] = time(1);       //--- Сохраняем время аннулирования
         invalidationSwings[j] = isForBullishBB ? high(1) : low(1); //--- Сохраняем свинговую точку аннулирования
         Print("Order Block invalidated: ", currentBlockName); //--- Записываем аннулирование в журнал
      }
   }
   if (!doesBlockExist) {                     //--- Проверяем, истёк ли срок действия блока
      ArrayRemove(blockNames, j, 1);          //--- Удаляем имя блока
      ArrayRemove(blockEndTimes, j, 1);       //--- Удаляем время окончания
      ArrayRemove(invalidatedStatus, j, 1);   //--- Удаляем статус аннулирования
      ArrayRemove(blockTypes, j, 1);          //--- Удаляем тип блока
      ArrayRemove(movedAwayStatus, j, 1);     //--- Удаляем статус отхода цены
      ArrayRemove(retestedStatus, j, 1);      //--- Удаляем статус ретеста
      ArrayRemove(blockLabels, j, 1);         //--- Удаляем имя подписи
      ArrayRemove(creationTimes, j, 1);       //--- Удаляем время создания
      ArrayRemove(invalidationTimes, j, 1);   //--- Удаляем время аннулирования
      ArrayRemove(invalidationSwings, j, 1);  //--- Удаляем свинговую точку аннулирования
      Print("Removed expired block at index ", j); //--- Выводим в журнал удаление блока
   }
}

Для сопровождения и аннулирования ордер-блоков перебираем массив «blockNames» в обратном порядке. Через ObjectGetDouble получаем верхнюю и нижнюю границы блока, а через ObjectGetInteger — время его начала и окончания. Блок считается существующим, пока время текущего бара не превышает время окончания. Для действующего ордер-блока проверяем условия аннулирования. Если цена закрытия пробивает вверх медвежий блок «OB-bearish», формируем бычий брейкер-блок и стрелку вверх с кодом 233 через «ObjectCreate» (OBJ_ARROW). Если цена пробивает вниз бычий блок «OB-bullish», формируем медвежий брейкер-блок и стрелку вниз с кодом 234. Цвет аннулированного блока затемняется функцией «DarkenColor». В MQL5 доступен большой набор кодов Wingdings, поэтому можно выбрать подходящие символы.

MQL5 WINGDINGS

Если «enableSwingValidation» включён, дополнительно подтверждаем аннулирование по свинговым точкам. Для медвежьего блока ищем более низкий минимум, а для бычьего — более высокий максимум с момента создания блока, используя iBarShift и функции «low»/«high». При успешной проверке обновляем цвет и подпись блока через ObjectSetInteger и ObjectCreate (OBJ_TEXT). Если включён «showSwingPoints», создаём подпись «LL» или «HH» в координатах свинговой точки. После аннулирования обновляем «invalidatedStatus», «blockTypes» и «invalidationTimes», выводим сообщение и сбрасываем статус ретеста. Если объект блока больше не существует, удаляем связанные элементы из массивов с помощью ArrayRemove, после чего обновляем график функцией ChartRedraw. После компиляции получаем следующий результат.

НЕДЕЙСТВИТЕЛЬНЫЕ ОРДЕР-БЛОКИ

После завершения аннулирования переходим к следующему шагу: отслеживаем возврат цены к аннулированным ордер-блокам и после ретеста отмечаем их как брейкер-блоки.

for (int j = ArraySize(blockNames) - 1; j >= 0; j--) { //--- Перебираем аннулированные блоки
   if (StringFind(blockTypes[j], "Invalidated-") != 0) continue; //--- Пропускаем действующие блоки
   string currentBlockName = blockNames[j];    //--- Получаем имя текущего блока
   double blockHigh = ObjectGetDouble(0, currentBlockName, OBJPROP_PRICE, 0); //--- Получаем верхнюю границу блока
   double blockLow = ObjectGetDouble(0, currentBlockName, OBJPROP_PRICE, 1); //--- Получаем нижнюю границу блока
   datetime blockStartTime = (datetime)ObjectGetInteger(0, currentBlockName, OBJPROP_TIME, 0); //--- Получаем начало блока
   datetime blockEndTime = (datetime)ObjectGetInteger(0, currentBlockName, OBJPROP_TIME, 1); //--- Получаем окончание блока
   bool isForBullishBB = (blockTypes[j] == "Invalidated-bearish"); //--- Проверяем бычий ли это брейкер-блок
   datetime currentBarTime = time(1);          //--- Получаем время текущего бара
   if (currentBarTime <= invalidationTimes[j]) continue; //--- Пропускаем блок, если это тот же бар или более ранний бар
   if (!movedAwayStatus[j]) {                  //--- Проверяем отошла ли цена от блока
      if (isForBullishBB && close(1) > blockHigh + moveAwayDistance * _Point) { //--- Проверяем отход цены вверх
         movedAwayStatus[j] = true;           //--- Устанавливаем статус отхода цены
         Print("Moved away for bullish BB setup: ", currentBlockName); //--- Выводим в журнал отход цены
      } else if (!isForBullishBB && close(1) < blockLow - moveAwayDistance * _Point) { //--- Проверяем отход цены вниз
         movedAwayStatus[j] = true;           //--- Устанавливаем статус отхода цены
         Print("Moved away for bearish BB setup: ", currentBlockName); //--- Выводим в журнал отход цены
      }
   }
   if (movedAwayStatus[j] && !retestedStatus[j]) { //--- Проверяем условия ретеста
      bool retestCondition = false;            //--- Инициализируем условие ретеста
      if (isForBullishBB && low(1) <= blockHigh && close(1) > blockHigh) { //--- Проверяем бычий ретест
         retestCondition = true;               //--- Задаём условие ретеста
      } else if (!isForBullishBB && high(1) >= blockLow && close(1) < blockLow) { //--- Проверяем медвежий ретест
         retestCondition = true;               //--- Задаём условие ретеста
      }
      bool validSwingForRetest = true;         //--- Предполагаем наличие допустимой свинговой точки
      int swingShift = -1;                     //--- Инициализируем сдвиг свинговой точки
      double swingPrice = 0.0;                 //--- Инициализируем цену свинговой точки
      if (enableSwingValidation && retestCondition) { //--- Проверяем, включена ли валидация по свинговым точкам
         int invalidShift = iBarShift(_Symbol, _Period, invalidationTimes[j], false); //--- Получаем сдвиг бара аннулирования
         if (invalidShift > 1) {               //--- Проверяем, достаточно ли баров
            double extreme = invalidationSwings[j]; //--- Получаем свинговую точку аннулирования
            if (isForBullishBB) {              //--- Обрабатываем бычий брейкер-блок
               double maxHigh = extreme;       //--- Инициализируем максимальное значение
               for (int k = invalidShift - 1; k > 1; k--) { //--- Находим более высокий максимум
                  if (high(k) > maxHigh) {     //--- Проверяем более высокий максимум
                     maxHigh = high(k);        //--- Обновляем максимальное значение
                     swingShift = k;           //--- Обновляем сдвиг свинговой точки
                  }
               }
               validSwingForRetest = maxHigh > extreme; //--- Проверяем свинговую точку
               swingPrice = maxHigh;           //--- Задаём цену свинговой точки
            } else {                           //--- Обрабатываем медвежий брейкер-блок
               double minLow = extreme;        //--- Инициализируем минимальное значение
               for (int k = invalidShift - 1; k > 1; k--) { //--- Находим более низкий минимум
                  if (low(k) < minLow) {       //--- Проверяем более низкий минимум
                     minLow = low(k);          //--- Обновляем минимальное значение
                     swingShift = k;           //--- Обновляем сдвиг свинговой точки
                  }
               }
               validSwingForRetest = minLow < extreme; //--- Проверяем свинговую точку
               swingPrice = minLow;            //--- Задаём цену свинговой точки
            }
         } else {                              //--- Недостаточно баров
            validSwingForRetest = false;       //--- Считаем свинговую точку недействительной
         }
      }
   }
}

Для поиска ретестов перебираем «blockNames» в обратном порядке и выбираем элементы, в типе которых присутствует «Invalidated-». Верхнюю и нижнюю границы получаем через ObjectGetDouble, время начала и окончания — через ObjectGetInteger. Если время текущего бара не позже «invalidationTimes», переходим к следующему блоку. Пока цена ещё не отошла от блока, проверяем, превышает ли закрытие верхнюю границу на «moveAwayDistance * _Point» для бычьего сценария либо опускается ли ниже нижней границы на такое же расстояние для медвежьего. После выполнения условия устанавливаем «movedAwayStatus» в true.

Для блока, от которого цена уже отошла, но который ещё не тестировался повторно, бычий ретест фиксируется, когда минимум бара достигает «blockHigh», а закрытие остаётся выше него. Медвежий ретест фиксируется, когда максимум достигает «blockLow», а закрытие остаётся ниже. При включённой проверке свинговых точек через iBarShift определяем бар аннулирования и ищем более высокий максимум для бычьего сценария либо более низкий минимум для медвежьего. Результаты сохраняются в «validSwingForRetest» и «swingPrice». После этого можно подтвердить ретест, преобразовать блок в брейкер-блок, изменить его цвет и открыть позицию.

if (retestCondition && validSwingForRetest) { //--- Подтверждаем ретест и свинговую точку
   if (enableTrading) {                  //--- Проверяем включена ли торговля
      double entryPrice = 0.0;           //--- Инициализируем цену входа
      double stopLossPrice = 0.0;        //--- Инициализируем Stop Loss
      double takeProfitPrice = 0.0;      //--- Инициализируем Take Profit
      if (isForBullishBB) {              //--- Обрабатываем сделку на покупку
         entryPrice = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); //--- Задаём цену входа по Ask
         stopLossPrice = entryPrice - stopLossDistance * _Point; //--- Задаём Stop Loss
         takeProfitPrice = entryPrice + takeProfitDistance * _Point; //--- Задаём Take Profit
         obj_Trade.Buy(tradeLotSize, _Symbol, entryPrice, stopLossPrice, takeProfitPrice); //--- Выполняем сделку на покупку
         Print("Buy trade on bullish BB retest: ", currentBlockName); //--- Выводим в журнал сделку на покупку
      } else {                           //--- Обрабатываем сделку на продажу
         entryPrice = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Задаём цену входа по Bid
         stopLossPrice = entryPrice + stopLossDistance * _Point; //--- Задаём Stop Loss
         takeProfitPrice = entryPrice - takeProfitDistance * _Point; //--- Задаём Take Profit
         obj_Trade.Sell(tradeLotSize, _Symbol, entryPrice, stopLossPrice, takeProfitPrice); //--- Выполняем сделку на продажу
         Print("Sell trade on bearish BB retest: ", currentBlockName); //--- Выводим в журнал сделку на продажу
      }
   }
   color bbColor = isForBullishBB ? clrBlueViolet : clrOrange; //--- Задаём цвет брейкер-блока
   ObjectSetInteger(0, currentBlockName, OBJPROP_COLOR, bbColor); //--- Обновляем цвет блока
   ObjectDelete(0, blockLabels[j]);     //--- Удаляем старую подпись
   string newLabel = isForBullishBB ? "Bullish Breaker Block" : "Bearish Breaker Block"; //--- Задаём новую подпись
   datetime labelTime = blockStartTime + (blockEndTime - blockStartTime) / 2; //--- Рассчитываем время подписи
   double labelPrice = (blockHigh + blockLow) / 2; //--- Рассчитываем цену подписи
   string newLabelObjectName = currentBlockName + " Label"; //--- Формируем имя новой подписи
   ObjectCreate(0, newLabelObjectName, OBJ_TEXT, 0, labelTime, labelPrice); //--- Создаём новую подпись
   ObjectSetString(0, newLabelObjectName, OBJPROP_TEXT, newLabel); //--- Задаём текст подписи
   ObjectSetInteger(0, newLabelObjectName, OBJPROP_COLOR, labelTextColor); //--- Задаём цвет подписи
   ObjectSetInteger(0, newLabelObjectName, OBJPROP_FONTSIZE, dynamicFontSize); //--- Задаём размер шрифта подписи
   ObjectSetInteger(0, newLabelObjectName, OBJPROP_ANCHOR, ANCHOR_CENTER); //--- Задаём точку привязки подписи
   if (enableSwingValidation && showSwingPoints && swingShift > 0) { //--- Проверяем отображение свинговых точек
      string swingLabelName = currentBlockName + "_retest_swing"; //--- Формируем имя подписи свинговой точки
      if (ObjectFind(0, swingLabelName) < 0) { //--- Проверяем существование подписи свинговой точки
         datetime swingTime = time(swingShift); //--- Получаем время свинговой точки
         ObjectCreate(0, swingLabelName, OBJ_TEXT, 0, swingTime, swingPrice); //--- Создаём подпись свинговой точки
         string swingText = isForBullishBB ? "HH" : "LL"; //--- Задаём текст свинговой точки
         ObjectSetString(0, swingLabelName, OBJPROP_TEXT, swingText); //--- Задаём текст подписи свинговой точки
         ObjectSetInteger(0, swingLabelName, OBJPROP_COLOR, swingLabelColor); //--- Задаём цвет подписи свинговой точки
         ObjectSetInteger(0, swingLabelName, OBJPROP_FONTSIZE, swingFontSize); //--- Задаём размер шрифта подписи свинговой точки
         ObjectSetInteger(0, swingLabelName, OBJPROP_ANCHOR, isForBullishBB ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER); //--- Задаём точку привязки подписи свинговой точки
      }
   }
   ChartRedraw(0);                       //--- Перерисовываем график
   blockTypes[j] = isForBullishBB ? "BB-bullish" : "BB-bearish"; //--- Обновляем тип блока
   retestedStatus[j] = true;             //--- Устанавливаем статус ретеста
   blockLabels[j] = newLabelObjectName;  //--- Обновляем имя подписи
   Print("Converted to ", newLabel, ": ", currentBlockName); //--- Выводим в журнал преобразование блока
}

Наконец, реализуем торговую и графическую логику для брейкер-блоков, прошедших ретест. Если «retestCondition» и «validSwingForRetest» равны true и автоматическая торговля включена, открываем сделку. Для бычьего брейкер-блока получаем цену Ask через SymbolInfoDouble, рассчитываем Stop Loss ниже цены входа и Take Profit выше неё, после чего вызываем «obj_Trade.Buy». Для медвежьего блока используем цену Bid, размещаем Stop Loss выше, Take Profit ниже и вызываем «obj_Trade.Sell».

Затем изменяем внешний вид блока: для бычьего сценария устанавливаем цвет clrBlueViolet, для медвежьего — «clrOrange» через ObjectSetInteger. Старую подпись удаляем функцией ObjectDelete и создаём новый объект OBJ_TEXT с текстом «Bullish Breaker Block» или «Bearish Breaker Block» в центре блока. Если включено отображение свинговых точек, добавляем подпись «HH» или «LL» через ObjectCreate. В завершение обновляем «blockTypes» до «BB-bullish» или «BB-bearish», устанавливаем «retestedStatus» в true, сохраняем имя подписи и перерисовываем график. После компиляции получаем следующий результат.

ПОДТВЕРЖДЕННЫЕ БРЕЙКЕР-БЛОКИ

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

//+------------------------------------------------------------------+
//| Применение трейлинг-стопа к открытым позициям                    |
//+------------------------------------------------------------------+
void applyTrailingStop(double trailingPoints, CTrade &trade_object, int magicNo = 0) {
   double buyStopLoss = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - trailingPoints * _Point, _Digits); //--- Рассчитываем Stop Loss для покупки
   double sellStopLoss = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + trailingPoints * _Point, _Digits); //--- Рассчитываем Stop Loss для продажи
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Перебираем открытые позиции
      ulong ticket = PositionGetTicket(i);         //--- Получаем тикет позиции
      if (ticket > 0 && PositionGetString(POSITION_SYMBOL) == _Symbol && (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)) { //--- Проверяем позицию
         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)); //--- Обновляем Stop Loss для покупки
         } 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)); //--- Обновляем Stop Loss для продажи
         }
      }
   }
}

//--- Вызываем функцию на каждом тике

//+------------------------------------------------------------------+
//| Функция обработки тика советника                                 |
//+------------------------------------------------------------------+
void OnTick() {
   if (enableTrailingStop) {                      //--- Проверяем включён ли трейлинг-стоп
      applyTrailingStop(trailingStopPoints, obj_Trade, uniqueMagicNumber); //--- Применяем трейлинг-стоп
   }

   //--- 

}

Здесь реализуем трейлинг-стоп и подключаем его к событийной логике. Функция «applyTrailingStop» рассчитывает новый Stop Loss для покупки как текущую цену Bid (SymbolInfoDouble с SYMBOL_BID) минус «trailingPoints * _Point», а для продажи — как цену Ask плюс «trailingPoints * _Point». Оба значения нормализуются функцией NormalizeDouble с учётом количества знаков символа. Затем в обратном порядке перебираем открытые позиции через PositionsTotal, получаем тикет функцией PositionGetTicket и проверяем соответствие символу и значению «magicNo» с помощью PositionGetString и «PositionGetInteger».

Для позиции на покупку (POSITION_TYPE_BUY) новый «buyStopLoss» должен быть выше цены открытия (PositionGetDouble(POSITION_PRICE_OPEN)) и выше текущего Stop Loss либо применяться при его отсутствии. Для позиции на продажу «sellStopLoss» должен быть ниже цены открытия и ниже текущего Stop Loss либо применяться при его отсутствии. Изменение выполняется методом «trade_object.PositionModify». Затем в функции OnTick проверяем «enableTrailingStop» и на каждом тике вызываем «applyTrailingStop» с параметрами «trailingStopPoints», «obj_Trade» и «uniqueMagicNumber». После компиляции получаем следующий результат.

ТРЕЙЛИНГ-СТОП ВКЛЮЧЕН

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

СВОДНАЯ АНИМАЦИЯ БРЕЙКЕР-БЛОКОВ

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


Бэктестирование

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

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

ГРАФИК

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

ОТЧЁТ


Заключение

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

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

Эта стратегия позволяет находить возможности для торговли на ретестах брейкер-блоков и её можно дальше совершенствовать по мере развития вашей торговой практики. Успешной торговли!

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
hiteshdaoya
hiteshdaoya | 27 нояб. 2025 в 08:06
Здравствуйте,

. Я попробовал добавить общий файл, но он не отображается — то есть на графике ничего не происходит, и сделки не открываются. Прошу помочь с процессом исполнения.

Заранее благодарю.
Alexey Volchanskiy
Alexey Volchanskiy | 2 авг. 2026 в 20:13
Зачем публиковать тут разный отстой? Про стратегию можно сказать одно - псевдотрейдер зарабатывает клепанием статеек из серии:
— Если спросите, откуда эти сказки и легенды, я скажу вам, я отвечу — я их высосал из пальца!
1,6% совершенно случайного профита за 8 месяце, это конечно крутой результат!
Возможности Мастера MQL5, которые вам нужно знать (Часть 82): Использование паттернов TRIX и WPR в обучении с подкреплением DQN Возможности Мастера MQL5, которые вам нужно знать (Часть 82): Использование паттернов TRIX и WPR в обучении с подкреплением DQN
В предыдущей статье мы рассмотрели сочетание Ишимоку и ADX в рамках обучения на основе инференса. В этой статье мы вновь обращаемся к обучению с подкреплением применительно к паре индикаторов, которую уже рассматривали в части 68: TRIX и Williams Percent Range. В этой статье используем алгоритм алгоритм DQN с квантильной регрессией. Как обычно, представляем его в виде пользовательского класса сигнала, предназначенного для использования с Мастером MQL5.
Теория графов: Алгоритм Форда—Фалкерсона для оценки ликвидности и пропускной способности рынка Теория графов: Алгоритм Форда—Фалкерсона для оценки ликвидности и пропускной способности рынка
В статье представлен советник на MQL5, который адаптирует метод максимального потока Форда-Фалкерсона в фильтр ликвидности и пропускной способности. Рыночные структуры — максимумы/минимумы свингов, ценовой дисбаланс (FVG), ордер-блоки и пулы ликвидности — образуют ориентированный граф с пропускной способностью ребер, определяемой объемом, ценовой реакцией, расстоянием и качеством структуры. Максимальный поток позволяет оценить возможности ICT-сделок, отфильтровать слабые пути и использовать динамическое управление размером позиции для последовательного двухэтапного процесса принятия решений.
Анализ CSV-данных (Часть 6): Нормализация результатов по нескольким брокерам и сверка CSV между платформами Анализ CSV-данных (Часть 6): Нормализация результатов по нескольким брокерам и сверка CSV между платформами
В статье представлен фреймворк нормализации CSV-данных от нескольких брокеров. Подключаемый файл MQL5 дополняет экспорт брокерскими метаданными. Модуль Python устраняет расхождения схем — различия в разрядности котирования, соглашениях о пунктах, именовании символов, смещениях часовых поясов, моделях учёта комиссии и валютах номинирования счёта и прибыли — и формирует единый канонический набор данных. Сравнительные визуализации распределений проскальзывания и результатов после учёта издержек позволяют надёжно сопоставлять результаты стратегии между разными брокерскими средами без незаметного искажения данных.
От начального до среднего уровня: Очереди, списки и деревья (II) От начального до среднего уровня: Очереди, списки и деревья (II)
Это статья, которую вам, уважаемый читатель, следует внимательно изучить. Это связано с типом материала, который будет в нём изложен. Хотя мы постарались изложить материал максимально просто и познавательно, представленная здесь информация, несомненно, может показаться довольно сложной для тех, кто только начинает изучать программирование. Тем не менее, это не повод для уныния или игнорирования того, что здесь объясняется, так как эта статья установит связь между двумя совершенно разными, хотя и тесно связанными темами.