Автоматизация торговых стратегий в MQL5 (Часть 45): Инверсный разрыв справедливой стоимости (IFVG)
Введение
В предыдущей статье (Часть 44) мы разработали на языке MetaQuotes Language 5 (MQL5) систему обнаружения смены характера рынка (CHoCH). Она сканировала бары, чтобы выявлять и помечать свинговые максимумы и минимумы для определения тренда. Система поддерживала побарный и потиковый режимы работы и отображала значки, подписи, линии пробоя и шрифты адаптивного размера. В Части 45 мы разработаем систему инвертированного разрыва справедливой стоимости (IFVG).
Система выявляет бычьи и медвежьи разрывы справедливой стоимости (FVG) на последних сформированных барах и применяет фильтр минимального размера разрыва. В зависимости от взаимодействия цены с зоной она отслеживает состояния Normal, Mitigated и Inverted, а возврат цены в зону после митигации учитывает отдельным флагом ret. Митигация происходит при пробое дальней границы, возврат — при повторном входе в зону, а инверсия фиксируется, когда после повторного входа цена закрывается за дальней границей, выходя изнутри зоны. Система игнорирует новые FVG, ценовые диапазоны которых перекрываются с уже существующими зонами, и ограничивает число отслеживаемых FVG. Для каждого FVG можно задать одну сделку, ограниченное число сделок или неограниченное число входов. При бычьем IFVG открывается покупка, при медвежьем — продажа. Зоны отображаются цветными прямоугольниками с подписями состояния и количества сделок, а события митигации отмечаются значками. Рассмотрим следующие темы:
- Основы системы инвертированного разрыва справедливой стоимости (IFVG)
- Реализация в MQL5
- Тестирование в тестере стратегий
- Заключение
В результате мы получим рабочую стратегию MQL5 для обнаружения FVG и торговли по сигналам IFVG с отслеживанием состояний, адаптивной визуализацией и настраиваемыми режимами. Перейдём к реализации.
Основы системы инвертированного разрыва справедливой стоимости (IFVG)
Разрыв справедливой стоимости (FVG) — это концепция Price Action (анализа движения цены), описывающая дисбаланс или незаполненную зону FVG между свечами, возникший из-за сильного давления покупателей или продавцов. Бычий FVG обычно образуется, когда минимум более поздней свечи находится выше максимума более ранней, а медвежий — когда максимум более поздней свечи расположен ниже минимума более ранней. Такие области могут выступать потенциальными зонами поддержки или сопротивления, к которым цена возвращается для заполнения разрыва. Инверсный разрыв справедливой стоимости (IFVG) возникает, , когда после митигации цена возвращается в FVG-зону, а затем закрывается за её дальней границей, выходя изнутри зоны. Это сигнализирует о развороте: митигированный бычий FVG становится медвежьим, если после повторного входа цена закрывается ниже его нижней границы. Митигированный медвежий FVG становится бычьим, если цена закрывается выше верхней границы. Последовательность состояний выглядит так: обычное состояние — исходный разрыв, митигация — пробой дальней границы зоны, возврат — повторный вход после митигации, инверсия — закрытие за дальней границей при выходе изнутри зоны. Именно инверсия служит ключевым торговым сигналом. Последовательность развития зоны включает этапы Normal, Mitigated, Retraced и Inverted; при этом в перечислении FVGState используются состояния Normal, Mitigated и Inverted, а этап Retraced фиксируется отдельным признаком ret.
Для митигированного медвежьего FVG, то есть исходного нисходящего разрыва, бычий IFVG формируется, когда цена после митигации повторно входит в зону и закрывается выше её верхней границы. В этом случае открывается покупка со Stop Loss ниже нижней границы и фиксированным уровнем Take Profit в пунктах. И наоборот, у митигированного бычьего FVG медвежий IFVG возникает при закрытии ниже нижней границы; открывается продажа со Stop Loss выше верхней границы. Ниже показаны возможные варианты таких моделей.

Мы будем обнаруживать FVG на последних сформированных барах с фильтром минимального размера, загружать исторические зоны при инициализации и обновлять их состояния Normal, Mitigated и Inverted — в зависимости от поведения цены. Новые FVG, ценовые диапазоны которых перекрываются с уже существующими зонами, будут игнорироваться, число отслеживаемых FVG — ограничиваться, а устаревшие записи — удаляться. По бычьим IFVG, образованным из исходных медвежьих FVG, будут открываться покупки, а по медвежьим IFVG, образованным из исходных бычьих FVG, — продажи. Также реализуем фиксированные торговые уровни, разные режимы и лимиты сделок для каждого FVG, трейлинг-стоп и визуализацию цветными прямоугольниками для разных состояний с подписями и значками митигации. Ниже представлена общая схема поставленных задач.

