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

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

MetaTrader 5Интеграция |
52 0
Christian Benjamin
Christian Benjamin

Содержание



Введение

В части 71 серии "Разработка инструментария для анализа Price Action" были представлены гэпы выходного дня как паттерн Price Action и создан инструмент для их обнаружения и отображения на графике. Однако трейдерам и разработчикам требуется нечто большее, чем визуальная маркировка. Требуется четкая автоматизируемая модель сигнала, определяющая условие входа, правила стоп-лосса и тейк-профита, срок действия каждого сетапа, а также выходные данные, пригодные для считывания советником и для последовательного тестирования как на истории, так и в реальном времени. В этой статье формализуется логика торговли по гэпам выходного дня и реализуется как индикатор MQL5.

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


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

Краткое содержание предыдущей статьи

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

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

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

Логика торговли

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

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

Бычий сетап

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

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

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

Медвежий сетап

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

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

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

Система сигналов

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

  • направление сделки (buy или sell);
  • расположение сигнала входа;
  • динамический уровень стоп-лосса на основе недавней рыночной структуры;
  • уровень тейк-профита на основе противоположной границы гэпа.

Связь торговых правил с логикой индикатора

Правило торговли
Реализация
Обнаружение гэпа выходного дня
Цена закрытия в пятницу и цена открытия в понедельник сравниваются в DetectAllGaps().
Окно действия сигнала
Гэп обрабатывается только до следующего понедельника в ProcessBarForGap().
Подтверждение входа
Подтверждение по закрытию свечи проверяется в ProcessBarForGap().
Правило стоп-лосса
SL рассчитывается по экстремуму недели в PublishSignal().
Правило тейк-профита
TP устанавливается на противоположной границе гэпа в PublishSignal().
Работа без перерисовки
Сигналы восстанавливаются по закрытым барам в ScanHistoricalSignals() и проверяются в реальном времени в CheckLiveSignals().
Выходные данные для советника
Цена сигнала и TP/SL записываются в шесть буферов в RenderSignalBuffers().
В следующем разделе торговые правила напрямую сопоставляются с логикой MQL5. Сначала индикатор обнаруживает гэп выходного дня и задает его допустимое торговое окно. Затем применяются правило подтверждения, правило стоп-лосса и правило тейк-профита. Наконец, индикатор записывает полученные значения в буферы, чтобы советник мог последовательно считывать их с помощью CopyBuffer().


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

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

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

Общая архитектура представлена на блок-схеме ниже.

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

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

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

Эти шесть буферов также служат интерфейсом между индикатором и любым советником, использующим его сигналы. BufferBuyArrow и BufferSellArrow хранят цену сигнала для сетапов buy и sell. BufferBuyTP и BufferBuySL хранят соответствующие значения тейк-профита и стоп-лосса для сигналов Buy, а BufferSellTP и BufferSellSL – соответствующие значения для сигналов Sell. Непустой буфер стрелок обозначает бар подтвержденного сигнала, а связанные буферы TP и SL предоставляют торговые уровни, которые советник может использовать для фильтрации или исполнения.

//+------------------------------------------------------------------+
//|                                        WeekendGapMultiSignal.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 6
#property indicator_plots   2

//--- visual properties for buy/sell arrows
#property indicator_label1  "Buy"
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrBlue
#property indicator_width1  2

#property indicator_label2  "Sell"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrOrange
#property indicator_width2  2

//--- arrow symbols (wingdings)
#define ARROW_UP   233
#define ARROW_DOWN 234

Первые два графических построения используются для стрелок Buy и Sell. Эти графические построения настраиваются с кодами стрелок Wingdings 233 и 234, которые обеспечивают наглядные маркеры вверх и вниз. Благодаря этому такие сигналы легко распознать визуально, даже когда на графике одновременно присутствуют несколько активных гэпов.

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input bool   ShowHistoricalGaps      = true;      // Show historical gap zones
input int    MaxHistoricalWeeks      = 26;       // Max weeks to scan backwards
input double MinGapPips              = 0.5;      // Minimum gap size to consider
input double MinTradableGapPips      = 1.0;      // Minimum gap size for signals
input bool   ShowDetailedLabels      = true;     // Show extra labels on gaps
input bool   ShowTradeLevels         = true;     // Draw TP/SL lines for signals
input double ConfirmationOffsetPips  = 0.0;      // Pips offset for signal confirmation
input double StopBufferPips          = 0.0;      // Extra pips beyond weekly high/low for SL
input color  ActiveFillColor         = clrGainsboro;  // Fill color for active gaps
input color  ActiveOutlineColor      = clrDimGray;    // Outline color for active gaps
input int    ActiveFillOpacity       = 45;            // Fill opacity (0-100)
input color  BuyArrowColor           = clrBlue;       // Buy arrow color
input color  SellArrowColor          = clrOrange;     // Sell arrow color
input color  TPLineColor             = clrBlue;       // Take profit line color
input color  SLLineColor             = clrTomato;     // Stop loss line color
input color  WeekSeparatorColor      = clrGray;       // Vertical line color
input int    WeekSeparatorStyle      = STYLE_DASH;    // Line style for week separators
input int    LineWidth               = 2;             // Rectangle border width
input int    FontSize                = 8;             // Label font size
input int    ArrowSize               = 2;             // Arrow symbol size
input bool   InvertSignals           = false;         // Invert generated signals (buy<->sell)
input bool   DrawWeekSeparators      = true;          // Draw vertical lines at week boundaries
input bool   EnableAlerts            = true;          // Show popup alerts
input bool   EnableSound             = false;         // Play sound on new signal
input string SoundFile               = "alert.wav";   // Sound file name
input bool   EnableNotification      = false;         // Send mobile notification
input bool   EnableEmail             = false;         // Send email on signal 

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

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

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

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

