English
preview
Разработка инструментария для анализа Price Action (Часть 71): Картирование структуры гэпов выходного дня в MQL5

Разработка инструментария для анализа Price Action (Часть 71): Картирование структуры гэпов выходного дня в MQL5

MetaTrader 5Торговые системы |
53 0
Christian Benjamin
Christian Benjamin

Содержание


Введение

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

В статье разрабатывается система MQL5 для гэпов выходного дня, которая определяет гэпы непосредственно по разрывам времени торговых сессий и отображает их как структурированные графические объекты. Индикатор отображает диапазон гэпа, его середину и метки уровней. Он сохраняет старые гэпы как исторические зоны и обновляет активные гэпы в реальном времени, используя состояния: FRESH (свежий), PARTIAL (частично заполнен), REACTION (реакция) или FILLED (заполнен). Реализация сосредоточена на стабильной визуализации графика, управлении жизненным циклом объектов и воспроизводимом отслеживании гэпов. Эта система впоследствии может поддерживать более сложный анализ взаимодействия цены с зоной и автоматизированную логику торговли по гэпам выходного дня.


Знакомство с концепцией

Что такое гэп

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

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

Что вызывает рыночные гэпы

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

На практике гэпы в основном обусловлены тремя основными условиями:

1. Перебои в непрерывном торговом потоке

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

2. Дисбаланс ликвидности и задержка в поступлении ордеров

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

3. Дисбаланс между Bid и Ask на цене открытия

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

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

Гэп выходного дня

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

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

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

С точки зрения структуры гэп выходного дня определяется сравнением двух ключевых точек:

  1. Итоговая цена закрытия в пятницу
  2. Цена открытия в понедельник

Разница между этими двумя уровнями образует зону гэпа выходного дня. На графике он выглядит как пустой ценовой промежуток между последней свечой недели и первой свечой новой недели.

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

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

Почему гэп выходного дня важен в торговле на рынке Форекс

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

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

Гэпы выходного дня важны по нескольким причинам:

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

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

Обзор системы

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

Система построена на трех основных компонентах:

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

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

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


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

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

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

Свойства и настройка индикатора

На первом этапе определяется, как индикатор будет работать в MetaTrader 5. Указывается, что индикатор должен отображаться непосредственно в основном окне графика, а не в отдельном подокне. Поскольку цель состоит в отображении структурированных графических объектов, а не традиционного линейного индикатора, код не создает буферов или графических построений. Вместо этого используются графические объекты OBJ_RECTANGLE, OBJ_VLINE, OBJ_TREND и OBJ_TEXT, чтобы каждый гэп можно было независимо отображать, обновлять и сохранять на графике.

//+------------------------------------------------------------------+
//|                                          WeekendGapIndicator.mq5 |
//|                              Copyright 2026, Christian Benjamin. |
//|                          https://www.mql5.com/ru/users/lynnchris |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Christian Benjamin."
#property link      "https://www.mql5.com/ru/users/lynnchris"
#property version   "1.0"
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

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

Пользовательские параметры и варианты настройки

Далее определяются несколько входных параметров, которые трейдеры могут настраивать в соответствии со своими предпочтениями. Эти настройки определяют визуальное отображение и объем исторической структуры, сохраняемой на графике. Благодаря этому индикатор адаптируется к различным темам графика и стилям торговли. MinGapPips задает минимальный учитываемый размер гэпа. Значение по умолчанию – 0,5 пипса, что отфильтровывает микрогэпы. ShowHistoricalGaps и MaxHistoricalWeeks определяют, отображаются ли прошлые недели и в каком количестве. Визуальные входные параметры – цвет заливки, цвет контура, непрозрачность и размер шрифта – позволяют системе адаптироваться к различным темам графика, сохраняя читаемость.

