English
preview
Автоматизация торговых стратегий с помощью MQL5 (Часть 48): Блоки ордеров, провокация ликвидности, пробой структуры

Автоматизация торговых стратегий с помощью MQL5 (Часть 48): Блоки ордеров, провокация ликвидности, пробой структуры

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

Введение

Трейдеры, использующие Price Action, часто не могут отличить истинные пробои от ловушек для ликвидности – движений цены, которые выглядят как сигнал продолжения тренда, но на деле специально провоцируются, чтобы снять стоп-лоссы перед разворотом. Без структурированной системы, позволяющей определить, где, вероятно, находятся институциональные ордера и как цена взаимодействует с этими уровнями, входы становятся несистемными, а управление рисками строится на догадках. Эта статья предназначена для разработчиков MetaQuotes Language 5 (MQL5) и алгоритмических трейдеров, которые хотят автоматизировать структурированный подход к торговле блоками ордеров на основе концепции Smart Money.

В предыдущей статье (Часть 47) мы автоматизировали стратегию Nick Rypock Trailing Reverse на языке MQL5, добавив функции хеджирования, охватывающие определение тренда, динамическую логику трейлинга и управление позициями как на покупку, так и на продажу. В этой статье мы создаем программу, основанную на концепции Smart Money и выявляющую блоки ордеров в зонах консолидации. Она подтверждает их на основе пробоя структуры и обнаружения провокации, а сделки совершает только тогда, когда условия соответствуют тренду на старшем таймфрейме. Мы рассмотрим следующие темы:

  1. Знакомство со стратегией Inducement Mitigation Block
  2. Реализация на MQL5
  3. Тестирование на исторических данных
  4. Заключение

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


Знакомство со стратегией Inducement Mitigation Block

Блоки ордеров (order blocks, OB) – это ценовые зоны, в которых, как считается, институциональные участники размещали крупные ордера на покупку или продажу; обычно они формируются как последняя противоположная свеча перед сильным импульсным движением от зоны консолидации. Когда цена впоследствии возвращается к этим зонам, она часто реагирует на них, создавая возможность для входа по направлению преобладающего институционального движения. Концепция основана на теории Smart Money, согласно которой крупные игроки накапливают или распределяют позиции в узких диапазонах, прежде чем резко двинуть цену в одном направлении.

Элемент провокации на снятие ликвидности (inducement) добавляет в эту систему важный фильтр. Перед настоящим пробоем структуры цена часто формирует небольшой свинг в противоположном направлении, чтобы снять стоп-лоссы розничных трейдеров и собрать ликвидность – это искусственно созданное движение и есть провокация. Ее распознавание позволяет отделять сетапы с высокой вероятностью от рыночного шума, поскольку пробой структуры после подтвержденной провокации имеет значительно больший вес, чем без него.

Используйте зоны блоков ордеров как основной ориентир для входа. Открывайте длинные позиции у бычьих блоков ордеров в сформировавшемся восходящем тренде и короткие – у медвежьих блоков ордеров в нисходящем тренде. Подтверждайте сетап только после пробоя структуры, произошедшего вслед за заметным свингом провокации, который указывает на сбор ликвидности и повышает вероятность подлинности движения. Используйте разрыв справедливой стоимости (fair value gap, FVG) внутри импульса как дополнительное подтверждение, рассматривая его как признак сильной направленности движения. Избегайте торговли блоками ордеров, которые были глубоко митигированы ценой без заметной реакции, и всегда соотносите входы с трендом на старшем таймфрейме, чтобы отфильтровать контртрендовый шум.

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

STRATEGY BLUEPRINT


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

Определение перечислений, структур, входных параметров и глобальных переменных

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

//+------------------------------------------------------------------+
//|                         Inducement Mitigation Block Strategy.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

#include <Trade/Trade.mqh>

//+------------------------------------------------------------------+
//| Enums                                                            |
//+------------------------------------------------------------------+

// Define modes for how many times an OB can be traded
enum TradeMode
  {
   TradeOnce,       // Trade Once: enter only one trade per OB
   LimitedTrades,   // Limited Trades: enter up to a fixed number of trades per OB
   UnlimitedTrades  // Unlimited Trades: no restriction on trades per OB
  };

// Define the mitigation state of an FVG or OB zone
enum FVGState
  {
   Normal,   // Normal: zone has not been mitigated yet
   Mitigated // Mitigated: price has swept through the zone
  };

// Define available trailing stop strategies
enum TrailingTypeEnum
  {
   Trailing_None   = 0, // None: no trailing stop applied
   Trailing_RR     = 1, // RR-Based: trail stop by risk:reward multiples
   Trailing_Points = 2  // Points-Based: trail stop by fixed pip distance
  };

// Define whether to allow trading on already-mitigated OBs
enum TradeMitigatedOBs
  {
   DoNotTradeMitigated, // Skip Mitigated: ignore OBs that have been mitigated
   TradeMitigated       // Allow Mitigated: still trade OBs after mitigation
  };

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "=== EA GENERAL SETTINGS ==="
input ENUM_TIMEFRAMES   higher_tf              = PERIOD_H4;             // Higher Timeframe for Trend
input ENUM_TIMEFRAMES   trade_tf               = PERIOD_CURRENT;        // Trading Timeframe for OB/FVG
input int               swing_strength         = 5;                     // Swing Point Strength in Bars
input double            risk_percent           = 0.1;                   // Risk Percentage per Trade (0.1%)
input int               sl_offset_pts          = 10;                    // SL Offset Points
input double            rr_ratio               = 4.0;                   // Risk:Reward Ratio
input int               minPts                 = 10;                    // Minimum Gap Size in Points
input int               FVG_Rec_Ext_Bars       = 10;                    // FVG Extension Bars
input int               OB_Rec_Ext_Bars        = 300;                   // OB Extension Bars
input int               inducement_ext_bars    = 10;                    // Inducement Trendline Extension Bars
input int               minIndDepthPts         = 20;                    // Minimum Inducement Depth in Points
input bool              prt                    = true;                  // Print Statements
input long              magic_number           = 123456789;             // Magic Number
input bool              ignoreOverlaps         = true;                  // Ignore new zones that overlap existing ones
input TradeMode         tradeMode              = TradeOnce;             // Mode for trading OBs
input int               maxTradesPerOB         = 2;                     // Maximum trades per OB for LimitedTrades
input int               maxZones               = 50;                    // Maximum OBs to track in array
input TrailingTypeEnum  TrailingType           = Trailing_RR;           // Trailing Stop Type
input double            Trailing_Stop_Pips     = 30.0;                  // Trailing Stop in Pips (for Points type)
input double            Min_Profit_To_Trail_Pips = 50.0;                // Min Profit to Start Trailing in Pips
input color             def_clr_up             = clrBlue;               // Swing High Color
input color             def_clr_down           = clrRed;                // Swing Low Color
input int               object_code            = 77;                    // Object Code for Swing Points
input int               width                  = 2;                     // Line Width
input TradeMitigatedOBs tradeMitigated         = TradeMitigated;        // Trade Mitigated OBs
input int               OBRangeCandles         = 7;                     // Number of Candles for Consolidation Range
input double            OBMaxDeviation         = 50.0;                  // Max Deviation in Points for Equal Highs/Lows
input int               OBWaitBars             = 3;                     // Wait Bars for Impulse Confirmation
input double            OBImpulseThreshold     = 1.0;                   // Impulse Threshold as Multiplier of Range
input bool              VisualizeLevels        = true;                  // Visualize SL/TP Levels on Chart

//+------------------------------------------------------------------+
//| Structures                                                       |
//+------------------------------------------------------------------+

// Store price and bar index together for range analysis
struct PriceIndex
  {
   double price; // Store the price level
   int    index; // Store the bar index
  };

// Store trade ticket, initial risk, and trailing level for RR trailing
struct TradeInfo
  {
   ulong  ticket;      // Store open position ticket
   double orig_risk;   // Store initial risk distance in price
   int    trail_level; // Store current RR trailing level reached
  };

// Store all metadata and state for a single Order Block zone
struct OBZone
  {
   string    name;        // Store the chart object name of the OB rectangle
   datetime  startTime;   // Store the time of the OB candle
   datetime  origEndTime; // Store the calculated expiry time of the OB
   datetime  mitTime;     // Store the time mitigation occurred (0 if not mitigated)
   datetime  fvgTime;     // Store the time of the associated FVG candle
   bool      signal;      // Store whether a trade signal has ever occurred
   bool      mit;         // Store whether the OB has been mitigated
   bool      origUp;      // Store whether the OB is bullish (true) or bearish (false)
   int       tradeCount;  // Store how many times this OB has been traded
   FVGState  state;       // Store the current mitigation state enum value
   bool      newSignal;   // Store whether a fresh signal occurred on this bar
   bool      valid;       // Store whether BOS and inducement have been confirmed
  };

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade   obj_Trade;                      // Trade execution object
long     chart_id          = -1;         // Higher timeframe chart ID (-1 means not open)
int      current_trend     = 0;          // Current trend direction: 1=up, -1=down, 0=none
int      current_font_size = 10;         // Dynamic font size updated by chart scale
string   fvg_names[];                    // Store names of all FVG rectangle objects
OBZone   obs[];                          // Store all tracked Order Block zones
TradeInfo trades[];                      // Store RR trailing data for open positions

//--- Define chart object name prefixes
#define FVG_Prefix    "FVG REC "  // Prefix for FVG rectangle objects
#define OB_Prefix     "OB REC "   // Prefix for OB rectangle objects
#define BOS_Prefix    "BOS_"      // Prefix for BOS line objects
#define IND_Prefix    "IND_"      // Prefix for inducement trendline objects

//--- Define zone colors per direction and state
#define CLR_UP        clrGreen   // Active bullish zone color
#define CLR_DOWN      clrRed     // Active bearish zone color
#define CLR_MIT_UP    clrPurple  // Mitigated bullish zone color
#define CLR_MIT_DOWN  clrOrange  // Mitigated bearish zone color

Мы начинаем реализацию с подключения библиотеки Trade.mqh, которая предоставляет доступ к классу CTrade  для упрощенного и надежного совершения сделок. Далее мы определяем четыре перечисления, которые задают основные параметры поведения программы. Перечисление TradeMode управляет тем, сколько раз можно торговать по одной и той же зоне блока ордеров – один раз, до заданного количества или без ограничений. Перечисление FVGState отслеживает, остается ли зона активной или уже митигирована ценой. Перечисление TrailingTypeEnum задает три режима трейлинг-стопа: отсутствует, по соотношению риска и вознаграждения и по фиксированному числу пунктов. Наконец, TradeMitigatedOBs определяет, должна ли программа продолжать торговать по зоне блока ордеров даже после того, как как цена уже прошла через эту зону.

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

Затем мы определяем три структуры данных. Структура PriceIndex связывает уровень цены с соответствующим индексом бара для анализа диапазона консолидации. Структура TradeInfo хранит тикет, начальную величину риска и текущий уровень трейлинга для каждой открытой позиции, которой управляет трейлинг по соотношению риска и вознаграждения. Структура OBZone является наиболее полной: она хранит все метаданные отслеживаемого блока ордеров – имя графического объекта, время начала и истечения, время митигации, время связанного разрыва справедливой стоимости, сигнальные флаги и флаги митигации, направление, количество сделок, состояние и статус проверки.

На глобальном уровне мы создаем объект CTrade для управления ордерами, объявляем идентификатор графика старшего таймфрейма, текущее направление тренда, переменную динамического размера шрифта и три динамических массива – для имен объектов FVG, для записей зон блоков ордеров на основе структуры OBZone и для отслеживания открытых сделок на основе TradeInfo. Мы также определяем препроцессорные константы для префиксов имен графических объектов и четырех цветов зон, обозначающих активные и митигированные бычьи и медвежьи состояния. Вы можете изменить все эти значения по своему усмотрению. Следующее, что мы делаем, – определяем несколько вспомогательных функций.

Вспомогательные функции и функции визуализации

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

//+------------------------------------------------------------------+
//| Get color based on state and direction                           |
//+------------------------------------------------------------------+
color GetZoneColor(bool isUp, FVGState currentState)
  {
   //--- Return bullish or bearish active color for normal state
   if (currentState == Normal)    return isUp ? CLR_UP   : CLR_DOWN;
   //--- Return bullish or bearish mitigated color for mitigated state
   if (currentState == Mitigated) return isUp ? CLR_MIT_UP : CLR_MIT_DOWN;
   //--- Return no color as fallback
   return clrNONE;
  }