Реализация в MQL5
Чтобы создать программу в MQL5, откройте MetaEditor, перейдите в окно «Навигатор», найдите папку Experts, нажмите команду «New» и следуйте указаниям мастера. После создания файла нужно объявить входные параметры и глобальные переменные, которые будут использоваться во всей программе.
//+------------------------------------------------------------------+ //| FVG Inverse.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; //--- Торговый объект #define FVG_Prefix "IFVG REC " //--- Префикс FVG // Обычные FVG #define CLR_UP clrGreen // Зеленый для обычного бычьего FVG #define CLR_DOWN clrRed // Красный для обычного медвежьего FVG // Митигированные FVG #define CLR_MIT_UP clrPurple // Фиолетовый для митигированного бычьего FVG #define CLR_MIT_DOWN clrOrange // Оранжевый для митигированного медвежьего FVG // Инвертированные FVG #define CLR_INV_UP clrRed // Красный для медвежьего IFVG, возникшего из исходно бычьего FVG #define CLR_INV_DOWN clrGreen // Зеленый для бычьего IFVG, возникшего из исходно медвежьего FVG //+------------------------------------------------------------------+ //| Перечисления | //+------------------------------------------------------------------+ enum TradeMode { // Определяем перечисление торговых режимов TradeOnce, // Одна сделка LimitedTrades, // Ограниченное число сделок UnlimitedTrades // Неограниченное число сделок }; enum FVGState { // Определяем перечисление состояний FVG Normal, // Обычное состояние Mitigated, // Митигированное состояние Inverted // Инвертированное состояние }; enum TrailingTypeEnum { // Определяем перечисление типов трейлинг-стопа Trailing_None = 0, // Нет Trailing_Points = 2 // В пунктах }; //+------------------------------------------------------------------+ //| Входные параметры | //+------------------------------------------------------------------+ input group "EA GENERAL SETTINGS" input double inpLot = 0.01; // Размер лота input int sl_pts = 300; // Stop Loss в пунктах input int tp_pts = 300; // Take Profit в пунктах input int minPts = 100; // Минимальный размер разрыва в пунктах input int FVG_Rec_Ext_Bars = 30; // Бары продления FVG input bool prt = true; // Вывод сообщений input long magic_number = 123456789; // Магический номер input bool ignoreOverlaps = true; // Игнорировать новые FVG, чьи ценовые диапазоны перекрываются с существующими зонами input TradeMode tradeMode = TradeOnce; // Режим торговли по FVG input int maxTradesPerFVG = 2; // Максимум сделок на FVG для LimitedTrades input int maxFVGs = 50; // Максимум отслеживаемых FVG в массиве input TrailingTypeEnum TrailingType = Trailing_None; // Тип трейлинг-стопа input double Trailing_Stop_Pips = 30.0; // Расстояние трейлинг-стопа (для режима в пунктах) input double Min_Profit_To_Trail_Pips = 50.0; // Минимальная прибыль для начала трейлинга
Сначала подключим торговую библиотеку директивой «#include <Trade/Trade.mqh>», которая предоставляет класс CTrade для управления ордерами и позициями. Затем объявим глобальный экземпляр CTrade с именем obj_Trade для выполнения всех торговых операций. Строковая константа FVG_Prefix со значением «IFVG REC» используется при формировании имен прямоугольников FVG. Для направлений и состояний FVG задаются цветовые константы: CLR_UP — зелёный для обычного бычьего FVG, CLR_DOWN — красный для обычного медвежьего, CLR_MIT_UP — фиолетовый для митигированного бычьего, CLR_MIT_DOWN — оранжевый для митигированного медвежьего, CLR_INV_UP — красный для инвертированного медвежьего FVG, первоначально бывшего бычьим, а CLR_INV_DOWN — зелёный для инвертированного бычьего FVG. Эти цвета выбраны условно для графика с белым фоном, и их можно заменить.
Далее создадим три перечисления для настройки системы. В TradeMode значение TradeOnce ограничивает торговлю одной сделкой на FVG, LimitedTrades разрешает заданное пользователем максимальное число сделок, а UnlimitedTrades снимает ограничение для отдельного FVG. Это полезно, когда требуется открывать сделки при повторных входах цены в зону. Перечисление FVGState задает состояния Normal для исходного разрыва, Mitigated после пробоя дальней границы и Inverted после появления сигнала инверсии. В TrailingTypeEnum значение Trailing_None отключает трейлинг-стоп, а Trailing_Points включает трейлинг в пунктах.
Входные параметры объединены в группу «EA GENERAL SETTINGS» для удобного отображения в окне свойств. inpLot задает объем позиции. sl_pts и tp_pts — расстояния Stop Loss и Take Profit в пунктах; minPts — минимальный размер разрыва, необходимый для признания зоны FVG; FVG_Rec_Ext_Bars — число баров, на которое прямоугольник FVG продлевается вправо; prt включает вывод сообщений; magic_number служит для идентификации сделок; ignoreOverlaps позволяет пропускать новые FVG, ценовые диапазоны которых перекрываются с уже существующими зонами. Такое исключение особенно полезно для наглядности, однако его можно отключить. Параметр tradeMode задает режим ограничения сделок, maxTradesPerFVG — предел для режима LimitedTrades, maxFVGs — максимальное число FVG в массиве, TrailingType — тип трейлинга, Trailing_Stop_Pips — расстояние трейлинг-стопа, а Min_Profit_To_Trail_Pips — минимальная прибыль для начала его переноса. После настройки входных параметров определим структуру данных и вспомогательные функции для управления торговыми моделями.
//+------------------------------------------------------------------+ //| Структура данных зоны FVG | //+------------------------------------------------------------------+ struct FVGZone { // Определяем структуру зоны FVG string name; //--- Имя зоны datetime startTime; //--- Время начала datetime origEndTime; //--- Исходное время окончания datetime mitTime; //--- Время митигации bool signal; //--- Признак сигнала bool inverted; //--- Признак инверсии bool mit; //--- Признак митигации bool ret; //--- Признак возврата bool origUp; //--- Признак исходного бычьего FVG int tradeCount; //--- Счетчик сделок FVGState state; //--- Состояние bool newSignal; //--- Признак нового сигнала }; FVGZone fvgs[]; //--- Массив зон FVG //+------------------------------------------------------------------+ //| Получение цвета по состоянию и направлению | //+------------------------------------------------------------------+ color GetFVGColor(bool isUp, FVGState currentState) { if (currentState == Normal) return isUp ? CLR_UP : CLR_DOWN; //--- Возвращаем цвет обычного состояния if (currentState == Mitigated) return isUp ? CLR_MIT_UP : CLR_MIT_DOWN; //--- Возвращаем цвет митигации if (currentState == Inverted) return isUp ? CLR_INV_UP : CLR_INV_DOWN; //--- Возвращаем цвет инверсии return clrNONE; //--- Возвращаем отсутствие цвета } //+------------------------------------------------------------------+ //| Вывод FVG для отладки | //+------------------------------------------------------------------+ void PrintFVGs() { if (!prt) return; //--- Выходим, если вывод отключен Print("Current FVGs count: ", ArraySize(fvgs)); //--- Выводим количество for (int i = 0; i < ArraySize(fvgs); i++) { //--- Перебираем FVG Print("FVG ", i, ": ", fvgs[i].name, " state=", EnumToString(fvgs[i].state), " mit=", fvgs[i].mit, " ret=", fvgs[i].ret, " inverted=", fvgs[i].inverted, " tradeCount=", fvgs[i].tradeCount, " newSignal=", fvgs[i].newSignal, " endTime=", TimeToString(fvgs[i].origEndTime)); //--- Выводим подробности } }
Сначала определим структуру FVGZone, содержащую сведения о каждом обнаруженном разрыве справедливой стоимости. Поле name хранит строковый идентификатор объекта; startTime и origEndTime — время начала и исходного окончания зоны; mitTime — момент митигации. Логические поля signal, inverted, mit и ret обозначают соответственно сигнал инверсии, факт инверсии, митигацию и повторный вход цены в зону; origUp показывает, был ли исходный разрыв бычьим. Поле tradeCount хранит число сделок по этому FVG, state — текущее состояние из перечисления FVGState, а newSignal — признак нового сигнала инверсии. Затем объявляется глобальный массив fvgs[] типа FVGZone для эффективного хранения и отслеживания нескольких активных зон вместе с их состояниями.
Функция GetFVGColor выбирает цвет прямоугольника FVG в зависимости от направления isUp и состояния currentState. Для Normal возвращается CLR_UP, если зона бычья, или CLR_DOWN, если медвежья; для Mitigated — CLR_MIT_UP либо CLR_MIT_DOWN; для Inverted — CLR_INV_UP либо CLR_INV_DOWN. В остальных случаях возвращается clrNONE. Функция PrintFVGs предназначена для отладки. Если prt равно false, она сразу завершает работу. В противном случае выводится текущее число элементов массива через ArraySize, после чего функция перебирает записи и печатает индекс, имя, состояние через EnumToString, признаки mit, ret и inverted, количество сделок, новый сигнал и время окончания через TimeToString. Далее можно перейти к функциям визуализации прямоугольников и подписей.
//+------------------------------------------------------------------+ //| Создание прямоугольника | //+------------------------------------------------------------------+ void CreateRec(string objName, datetime time1, double price1, datetime time2, double price2, color clr) { ObjectCreate(0, objName, OBJ_RECTANGLE, 0, time1, price1, time2, price2); //--- Создаем прямоугольник ObjectSetInteger(0, objName, OBJPROP_FILL, true); //--- Включаем заливку ObjectSetInteger(0, objName, OBJPROP_COLOR, clr); //--- Задаем цвет ObjectSetInteger(0, objName, OBJPROP_BACK, false); //--- Размещаем на переднем плане datetime midTime = time1 + (time2 - time1) / 2; //--- Рассчитываем среднее время double midPrice = (price1 + price2) / 2; //--- Рассчитываем среднюю цену CreateLabel(objName, midTime, midPrice); //--- Создаем подпись ChartRedraw(0); //--- Перерисовываем график } //+------------------------------------------------------------------+ //| Обновление прямоугольника | //+------------------------------------------------------------------+ void UpdateRec(string objName, datetime time1, double price1, datetime time2, double price2, color clr) { if (ObjectFind(0, objName) >= 0) { //--- Проверяем наличие ObjectSetInteger(0, objName, OBJPROP_TIME, 0, time1); //--- Задаем time1 ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, price1); //--- Задаем price1 ObjectSetInteger(0, objName, OBJPROP_TIME, 1, time2); //--- Задаем time2 ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, price2); //--- Задаем price2 ObjectSetInteger(0, objName, OBJPROP_COLOR, clr); //--- Задаем цвет datetime midTime = time1 + (time2 - time1) / 2; //--- Рассчитываем среднее время double midPrice = (price1 + price2) / 2; //--- Рассчитываем среднюю цену UpdateLabel(objName, midTime, midPrice); //--- Обновляем подпись ChartRedraw(0); //--- Перерисовываем график } } //+------------------------------------------------------------------+ //| Создаем подпись | //+------------------------------------------------------------------+ void CreateLabel(string zoneName, datetime time, double price) { string lblName = zoneName + "_Label"; //--- Имя подписи ObjectCreate(0, lblName, OBJ_TEXT, 0, time, price); //--- Создаем текст ObjectSetInteger(0, lblName, OBJPROP_ANCHOR, ANCHOR_CENTER); //--- Задаем точку привязки ObjectSetInteger(0, lblName, OBJPROP_COLOR, clrBlack); //--- Задаем цвет UpdateLabelText(lblName, zoneName); //--- Обновляем текст } //+------------------------------------------------------------------+ //| Обновление положения подписи | //+------------------------------------------------------------------+ void UpdateLabel(string zoneName, datetime time, double price) { string lblName = zoneName + "_Label"; //--- Имя подписи if (ObjectFind(0, lblName) >= 0) { //--- Проверяем наличие ObjectSetInteger(0, lblName, OBJPROP_TIME, 0, time); //--- Задаем время ObjectSetDouble(0, lblName, OBJPROP_PRICE, 0, price); //--- Задаем цену UpdateLabelText(lblName, zoneName); //--- Обновляем текст } } //+------------------------------------------------------------------+ //| Обновление текста подписи | //+------------------------------------------------------------------+ void UpdateLabelText(string lblName, string zoneName) { string text = ""; //--- Инициализируем текст int tradeCnt = 0; //--- Инициализируем счетчик FVGState state = Normal; //--- Инициализируем состояние bool origUp = false; //--- Инициализируем исходное направление for (int idx = 0; idx < ArraySize(fvgs); idx++) { //--- Перебираем FVG if (fvgs[idx].name == zoneName) { //--- Проверяем совпадение tradeCnt = fvgs[idx].tradeCount; //--- Получаем счетчик state = fvgs[idx].state; //--- Получаем состояние origUp = fvgs[idx].origUp; //--- Получаем исходное направление break; //--- Прерываем цикл } } if (state == Normal) { //--- Проверяем обычное состояние text = origUp ? "Bullish FVG" : "Bearish FVG"; //--- Задаем текст } else if (state == Mitigated) { //--- Проверяем митигацию text = origUp ? "Mitigated Bullish FVG" : "Mitigated Bearish FVG"; //--- Задаем текст } else if (state == Inverted) { //--- Проверяем инверсию text = origUp ? "Bearish Inversed FVG" : "Bullish Inversed FVG"; //--- Задаем текст } if (tradeCnt > 0) { //--- Проверяем наличие сделок text += " (Traded " + IntegerToString(tradeCnt) + " times)"; //--- Добавляем сведения о сделках } ObjectSetString(0, lblName, OBJPROP_TEXT, text); //--- Задаем текст } //+------------------------------------------------------------------+ //| Отрисовка значка митигации | //+------------------------------------------------------------------+ void DrawMitIcon(string fvgNAME, datetime mitTime, double fvgHigh, double fvgLow, bool isUp) { string iconName = fvgNAME + "_MitIcon"; //--- Имя значка double iconPrice = isUp ? fvgLow : fvgHigh; //--- Цена значка ObjectCreate(0, iconName, OBJ_ARROW, 0, mitTime, iconPrice); //--- Создаем стрелку ObjectSetInteger(0, iconName, OBJPROP_ARROWCODE, 251); //--- Задаем код ObjectSetInteger(0, iconName, OBJPROP_COLOR, clrBlue); //--- Задаем цвет ObjectSetInteger(0, iconName, OBJPROP_ANCHOR, isUp ? ANCHOR_TOP : ANCHOR_BOTTOM); //--- Задаем точку привязки ChartRedraw(0); //--- Перерисовываем график }
Для визуализации сначала определим функцию CreateRec, которая рисует на графике новый прямоугольник зоны FVG. Объект OBJ_RECTANGLE создается функцией ObjectCreate между точками time1/price1 и time2/price2. Свойство OBJPROP_FILL включает заливку, OBJPROP_COLOR задает переданный цвет, а OBJPROP_BACK со значением false размещает объект на переднем плане. Затем рассчитываются середина временного интервала и средняя цена, после чего CreateLabel добавляет в центр поясняющую подпись, а ChartRedraw обновляет график. Функция UpdateRec изменяет уже существующий прямоугольник при смене его состояния или цвета. Если ObjectFind подтверждает наличие объекта, обновляются обе временные и ценовые координаты, применяется новый цвет, заново вычисляется центр, функция UpdateLabel перемещает и обновляет подпись, после чего график перерисовывается.
Затем создадим функцию CreateLabel для размещения текстовой подписи внутри прямоугольника FVG. Имя формируется добавлением суффикса «_Label» к имени зоны. В заданных координатах создается объект OBJ_TEXT, его точка привязки устанавливается в центре, цвет задается черным, после чего UpdateLabelText формирует первоначальный текст. Функция UpdateLabel перемещает существующую подпись при изменении прямоугольника: если объект найден, обновляются его координаты и содержимое. В UpdateLabelText сначала создается пустая строка, затем выполняется поиск соответствующей зоны в массиве fvgs, чтобы получить число сделок, состояние и исходное направление.
В коде используются следующие англоязычные строковые подписи и сообщения журнала; ниже они приведены без перевода, поскольку соответствуют литералам программы. В коде используются англоязычные строковые значения; в пояснении ниже они приведены как подписи объектов и сообщения журнала. В зависимости от состояния подпись принимает следующий вид: для состояния Normal выводится текст «Bullish FVG» или «Bearish FVG», для Mitigated — «Mitigated Bullish FVG» или «Mitigated Bearish FVG», а для Inverted — «Bearish Inversed FVG» или «Bullish Inversed FVG». Если число сделок больше нуля, к подписи добавляется строка «(Traded X times)». Готовый текст задается свойством OBJPROP_TEXT через ObjectSetString. Наконец, функция DrawMitIcon размещает визуальный маркер события митигации. К имени FVG добавляется суффикс «_MitIcon», после чего в момент митигации и на соответствующей цене — у нижней границы бычьей зоны или верхней границы медвежьей — создается OBJ_ARROW. Для стрелки используется код 251, синий цвет и верхняя либо нижняя точка привязки в зависимости от isUp. Код символа можно заменить, используя таблицу Wingdings в MQL5, показанную ниже. Форма «Inversed» оставлена как в оригинальном коде; в основном тексте используется термин «инвертированный FVG».