//+------------------------------------------------------------------+
//| INPUTS                                                           |
//+------------------------------------------------------------------+
input bool     ShowHistoricalGaps  = true;          // Show historical gaps
input int      MaxHistoricalWeeks  = 26;            // Maximum historical weeks (visual only)
input double   MinGapPips          = 0.5;           // Minimum gap size in pips
input bool     ShowDetailedLabels  = true;          // Show Fri Close / Mon Open / Mid levels
input color    ActiveFillColor     = clrGainsboro;  // Very light grey fill
input color    ActiveOutlineColor  = clrDimGray;    // Dark grey outline
input color    ReactionColor       = clrDarkOrange; // Reaction state
input color    MemoryOutlineColor  = clrSilver;     // Light silver memory lines
input int      ActiveFillOpacity   = 25;            // Fill opacity (0-100)
input int      LineWidth           = 2;             // Outline width
input int      FontSize            = 7;             // Small professional labels

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

Глобальные переменные и структуры данных

Чтобы поддерживать порядок в коде, определяются две структуры и несколько глобальных переменных. Перечисление ENUM_GAP_STATE описывает жизненный цикл каждого гэпа, а структура WeekendGapRecord хранит все структурные данные, необходимые для отслеживания одного гэпа выходного дня во времени. Структура VisualSettings объединяет параметры оформления, чтобы визуальным поведением можно было централизованно управлять из одного места.

//+------------------------------------------------------------------+
//| ENUMS & STRUCTURES                                               |
//+------------------------------------------------------------------+
enum ENUM_GAP_STATE
  {
   GAP_FRESH,
   GAP_PARTIAL,
   GAP_REACTION,
   GAP_FILLED,
   GAP_HISTORICAL
  };

struct WeekendGapRecord
  {
   datetime          startTime;
   datetime          endTime;
   double            gapHigh;
   double            gapLow;
   double            midpoint;
   bool              isGapDown;
   ENUM_GAP_STATE    state;
   bool              activeWeek;
  };

struct VisualSettings
  {
   color             activeFillColor;
   color             activeOutlineColor;
   color             reactionColor;
   color             memoryOutlineColor;
   int               activeFillOpacity;
   int               lineWidth;
   int               fontSize;
  };

//+------------------------------------------------------------------+
//| GLOBAL VARIABLES                                                 |
//+------------------------------------------------------------------+
WeekendGapRecord   m_gaps[];
VisualSettings     m_vis;
datetime           m_lastBarTime = 0;
bool               m_firstRun = true

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

Инициализация (OnInit) – подготовка данных и визуальных настроек

При загрузке индикатора функция OnInit() подготавливает визуальные настройки, необходимые для отображения в реальном времени. Она копирует все входные параметры в структуру m_vis для удобного доступа и не перегружает этап инициализации. Фактическое обнаружение гэпов откладывается до первого вызова OnCalculate(), чтобы индикатор мог быстро завершить загрузку перед сканированием исторических данных.

//+------------------------------------------------------------------+
//| EVENT HANDLERS                                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   m_vis.activeFillColor    = ActiveFillColor;
   m_vis.activeOutlineColor = ActiveOutlineColor;
   m_vis.reactionColor      = ReactionColor;
   m_vis.memoryOutlineColor = MemoryOutlineColor;
   m_vis.activeFillOpacity  = ActiveFillOpacity;
   m_vis.lineWidth          = LineWidth;
   m_vis.fontSize           = FontSize;

   m_firstRun = true;
   return(INIT_SUCCEEDED);
  }

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

Очистка (OnDeinit) – удаление объектов и освобождение ресурсов

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

//+------------------------------------------------------------------+
//| OnDeinit: Clean up objects on deinitialization                   |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ObjectsDeleteAll(0, "WG_");
  }

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

Основной расчет (OnCalculate) – определение гэпов и обновление отображения

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

//+------------------------------------------------------------------+
//| OnCalculate: Main indicator calculation function                 |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   if(rates_total < 2)
      return(0);

   if(m_firstRun)
     {
      DetectAllGaps(time, open, close, rates_total);
      for(int i = 0; i < ArraySize(m_gaps); i++)
         CreateGapObjects(m_gaps[i], PrefixForIndex(i));
      m_firstRun = false;
     }

   UpdateCurrentState();

   datetime currentBarTime = time[0];
   if(currentBarTime != m_lastBarTime)
     {
      m_lastBarTime = currentBarTime;
      datetime barMonday = GetWeekMonday(currentBarTime);
      for(int i = 0; i < ArraySize(m_gaps); i++)
        {
         if(m_gaps[i].activeWeek && m_gaps[i].startTime < barMonday)
           {
            m_gaps[i].activeWeek = false;
            m_gaps[i].state = GAP_HISTORICAL;
            UpdateGapVisuals(m_gaps[i], PrefixForIndex(i));
           }
        }
     }

   return(rates_total);
  }

