English Deutsch 日本語
preview
Автоматизация торговых стратегий в MQL5 (Часть 46): Снятие ликвидности после пробоя структуры (BoS)

Автоматизация торговых стратегий в MQL5 (Часть 46): Снятие ликвидности после пробоя структуры (BoS)

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

Введение

В предыдущей статье (Часть 45) мы разработали на языке MetaQuotes Language 5 (MQL5) систему инвертированных разрывов справедливой стоимости (IFVG). Она обнаруживала разрывы с фильтрацией по минимальному размеру, отслеживала состояния Normal, Mitigated и Inverted (обычное, отработанное и инвертированное), игнорировала перекрытия, открывала сделки по сигналам инверсии с фиксированными уровнями Stop Loss, поддерживала различные режимы торговли и трейлинг-стоп, а также отображала прямоугольники с подписями и значками. В Части 46 мы разработаем систему снятия ликвидности при пробое структуры (Break of Structure, BoS).

Система обнаруживает свинговые максимумы и минимумы на заданном количестве баров и присваивает им метки HH, HL, LH и LL для определения пробоя структуры (BoS): HH указывает на бычий BoS, а LL — на медвежий. Снятие ликвидности фиксируется, когда тень свечи выходит за уровень соответствующего свинга, но бычья или медвежья свеча закрывается обратно в пределах этого уровня. При бычьем BoS система открывает покупки после снятия ликвидности под минимумом (Sell Side Liquidity, SSL), а при медвежьем — продажи после снятия ликвидности над максимумом (Buy Side Liquidity, BSL). Система использует динамические торговые уровни, включая Stop Loss и Take Profit; также задаётся максимальное число одновременно открытых позиций, а позиции противоположного направления закрываются. Сигналы визуализируются с помощью значков, подписей, прямоугольников, пунктирных линий и стрелок; размер шрифта динамически подстраивается под масштаб.

  1. Принцип стратегии снятия ликвидности при пробое структуры (BoS)
  2. Реализация на MQL5
  3. Тестирование
  4. Заключение

В результате мы получим работоспособную стратегию MQL5 для торговли на снятии ликвидности после BoS с визуализацией и средствами управления риском. Перейдём к рассмотрению.

Принцип стратегии снятия ликвидности при пробое структуры (BoS)

Стратегия снятия ликвидности при пробое структуры (BoS) сочетает определение тренда по точкам свинга с выявлением манипулятивных выходов цены за эти точки, при которых цена собирает ликвидность перед разворотом. Мы анализируем соседние бары, чтобы найти свинговые максимумы — значения выше соседей слева и справа — и свинговые минимумы — значения ниже соседей. Затем сравниваем их с предыдущими экстремумами: максимумы получают метки HH (более высокий максимум) или LH (более низкий максимум), а минимумы — HL (более высокий минимум) или LL (более низкий минимум). BoS формируется при появлении HH в восходящем тренде, что указывает на бычье продолжение, либо LL в нисходящем тренде, что указывает на медвежье продолжение. Снятие ликвидности происходит, когда тень свечи выходит за уровень соответствующего свинга — SSL ниже минимума при восходящем тренде или BSL выше максимума при нисходящем, — но бычья или медвежья свеча закрывается обратно в пределах этого уровня. Это указывает на срабатывание стоп-приказов перед истинным движением.

План работы следующий. Система будет находить свинги по заданному числу баров, и присваивать им метки HH, HL, LH и LL для определения направления BoS. Затем она будет выявлять снятие ликвидности по выходу тени за свинговый уровень и закрытию бычьей или медвежьей подтверждающей свечи обратно в пределах уровня. После SSL при бычьем BoS будут открываться покупки, а после BSL при медвежьем BoS — продажи. Советник также рассчитает динамические торговые уровни, включая Stop Loss и Take Profit, ограничит число одновременно открытых позиций, закроет позиции противоположного направления и отобразит на графике свинги, зоны снятия ликвидности, уровни BoS и стрелки входа. Снятие ликвидности можно использовать в рамках любых сетапов. Здесь выбрана стратегия пробоя структуры, поскольку она достаточно наглядна, однако при необходимости её можно заменить другим подходом, например сетапами дисбаланса.

СХЕМА СНЯТИЯ ЛИКВИДНОСТИ ПОСЛЕ BoS

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

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

//+------------------------------------------------------------------+
//|                                       BOS Liquidity Sweep EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

#include <Trade/Trade.mqh>

//+------------------------------------------------------------------+
//| Глобальные переменные                                                 |
//+------------------------------------------------------------------+
CTrade obj_Trade;                                                 //--- Торговый объект

//+------------------------------------------------------------------+
//| Входные параметры                                                 |
//+------------------------------------------------------------------+
input group "EA GENERAL SETTINGS"
input int    SwingLength       = 5;                               // Длина свинга в барах (проверка слева/справа)
input double LotSize           = 0.01;                            // Фиксированный размер лота
input double SL_Buffer_Pips    = 10.0;                            // Отступ SL в пунктах ниже/выше экстремума снятия ликвидности
input double RiskRewardRatio   = 2.0;                             // Множитель Take Profit (например, R:R 2:1)
input int    MaxTrades         = 1;                               // Максимальное число открытых позиций
input long   MagicNumber       = 12345;                           // Уникальный магический номер
input group "VISUALIZATION SETTINGS"
input color  clr_Bullish       = clrBlue;                         // Цвет бычьих меток (HH/HL)
input color  clr_Bearish       = clrRed;                          // Цвет медвежьих меток (LL/LH)
input color  clr_SSL_Rect      = clrLightBlue;                    // Цвет прямоугольника SSL
input color  clr_BSL_Rect      = clrLightCoral;                   // Цвет прямоугольника BSL
input color  clr_SSL_Line      = clrBlue;                         // Цвет линии SSL
input color  clr_BSL_Line      = clrRed;                          // Цвет линии BSL
input color  clr_BullBOS       = clrGreen;                        // Цвет линии бычьего BoS
input color  clr_BearBOS       = clrMaroon;                       // Цвет линии медвежьего BoS
input int    LineWidth         = 2;                               // Толщина линии
input bool   PrintLogs         = true;                            // Вывод сообщений

