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

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

MetaTrader 5 — Индикаторы |
235 0
Christian Benjamin
Christian Benjamin

Содержание


Введение

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

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


Стратегия заполнения гэпов

Что такое заполнение гэпа?

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

Заполнения гэпов важны для данной системы, поскольку они представляют измеримые события Price Action, которые можно отслеживать и использовать для формирования сигналов.

Типы заполнения гэпов

Заполнение гэпа может происходить в двух основных вариантах в зависимости от направления исходного гэпа:

  1. Заполнение бычьего гэпа
  2. Заполнение медвежьего гэпа

Заполнение бычьего гэпа

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

Заполнение медвежьего гэпа

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

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

Факторы заполнения гэпов

Несколько рыночных факторов могут способствовать заполнению гэпов:

Чрезмерная реакция рынка

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

Фиксация прибыли

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

Технические уровни

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

Временные изменения настроений

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

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



Обзор решения

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

Краткий обзор процесса

1) Обнаружение гэпа выходного дня

  • Выявление гэпов между ценой закрытия в пятницу и ценой открытия в понедельник.
2) Сохранение информации о гэпе
  • Сохранение границ гэпа, направления и статуса отслеживания.
3) Отслеживание движения цены
  • Непрерывная оценка Price Action относительно активной зоны гэпа.
4) Подтверждение заполнения гэпа
  • Определение момента полного заполнения гэпа ценой и пометка его как заполненного.
5) Формирование сигнала
  • Построение стрелки Buy или Sell, обновление буферов индикатора и вызов алертов, если они включены.



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

В этом разделе с нуля создается сигнальный индикатор гэпов выходного дня. Индикатор обнаруживает гэпы выходного дня и отслеживает их статус заполнения в реальном времени. Он формирует сигналы без перерисовки, которые любой советник сможет считать через CopyBuffer(). Также добавляется визуальная обратная связь: прямоугольники, цветовое оформление по состояниям, недельные разделители и сигнальные стрелки. Кроме того, добавляются настраиваемые алерты: всплывающие окна, звук, push-уведомления и email-уведомления. Рассмотрим каждый компонент в порядке его появления в коде.

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

Сначала рассматриваются стандартные директивы #property, определяющие поведение и внешний вид индикатора. Индикатор работает в окне графика и использует четыре буфера: два для отображения стрелок Buy и Sell и два для вспомогательных данных. Для наглядного отображения стрелочных графических построений задаются код 233 для стрелки вверх и код 234 для стрелки вниз.

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

//--- Buy arrow plot
#property indicator_label1  "Buy"
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrDodgerBlue
#property indicator_width1  2

//--- Sell arrow plot
#property indicator_label2  "Sell"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrRed
#property indicator_width2  2

//--- arrow codes
#define ARROW_UP   233
#define ARROW_DOWN 234

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

//--- input parameters
input bool     ShowHistoricalGaps   = true;        // Show historical gaps (visual only)
input int      MaxHistoricalWeeks    = 26;          // Max weeks to look back
input double   MinGapPips            = 0.5;         // Minimum gap size in pips
input bool     ShowDetailedLabels    = true;        // Show extra price labels
input color    ActiveFillColor       = clrGainsboro;
input color    ActiveOutlineColor    = clrDimGray;
input color    ReactionColor         = clrDarkOrange;
input color    MemoryOutlineColor    = clrSilver;
input int      ActiveFillOpacity     = 50;          // Rectangle fill opacity (0-100)
input int      LineWidth             = 2;
input int      FontSize              = 7;
input int      ArrowSize             = 2;
input bool     InvertSignals         = false;       // Invert buy/sell logic

//--- week separator inputs
input bool     DrawWeekSeparators    = true;        // Draw vertical lines between weeks
input color    WeekSeparatorColor    = clrGray;     // Color of week separator lines
input int      WeekSeparatorStyle    = STYLE_DASH;  // Line style

//--- arrow color inputs
input color    BuyArrowColor         = clrDodgerBlue; // Buy arrows (chart objects)
input color    SellArrowColor        = clrRed;        // Sell arrows (chart objects)

//--- alert inputs
input bool     EnableAlerts          = true;        // Enable popup alerts on gap fills
input bool     EnableSound           = false;       // Play sound on fill
input string   SoundFile             = "alert.wav"; // Sound file name
input bool     EnableNotification    = false;       // Send push notification
input bool     EnableEmail           = false;       // Send email

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

Архитектура буферов, совместимая с советниками

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

//--- EA signal buffers (exposed via CopyBuffer)
double BufferBuy[];          // Buy signals: fill price at bar index
double BufferSell[];         // Sell signals: fill price at bar index
double BufferGapState[];     // Gap state at bar (for EA, 0..4)
double BufferFillPrice[];    // Fill price at bar (optional)

В OnInit() буферы индикатора привязываются с помощью SetIndexBuffer(). Затем все буферы настраиваются с помощью ArraySetAsSeries(..., false), что гарантирует соответствие индекса 0 самому старому бару. Это выравнивает индексацию буферов со стандартным (несерийным) порядком данных, используемым в OnCalculate(), и помогает избежать распространенных ошибок сдвига индекса на единицу.

Каждый буфер затем инициализируется значением EMPTY_VALUE, чтобы обеспечить чистое состояние перед началом вычислений. Для построений стрелок также отключаются соединительные линии установкой clrNONE, а символы стрелок назначаются с помощью PlotIndexSetInteger() с соответствующими кодами.

