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

Если «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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Возможности Мастера MQL5, которые вам нужно знать (Часть 82): Использование паттернов TRIX и WPR в обучении с подкреплением DQN
Теория графов: Алгоритм Форда—Фалкерсона для оценки ликвидности и пропускной способности рынка
Анализ CSV-данных (Часть 6): Нормализация результатов по нескольким брокерам и сверка CSV между платформами
От начального до среднего уровня: Очереди, списки и деревья (II)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
. Я попробовал добавить общий файл, но он не отображается — то есть на графике ничего не происходит, и сделки не открываются. Прошу помочь с процессом исполнения.
Заранее благодарю.
— Если спросите, откуда эти сказки и легенды, я скажу вам, я отвечу — я их высосал из пальца!
1,6% совершенно случайного профита за 8 месяце, это конечно крутой результат!