//+------------------------------------------------------------------+
//| Print OBs for debugging                                          |
//+------------------------------------------------------------------+
void PrintOBs()
  {
   //--- Skip if printing is disabled
   if (!prt) return;
   //--- Print total count of tracked OBs
   Print("Current OBs count: ", ArraySize(obs));
   //--- Iterate and print each OB's key properties
   for (int i = 0; i < ArraySize(obs); i++)
     {
      Print("OB ", i, ": ", obs[i].name,
            " state=",      EnumToString(obs[i].state),
            " mit=",        obs[i].mit,
            " tradeCount=", obs[i].tradeCount,
            " newSignal=",  obs[i].newSignal,
            " valid=",      obs[i].valid,
            " endTime=",    TimeToString(obs[i].origEndTime));
     }
  }

//+------------------------------------------------------------------+
//| Draw trend label on main chart                                   |
//+------------------------------------------------------------------+
void DrawTrendLabel()
  {
   //--- Define the label object name
   string trendObj  = "EA_TrendLabel";
   //--- Build the trend text based on current direction
   string trendText = (current_trend ==  1) ? "UPTREND"   :
                      (current_trend == -1) ? "DOWNTREND" : "NO TREND";
   //--- Choose display color matching the trend direction
   color  trendClr  = (current_trend ==  1) ? clrLimeGreen :
                      (current_trend == -1) ? clrRed       : clrGray;
   //--- Create the label if it does not already exist
   if (ObjectFind(0, trendObj) < 0)
      ObjectCreate(0, trendObj, OBJ_LABEL, 0, 0, 0);
   //--- Position the label in the upper-left corner of the chart
   ObjectSetInteger(0, trendObj, OBJPROP_CORNER,    CORNER_LEFT_UPPER);
   ObjectSetInteger(0, trendObj, OBJPROP_XDISTANCE, 12);
   ObjectSetInteger(0, trendObj, OBJPROP_YDISTANCE, 30);
   //--- Set label text, color, font, and size
   ObjectSetString (0, trendObj, OBJPROP_TEXT,      "H4 Trend: " + trendText);
   ObjectSetInteger(0, trendObj, OBJPROP_COLOR,     trendClr);
   ObjectSetInteger(0, trendObj, OBJPROP_FONTSIZE,  14);
   ObjectSetString (0, trendObj, OBJPROP_FONT,      "Arial Bold");
   //--- Refresh the chart to display the updated label
   ChartRedraw(0);
  }

//+------------------------------------------------------------------+
//| Update font sizes based on chart scale                           |
//+------------------------------------------------------------------+
void UpdateFontSizes()
  {
   long scale = 0;
   //--- Read the current chart zoom scale level
   if (ChartGetInteger(0, CHART_SCALE, 0, scale))
     {
      //--- Compute new font size proportional to scale
      current_font_size = (int)(7 + scale * 0.7);
      //--- Clamp font size to a minimum of 6
      if (current_font_size < 6)  current_font_size = 6;
      //--- Clamp font size to a maximum of 15
      if (current_font_size > 15) current_font_size = 15;
      //--- Iterate all chart objects and update text font sizes
      for (int i = ObjectsTotal(0, -1, -1) - 1; i >= 0; i--)
        {
         string name = ObjectName(0, i, -1, -1);
         //--- Apply new font size only to text objects
         if (ObjectGetInteger(0, name, OBJPROP_TYPE) == OBJ_TEXT)
            ObjectSetInteger(0, name, OBJPROP_FONTSIZE, current_font_size);
        }
      //--- Refresh the chart after updating all text objects
      ChartRedraw(0);
     }
  }

//+------------------------------------------------------------------+
//| Create label for a zone rectangle                                |
//+------------------------------------------------------------------+
void CreateLabel(string zoneName, datetime time, double price)
  {
   //--- Build the label object name from the zone name
   string lblName = zoneName + "_Label";
   //--- Create a text object at the midpoint of the zone
   ObjectCreate(0, lblName, OBJ_TEXT, 0, time, price);
   //--- Center the anchor point of the label
   ObjectSetInteger(0, lblName, OBJPROP_ANCHOR,   ANCHOR_CENTER);
   //--- Set label color to black for readability on zone fill
   ObjectSetInteger(0, lblName, OBJPROP_COLOR,    clrBlack);
   //--- Apply the current dynamic font size
   ObjectSetInteger(0, lblName, OBJPROP_FONTSIZE, current_font_size);
   //--- Populate the label text based on zone state
   UpdateLabelText(lblName, zoneName);
  }

//+------------------------------------------------------------------+
//| Update label position                                            |
//+------------------------------------------------------------------+
void UpdateLabel(string zoneName, datetime time, double price)
  {
   //--- Build the label object name
   string lblName = zoneName + "_Label";
   //--- Update only if the label object already exists
   if (ObjectFind(0, lblName) >= 0)
     {
      //--- Move the label to the new time position
      ObjectSetInteger(0, lblName, OBJPROP_TIME,    0, time);
      //--- Move the label to the new price position
      ObjectSetDouble (0, lblName, OBJPROP_PRICE,   0, price);
      //--- Sync the font size with the current dynamic value
      ObjectSetInteger(0, lblName, OBJPROP_FONTSIZE, current_font_size);
      //--- Refresh the label text to reflect any state changes
      UpdateLabelText(lblName, zoneName);
     }
  }

//+------------------------------------------------------------------+
//| Update label text based on zone state                            |
//+------------------------------------------------------------------+
void UpdateLabelText(string lblName, string zoneName)
  {
   //--- Initialize text and tracking variables
   string   text     = "";
   int      tradeCnt = 0;
   FVGState state    = Normal;
   bool     origUp   = false;
   //--- Search OB array for a matching zone and extract its properties
   for (int idx = 0; idx < ArraySize(obs); idx++)
     {
      if (obs[idx].name == zoneName)
        {
         tradeCnt = obs[idx].tradeCount;
         state    = obs[idx].state;
         origUp   = obs[idx].origUp;
         break;
        }
     }
   //--- Build FVG label text from zone color when it is an FVG object
   if (StringFind(zoneName, FVG_Prefix) >= 0)
     {
      color zoneClr   = (color)ObjectGetInteger(0, zoneName, OBJPROP_COLOR);
      bool  isBullish = (zoneClr == CLR_UP);
      text = isBullish ? "Bullish FVG" : "Bearish FVG";
     }
   else
     {
      //--- Assign OB direction label based on state
      if      (state == Normal)    text = origUp ? "Bullish OB" : "Bearish OB";
      else if (state == Mitigated) text = origUp ? "Mitigated Bullish OB" : "Mitigated Bearish OB";
      //--- Append trade count if at least one trade has been taken
      if (tradeCnt > 0) text += " (Traded " + IntegerToString(tradeCnt) + " times)";
     }
   //--- Write the constructed text to the chart label object
   ObjectSetString(0, lblName, OBJPROP_TEXT, text);
  }

Мы начинаем с функции GetZoneColor, которая определяет цвет отображения любой зоны на основе ее направления и текущего состояния митигации. Для активных зон она возвращает зеленый цвет для бычьих и красный для медвежьих, а для митигированных зон – фиолетовый для бычьих и оранжевый для медвежьих; если ни одно состояние не подходит, цвет не задается. Функция PrintOBs служит отладочной утилитой. Когда вывод включен, функция печатает общее количество отслеживаемых блоков ордеров, а затем для каждой зоны выводит имя, состояние, флаг митигации, количество сделок, сигнальный флаг, статус проверки и время истечения, используя EnumToString для преобразования перечисления состояния (state) в читаемый текст.

Функция DrawTrendLabel отвечает за отображение тренда на графике. Она создает объект метки с именем EA_TrendLabel и размещает его в верхнем левом углу графика. Текст и цвет обновляются динамически – светло-зеленый для восходящего тренда, красный для нисходящего и серый, если тренд не определен, – после чего вызывается ChartRedraw  для немедленного отображения изменений. Чтобы текст оставался читаемым при разных масштабах, мы определяем функцию UpdateFontSizes. Она считывает текущий масштаб графика с помощью ChartGetInteger и вычисляет пропорциональный размер шрифта, ограниченный значениями от 6 до 15. Затем она перебирает все графические объекты и применяет обновленный размер ко всем найденным текстовым объектам, завершая работу перерисовкой графика.

Для обозначения зон мы определяем три тесно связанные функции. Функция CreateLabel размещает центрированную черную текстовую метку по центру прямоугольника зоны, применяет текущий динамический размер шрифта и сразу вызывает UpdateLabelText для заполнения текста. Функция UpdateLabel перемещает и обновляет существующую метку при изменении временных или ценовых границ зоны. Функция UpdateLabelText определяет содержимое метки. Она ищет в массиве блоков ордеров зону с соответствующим именем и формирует строку метки. Для объектов FVG она считывает цвет зоны, чтобы определить направление, и записывает в метку либо "Bullish FVG", либо "Bearish FVG". Для объектов блоков ордеров она записывает направление и состояние, добавляя суффикс с количеством сделок через IntegerToString, если по этой зоне была совершена хотя бы одна сделка. Далее мы определим функции для прямоугольников и линий.

Функции отрисовки прямоугольников зон и структурных элементов

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

//+------------------------------------------------------------------+
//| Create Rectangle zone on chart                                   |
//+------------------------------------------------------------------+
void CreateRec(string objName, datetime time1, double price1, datetime time2, double price2, color clr)
  {
   //--- Sync font size before drawing
   UpdateFontSizes();
   //--- Create the rectangle object spanning time and price boundaries
   ObjectCreate(0, objName, OBJ_RECTANGLE, 0, time1, price1, time2, price2);
   //--- Determine if this object is an FVG by checking its name prefix
   bool isFVG = StringFind(objName, FVG_Prefix) >= 0;
   //--- FVGs are unfilled outlines; OBs are solid filled rectangles
   ObjectSetInteger(0, objName, OBJPROP_FILL,  isFVG ? false : true);
   //--- FVGs use a thicker border; OBs use a thinner border
   ObjectSetInteger(0, objName, OBJPROP_WIDTH,  isFVG ? 2 : 1);
   //--- Apply the provided zone color
   ObjectSetInteger(0, objName, OBJPROP_COLOR,  clr);
   //--- Keep the rectangle in the foreground layer
   ObjectSetInteger(0, objName, OBJPROP_BACK,   false);
   //--- Compute midpoint time and price for label placement
   datetime midTime  = time1 + (time2 - time1) / 2;
   double   midPrice = (price1 + price2) / 2;
   //--- Create the descriptive text label at the zone midpoint
   CreateLabel(objName, midTime, midPrice);
   //--- Refresh the chart to render the new rectangle and label
   ChartRedraw(0);
  }

//+------------------------------------------------------------------+
//| Update Rectangle zone on chart                                   |
//+------------------------------------------------------------------+
void UpdateRec(string objName, datetime time1, double price1, datetime time2, double price2, color clr)
  {
   //--- Sync font size before updating
   UpdateFontSizes();
   //--- Update only if the rectangle object already exists on the chart
   if (ObjectFind(0, objName) >= 0)
     {
      //--- Update the start time anchor of the rectangle
      ObjectSetInteger(0, objName, OBJPROP_TIME,  0, time1);
      //--- Update the start price anchor of the rectangle
      ObjectSetDouble (0, objName, OBJPROP_PRICE, 0, price1);
      //--- Update the end time anchor of the rectangle
      ObjectSetInteger(0, objName, OBJPROP_TIME,  1, time2);
      //--- Update the end price anchor of the rectangle
      ObjectSetDouble (0, objName, OBJPROP_PRICE, 1, price2);
      //--- Apply the updated zone color (e.g., after mitigation)
      ObjectSetInteger(0, objName, OBJPROP_COLOR,    clr);
      //--- Recompute midpoint for label repositioning
      datetime midTime  = time1 + (time2 - time1) / 2;
      double   midPrice = (price1 + price2) / 2;
      //--- Move the label to the updated midpoint
      UpdateLabel(objName, midTime, midPrice);
      //--- Refresh the chart to show the updated rectangle
      ChartRedraw(0);
     }
  }