//+------------------------------------------------------------------+
//| Custom indicator initialization                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Set indicator buffers (EA accessible)
   SetIndexBuffer(0, BufferBuy, INDICATOR_DATA);
   SetIndexBuffer(1, BufferSell, INDICATOR_DATA);
   SetIndexBuffer(2, BufferGapState, INDICATOR_CALCULATIONS);
   SetIndexBuffer(3, BufferFillPrice, INDICATOR_CALCULATIONS);

   //--- Arrow plotting properties
   PlotIndexSetInteger(0, PLOT_ARROW, ARROW_UP);
   PlotIndexSetInteger(1, PLOT_ARROW, ARROW_DOWN);
   PlotIndexSetInteger(0, PLOT_LINE_COLOR, clrNONE);
   PlotIndexSetInteger(1, PLOT_LINE_COLOR, clrNONE);

   //--- Set series indexing (oldest at 0)
   ArraySetAsSeries(BufferBuy, false);
   ArraySetAsSeries(BufferSell, false);
   ArraySetAsSeries(BufferGapState, false);
   ArraySetAsSeries(BufferFillPrice, false);

   //--- Initialize buffers to EMPTY_VALUE
   ArrayInitialize(BufferBuy, EMPTY_VALUE);
   ArrayInitialize(BufferSell, EMPTY_VALUE);
   ArrayInitialize(BufferGapState, EMPTY_VALUE);
   ArrayInitialize(BufferFillPrice, EMPTY_VALUE);

Основные структуры данных

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

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

//--- Gap data structure (full record)
struct WeekendGapRecord
  {
   datetime          startTime;          
   datetime          endTime;            
   double            gapHigh;            
   double            gapLow;             
   double            midpoint;           
   bool              isGapDown;          
   ENUM_GAP_STATE    state;              
   bool              activeWeek;         
   datetime          fillTime;           
   double            fillPrice;         
   bool              signalPublished;    
   int               fillBarIndex;       
   int               lastScanBar;        
  };

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

//--- Active gap tracking
struct ActiveGap
  {
   int               gapIndex;           
   int               lastUpdateBar;      
   bool              isBull;             
   double            gapHigh, gapLow;
   datetime          startTime;
   ENUM_GAP_STATE    state;
   bool              signalPublished;
  };

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

Машина состояний гэпа

Перед реализацией логики обнаружения гэпов возможные состояния гэпа задаются с помощью перечисления ENUM_GAP_STATE. Оно включает пять значений: GAP_FRESH – недавно образовавшийся гэп без взаимодействия цены; GAP_PARTIAL – цена вошла в гэп, но не достигла противоположной границы; GAP_REACTION – цена вышла за противоположную границу, например, для гэпа вверх – поднялась выше верхней границы; GAP_FILLED – гэп полностью заполнен и сформирован торговый сигнал; GAP_HISTORICAL – прошлый гэп, отображаемый только как ориентир и не отслеживаемый активно.

//--- Enum for gap state
enum ENUM_GAP_STATE
  {
   GAP_FRESH = 0,
   GAP_PARTIAL = 1,
   GAP_REACTION = 2,
   GAP_FILLED = 3,
   GAP_HISTORICAL = 4
  };

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

Инициализация и очистка

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

//+------------------------------------------------------------------+
//| initialization                                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Set indicator buffers (EA accessible)
   SetIndexBuffer(0, BufferBuy, INDICATOR_DATA);
   SetIndexBuffer(1, BufferSell, INDICATOR_DATA);
   SetIndexBuffer(2, BufferGapState, INDICATOR_CALCULATIONS);
   SetIndexBuffer(3, BufferFillPrice, INDICATOR_CALCULATIONS);

//--- Arrow plotting properties
   PlotIndexSetInteger(0, PLOT_ARROW, ARROW_UP);
   PlotIndexSetInteger(1, PLOT_ARROW, ARROW_DOWN);
   PlotIndexSetInteger(0, PLOT_LINE_COLOR, clrNONE);
   PlotIndexSetInteger(1, PLOT_LINE_COLOR, clrNONE);

//--- Set series indexing (oldest at 0)
   ArraySetAsSeries(BufferBuy, false);
   ArraySetAsSeries(BufferSell, false);
   ArraySetAsSeries(BufferGapState, false);
   ArraySetAsSeries(BufferFillPrice, false);

//--- Initialize buffers to EMPTY_VALUE
   ArrayInitialize(BufferBuy, EMPTY_VALUE);
   ArrayInitialize(BufferSell, EMPTY_VALUE);
   ArrayInitialize(BufferGapState, EMPTY_VALUE);
   ArrayInitialize(BufferFillPrice, EMPTY_VALUE);

//--- Visual settings
   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;

//--- Optional ATR handle (for future expansion)
   atrHandle = iATR(_Symbol, PERIOD_CURRENT, 14);
   if(atrHandle == INVALID_HANDLE)
      Print("Warning: ATR handle creation failed");

   m_firstRun = true;
   return INIT_SUCCEEDED;
  }

В OnDeinit() хэндл ATR освобождается с помощью IndicatorRelease(), если он действителен. Все графические объекты с именами, начинающимися с префикса "WG_", удаляются специальной функцией очистки. Кроме того, динамические массивы m_gaps[] и activeGaps[] освобождаются для предотвращения утечек памяти. Такая очистка обеспечивает корректное управление ресурсами и стабильную работу индикатора.