Фаза 1 – однократное определение гэпов

При первом вызове DetectAllGaps() сканирует исторические ценовые данные на предмет разрывов выходного дня продолжительностью 48 часов и более между барами. Для каждого гэпа, соответствующего минимальному порогу в пипсах, функция создает структуру WeekendGapRecord и сохраняет ее в массиве m_gaps. Затем индикатор вызывает CreateGapObjects() для отображения прямоугольника, вертикальной линии, линии середины и меток каждого гэпа. Однократное выполнение этого шага предотвращает ненужное повторное сканирование истории цен на каждом тике и сохраняет легковесность индикатора при работе графика в реальном времени.

Фаза 2 – обновление состояния в реальном времени

UpdateCurrentState() выполняется на каждом тике и оценивает взаимодействие текущей цены с активным диапазоном гэпа. Она сравнивает текущую цену Bid с сохраненными границами и обновляет состояние гэпа: FRESH, PARTIAL, REACTION или FILLED. Каждый раз при изменении состояния UpdateGapVisuals() обновляет стиль объекта и текст метки, чтобы график всегда отражал текущее структурное состояние гэпа.

Фаза 3 – переход к новой неделе

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

Определение гэпов (DetectAllGaps) – подробный разбор

Функция определения гэпов является основной функцией индикатора. Она копирует данные времени, цены открытия и цены закрытия, затем перебирает бары в поисках разрыва выходного дня.

//+------------------------------------------------------------------+
//| DETECT WEEKEND GAPS (time gap)                                   |
//+------------------------------------------------------------------+
void DetectAllGaps(const datetime &time[],
                   const double   &open[],
                   const double   &close[],
                   int             rates_total)
  {
   ArrayResize(m_gaps, 0);
   if(rates_total < 2)
      return;

   datetime timeArr[];
   double   openArr[];
   double   closeArr[];
   ArraySetAsSeries(timeArr, true);
   ArraySetAsSeries(openArr, true);
   ArraySetAsSeries(closeArr, true);
   if(CopyTime(_Symbol, _Period, 0, rates_total, timeArr) < 0)
      return;
   if(CopyOpen(_Symbol, _Period, 0, rates_total, openArr) < 0)
      return;
   if(CopyClose(_Symbol, _Period, 0, rates_total, closeArr) < 0)
      return;

   double pip = PipSize();
   int limit = MathMin(rates_total - 1, 10000);

   for(int i = 1; i < limit; i++)
     {
      double diffSeconds = (double)(timeArr[i-1] - timeArr[i]);
      if(diffSeconds >= 172800)   //--- 48 hours
        {
         double fridayClose = closeArr[i];
         double mondayOpen  = openArr[i-1];
         double gapPips = MathAbs(mondayOpen - fridayClose) / pip;

         if(gapPips >= MinGapPips)
           {
            WeekendGapRecord gap;
            gap.startTime  = timeArr[i-1];
            gap.endTime    = GetNextMondayOpen(timeArr[i-1]);
            gap.gapHigh    = MathMax(mondayOpen, fridayClose);
            gap.gapLow     = MathMin(mondayOpen, fridayClose);
            gap.midpoint   = (gap.gapHigh + gap.gapLow) / 2.0;
            gap.isGapDown  = (fridayClose > mondayOpen);
            gap.activeWeek = (GetWeekMonday(timeArr[i-1]) == GetWeekMonday(TimeCurrent()));
            gap.state      = gap.activeWeek ? GAP_FRESH : GAP_HISTORICAL;

            int size = ArraySize(m_gaps);
            ArrayResize(m_gaps, size + 1);
            m_gaps[size] = gap;
           }
        }
     }

   Print("Weekend Gap Indicator: Detected ", ArraySize(m_gaps), " gaps.");
  }