//+------------------------------------------------------------------+
//| Indicator buffers                                                |
//+------------------------------------------------------------------+
double BufferBuyArrow[];   // Buy arrow prices
double BufferSellArrow[];  // Sell arrow prices
double BufferBuyTP[];      // Buy take profit levels (for information)
double BufferBuySL[];      // Buy stop loss levels
double BufferSellTP[];     // Sell take profit levels
double BufferSellSL[];     // Sell stop loss levels

Оставшиеся буферы BufferBuyTP, BufferBuySL, BufferSellTP и BufferSellSL хранят значения тейк-профита и стоп-лосса для соответствующих сигналов. Эти буферы предоставляют дополнительный торговый контекст, который советник может использовать для фильтрации, управления рисками или размещения ордеров.

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

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

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

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

//+------------------------------------------------------------------+
//| Structure: one trade signal                                      |
//+------------------------------------------------------------------+
struct SignalRecord
  {
   datetime          signalTime;     // Bar opening time of signal
   double            signalPrice;    // Price where arrow is drawn
   double            signalTP;       // Take profit level
   double            signalSL;       // Stop loss level
   bool              signalIsBuy;    // true = buy, false = sell
  };

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

//+------------------------------------------------------------------+
//| Structure: weekly gap information                                |
//+------------------------------------------------------------------+
struct GapInfo
  {
   datetime          monOpenTime;      // Monday 00:00 of this gap week
   double            gapHigh;          // Upper edge of gap (max of Fri close / Mon open)
   double            gapLow;           // Lower edge of gap
   bool              isGapDown;        // true if gap down (Fri close > Mon open)
   bool              gapFilled;        // Has price touched the opposite edge?
   int               scanStartIndex;   // Bar index where this week starts (for historical scan)
   double            weekLow;          // Lowest price seen so far this week
   double            weekHigh;         // Highest price seen so far this week
   datetime          lastSignalBarTime;// Last bar that generated a signal (avoid duplicates)
   SignalRecord      signals[];        // All signals recorded for this gap
  };

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

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

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

Сначала описывается PipSize(), возвращающая корректное значение пипса в зависимости от числа знаков текущего символа. Это необходимо, поскольку котировки разных символов имеют разную точность. Валютные пары Форекс, индексы и товары могут иметь разные форматы точности, поэтому индикатор должен каждый раз корректно рассчитывать пипсы.

//+------------------------------------------------------------------+
//| Helper: return pip size for current symbol                       |
//+------------------------------------------------------------------+
double PipSize()
  {
   return (_Digits == 3 || _Digits == 5) ? (_Point * 10.0) : _Point;
  }

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

//+------------------------------------------------------------------+
//| Helper: convert pips to price offset                             |
//+------------------------------------------------------------------+
double PipOffset(double pips)
  {
   return pips * PipSize();
  }

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

//+------------------------------------------------------------------+
//| Helper: apply alpha transparency to a color                      |
//+------------------------------------------------------------------+
color ColorSetAlpha(color clr, uchar alpha)
  {
   return (color)((clr & 0x00FFFFFF) | ((uint)alpha << 24));
  }

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

//+------------------------------------------------------------------+
//| Helper: return next Monday 00:00 after given Monday              |
//+------------------------------------------------------------------+
datetime GetNextMondayOpen(datetime monday)
  {
   return monday + 7 * 86400;
  }

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

CandleTouchesLevel() проверяет, касается ли диапазон свечи заданного ценового уровня. Эта вспомогательная функция используется при обнаружении заполнения гэпа. GapPips() возвращает размер гэпа в пипсах, а IsTradableGap() сравнивает этот размер с минимальным порогом для торговли.

//+------------------------------------------------------------------+
//| Helper: check if candle's high/low range contains a level        |
//+------------------------------------------------------------------+
bool CandleTouchesLevel(double hi, double lo, double lvl)
  {
   return (hi >= lvl && lo <= lvl);
  }

//+------------------------------------------------------------------+
//| Helper: return gap size in pips for given gap index              |
//+------------------------------------------------------------------+
double GapPips(int gapIdx)
  {
   if(gapIdx < 0 || gapIdx >= ArraySize(g_gaps))
      return 0.0;
   return MathAbs(g_gaps[gapIdx].gapHigh - g_gaps[gapIdx].gapLow) / PipSize();
  }

//+------------------------------------------------------------------+
//| Helper: check if gap meets the tradable size threshold           |
//+------------------------------------------------------------------+
bool IsTradableGap(int gapIdx)
  {
   return (GapPips(gapIdx) >= MinTradableGapPips);
  }