//+------------------------------------------------------------------+
//| Deinitialization                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(atrHandle != INVALID_HANDLE)
      IndicatorRelease(atrHandle);
   DeleteWGObjects();
   ArrayFree(m_gaps);
   ArrayFree(activeGaps);
  }

Основной цикл вычислений – двухфазная обработка

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

Независимо от фазы выполнения первым шагом является сброс буферов индикатора для всех пересчитываемых баров. Для этого выполняется цикл от prev_calculated до rates_total - 1, и каждому элементу буфера присваивается EMPTY_VALUE. Этот шаг необходим для работы без перерисовки: он очищает ранее вычисленные значения, которые могли быть основаны на неполных данных, прежде чем записываются новые подтвержденные значения.

//+------------------------------------------------------------------+
//| Main calculation loop (EA-compatible)                            |
//+------------------------------------------------------------------+
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;

   //--- For each new bar (from prev_calculated onward), reset buffers to prevent repainting
   for(int i = prev_calculated; i < rates_total; i++)
     {
      BufferBuy[i]       = EMPTY_VALUE;
      BufferSell[i]      = EMPTY_VALUE;
      BufferGapState[i]  = EMPTY_VALUE;
      BufferFillPrice[i] = EMPTY_VALUE;
     }

Когда prev_calculated == 0, индикатор входит в фазу первого запуска. Сначала вызывается DetectAllGaps() для сканирования доступной истории цен и построения массива m_gaps[]. Далее ComputeHistoricalFills() восстанавливает прошлые события заполнения гэпов, используя только закрытые исторические бары. Это обеспечивает результаты без перерисовки. Каждый гэп, уже отмеченный как заполненный, передается в PublishSignal() с флагом isHistorical, установленным на true. Это позволяет записать сигнал в соответствующий буфер на точном баре заполнения и подавить алерты для исторических событий. Затем индикатор добавляет все незаполненные гэпы в массив activeGaps[] с помощью AddNewGapToActive(). Когда включен флаг ShowHistoricalGaps, для каждого обнаруженного гэпа вызывается функция CreateGapObjects(), создающая связанные графические объекты: прямоугольники, метки и недельные разделители.

//--- First run: full historical scan, fill reconstruction, and active gap initialization
   if(prev_calculated == 0)
     {
      //--- 1. Detect all historical and current gaps
      DetectAllGaps();

      //--- 2. Reconstruct historical fills (scan past price action)
      ComputeHistoricalFills();

      //--- 3. Publish all historical signals into buffers (for backtesting)
      //--- No alerts for historical fills
      for(int g = 0; g < ArraySize(m_gaps); g++)
        {
         if(m_gaps[g].state == GAP_FILLED &&
            m_gaps[g].fillBarIndex >= 0 &&
            m_gaps[g].fillBarIndex < rates_total)
           {
            PublishSignal(g, m_gaps[g].fillBarIndex, m_gaps[g].fillPrice, true);
           }
        }

      //--- 4. Build active gaps list for those not yet filled
      for(int g = 0; g < ArraySize(m_gaps); g++)
        {
         if(m_gaps[g].state != GAP_FILLED)
            AddNewGapToActive(g);
        }

      //--- 5. Create visual objects (only if needed, EA doesn't rely on them)
      if(ShowHistoricalGaps)
        {
         for(int g = 0; g < ArraySize(m_gaps); g++)
            CreateGapObjects(m_gaps[g], PrefixForIndex(g));
        }

      m_firstRun = false;
      return rates_total;
     }

Когда prev_calculated > 0, индикатор работает в режиме инкрементального обновления. Число вновь доступных баров рассчитывается как newBars = rates_total - prev_calculated. Значение, меньшее или равное нулю, указывает на отсутствие новых баров; в таком случае функция немедленно завершает работу. В противном случае каждый вновь сформированный бар обрабатывается с помощью UpdateActiveGaps() для оценки переходов состояний и обнаружения событий заполнения гэпов. Затем индикатор сканирует недавний участок истории цен, до 100 баров, на предмет вновь образовавшихся гэпов выходного дня. Эта дополнительная проверка учитывает ситуации, в которых гэп мог сформироваться, пока терминал MetaTrader был закрыт в выходные или во время длительного перерыва на рынке. Когда обнаруживается ранее неизвестный гэп, создается новая запись, которая добавляется в m_gaps[]. Незаполненные гэпы также регистрируются в списке активного мониторинга, а связанные с ними графические объекты создаются, если включено отображение исторических гэпов.

//--- Incremental update: only new bars have arrived
   int newBars = rates_total - prev_calculated;
   if(newBars <= 0)
      return rates_total;

//--- For each newly arrived bar, update active gaps (check fills, state changes)
   for(int bar = prev_calculated; bar < rates_total; bar++)
     {
      UpdateActiveGaps(bar, high, low, close, time);
     }