Массивы настраиваются как серии с помощью ArraySetAsSeries(true), поэтому индекс 0 соответствует самому новому бару. Это упрощает логику цикла: i-1 соответствует более новому бару, а i – более старому. Разрыв выходного дня определяется, когда разница во времени между двумя соседними барами достигает 172800 секунд или более. Это позволяет индикатору определять разрывы выходного дня, даже если брокер включает воскресную сессию. Размер гэпа в пипсах рассчитывается с помощью PipSize(), которая автоматически адаптируется к символам с тремя и пятью знаками после запятой. Флаг activeWeek определяется сравнением понедельника обнаруженного гэпа с понедельником текущего времени на графике. Все обнаруженные гэпы сохраняются в массиве m_gaps, что позволяет индикатору хранить текущую и историческую структуру в одном постоянном наборе данных.

Создание и обновление графических объектов

Графическими объектами управляют две основные функции: CreateGapObjects() создает все объекты с нуля, а UpdateGapVisuals() изменяет существующие объекты при смене состояния гэпа.

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

//+------------------------------------------------------------------+
//| CreateGapObjects: Draw rectangle, lines and labels for a gap     |
//+------------------------------------------------------------------+
void CreateGapObjects(const WeekendGapRecord &gap, string prefix)
  {
   datetime leftTime  = gap.startTime;
   datetime rightEdge = GetNextMondayOpen(gap.startTime);
   long     weekLength = rightEdge - leftTime;

//--- Rectangle
   ObjectCreate(0, prefix + "_RECT", OBJ_RECTANGLE, 0,
                leftTime, gap.gapHigh, rightEdge, gap.gapLow);
   ObjectSetInteger(0, prefix + "_RECT", OBJPROP_COLOR, m_vis.activeOutlineColor);
   ObjectSetInteger(0, prefix + "_RECT", OBJPROP_STYLE, STYLE_SOLID);
   ObjectSetInteger(0, prefix + "_RECT", OBJPROP_WIDTH, m_vis.lineWidth);
   ObjectSetInteger(0, prefix + "_RECT", OBJPROP_BACK, false);
   ObjectSetInteger(0, prefix + "_RECT", OBJPROP_FILL, true);
   ObjectSetInteger(0, prefix + "_RECT", OBJPROP_BGCOLOR,
                    ColorSetAlpha(m_vis.activeFillColor, (uchar)(m_vis.activeFillOpacity * 255 / 100)));

//--- Bold black week separator line
   ObjectCreate(0, prefix + "_VMARK", OBJ_VLINE, 0, leftTime, 0);
   ObjectSetInteger(0, prefix + "_VMARK", OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(0, prefix + "_VMARK", OBJPROP_STYLE, STYLE_SOLID);
   ObjectSetInteger(0, prefix + "_VMARK", OBJPROP_WIDTH, 2);
   ObjectSetInteger(0, prefix + "_VMARK", OBJPROP_BACK, false);

//--- Midpoint line
   double mid = (gap.gapHigh + gap.gapLow) / 2.0;
   ObjectCreate(0, prefix + "_MID", OBJ_TREND, 0,
                leftTime, mid, rightEdge, mid);
   ObjectSetInteger(0, prefix + "_MID", OBJPROP_COLOR, clrDarkGray);
   ObjectSetInteger(0, prefix + "_MID", OBJPROP_STYLE, STYLE_DASH);
   ObjectSetInteger(0, prefix + "_MID", OBJPROP_WIDTH, 1);
   ObjectSetInteger(0, prefix + "_MID", OBJPROP_RAY_RIGHT, false);

//--- Main summary label – bold white, placed at 25% of week width
   double pipDist = (gap.gapHigh - gap.gapLow) / PipSize();
   string text = StringFormat("WG | %.1fp | %s", pipDist, StateToString(gap.state));
   datetime mainLabelTime = (datetime)(leftTime + (long)(weekLength * 0.25));
   ObjectCreate(0, prefix + "_LBL", OBJ_TEXT, 0, mainLabelTime, mid);
   ObjectSetString(0, prefix + "_LBL", OBJPROP_TEXT, text);
   ObjectSetInteger(0, prefix + "_LBL", OBJPROP_FONTSIZE, m_vis.fontSize);
   ObjectSetInteger(0, prefix + "_LBL", OBJPROP_COLOR, clrWhite);
   ObjectSetString(0, prefix + "_LBL", OBJPROP_FONT, "Arial Bold");
   ObjectSetInteger(0, prefix + "_LBL", OBJPROP_ANCHOR, ANCHOR_CENTER);

//--- Detailed level labels – inside the rectangle, bold white
   if(ShowDetailedLabels)
     {
      datetime detailTime = (datetime)(leftTime + (long)(weekLength * 0.05));
      double gapHeight = gap.gapHigh - gap.gapLow;
      double offset = gapHeight * 0.1;
      int smallFont = MathMax(m_vis.fontSize - 1, 6);

      //--- Friday close
      double friPrice = gap.isGapDown ? gap.gapHigh : gap.gapLow;
      string friText = StringFormat("Fri Close: %." + IntegerToString(_Digits) + "f", friPrice);
      ObjectCreate(0, prefix + "_TOP", OBJ_TEXT, 0, detailTime, gap.gapHigh - offset);
      ObjectSetString(0, prefix + "_TOP", OBJPROP_TEXT, friText);
      ObjectSetInteger(0, prefix + "_TOP", OBJPROP_FONTSIZE, smallFont);
      ObjectSetInteger(0, prefix + "_TOP", OBJPROP_COLOR, clrWhite);
      ObjectSetString(0, prefix + "_TOP", OBJPROP_FONT, "Arial Bold");
      ObjectSetInteger(0, prefix + "_TOP", OBJPROP_ANCHOR, ANCHOR_LEFT);

      //--- Monday open
      double monPrice = gap.isGapDown ? gap.gapLow : gap.gapHigh;
      string monText = StringFormat("Mon Open: %." + IntegerToString(_Digits) + "f", monPrice);
      ObjectCreate(0, prefix + "_BOT", OBJ_TEXT, 0, detailTime, gap.gapLow + offset);
      ObjectSetString(0, prefix + "_BOT", OBJPROP_TEXT, monText);
      ObjectSetInteger(0, prefix + "_BOT", OBJPROP_FONTSIZE, smallFont);
      ObjectSetInteger(0, prefix + "_BOT", OBJPROP_COLOR, clrWhite);
      ObjectSetString(0, prefix + "_BOT", OBJPROP_FONT, "Arial Bold");
      ObjectSetInteger(0, prefix + "_BOT", OBJPROP_ANCHOR, ANCHOR_LEFT);

      //--- Midpoint price
      string midText = StringFormat("Mid: %." + IntegerToString(_Digits) + "f", gap.midpoint);
      ObjectCreate(0, prefix + "_MIDPRICE", OBJ_TEXT, 0, detailTime, gap.midpoint);
      ObjectSetString(0, prefix + "_MIDPRICE", OBJPROP_TEXT, midText);
      ObjectSetInteger(0, prefix + "_MIDPRICE", OBJPROP_FONTSIZE, smallFont);
      ObjectSetInteger(0, prefix + "_MIDPRICE", OBJPROP_COLOR, clrWhite);
      ObjectSetString(0, prefix + "_MIDPRICE", OBJPROP_FONT, "Arial Bold");
      ObjectSetInteger(0, prefix + "_MIDPRICE", OBJPROP_ANCHOR, ANCHOR_LEFT);
     }
  }

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

//+------------------------------------------------------------------+
//| UpdateGapVisuals: Change appearance based on state (active/hist) |
//+------------------------------------------------------------------+
void UpdateGapVisuals(const WeekendGapRecord &gap, string prefix)
  {
   if(ObjectFind(0, prefix + "_RECT") < 0)
      return;

   if(!gap.activeWeek || gap.state == GAP_HISTORICAL)
     {
      //--- Historical – faint silver outline, no fill
      ObjectSetInteger(0, prefix + "_RECT", OBJPROP_COLOR, m_vis.memoryOutlineColor);
      ObjectSetInteger(0, prefix + "_RECT", OBJPROP_STYLE, STYLE_DASH);
      ObjectSetInteger(0, prefix + "_RECT", OBJPROP_WIDTH, 1);
      ObjectSetInteger(0, prefix + "_RECT", OBJPROP_BGCOLOR, clrNONE);
      ObjectSetInteger(0, prefix + "_RECT", OBJPROP_FILL, false);

      //--- Main label – dark grey, normal
      if(ObjectFind(0, prefix + "_LBL") >= 0)
        {
         ObjectSetInteger(0, prefix + "_LBL", OBJPROP_COLOR, clrDimGray);
         ObjectSetString(0, prefix + "_LBL", OBJPROP_FONT, "Arial");
        }
      //--- Detailed labels – dim grey, normal
      if(ObjectFind(0, prefix + "_TOP") >= 0)
        {
         ObjectSetInteger(0, prefix + "_TOP", OBJPROP_COLOR, clrDimGray);
         ObjectSetString(0, prefix + "_TOP", OBJPROP_FONT, "Arial");
        }
      if(ObjectFind(0, prefix + "_BOT") >= 0)
        {
         ObjectSetInteger(0, prefix + "_BOT", OBJPROP_COLOR, clrDimGray);
         ObjectSetString(0, prefix + "_BOT", OBJPROP_FONT, "Arial");
        }
      if(ObjectFind(0, prefix + "_MIDPRICE") >= 0)
        {
         ObjectSetInteger(0, prefix + "_MIDPRICE", OBJPROP_COLOR, clrDimGray);
         ObjectSetString(0, prefix + "_MIDPRICE", OBJPROP_FONT, "Arial");
        }
     }
   else
     {
      //--- Active week – solid outline with light fill
      ObjectSetInteger(0, prefix + "_RECT", OBJPROP_COLOR, m_vis.activeOutlineColor);
      ObjectSetInteger(0, prefix + "_RECT", OBJPROP_STYLE, STYLE_SOLID);
      ObjectSetInteger(0, prefix + "_RECT", OBJPROP_WIDTH, m_vis.lineWidth);
      ObjectSetInteger(0, prefix + "_RECT", OBJPROP_FILL, true);
      ObjectSetInteger(0, prefix + "_RECT", OBJPROP_BGCOLOR,
                       ColorSetAlpha(m_vis.activeFillColor, (uchar)(m_vis.activeFillOpacity * 255 / 100)));

      //--- Main label – bold white
      if(ObjectFind(0, prefix + "_LBL") >= 0)
        {
         ObjectSetInteger(0, prefix + "_LBL", OBJPROP_COLOR, clrWhite);
         ObjectSetString(0, prefix + "_LBL", OBJPROP_FONT, "Arial Bold");
        }
      //--- Detailed labels – bold white
      if(ObjectFind(0, prefix + "_TOP") >= 0)
        {
         ObjectSetInteger(0, prefix + "_TOP", OBJPROP_COLOR, clrWhite);
         ObjectSetString(0, prefix + "_TOP", OBJPROP_FONT, "Arial Bold");
        }
      if(ObjectFind(0, prefix + "_BOT") >= 0)
        {
         ObjectSetInteger(0, prefix + "_BOT", OBJPROP_COLOR, clrWhite);
         ObjectSetString(0, prefix + "_BOT", OBJPROP_FONT, "Arial Bold");
        }
      if(ObjectFind(0, prefix + "_MIDPRICE") >= 0)
        {
         ObjectSetInteger(0, prefix + "_MIDPRICE", OBJPROP_COLOR, clrWhite);
         ObjectSetString(0, prefix + "_MIDPRICE", OBJPROP_FONT, "Arial Bold");
        }
     }

//--- Update midpoint visibility
   if(ObjectFind(0, prefix + "_MID") >= 0)
      ObjectSetInteger(0, prefix + "_MID", OBJPROP_TIMEFRAMES,
                       (gap.activeWeek && gap.state != GAP_HISTORICAL) ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS);

//--- Refresh main label text
   if(ObjectFind(0, prefix + "_LBL") >= 0)
     {
      double pipDist = (gap.gapHigh - gap.gapLow) / PipSize();
      string text = StringFormat("WG | %.1fp | %s", pipDist, StateToString(gap.state));
      ObjectSetString(0, prefix + "_LBL", OBJPROP_TEXT, text);
     }
  }

Машина состояний для активных гэпов (UpdateCurrentState)

Функция UpdateCurrentState() определяет машину состояний, управляющую изменением каждого активного гэпа во времени. Она выполняется на каждом тике и сравнивает текущую цену Bid с сохраненными верхней и нижней границами. Это позволяет индикатору последовательно классифицировать взаимодействие цены с зоной, не полагаясь на субъективную интерпретацию.

//+------------------------------------------------------------------+
//| STATE UPDATE (active week only)                                  |
//+------------------------------------------------------------------+
void UpdateCurrentState()
  {
   double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   for(int i = 0; i < ArraySize(m_gaps); i++)
     {
      if(!m_gaps[i].activeWeek)
         continue;
      if(m_gaps[i].state == GAP_HISTORICAL || m_gaps[i].state == GAP_FILLED)
         continue;

      bool isGapDown = m_gaps[i].isGapDown;
      double high = m_gaps[i].gapHigh;
      double low  = m_gaps[i].gapLow;
      ENUM_GAP_STATE newState = m_gaps[i].state;

      if(isGapDown)
        {
         if(currentPrice > high)
            newState = GAP_FILLED;
         else
            if(currentPrice > low && currentPrice <= high)
              {
               if(m_gaps[i].state == GAP_FRESH)
                  newState = GAP_PARTIAL;
              }
            else
               if(currentPrice <= low)
                 {
                  if(m_gaps[i].state == GAP_PARTIAL)
                     newState = GAP_REACTION;
                 }
        }
      else
        {
         if(currentPrice < low)
            newState = GAP_FILLED;
         else
            if(currentPrice >= low && currentPrice < high)
              {
               if(m_gaps[i].state == GAP_FRESH)
                  newState = GAP_PARTIAL;
              }
            else
               if(currentPrice >= high)
                 {
                  if(m_gaps[i].state == GAP_PARTIAL)
                     newState = GAP_REACTION;
                 }
        }

      if(newState != m_gaps[i].state)
        {
         m_gaps[i].state = newState;
         UpdateGapVisuals(m_gaps[i], PrefixForIndex(i));
        }
     }
  }

Гэп вниз (цена закрытия в пятницу выше цены открытия в понедельник): зона гэпа простирается от нижней границы (цены открытия в понедельник) до верхней границы (цены закрытия в пятницу).

  • Цена выше верхней границы → гэп полностью заполнен (GAP_FILLED).
  • Цена внутри гэпа → GAP_PARTIAL, только если ранее состояние было FRESH.
  • Цена ниже нижней границы после состояния PARTIAL → GAP_REACTION (ретест противоположной границы гэпа).

Гэп вверх (цена закрытия в пятницу ниже цены открытия в понедельник): зона гэпа простирается от нижней границы (цены закрытия в пятницу) до верхней границы (цены открытия в понедельник). Логика зеркальна.

Изменение состояния вызывает UpdateGapVisuals(), которая обновляет цвет контура прямоугольника и текст метки. Входной параметр ReactionColor пока не используется, но позднее может применяться для выделения прямоугольника при состоянии GAP_REACTION.

Вспомогательные функции – поддержка основной логики

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

double PipSize()
  {
   return (_Digits == 3 || _Digits == 5) ? (_Point * 10.0) : _Point;
  }

GetWeekMonday() для любого заданного значения datetime возвращает значение datetime для понедельника в 00:00 на той же неделе. Она корректно обрабатывает воскресенье (day_of_week == 0), вычитая 6 дней.

datetime GetWeekMonday(datetime t)
  {
   MqlDateTime dt;
   TimeToStruct(t, dt);
   int daysSinceMonday = (dt.day_of_week == 0) ? 6 : (dt.day_of_week - 1);
   return t - daysSinceMonday * 86400 - (t % 86400);
  }

GetNextMondayOpen() просто добавляет семь дней к значению datetime понедельника, чтобы определить правую границу прямоугольника.

datetime GetNextMondayOpen(datetime thisMondayOpenTime)
  {
   return thisMondayOpenTime + 7 * 86400;
  }

ColorSetAlpha() применяет альфа-канал к цвету MQL5, позволяя создавать полупрозрачные заливки. Входное значение ActiveFillOpacity (0-100) преобразуется в значение альфа-канала от 0 до 255.

color ColorSetAlpha(color clr, uchar alpha)
  {
   return (color)((clr & 0x00FFFFFF) | ((uchar)alpha << 24));
  }

StateToString() преобразует перечисление ENUM_GAP_STATE в короткую строку для отображения в основной метке.

string StateToString(ENUM_GAP_STATE state)
  {
   switch(state)
     {
      case GAP_FRESH:     return "FRESH";
      case GAP_PARTIAL:   return "PARTIAL";
      case GAP_REACTION:  return "REACTION";
      case GAP_FILLED:    return "FILLED";
      case GAP_HISTORICAL:return "HIST";
     }
   return "";
  }

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


Результаты

Чтобы протестировать индикатор, загрузите прикрепленный файл исходного кода на компьютер и скомпилируйте его в MetaEditor. Затем откройте MetaTrader 5, перейдите в раздел "Индикаторы" и перетащите индикатор на график.

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

Каждая структура гэпа выходного дня отображается как полупрозрачная структурная зона со срединной линией, проведенной через центр гэпа. Метки на графике отображают идентификатор гэпа (WG), измеренный размер гэпа в пипсах и текущее структурное состояние гэпа. В большинстве исторических случаев состояние отображается как HIST. Это означает, что гэп относится к уже завершенной торговой неделе и рассматривается как исторический.

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

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

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


Заключение

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

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

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

Прикрепленные файлы |
Моделирование рынка: Position View (XV) Моделирование рынка: Position View (XV)
В этой статье я постараюсь как можно проще объяснить, как можно использовать обмен сообщениями между приложениям. Цель в том, чтобы вы могли создать что-то работоспособное максимально простым и эффективным способом, когда это возможно. Не знаю, удастся ли мне донести идею, лежащую в основе этой концепции, поскольку тому, кто сталкивается с ней впервые, её не так-то просто понять. Кроме того, я воспользуюсь случаем, чтобы показать вам, как изменить систему репликации/моделирования, чтобы вы могли отлаживать советник или любой другой код, который разрабатываете. И всё это — столь же просто и прямо.
Возможности Мастера MQL5, которые вам нужно знать (Часть 92): Использование индексации B-дерева и байесовской нейронной сети в пользовательском классе сигнала Возможности Мастера MQL5, которые вам нужно знать (Часть 92): Использование индексации B-дерева и байесовской нейронной сети в пользовательском классе сигнала
В этой статье представлен очередной пользовательский класс сигнала MQL5 – CSignalBTreeBayesian. Здесь алгоритм сбалансированного дерева объединяется с нейронной сетью, построенной на байесовских принципах, для создания еще одного пользовательского сигнала. Благодаря Мастеру MQL5 его можно тестировать отдельно или вместе с другими сигналами.
От начального до среднего уровня: Классы (III) От начального до среднего уровня: Классы (III)
В этой статье мы рассмотрим, как лучше контролировать код при работе с объектно-ориентированным программированием. Хотя мы только начинаем изучать объектно-ориентированное программирование, то, что мы здесь рассмотрим, поможет вам понять различные аспекты. Это также поможет развеять сомнения, которые могут возникнуть позже.
Интеграция MQL5 с пакетами обработки данных (часть 8): Использование графовых нейронных сетей для выявления зон ликвидности Интеграция MQL5 с пакетами обработки данных (часть 8): Использование графовых нейронных сетей для выявления зон ликвидности
В этой статье показано, как представить рыночную структуру в MQL5 в виде графа, представив максимумы и минимумы свингов как узлы графа с набором признаков. В статье обучается графовая нейронная сеть для оценки потенциальных зон ликвидности, после чего модель экспортируется в ONNX, а инференс в реальном времени выполняется в советнике. Рассматривается построение пайплайна данных, интеграция модели, визуализация зон на графике и использование сигналов для исполнения сделок по заданным правилам.