Имея эти функции, можно сформировать исходную разметку торговых сценариев по доступной истории графика, чтобы зоны отображались уже при инициализации программы. Эту логику также оформим отдельной функцией.
//+------------------------------------------------------------------+ //| Обработка исторической митигации, возврата и сигнала FVG | //+------------------------------------------------------------------+ void ProcessHistoricalState(int idx) { string fvgNAME = fvgs[idx].name; //--- Получаем имя datetime timeSTART = fvgs[idx].startTime; //--- Получаем время начала datetime endTime = fvgs[idx].origEndTime; //--- Получаем время окончания double fvgLow = MathMin(ObjectGetDouble(0, fvgNAME, OBJPROP_PRICE, 0), ObjectGetDouble(0, fvgNAME, OBJPROP_PRICE, 1)); //--- Рассчитываем нижнюю границу double fvgHigh = MathMax(ObjectGetDouble(0, fvgNAME, OBJPROP_PRICE, 0), ObjectGetDouble(0, fvgNAME, OBJPROP_PRICE, 1)); //--- Рассчитываем верхнюю границу int fvgBar = iBarShift(_Symbol, _Period, timeSTART); //--- Получаем бар if (fvgBar < 0) return; //--- Выходим при некорректном значении bool isMit = false, isRet = false, isSig = false; //--- Инициализируем признаки datetime mitTime = 0; //--- Инициализируем время митигации int mitK = -1, sigK = -1; //--- Инициализируем индексы for (int k = fvgBar - 1; k >= 0; k--) { //--- Перебираем бары double barLow = iLow(_Symbol, _Period, k); //--- Получаем минимум бара double barHigh = iHigh(_Symbol, _Period, k); //--- Получаем максимум бара double barClose = iClose(_Symbol, _Period, k); //--- Получаем закрытие бара if (!isMit) { //--- Проверяем отсутствие митигации bool breakFar = (fvgs[idx].origUp && barLow < fvgLow) || (!fvgs[idx].origUp && barHigh > fvgHigh); //--- Проверяем пробой дальней границы if (breakFar) { //--- Пробой дальней границы isMit = true; //--- Устанавливаем митигацию mitK = k; //--- Сохраняем индекс митигации mitTime = iTime(_Symbol, _Period, k); //--- Сохраняем время митигации if (prt) Print("Historical Mitigated: ", fvgNAME, " at bar ", k, " time=", TimeToString(mitTime)); //--- Выводим сообщение о митигации } } if (isMit && !isRet) { //--- Проверяем митигацию без возврата bool inside = (barHigh > fvgLow && barLow < fvgHigh); //--- Проверяем вход в зону if (inside) { //--- Цена внутри зоны isRet = true; //--- Устанавливаем возврат if (prt) Print("Historical Retraced: ", fvgNAME, " at bar ", k); //--- Выводим сообщение о возврате } } if (isMit && isRet && !isSig) { //--- Проверяем митигацию и возврат без сигнала bool signal = (fvgs[idx].origUp && barClose < fvgLow) || (!fvgs[idx].origUp && barClose > fvgHigh); //--- Проверяем сигнал if (signal) { //--- Сигнал if (k + 1 < iBars(_Symbol, _Period)) { //--- Проверяем предыдущий бар double prevClose = iClose(_Symbol, _Period, k + 1); //--- Получаем закрытие предыдущего бара bool prevInside = (prevClose > fvgLow && prevClose < fvgHigh); //--- Проверяем нахождение предыдущего закрытия внутри зоны if (prevInside) { //--- Предыдущее закрытие внутри зоны isSig = true; //--- Устанавливаем сигнал sigK = k; //--- Сохраняем индекс сигнала if (prt) Print("Historical Signal/Inverted: ", fvgNAME, " at bar ", k, " time=", TimeToString(iTime(_Symbol, _Period, k))); //--- Выводим сообщение о сигнале } } } } } fvgs[idx].mit = isMit; //--- Устанавливаем митигацию fvgs[idx].ret = isRet; //--- Устанавливаем возврат fvgs[idx].inverted = isSig; //--- Устанавливаем инверсию fvgs[idx].signal = isSig; //--- Устанавливаем признак сигнала fvgs[idx].mitTime = mitTime; //--- Сохраняем время митигации fvgs[idx].state = isSig ? Inverted : (isMit ? Mitigated : Normal); //--- Задаем состояние fvgs[idx].newSignal = false; //--- Сбрасываем новый сигнал color currentClr = GetFVGColor(fvgs[idx].origUp, fvgs[idx].state); //--- Получаем цвет UpdateRec(fvgs[idx].name, fvgs[idx].startTime, fvgLow, fvgs[idx].origEndTime, fvgHigh, currentClr); //--- Обновляем прямоугольник if (mitTime > 0) DrawMitIcon(fvgs[idx].name, mitTime, fvgHigh, fvgLow, fvgs[idx].origUp); //--- Рисуем значок митигации }
Функция ProcessHistoricalState анализирует и обновляет состояние конкретного FVG в массиве во время инициализации, проверяя митигацию, повторный вход цены в зону и инверсию по историческим барам после начала зоны. Сначала из структуры по указанному индексу извлекаются имя, время начала и исходное время окончания. Нижняя и верхняя границы рассчитываются функциями MathMin и MathMax по ценовым координатам прямоугольника, полученным через ObjectGetDouble и OBJPROP_PRICE. Функция iBarShift определяет индекс бара, соответствующего времени начала; при некорректном значении выполнение прекращается. Признаки митигации, повторного входа цены в зону и сигнала изначально устанавливаются в false, время митигации и индексы баров — в -1. Затем выполняется обратный проход от бара перед FVG к нулевому. Для каждого бара через iLow, iHigh и iClose извлекаются минимум, максимум и цена закрытия. Пока митигации нет, проверяется пробой дальней границы зоны: у исходного бычьего FVG минимум бара должен опуститься ниже нижней границы, а у медвежьего максимум должен подняться выше верхней. При выполнении условия фиксируются признак, индекс и время митигации через iTime, а при включенном prt выводится сообщение.
Если зона уже митигирована, но цена ещё не вошла в неё повторно, проверяется пересечение бара с FVG: максимум должен находиться выше нижней границы, а минимум — ниже верхней. При этом устанавливается признак повторного входа и выводится сообщение. Когда митигация и повторный вход цены в зону уже произошли, но сигнала ещё нет, проверяется инверсия. Для исходного бычьего FVG закрытие бара должно быть ниже нижней границы, а для исходного медвежьего — выше верхней. Дополнительно подтверждается, что закрытие предыдущего бара находилось внутри зоны, то есть выше нижней и ниже верхней границы. Это подтверждает выход изнутри FVG; после него устанавливаются признак сигнала, индекс бара и сообщение журнала.
Затем в структуре обновляются признаки митигации, повторного входа цены в зону и инверсии и сигнала, время митигации и состояние: Inverted при наличии сигнала, Mitigated при митигации либо Normal. Признак newSignal сбрасывается в false. Текущий цвет определяется функцией GetFVGColor по исходному направлению и состоянию. UpdateRec обновляет координаты и цвет прямоугольника, а при наличии митигации DrawMitIcon размещает значок в соответствующее время у дальней границы — нижней для бычьего FVG и верхней для медвежьего. Теперь эту функцию можно использовать при инициализации для построения первых торговых сценариев.
//+------------------------------------------------------------------+ //| Функция инициализации советника | //+------------------------------------------------------------------+ int OnInit() { obj_Trade.SetExpertMagicNumber(magic_number); //--- Задаем магический номер ObjectsDeleteAll(0, FVG_Prefix); //--- Удаляем объекты FVG ArrayResize(fvgs, 0); //--- Очищаем массив if (prt) Print("Initializing: Deleted all existing FVG objects and reset array."); //--- Выводим сообщение об инициализации int visibleBars = (int)ChartGetInteger(0, CHART_VISIBLE_BARS); //--- Получаем число видимых баров if (prt) Print("Total Visible Bars On Chart = ", visibleBars); //--- Выводим число видимых баров // Обнаруживаем исторические FVG от старых к новым for (int i = visibleBars - 3; i >= 0; i--) { //--- Перебираем бары double low0 = iLow(_Symbol, _Period, i); //--- Получаем low0 double high2 = iHigh(_Symbol, _Period, i + 2); //--- Получаем high2 double gap_L0_H2 = NormalizeDouble((low0 - high2) / _Point, _Digits); //--- Рассчитываем разрыв L0-H2 double high0 = iHigh(_Symbol, _Period, i); //--- Получаем high0 double low2 = iLow(_Symbol, _Period, i + 2); //--- Получаем low2 double gap_H0_L2 = NormalizeDouble((low2 - high0) / _Point, _Digits); //--- Рассчитываем разрыв H0-L2 bool FVG_UP = low0 > high2 && gap_L0_H2 > minPts; //--- Проверяем бычий FVG bool FVG_DOWN = low2 > high0 && gap_H0_L2 > minPts; //--- Проверяем медвежий FVG if (FVG_UP || FVG_DOWN) { //--- Проверяем наличие FVG datetime time1 = iTime(_Symbol, _Period, i + 1); //--- Получаем time1 double price1 = FVG_UP ? high2 : high0; //--- Задаем price1 double price2 = FVG_UP ? low0 : low2; //--- Задаем price2 double newLow = MathMin(price1, price2); //--- Рассчитываем новую нижнюю границу double newHigh = MathMax(price1, price2); //--- Рассчитываем новую верхнюю границу bool overlaps = false; //--- Инициализируем признак пересечения if (ignoreOverlaps) { //--- Проверяем режим игнорирования пересечений for (int ex = 0; ex < ArraySize(fvgs); ex++) { //--- Перебираем существующие зоны double exLow = ObjectGetDouble(0, fvgs[ex].name, OBJPROP_PRICE, 0); //--- Получаем нижнюю границу существующей зоны double exHigh = ObjectGetDouble(0, fvgs[ex].name, OBJPROP_PRICE, 1); //--- Получаем верхнюю границу существующей зоны exLow = MathMin(exLow, exHigh); //--- Минимальная граница существующей зоны exHigh = MathMax(exLow, exHigh); //--- Максимальная граница существующей зоны if (MathMax(newLow, exLow) < MathMin(newHigh, exHigh)) { //--- Проверяем пересечение overlaps = true; //--- Устанавливаем пересечение if (prt) Print("Historical: Skipping overlapping FVG at ", TimeToString(time1)); //--- Выводим сообщение о пропуске break; //--- Прерываем цикл } } } if (overlaps) continue; //--- Переходим дальше при пересечении string fvgNAME = FVG_Prefix + "(" + TimeToString(time1) + ")"; //--- Имя FVG color fvgClr = FVG_UP ? CLR_UP : CLR_DOWN; //--- Задаем цвет CreateRec(fvgNAME, time1, price1, time1 + PeriodSeconds(_Period) * FVG_Rec_Ext_Bars, price2, fvgClr); //--- Создаем прямоугольник int size = ArraySize(fvgs); //--- Получаем размер if (size >= maxFVGs) { //--- Проверяем максимум if (prt) Print("Historical: Max FVGs reached, removing oldest."); //--- Выводим сообщение о достижении максимума ArrayRemove(fvgs, 0, 1); //--- Удаляем старейшую запись PrintFVGs(); //--- Выводим FVG } ArrayResize(fvgs, size + 1); //--- Изменяем размер массива fvgs[size].name = fvgNAME; //--- Задаем имя fvgs[size].startTime = time1; //--- Задаем время начала fvgs[size].origEndTime = time1 + PeriodSeconds(_Period) * FVG_Rec_Ext_Bars; //--- Задаем время окончания fvgs[size].mitTime = 0; //--- Сохраняем время митигации fvgs[size].signal = false; //--- Устанавливаем признак сигнала fvgs[size].inverted = false; //--- Устанавливаем инверсию fvgs[size].mit = false; //--- Устанавливаем митигацию fvgs[size].ret = false; //--- Устанавливаем возврат fvgs[size].origUp = FVG_UP; //--- Задаем исходное направление fvgs[size].tradeCount = 0; //--- Задаем счетчик сделок fvgs[size].state = Normal; //--- Задаем состояние fvgs[size].newSignal = false; //--- Задаем новый сигнал if (prt) Print("Historical FVG created: ", fvgNAME, " origUp=", FVG_UP, " endTime=", TimeToString(fvgs[size].origEndTime)); //--- Выводим сообщение о создании PrintFVGs(); //--- Выводим FVG } } // Обрабатываем исторические состояния for (int j = 0; j < ArraySize(fvgs); j++) { //--- Перебираем FVG ProcessHistoricalState(j); //--- Обрабатываем состояние } PrintFVGs(); //--- Выводим FVG return(INIT_SUCCEEDED); //--- Возвращаем успешную инициализацию } //+------------------------------------------------------------------+ //| Функция деинициализации советника | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { for (int i = 0; i < ArraySize(fvgs); i++) { //--- Перебираем FVG ObjectDelete(0, fvgs[i].name); //--- Удаляем объект ObjectDelete(0, fvgs[i].name + "_Label"); //--- Удаляем подпись ObjectDelete(0, fvgs[i].name + "_MitIcon"); //--- Удаляем значок митигации } ArrayResize(fvgs, 0); //--- Очищаем массив ChartRedraw(0); //--- Перерисовываем график if (prt) Print("Deinit: Deleted all FVG objects and reset array."); //--- Выводим сообщение о деинициализации }
В обработчике OnInit сначала передадим входной magic_number объекту obj_Trade через SetExpertMagicNumber, чтобы идентифицировать сделки советника. Затем ObjectsDeleteAll удаляет существующие прямоугольники с префиксом FVG_Prefix, ArrayResize обнуляет массив fvgs, а при prt выводится сообщение об инициализации. Число видимых баров графика считывается функцией ChartGetInteger со свойством CHART_VISIBLE_BARS в переменную visibleBars и также при необходимости выводится в журнал.
Для поиска исторических FVG от самых старых к новым выполняется цикл от visibleBars - 3 до 0. На каждом баре i потенциальные разрывы рассчитываются сравнением минимума бара i с максимумом бара i+2 — нормализованное расстояние в пунктах записывается в gap_L0_H2 — и максимума бара i с минимумом бара i+2, результат записывается в gap_H0_L2. FVG_UP устанавливается, если low0 выше high2 и разрыв превышает minPts, то есть сформирован бычий FVG. FVG_DOWN устанавливается, если low2 выше high0 и разрыв превышает minPts, то есть сформирован медвежий FVG. Для обнаружения разрыва необходимы как минимум три завершенных бара. Важно хорошо понять этот принцип. Для наглядности мы показали его на схеме ниже.