//+------------------------------------------------------------------+
//| Draw swing point arrow and BoS text on target chart              |
//+------------------------------------------------------------------+
void drawSwingPoint(string objName, datetime time, double price, int arrCode, color clr, int direction, long cid = 0)
  {
   //--- Skip creation if the arrow object already exists on the target chart
   if (ObjectFind(cid, objName) >= 0) return;
   //--- Create the arrow object at the swing price and time
   ObjectCreate(cid, objName, OBJ_ARROW, 0, time, price);
   //--- Set the arrow glyph code
   ObjectSetInteger(cid, objName, OBJPROP_ARROWCODE, arrCode);
   //--- Apply the directional color to the arrow
   ObjectSetInteger(cid, objName, OBJPROP_COLOR,     clr);
   //--- Set the font size for the arrow (shared with text objects)
   ObjectSetInteger(cid, objName, OBJPROP_FONTSIZE,  current_font_size);
   //--- Anchor arrow above the bar for upward-pointing swings
   if (direction > 0) ObjectSetInteger(cid, objName, OBJPROP_ANCHOR, ANCHOR_TOP);
   //--- Anchor arrow below the bar for downward-pointing swings
   if (direction < 0) ObjectSetInteger(cid, objName, OBJPROP_ANCHOR, ANCHOR_BOTTOM);
   //--- Build the companion BoS label object name
   string txt          = " BoS";
   string objNameDescr = objName + txt;
   //--- Create the descriptive text label at the same time and price
   ObjectCreate(cid, objNameDescr, OBJ_TEXT, 0, time, price);
   //--- Apply color and font size to the text label
   ObjectSetInteger(cid, objNameDescr, OBJPROP_COLOR,    clr);
   ObjectSetInteger(cid, objNameDescr, OBJPROP_FONTSIZE, current_font_size);
   //--- Position text above the bar for upward swings
   if (direction > 0)
     {
      ObjectSetInteger(cid, objNameDescr, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
      ObjectSetString (cid, objNameDescr, OBJPROP_TEXT,   " " + txt);
     }
   //--- Position text below the bar for downward swings
   if (direction < 0)
     {
      ObjectSetInteger(cid, objNameDescr, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER);
      ObjectSetString (cid, objNameDescr, OBJPROP_TEXT,   " " + txt);
     }
   //--- Refresh the main chart only when drawing on it
   if (cid == 0) ChartRedraw(0);
  }

//+------------------------------------------------------------------+
//| Draw break level arrowed line on main chart                      |
//+------------------------------------------------------------------+
void drawBreakLevel(string objName, datetime time1, double price1, datetime time2, double price2, color clr, int direction)
  {
   //--- Skip if the break level line already exists
   if (ObjectFind(0, objName) >= 0) return;
   //--- Create a horizontal arrowed line from swing time to break time
   ObjectCreate(0, objName, OBJ_ARROWED_LINE, 0, time1, price1, time2, price2);
   //--- Explicitly set both anchor time and price points
   ObjectSetInteger(0, objName, OBJPROP_TIME,  0, time1);
   ObjectSetDouble (0, objName, OBJPROP_PRICE, 0, price1);
   ObjectSetInteger(0, objName, OBJPROP_TIME,  1, time2);
   ObjectSetDouble (0, objName, OBJPROP_PRICE, 1, price2);
   //--- Apply color and width to the line
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, objName, OBJPROP_WIDTH, width);
   //--- Build the companion "Break" text label name
   string txt          = " Break ";
   string objNameDescr = objName + txt;
   //--- Create the text label at the end point of the break line
   ObjectCreate(0, objNameDescr, OBJ_TEXT, 0, time2, price2);
   //--- Apply color and font size to the break label
   ObjectSetInteger(0, objNameDescr, OBJPROP_COLOR,    clr);
   ObjectSetInteger(0, objNameDescr, OBJPROP_FONTSIZE, current_font_size);
   //--- Anchor above for downward-facing breaks
   if (direction > 0)
     {
      ObjectSetInteger(0, objNameDescr, OBJPROP_ANCHOR, ANCHOR_RIGHT_UPPER);
      ObjectSetString (0, objNameDescr, OBJPROP_TEXT,   " " + txt);
     }
   //--- Anchor below for upward-facing breaks
   if (direction < 0)
     {
      ObjectSetInteger(0, objNameDescr, OBJPROP_ANCHOR, ANCHOR_RIGHT_LOWER);
      ObjectSetString (0, objNameDescr, OBJPROP_TEXT,   " " + txt);
     }
   //--- Refresh the chart to show the new break level
   ChartRedraw(0);
  }

//+------------------------------------------------------------------+
//| Draw dotted horizontal trend line for entry/SL/TP visualization |
//+------------------------------------------------------------------+
void DrawDottedLine(string name, datetime t1, double p, datetime t2, color lineColor)
  {
   //--- Skip if the line already exists on the chart
   if (ObjectFind(0, name) >= 0) return;
   //--- Create a horizontal trend line between the two time points
   ObjectCreate(0, name, OBJ_TREND, 0, t1, p, t2, p);
   //--- Apply the specified color to the line
   ObjectSetInteger(0, name, OBJPROP_COLOR, lineColor);
   //--- Use a dotted style to distinguish it from structural lines
   ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_DOT);
   //--- Set line width to 1 pixel
   ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
   //--- Refresh the chart to display the new dotted line
   ChartRedraw(0);
  }

//+------------------------------------------------------------------+
//| Draw anchored text label for trade level annotation              |
//+------------------------------------------------------------------+
void DrawTextEx(string name, string text, datetime t, double p, color cl, bool isHigh)
  {
   //--- Skip if the text object already exists
   if (ObjectFind(0, name) >= 0) return;
   //--- Create a text object at the specified time and price
   ObjectCreate(0, name, OBJ_TEXT, 0, t, p);
   //--- Set the display text content
   ObjectSetString (0, name, OBJPROP_TEXT,     text);
   //--- Apply the specified color
   ObjectSetInteger(0, name, OBJPROP_COLOR,    cl);
   //--- Apply the current dynamic font size
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, current_font_size);
   //--- Use bold Arial for clear readability
   ObjectSetString (0, name, OBJPROP_FONT,     "Arial Bold");
   //--- Anchor below the price for swing highs, above for swing lows
   ObjectSetInteger(0, name, OBJPROP_ANCHOR,   isHigh ? ANCHOR_BOTTOM : ANCHOR_TOP);
   //--- Center the text horizontally around the anchor time
   ObjectSetInteger(0, name, OBJPROP_ALIGN,    ALIGN_CENTER);
   //--- Refresh the chart to render the new label
   ChartRedraw(0);
  }

//+------------------------------------------------------------------+
//| Draw mitigation icon arrow on the OB zone                        |
//+------------------------------------------------------------------+
void DrawMitIcon(string zoneNAME, datetime mitTime, double zoneHigh, double zoneLow, bool isUp)
  {
   //--- Build the mitigation icon object name
   string iconName  = zoneNAME + "_MitIcon";
   //--- Place the icon at the near edge of the zone relative to direction
   double iconPrice = isUp ? zoneLow : zoneHigh;
   //--- Create the arrow icon at the mitigation time and price
   ObjectCreate(0, iconName, OBJ_ARROW, 0, mitTime, iconPrice);
   //--- Use arrow code 251 as the mitigation marker glyph
   ObjectSetInteger(0, iconName, OBJPROP_ARROWCODE, 251);
   //--- Apply blue color for mitigation icons
   ObjectSetInteger(0, iconName, OBJPROP_COLOR,     clrBlue);
   //--- Anchor above the price for bullish zones, below for bearish
   ObjectSetInteger(0, iconName, OBJPROP_ANCHOR,    isUp ? ANCHOR_TOP : ANCHOR_BOTTOM);
   //--- Refresh the chart to display the icon
   ChartRedraw(0);
  }

Функция CreateRec рисует новый прямоугольник зоны на графике. Сначала она синхронизирует размер шрифта, затем создает объект прямоугольника с заданными временными и ценовыми границами. Она проверяет имя объекта на наличие префикса FVG с помощью StringFind , чтобы определить тип зоны: разрывы справедливой стоимости отображаются как незаполненные контуры с более толстой границей, тогда как блоки ордеров рисуются сплошной заливкой с более тонкой границей. Затем она вычисляет центр прямоугольника по времени и цене, размещает в нем описательную метку и завершает работу перерисовкой графика. Вспомогательная функция UpdateRec изменяет существующие прямоугольники: обновляет все четыре граничные точки, применяет новый цвет для отражения изменений состояния, например митигации, перемещает метку в обновленный центр и перерисовывает график.

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

Функция drawBreakLevel дополняет точку свинга, рисуя линию со стрелкой от времени свинга до бара, на котором был подтвержден пробой структуры. Для обеих конечных точек явно задаются координаты по времени и цене, а в конце линии размещается текстовая метка "Break" с направленной привязкой, чтобы она оставалась читаемой независимо от направления пробоя.

Для визуализации торговых уровней мы определяем две небольшие функции. DrawDottedLine создает пунктирную горизонтальную линию между двумя моментами времени на фиксированном уровне цены; она оформляется с помощью STYLE_DOT , чтобы визуально отделить уровни входа, стоп-лосса и тейк-профита от структурных линий. DrawTextEx размещает полужирную текстовую метку шрифтом Arial в заданной точке по времени и цене, размещая ее ниже цены для максимумов свинга и выше для минимумов свинга, с выравниванием по центру для аккуратного отображения на графике.

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

MQL5 WINGDINGS

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

Функции обнаружения свингов и анализа консолидации

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

//+------------------------------------------------------------------+
//| Check if bar at index is a valid swing high                      |
//+------------------------------------------------------------------+
bool isSwingHigh(int curr_bar, ENUM_TIMEFRAMES tf, int length)
  {
   //--- Reject if not enough bars exist on either side
   if (curr_bar < length || curr_bar + length >= iBars(_Symbol, tf)) return false;
   //--- Verify the candidate bar is strictly the highest in the window
   for (int j = 1; j <= length; j++)
     {
      //--- Fail if a newer bar within length is higher or equal
      if (iHigh(_Symbol, tf, curr_bar) <= iHigh(_Symbol, tf, curr_bar - j)) return false;
      //--- Fail if an older bar within length is higher
      if (iHigh(_Symbol, tf, curr_bar) <  iHigh(_Symbol, tf, curr_bar + j)) return false;
     }
   //--- Confirm this bar qualifies as a swing high
   return true;
  }

//+------------------------------------------------------------------+
//| Check if bar at index is a valid swing low                       |
//+------------------------------------------------------------------+
bool isSwingLow(int curr_bar, ENUM_TIMEFRAMES tf, int length)
  {
   //--- Reject if not enough bars exist on either side
   if (curr_bar < length || curr_bar + length >= iBars(_Symbol, tf)) return false;
   //--- Verify the candidate bar is strictly the lowest in the window
   for (int j = 1; j <= length; j++)
     {
      //--- Fail if a newer bar within length is lower or equal
      if (iLow(_Symbol, tf, curr_bar) >= iLow(_Symbol, tf, curr_bar - j)) return false;
      //--- Fail if an older bar within length is lower
      if (iLow(_Symbol, tf, curr_bar) >  iLow(_Symbol, tf, curr_bar + j)) return false;
     }
   //--- Confirm this bar qualifies as a swing low
   return true;
  }

//+------------------------------------------------------------------+
//| Check consolidation by equal highs and lows across range         |
//+------------------------------------------------------------------+
bool IsConsolidationEqualHighsAndLows(int rangeCandles, double maxDeviation, int startingIndex, ENUM_TIMEFRAMES tf)
  {
   //--- Compare adjacent bar highs and lows across the full consolidation range
   for (int i = startingIndex; i < startingIndex + rangeCandles - 1; i++)
     {
      //--- Reject if adjacent highs differ by more than the allowed deviation
      if (MathAbs(iHigh(_Symbol, tf, i) - iHigh(_Symbol, tf, i + 1)) > maxDeviation * _Point) return false;
      //--- Reject if adjacent lows differ by more than the allowed deviation
      if (MathAbs(iLow (_Symbol, tf, i) - iLow (_Symbol, tf, i + 1)) > maxDeviation * _Point) return false;
     }
   //--- Confirm range qualifies as a consolidation zone
   return true;
  }

//+------------------------------------------------------------------+
//| Get highest high price and bar index within range                |
//+------------------------------------------------------------------+
void GetHighestHigh(int rangeCandles, int startingIndex, PriceIndex &highestHighRef, ENUM_TIMEFRAMES tf)
  {
   //--- Seed with the first bar in the range
   highestHighRef.price = iHigh(_Symbol, tf, startingIndex);
   highestHighRef.index = startingIndex;
   //--- Scan remaining bars and update if a higher high is found
   for (int i = startingIndex + 1; i < startingIndex + rangeCandles; i++)
     {
      if (iHigh(_Symbol, tf, i) > highestHighRef.price)
        {
         //--- Update the reference with the new highest high
         highestHighRef.price = iHigh(_Symbol, tf, i);
         highestHighRef.index = i;
        }
     }
  }

//+------------------------------------------------------------------+
//| Get lowest low price and bar index within range                  |
//+------------------------------------------------------------------+
void GetLowestLow(int rangeCandles, int startingIndex, PriceIndex &lowestLowRef, ENUM_TIMEFRAMES tf)
  {
   //--- Seed with the first bar in the range
   lowestLowRef.price = iLow(_Symbol, tf, startingIndex);
   lowestLowRef.index = startingIndex;
   //--- Scan remaining bars and update if a lower low is found
   for (int i = startingIndex + 1; i < startingIndex + rangeCandles; i++)
     {
      if (iLow(_Symbol, tf, i) < lowestLowRef.price)
        {
         //--- Update the reference with the new lowest low
         lowestLowRef.price = iLow(_Symbol, tf, i);
         lowestLowRef.index = i;
        }
     }
  }

Функция isSwingHigh определяет, является ли заданный бар корректным максимумом свинга, проверяя, что в окне, задаваемом параметром length, его максимум строго выше максимумов остальных баров. Сначала функция с помощью iBars отбрасывает бар-кандидат, если по обе стороны от него недостаточно баров, затем перебирает все смещения от 1 до length, сравнивая максимум бара-кандидата с максимумами более новых и более старых соседних баров, полученными через iHigh. Если среди более новых баров находится сосед с максимумом не ниже, либо среди более старых – с максимумом строго выше, функция немедленно возвращает false. Только если все сравнения пройдены успешно, функция подтверждает максимум свинга. Функция isSwingLow полностью повторяет ту же логику для минимумов свинга, используя iLow и меняя направления сравнений, чтобы найти в окне бар со строго самым низким минимумом.

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

Для вычисления границ зоны мы определяем функции GetHighestHigh и GetLowestLow. Обе функции принимают ссылку на структуру PriceIndex и просматривают заданный диапазон баров, чтобы найти экстремальную цену и индекс соответствующего бара. Функция GetHighestHigh инициализирует структуру значениями первого бара диапазона и обновляет их всякий раз, когда далее в диапазоне встречается более высокий максимум. Функция GetLowestLow делает то же самое в обратном направлении, отслеживая самый низкий минимум и его позицию. Позже эти экстремумы используются для определения границ консолидации, относительно которых измеряются направление пробоя и сила импульса. Теперь мы можем обнаруживать и создавать зоны блоков ордеров с полной визуализацией.

Обнаружение и создание зон блоков ордеров

Это основная функция обнаружения, объединяющая анализ консолидации, подтверждение пробоя, проверку импульса и поиск разрыва справедливой стоимости для выявления и регистрации новой зоны блока ордеров на графике.

//+------------------------------------------------------------------+
//| Detect and create OB zone with FVG in impulsive area             |
//+------------------------------------------------------------------+
void DetectAndCreateZone(int i, ENUM_TIMEFRAMES tf)
  {
   //--- Skip detection if no trend has been established
   if (current_trend == 0) return;
   //--- Set the consolidation range to start one bar after the signal bar
   int consol_start_index = i + 1;
   //--- Ensure enough bars exist for the full consolidation range
   if (consol_start_index + OBRangeCandles - 1 >= iBars(_Symbol, tf)) return;
   //--- Validate the consolidation range with equal highs and lows check
   if (IsConsolidationEqualHighsAndLows(OBRangeCandles, OBMaxDeviation, consol_start_index, tf))
     {
      //--- Find the highest high and lowest low within the consolidation
      PriceIndex hh, ll;
      GetHighestHigh(OBRangeCandles, consol_start_index, hh, tf);
      GetLowestLow  (OBRangeCandles, consol_start_index, ll, tf);
      double breakout_close = iClose(_Symbol, tf, i);
      //--- Determine if price broke out bullishly above the consolidation high
      bool   breakout_up    = (current_trend ==  1) && breakout_close > hh.price;
      //--- Determine if price broke out bearishly below the consolidation low
      bool   breakout_down  = (current_trend == -1) && breakout_close < ll.price;
      //--- Proceed only if a valid directional breakout occurred
      if (breakout_up || breakout_down)
        {
         //--- Calculate the consolidation range height
         double range_val  = hh.price - ll.price;
         //--- Apply the impulse multiplier threshold to the range
         double threshold  = range_val * OBImpulseThreshold;
         bool   impulsive  = false;
         int    impulse_start_bar = -1;
         //--- Search the next few bars for an impulsive extension past the threshold
         for (int k = 1; k <= OBWaitBars; k++)
           {
            int    impulse_shift = i - k;
            if (impulse_shift < 0) break;
            double c = iClose(_Symbol, tf, impulse_shift);
            //--- Confirm bullish impulse if close exceeds high plus threshold
            if (breakout_up   && c >= hh.price + threshold)
              {
               impulsive        = true;
               impulse_start_bar = impulse_shift;
               break;
              }
            //--- Confirm bearish impulse if close falls below low minus threshold
            else if (breakout_down && c <= ll.price - threshold)
              {
               impulsive        = true;
               impulse_start_bar = impulse_shift;
               break;
              }
           }
         //--- Continue only if an impulsive move was confirmed
         if (impulsive)
           {
            //--- Set FVG direction flags based on breakout direction
            bool     FVG_UP    = breakout_up;
            bool     FVG_DOWN  = breakout_down;
            bool     fvg_found = false;
            datetime fvg_time  = 0;
            double   fvg_price1 = 0, fvg_price2 = 0;
            //--- Scan from breakout bar to impulse bar looking for a valid FVG gap
            for (int f = i; f >= impulse_start_bar; f--)
              {
               double low0  = iLow (_Symbol, tf, f);
               double high2 = iHigh(_Symbol, tf, f + 2);
               //--- Compute the gap between bar[f] low and bar[f+2] high (bullish gap)
               double gap_L0_H2 = NormalizeDouble((low0 - high2) / _Point, _Digits);
               double high0 = iHigh(_Symbol, tf, f);
               double low2  = iLow (_Symbol, tf, f + 2);
               //--- Compute the gap between bar[f+2] low and bar[f] high (bearish gap)
               double gap_H0_L2 = NormalizeDouble((low2 - high0) / _Point, _Digits);
               //--- Validate and record a bullish FVG if gap exceeds minimum points
               if (FVG_UP && low0 > high2 && gap_L0_H2 > minPts)
                 {
                  fvg_time   = iTime(_Symbol, tf, f + 1);
                  fvg_price1 = high2;
                  fvg_price2 = low0;
                  fvg_found  = true;
                  break;
                 }
               //--- Validate and record a bearish FVG if gap exceeds minimum points
               else if (FVG_DOWN && low2 > high0 && gap_H0_L2 > minPts)
                 {
                  fvg_time   = iTime(_Symbol, tf, f + 1);
                  fvg_price1 = high0;
                  fvg_price2 = low2;
                  fvg_found  = true;
                  break;
                 }
              }
            //--- Proceed only if an FVG was successfully found
            if (fvg_found)
              {
               //--- Normalize FVG boundaries to low/high order
               double newLow  = MathMin(fvg_price1, fvg_price2);
               double newHigh = MathMax(fvg_price1, fvg_price2);
               bool   fvg_overlaps = false;
               //--- Check new FVG against all existing FVGs for overlap
               if (ignoreOverlaps)
                 {
                  for (int ex = 0; ex < ArraySize(fvg_names); ex++)
                    {
                     double exLow  = ObjectGetDouble(0, fvg_names[ex], OBJPROP_PRICE, 0);
                     double exHigh = ObjectGetDouble(0, fvg_names[ex], OBJPROP_PRICE, 1);
                     //--- Sort existing zone bounds
                     exLow  = MathMin(exLow, exHigh);
                     exHigh = MathMax(exLow, exHigh);
                     //--- Check if the new FVG overlaps with the existing one
                     if (MathMax(newLow, exLow) < MathMin(newHigh, exHigh))
                       {
                        fvg_overlaps = true;
                        if (prt) Print("Historical: Skipping overlapping FVG at ", TimeToString(fvg_time));
                        break;
                       }
                    }
                 }
               //--- Abort if this FVG overlaps an existing one
               if (fvg_overlaps) return;
               //--- Use the first consolidation candle as the OB candle reference
               int      last_consol_index = consol_start_index;
               datetime obTime  = iTime (_Symbol, tf, last_consol_index);
               double   obLow   = iLow  (_Symbol, tf, last_consol_index);
               double   obHigh  = iHigh (_Symbol, tf, last_consol_index);
               bool     is_opposing = false;
               //--- Check that OB candle is bearish (opposing) for a bullish breakout
               if (breakout_up   && iOpen(_Symbol, tf, last_consol_index) > iClose(_Symbol, tf, last_consol_index))
                  is_opposing = true;
               //--- Check that OB candle is bullish (opposing) for a bearish breakout
               if (breakout_down && iOpen(_Symbol, tf, last_consol_index) < iClose(_Symbol, tf, last_consol_index))
                  is_opposing = true;
               //--- Reject if the consolidation candle is not opposing
               if (!is_opposing)
                 {
                  if (prt) Print("Skipping OB creation: Last consolidation candle not opposing at ", TimeToString(obTime));
                  return;
                 }
               //--- Normalize OB boundaries to low/high order
               double obNewLow  = MathMin(obLow, obHigh);
               double obNewHigh = MathMax(obLow, obHigh);
               bool   ob_overlaps = false;
               //--- Check new OB against all existing OBs for overlap
               if (ignoreOverlaps)
                 {
                  for (int ex = 0; ex < ArraySize(obs); ex++)
                    {
                     double exLow  = ObjectGetDouble(0, obs[ex].name, OBJPROP_PRICE, 0);
                     double exHigh = ObjectGetDouble(0, obs[ex].name, OBJPROP_PRICE, 1);
                     //--- Sort existing OB bounds
                     exLow  = MathMin(exLow, exHigh);
                     exHigh = MathMax(exLow, exHigh);
                     //--- Check if the new OB overlaps with the existing one
                     if (MathMax(obNewLow, exLow) < MathMin(obNewHigh, exHigh))
                       {
                        ob_overlaps = true;
                        if (prt) Print("Historical: Skipping overlapping OB at ", TimeToString(obTime));
                        break;
                       }
                    }
                 }
               //--- Abort if this OB overlaps an existing one
               if (ob_overlaps) return;
               //--- Build the FVG object name from its timestamp
               string   fvgNAME    = FVG_Prefix + "(" + TimeToString(fvg_time) + ")";
               color    fvgClr     = FVG_UP ? CLR_UP : CLR_DOWN;
               //--- Calculate the FVG rectangle end time based on extension bars
               datetime fvgEndTime = fvg_time + PeriodSeconds(tf) * FVG_Rec_Ext_Bars;
               //--- Draw the FVG rectangle on the chart
               CreateRec(fvgNAME, fvg_time, fvg_price1, fvgEndTime, fvg_price2, fvgClr);
               //--- Register the FVG name in the tracking array
               int fvg_size = ArraySize(fvg_names);
               ArrayResize(fvg_names, fvg_size + 1);
               fvg_names[fvg_size] = fvgNAME;
               if (prt) Print("Historical FVG created: ", fvgNAME);
               //--- Build the OB object name from its timestamp
               string   obNAME   = OB_Prefix + "(" + TimeToString(obTime) + ")";
               color    obClr    = FVG_UP ? CLR_UP : CLR_DOWN;
               //--- Calculate the OB rectangle end time based on extension bars
               datetime obEndTime = obTime + PeriodSeconds(tf) * OB_Rec_Ext_Bars;
               //--- Draw the OB rectangle on the chart
               CreateRec(obNAME, obTime, obLow, obEndTime, obHigh, obClr);
               //--- Evict the oldest OB if the storage array is at capacity
               int ob_size = ArraySize(obs);
               if (ob_size >= maxZones)
                 {
                  if (prt) Print("Historical: Max OBs reached, removing oldest.");
                  ArrayRemove(obs, 0, 1);
                  PrintOBs();
                 }
               //--- Append the new OB entry to the storage array
               ArrayResize(obs, ob_size + 1);
               obs[ob_size].name        = obNAME;
               obs[ob_size].startTime   = obTime;
               obs[ob_size].origEndTime = obEndTime;
               obs[ob_size].mitTime     = 0;
               obs[ob_size].fvgTime     = fvg_time;
               obs[ob_size].signal      = false;
               obs[ob_size].mit         = false;
               obs[ob_size].origUp      = FVG_UP;
               obs[ob_size].tradeCount  = 0;
               obs[ob_size].state       = Normal;
               obs[ob_size].newSignal   = false;
               obs[ob_size].valid       = false;
               if (prt) Print("Historical OB created: ", obNAME, " origUp=", FVG_UP, " endTime=", TimeToString(obEndTime));
               PrintOBs();
              }
           }
        }
     }
  }

Функция DetectAndCreateZone сразу завершает работу, если тренд еще не определен. В противном случае она задает диапазон консолидации со сдвигом на один бар относительно сигнального бара и проверяет его с помощью ранее определенной функции проверки консолидации. Если диапазон подходит, мы определяем его наибольший максимум и наименьший минимум, а затем проверяем, закрылся ли сигнальный бар выше максимума консолидации в восходящем тренде или ниже минимума консолидации в нисходящем. Когда направленный пробой подтвержден, мы вычисляем порог импульса, умножая высоту диапазона консолидации на заданный множитель. Затем мы просматриваем несколько следующих баров в поисках бара, закрытие которого выходит за этот порог, чтобы убедиться, что пробой действительно был импульсным, а не представлял собой слабое движение. Если в пределах окна ожидания такой бар не найден, зона отбраковывается.