//+------------------------------------------------------------------+
//| Продолжение глобальных переменных                                       |
//+------------------------------------------------------------------+
static double   current_swing_high = -1.0, current_swing_low = -1.0; //--- Текущие свинговые максимум и минимум
static datetime swing_high_time = 0, swing_low_time = 0;          //--- Время свинговых максимума и минимума
int    MarketTrend = 0;                                           //--- Рыночный тренд (1: бычий BoS, -1: медвежий BoS, 0: нейтральный)
int    OpenTrades = 0;                                            //--- Число открытых позиций
int    current_font_size = 10;                                    //--- Текущий размер шрифта
int    object_code = 174;                                         //--- Код стрелки Wingdings для свингов
int    buy_arrow_code = 233;                                      //--- Стрелка Wingdings вверх для покупки
int    sell_arrow_code = 234;                                     //--- Стрелка Wingdings вниз для продажи
string ObjPrefix = "BOSLiqSweep_";                                //--- Префикс объектов

Сначала подключим торговую библиотеку директивой "#include <Trade/Trade.mqh>". Она предоставляет класс CTrade для управления ордерами и позициями. Создадим глобальный экземпляр CTrade с именем obj_Trade для выполнения торговых операций. В группе входных параметров "EA GENERAL SETTINGS" задаются: SwingLength — число баров слева и справа для проверки свинга; LotSize — фиксированный объём позиции; SL_Buffer_Pips — отступ Stop Loss ниже или выше экстремума снятия ликвидности; RiskRewardRatio — множитель для расчёта Take Profit; MaxTrades — максимальное число открытых позиций; MagicNumber — магический номер для идентификации сделок. В группе "VISUALIZATION SETTINGS" задаются цвета: clr_Bullish для HH/HL, clr_Bearish для LL/LH, clr_SSL_Rect и clr_BSL_Rect для прямоугольников SSL и BSL, clr_SSL_Line и clr_BSL_Line для линий SSL и BSL, clr_BullBOS и clr_BearBOS для бычьего и медвежьего BoS. Параметр LineWidth определяет толщину линий, а PrintLogs включает или отключает вывод сообщений в журнал.

ВХОДНЫЕ ПАРАМЕТРЫ

Далее объявляются дополнительные глобальные переменные. Статические переменные current_swing_high и current_swing_low, изначально равные -1.0, хранят последние свинговые максимум и минимум, а swing_high_time и swing_low_time — время их формирования. MarketTrend принимает значение 1 при бычьем BoS, -1 при медвежьем и 0 при нейтральном состоянии. OpenTrades хранит число открытых позиций, current_font_size — текущий размер шрифта, object_code со значением 174 — код значка Wingdings для свингов, buy_arrow_code и sell_arrow_code со значениями 233 и 234 — коды стрелок покупки и продажи. Строка ObjPrefix со значением "BOSLiqSweep_" используется как префикс имён графических объектов. Перед инициализацией основной логики определим вспомогательные функции, включая удаление ранее созданных объектов для очистки графика.

//+------------------------------------------------------------------+
//| Обновление размеров шрифта                                                |
//+------------------------------------------------------------------+
void UpdateFontSizes() {
   long scale = 0;                                                //--- Инициализация масштаба
   if (ChartGetInteger(0, CHART_SCALE, 0, scale)) {               //--- Получение масштаба
      current_font_size = (int)(7 + scale * 0.7);                 //--- Расчёт размера шрифта
      if (current_font_size < 6) current_font_size = 6;           //--- Установка минимального размера шрифта
      if (current_font_size > 15) current_font_size = 15;         //--- Установка максимального размера шрифта
      for (int i = ObjectsTotal(0, -1, -1) - 1; i >= 0; i--) {    //--- Обратный перебор объектов
         string name = ObjectName(0, i, -1, -1);                  //--- Получение имени объекта
         long type = ObjectGetInteger(0, name, OBJPROP_TYPE);     //--- Получение типа объекта
         if (type == OBJ_TEXT) {                                  //--- Проверка текстового типа
            ObjectSetInteger(0, name, OBJPROP_FONTSIZE, current_font_size); //--- Установка размера шрифта
         }
      }
      ChartRedraw(0);                                             //--- Перерисовка графика
   }
}

//+------------------------------------------------------------------+
//| Удаление объектов по префиксу                                         |
//+------------------------------------------------------------------+
void DeleteObjectsByPrefix(string prefix) {
   int total = ObjectsTotal(0, 0, -1);                            //--- Получение общего числа объектов
   for (int i = total - 1; i >= 0; i--) {                         //--- Обратный перебор
      string name = ObjectName(0, i, 0, -1);                      //--- Получение имени
      if (StringFind(name, prefix) == 0) {                        //--- Проверка префикса
         ObjectDelete(0, name);                                   //--- Удаление объекта
      }
   }
}

//+------------------------------------------------------------------+
//| Подсчёт открытых позиций                                                |
//+------------------------------------------------------------------+
int CountOpenTrades() {
   int count = 0;                                                 //--- Инициализация счётчика
   for (int i = PositionsTotal() - 1; i >= 0; i--) {              //--- Обратный перебор
      ulong ticket = PositionGetTicket(i);                        //--- Получение тикета
      if (PositionSelectByTicket(ticket)) {                       //--- Выбор позиции
         if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) { //--- Проверка символа и магического номера
            count++;                                              //--- Увеличение счётчика
         }
      }
   }
   return count;                                                  //--- Возврат счётчика
}

Функция UpdateFontSizes динамически изменяет размер текстовых объектов в зависимости от текущего масштаба графика. Переменная scale инициализируется нулём, после чего функция ChartGetInteger получает значение CHART_SCALE. При успешном вызове current_font_size рассчитывается как 7 плюс 70% значения масштаба и ограничивается диапазоном от 6 до 15. Затем программа в обратном порядке перебирает все объекты графика с помощью ObjectsTotal, получает их имена через ObjectName и типы через ObjectGetInteger с OBJPROP_TYPE. Для объектов OBJ_TEXT размер шрифта устанавливается функцией ObjectSetInteger с параметром OBJPROP_FONTSIZE, после чего график перерисовывается.