Если обнаружен один из типов FVG, время бара i+1 записывается в time1, задаются соответствующие ценовые координаты, а нижняя и верхняя границы зоны рассчитываются через MathMin и MathMax. При включенном ignoreOverlaps новая зона сравнивается со всеми элементами fvgs: их цены извлекаются функцией ObjectGetDouble, а пересечение определяется условием, при котором максимум нижних границ меньше минимума верхних. При обнаружении перекрытия overlaps становится равным true, при prt выводится сообщение, и обработка переходит к следующему кандидату. Если перекрытия нет, имя формируется из FVG_Prefix и TimeToString(time1), выбирается цвет по направлению, а CreateRec рисует прямоугольник, продленный вправо на FVG_Rec_Ext_Bars периодов. Когда размер массива достигает maxFVGs, самый старый элемент массива удаляется через ArrayRemove. Затем массив расширяется на один элемент, новая запись заполняется именем, временем, сброшенными признаками, исходным направлением FVG_UP, нулевым счётчиком сделок, состоянием Normal и newSignal=false. При включенном журнале выводится сообщение о создании, после чего вызывается PrintFVGs. Перекрытия ухудшают визуальное восприятие, но не мешают самой торговой работе системы.

Как видно на графике, перекрывающиеся зоны затрудняют восприятие разметки. После обнаружения всех исторических FVG программа перебирает массив fvgs и вызывает ProcessHistoricalState для каждого элемента, чтобы установить начальные состояния по прошлому движению цены. Затем снова вызывается PrintFVGs и возвращается INIT_SUCCEEDED. В OnDeinit выполняется обход fvgs с удалением каждого прямоугольника, подписи с суффиксом «_Label» и значка митигации «_MitIcon» через ObjectDelete. После этого массив обнуляется, график перерисовывается, а при prt выводится сообщение о деинициализации. После компиляции получаем результат, показанный ниже.