После подтверждения импульсного движения мы просматриваем бары между баром пробоя и баром импульса в поисках разрыва справедливой стоимости. Для бычьего разрыва мы проверяем, что минимум текущего бара находится выше максимума бара двумя позициями ранее и что величина разрыва в пунктах превышает минимальный порог. Для медвежьего разрыва логика обратная. После обнаружения допустимого разрыва сохраняются время среднего бара и две граничные цены, а для обеспечения точности используется NormalizeDouble . Если разрыв справедливой стоимости найден, мы проверяем наличие перекрытий со всеми существующими разрывами справедливой стоимости и блоками ордеров, используя MathMin и MathMax для нормирования границ перед сравнением. Мы также проверяем, что первая свеча консолидации является противоположной – медвежьей для бычьего пробоя и бычьей для медвежьего, поскольку именно она и представляет собой свечу блока ордеров. Если проверка перекрытия или проверка на противоположную свечу не пройдена, обнаружение прерывается.

Наконец, прямоугольники разрыва справедливой стоимости и блока ордеров рисуются на графике с помощью CreateRec, а их конечное время вычисляется на основе соответствующих настроек расширения в барах и PeriodSeconds. Новый блок ордеров добавляется в массив отслеживания с инициализацией всех метаданных, а если массив достиг максимального лимита зон, перед изменением размера с помощью ArrayRemove сначала удаляется самая старая запись. Далее мы переходим к проверкам и анализу тренда.

Обработка исторического состояния, проверка пробоя структуры и провокации, анализ тренда

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

//+------------------------------------------------------------------+
//| Process historical mitigation and signal state for one OB        |
//+------------------------------------------------------------------+
void ProcessHistoricalState(int idx, ENUM_TIMEFRAMES tf)
  {
   //--- Retrieve the OB's stored name and time references
   string   obNAME    = obs[idx].name;
   datetime timeSTART = obs[idx].startTime;
   datetime endTime   = obs[idx].origEndTime;
   //--- Retrieve and normalize OB price boundaries from the chart object
   double   obLow  = MathMin(ObjectGetDouble(0, obNAME, OBJPROP_PRICE, 0), ObjectGetDouble(0, obNAME, OBJPROP_PRICE, 1));
   double   obHigh = MathMax(ObjectGetDouble(0, obNAME, OBJPROP_PRICE, 0), ObjectGetDouble(0, obNAME, OBJPROP_PRICE, 1));
   //--- Convert OB start time to a bar index on the trading timeframe
   int      obBar  = iBarShift(_Symbol, tf, timeSTART);
   if (obBar < 0) return;
   //--- Initialize mitigation and signal state flags
   bool     isMit  = false, isSig = false;
   datetime mitTime = 0;
   //--- Iterate bars from just before the OB candle toward the present
   for (int k = obBar - 1; k >= 0; k--)
     {
      double barLow   = iLow  (_Symbol, tf, k);
      double barHigh  = iHigh (_Symbol, tf, k);
      double barClose = iClose(_Symbol, tf, k);
      //--- Check for mitigation: price broke past the far edge of the OB
      if (!isMit)
        {
         bool breakFar = (obs[idx].origUp && barLow < obLow) || (!obs[idx].origUp && barHigh > obHigh);
         if (breakFar)
           {
            isMit   = true;
            mitTime = iTime(_Symbol, tf, k);
            if (prt) Print("Historical Mitigated: ", obNAME, " at time=", TimeToString(mitTime));
           }
        }
      //--- Check for trade signal: previous close inside zone, current close outside
      if ((!isMit || tradeMitigated == TradeMitigated) && !isSig && k >= 1)
        {
         double close2   = iClose(_Symbol, tf, k + 1);
         double close1   = barClose;
         //--- Previous bar closed inside the OB
         bool   inside2  = (close2 >= obLow && close2 <= obHigh);
         //--- Current bar closed outside the OB in the trend direction
         bool   outside1 = obs[idx].origUp ? (close1 > obHigh) : (close1 < obLow);
         if (inside2 && outside1) isSig = true;
        }
     }
   //--- Commit the computed mitigation and signal flags to the OB record
   obs[idx].mit      = isMit;
   obs[idx].signal   = isSig;
   obs[idx].mitTime  = mitTime;
   obs[idx].state    = isMit ? Mitigated : Normal;
   obs[idx].newSignal = false;
   //--- Retrieve the appropriate color for the current OB state
   color currentClr = GetZoneColor(obs[idx].origUp, obs[idx].state);
   //--- Redraw the OB rectangle with the updated color
   UpdateRec(obs[idx].name, obs[idx].startTime, obLow, obs[idx].origEndTime, obHigh, currentClr);
   //--- Draw the mitigation icon if the zone was mitigated
   if (mitTime > 0) DrawMitIcon(obs[idx].name, mitTime, obHigh, obLow, obs[idx].origUp);
  }

//+------------------------------------------------------------------+
//| Check for BOS and inducement, draw objects if confirmed          |
//+------------------------------------------------------------------+
bool HasBOSAndInducement(int idx, ENUM_TIMEFRAMES stf)
  {
   //--- Extract OB time references and direction
   datetime obTime  = obs[idx].startTime;
   datetime endTime = obs[idx].origEndTime;
   datetime fvgTime = obs[idx].fvgTime;
   bool     isBearish = !obs[idx].origUp;
   //--- Convert OB and FVG times to bar indices
   int ob_bar  = iBarShift(_Symbol, stf, obTime);
   int fvg_bar = iBarShift(_Symbol, stf, fvgTime);
   if (ob_bar < 0 || fvg_bar < 0) return false;
   //--- Retrieve OB price boundaries from bar data
   double   obLow    = MathMin(iLow(_Symbol, stf, ob_bar), iHigh(_Symbol, stf, ob_bar));
   double   obHigh   = MathMax(iLow(_Symbol, stf, ob_bar), iHigh(_Symbol, stf, ob_bar));
   //--- Initialize swing search result variables
   double   swing_ext  = 0.0;
   datetime swing_time = 0;
   int      swing_bar  = -1;
   //--- Search bars after the FVG for the first valid swing that lies outside the OB
   for (int k = fvg_bar - 1; k >= 0; k--)
     {
      datetime bar_time = iTime(_Symbol, stf, k);
      //--- Stop scanning once we pass the OB expiry time
      if (bar_time > endTime) break;
      //--- Check for swing low in bearish context, swing high in bullish
      bool is_swing = isBearish ? isSwingLow(k, stf, swing_strength) : isSwingHigh(k, stf, swing_strength);
      if (is_swing)
        {
         double temp_swing_ext = isBearish ? iLow(_Symbol, stf, k) : iHigh(_Symbol, stf, k);
         //--- Skip swings that fall within the OB price range
         if (temp_swing_ext >= obLow && temp_swing_ext <= obHigh) continue;
         //--- Record this as the reference swing for BOS detection
         swing_ext  = temp_swing_ext;
         swing_time = bar_time;
         swing_bar  = k;
         break;
        }
     }
   //--- Abort if no qualifying swing was found after the FVG
   if (swing_ext == 0.0)
     {
      if (prt) Print("No swing found after FVG for: ", obs[idx].name);
      return false;
     }
   //--- Initialize BOS break search variables
   datetime break_time = 0;
   int      break_bar  = -1;
   //--- Search bars after the swing for a bar that closes past the swing level
   for (int k = swing_bar - 1; k >= 0; k--)
     {
      datetime bar_time  = iTime (_Symbol, stf, k);
      //--- Stop scanning once we pass the OB expiry time
      if (bar_time > endTime) break;
      double   bar_low   = iLow  (_Symbol, stf, k);
      double   bar_high  = iHigh (_Symbol, stf, k);
      double   bar_close = iClose(_Symbol, stf, k);
      //--- Confirm a bearish BOS if both low and close are below the swing low
      bool     broken = isBearish
                        ? (bar_low < swing_ext && bar_close < swing_ext)
                        : (bar_high > swing_ext && bar_close > swing_ext);
      if (broken)
        {
         break_time = bar_time;
         break_bar  = k;
         break;
        }
     }
   //--- Abort if no break of structure was confirmed
   if (break_bar == -1)
     {
      if (prt) Print("No BOS break found for OB: ", obs[idx].name);
      return false;
     }
   //--- Build and draw the BOS swing arrow and horizontal break level
   string bos_name = BOS_Prefix + TimeToString(obTime);
   color  bos_clr  = isBearish ? def_clr_down : def_clr_up;
   int    bos_dir  = isBearish ? 1 : -1;
   drawSwingPoint(TimeToString(swing_time), swing_time, swing_ext, object_code, bos_clr, bos_dir, 0);
   drawBreakLevel(bos_name, swing_time, swing_ext, break_time, swing_ext, bos_clr, bos_dir);
   if (prt) Print("BOS detected and drawn for OB: ", obs[idx].name);
   //--- Calculate the reduced strength for inducement swing detection
   int    ind_strength = MathMax(swing_strength / 2, 1);
   bool   found_ind    = false;
   datetime ind_time   = 0;
   double   ind_ext    = 0.0;
   //--- Search between the swing bar and the break bar for an inducement swing
   for (int m = swing_bar - 1; m >= break_bar; m--)
     {
      //--- Check for inducement swing in the opposing direction of the BOS
      bool is_ind_swing = isBearish ? isSwingHigh(m, stf, ind_strength) : isSwingLow(m, stf, ind_strength);
      if (is_ind_swing)
        {
         double temp_ind_ext = isBearish ? iHigh(_Symbol, stf, m) : iLow(_Symbol, stf, m);
         //--- Skip inducements that fall within the OB price range
         if (temp_ind_ext >= obLow && temp_ind_ext <= obHigh) continue;
         //--- Compute the depth of the inducement from the swing level
         double depth = MathAbs(temp_ind_ext - swing_ext) / _Point;
         //--- Accept only inducements deeper than the minimum threshold
         if (depth > minIndDepthPts)
           {
            ind_time  = iTime(_Symbol, stf, m);
            ind_ext   = temp_ind_ext;
            found_ind = true;
           }
        }
     }
   //--- Abort if no valid inducement was found between swing and break
   if (!found_ind)
     {
      if (prt) Print("No significant/non-overlapping inducement found for OB: ", obs[idx].name);
      return false;
     }
   //--- Calculate the end time for the inducement trendline display
   datetime ind_end  = ind_time + PeriodSeconds(stf) * inducement_ext_bars;
   //--- Build the inducement trendline and label object names
   string   ind_name = IND_Prefix + TimeToString(obTime);
   //--- Draw a horizontal dashed trendline at the inducement level
   ObjectCreate(0, ind_name, OBJ_TREND, 0, ind_time, ind_ext, ind_end, ind_ext);
   ObjectSetInteger(0, ind_name, OBJPROP_COLOR,     bos_clr);
   ObjectSetInteger(0, ind_name, OBJPROP_STYLE,     STYLE_DASH);
   ObjectSetInteger(0, ind_name, OBJPROP_WIDTH,     1);
   ObjectSetInteger(0, ind_name, OBJPROP_RAY_RIGHT, false);
   //--- Create the "Inducement" text label above or below the trendline
   string lbl = ind_name + "_label";
   ObjectCreate(0, lbl, OBJ_TEXT, 0, ind_time, ind_ext);
   ObjectSetString (0, lbl, OBJPROP_TEXT,     "Inducement");
   ObjectSetInteger(0, lbl, OBJPROP_COLOR,    bos_clr);
   ObjectSetInteger(0, lbl, OBJPROP_FONTSIZE, current_font_size);
   //--- Anchor text below the line for bearish inducements, above for bullish
   ObjectSetInteger(0, lbl, OBJPROP_ANCHOR,   isBearish ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
   if (prt) Print("Inducement detected and drawn for OB: ", obs[idx].name);
   return true;
  }

//+------------------------------------------------------------------+
//| Update trend direction from higher timeframe swing analysis      |
//+------------------------------------------------------------------+
void UpdateTrend()
  {
   //--- Retrieve the total number of bars on the higher timeframe
   int bars = iBars(_Symbol, higher_tf);
   double curr_swing_high = -1.0, curr_swing_low = -1.0;
   int    new_trend = 0;
   //--- Clear all objects on the H4 chart when it is open
   if (chart_id > 0) ObjectsDeleteAll(chart_id, -1, -1);
   //--- Iterate from oldest to newest bar looking for swing highs and lows
   for (int i = bars - swing_strength - 1; i >= swing_strength; i--)
     {
      //--- Detect swing high: current bar must be strictly highest in window
      bool isHigh = true;
      for (int j = 1; j <= swing_strength; j++)
        {
         if (iHigh(_Symbol, higher_tf, i) <= iHigh(_Symbol, higher_tf, i + j)) isHigh = false;
         if (iHigh(_Symbol, higher_tf, i) <= iHigh(_Symbol, higher_tf, i - j)) isHigh = false;
        }
      if (isHigh)
        {
         double new_high = iHigh(_Symbol, higher_tf, i);
         //--- Label as HH (higher high) or LH (lower high) relative to last swing high
         string label = (curr_swing_high < 0) ? "H" : (new_high > curr_swing_high ? "HH" : "LH");
         color  clr   = (label == "HH") ? clrBlue : clrRed;
         //--- Draw the swing label on the H4 chart if it is open
         if (chart_id > 0)
            drawSwingPoint(TimeToString(iTime(_Symbol, higher_tf, i)),
                           iTime(_Symbol, higher_tf, i), new_high, 174, clr, -1, chart_id);
         //--- Update trend to bullish on higher high confirmation
         if (label == "HH")   new_trend =  1;
         //--- Update trend to bearish on lower high confirmation
         else if (label == "LH") new_trend = -1;
         curr_swing_high = new_high;
        }
      //--- Detect swing low: current bar must be strictly lowest in window
      bool isLow = true;
      for (int j = 1; j <= swing_strength; j++)
        {
         if (iLow(_Symbol, higher_tf, i) >= iLow(_Symbol, higher_tf, i + j)) isLow = false;
         if (iLow(_Symbol, higher_tf, i) >= iLow(_Symbol, higher_tf, i - j)) isLow = false;
        }
      if (isLow)
        {
         double new_low = iLow(_Symbol, higher_tf, i);
         //--- Label as LL (lower low) or HL (higher low) relative to last swing low
         string label = (curr_swing_low < 0) ? "L" : (new_low < curr_swing_low ? "LL" : "HL");
         color  clr   = (label == "HL") ? clrBlue : clrRed;
         //--- Draw the swing label on the H4 chart if it is open
         if (chart_id > 0)
            drawSwingPoint(TimeToString(iTime(_Symbol, higher_tf, i)),
                           iTime(_Symbol, higher_tf, i), new_low, 174, clr, 1, chart_id);
         //--- Update trend to bullish on higher low confirmation
         if (label == "HL")   new_trend =  1;
         //--- Update trend to bearish on lower low confirmation
         else if (label == "LL") new_trend = -1;
         curr_swing_low = new_low;
        }
     }
   //--- Commit the determined trend direction to the global variable
   current_trend = new_trend;
   //--- Render the updated trend label on the main chart
   DrawTrendLabel();
   //--- Refresh the higher timeframe chart after redrawing swings
   if (chart_id > 0) ChartRedraw(chart_id);
  }

Функция ProcessHistoricalState просматривает бары начиная с бара непосредственно перед свечой блока ордеров до текущего момента. Для каждого бара она проверяет два условия. Во-первых, она смотрит, пробила ли цена дальнюю границу зоны – ниже минимума для бычьих зон или выше максимума для медвежьих, – и если да, помечает зону как митигированную и записывает время. Во-вторых, с учетом настройки торговли по митигированным зонам, она проверяет, закрылся ли предыдущий бар внутри зоны, а текущий – за ее пределами в ожидаемом направлении, помечая это как исторический сигнал. После завершения прохода флаги митигации и сигнала сохраняются в записи зоны, прямоугольник перерисовывается в соответствующий цвет через GetZoneColor, а если зона была митигирована, на график добавляется значок митигации.

Функция HasBOSAndInducement служит проверочным фильтром, определяющим, пригодна ли зона для торговли. Сначала она преобразует время блока ордеров и разрыва справедливой стоимости в индексы баров с помощью iBarShift, а затем просматривает бары после бара разрыва справедливой стоимости в поисках первой подходящей точки свинга за пределами ценового диапазона блока ордеров – максимума свинга для бычьих зон и минимума свинга для медвежьих. Эта точка свинга становится опорным уровнем для обнаружения пробоя структуры. Затем второй проход ищет бар, который уверенно закрывается за пределами этого уровня свинга, подтверждая пробой структуры. Если ни точка свинга, ни пробой не обнаружены в пределах срока действия зоны, функция возвращает false, и зона остается неподтвержденной.

После подтверждения пробоя структуры на графике рисуются стрелка свинга и линия уровня пробоя с помощью ранее определенных функций. Затем функция сужает область поиска до интервала между опорным баром свинга и баром пробоя, ища провоцирующий свинг в противоположном направлении – максимум свинга между медвежьим свингом и его пробоем либо минимум свинга в бычьем сценарии. Здесь используется уменьшенное значение силы свинга, вычисляемое с помощью MathMax, чтобы улавливать более мелкие снятия ликвидности. Провокация также должна выходить за пределы диапазона блока ордеров и превышать минимальный порог глубины в пунктах. Если найдена корректная провокация, на этом уровне рисуются пунктирная горизонтальная линия и текстовая метка "Inducement", после чего функция возвращает true.

Функция UpdateTrend выполняет структурный анализ на старшем таймфрейме. Она просматривает все бары на старшем таймфрейме от старых к новым, выявляя максимумы и минимумы свинга с помощью той же строгой логики оконного сравнения. Каждый максимум свинга помечается как более высокий максимум или более низкий максимум относительно предыдущего максимума свинга, а каждый минимум свинга – как более высокий минимум или более низкий минимум. Более высокий максимум или более высокий минимум переводят тренд в бычье состояние, тогда как более низкий максимум или более низкий минимум – в медвежье. Все метки свинга рисуются на отдельно открытом графике старшего таймфрейма, если он доступен. После полного прохода выявленное направление тренда сохраняется в глобальной переменной, а метка тренда на основном графике обновляется. Прежде чем двигаться дальше, инициализируем программу и посмотрим на промежуточный результат. Для этого используется следующая логика.

Обработчик события инициализации

Обработчик события OnInit инициализирует состояние программы при запуске – настраивает торговый объект, удаляет устаревшие графические объекты, заново строит карту исторических зон и проверяет существующие зоны до поступления первого тика.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   //--- Assign the magic number to the trade object for order identification
   obj_Trade.SetExpertMagicNumber(magic_number);
   //--- Open a separate chart on the higher timeframe for swing visualization
   chart_id = ChartOpen(_Symbol, higher_tf);
   if (chart_id <= 0)
      Print("Failed to open higher TF chart (possibly in tester). Proceeding without visuals.");
   //--- Remove all previously drawn zone objects from the main chart
   ObjectsDeleteAll(0, FVG_Prefix);
   ObjectsDeleteAll(0, OB_Prefix);
   ObjectsDeleteAll(0, BOS_Prefix);
   ObjectsDeleteAll(0, IND_Prefix);
   //--- Remove the trend label left by any previous EA run
   ObjectDelete(0, "EA_TrendLabel");
   //--- Reset all tracking arrays to empty
   ArrayResize(obs,       0);
   ArrayResize(fvg_names, 0);
   ArrayResize(trades,    0);
   if (prt) Print("Initializing: Deleted all existing objects and reset arrays.");
   //--- Analyze the higher timeframe to establish the initial trend direction
   UpdateTrend();
   //--- Resolve the actual trading timeframe
   ENUM_TIMEFRAMES use_tf    = (trade_tf == PERIOD_CURRENT) ? _Period : trade_tf;
   int             visibleBars = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
   //--- Scan all visible bars historically to build the initial OB map
   for (int i = visibleBars - 3; i >= 0; i--)
      DetectAndCreateZone(i, use_tf);
   //--- Replay historical price action to set mitigation and signal states
   for (int j = 0; j < ArraySize(obs); j++)
      ProcessHistoricalState(j, use_tf);
   //--- Mark OBs that already have confirmed BOS and inducement as valid
   for (int j = 0; j < ArraySize(obs); j++)
     {
      if (!obs[j].mit && HasBOSAndInducement(j, use_tf))
         obs[j].valid = true;
     }
   //--- Log the full initial OB array
   PrintOBs();
   //--- Sync label font sizes to the current chart scale
   UpdateFontSizes();
//---
   return(INIT_SUCCEEDED);
  }

Сначала мы присваиваем торговому объекту магическое число с помощью метода SetExpertMagicNumber, чтобы все ордера, размещенные этой программой, можно было однозначно идентифицировать. Затем с помощью ChartOpen мы открываем отдельный график на старшем таймфрейме для визуализации свингов; если это не удается, выводим предупреждение, поскольку такое возможно в тестере стратегий. Затем с основного графика удаляются все ранее созданные объекты зон с помощью ObjectsDeleteAll для каждого префикса – разрывов справедливой стоимости, блоков ордеров, линий пробоя структуры и линий провокации, – а также метка тренда. После этого все три массива отслеживания с помощью ArrayResize сбрасываются к нулевому размеру, чтобы начать с чистого состояния.

После очистки графика мы вызываем UpdateTrend, чтобы до запуска обнаружения зон определить начальное направление тренда по старшему таймфрейму. Затем мы определяем активный торговый таймфрейм: если выбран PERIOD_CURRENT , по умолчанию используется период текущего графика; после этого с помощью ChartGetInteger получаем количество видимых баров. Далее мы просматриваем все видимые бары от старых к новым, вызывая DetectAndCreateZone для каждого из них, чтобы построить начальную карту блоков ордеров.

После создания всех зон мы воспроизводим историческое ценовое движение для каждой из них, вызывая ProcessHistoricalState, чтобы задать состояния митигации и сигнала. Затем во втором проходе вызывается HasBOSAndInducement для всех немитигированных зон; зоны, для которых подтверждены пробой структуры и провокация, помечаются как прошедшие проверку, а значит пригодные для торговли. Наконец, перед возвратом INIT_SUCCEEDED мы выводим в лог весь массив зон и синхронизируем размеры шрифта. После компиляции получаем следующий результат.

INITIALIZATION

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

Управление трейлинг-стопом, обнаружение зон и обновление состояния

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

//+------------------------------------------------------------------+
//| Handle RR-based trailing stop for all open positions             |
//+------------------------------------------------------------------+
void HandleRRTrailing()
  {
   double point = _Point;
   double bid   = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double ask   = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   //--- Iterate all tracked trades in reverse for safe removal
   for (int i = ArraySize(trades) - 1; i >= 0; i--)
     {
      //--- Remove the entry if the position no longer exists
      if (!PositionSelectByTicket(trades[i].ticket))
        {
         ArrayRemove(trades, i, 1);
         continue;
        }
      //--- Skip positions on other symbols
      if (PositionGetString (POSITION_SYMBOL) != _Symbol)      continue;
      //--- Skip positions opened by other EAs
      if (PositionGetInteger(POSITION_MAGIC)  != magic_number) continue;
      //--- Read current position details
      double entry  = PositionGetDouble (POSITION_PRICE_OPEN);
      double sl     = PositionGetDouble (POSITION_SL);
      double tp     = PositionGetDouble (POSITION_TP);
      ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      //--- Compute current profit distance from entry in price terms
      double profit_dist = (type == POSITION_TYPE_BUY) ? (bid - entry) : (entry - ask);
      double orig_risk   = trades[i].orig_risk;
      int    level       = trades[i].trail_level;
      //--- Advance trailing level while profit exceeds the next RR threshold
      while (profit_dist >= (level + 1) * orig_risk)
        {
         //--- Compute the new SL at the current RR level from entry
         double new_sl = (type == POSITION_TYPE_BUY)
                         ? NormalizeDouble(entry + level * orig_risk, _Digits)
                         : NormalizeDouble(entry - level * orig_risk, _Digits);
         //--- Read broker stop and freeze levels to enforce minimum distance
         long   stop_level   = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
         long   freeze_level = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
         double min_dist     = MathMax(stop_level, freeze_level) * point;
         //--- Clamp the buy SL so it does not violate the minimum stop distance
         if (type == POSITION_TYPE_BUY)
           {
            double min_sl = bid - min_dist;
            if (new_sl > min_sl) new_sl = min_sl;
           }
         else
           {
            //--- Clamp the sell SL so it does not violate the minimum stop distance
            double max_sl = ask + min_dist;
            if (new_sl < max_sl) new_sl = max_sl;
           }
         //--- Skip if the new SL matches the current SL (no change needed)
         if (new_sl == sl) break;
         //--- Apply the trailing SL modification to the broker
         if (obj_Trade.PositionModify(trades[i].ticket, new_sl, tp))
           {
            sl = new_sl;
            trades[i].trail_level++;
           }
         else break;
         level = trades[i].trail_level;
        }
     }
  }