Функция DeleteObjectsByPrefix удаляет с графика все объекты, имена которых начинаются с заданного префикса. Она получает общее число объектов, перебирает их в обратном порядке, извлекает имя каждого объекта и проверяет через StringFind, находится ли префикс в позиции 0. Совпавшие объекты удаляются функцией ObjectDelete. Функция CountOpenTrades подсчитывает открытые позиции, принадлежащие этому советнику. Она перебирает позиции из PositionsTotal, получает тикет через PositionGetTicket и выбирает позицию функцией PositionSelectByTicket. Если символ позиции совпадает с _Symbol, а POSITION_MAGIC — с MagicNumber, счётчик увеличивается. После завершения обхода функция возвращает число позиций. Далее определим дополнительные функции визуализации.

//+------------------------------------------------------------------+
//| Отрисовка точки свинга с подписью                                      |
//+------------------------------------------------------------------+
void DrawSwingPoint(string objName, datetime time, double price, int arrCode, color clr, int direction, string label) {
   UpdateFontSizes();                                             //--- Обновление размеров шрифта
   objName = ObjPrefix + label + TimeToString(time);              //--- Задание имени объекта
   if (ObjectFind(0, objName) < 0) {                              //--- Проверка отсутствия объекта
      string iconName = objName + "_icon";                        //--- Имя значка
      ObjectCreate(0, iconName, OBJ_TEXT, 0, time, price);        //--- Создание значка
      ObjectSetString(0, iconName, OBJPROP_FONT, "Wingdings");    //--- Установка шрифта
      ObjectSetInteger(0, iconName, OBJPROP_FONTSIZE, current_font_size); //--- Установка размера шрифта
      ObjectSetString(0, iconName, OBJPROP_TEXT, CharToString((uchar)arrCode)); //--- Установка текста
      ObjectSetInteger(0, iconName, OBJPROP_COLOR, clr);          //--- Установка цвета
      ObjectSetInteger(0, iconName, OBJPROP_ANCHOR, ANCHOR_RIGHT); //--- Установка привязки
      string txtName = objName + "_txt";                          //--- Имя текста
      ObjectCreate(0, txtName, OBJ_TEXT, 0, time, price);         //--- Создание текста
      ObjectSetString(0, txtName, OBJPROP_FONT, "Arial");         //--- Установка шрифта
      ObjectSetInteger(0, txtName, OBJPROP_COLOR, clr);           //--- Установка цвета
      ObjectSetInteger(0, txtName, OBJPROP_FONTSIZE, current_font_size); //--- Установка размера шрифта
      ObjectSetInteger(0, txtName, OBJPROP_ANCHOR, ANCHOR_LEFT);  //--- Установка привязки
      ObjectSetString(0, txtName, OBJPROP_TEXT, label);           //--- Установка текста
   }
   ChartRedraw(0);                                                //--- Перерисовка графика
}

//+------------------------------------------------------------------+
//| Отрисовка прямоугольника снятия ликвидности (без текста)                                   |
//+------------------------------------------------------------------+
void DrawSweepRectangle(string objName, datetime time, double level, double extremum, color clr, bool is_ssl) {
   UpdateFontSizes();                                             //--- Обновление размеров шрифта
   objName = ObjPrefix + objName + TimeToString(time, TIME_SECONDS); //--- Задание имени объекта
   if (ObjectFind(0, objName) < 0) {                              //--- Проверка отсутствия объекта
      double top = MathMax(level, extremum);                      //--- Расчёт верхней границы
      double bottom = MathMin(level, extremum);                   //--- Расчёт нижней границы
      datetime end_time = time + PeriodSeconds(_Period);          //--- Расчёт времени окончания
      ObjectCreate(0, objName, OBJ_RECTANGLE, 0, time, top, end_time, bottom); //--- Создание прямоугольника
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);           //--- Установка цвета
      ObjectSetInteger(0, objName, OBJPROP_BACK, true);           //--- Размещение на заднем плане
      ObjectSetInteger(0, objName, OBJPROP_FILL, true);           //--- Включение заливки
      ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_SOLID);   //--- Установка стиля
      // Без текста внутри прямоугольника, чтобы не перегружать график
   }
   ChartRedraw(0);                                                //--- Перерисовка графика
}

//+------------------------------------------------------------------+
//| Отрисовка горизонтального пунктирного уровня пробоя                               |
//+------------------------------------------------------------------+
void DrawBreakLevel(string objName, datetime time1, double price, datetime time2, double price2, color clr, int direction, string label) {
   UpdateFontSizes();                                             //--- Обновление размеров шрифта
   objName = ObjPrefix + objName + label + TimeToString(time2, TIME_SECONDS); //--- Задание имени объекта
   if (ObjectFind(0, objName) < 0) {                              //--- Проверка отсутствия объекта
      ObjectCreate(0, objName, OBJ_TREND, 0, time1, price, time2, price); //--- Создание трендовой линии
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);           //--- Установка цвета
      ObjectSetInteger(0, objName, OBJPROP_WIDTH, LineWidth);     //--- Установка толщины
      ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DASH);    //--- Установка стиля
      ObjectSetInteger(0, objName, OBJPROP_RAY_RIGHT, false);     //--- Отключение луча вправо
      string txt = label + " Sweep";                              //--- Установка текста
      string txtName = objName + "_txt";                          //--- Имя текста
      ObjectCreate(0, txtName, OBJ_TEXT, 0, time2, price);        //--- Создание текста
      ObjectSetInteger(0, txtName, OBJPROP_COLOR, clr);           //--- Установка цвета
      ObjectSetInteger(0, txtName, OBJPROP_FONTSIZE, current_font_size); //--- Установка размера шрифта
      if (direction > 0) {                                        //--- Проверка положительного направления
         ObjectSetInteger(0, txtName, OBJPROP_ANCHOR, ANCHOR_RIGHT_UPPER); //--- Установка привязки
         ObjectSetString(0, txtName, OBJPROP_TEXT, " " + txt);    //--- Установка текста
      } else {                                                    //--- Отрицательное направление
         ObjectSetInteger(0, txtName, OBJPROP_ANCHOR, ANCHOR_RIGHT_LOWER); //--- Установка привязки
         ObjectSetString(0, txtName, OBJPROP_TEXT, " " + txt);    //--- Установка текста
      }
   }
   ChartRedraw(0);                                                //--- Перерисовка графика
}