//--- Also check for newly formed weekend gaps on the latest bar (if Monday open)
   int scanStart = MathMax(0, rates_total - 100);
   for(int i = scanStart; i < rates_total - 1; i++)
     {
      double diffSeconds = (double)(time[i + 1] - time[i]);
      if(diffSeconds >= 172800)
        {
         double fridayClose = close[i];
         double mondayOpen  = open[i + 1];
         double gapPips     = MathAbs(mondayOpen - fridayClose) / PipSize();

         if(gapPips >= MinGapPips)
           {
            bool exists = false;
            for(int g = 0; g < ArraySize(m_gaps); g++)
              {
               if(m_gaps[g].startTime == time[i + 1])
                 {
                  exists = true;
                  break;
                 }
              }

            if(!exists)
              {
               WeekendGapRecord newGap;
               newGap.startTime       = time[i + 1];
               newGap.endTime         = GetNextMondayOpen(time[i + 1]);
               newGap.gapHigh         = MathMax(mondayOpen, fridayClose);
               newGap.gapLow          = MathMin(mondayOpen, fridayClose);
               newGap.midpoint        = (newGap.gapHigh + newGap.gapLow) / 2.0;
               newGap.isGapDown       = (fridayClose > mondayOpen);
               newGap.activeWeek      = (GetWeekMonday(time[i + 1]) == GetWeekMonday(TimeCurrent()));
               newGap.state           = newGap.activeWeek ? GAP_FRESH : GAP_HISTORICAL;
               newGap.fillTime        = 0;
               newGap.fillPrice       = 0;
               newGap.signalPublished = false;
               newGap.fillBarIndex    = -1;
               newGap.lastScanBar     = 0;

               int idx = ArraySize(m_gaps);
               ArrayResize(m_gaps, idx + 1);
               m_gaps[idx] = newGap;

               if(newGap.state != GAP_FILLED)
                  AddNewGapToActive(idx);

               if(ShowHistoricalGaps)
                  CreateGapObjects(m_gaps[idx], PrefixForIndex(idx));
              }
           }
        }
     }

   return rates_total;
  }

Обнаружение всех гэпов

Функция DetectAllGaps() начинает работу с копирования с графика массивов времени, цены открытия и цены закрытия с помощью CopyTime(), CopyOpen() и CopyClose(). Эти массивы настраиваются как серии, где сначала располагаются самые новые данные, что упрощает обработку в обратном хронологическом порядке. Затем функция перебирает доступные бары в обратном порядке, сравнивая каждый бар с предыдущим. Гэп выходного дня обнаруживается, когда разница во времени между двумя последовательными барами составляет не менее 48 часов, или 172800 секунд. В этот момент извлекаются цена закрытия в пятницу из более старого бара и цена открытия в понедельник из более нового, после чего размер гэпа рассчитывается в пипсах с помощью PipSize(). Когда размер гэпа достигает порога MinGapPips, создается и заполняется новая структура WeekendGapRecord, которая добавляется в массив m_gaps[]. Функция также определяет принадлежность гэпа текущей торговой неделе, сравнивая время его начала в понедельник с понедельником, соответствующим TimeCurrent().

//+------------------------------------------------------------------+
//| Detect all gaps (historical and current)                         |
//+------------------------------------------------------------------+
void DetectAllGaps()
  {
   ArrayResize(m_gaps, 0);
   int bars = Bars(_Symbol, _Period);
   if(bars < 2)
      return;

   datetime timeArr[];
   double openArr[], closeArr[];
   ArraySetAsSeries(timeArr, true);
   ArraySetAsSeries(openArr, true);
   ArraySetAsSeries(closeArr, true);

   if(CopyTime(_Symbol, _Period, 0, bars, timeArr) <= 0)
      return;
   if(CopyOpen(_Symbol, _Period, 0, bars, openArr) <= 0)
      return;
   if(CopyClose(_Symbol, _Period, 0, bars, closeArr) <= 0)
      return;

   double pip = PipSize();
   int limit = MathMin(bars - 1, 10000);
   datetime currentWeek = GetWeekMonday(TimeCurrent());

   for(int i = 1; i < limit; i++)
     {
      double diffSeconds = (double)(timeArr[i - 1] - timeArr[i]);
      if(diffSeconds >= 172800)
        {
         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]) == currentWeek);
            gap.state           = gap.activeWeek ? GAP_FRESH : GAP_HISTORICAL;
            gap.fillTime        = 0;
            gap.fillPrice       = 0;
            gap.signalPublished = false;
            gap.fillBarIndex    = -1;
            gap.lastScanBar     = 0;

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

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

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

Реконструкция исторических заполнений

После обнаружения всех гэпов следующим шагом является выяснение, какие из них были заполнены в прошлом. Функция ComputeHistoricalFills() выполняет эту реконструкцию без перерисовки. Она обрабатывает гэпы, которые остаются незаполненными и не относятся к активной торговой неделе, то есть сформировались в предыдущие недели. Для каждого гэпа функция копирует массивы цен максимума, минимума и времени между временем начала гэпа и текущим временем с помощью CopyHigh(), CopyLow() и CopyTime(). Эти массивы настраиваются в порядке, не являющемся серией, то есть от самых старых к самым новым данным, для последовательного сканирования исторических данных. Сканирование начинается с позиции lastScanBar гэпа. Эта оптимизация позволяет при повторном вызове функции возобновить обработку с предыдущей точки остановки, хотя текущая реализация вызывает ее только один раз при инициализации.