//+------------------------------------------------------------------+
//| Apply fixed points trailing stop to all open positions           |
//+------------------------------------------------------------------+
void ApplyPointsTrailing()
  {
   double point = _Point;
   //--- Iterate all open positions in reverse for safe modification
   for (int i = PositionsTotal() - 1; i >= 0; i--)
     {
      if (PositionGetTicket(i) > 0)
        {
         //--- Filter by symbol and magic number to target only this EA's trades
         if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == magic_number)
           {
            double sl        = PositionGetDouble(POSITION_SL);
            double tp        = PositionGetDouble(POSITION_TP);
            double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
            ulong  ticket    = PositionGetInteger(POSITION_TICKET);
            //--- Handle buy position trailing
            if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
              {
               //--- Compute new SL below current bid by the trailing distance
               double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - Trailing_Stop_Pips * point, _Digits);
               //--- Move SL only if it improves and profit threshold is met
               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)
              {
               //--- Compute new SL above current ask by the trailing distance
               double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + Trailing_Stop_Pips * point, _Digits);
               //--- Move SL only if it improves and profit threshold is met
               if (newSL < sl && openPrice - SymbolInfoDouble(_Symbol, SYMBOL_ASK) > Min_Profit_To_Trail_Pips * point)
                  obj_Trade.PositionModify(ticket, newSL, tp);
              }
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Detect new OB zones on recent bars                               |
//+------------------------------------------------------------------+
void DetectNewZones(ENUM_TIMEFRAMES tf)
  {
   //--- Skip detection when no trend direction is defined
   if (current_trend == 0) return;
   //--- Scan the last 50 confirmed bars for new OB formations
   for (int i = 50; i >= 1; i--)
      DetectAndCreateZone(i, tf);
  }

//+------------------------------------------------------------------+
//| Update mitigation and signal states for all tracked OBs          |
//+------------------------------------------------------------------+
void UpdateOBs(ENUM_TIMEFRAMES tf)
  {
   //--- Read the last two confirmed bar closes and boundaries
   double   prevClose  = iClose(_Symbol, tf, 1);
   double   close2     = iClose(_Symbol, tf, 2);
   double   prevLow    = iLow  (_Symbol, tf, 1);
   double   prevHigh   = iHigh (_Symbol, tf, 1);
   datetime curBarTime = iTime (_Symbol, tf, 1);
   bool     removed    = false;
   //--- Iterate OBs in reverse to allow safe in-place removal
   for (int j = ArraySize(obs) - 1; j >= 0; j--)
     {
      //--- Remove OB entries whose chart objects have been deleted externally
      if (ObjectFind(0, obs[j].name) < 0)
        {
         if (prt) Print("Update: Removed non-existent OB from storage: ", obs[j].name);
         ArrayRemove(obs, j, 1);
         removed = true;
         continue;
        }
      //--- Read the current OB price boundaries from the chart object
      double obLow  = MathMin(ObjectGetDouble(0, obs[j].name, OBJPROP_PRICE, 0), ObjectGetDouble(0, obs[j].name, OBJPROP_PRICE, 1));
      double obHigh = MathMax(ObjectGetDouble(0, obs[j].name, OBJPROP_PRICE, 0), ObjectGetDouble(0, obs[j].name, OBJPROP_PRICE, 1));
      //--- Validate unconfirmed OBs by checking for BOS and inducement now
      if (!obs[j].valid && !obs[j].mit)
        {
         if (HasBOSAndInducement(j, tf))
           {
            obs[j].valid = true;
            if (prt) Print("OB validated with BOS/Inducement: ", obs[j].name);
           }
        }
      //--- Check for mitigation on the last confirmed bar
      if (!obs[j].mit)
        {
         bool breakFar = (obs[j].origUp && prevLow < obLow) || (!obs[j].origUp && prevHigh > obHigh);
         if (breakFar)
           {
            //--- Mark OB as mitigated and record the mitigation bar time
            obs[j].mit     = true;
            obs[j].mitTime = curBarTime;
            obs[j].state   = Mitigated;
            if (prt) Print("Mitigated OB: ", obs[j].name, " at time=", TimeToString(curBarTime));
            //--- Redraw the OB rectangle with the mitigated color
            color mitClr = GetZoneColor(obs[j].origUp, obs[j].state);
            UpdateRec(obs[j].name, obs[j].startTime, obLow, obs[j].origEndTime, obHigh, mitClr);
            //--- Place the mitigation icon on the chart
            DrawMitIcon(obs[j].name, curBarTime, obHigh, obLow, obs[j].origUp);
           }
        }
      //--- Check for a new trade signal on valid OBs that allow it
      if ((!obs[j].mit || tradeMitigated == TradeMitigated) && obs[j].valid)
        {
         //--- Bar two closes ago was inside the OB
         bool inside2  = (close2 >= obLow && close2 <= obHigh);
         //--- Previous bar closed outside the OB in the expected breakout direction
         bool outside1 = obs[j].origUp ? (prevClose > obHigh) : (prevClose < obLow);
         if (inside2 && outside1)
           {
            //--- Flag a new tradeable signal for this OB
            obs[j].newSignal = true;
            obs[j].signal    = true;
            if (prt) Print("Signal (tap) on OB: ", obs[j].name, " at time=", TimeToString(curBarTime));
           }
        }
     }
   //--- Log OB array state after any removals
   if (removed) PrintOBs();
  }

Функция HandleRRTrailing управляет трейлингом на основе соотношения риска и вознаграждения для всех открытых позиций. Она перебирает массив сделок в обратном порядке, удаляя все записи, для которых позиции больше не существуют, с помощью PositionSelectByTicket, и пропуская позиции, относящиеся к другим символам или программам. Для каждой подходящей позиции она считывает цену входа, стоп-лосс, тейк-профит и тип, а затем вычисляет текущее расстояние от цены входа в сторону прибыли. Цикл while сдвигает уровень трейлинга до тех пор, пока расстояние от цены входа в сторону прибыли достигает следующего порога по соотношению риска и вознаграждения, вычисляя новый стоп-лосс на каждом уровне с помощью NormalizeDouble. Перед внесением любых изменений она считывает уровни стопа и заморозки брокера через SymbolInfoInteger и ограничивает новый стоп-лосс так, чтобы соблюдалось требование минимального расстояния. Изменение применяется с помощью метода PositionModify, а уровень трейлинга увеличивается только в случае успеха.

Функция ApplyPointsTrailing обрабатывает режим трейлинга по фиксированному числу пунктов. Она перебирает все открытые позиции, фильтруя их по символу и магическому числу, а затем вычисляет новый стоп-лосс, смещая текущую цену Bid или Ask на заданное расстояние трейлинга. Для позиций на покупку стоп-лосс переносится вверх только в том случае, если это улучшает его положение и прибыль превышает минимальный порог; для позиций на продажу логика зеркально повторяется в противоположном направлении.

Функция DetectNewZones – это небольшая функция-обертка, которая просматривает последние 50 подтвержденных баров и вызывает DetectAndCreateZone для каждого из них, а если тренд не установлен, сразу завершает работу.

Функция UpdateOBs обрабатывает все отслеживаемые блоки ордеров на каждом новом баре. Она считывает цены закрытия двух последних подтвержденных баров, а также максимум и минимум предыдущего бара, затем перебирает массив в обратном порядке. Любая зона, чей прямоугольник на графике был удален извне, удаляется из массива с помощью функции ArrayRemove. Для оставшихся зон она сначала выполняет проверку пробоя структуры и провокации для всех зон, которые еще не подтверждены.

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

Обработчик события OnTick

Обработчик события OnTick координирует управление трейлинг-стопом, обновление тренда и логику зон на каждом ценовом тике.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   //--- Run RR trailing logic if that mode is active and positions are open
   if (TrailingType == Trailing_RR     && PositionsTotal() > 0) HandleRRTrailing();
   //--- Run points trailing logic if that mode is active and positions are open
   if (TrailingType == Trailing_Points && PositionsTotal() > 0) ApplyPointsTrailing();
   //--- Check for a new bar on the higher timeframe and refresh the trend
   static datetime lastHigherBarTime = 0;
   datetime curHigherBarTime = iTime(_Symbol, higher_tf, 0);
   if (curHigherBarTime != lastHigherBarTime)
     {
      lastHigherBarTime = curHigherBarTime;
      UpdateTrend();
     }
   //--- Resolve the trading timeframe for bar detection
   ENUM_TIMEFRAMES use_tf = (trade_tf == PERIOD_CURRENT) ? _Period : trade_tf;
   //--- Detect a new bar on the trading timeframe
   static datetime lastBarTime = 0;
   datetime curBarTime = iTime(_Symbol, use_tf, 0);
   bool newBar = (curBarTime != lastBarTime);
   //--- Skip all zone logic if this is not a new bar
   if (!newBar) return;
   lastBarTime = curBarTime;
   //--- Scan for newly formed OB zones
   DetectNewZones(use_tf);
   //--- Update mitigation and signal states for all tracked OBs
   UpdateOBs(use_tf);
  }

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

Далее мы определяем, сформировался ли новый бар на старшем таймфрейме, сравнивая время открытия текущего бара с последним зафиксированным временем бара этого таймфрейма с помощью статической переменной. Когда обнаруживается новый бар на старшем таймфрейме, вызывается UpdateTrend для обновления направления структурного тренда.

Для торгового таймфрейма вторая статическая переменная отслеживает время открытия последнего известного бара. Время текущего бара сравнивается с этим значением, чтобы определить, сформировался ли новый бар. Если новый бар не сформировался, функция немедленно завершается, благодаря чему вся логика зон выполняется только один раз на подтвержденную свечу. Когда новый бар подтвержден, мы обновляем время последнего бара, а затем последовательно вызываем DetectNewZones и UpdateOBs для поиска новых зон и обновления состояний всех отслеживаемых зон. После компиляции получаем следующий результат.

CONFIRMED SETUP

Вывод подтверждает, что сетап проходит проверку в логике анализа, выполняемой обработчиком события тика. Далее переходим к торговле по этим сетапам.

Совершение сделок и очистка истекших зон

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