//+------------------------------------------------------------------+
//| Отрисовка стрелки входа Wingdings                                  |
//+------------------------------------------------------------------+
void DrawEntryArrow(datetime time, double price, bool is_buy) {
   UpdateFontSizes();                                             //--- Обновление размеров шрифта
   string objName = ObjPrefix + "Entry_" + TimeToString(time, TIME_SECONDS); //--- Задание имени объекта
   if (ObjectFind(0, objName) < 0) {                              //--- Проверка отсутствия объекта
      int arrCode = is_buy ? buy_arrow_code : sell_arrow_code;    //--- Задание кода стрелки
      color arrow_color = is_buy ? clrBlue : clrRed;              //--- Установка цвета
      ObjectCreate(0, objName, OBJ_TEXT, 0, time, price);         //--- Создание текста
      ObjectSetString(0, objName, OBJPROP_FONT, "Wingdings");     //--- Установка шрифта
      ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, current_font_size); //--- Установка размера шрифта
      ObjectSetString(0, objName, OBJPROP_TEXT, CharToString((uchar)arrCode)); //--- Установка текста
      ObjectSetInteger(0, objName, OBJPROP_COLOR, arrow_color);   //--- Установка цвета
      ObjectSetInteger(0, objName, OBJPROP_ANCHOR, is_buy ? ANCHOR_UPPER : ANCHOR_LOWER); //--- Установка привязки
   }
   ChartRedraw(0);                                                //--- Перерисовка графика
}

Функция DrawSwingPoint отображает найденную точку свинга с помощью значка и подписи. Сначала вызывается UpdateFontSizes, затем формируется уникальное имя объекта из ObjPrefix, метки и времени. Если объект ещё не существует, создаётся объект OBJ_TEXT со значком Wingdings и суффиксом "_icon". Для него задаются шрифт Wingdings, текущий размер, символ из arrCode, цвет и правое выравнивание. После этого создаётся вторая текстовая метка с суффиксом "_txt", шрифтом Arial, теми же цветом и размером, левым выравниванием и текстом label. В завершение график перерисовывается функцией ChartRedraw.

Функция DrawSweepRectangle рисует заполненный прямоугольник зоны снятия ликвидности без текста внутри, чтобы не перегружать график. После вызова UpdateFontSizes формируется имя из ObjPrefix и времени с точностью до секунд. Если объект отсутствует, верхняя и нижняя границы вычисляются через MathMax и MathMin, а время окончания — как время сигнала плюс длительность одного бара. Затем создаётся OBJ_RECTANGLE, задаются цвет, размещение на заднем плане, заливка и сплошной стиль. Функция DrawBreakLevel отмечает пробой BoS горизонтальной пунктирной линией и подписью. Она создаёт объект OBJ_TREND между двумя моментами времени на одном ценовом уровне, задаёт цвет, толщину, пунктирный стиль и отключает продолжение луча вправо. Текстовая подпись размещается у второй точки: при положительном направлении используется привязка справа сверху, при отрицательном — справа снизу. Текст подписи формируется как label + " Sweep" и размещается у второй точки линии.

Функция DrawEntryArrow размещает на графике стрелку Wingdings, обозначающую вход в позицию. После обновления размера шрифта формируется имя из ObjPrefix, строки "Entry_" и времени с точностью до секунд. Если объект отсутствует, для покупки выбирается buy_arrow_code и синий цвет, а для продажи — sell_arrow_code и красный цвет. Затем создаётся OBJ_TEXT, задаются шрифт Wingdings, размер, символ, цвет и привязка: сверху для покупки и снизу для продажи. После этого график перерисовывается. Теперь можно перейти к инициализации, очистке графика и проверке входных параметров.

//+------------------------------------------------------------------+
//| Функция инициализации советника                                   |
//+------------------------------------------------------------------+
int OnInit() {
   obj_Trade.SetExpertMagicNumber(MagicNumber);                   //--- Задание магического номера торговому объекту
   if (SwingLength < 1 || LotSize <= 0 || SL_Buffer_Pips < 0 || RiskRewardRatio < 1.0 || MaxTrades < 1) { //--- Проверка некорректных параметров
      Print("Invalid input parameters.");                         //--- Вывод сообщения о некорректных параметрах
      return(INIT_PARAMETERS_INCORRECT);                          //--- Возврат ошибки параметров
   }
   DeleteObjectsByPrefix(ObjPrefix);                              //--- Удаление объектов по префиксу
   UpdateFontSizes();                                             //--- Обновление размеров шрифта
   Print("EA Initialized Successfully.");                         //--- Вывод сообщения об успешной инициализации
   return(INIT_SUCCEEDED);                                        //--- Возврат успешного результата
}

//+------------------------------------------------------------------+
//| Функция деинициализации советника                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   DeleteObjectsByPrefix(ObjPrefix);                              //--- Удаление объектов по префиксу
   Print("EA Deinitialized.");                                    //--- Вывод сообщения о деинициализации
}

Обработчик OnInit выполняется при добавлении советника на график или его загрузке. С помощью SetExpertMagicNumber он передаёт MagicNumber объекту obj_Trade для идентификации позиций. Затем проверяются основные параметры. Если SwingLength меньше 1, LotSize не положителен, SL_Buffer_Pips отрицателен, RiskRewardRatio меньше 1.0 или MaxTrades меньше 1, в журнал выводится сообщение "Invalid input parameters", а функция возвращает INIT_PARAMETERS_INCORRECT. При корректных параметрах DeleteObjectsByPrefix удаляет прежние графические объекты, UpdateFontSizes задаёт исходный размер текста, в журнал выводится "EA Initialized Successfully", после чего возвращается INIT_SUCCEEDED. Обработчик OnDeinit вызывается при удалении советника или закрытии терминала и удаляет все объекты с нашим префиксом. Далее перейдём к логике OnTick, начав с обнаружения свингов и пробоев структуры.

//+------------------------------------------------------------------+
//| Обнаружение свингов и BoS                                            |
//+------------------------------------------------------------------+
void DetectSwingsAndBOS() {
   int curr_bar = SwingLength;                                    //--- Задание текущего бара
   bool isSwingHigh = true, isSwingLow = true;                    //--- Инициализация признаков свинга
   for (int j = 1; j <= SwingLength; j++) {                       //--- Перебор заданной длины
      int right_index = curr_bar - j;                             //--- Расчёт правого индекса (более новый бар)
      int left_index = curr_bar + j;                              //--- Расчёт левого индекса (более старый бар)
      if (iHigh(_Symbol, _Period, curr_bar) <= iHigh(_Symbol, _Period, right_index) || iHigh(_Symbol, _Period, curr_bar) < iHigh(_Symbol, _Period, left_index)) { //--- Проверка, что это не максимум
         isSwingHigh = false;                                     //--- Сброс признака максимума
      }
      if (iLow(_Symbol, _Period, curr_bar) >= iLow(_Symbol, _Period, right_index) || iLow(_Symbol, _Period, curr_bar) > iLow(_Symbol, _Period, left_index)) { //--- Проверка, что это не минимум
         isSwingLow = false;                                      //--- Сброс признака минимума
      }
   }
   if (isSwingHigh) {                                             //--- Проверка свингового максимума
      double new_high = iHigh(_Symbol, _Period, curr_bar);        //--- Получение нового максимума
      string label = "H";                                         //--- Инициализация метки
      color clr = clr_Bullish;                                    //--- Установка цвета
      if (current_swing_high > 0) {                               //--- Проверка существующего максимума
         if (new_high > current_swing_high) {                     //--- Проверка более высокого значения
            label = "HH";                                         //--- Задание HH
            MarketTrend = 1;                                      //--- Задание бычьего тренда
            if (PrintLogs) Print("Bullish BOS Detected");         //--- Вывод сообщения о бычьем BoS
            datetime break_time = FindBreakTime(swing_high_time, current_swing_high, true); //--- Поиск времени пробоя
            if (break_time > 0) DrawBreakLevel("Bull_BOS_", swing_high_time, current_swing_high, break_time, current_swing_high, clr_BullBOS, -1, "Bullish BOS"); //--- Отрисовка BoS
         } else {                                                 //--- Более низкое значение
            label = "LH";                                         //--- Задание LH
            clr = clr_Bearish;                                    //--- Задание медвежьего цвета
         }
      }
      if (PrintLogs) Print("SWING HIGH @ BAR INDEX ", curr_bar, " of High: ", new_high, " Label: ", label); //--- Вывод данных о максимуме
      DrawSwingPoint(TimeToString(iTime(_Symbol, _Period, curr_bar)), iTime(_Symbol, _Period, curr_bar), new_high, object_code, clr, -1, label); //--- Отрисовка точки максимума
      current_swing_high = new_high;                              //--- Обновление максимума
      swing_high_time = iTime(_Symbol, _Period, curr_bar);        //--- Обновление времени максимума
   }
   if (isSwingLow) {                                              //--- Проверка свингового минимума
      double new_low = iLow(_Symbol, _Period, curr_bar);          //--- Получение нового минимума
      string label = "L";                                         //--- Инициализация метки
      color clr = clr_Bearish;                                    //--- Установка цвета
      if (current_swing_low > 0) {                                //--- Проверка существующего минимума
         if (new_low < current_swing_low) {                       //--- Проверка более низкого значения
            label = "LL";                                         //--- Задание LL
            MarketTrend = -1;                                     //--- Задание медвежьего тренда
            if (PrintLogs) Print("Bearish BOS Detected");         //--- Вывод сообщения о медвежьем BoS
            datetime break_time = FindBreakTime(swing_low_time, current_swing_low, false); //--- Поиск времени пробоя
            if (break_time > 0) DrawBreakLevel("Bear_BOS_", swing_low_time, current_swing_low, break_time, current_swing_low, clr_BearBOS, 1, "Bearish BOS"); //--- Отрисовка BoS
         } else {                                                 //--- Более высокое значение
            label = "HL";                                         //--- Задание HL
            clr = clr_Bullish;                                    //--- Задание бычьего цвета
         }
      }
      if (PrintLogs) Print("SWING LOW @ BAR INDEX ", curr_bar, " of Low: ", new_low, " Label: ", label); //--- Вывод данных о минимуме
      DrawSwingPoint(TimeToString(iTime(_Symbol, _Period, curr_bar)), iTime(_Symbol, _Period, curr_bar), new_low, object_code, clr, 1, label); //--- Отрисовка точки минимума
      current_swing_low = new_low;                                //--- Обновление минимума
      swing_low_time = iTime(_Symbol, _Period, curr_bar);         //--- Обновление времени минимума
   }
}

//+------------------------------------------------------------------+
//| Поиск времени свечи пробоя (по закрытию)                          |
//+------------------------------------------------------------------+
datetime FindBreakTime(datetime prev_time, double prev_level, bool is_high_break) {
   int prev_shift = iBarShift(_Symbol, _Period, prev_time);       //--- Получение предыдущего сдвига
   if (prev_shift < 0) return 0;                                  //--- Возврат некорректного значения
   for (int i = prev_shift - 1; i >= 0; i--) {                    //--- Обратный перебор
      if (is_high_break) {                                        //--- Проверка пробоя максимума
         if (iClose(_Symbol, _Period, i) > prev_level) return iTime(_Symbol, _Period, i); //--- Возврат времени при пробое
      } else {                                                    //--- Пробой минимума
         if (iClose(_Symbol, _Period, i) < prev_level) return iTime(_Symbol, _Period, i); //--- Возврат времени при пробое
      }
   }
   return 0;                                                      //--- Возврат при отсутствии пробоя
}

Функция DetectSwingsAndBOS обнаруживает точки свинга и пробои структуры после формирования каждого нового бара, обновляя направление тренда и графические объекты. Целевой бар curr_bar получает значение SwingLength, а признаки isSwingHigh и isSwingLow изначально устанавливаются в true. Затем цикл от 1 до SwingLength вычисляет для каждого j индексы right_index = curr_bar - j для более новых баров и left_index = curr_bar + j для более старых. Если максимум целевого бара не выше обоих соседних максимумов, isSwingHigh становится false. Аналогично, если минимум целевого бара не ниже обоих соседних минимумов, isSwingLow становится false.

Если isSwingHigh остаётся true, максимум сохраняется в new_high, начальная метка задаётся как "H", а цвет — clr_Bullish. При наличии предыдущего current_swing_high значения сравниваются. Если новый максимум выше предыдущего, используется метка "HH", MarketTrend устанавливается в 1, а при включённом PrintLogs в журнал выводится "Bullish BOS Detected". Затем FindBreakTime определяет время пробоя по swing_high_time и current_swing_high. Если время корректно, DrawBreakLevel рисует бычий BoS с префиксом "Bull_BOS_", цветом clr_BullBOS, направлением -1 и подписью "Bullish BOS".

Если новый максимум ниже предыдущего, используется метка "LH" и цвет clr_Bearish. При включённом журналировании выводится информация о свинге, после чего DrawSwingPoint отображает точку, а current_swing_high и swing_high_time обновляются. Для свингового минимума применяется зеркальная логика: более низкий минимум получает метку "LL" и задаёт медвежье направление, а более высокий минимум — метку "HL" и бычий цвет. Функция FindBreakTime ищет первый бар, закрывшийся за предыдущим свинговым уровнем. Она получает индекс prev_time через iBarShift и возвращает 0 при некорректном значении. Затем перебираются бары от prev_shift - 1 до 0: при пробое максимума возвращается время первого закрытия выше prev_level, при пробое минимума — ниже него. Если пробой не найден, возвращается 0. Теперь вызовем обнаружение после формирования каждого нового бара.

//+------------------------------------------------------------------+
//| Тиковая функция советника                                             |
//+------------------------------------------------------------------+
void OnTick() {
   static bool isNewBar = false;                                  //--- Признак нового бара
   int currBars = iBars(_Symbol, _Period);                        //--- Получение текущего числа баров
   static int prevBars = currBars;                                //--- Предыдущее число баров
   if (prevBars == currBars) {                                    //--- Проверка неизменного числа баров
      isNewBar = false;                                           //--- Сброс признака нового бара
   } else if (prevBars != currBars) {                             //--- Проверка новых баров
      isNewBar = true;                                            //--- Установка признака нового бара
      prevBars = currBars;                                        //--- Обновление предыдущего числа баров
   }
   if (!isNewBar) return;                                         //--- Выход при отсутствии нового бара
   OpenTrades = CountOpenTrades();                                //--- Подсчёт открытых позиций
   if (OpenTrades >= MaxTrades) return;                           //--- Выход при достижении максимума позиций
   DetectSwingsAndBOS();                                          //--- Обнаружение свингов и BoS
}

Обработчик OnTick выполняется на каждом тике, но основная логика запускается только при появлении нового бара. Статические переменные isNewBar и prevBars используются для отслеживания изменений числа баров. Текущее число баров currBars получается функцией iBars. Если оно не изменилось, isNewBar устанавливается в false; если увеличилось — в true, а prevBars обновляется. При отсутствии нового бара функция завершается. В противном случае OpenTrades обновляется вызовом CountOpenTrades. Если достигнуто значение MaxTrades, новые входы не рассматриваются. Затем вызывается DetectSwingsAndBOS для поиска свингов и пробоев структуры. После компиляции получаем следующий результат.

Медвежий сценарий снятия ликвидности:

МЕДВЕЖИЙ СЦЕНАРИЙ СНЯТИЯ ЛИКВИДНОСТИ

Бычий сценарий снятия ликвидности:

БЫЧИЙ СЦЕНАРИЙ СНЯТИЯ ЛИКВИДНОСТИ

После завершения обнаружения необходимо реализовать открытие сделок по сигналам снятия ликвидности. Для модульности вынесем эту логику в отдельную функцию.

//+------------------------------------------------------------------+
//| Обнаружение и торговля снятием ликвидности после BoS                                    |
//+------------------------------------------------------------------+
void DetectAndTradeSweepOnBOS() {
   if (MarketTrend == 0) return;                                  //--- Выход при нейтральном состоянии
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);            //--- Получение Bid
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);            //--- Получение Ask
   // Бычий BoS + снятие SSL для покупки
   if (MarketTrend == 1 && current_swing_low > 0.0 && iLow(_Symbol, _Period, 1) < current_swing_low && iClose(_Symbol, _Period, 1) > current_swing_low && iClose(_Symbol, _Period, 1) > iOpen(_Symbol, _Period, 1)) { //--- Проверка бычьего снятия ликвидности
      if (PrintLogs) Print("Bullish BOS + SSL Sweep Detected");   //--- Вывод сообщения о снятии ликвидности
      double sweep_low = iLow(_Symbol, _Period, 1);               //--- Получение минимума снятия
      datetime sweep_time = iTime(_Symbol, _Period, 1);           //--- Получение времени снятия
      DrawSweepRectangle("SSL_Rect_", sweep_time, current_swing_low, sweep_low, clr_SSL_Rect, true); //--- Отрисовка прямоугольника SSL
      DrawBreakLevel("SSL_Line_", swing_low_time, current_swing_low, sweep_time, current_swing_low, clr_SSL_Line, 1, "SSL"); //--- Отрисовка линии SSL
      CloseOpposite(true);                                        //--- Закрытие противоположных позиций
      double sl = NormalizeDouble(sweep_low - SL_Buffer_Pips * _Point, _Digits); //--- Расчёт SL
      double entry = ask;                                         //--- Задание цены входа
      double risk = entry - sl;                                   //--- Расчёт риска
      double tp = NormalizeDouble(entry + risk * RiskRewardRatio, _Digits); //--- Расчёт TP
      obj_Trade.Buy(LotSize, _Symbol, entry, sl, tp, "BOS SSL Buy"); //--- Открытие покупки
      if (obj_Trade.ResultRetcode() == TRADE_RETCODE_DONE) {      //--- Проверка успешного выполнения
         DrawEntryArrow(sweep_time, iLow(_Symbol,_Period, 1), true);                 //--- Отрисовка стрелки покупки
         MarketTrend = 0;                                         //--- Сброс направления
      } else Print("Buy order failed: ", obj_Trade.ResultRetcodeDescription()); //--- Вывод сообщения об ошибке
   }
   // Медвежий BoS + снятие BSL для продажи
   if (MarketTrend == -1 && current_swing_high > 0.0 && iHigh(_Symbol, _Period, 1) > current_swing_high && iClose(_Symbol, _Period, 1) < current_swing_high && iClose(_Symbol, _Period, 1) < iOpen(_Symbol, _Period, 1)) { //--- Проверка медвежьего снятия ликвидности
      if (PrintLogs) Print("Bearish BOS + BSL Sweep Detected");   //--- Вывод сообщения о снятии ликвидности
      double sweep_high = iHigh(_Symbol, _Period, 1);             //--- Получение максимума снятия
      datetime sweep_time = iTime(_Symbol, _Period, 1);           //--- Получение времени снятия
      DrawSweepRectangle("BSL_Rect_", sweep_time, current_swing_high, sweep_high, clr_BSL_Rect, false); //--- Отрисовка прямоугольника BSL
      DrawBreakLevel("BSL_Line_", swing_high_time, current_swing_high, sweep_time, current_swing_high, clr_BSL_Line, -1, "BSL"); //--- Отрисовка линии BSL
      CloseOpposite(false);                                       //--- Закрытие противоположных позиций
      double sl = NormalizeDouble(sweep_high + SL_Buffer_Pips * _Point, _Digits); //--- Расчёт SL
      double entry = bid;                                         //--- Задание цены входа
      double risk = sl - entry;                                   //--- Расчёт риска
      double tp = NormalizeDouble(entry - risk * RiskRewardRatio, _Digits); //--- Расчёт TP
      obj_Trade.Sell(LotSize, _Symbol, entry, sl, tp, "BOS BSL Sell"); //--- Открытие продажи
      if (obj_Trade.ResultRetcode() == TRADE_RETCODE_DONE) {      //--- Проверка успешного выполнения
         DrawEntryArrow(sweep_time, iHigh(_Symbol,_Period,1), false);                //--- Отрисовка стрелки продажи
         MarketTrend = 0;                                         //--- Сброс направления
      } else Print("Sell order failed: ", obj_Trade.ResultRetcodeDescription()); //--- Вывод сообщения об ошибке
   }
}

Функция DetectAndTradeSweepOnBOS обнаруживает снятие ликвидности после пробоя структуры, выполняет сделки, обновляет визуализацию и управляет существующими позициями. Если MarketTrend равен 0, функция сразу завершается, поскольку активного BoS нет. Текущие цены Bid и Ask получаются через SymbolInfoDouble с SYMBOL_BID и SYMBOL_ASK. При бычьем BoS (MarketTrend == 1) и корректном current_swing_low проверяется снятие SSL: минимум предыдущего бара должен опуститься ниже current_swing_low, а закрытие — оказаться выше этого уровня и выше цены открытия. Это подтверждает бычью свечу, которая собрала стоп-приказы продавцов. При обнаружении сигнала в журнал выводится "Bullish BOS + SSL Sweep Detected", сохраняются минимум и время свечи, а DrawSweepRectangle рисует прямоугольник SSL с префиксом "SSL_Rect_" и цветом clr_SSL_Rect. В бычьем сценарии после вызова DrawSweepRectangle функция DrawBreakLevel рисует линию SSL. Затем CloseOpposite(true) закрывает противоположные продажи, рассчитываются Stop Loss, цена входа, риск и Take Profit, после чего obj_Trade.Buy открывает покупку. При успешном открытии рисуется стрелка входа, а MarketTrend сбрасывается в 0.

Для медвежьего BoS (MarketTrend == -1) и корректного current_swing_high применяется зеркальная логика: максимум предыдущего бара должен превысить current_swing_high, а закрытие — оказаться ниже этого уровня и ниже цены открытия, подтверждая медвежью свечу. В журнал выводится "Bearish BOS + BSL Sweep Detected", сохраняются максимум и время свечи, рисуются прямоугольник BSL и линия уровня с подписью "BSL". Вызов CloseOpposite(false) закрывает покупки. Stop Loss устанавливается выше максимума снятия ликвидности с учётом буфера, цена входа равна Bid, риск определяется как разность между Stop Loss и входом, а Take Profit — как цена входа минус риск, умноженный на RiskRewardRatio. Затем obj_Trade.Sell открывает продажу с комментарием "BOS BSL Sell". При успешном выполнении рисуется стрелка продажи и MarketTrend сбрасывается в 0; при ошибке выводится описание результата. После добавления вызова функции в OnTick получаем следующий результат.

ТЕСТ СИСТЕМЫ СНЯТИЯ ЛИКВИДНОСТИ ПОСЛЕ BoS

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

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

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

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

ГРАФИК

Отчёт тестера стратегий:

ОТЧЁТ

Заключение