//+------------------------------------------------------------------+
//| Scan historical price action to find fills for past gaps         |
//+------------------------------------------------------------------+
void ComputeHistoricalFills()
  {
   for(int g = 0; g < ArraySize(m_gaps); g++)
     {
      if(m_gaps[g].state == GAP_FILLED)
         continue;
      if(m_gaps[g].activeWeek)
         continue;

      datetime from = m_gaps[g].startTime;
      datetime to   = TimeCurrent();
      int startBar  = m_gaps[g].lastScanBar;

      double high[], low[];
      datetime timeHist[];
      if(CopyHigh(_Symbol, _Period, from, to, high) <= 0)
         continue;
      if(CopyLow(_Symbol, _Period, from, to, low) <= 0)
         continue;
      if(CopyTime(_Symbol, _Period, from, to, timeHist) <= 0)
         continue;

      ArraySetAsSeries(high, false);
      ArraySetAsSeries(low, false);
      ArraySetAsSeries(timeHist, false);

      for(int i = startBar; i < ArraySize(timeHist); i++)
        {
         if(timeHist[i] < from)
            continue;

         if(m_gaps[g].isGapDown)
           {
            if(high[i] >= m_gaps[g].gapHigh)
              {
               m_gaps[g].state         = GAP_FILLED;
               m_gaps[g].fillTime      = timeHist[i];
               m_gaps[g].fillPrice     = m_gaps[g].gapHigh;
               m_gaps[g].fillBarIndex  = FindBarIndexByTime(timeHist[i], timeHist);
               break;
              }
           }
         else
           {
            if(low[i] <= m_gaps[g].gapLow)
              {
               m_gaps[g].state         = GAP_FILLED;
               m_gaps[g].fillTime      = timeHist[i];
               m_gaps[g].fillPrice     = m_gaps[g].gapLow;
               m_gaps[g].fillBarIndex  = FindBarIndexByTime(timeHist[i], timeHist);
               break;
              }
           }

         m_gaps[g].lastScanBar = i + 1;
        }
     }
  }

Для гэпа вниз (isGapDown == true) функция проверяет, достигает ли цена максимума любого бара значения gapHigh либо превышает его. Для гэпа вверх, напротив, проверяется, достигает ли цена минимума любого бара значения gapLow либо опускается ниже него. После обнаружения условия заполнения состоянию гэпа присваивается значение GAP_FILLED, а время заполнения, цена заполнения – достигнутая граница – и индекс бара фиксируются с помощью FindBarIndexByTime(). После этого сканирование данного гэпа завершается, поскольку заполнение уже подтверждено. Поскольку эта реконструкция основана исключительно на неизменяемых исторических данных, результаты остаются детерминированными и без перерисовки.

Мониторинг активных гэпов в реальном времени

UpdateActiveGaps() – основная процедура, выполняемая каждый раз при появлении нового бара. Она обрабатывает массив activeGaps[] в обратном порядке – это распространенный прием, позволяющий безопасно удалять элементы во время итерации. Для каждого элемента функция получает соответствующий индекс гэпа и обращается к связанной с ним записи в m_gaps[]. Гэпы, уже отмеченные как заполненные, удаляются из активного списка и пропускаются. Такие случаи редки, поскольку заполненные гэпы обычно удаляются сразу после обнаружения. Функция также игнорирует бары, временные метки которых предшествуют времени начала гэпа, поскольку в этой точке истории цен гэп еще не сформировался.

//+------------------------------------------------------------------+
//| Update all active gaps based on new bar data                     |
//+------------------------------------------------------------------+
void UpdateActiveGaps(int newBarIdx, const double &high[], const double &low[], const double &close[], const datetime &time[])
  {
   for(int a = ArraySize(activeGaps) - 1; a >= 0; a--)
     {
      int gapIdx = activeGaps[a].gapIndex;

      if(m_gaps[gapIdx].state == GAP_FILLED)
        {
         RemoveActiveGap(a);
         continue;
        }

      if(time[newBarIdx] < m_gaps[gapIdx].startTime)
         continue;

      double currentPrice = close[newBarIdx];
      bool isGapDown      = m_gaps[gapIdx].isGapDown;
      double gHigh        = m_gaps[gapIdx].gapHigh;
      double gLow         = m_gaps[gapIdx].gapLow;
      ENUM_GAP_STATE newState = m_gaps[gapIdx].state;

      if(isGapDown)
        {
         if(currentPrice > gHigh)
            newState = GAP_FILLED;
         else
            if(currentPrice > gLow && currentPrice <= gHigh)
               newState = (newState == GAP_FRESH || newState == GAP_REACTION) ? GAP_PARTIAL : newState;
            else
               if(currentPrice <= gLow)
                  newState = GAP_REACTION;
        }
      else
        {
         if(currentPrice < gLow)
            newState = GAP_FILLED;
         else
            if(currentPrice >= gLow && currentPrice < gHigh)
               newState = (newState == GAP_FRESH || newState == GAP_REACTION) ? GAP_PARTIAL : newState;
            else
               if(currentPrice >= gHigh)
                  newState = GAP_REACTION;
        }

      if(newState != m_gaps[gapIdx].state)
        {
         m_gaps[gapIdx].state   = newState;
         activeGaps[a].state    = newState;

         if(newState == GAP_FILLED)
           {
            m_gaps[gapIdx].fillTime      = time[newBarIdx];
            m_gaps[gapIdx].fillPrice     = isGapDown ? gHigh : gLow;
            m_gaps[gapIdx].fillBarIndex  = newBarIdx;

            if(!m_gaps[gapIdx].signalPublished)
              {
               PublishSignal(gapIdx, newBarIdx, m_gaps[gapIdx].fillPrice, false);
               m_gaps[gapIdx].signalPublished   = true;
               activeGaps[a].signalPublished    = true;
              }

            RemoveActiveGap(a);
            continue;
           }

         if(ShowHistoricalGaps)
            UpdateGapVisuals(m_gaps[gapIdx], PrefixForIndex(gapIdx));
        }

      activeGaps[a].lastUpdateBar = newBarIdx;
     }
  }