На изображении видно, что при инициализации программа сканирует историю, наносит найденные FVG на график и определяет их текущее состояние. Далее необходимо продолжать обнаружение и обновление сценариев на новых барах. Начнем с логики поиска.
//+------------------------------------------------------------------+ //| Обнаружение новых FVG на последних сформированных барах | //+------------------------------------------------------------------+ void DetectFVGs() { for (int i = 3; i >= 1; i--) { //--- Перебираем последние бары double low0 = iLow(_Symbol, _Period, i); //--- Получаем low0 double high2 = iHigh(_Symbol, _Period, i + 2); //--- Получаем high2 double gap_L0_H2 = NormalizeDouble((low0 - high2) / _Point, _Digits); //--- Рассчитываем разрыв L0-H2 double high0 = iHigh(_Symbol, _Period, i); //--- Получаем high0 double low2 = iLow(_Symbol, _Period, i + 2); //--- Получаем low2 double gap_H0_L2 = NormalizeDouble((low2 - high0) / _Point, _Digits); //--- Рассчитываем разрыв H0-L2 bool FVG_UP = low0 > high2 && gap_L0_H2 > minPts; //--- Проверяем бычий FVG bool FVG_DOWN = low2 > high0 && gap_H0_L2 > minPts; //--- Проверяем медвежий FVG if (FVG_UP || FVG_DOWN) { //--- Проверяем наличие FVG datetime time1 = iTime(_Symbol, _Period, i + 1); //--- Получаем time1 double price1 = FVG_UP ? high2 : high0; //--- Задаем price1 double price2 = FVG_UP ? low0 : low2; //--- Задаем price2 double newLow = MathMin(price1, price2); //--- Рассчитываем новую нижнюю границу double newHigh = MathMax(price1, price2); //--- Рассчитываем новую верхнюю границу bool overlaps = false; //--- Инициализируем признак пересечения if (ignoreOverlaps) { //--- Проверяем режим игнорирования пересечений for (int ex = 0; ex < ArraySize(fvgs); ex++) { //--- Перебираем существующие зоны double exLow = MathMin(ObjectGetDouble(0, fvgs[ex].name, OBJPROP_PRICE, 0), ObjectGetDouble(0, fvgs[ex].name, OBJPROP_PRICE, 1)); //--- Рассчитываем нижнюю границу существующей зоны double exHigh = MathMax(ObjectGetDouble(0, fvgs[ex].name, OBJPROP_PRICE, 0), ObjectGetDouble(0, fvgs[ex].name, OBJPROP_PRICE, 1)); //--- Рассчитываем верхнюю границу существующей зоны if (MathMax(newLow, exLow) < MathMin(newHigh, exHigh)) { //--- Проверяем пересечение overlaps = true; //--- Устанавливаем пересечение if (prt) Print("Detect: Skipping overlapping FVG at ", TimeToString(time1)); //--- Выводим сообщение о пропуске break; //--- Прерываем цикл } } } if (overlaps) continue; //--- Переходим дальше при пересечении string fvgNAME = FVG_Prefix + "(" + TimeToString(time1) + ")"; //--- Имя FVG if (ObjectFind(0, fvgNAME) >= 0) continue; //--- Пропускаем дубликат color fvgClr = FVG_UP ? CLR_UP : CLR_DOWN; //--- Задаем цвет datetime endTime = time1 + PeriodSeconds(_Period) * FVG_Rec_Ext_Bars; //--- Рассчитываем время окончания CreateRec(fvgNAME, time1, price1, endTime, price2, fvgClr); //--- Создаем прямоугольник int size = ArraySize(fvgs); //--- Получаем размер if (size >= maxFVGs) { //--- Проверяем максимум if (prt) Print("Detect: Max FVGs reached, removing oldest."); //--- Выводим сообщение о достижении максимума ArrayRemove(fvgs, 0, 1); //--- Удаляем старейшую запись PrintFVGs(); //--- Выводим FVG } ArrayResize(fvgs, size + 1); //--- Изменяем размер массива fvgs[size].name = fvgNAME; //--- Задаем имя fvgs[size].startTime = time1; //--- Задаем время начала fvgs[size].origEndTime = endTime; //--- Задаем время окончания fvgs[size].mitTime = 0; //--- Сохраняем время митигации fvgs[size].signal = false; //--- Устанавливаем признак сигнала fvgs[size].inverted = false; //--- Устанавливаем инверсию fvgs[size].mit = false; //--- Устанавливаем митигацию fvgs[size].ret = false; //--- Устанавливаем возврат fvgs[size].origUp = FVG_UP; //--- Задаем исходное направление fvgs[size].tradeCount = 0; //--- Задаем счетчик сделок fvgs[size].state = Normal; //--- Задаем состояние fvgs[size].newSignal = false; //--- Задаем новый сигнал if (prt) Print("New FVG added to storage: ", fvgNAME, " origUp=", FVG_UP, " endTime=", TimeToString(endTime)); //--- Выводим сообщение о добавлении PrintFVGs(); //--- Выводим FVG } } }
Функция DetectFVGs сканирует последние бары при формировании каждого нового бара и добавляет в систему отслеживания новые разрывы справедливой стоимости, соответствующие заданным условиям. Цикл от сдвига 3 до 1 проверяет несколько последних завершенных баров. Для каждого i минимум i-го бара записывается в low0, максимум бара i+2 — в high2, а нормализованный размер разрыва в пунктах — в gap_L0_H2. Аналогично максимум i-го бара записывается в high0, минимум бара i+2 — в low2, а расстояние — в gap_H0_L2. FVG_UP становится true, если low0 > high2 и разрыв превышает minPts; FVG_DOWN — если low2 > high0 и разрыв превышает minPts.
При обнаружении FVG время бара i+1 сохраняется в time1. Для бычьей зоны price1 получает high2, а price2 — low0; для медвежьей — high0 и low2. Затем рассчитываются нижняя и верхняя границы. При включенном ignoreOverlaps новая зона сравнивается со всеми элементами fvgs по ценам, полученным через ObjectGetDouble; диапазоны определяются функциями MathMin и MathMax. Если новый разрыв перекрывает хотя бы одну существующую зону, устанавливается overlaps, при prt выводится сообщение, и кандидат пропускается. Если пересечения и дублирующего объекта по ObjectFind нет, имя создается из FVG_Prefix и TimeToString(time1), выбирается зелёный или красный цвет, endTime рассчитывается как time1 + PeriodSeconds(_Period) * FVG_Rec_Ext_Bars, после чего CreateRec строит прямоугольник. Если число элементов достигло maxFVGs, ArrayRemove удаляет старейший. Затем ArrayResize расширяет массив, а новая запись получает имя, время, нулевое время митигации, сброшенные признаки, направление origUp=FVG_UP, нулевой счётчик сделок, состояние Normal и newSignal=false. При prt выводится сообщение и вызывается PrintFVGs. Таким образом, функция сохраняет только корректные, новые FVG без перекрытия с существующими зонами и продлевает их вправо для наглядности. Вызовем её в обработчике тиков.
//+------------------------------------------------------------------+ //| Функция обработки тиков советника | //+------------------------------------------------------------------+ void OnTick() { static datetime lastBarTime = 0; //--- Время последнего бара datetime curBarTime = iTime(_Symbol, _Period, 0); //--- Время текущего бара bool newBar = (curBarTime != lastBarTime); //--- Проверяем новый бар if (!newBar) return; //--- Выходим, если нового бара нет lastBarTime = curBarTime; //--- Обновляем последнее время DetectFVGs(); //--- Обнаруживаем FVG }
В обработчике OnTick, который выполняется на каждом тике, статическая переменная lastBarTime хранит время открытия предыдущего бара. В curBarTime записывается время текущего бара через iTime со сдвигом 0. Если значения различаются, newBar становится true, что означает, что сформировался новый бар. Пока нового бара нет, функция завершается, чтобы избежать повторной обработки. При появлении нового бара lastBarTime обновляется, а DetectFVGs сканирует последние завершенные бары. Получаем следующий результат.