Наконец, реализуются DoAlert() для отправки алертов и DeleteWGObjects() для очистки. Эти вспомогательные функции позволяют основным функциям сохранять фокус и упрощают сопровождение кода.

//+------------------------------------------------------------------+
//| Alert dispatcher: popup, sound, push, email                      |
//+------------------------------------------------------------------+
void DoAlert(string msg, bool sound, bool push, bool mail)
  {
   if(!EnableAlerts)
      return;
   Alert(msg);
   if(sound)
      PlaySound(SoundFile);
   if(push)
      SendNotification(msg);
   if(mail)
      SendMail("Weekend Gap Signal", msg);
  }

//+------------------------------------------------------------------+
//| Delete all chart objects prefixed with "WG_"                     |
//+------------------------------------------------------------------+
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);
     }
  }

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

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

//+------------------------------------------------------------------+
//| Indicator initialization function                                 |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- remove old objects
   DeleteWGObjects();

//--- set indicator buffers
   SetIndexBuffer(0, BufferBuyArrow, INDICATOR_DATA);
   SetIndexBuffer(1, BufferSellArrow, INDICATOR_DATA);
   SetIndexBuffer(2, BufferBuyTP, INDICATOR_CALCULATIONS);
   SetIndexBuffer(3, BufferBuySL, INDICATOR_CALCULATIONS);
   SetIndexBuffer(4, BufferSellTP, INDICATOR_CALCULATIONS);
   SetIndexBuffer(5, BufferSellSL, INDICATOR_CALCULATIONS);

//--- configure arrow plots
   PlotIndexSetInteger(0, PLOT_ARROW, ARROW_UP);
   PlotIndexSetInteger(1, PLOT_ARROW, ARROW_DOWN);
   PlotIndexSetInteger(0, PLOT_LINE_WIDTH, ArrowSize);
   PlotIndexSetInteger(1, PLOT_LINE_WIDTH, ArrowSize);
   PlotIndexSetInteger(0, PLOT_LINE_COLOR, 0, BuyArrowColor);
   PlotIndexSetInteger(1, PLOT_LINE_COLOR, 0, SellArrowColor);

//--- series orientation for easy indexing
   ArraySetAsSeries(BufferBuyArrow, true);
   ArraySetAsSeries(BufferSellArrow, true);
   ArraySetAsSeries(BufferBuyTP, true);
   ArraySetAsSeries(BufferBuySL, true);
   ArraySetAsSeries(BufferSellTP, true);
   ArraySetAsSeries(BufferSellSL, true);

//--- initialise buffers with empty values
   ArrayInitialize(BufferBuyArrow, EMPTY_VALUE);
   ArrayInitialize(BufferSellArrow, EMPTY_VALUE);
   ArrayInitialize(BufferBuyTP, EMPTY_VALUE);
   ArrayInitialize(BufferBuySL, EMPTY_VALUE);
   ArrayInitialize(BufferSellTP, EMPTY_VALUE);
   ArrayInitialize(BufferSellSL, EMPTY_VALUE);

//--- set short name for the indicator (as it appears in the chart)
   IndicatorSetString(INDICATOR_SHORTNAME, "Weekend Gap Multi-Signal");

   g_firstRun = true;
   g_lastBarTime = 0;
   return INIT_SUCCEEDED;
  }

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

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

//+------------------------------------------------------------------+
//| Indicator deinitialization function                               |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   DeleteWGObjects();
   ArrayFree(g_gaps);
  }

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

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

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

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

//+------------------------------------------------------------------+
//| Scan entire history to detect all weekly gaps                    |
//+------------------------------------------------------------------+
void DetectAllGaps()
  {
   ArrayResize(g_gaps, 0);
   int bars = Bars(_Symbol, _Period);
   if(bars < 3)
      return;

//--- load historical price data
   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 barsPerWeek = (int)((7 * 86400) / PeriodSeconds(_Period));
   if(barsPerWeek < 1)
      barsPerWeek = 1;
   int limit = MathMin(bars - 2, MaxHistoricalWeeks * barsPerWeek);
   if(limit < 1)
      return;

//--- iterate bars to find weekend gaps
   for(int i = 0; i < limit; i++)
     {
      double diff = (double)(timeArr[i] - timeArr[i + 1]);
      if(diff >= 172800)   
        {
         double mondayOpen = openArr[i];
         double fridayClose = closeArr[i + 1];
         double gapPips = MathAbs(mondayOpen - fridayClose) / pip;
         if(gapPips >= MinGapPips && !GapExistsByMonOpen(timeArr[i]))
           {
            GapInfo gap;
            gap.monOpenTime = timeArr[i];
            gap.gapHigh = MathMax(mondayOpen, fridayClose);
            gap.gapLow = MathMin(mondayOpen, fridayClose);
            gap.isGapDown = (fridayClose > mondayOpen);
            gap.gapFilled = false;
            gap.scanStartIndex = i;
            gap.weekLow = 0;
            gap.weekHigh = 0;
            gap.lastSignalBarTime = 0;
            ArrayResize(gap.signals, 0);
            AddGap(gap);
           }
        }
     }
  }

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

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

Историческая реконструкция сигналов

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

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