Затем функция оценивает переход состояния по цене закрытия текущего бара в соответствии с правилами, определенными в разделе о машине состояний. Каждый раз, когда вычисленное состояние отличается от текущего, обновляются и запись о гэпе, и соответствующий элемент активного гэпа. При переходе к GAP_FILLED фиксируются время заполнения, цена заполнения и индекс бара заполнения. Затем вызывается PublishSignal() с параметром isHistorical, установленным на false. Это публикует сигнал в буферы индикатора, запускает включенные алерты и записывает событие в журнал. После обработки сигнала гэп удаляется из активного списка с помощью RemoveActiveGap(), которая меняет элемент местами с последним элементом массива перед уменьшением его размера. При всех остальных изменениях состояния связанные графические объекты обновляются с помощью UpdateGapVisuals(), чтобы график отражал актуальный статус гэпа.

Публикация сигналов и алертов

PublishSignal() выполняет три основные задачи. Сначала определяется торговое направление: по умолчанию заполненный гэп вверх формирует сигнал на покупку, а заполненный гэп вниз – сигнал на продажу. Это поведение меняется на противоположное, когда включен параметр InvertSignals. Затем функция записывает цену заполнения в BufferBuy или BufferSell, если fillBarIdx находится в допустимом диапазоне буфера. Кроме того, она сохраняет GAP_FILLED в BufferGapState и записывает цену заполнения в BufferFillPrice, позволяя советникам получать дополнительную информацию о сигнале. Наконец, заполнения в реальном времени вызывают DoAlert(), тогда как исторические заполнения записываются в буферы без генерации уведомлений.

//+------------------------------------------------------------------+
//| Publish signal into indicator buffers at the given bar index     |
//+------------------------------------------------------------------+
void PublishSignal(int gapIdx, int fillBarIdx, double fillPrice, bool isHistorical)
  {
   bool isGapUp   = !m_gaps[gapIdx].isGapDown;
   bool buySignal = InvertSignals ? !isGapUp : isGapUp;

   if(buySignal)
     {
      if(fillBarIdx < ArraySize(BufferBuy))
        {
         BufferBuy[fillBarIdx]       = fillPrice;
         BufferGapState[fillBarIdx]  = (double)GAP_FILLED;
         BufferFillPrice[fillBarIdx] = fillPrice;
        }
     }
   else
     {
      if(fillBarIdx < ArraySize(BufferSell))
        {
         BufferSell[fillBarIdx]      = fillPrice;
         BufferGapState[fillBarIdx]  = (double)GAP_FILLED;
         BufferFillPrice[fillBarIdx] = fillPrice;
        }
     }

//--- Visual arrow (only if ShowHistoricalGaps is true)
   if(ShowHistoricalGaps)
      DrawSignalArrow(m_gaps[gapIdx], PrefixForIndex(gapIdx));

//--- Alerts & log for real-time fills only
   if(!isHistorical)
     {
      string signalType = buySignal ? "BUY" : "SELL";
      string msg = StringFormat("Weekend Gap %s Signal on %s %s at %s (Price: %.5f)",
                                signalType, _Symbol, EnumToString(Period()),
                                TimeToString(TimeCurrent()), fillPrice);
      DoAlert(msg, EnableSound, EnableNotification, EnableEmail);
      Print(msg);
     }
  }

DoAlert() учитывает глобальный флаг EnableAlerts и в зависимости от настроек пользователя запускает уведомления через Alert(), PlaySound(), SendNotification() и SendMail(). Сообщение алерта содержит символ, таймфрейм, тип сигнала и цену заполнения, обеспечивая трейдеру немедленное уведомление о торговом сигнале. Функция также поддерживает внешнюю интеграцию через push- и email-уведомления, обеспечивая доставку сигналов в реальном времени за пределами платформы.

//+------------------------------------------------------------------+
//| Send alerts (popup, sound, push, email)                          |
//+------------------------------------------------------------------+
void DoAlert(string msg, bool playSound, bool pushNote, bool sendMail)
  {
   if(!EnableAlerts)
      return;
   Alert(msg);
   if(playSound)
      PlaySound(SoundFile);
   if(pushNote)
      SendNotification(msg);
   if(sendMail)
      SendMail("Weekend Gap Indicator Alert", msg);
  }

Графические объекты – прямоугольники, метки и недельные разделители

Когда флаг ShowHistoricalGaps включен, CreateGapObjects() создает набор графических объектов для каждого гэпа. Основной объект – заполненный прямоугольник (OBJ_RECTANGLE), простирающийся от понедельника 00:00 до следующего понедельника 00:00. Его ценовые границы задаются gapLow и gapHigh. Прямоугольник размещается за ценовыми барами (OBJPROP_BACK = true), чтобы не закрывать свечи. Цвет заливки делается полупрозрачным с помощью функции ColorSetAlpha(), которая преобразует процент непрозрачности в значение альфа-канала от 0 до 255. Цвет и ширина контура берутся непосредственно из визуальных настроек.

Функция также отображает пунктирную срединную линию OBJ_TREND, чтобы выделить центр гэпа. Основная текстовая метка OBJ_TEXT размещается на расстоянии 25% ширины недели от левого края и отображает размер гэпа в пипсах и текущее состояние. Когда включен ShowDetailedLabels, добавляются три дополнительные метки с ценой закрытия в пятницу, ценой открытия в понедельник и значением середины диапазона. Это дает дополнительную информацию для ручного анализа.

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