После реализации первоначального обнаружения можно перейти к обновлению торговых сценариев. Используем следующую логику.
//+------------------------------------------------------------------+ //| Обновление состояний всех FVG | //+------------------------------------------------------------------+ void UpdateFVGs() { double prevClose = iClose(_Symbol, _Period, 1); //--- Получаем закрытие предыдущего бара double prevLow = iLow(_Symbol, _Period, 1); //--- Получаем предыдущий минимум double prevHigh = iHigh(_Symbol, _Period, 1); //--- Получаем предыдущий максимум double bar2Close = iClose(_Symbol, _Period, 2); //--- Получаем закрытие второго бара datetime curBarTime = iTime(_Symbol, _Period, 1); //--- Получаем время предыдущего бара bool removed = false; //--- Инициализируем признак удаления for (int j = ArraySize(fvgs) - 1; j >= 0; j--) { //--- Перебираем в обратном порядке if (ObjectFind(0, fvgs[j].name) < 0) { //--- Проверяем отсутствие объекта if (prt) Print("Update: Removed non-existent FVG from storage: ", fvgs[j].name); //--- Выводим сообщение об удалении ArrayRemove(fvgs, j, 1); //--- Удаляем из массива removed = true; //--- Устанавливаем признак удаления continue; //--- Продолжаем } double fvgLow = MathMin(ObjectGetDouble(0, fvgs[j].name, OBJPROP_PRICE, 0), ObjectGetDouble(0, fvgs[j].name, OBJPROP_PRICE, 1)); //--- Рассчитываем нижнюю границу double fvgHigh = MathMax(ObjectGetDouble(0, fvgs[j].name, OBJPROP_PRICE, 0), ObjectGetDouble(0, fvgs[j].name, OBJPROP_PRICE, 1)); //--- Рассчитываем верхнюю границу if (!fvgs[j].mit) { //--- Проверяем отсутствие митигации bool breakFar = (fvgs[j].origUp && prevLow < fvgLow) || (!fvgs[j].origUp && prevHigh > fvgHigh); //--- Проверяем пробой дальней границы if (breakFar) { //--- Пробой дальней границы fvgs[j].mit = true; //--- Устанавливаем митигацию fvgs[j].mitTime = curBarTime; //--- Сохраняем время митигации fvgs[j].state = Mitigated; //--- Задаем состояние if (prt) Print("Mitigated FVG: ", fvgs[j].name, " at time=", TimeToString(curBarTime)); //--- Выводим сообщение о митигации color mitClr = GetFVGColor(fvgs[j].origUp, fvgs[j].state); //--- Получаем цвет UpdateRec(fvgs[j].name, fvgs[j].startTime, fvgLow, fvgs[j].origEndTime, fvgHigh, mitClr); //--- Обновляем прямоугольник DrawMitIcon(fvgs[j].name, curBarTime, fvgHigh, fvgLow, fvgs[j].origUp); //--- Рисуем значок } } if (fvgs[j].mit && !fvgs[j].ret) { //--- Проверяем митигацию без возврата bool inside = (prevHigh > fvgLow && prevLow < fvgHigh); //--- Проверяем вход в зону if (inside) { //--- Цена внутри зоны fvgs[j].ret = true; //--- Устанавливаем возврат if (prt) Print("Retraced into FVG: ", fvgs[j].name); //--- Выводим сообщение о возврате } } if (fvgs[j].mit && fvgs[j].ret) { //--- Проверяем митигацию и возврат bool signal = (fvgs[j].origUp && prevClose < fvgLow) || (!fvgs[j].origUp && prevClose > fvgHigh); //--- Проверяем сигнал bool prevInside = (bar2Close > fvgLow && bar2Close < fvgHigh); //--- Проверяем нахождение предыдущего закрытия внутри зоны if (signal && curBarTime != fvgs[j].mitTime && prevInside) { //--- Проверяем условия сигнала fvgs[j].newSignal = true; //--- Задаем новый сигнал if (!fvgs[j].inverted) { //--- Проверяем отсутствие инверсии fvgs[j].inverted = true; //--- Устанавливаем инверсию fvgs[j].state = Inverted; //--- Задаем состояние if (prt) Print("Signal/Inverted FVG: ", fvgs[j].name, " at time=", TimeToString(curBarTime)); //--- Выводим сообщение о сигнале color sigClr = GetFVGColor(fvgs[j].origUp, fvgs[j].state); //--- Получаем цвет UpdateRec(fvgs[j].name, fvgs[j].startTime, fvgLow, fvgs[j].origEndTime, fvgHigh, sigClr); //--- Обновляем прямоугольник } } } } if (removed) PrintFVGs(); //--- Выводим данные после удаления }
Функция UpdateFVGs обновляет состояния всех отслеживаемых FVG на каждом новом баре, используя данные предыдущего завершенного бара для определения митигации, повторного входа цены в зону и инверсии в реальном времени. Цена закрытия предыдущего бара считывается через iClose со сдвигом 1 в prevClose, его минимум через iLow — в prevLow, максимум через iHigh — в prevHigh. Закрытие ещё более раннего бара сохраняется в bar2Close со сдвигом 2, а время предыдущего бара через iTime — в curBarTime. Признак removed изначально равен false. Затем массив fvgs перебирается в обратном порядке, чтобы можно было безопасно удалять элементы. Если прямоугольник FVG отсутствует и ObjectFind возвращает отрицательное значение, при prt выводится сообщение, запись удаляется через ArrayRemove, removed становится true, и цикл продолжается. В противном случае текущие границы зоны определяются функциями MathMin и MathMax по двум ценовым координатам OBJPROP_PRICE, полученным через ObjectGetDouble.
Если зона ещё не митигирована, проверяется пробой дальней границы зоны: у исходного бычьего FVG prevLow должен опуститься ниже нижней границы, а у медвежьего prevHigh должен подняться выше верхней. При выполнении условия устанавливается признак митигации, mitTime получает curBarTime, состояние меняется на Mitigated, при prt выводится сообщение, GetFVGColor выбирает новый цвет, UpdateRec обновляет прямоугольник, а DrawMitIcon рисует значок у дальней границы — верхней для медвежьей зоны и нижней для бычьей. Если митигация уже произошла, но цена ещё не вошла в неё повторно, проверяется пересечение предыдущего бара с FVG: максимум должен быть выше нижней границы, а минимум — ниже верхней. При этом признак ret становится равным true и выводится сообщение.
Если произошли митигация и повторный вход цены в зону, проверяется инверсия: у исходного бычьего FVG prevClose должен быть ниже нижней границы, а у исходного медвежьего — выше верхней. Дополнительно исключается сам бар митигации и подтверждается, что bar2Close находился внутри FVG — выше нижней и ниже верхней границы. Это подтверждает выход цены изнутри зоны. Затем newSignal устанавливается в true. Если зона ещё не была инвертирована, inverted становится true, состояние меняется на Inverted, выводится сообщение, выбирается цвет инверсии и обновляется прямоугольник. Если какие-либо записи были удалены, вызывается PrintFVGs. Так состояния FVG постоянно поддерживаются в актуальном виде, формируются корректные сигналы инверсии и безопасно обрабатываются удаленные графические объекты. После вызова функции получаем результат, показанный ниже.