В результате мы разработали на MQL5 систему снятия ликвидности при пробое структуры (BoS). Она обнаруживает свинги на заданном количестве баров и присваивает точкам метки для определения тренда. Снятие ликвидности фиксируется, когда тень свечи выходит за уровень соответствующего свинга, а бычья или медвежья свеча закрывается обратно в пределах этого уровня. При бычьем BoS система открывает покупки после снятия ликвидности со стороны продавцов (SSL), а при медвежьем — продажи после снятия ликвидности со стороны покупателей (BSL). Она использует динамические торговые уровни, включая Stop Loss и Take Profit, ограничивает число открытых позиций и закрывает противоположные позиции. Визуализация включает значки свингов, пунктирные линии пробоев, заполненные прямоугольники зон снятия ликвидности, стрелки входов и адаптивный размер шрифта при изменении масштаба.

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

Стратегия снятия ликвидности после пробоя структуры (BoS) сочетает определение тренда по точкам свинга с поиском ложных выходов цены за эти уровни, при которых собирается ликвидность перед последующим движением. Мы анализируем соседние бары и определяем свинговые максимумы и минимумы. Затем сравниваем их с предыдущими экстремумами: максимумы получают метки HH или LH, а минимумы — HL или LL. HH формирует бычий BoS, а LL — медвежий. Снятие ликвидности происходит, когда тень свечи выходит за уровень соответствующего свинга, но цена закрывается обратно внутрь уровня: выше минимума для SSL при бычьем BoS и ниже максимума для BSL при медвежьем BoS.

Эта стратегия Liquidity Sweep на основе Break of Structure позволяет выявлять манипулятивные выносы после BoS и использовать их для торговли разворотных сетапов с возможностью дальнейшей оптимизации.

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

Прикрепленные файлы |
Liquidity_Sweep.mq5 (55.03 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (5)
jumah55
jumah55 | 14 дек. 2025 в 15:01
Приложенный файл не работает в MT5
Allan Munene Mutiiria
Allan Munene Mutiiria | 15 дек. 2025 в 19:41
jumah55 #:
Прикрепленный файл не работает в MT5
Появляются какие-то ошибки или предупреждения, или как именно не работает?
ersan yurdakul
ersan yurdakul | 20 дек. 2025 в 20:56
Этот код вызывает перерисовку?
Emile Munro
Emile Munro | 2 янв. 2026 в 10:14

Привет, Аллан, с Новым годом! Спасибо за статью — она помогает мне разобраться, как написать советник Multi Timeframe BOS, следуя твоим инструкциям и вводя код по ходу работы, а не просто копируя и вставляя его. Однако сейчас я столкнулся с ошибкой в разделе кода «Detect Swings and BOS»: при компиляции появляется ошибка «необъявленный идентификатор» в строке кода «FindBreakTime». Как вам удалось заставить его работать с использованием «FindBreakTime», ведь, судя по всему, требуется использовать что-то вроде «SymbolInfoSessionsTrade»?

Спасибо.

Emile Munro
Emile Munro | 2 янв. 2026 в 13:13
Emile Munro #:

Привет, Аллан, с Новым годом! Спасибо за статью, она помогает мне разобраться, как написать код для моего советника Multi Timeframe BOS: я следую твоим инструкциям и ввожу код по ходу работы, а не просто копирую и вставляю. Однако сейчас я столкнулся с ошибкой в разделе кода «Detect Swings and BOS»: при компиляции появляется ошибка «необъявленный идентификатор» в строке кода «FindBreakTime». Как вам удалось заставить его работать с использованием «FindBreakTime», ведь, судя по всему, система требует, чтобы я использовал что-то вроде «SymbolInfoSessionsTrade»?

Спасибо.

Ладно, неважно, разобрался, спасибо.
Как мы построили самую мощную торговую платформу с Machine Learning: хроника развития MQL и MetaTrader по архивам, форумам и релизам Как мы построили самую мощную торговую платформу с Machine Learning: хроника развития MQL и MetaTrader по архивам, форумам и релизам
Техническая хроника развития MQL: от ограниченного MQL/MQL II через процедурный MQL4 к объектному MQL5 с нативной компиляцией, богатыми API и полноценной инженерной средой. Показаны ключевые возможности языка и интеграции Python, OpenCL, ONNX, OpenBLAS, базы данных, DirectX, агентный AI Assistant и Model Context Protocol (MCP), который связывает ИИ с терминалом, MetaEditor, рыночными данными, торговлей и инструментами разработки. Рассматриваются архивные источники о становлении MetaQuotes/MetaTrader, запуск MQL4.COM и MQL5.COM, чемпионаты, Algo Forge и их влияние на экосистему.
Управление позициями: Пирамидальное наращивание прибыльных позиций при снижающемся риске Управление позициями: Пирамидальное наращивание прибыльных позиций при снижающемся риске
Представляем CPyramidBridge, тонкий слой на MQL5, который сопоставляет результаты определения размера ставки с CPyramidEngine. Этот мост применяет вероятностный подход к расчету начального объема позиции, обеспечивает входной контроль с учетом доступных лимитов, активирует добавления к позиции на основе динамической дивергенции, адаптирует трейлинг-стоп к оценкам резерва и синхронизирует сигналы на закрытии пирамиды, что позволяет советнику преобразовывать уверенность модели и текущую параллельную активность сигналов в структурированную пирамиду с уменьшающимся риском.
Моделирование рынка: Position View (XIII) Моделирование рынка: Position View (XIII)
В этой статье мы рассмотрим, как без особых усилий реализовать индикатор, который показывает, приносит ли позиция убытки или прибыль. Процедура проста и эффективна. Даже без глубоких знаний этот индикатор позволит вам легко распознавать, когда следует закрывать позицию. Так вы избежите неожиданных результатов, поскольку расчёт отражает фактический результат, который дало бы закрытие позиции.
От начального до среднего уровня: Очереди, списки и деревья (V) От начального до среднего уровня: Очереди, списки и деревья (V)
В этой статье мы реализовали первые компоненты древовидной структуры. Поскольку я понимаю, что эта структура может быть очень сложной на начальном этапе обучения, мы будем внедрять её спокойно и шаг за шагом. Таким образом, каждый сможет понять, как работает дерево и когда лучше всего его использовать.