//+------------------------------------------------------------------+
//| Create visual objects for a gap (rectangle, text, etc.)          |
//+------------------------------------------------------------------+
void CreateGapObjects(const WeekendGapRecord &gap, string prefix)
  {
   if(!ShowHistoricalGaps)
      return;

   if(DrawWeekSeparators)
     {
      int weekNum = (int)(gap.startTime / 86400 / 7);
      DrawWeekSeparator(gap.startTime, weekNum);
     }

   datetime leftTime   = gap.startTime;
   datetime rightEdge  = GetNextMondayOpen(gap.startTime);
   long     weekLength  = rightEdge - leftTime;
   double   mid         = (gap.gapHigh + gap.gapLow) / 2.0;

//--- Rectangle – placed behind bars
   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_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)));
   ObjectSetInteger(0, prefix + "_RECT", OBJPROP_BACK, true);

//--- Mid line (also behind)
   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_BACK, true);

//--- Main label (stays on top)
   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);

   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);
      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);

      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);

      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);
     }
  }

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

//+------------------------------------------------------------------+
//| Update visual objects based on gap state                         |
//+------------------------------------------------------------------+
void UpdateGapVisuals(const WeekendGapRecord &gap, string prefix)
  {
   if(!ShowHistoricalGaps)
      return;
   if(ObjectFind(0, prefix + "_RECT") < 0)
      return;

   if(!gap.activeWeek || gap.state == GAP_HISTORICAL)
     {
      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_FILL, false);

      if(ObjectFind(0, prefix + "_LBL") >= 0)
        {
         ObjectSetInteger(0, prefix + "_LBL", OBJPROP_COLOR, clrDimGray);
         ObjectSetString(0, prefix + "_LBL", OBJPROP_FONT, "Arial");
        }
     }
   else
     {
      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)));

      if(ObjectFind(0, prefix + "_LBL") >= 0)
        {
         ObjectSetInteger(0, prefix + "_LBL", OBJPROP_COLOR, clrWhite);
         ObjectSetString(0, prefix + "_LBL", OBJPROP_FONT, "Arial Bold");
        }
     }

   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);
     }
  }

Стрелки сигналов

Когда гэп отмечен как заполненный, DrawSignalArrow() создает объект OBJ_ARROW по соответствующим координатам времени и цены заполнения. Для сигналов на покупку стрелка использует код 233 – вверх, а для сигналов на продажу – 234, вниз. Цвета берутся из BuyArrowColor и SellArrowColor. Для стрелок Buy устанавливается позиция точки привязки ANCHOR_BOTTOM, чтобы кончик касался цены заполнения сверху. Для стрелок Sell используется ANCHOR_TOP, чтобы кончик касался цены снизу. Это улучшает наглядность на графике. Параметр InvertSignals полностью учитывается, чтобы направление стрелки соответствовало фактической торговой логике. Перед созданием функция проверяет наличие стрелки для того же гэпа, чтобы предотвратить дублирование объектов.

//+------------------------------------------------------------------+
//| Draw an arrow on chart when gap fills (visual only)              |
//+------------------------------------------------------------------+
void DrawSignalArrow(const WeekendGapRecord &gap, string prefix)
  {
   if(gap.state != GAP_FILLED || gap.fillTime == 0)
      return;
   if(ObjectFind(0, prefix + "_ARROW") >= 0)
      return;

   bool defaultIsBuy = !gap.isGapDown;
   bool isBuy = InvertSignals ? !defaultIsBuy : defaultIsBuy;

   ObjectCreate(0, prefix + "_ARROW", OBJ_ARROW, 0, gap.fillTime, gap.fillPrice);
   ObjectSetInteger(0, prefix + "_ARROW", OBJPROP_COLOR, isBuy ? BuyArrowColor : SellArrowColor);
   ObjectSetInteger(0, prefix + "_ARROW", OBJPROP_WIDTH, ArrowSize);
   ObjectSetInteger(0, prefix + "_ARROW", OBJPROP_ANCHOR, isBuy ? ANCHOR_BOTTOM : ANCHOR_TOP);
   ObjectSetInteger(0, prefix + "_ARROW", OBJPROP_ARROWCODE, isBuy ? 233 : 234);
  }

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

Для повышения модульности и удобства сопровождения кода используются несколько вспомогательных функций. PipSize() возвращает корректное значение пипса на основе _Digits, поддерживая валютные пары Форекс с пятью или тремя знаками после запятой, индексы с двумя знаками после запятой и товары с четырьмя знаками после запятой. GetWeekMonday(datetime t) вычисляет временную метку понедельника 00:00 для недели, содержащей t, включая корректную обработку воскресенья, когда day_of_week == 0. GetNextMondayOpen() сдвигает заданную временную метку понедельника на семь дней вперед, чтобы получить следующую недельную границу. ColorSetAlpha() применяет альфа-канал к RGB-цвету, позволяя создавать полупрозрачные графические элементы. StateToString() преобразует значения перечислений состояния гэпа в читаемые метки для аннотаций на графике. PrefixForIndex(int idx) генерирует уникальные префиксы имен объектов, например "WG_5", чтобы предотвратить конфликты имен. Наконец, FindBarIndexByTime() определяет индекс конкретной временной метки в массиве времени и используется при исторической реконструкции заполнения гэпов.

//+------------------------------------------------------------------+
//| Helper: Convert state to string                                  |
//+------------------------------------------------------------------+
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 "";
  }

//+------------------------------------------------------------------+
//| Helper: Apply alpha to color (for rectangle fill)                |
//+------------------------------------------------------------------+
color ColorSetAlpha(color clr, uchar alpha)
  {
   return (color)((clr & 0x00FFFFFF) | ((uint)alpha << 24));
  }

//+------------------------------------------------------------------+
//| Helper: Get Monday 00:00 of the week containing t                |
//+------------------------------------------------------------------+
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);
  }

//+------------------------------------------------------------------+
//| Helper: Pip size based on digits                                 |
//+------------------------------------------------------------------+
double PipSize()
  {
   return (_Digits == 3 || _Digits == 5) ? (_Point * 10.0) : _Point;
  }

//+------------------------------------------------------------------+
//| Helper: Next Monday open time (00:00)                            |
//+------------------------------------------------------------------+
datetime GetNextMondayOpen(datetime thisMondayOpenTime)
  {
   return thisMondayOpenTime + 7 * 86400;
  }

//+------------------------------------------------------------------+
//| Helper: Unique object name prefix for a gap index                |
//+------------------------------------------------------------------+
string PrefixForIndex(int idx)
  {
   return "WG_" + IntegerToString(idx);
  }

//+------------------------------------------------------------------+
//| Delete all indicator-created objects                              |
//+------------------------------------------------------------------+
void DeleteWGObjects()
  {
   const long chart_id = 0;
   int total = ObjectsTotal(chart_id, 0, -1);
   for(int i = total - 1; i >= 0; --i)
     {
      string name = ObjectName(chart_id, i, 0, -1);
      if(StringFind(name, "WG_") == 0)
         ObjectDelete(chart_id, name);
     }
  }

//+------------------------------------------------------------------+
//| Draw a vertical line to separate weeks                           |
//+------------------------------------------------------------------+
void DrawWeekSeparator(datetime lineTime, int weekNumber)
  {
   if(!DrawWeekSeparators)
      return;

   string objName = "WG_WeekSep_" + IntegerToString(weekNumber);
   if(ObjectFind(0, objName) >= 0)
      return;

   ObjectCreate(0, objName, OBJ_VLINE, 0, lineTime, 0);
   ObjectSetInteger(0, objName, OBJPROP_COLOR, WeekSeparatorColor);
   ObjectSetInteger(0, objName, OBJPROP_STYLE, WeekSeparatorStyle);
   ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1);
   ObjectSetInteger(0, objName, OBJPROP_BACK, true);
  }

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



Результаты

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

Тестирование на графике в реальном времени

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

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

Оценка в Тестере стратегий

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

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

Важные аспекты торговли

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

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

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

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


Заключение

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

Реализация предназначена не только для ручного анализа, но и для интеграции с автоматизированными системами. Предоставляя буферы сигналов Buy и Sell без перерисовки, индикатор позволяет советникам получать сигналы заполнения гэпов через CopyBuffer() и торговать по ним. Это проверено в Тестере стратегий с помощью прилагаемого тестового советника.

Итоговая система обеспечивает:

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

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

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

Прикрепленные файлы |
Разработка индикатора Volume Bubble на MQL5 с использованием стандартного отклонения Разработка индикатора Volume Bubble на MQL5 с использованием стандартного отклонения
В статье показано, как создать индикатор Volume Bubble на MQL5, который визуализирует рыночную активность с использованием статистической нормализации. В нем рассматривается работа с тиковым и реальным объемом, вычисление среднего значения и стандартного отклонения по скользящему окну, а также нормализация значений объема для определения относительной силы. Вы реализуете графические объекты для отображения пузырьков с динамическими размером и цветом, обеспечивая наглядное представление интенсивности торговой активности прямо на графике
Возможности Мастера MQL5, которые вам нужно знать (Часть 98): Использование сигма-точечного фильтра Калмана и капсульной сети в пользовательском классе сигналов Возможности Мастера MQL5, которые вам нужно знать (Часть 98): Использование сигма-точечного фильтра Калмана и капсульной сети в пользовательском классе сигналов
В этой статье представлен пользовательский класс CSignalUKFCapsNet, написанный на MQL5. Этот класс используется с Мастером MQL5 при сборке советника. После выбора в Мастере он определяет сигналы входа советника. При создании этого пользовательского класса были объединены сигма-точечный фильтр Калмана и капсульная нейронная сеть. Алгоритм представлен в четырех режимах работы, а его реализация в виде пользовательского класса для Мастера MQL5 позволяет тестировать различные методы трейлинг-стопа и системы управления капиталом.
От начального до среднего уровня: Перегрузка операторов (V) От начального до среднего уровня: Перегрузка операторов (V)
В этой статье мы рассмотрим, как изменить код, чтобы реализовать решение, совершенно отличающееся от того, что многие считают возможным в MQL5. Важное замечание: чтобы правильно понять данный материал, необходимо хорошо усвоить понятия, рассмотренные в предыдущих статьях.
Моделирование рынка: В единстве — сила (III) Моделирование рынка: В единстве — сила (III)
В этой статье я представлю нашу систему для моделирования рыночных операций. Хотя всё уже практически завершено, ещё остаётся реализовать некоторые вещи и внести несколько изменений. Однако, признаюсь, после всего, что мы уже разработали, я устал от того, что всё ещё застрял в реализации этой системы.