Мы видим, что модели обновляются при взаимодействии цены с зонами. Далее необходимо реализовать торговлю по сигналам инверсии. Для модульности оформим эту логику отдельной функцией.
//+------------------------------------------------------------------+ //| Торговля по сигналам FVG | //+------------------------------------------------------------------+ void TradeOnFVGs() { double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); //--- Получаем Ask double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Получаем Bid for (int j = 0; j < ArraySize(fvgs); j++) { //--- Перебираем FVG if (!fvgs[j].newSignal || fvgs[j].mitTime == 0) continue; //--- Пропускаем при отсутствии сигнала или митигации if (tradeMode == TradeOnce && fvgs[j].tradeCount >= 1) { //--- Проверяем режим одной сделки и счетчик fvgs[j].newSignal = false; //--- Сбрасываем сигнал continue; //--- Продолжаем } if (tradeMode == LimitedTrades && fvgs[j].tradeCount >= maxTradesPerFVG) { //--- Проверяем ограниченный режим и максимум fvgs[j].newSignal = false; //--- Сбрасываем сигнал continue; //--- Продолжаем } double fvgLow = MathMin(ObjectGetDouble(0, fvgs[j].name, OBJPROP_PRICE, 0), ObjectGetDouble(0, fvgs[j].name, OBJPROP_PRICE, 1)); //--- Рассчитываем нижнюю границу double fvgHigh = MathMax(ObjectGetDouble(0, fvgs[j].name, OBJPROP_PRICE, 0), ObjectGetDouble(0, fvgs[j].name, OBJPROP_PRICE, 1)); //--- Рассчитываем верхнюю границу if (!fvgs[j].origUp) { //--- Проверяем исходный медвежий FVG: бычий IFVG, покупка if (prt) Print("BULLISH IFVG TRADE SIGNAL For ", fvgs[j].name, " at ", Bid); //--- Выводим сигнал покупки double SL_Buy = NormalizeDouble(fvgLow - sl_pts * _Point, _Digits); //--- Рассчитываем SL покупки double TP_Buy = NormalizeDouble(Ask + tp_pts * _Point, _Digits); //--- Рассчитываем TP покупки obj_Trade.Buy(inpLot, _Symbol, Ask, SL_Buy, TP_Buy, "IFVG Buy"); //--- Открываем покупку } else { //--- Исходный бычий FVG: медвежий IFVG, продажа if (prt) Print("BEARISH IFVG TRADE SIGNAL For ", fvgs[j].name, " at ", Ask); //--- Выводим сигнал продажи double SL_Sell = NormalizeDouble(fvgHigh + sl_pts * _Point, _Digits); //--- Рассчитываем SL продажи double TP_Sell = NormalizeDouble(Bid - tp_pts * _Point, _Digits); //--- Рассчитываем TP продажи obj_Trade.Sell(inpLot, _Symbol, Bid, SL_Sell, TP_Sell, "IFVG Sell"); //--- Открываем продажу } fvgs[j].tradeCount++; //--- Увеличиваем счетчик fvgs[j].newSignal = false; //--- Сбрасываем сигнал fvgs[j].ret = false; //--- Сбрасываем возврат if (prt) Print("Trade executed on ", fvgs[j].name, ", tradeCount now=", fvgs[j].tradeCount); //--- Выводим сообщение об исполнении double midPrice = (fvgLow + fvgHigh) / 2; //--- Рассчитываем среднюю цену datetime midTime = fvgs[j].startTime + (fvgs[j].origEndTime - fvgs[j].startTime) / 2; //--- Рассчитываем среднее время UpdateLabel(fvgs[j].name, midTime, midPrice); //--- Обновляем подпись } } //+------------------------------------------------------------------+ //| Удаление устаревших FVG из массива (с сохранением на графике) | //+------------------------------------------------------------------+ void CleanupExpiredFVGs(datetime curBarTime) { bool removed = false; //--- Инициализируем признак удаления for (int j = ArraySize(fvgs) - 1; j >= 0; j--) { //--- Перебираем в обратном порядке if (curBarTime > fvgs[j].origEndTime) { //--- Проверяем истечение срока if (prt) Print("Expired FVG removed from storage (kept on chart): ", fvgs[j].name, " endTime=", TimeToString(fvgs[j].origEndTime)); //--- Выводим сообщение об истечении срока ArrayRemove(fvgs, j, 1); //--- Удаляем из массива removed = true; //--- Устанавливаем признак удаления } } if (removed) PrintFVGs(); //--- Выводим данные после удаления }
Функция TradeOnFVGs открывает сделки по новым сигналам инверсии FVG с учетом выбранного режима и установленных лимитов. Сначала текущая цена Ask считывается через SymbolInfoDouble и SYMBOL_ASK и нормализуется по числу знаков. Аналогично через SYMBOL_BID определяется Bid. Затем перебирается массив fvgs. Элемент пропускается, если нового сигнала нет или время митигации равно нулю. Далее учитывается торговый режим: при TradeOnce запись пропускается, если tradeCount уже равен 1 или больше; при LimitedTrades — если счётчик достиг maxTradesPerFVG. Перед переходом к следующему элементу newSignal сбрасывается.
Для действительного сигнала нижняя и верхняя границы FVG рассчитываются функциями MathMin и MathMax по ценам прямоугольника, полученным через ObjectGetDouble. Если origUp равно false, то исходный FVG был медвежьим и инвертировался в бычий. При prt выводится сигнал покупки, Stop Loss устанавливается ниже нижней границы на sl_pts * _Point, Take Profit — выше Ask на tp_pts * _Point, после чего obj_Trade. Buy открывает позицию объемом inpLot по символу с рассчитанными уровнями и комментарием «IFVG Buy». Для медвежьего сигнала применяется зеркальная логика продажи. Затем tradeCount увеличивается, признаки newSignal и ret сбрасываются, при prt выводится сообщение об исполнении и текущем числе сделок, рассчитываются средняя цена и время, а UpdateLabel обновляет положение и текст подписи.
Функция CleanupExpiredFVGs удаляет устаревшие записи FVG из массива, сохраняя их графические объекты на графике. В неё передается время предыдущего бара. Признак removed сначала равен false, затем массив fvgs перебирается в обратном порядке. Если переданное время превышает origEndTime, при prt выводится сообщение об истечении срока, элемент удаляется через ArrayRemove, а removed становится true. Если удаление произошло, вызывается PrintFVGs. Это позволяет хранить в массиве только актуальные для отслеживания сценарии. После вызова функций получаем результат, показанный ниже.