//+------------------------------------------------------------------+
//| Execute trades on OBs that carry a new signal                    |
//+------------------------------------------------------------------+
void TradeOnOBs()
  {
   //--- Retrieve current market prices
   double Ask     = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits);
   double Bid     = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits);
   //--- Retrieve account and symbol parameters for lot sizing
   double balance    = AccountInfoDouble(ACCOUNT_BALANCE);
   double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double min_lot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double max_lot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double lot_step   = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   //--- Resolve the trading timeframe
   ENUM_TIMEFRAMES use_tf    = (trade_tf == PERIOD_CURRENT) ? _Period : trade_tf;
   datetime        curBarTime = iTime(_Symbol, use_tf, 0);
   //--- Iterate all tracked OBs looking for actionable signals
   for (int j = 0; j < ArraySize(obs); j++)
     {
      //--- Skip OBs without a fresh signal, not validated, or blocked by settings
      if (!obs[j].newSignal || (obs[j].mitTime != 0 && tradeMitigated != TradeMitigated) || !obs[j].valid) continue;
      //--- Enforce the TradeOnce limit
      if (tradeMode == TradeOnce     && obs[j].tradeCount >= 1)             { obs[j].newSignal = false; continue; }
      //--- Enforce the LimitedTrades count limit
      if (tradeMode == LimitedTrades && obs[j].tradeCount >= maxTradesPerOB) { obs[j].newSignal = false; continue; }
      //--- Read OB price boundaries for entry and SL calculation
      double obLow  = MathMin(ObjectGetDouble(0, obs[j].name, OBJPROP_PRICE, 0), ObjectGetDouble(0, obs[j].name, OBJPROP_PRICE, 1));
      double obHigh = MathMax(ObjectGetDouble(0, obs[j].name, OBJPROP_PRICE, 0), ObjectGetDouble(0, obs[j].name, OBJPROP_PRICE, 1));
      //--- Set entry at ask for buys, bid for sells
      double entry  = obs[j].origUp ? Ask : Bid;
      //--- Set SL below the OB for buys, above for sells, with an offset buffer
      double sl     = obs[j].origUp
                      ? NormalizeDouble(obLow  - sl_offset_pts * _Point, _Digits)
                      : NormalizeDouble(obHigh + sl_offset_pts * _Point, _Digits);
      double risk_dist = MathAbs(entry - sl);
      if (risk_dist <= 0) continue;
      //--- Compute risk in points for lot sizing
      double points      = risk_dist / _Point;
      double risk_amount = balance * (risk_percent / 100.0);
      //--- Calculate the lot size that risks exactly the target amount
      double lot_size    = risk_amount / (points * tick_value);
      //--- Round to lot step and clamp within broker limits
      lot_size = MathMin(MathMax(MathRound(lot_size / lot_step) * lot_step, min_lot), max_lot);
      if (lot_size < min_lot) continue;
      //--- Compute TP based on the risk distance and RR ratio
      double tp = obs[j].origUp
                  ? NormalizeDouble(entry + risk_dist * rr_ratio, _Digits)
                  : NormalizeDouble(entry - risk_dist * rr_ratio, _Digits);
      bool result = false;
      //--- Place a buy market order for bullish OBs
      if (obs[j].origUp)
        {
         if (prt) Print("BULLISH OB TRADE SIGNAL For ", obs[j].name, " at ", Bid, " Lot: ", lot_size);
         result = obj_Trade.Buy(lot_size, _Symbol, Ask, sl, tp, "OB Buy");
        }
      else
        {
         //--- Place a sell market order for bearish OBs
         if (prt) Print("BEARISH OB TRADE SIGNAL For ", obs[j].name, " at ", Ask, " Lot: ", lot_size);
         result = obj_Trade.Sell(lot_size, _Symbol, Bid, sl, tp, "OB Sell");
        }
      //--- Process post-trade bookkeeping if the order was accepted
      if (result && obj_Trade.ResultRetcode() == TRADE_RETCODE_DONE)
        {
         ulong ticket = obj_Trade.ResultOrder();
         //--- Register the trade for RR trailing if that mode is active
         if (TrailingType == Trailing_RR)
           {
            int tsize = ArraySize(trades);
            ArrayResize(trades, tsize + 1);
            trades[tsize].ticket      = ticket;
            trades[tsize].orig_risk   = risk_dist;
            trades[tsize].trail_level = 0;
           }
         //--- Increment trade count and clear the fresh signal flag
         obs[j].tradeCount++;
         obs[j].newSignal = false;
         if (prt) Print("Trade executed on ", obs[j].name, ", tradeCount now=", obs[j].tradeCount);
         //--- Update the zone label to reflect the new trade count
         double   midPrice = (obLow + obHigh) / 2;
         datetime midTime  = obs[j].startTime + (obs[j].origEndTime - obs[j].startTime) / 2;
         UpdateLabel(obs[j].name, midTime, midPrice);
         //--- Draw entry, SL, and TP level lines on the chart if enabled
         if (VisualizeLevels)
           {
            datetime drawStart  = curBarTime;
            datetime drawEnd    = drawStart + PeriodSeconds(use_tf) * 5;
            datetime labelTime  = drawEnd + PeriodSeconds(use_tf) / 2;
            bool     isBuy      = obs[j].origUp;
            double   sign       = isBuy ? 1.0 : -1.0;
            //--- Draw and label the entry level
            DrawDottedLine(obs[j].name + "_Entry",       drawStart, entry, drawEnd, clrMagenta);
            DrawTextEx    (obs[j].name + "_Entry_label", "Entry",   labelTime, entry, clrMagenta, isBuy);
            //--- Draw and label the stop loss level
            DrawDottedLine(obs[j].name + "_SL",       drawStart, sl, drawEnd, clrRed);
            DrawTextEx    (obs[j].name + "_SL_label", "SL",       labelTime, sl, clrRed, !isBuy);
            //--- Define a set of green shades for progressive TP levels
            color tpColors[4] = {clrForestGreen, clrGreen, clrDarkGreen, clrLime};
            int   maxLevels   = (int)rr_ratio;
            //--- Draw and label each TP level up to the full RR ratio
            for (int lev = 1; lev <= maxLevels; lev++)
              {
               double lev_p   = entry + sign * risk_dist * lev;
               color  c       = (lev - 1 < 4) ? tpColors[lev - 1] : clrLime;
               //--- Label the final level as FullTP, intermediate levels as TP1, TP2, etc.
               string levName = (lev == maxLevels) ? "FullTP" : "TP" + IntegerToString(lev);
               DrawDottedLine(obs[j].name + "_" + levName,           drawStart, lev_p, drawEnd, c);
               DrawTextEx    (obs[j].name + "_" + levName + "_label", levName,   labelTime, lev_p, c, isBuy);
              }
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Remove expired OBs from the tracking array (keep chart objects)  |
//+------------------------------------------------------------------+
void CleanupExpiredOBs(datetime curBarTime)
  {
   bool removed = false;
   //--- Iterate in reverse to safely remove expired entries
   for (int j = ArraySize(obs) - 1; j >= 0; j--)
     {
      //--- Remove from array once the current bar has passed the OB expiry
      if (curBarTime > obs[j].origEndTime)
        {
         if (prt) Print("Expired OB removed from storage (kept on chart): ", obs[j].name, " endTime=", TimeToString(obs[j].origEndTime));
         ArrayRemove(obs, j, 1);
         removed = true;
        }
     }
   //--- Log the updated OB list after any removals
   if (removed) PrintOBs();
  }

Функция TradeOnOBs начинает с получения текущих цен Ask и Bid, баланса счета и параметров объема символа, необходимых для расчета лота. Затем она перебирает все отслеживаемые блоки ордеров, пропуская зоны без свежего сигнала, не прошедшие проверку или заблокированные настройкой торговли по митигированным зонам.

Перед размещением любого ордера применяются ограничения торгового режима: зоны, по которым уже была открыта сделка, пропускаются в режиме TradeOnce, а зоны, достигшие максимального числа сделок, – в режиме LimitedTrades; в обоих случаях флаг сигнала сбрасывается. Цена входа задается по Ask для бычьих зон и по Bid для медвежьих, а стоп-лосс размещается ниже минимума блока ордеров для покупок и выше максимума для продаж с применением заданного смещения в пунктах через NormalizeDouble. Затем расстояние риска вместе с балансом счета и значением тика используется для расчета точного размера лота, который округляется до шага лота брокера с помощью MathRound и ограничивается минимальным и максимальным значениями объема. Тейк-профит задается путем откладывания расстояния риска в направлении сделки с учетом настроенного соотношения риска и вознаграждения.

Ордера на покупку размещаются с помощью метода Buy , а ордера на продажу – с помощью Sell, в обоих случаях через объект CTrade. Если ордер подтвержден с кодом TRADE_RETCODE_DONE, возвращенный тикет регистрируется в массиве сделок для трейлинга на основе соотношения риска и вознаграждения. Если этот режим активен, счетчик сделок для зоны увеличивается, а флаг сигнала сбрасывается. Затем метка зоны обновляется, чтобы отразить новое количество сделок. Когда включена визуализация уровней, для входа, стоп-лосса и каждого последующего уровня тейк-профита вплоть до полного соотношения риска и вознаграждения рисуются пунктирные линии и жирные текстовые метки. Каждый промежуточный уровень последовательно получает метки "TP1", "TP2" и так далее, а финальный уровень – метку "FullTP"; при этом используется набор оттенков зеленого, заданный в локальном массиве цветов.

Функция CleanupExpiredOBs перебирает массив отслеживания в обратном порядке и удаляет все зоны, для которых текущее время бара уже превысило время истечения, используя ArrayRemove для безопасного удаления на месте. Функция намеренно оставляет прямоугольники на графике в качестве ориентира, а после удаления записей выводит обновленное состояние массива в лог. Когда эти функции вызываются в обработчике события тика, получается следующий результат.

TRADED SETUP

Скриншот показывает, что сетапы с подтвержденными сигналами открывают соответствующие сделки. Остался последний шаг – деинициализация программы.

Деинициализация и обработчики событий графика

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   //--- Delete all OB rectangle objects and associated labels and icons
   for (int i = 0; i < ArraySize(obs); i++)
     {
      ObjectDelete(0, obs[i].name);
      ObjectDelete(0, obs[i].name + "_Label");
      ObjectDelete(0, obs[i].name + "_MitIcon");
     }
   //--- Delete all FVG rectangle objects
   for (int i = 0; i < ArraySize(fvg_names); i++)
      ObjectDelete(0, fvg_names[i]);
   //--- Delete all BOS, inducement, and OB prefixed objects
   ObjectsDeleteAll(0, BOS_Prefix);
   ObjectsDeleteAll(0, IND_Prefix);
   ObjectsDeleteAll(0, OB_Prefix);
   //--- Remove the trend label from the main chart
   ObjectDelete(0, "EA_TrendLabel");
   //--- Release all dynamic arrays
   ArrayResize(obs,       0);
   ArrayResize(fvg_names, 0);
   ArrayResize(trades,    0);
   //--- Close the higher timeframe chart if it was opened by this EA
   if (chart_id > 0) { ChartClose(chart_id); chart_id = -1; }
   //--- Refresh the main chart after cleanup
   ChartRedraw(0);
   if (prt) Print("Deinit: Deleted all objects, reset arrays, closed higher TF chart if open.");
//---
  }

//+------------------------------------------------------------------+
//| Expert chart event function                                      |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   //--- Respond to chart scale or size changes by updating all font sizes
   if (id == CHARTEVENT_CHART_CHANGE) UpdateFontSizes();
  }

В обработчике событий OnDeinit мы выполняем полную очистку всех объектов и ресурсов, созданных за время работы программы. Мы перебираем массив блоков ордеров и по отдельности удаляем прямоугольник, метку и значок митигации каждой зоны с помощью функции ObjectDelete. Все прямоугольники разрыва справедливой стоимости удаляются во втором цикле, а затем ObjectsDeleteAll используется для удаления всех оставшихся объектов с префиксами пробоя структуры, провокации и блока ордеров, которые могли быть пропущены. Метка тренда также удаляется. После этого все три динамических массива освобождаются путем изменения их размера на ноль, а если программа открыла график старшего таймфрейма, он закрывается с помощью ChartClose, при этом его идентификатор сбрасывается до значения -1. Финальная перерисовка визуально очищает график.

В обработчике событий OnChartEvent мы отслеживаем событие CHARTEVENT_CHART_CHANGE, которое срабатывает при каждом изменении размера графика или масштаба. При его обнаружении мы вызываем UpdateFontSizes, чтобы пересчитать и применить подходящий размер шрифта ко всем текстовым объектам и сохранить читаемость меток при любом масштабе. Далее переходим к тестированию программы на исторических данных, которое рассматривается в следующем разделе.



Тестирование на исторических данных

Мы протестировали программу на паре AUDUSD на минутном таймфрейме с помощью тестера стратегий MetaTrader 5. Ниже представлен результат в формате GIF.

BACKTEST GIF

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

График тестирования на исторических данных:

GRAPH

Отчет о тестировании на исторических данных:

REPORT


Заключение

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

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

После прочтения этой статьи вы сможете:

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

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

Прикрепленные файлы |
Рыночные секреты Ларри Уильямса (Часть 14): Обнаружение разворотов Hidden Smash Day с помощью пользовательского индикатора Рыночные секреты Ларри Уильямса (Часть 14): Обнаружение разворотов Hidden Smash Day с помощью пользовательского индикатора
В этой статье разрабатывается практический индикатор MQL5, который обнаруживает бары Hidden Smash Day по строгим числовым критериям и, при необходимости, по подтверждению на следующей сессии. Рассматриваются процедуры обнаружения, регистрация буферов и настройка отрисовки, позволяющая размещать стрелки на барах, соответствующих условиям. Такой подход дает стабильные, не перерисовывающиеся сигналы для исторического тестирования и мониторинга в реальном времени.
Разработка инструментария для анализа Price Action (Часть 67): Автоматизация мониторинга уровней поддержки и сопротивления в MQL5 Разработка инструментария для анализа Price Action (Часть 67): Автоматизация мониторинга уровней поддержки и сопротивления в MQL5
В этой статье реализован полноценный советник на языке MQL5, который в реальном времени отслеживает вручную построенные уровни поддержки и сопротивления. Он синхронизирует горизонтальные линии, обнаруживает приближения, касания, пробои, развороты и ретесты, а также выполняет проверку свечных паттернов при необходимости. Алерты и маркеры на графике обеспечивают четкую и воспроизводимую обратную связь, позволяя сохранить ручной анализ и при этом автоматизировать отслеживание ключевых ценовых уровней.
Разработка инструментария для анализа Price Action (Часть 68): Панель RSI с привязкой к цене на языке MQL5 Разработка инструментария для анализа Price Action (Часть 68): Панель RSI с привязкой к цене на языке MQL5
Мы представляем встроенную в график панель RSI, которая устраняет необходимость в отдельном окне, привязывая данные о моментуме непосредственно к текущей цене. В статье рассматриваются концепция решения и код на MQL5: получение значений RSI в реальном времени, классификация сигналов по наклону и адаптивное позиционирование. Трейдеры получают значение RSI, состояние и силу сигнала прямо в точке принятия решения, что повышает ясность анализа на разных таймфреймах.
Рыночные секреты Ларри Уильямса (Часть 13): Автоматизация разворотных паттернов Hidden Smash Day Рыночные секреты Ларри Уильямса (Часть 13): Автоматизация разворотных паттернов Hidden Smash Day
В статье создается прозрачный советник MQL5 для скрытых разворотов Smash Day Ларри Уильямса. Сигналы формируются только на новых барах: сначала проверяется сетап-бар, затем он подтверждается, когда следующая сессия торгуется за его экстремумом. Риск управляется через ATR или структурные стопы с заданным соотношением риска и прибыли, размер позиции может быть фиксированным или рассчитываться от баланса, а фильтры направления и правило одной позиции помогают обеспечить воспроизводимость тестов.