//+------------------------------------------------------------------+
//| Scan historical signals for all detected gaps                    |
//+------------------------------------------------------------------+
void ScanHistoricalSignals()
  {
   int bars = Bars(_Symbol, _Period);
   if(bars < 3)
      return;

//--- load price arrays for scanning
   datetime timeArr[];
   double highArr[], lowArr[], closeArr[];
   ArraySetAsSeries(timeArr, true);
   ArraySetAsSeries(highArr, true);
   ArraySetAsSeries(lowArr, true);
   ArraySetAsSeries(closeArr, true);
   if(CopyTime(_Symbol, _Period, 0, bars, timeArr) <= 0)
      return;
   if(CopyHigh(_Symbol, _Period, 0, bars, highArr) <= 0)
      return;
   if(CopyLow(_Symbol, _Period, 0, bars, lowArr) <= 0)
      return;
   if(CopyClose(_Symbol, _Period, 0, bars, closeArr) <= 0)
      return;

//--- for each gap, walk forward from Monday and simulate signals
   for(int g = 0; g < ArraySize(g_gaps); g++)
     {
      int mondayIdx = g_gaps[g].scanStartIndex;
      if(mondayIdx < 0 || mondayIdx >= bars)
         continue;
      datetime nextMonday = g_gaps[g].monOpenTime + 7 * 86400;
      for(int s = mondayIdx - 1; s >= 0; s--)   
        {
         if(timeArr[s] >= nextMonday)
            break;
         ProcessBarForGap(g, s, highArr, lowArr, closeArr, timeArr, true);
         if(g_gaps[g].gapFilled)
            break;
        }
     }
  }

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

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

Обработка гэпов бар за баром

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

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

//+------------------------------------------------------------------+
//| Process a single bar for a given gap: signal detection & fill    |
//+------------------------------------------------------------------+
void ProcessBarForGap(int gapIdx, int shift, const double &high[], const double &low[],
                      const double &close[], const datetime &time[], bool historical=false)
  {
   if(gapIdx < 0 || gapIdx >= ArraySize(g_gaps))
      return;
   if(time[shift] <= g_gaps[gapIdx].monOpenTime)
      return;
   datetime nextMonday = g_gaps[gapIdx].monOpenTime + 7 * 86400;
   if(time[shift] >= nextMonday)
      return;
   if(g_gaps[gapIdx].gapFilled)
      return;
   if(!IsTradableGap(gapIdx))
      return;

//--- initialise weekly high/low if not set
   if(g_gaps[gapIdx].weekLow == 0)
      g_gaps[gapIdx].weekLow = g_gaps[gapIdx].gapLow;
   if(g_gaps[gapIdx].weekHigh == 0)
      g_gaps[gapIdx].weekHigh = g_gaps[gapIdx].gapHigh;

//--- update weekly extremes
   if(high[shift] > g_gaps[gapIdx].weekHigh)
      g_gaps[gapIdx].weekHigh = high[shift];
   if(low[shift] < g_gaps[gapIdx].weekLow)
      g_gaps[gapIdx].weekLow = low[shift];

//--- check if gap got filled (price touches the opposite edge)
   if((g_gaps[gapIdx].isGapDown && CandleTouchesLevel(high[shift], low[shift], g_gaps[gapIdx].gapHigh)) ||
      (!g_gaps[gapIdx].isGapDown && CandleTouchesLevel(high[shift], low[shift], g_gaps[gapIdx].gapLow)))
     {
      g_gaps[gapIdx].gapFilled = true;
      return;
     }

//--- determine raw signal conditions (using confirmation offset for logic)
   bool buySignal = false, sellSignal = false;
   if(g_gaps[gapIdx].isGapDown)
     {
      buySignal = (low[shift] <= (g_gaps[gapIdx].gapLow + PipOffset(ConfirmationOffsetPips)) &&
                   close[shift] > g_gaps[gapIdx].gapLow);
     }
   else
     {
      sellSignal = (high[shift] >= (g_gaps[gapIdx].gapHigh - PipOffset(ConfirmationOffsetPips)) &&
                    close[shift] < g_gaps[gapIdx].gapHigh);
     }

   bool signalNow = (buySignal || sellSignal);
   if(signalNow && g_gaps[gapIdx].lastSignalBarTime != time[shift])
     {
      bool isBuy = buySignal;
      if(InvertSignals)
         isBuy = !isBuy;
      PublishSignal(gapIdx, shift, isBuy, historical, time, high, low);
     }
  }

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

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

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

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

Мониторинг сигналов в реальном времени

Для обработки обновлений графика в реальном времени реализуется CheckLiveSignals(). Эта функция обрабатывает только последний завершенный бар, что позволяет индикатору работать без перерисовки. Текущая незакрытая свеча игнорируется, поскольку ее окончательная цена закрытия еще неизвестна.

//+------------------------------------------------------------------+
//| Check for new signals on the current (latest) bar                |
//+------------------------------------------------------------------+
void CheckLiveSignals(const datetime &time[], const double &high[],
                      const double &low[], const double &close[])
  {
   int shift = 1;   
   if(ArraySize(time) <= shift + 1)
      return;

   for(int g = 0; g < ArraySize(g_gaps); g++)
     {
      datetime nextMonday = g_gaps[g].monOpenTime + 7 * 86400;
      if(time[shift] >= nextMonday)
         continue;
      ProcessBarForGap(g, shift, high, low, close, time, false);
     }
  }

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

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

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

Каждый раз, когда ProcessBarForGap() выявляет допустимый сетап, она вызывает PublishSignal(). Эта функция сохраняет сигнал в массиве сигналов гэпа и также подготавливает данные для графика и буферов.

PublishSignal() определяет направление сигнала, задает цену стрелки, рассчитывает уровни TP и SL и записывает результат в соответствующую сигнальную запись. По умолчанию гэп вниз формирует сигнал на покупку, а гэп вверх – сигнал на продажу, но это направление можно изменить с помощью InvertSignals.

//+------------------------------------------------------------------+
//| Record a new signal and draw its objects/alert                   |
//+------------------------------------------------------------------+
void PublishSignal(int gapIdx, int barIdx, bool isBuy, bool isHistorical,
                   const datetime &time[], const double &high[], const double &low[])
  {
   SignalRecord newSig;
   newSig.signalTime  = time[barIdx];
   newSig.signalIsBuy = isBuy;

//--- visual arrow placement: always 2 pips away from candle edge to avoid overlap
   double visualOffset = PipOffset(2.0);
   newSig.signalPrice = isBuy ? (low[barIdx] - visualOffset) : (high[barIdx] + visualOffset);
   newSig.signalTP    = isBuy ? g_gaps[gapIdx].gapHigh : g_gaps[gapIdx].gapLow;
   newSig.signalSL    = isBuy ? (g_gaps[gapIdx].weekLow - PipOffset(StopBufferPips))
                        : (g_gaps[gapIdx].weekHigh + PipOffset(StopBufferPips));

//--- store signal
   int sz = ArraySize(g_gaps[gapIdx].signals);
   ArrayResize(g_gaps[gapIdx].signals, sz + 1);
   g_gaps[gapIdx].signals[sz] = newSig;
   g_gaps[gapIdx].lastSignalBarTime = time[barIdx];

//--- draw TP/SL levels if requested
   if(ShowTradeLevels)
      DrawTradeLevels(gapIdx, sz, PrefixForIndex(gapIdx));

//--- for live signals (not historical), send alerts and print
   if(!isHistorical)
     {
      string signalType = isBuy ? "BUY" : "SELL";
      string msg = StringFormat("Weekend Gap %s signal on %s %s at %s | TP: %.*f | SL: %.*f",
                                signalType, _Symbol, EnumToString((ENUM_TIMEFRAMES)_Period),
                                TimeToString(newSig.signalTime, TIME_DATE | TIME_MINUTES),
                                _Digits, newSig.signalTP, _Digits, newSig.signalSL);
      DoAlert(msg, EnableSound, EnableNotification, EnableEmail);
      Print(msg);
     }
  }

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

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

Отрисовка торговых уровней

Если включена опция ShowTradeLevels, PublishSignal() также вызывает функцию DrawTradeLevels(). Эта функция отображает линии TP и SL для каждого сигнала, чтобы на графике была видна полная структура сетапа.

Линии TP и SL начинаются в момент сигнала и продолжаются до конца недели гэпа. Благодаря этому торговые уровни соответствуют тому же недельному окну, которое используется остальной частью индикатора. Поскольку линии именуются с использованием индекса гэпа и индекса сигнала, несколько сигналов одного гэпа могут иметь собственные отдельные объекты TP и SL.

//+------------------------------------------------------------------+
//| Draw TP/SL trend lines for a specific signal                     |
//+------------------------------------------------------------------+
void DrawTradeLevels(int gapIdx, int sigIdx, string prefix)
  {
   if(!ShowTradeLevels)
      return;
   if(gapIdx < 0 || gapIdx >= ArraySize(g_gaps))
      return;
   if(sigIdx < 0 || sigIdx >= ArraySize(g_gaps[gapIdx].signals))
      return;

   SignalRecord sig = g_gaps[gapIdx].signals[sigIdx];
   datetime endTime = g_gaps[gapIdx].monOpenTime + 7 * 86400;
   string id = prefix + "_S" + IntegerToString(sigIdx);
   string tpName = id + "_TP";
   string slName = id + "_SL";

   ObjectDelete(0, tpName);
   ObjectDelete(0, slName);

//--- draw take profit line (horizontal from signal time to week end)
   ObjectCreate(0, tpName, OBJ_TREND, 0, sig.signalTime, sig.signalTP, endTime, sig.signalTP);
   ObjectSetInteger(0, tpName, OBJPROP_COLOR, TPLineColor);
   ObjectSetInteger(0, tpName, OBJPROP_WIDTH, 2);
   ObjectSetInteger(0, tpName, OBJPROP_RAY_RIGHT, false);

//--- draw stop loss line
   ObjectCreate(0, slName, OBJ_TREND, 0, sig.signalTime, sig.signalSL, endTime, sig.signalSL);
   ObjectSetInteger(0, slName, OBJPROP_COLOR, SLLineColor);
   ObjectSetInteger(0, slName, OBJPROP_WIDTH, 2);
   ObjectSetInteger(0, slName, OBJPROP_RAY_RIGHT, false);
  }

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

Графические объекты для визуализации гэпов

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

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

//+------------------------------------------------------------------+
//| Create visual objects for a gap (rectangle, midline, labels)     |
//+------------------------------------------------------------------+
void CreateGapObjects(int gapIdx, string prefix)
  {
   if(!ShowHistoricalGaps || gapIdx < 0 || gapIdx >= ArraySize(g_gaps))
      return;

//--- draw week separator if enabled
   if(DrawWeekSeparators)
     {
      int weekNum = (int)(g_gaps[gapIdx].monOpenTime / 86400 / 7);
      DrawWeekSeparator(g_gaps[gapIdx].monOpenTime, weekNum);
     }

   datetime leftTime   = g_gaps[gapIdx].monOpenTime;
   datetime rightTime  = GetNextMondayOpen(leftTime);
   long     weekLength = rightTime - leftTime;
   double   mid        = (g_gaps[gapIdx].gapHigh + g_gaps[gapIdx].gapLow) / 2.0;

   string rect = prefix + "_RECT";
   string midL = prefix + "_MID";
   string lbl  = prefix + "_LBL";

//--- gap rectangle
   if(ObjectFind(0, rect) < 0)
     {
      ObjectCreate(0, rect, OBJ_RECTANGLE, 0, leftTime, g_gaps[gapIdx].gapHigh, rightTime, g_gaps[gapIdx].gapLow);
      ObjectSetInteger(0, rect, OBJPROP_COLOR, ActiveOutlineColor);
      ObjectSetInteger(0, rect, OBJPROP_WIDTH, LineWidth);
      ObjectSetInteger(0, rect, OBJPROP_FILL, true);
      ObjectSetInteger(0, rect, OBJPROP_BGCOLOR, ColorSetAlpha(ActiveFillColor, (uchar)(ActiveFillOpacity * 255 / 100)));
      ObjectSetInteger(0, rect, OBJPROP_BACK, true);
     }

//--- midline (solid yellow for clear visibility)
   if(ObjectFind(0, midL) < 0)
     {
      ObjectCreate(0, midL, OBJ_TREND, 0, leftTime, mid, rightTime, mid);
      ObjectSetInteger(0, midL, OBJPROP_COLOR, clrYellow);
      ObjectSetInteger(0, midL, OBJPROP_STYLE, STYLE_SOLID);
      ObjectSetInteger(0, midL, OBJPROP_WIDTH, 2);
      ObjectSetInteger(0, midL, OBJPROP_BACK, true);
     }

//--- main label showing gap size
   if(ObjectFind(0, lbl) < 0)
     {
      datetime labelTime = (datetime)(leftTime + (long)(weekLength * 0.25));
      DrawTextLabel(lbl, labelTime, mid, StringFormat("WG | %.1fp", GapPips(gapIdx)), clrBlack, FontSize, "Arial Bold", ANCHOR_CENTER);
     }

//--- detailed labels (Friday close, Monday open, midpoint)
   if(ShowDetailedLabels)
     {
      datetime detailTime = (datetime)(leftTime + (long)(weekLength * 0.05));
      double gapHeight    = g_gaps[gapIdx].gapHigh - g_gaps[gapIdx].gapLow;
      double offset       = gapHeight * 0.1;
      int smallFont       = MathMax(FontSize - 1, 6);

      string topText;
      string botText;

      if(g_gaps[gapIdx].isGapDown)
        {
         topText = "FRIDAY CLOSE";
         botText = "MONDAY OPEN";
        }
      else
        {
         topText = "MONDAY OPEN";
         botText = "FRIDAY CLOSE";
        }

      DrawTextLabel(prefix + "_TOP", detailTime, g_gaps[gapIdx].gapHigh - offset, topText, clrBlack, smallFont, "Arial");
      DrawTextLabel(prefix + "_BOT", detailTime, g_gaps[gapIdx].gapLow + offset,  botText, clrBlack, smallFont, "Arial");
      DrawTextLabel(prefix + "_MIDPRICE", detailTime, mid,                        "MIDPOINT",     clrBlack, smallFont, "Arial");
     }
  }

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

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

Обновление графических объектов гэпа

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

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

//+------------------------------------------------------------------+
//| Update visuals when gap state changes (signal added, filled)     |
//+------------------------------------------------------------------+
void UpdateGapVisuals(int gapIdx, string prefix)
  {
   if(!ShowHistoricalGaps || gapIdx < 0 || gapIdx >= ArraySize(g_gaps))
      return;

   string rect = prefix + "_RECT";
   string lbl  = prefix + "_LBL";

   if(ObjectFind(0, rect) < 0)
      return;

   int sigCount = ArraySize(g_gaps[gapIdx].signals);

//--- if there is at least one signal, highlight rectangle with signal color
   if(sigCount > 0)
     {
      SignalRecord latest = g_gaps[gapIdx].signals[sigCount - 1];
      color sigColor = latest.signalIsBuy ? clrLimeGreen : clrRed;
      ObjectSetInteger(0, rect, OBJPROP_COLOR, sigColor);
      ObjectSetInteger(0, rect, OBJPROP_FILL, true);
      //--- use semi-transparent version of signal color for background
      color bgColor = ColorSetAlpha(sigColor, 60);
      ObjectSetInteger(0, rect, OBJPROP_BGCOLOR, bgColor);

      if(ObjectFind(0, lbl) >= 0)
        {
         ObjectSetInteger(0, lbl, OBJPROP_COLOR, clrBlack);
         string side = latest.signalIsBuy ? "BUY" : "SELL";
         ObjectSetString(0, lbl, OBJPROP_TEXT, StringFormat("WG | %.1fp | %s", GapPips(gapIdx), side));
        }
      return;
     }

//--- no signal: default inactive appearance
   ObjectSetInteger(0, rect, OBJPROP_COLOR, ActiveOutlineColor);
   ObjectSetInteger(0, rect, OBJPROP_STYLE, STYLE_SOLID);
   ObjectSetInteger(0, rect, OBJPROP_WIDTH, LineWidth);
   ObjectSetInteger(0, rect, OBJPROP_FILL, true);
   ObjectSetInteger(0, rect, OBJPROP_BGCOLOR, ColorSetAlpha(ActiveFillColor, (uchar)(ActiveFillOpacity * 255 / 100)));

   if(ObjectFind(0, lbl) >= 0)
     {
      ObjectSetInteger(0, lbl, OBJPROP_COLOR, clrBlack);
      ObjectSetString(0, lbl, OBJPROP_TEXT, StringFormat("WG | %.1fp", GapPips(gapIdx)));
     }
  }

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

Недельные разделители и текстовые метки

DrawWeekSeparator() создает вертикальную линию в начале каждой недели гэпа. Эта функция поддерживает порядок на графике и упрощает определение недельных границ.

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

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

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

//+------------------------------------------------------------------+
//| Draw or update a text label on the chart                         |
//+------------------------------------------------------------------+
void DrawTextLabel(string name, datetime t, double p, string txt, color clr, int sz, string font, int anchor = ANCHOR_LEFT)
  {
   if(ObjectFind(0, name) < 0)
      ObjectCreate(0, name, OBJ_TEXT, 0, t, p);
   else
      ObjectMove(0, name, 0, t, p);

   ObjectSetString(0, name, OBJPROP_TEXT, txt);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, sz);
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetString(0, name, OBJPROP_FONT, font);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor);
  }

Основной цикл расчета

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

При первом запуске OnCalculate() вызывает DetectAllGaps() и ScanHistoricalSignals(). Затем она создает графические объекты для каждого обнаруженного гэпа и обновляет их внешний вид. После этого вызывается RenderSignalBuffers(), чтобы исторические сигналы были записаны в буферы индикатора.

//+------------------------------------------------------------------+
//| Custom indicator iteration function (main calculation)           |
//+------------------------------------------------------------------+
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[])
  {
//--- ensure all arrays are series-aligned (newest bar at index 0)
   ArraySetAsSeries(time, true);
   ArraySetAsSeries(open, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);
   ArraySetAsSeries(close, true);

   if(rates_total < 3)
      return 0;

//--- detect new bar (closed bar)
   bool newBar = (time[0] != g_lastBarTime);
   if(newBar)
      g_lastBarTime = time[0];

//--- first run or full recalculation: build gap database and historical signals
   if(prev_calculated == 0 || g_firstRun)
     {
      DetectAllGaps();
      ScanHistoricalSignals();

      if(ShowHistoricalGaps)
        {
         for(int g = 0; g < ArraySize(g_gaps); g++)
           {
            string prefix = PrefixForIndex(g);
            CreateGapObjects(g, prefix);
            UpdateGapVisuals(g, prefix);
           }
        }

      RenderSignalBuffers(rates_total);
      g_firstRun = false;
      return rates_total;
     }

//--- on a new bar, check for new gaps and live signals
   if(newBar)
     {
      //--- detect potential new weekend gap using closed bars only
      if((time[1] - time[2]) >= 172800)
        {
         if(!GapExistsByMonOpen(time[1]))
           {
            double mondayOpen = open[1];
            double fridayClose = close[2];
            double gp = MathAbs(mondayOpen - fridayClose) / PipSize();
            if(gp >= MinGapPips)
              {
               GapInfo gap;
               gap.monOpenTime = time[1];
               gap.gapHigh = MathMax(mondayOpen, fridayClose);
               gap.gapLow = MathMin(mondayOpen, fridayClose);
               gap.isGapDown = (fridayClose > mondayOpen);
               gap.gapFilled = false;
               gap.scanStartIndex = 1;
               gap.weekLow = 0;
               gap.weekHigh = 0;
               gap.lastSignalBarTime = 0;
               ArrayResize(gap.signals, 0);
               AddGap(gap);
               int idx = ArraySize(g_gaps) - 1;
               if(ShowHistoricalGaps)
                 {
                  string prefix = PrefixForIndex(idx);
                  CreateGapObjects(idx, prefix);
                  UpdateGapVisuals(idx, prefix);
                 }
              }
           }
        }

      //--- check for signals on the latest completed bar
      CheckLiveSignals(time, high, low, close);
      RenderSignalBuffers(rates_total);

      //--- update visual appearance of all gaps
      if(ShowHistoricalGaps)
        {
         for(int g = 0; g < ArraySize(g_gaps); g++)
            UpdateGapVisuals(g, PrefixForIndex(g));
        }
     }

   return rates_total;
  }

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

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

Отрисовка буферов

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

Сначала функция очищает все ячейки буфера, присваивая им EMPTY_VALUE. Благодаря этому только позиции подтвержденных сигналов будут содержать значения. После этого функция перебирает каждый гэп и каждый сигнал внутри него. Для каждого сигнала функция находит соответствующий индекс бара с помощью iBarShift() и записывает цену сигнала и значения TP/SL в соответствующие ячейки буферов.

//+------------------------------------------------------------------+
//| Render arrow buffers from stored signals                         |
//+------------------------------------------------------------------+
void RenderSignalBuffers(const int rates_total)
  {
//--- reset buffers (called only on new bar, so performance acceptable for most cases)
   for(int i = 0; i < rates_total; i++)
     {
      BufferBuyArrow[i] = EMPTY_VALUE;
      BufferSellArrow[i] = EMPTY_VALUE;
      BufferBuyTP[i]    = EMPTY_VALUE;
      BufferBuySL[i]    = EMPTY_VALUE;
      BufferSellTP[i]   = EMPTY_VALUE;
      BufferSellSL[i]   = EMPTY_VALUE;
     }

//--- fill from gap signals
   for(int g = 0; g < ArraySize(g_gaps); g++)
     {
      for(int s = 0; s < ArraySize(g_gaps[g].signals); s++)
        {
         SignalRecord sig = g_gaps[g].signals[s];
         int sh = iBarShift(_Symbol, _Period, sig.signalTime, false);
         if(sh < 0 || sh >= rates_total)
            continue;

         if(sig.signalIsBuy)
           {
            BufferBuyArrow[sh] = sig.signalPrice;
            BufferBuyTP[sh]    = NormalizeDouble(sig.signalTP, _Digits);
            BufferBuySL[sh]    = NormalizeDouble(sig.signalSL, _Digits);
           }
         else
           {
            BufferSellArrow[sh] = sig.signalPrice;
            BufferSellTP[sh]    = NormalizeDouble(sig.signalTP, _Digits);
            BufferSellSL[sh]    = NormalizeDouble(sig.signalSL, _Digits);
           }
        }
     }
  }

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

Окончательная очистка и управление ресурсами

Наконец, реализуется очистка ресурсов. DeleteWGObjects() удаляет все графические объекты, созданные индикатором, по префиксу "WG_". Благодаря этому после удаления индикатора на графике не остается лишних объектов.

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



Результаты

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

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


Выводы

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

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

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

Прикрепленные файлы |
Возможности Мастера MQL5, которые вам нужно знать (Часть 97): Использование выпуклой оболочки и миниатюрной GRU-сети в пользовательском классе трейлинг-стопа Возможности Мастера MQL5, которые вам нужно знать (Часть 97): Использование выпуклой оболочки и миниатюрной GRU-сети в пользовательском классе трейлинг-стопа
В этой статье рассматривается пользовательский класс Мастера MQL5 для трейлинг-стопов. Пользовательский класс CTrailingConvexHullGRU создается путем объединения алгоритма выпуклой оболочки и GRU-сети. Как и в других статьях серии, разрабатывается модель, которую можно тестировать с советниками, собранными Мастером MQL5, и настраивать с помощью различных классов управления капиталом и сигналов входа. В тестировании используются классы сигналов Envelopes и RSI.
Моделирование рынка: В единстве — сила (II) Моделирование рынка: В единстве — сила (II)
До сих пор приложение, разрабатываемое в рамках этой серии статей, было сосредоточено исключительно на моделировании графической части. Однако, чтобы получить более полную систему, в которой мы сможем протестировать советник в рамках сервиса репликации/моделирования, нам также необходимо смоделировать торговый сервер. Вы заметите, что это моделирование будет включать только самые необходимые элементы. Тем не менее вы, уважаемый читатель, сможете дополнить недостающие части. Поскольку эти дополнительные компоненты не влияют на то, что я хочу показать, у нас уже более чем достаточно для реализации задуманного.
Разработка инструментария для анализа Price Action (Часть 74): Создание советника MQL5 на основе буферов индикатора Разработка инструментария для анализа Price Action (Часть 74): Создание советника MQL5 на основе буферов индикатора
В этой статье реализован советник MQL5, который подключается к индикатору гэпов выходного дня через iCustom() и CopyBuffer() и считывает шесть буферов с сигналами на покупку и продажу, а также уровнями SL/TP. Он проверяет требования брокера к минимальному расстоянию до стоп-уровней, обрабатывает подтверждение закрытым баром и контролирует дублирование сигналов, а также исполняет ордера с настраиваемым магическим числом. Советник также включает механику переноса стоп-лосса при достижении срединного уровня между входом и тейк-профитом, а также процедуру тестирования на исторических данных, позволяющую проверить поведение и адаптировать параметры под вашу конфигурацию.
Причинный граф как признак машинного обучения Причинный граф как признак машинного обучения
В статье условный граф причинности Грейнджера по 10 валютным парам и трём таймфреймам преобразуется в числовые признаки для CatBoost и MQL5. Абляция сравнивает модели price-only, graph-only и price+graph: графовые признаки почти не работают самостоятельно, но в сочетании с ценой улучшают контекст, режимную фильтрацию и риск. На форварде совместная модель показала более высокий Profit Factor и меньшую просадку.