Теперь система может обрабатывать сигналы и открывать позиции. Далее необходимо реализовать сопровождение сделок с помощью трейлинг-стопа. Для этого используется следующая логика.
//+------------------------------------------------------------------+ //| Применение трейлинг-стопа в пунктах | //+------------------------------------------------------------------+ void ApplyPointsTrailing() { double point = _Point; //--- Получаем размер пункта for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Перебираем позиции в обратном порядке if (PositionGetTicket(i) > 0) { //--- Проверяем корректность тикета if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == magic_number) { //--- Проверяем символ и магический номер double sl = PositionGetDouble(POSITION_SL); //--- Получаем SL double tp = PositionGetDouble(POSITION_TP); //--- Получаем TP double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Получаем цену открытия ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Получаем тикет if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Проверяем покупку double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - Trailing_Stop_Pips * point, _Digits); //--- Рассчитываем новый SL if (newSL > sl && SymbolInfoDouble(_Symbol, SYMBOL_BID) - openPrice > Min_Profit_To_Trail_Pips * point) { //--- Проверяем условия obj_Trade.PositionModify(ticket, newSL, tp); //--- Изменяем позицию } } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Проверяем продажу double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + Trailing_Stop_Pips * point, _Digits); //--- Рассчитываем новый SL if (newSL < sl && openPrice - SymbolInfoDouble(_Symbol, SYMBOL_ASK) > Min_Profit_To_Trail_Pips * point) { //--- Проверяем условия obj_Trade.PositionModify(ticket, newSL, tp); //--- Изменяем позицию } } } } } } //--- Вызываем функцию в обработчике тиков на каждом тике if (TrailingType == Trailing_Points && PositionsTotal() > 0) { //--- Проверяем трейлинг ApplyPointsTrailing(); //--- Применяем трейлинг }
Функция ApplyPointsTrailing реализует трейлинг-стоп в пунктах и в реальном времени изменяет Stop Loss по мере движения цены в прибыльную сторону. Сначала значение пункта символа _Point сохраняется в point. Затем открытые позиции перебираются в обратном порядке по PositionsTotal, чтобы избежать проблем с индексами при модификации. Корректность каждого тикета проверяется через PositionGetTicket. Для позиций текущего символа с соответствующим magic_number считываются Stop Loss через PositionGetDouble и POSITION_SL, Take Profit через POSITION_TP, цена открытия через POSITION_PRICE_OPEN и тикет через POSITION_TICKET. Для позиции на покупку новый Stop Loss рассчитывается как текущий Bid минус Trailing_Stop_Pips * point и нормализуется по числу знаков. Если новый уровень выше существующего Stop Loss, а нереализованная прибыль превышает Min_Profit_To_Trail_Pips * point, позиция изменяется через obj_Trade. PositionModify без изменения Take Profit. Для продажи применяется зеркальная логика: новый Stop Loss равен Ask плюс расстояние трейлинга и переносится только при улучшении уровня и достижении порога прибыли.
В OnTick, если TrailingType равно Trailing_Points и PositionsTotal показывает наличие открытых позиций, функция ApplyPointsTrailing вызывается на каждом тике для своевременного переноса уровней. После компиляции получаем результат, показанный ниже.

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

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

Заключение
Итак, мы разработали в MQL5 систему инвертированного разрыва справедливой стоимости (IFVG), которая обнаруживает бычьи и медвежьи FVG на последних сформированных барах с фильтром минимального размера, отслеживает состояния Normal, Mitigated и Inverted по взаимодействию цены с зонами, игнорирует новые FVG, ценовые диапазоны которых перекрываются с уже существующими зонами, ограничивает число отслеживаемых FVG, загружает исторические зоны при инициализации. Далее они обновляются в реальном времени, а устаревшие записи удаляются. Для каждого FVG поддерживается одна сделка, ограниченное число сделок или неограниченное число входов. Система открывает покупки по бычьим IFVG и продажи по медвежьим, использует фиксированные торговые уровни, ограничения по количеству сделок и трейлинг-стоп, а также отображает цветные прямоугольники с подписями состояния и количества сделок и значками митигации.
Отказ от ответственности: статья предназначена исключительно для образовательных целей. Торговля связана со значительными финансовыми рисками, а волатильность рынка может привести к убыткам. Перед использованием программы на реальном счете необходимо провести тщательное тестирование и применять продуманное управление рисками.
Стратегия на основе IFVG отслеживает состояния зон и формирует сигналы инверсии, позволяя открывать позиции по сигналам ценового дисбаланса и продолжать оптимизацию системы при дальнейшей работе над стратегией. Успешной торговли!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20361
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От начального до среднего уровня: Очереди, списки и деревья (V)
Изучение Стандартной библиотеки MQL5 (Часть 4): Создание библиотеки пользовательских сигналов
ARD-регрессия на MQL5: автоматический отбор признаков и случайные признаки Фурье
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования