English
preview
Автоматизация графических паттернов в MQL5 (Часть 1): Мультитаймфреймовый движок для анализа свинговой структуры

Автоматизация графических паттернов в MQL5 (Часть 1): Мультитаймфреймовый движок для анализа свинговой структуры

MetaTrader 5 — Примеры |
54 0
Tola Moses Hector
Tola Moses Hector

Введение

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

Есть и вторая, связанная с первой проблема — столь же распространенная, но гораздо реже обсуждаемая. Большинство советников, использующих свинги, определяют рыночную структуру на том же таймфрейме, на котором торгуют. В результате скальпинговый советник на M15 определяет "тренд" по свингам M15, которым свойственен высокий уровень шума, и десятки раз в день пересматривает его направление. Трейдер, анализирующий график, никогда бы так не поступил: сначала он посмотрел бы на H4 или дневной график, чтобы определить предпочтительное направление торговли, а затем перешел бы на младший таймфрейм для выбора момента входа. Описанный в статье движок реализует ту же дисциплину в коде: рыночная структура всегда определяется по H4 независимо от таймфрейма графика, на котором запущен советник или индикатор.

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

В этой статье закладывается основа: CSwingEngine — самодостаточный, пригодный для повторного использования класс, который можно подключить к любому советнику или индикатору одной строчкой кода. Движок определяет свинговые точки на H4 с учетом настраиваемых глубины истории и силы свинга, присваивает каждому свингу метку HH, LH, HL или LL по мере его формирования и определяет рыночную структуру по последовательности этих меток. Он также предоставляет понятный публичный интерфейс для получения текущего состояния тренда и массива свингов. При этом объекты корректно отображаются на любом графике младшего таймфрейма, на котором запущена использующая движок программа.

Главный вопрос, на который призван ответить CSwingEngine, звучит так:

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

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

Мы рассмотрим следующие темы:

  1. Почему обнаружение паттернов без учета контекста не работает
  2. Что делает свинговую точку значимой
  3. Как последовательности свингов определяют рыночную структуру
  4. Архитектура — расчеты на H4, отрисовка на таймфрейме графика
  5. Реализация на MQL5
  6. Как этот движок используется в последующих статьях
  7. Тестирование на исторических данных и проверка корректности
  8. Известные ограничения
  9. Заключение


Почему обнаружение паттернов без учета контекста не работает

Why context matters

Рис. 1. Почему важен контекст

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

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

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

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

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

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

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


Что делает свинговую точку значимой

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

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

У этого движка есть два отличия от алгоритма, который определяет свинги на торговом таймфрейме:

  1. Параметр N применяется к барам H4, а не к барам текущего графика. При N = 3 на H4 окно уже охватывает примерно половину торгового дня с каждой стороны от предполагаемой свинговой точки. По значимости это сопоставимо с гораздо большим N на M15 или H1. Это сделано намеренно. Параметр силы свинга должен отражать значимость точки в рыночной структуре, а эта значимость определяется на старшем таймфрейме.
  2. Каждому подтвержденному свингу метка присваивается сразу в момент классификации по той же схеме, которую используют индикаторы Smart Money Concept: HH (higher high, более высокий максимум) или LH (lower high, более низкий максимум) — для свинговых максимумов; HL (higher low, более высокий минимум) или LL (lower low, более низкий минимум) — для свинговых минимумов. Свинговому максимуму присваивается метка HH, если он выше предыдущего подтвержденного свингового максимума; в противном случае — LH. Свинговому минимуму присваивается метка HL, если он выше предыдущего подтвержденного свингового минимума; в противном случае — LL. Эти метки нужны не только для наглядности: алгоритм классификации тренда из раздела 3 использует их напрямую. Они также позволяют понять график с первого взгляда, без мысленного сравнения ценовых уровней.

Свинговую точку можно подтвердить только после формирования N баров справа от нее — на H4, а не на таймфрейме текущего графика.


Как последовательности свингов определяют рыночную структуру

Swing labels and market structure

Рис. 2. Метки свингов и рыночная структура

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

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

Для нисходящего тренда ситуация зеркальная: последний свинговый максимум имеет метку LH, а последний свинговый минимум — LL.

К боковому рынку относятся все остальные случаи — сочетание HH и LL, сочетание LH и HL или недостаточная история свингов. Смешанное сочетание меток означает, что рынок обновил экстремум в одном направлении, но не в другом, — это и есть признак переходного состояния или бокового диапазона.

Эта классификация определяет работу алгоритмов в последующих статьях, а ее результат всегда получается по данным H4. Когда во второй части серии проверяется паттерн "двойная вершина", например на графике H1, сначала по-прежнему проверяется, возвращает ли CSwingEngine значение TREND_UP. Но этот результат отражает структуру на H4, а не шум на H1. Если движок не возвращает TREND_UP, проверка паттерна немедленно прекращается. Форма потенциального паттерна по-прежнему определяется по движению цены на H1 (или на другом таймфрейме графика, на котором запущен советник). Но ее проверка начинается только тогда, когда рыночный контекст на H4 допускает такой паттерн.


Архитектура — расчеты на H4, отрисовка на таймфрейме графика

Реализация CSwingEngine организована в двух файлах.

  1. SwingEngine.mqh содержит самодостаточный класс движка. Класс обнаруживает свинговые точки на H4, присваивает им метки, классифицирует рыночную структуру и предоставляет доступ к массиву свингов и состоянию тренда через простой публичный интерфейс. Класс не содержит логики распознавания конкретных графических паттернов, а его таймфрейм анализа не зависит от графика, на котором запущена использующая его программа.
  2. SwingDemo.mq5 — демонстрационный индикатор. Индикатор позволяет использовать CSwingEngine на любом графике — обычно на младшем таймфрейме, на котором вы планируете торговать, например M15 или H1. Он отображает каждый обнаруженный свинговый максимум и минимум на H4 со стрелкой и соответствующей меткой HH/LH/HL/LL. Цвета меток выбраны в стиле инструментов Smart Money Concept: ярко-голубой для HH, приглушенный синий для LH, ярко-зеленый для HL и приглушенный зеленый для LL. Текущее состояние тренда отображается в виде метки в левом верхнем углу графика.

Ключевая особенность архитектуры — отсутствие необходимости преобразовывать координаты между таймфреймами. Для каждой свинговой точки движок сохраняет время открытия соответствующего бара H4 в формате datetime. MetaTrader 5 размещает графические объекты (стрелки, метки, трендовые линии) по значению datetime на горизонтальной оси независимо от периода графика. Свинговый максимум на H4 с временной координатой, например, 08:00 определенного дня отобразится на графике M15 точно в нужном месте по горизонтали — над группой свечей M15, образующих этот бар H4. Дополнительный код для этого не требуется. Это штатная возможность графиков MetaTrader 5, которую не нужно отдельно реализовывать в движке. О ней стоит сказать прямо: именно благодаря ей архитектура остается простой, без ненадежных дополнительных механизмов.

Swing engine architecture

Рис. 3. Архитектура движка анализа свингов

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


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

Движок создается по частям: сначала дается объяснение каждого компонента, а затем приводится код.

Структура свинговой точки и перечисление состояний тренда

Каждая обнаруженная свинговая точка сохраняется в простой структуре, которая теперь помимо исходных полей содержит метку HH/LH/HL/LL. В перечислении определены три возможных состояния рыночной структуры.

Сохраните файл по пути MQL5\Include\ChartPatterns\SwingEngine.mqh.

//+------------------------------------------------------------------+
//|                                                SwingEngine.mqh   |
//|   Multi-timeframe swing detection and trend classification       |
//|   Swings are calculated on a higher timeframe (default H4) and   |
//|   drawn correctly on whatever timeframe the chart is showing.    |
//+------------------------------------------------------------------+
#ifndef SWINGENGINE_MQH
#define SWINGENGINE_MQH

//+------------------------------------------------------------------+
//| Market structure classification                                  |
//+------------------------------------------------------------------+
enum ENUM_SWING_TREND
  {
   TREND_UP,    // Last confirmed swing high is HH and swing low is HL
   TREND_DOWN,  // Last confirmed swing high is LH and swing low is LL
   TREND_RANGE  // Mixed labels or insufficient swing history
  };

//+------------------------------------------------------------------+
//| One detected swing point                                         |
//+------------------------------------------------------------------+
struct SSwingPoint
  {
   bool              is_high;   // true = swing high, false = swing low
   double            price;     // Price of the swing (H4 high or low)
   datetime          time;      // Open time of the H4 swing bar
   int               bar_index; // H4 bars ago at time of detection
   string            label;     // "HH", "LH", "HL", or "LL"
  };

Слой отрисовки использует поле time. В поле time хранится фактическое время открытия бара H4, а не индекс бара на текущем графике. Поэтому объект, привязанный к этому времени, корректно размещается на графике любого таймфрейма. Поле bar_index содержит индекс бара H4, а не бара текущего графика. Это важно, если алгоритм обнаружения паттернов в одной из следующих статей использует его для измерения расстояния между двумя свингами. К этому вопросу мы вернемся в разделе 6.

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

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

//+------------------------------------------------------------------+
//| Monetary value of one pip for a given symbol and lot size        |
//+------------------------------------------------------------------+
double GetPipValue(const string symbol, double lots)
  {
   double tick_val  = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);   // Tick value
   double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);    // Tick size
   int    digits    = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);       // Symbol digits
   double point     = SymbolInfoDouble(symbol, SYMBOL_POINT);              // Point size
   double pip_size  = (digits == 3 || digits == 5) ? point * 10.0 : point; // Pip size
   if(tick_size <= 0 || tick_val <= 0)
      return 0;                                                            // Validate inputs
   return (pip_size / tick_size) * tick_val * lots;                        // Return pip value
  }

//+------------------------------------------------------------------+
//| Returns pip size in price units for the given symbol             |
//+------------------------------------------------------------------+
double PipSize(const string symbol)
  {
   int    digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);          // Symbol digits
   double point  = SymbolInfoDouble(symbol, SYMBOL_POINT);                 // Point size
   return (digits == 3 || digits == 5) ? point * 10.0 : point;             // Return pip size
  }

Класс движка

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

//+------------------------------------------------------------------+
//| CSwingEngine—HTF swing detection, chart-timeframe-agnostic       |
//+------------------------------------------------------------------+
class CSwingEngine
  {
private:
   SSwingPoint       m_swings[];     // Confirmed swing points (newest first)
   int               m_count;        // Number of swings currently stored
   int               m_strength;     // H4 bars required on each side to confirm a swing
   int               m_lookback;     // Maximum H4 bars to scan on each Update() call
   ENUM_SWING_TREND  m_trend;        // Current classified trend
   string            m_symbol;       // Symbol being analyzed
   ENUM_TIMEFRAMES   m_swing_tf;     // Timeframe swings are CALCULATED on (default H4)
   datetime          m_last_htf_bar; // Last H4 bar time processed

   //+------------------------------------------------------------------+
   //| Checks if H4 bar at index is a confirmed swing high              |
   //+------------------------------------------------------------------+
   bool              IsSwingHigh(const double &high[], int idx, int total)
     {
      if(idx - m_strength < 0 || idx + m_strength >= total)
         return false;                                                      // Boundary check
      for(int i = 1; i <= m_strength; i++)
        {
         if(high[idx - i] >= high[idx])
            return false;                                                   // Left side must be lower
         if(high[idx + i] >= high[idx])
            return false;                                                   // Right side must be lower
        }
      return true;                                                          // Confirmed swing high
     }

   //+------------------------------------------------------------------+
   //| Checks if H4 bar at index is a confirmed swing low               |
   //+------------------------------------------------------------------+
   bool              IsSwingLow(const double &low[], int idx, int total)
     {
      if(idx - m_strength < 0 || idx + m_strength >= total)
         return false;                                                      // Boundary check
      for(int i = 1; i <= m_strength; i++)
        {
         if(low[idx - i] <= low[idx])
            return false;                                                   // Left side must be higher
         if(low[idx + i] <= low[idx])
            return false;                                                   // Right side must be higher
        }
      return true;                                                          // Confirmed swing low
     }

   //+------------------------------------------------------------------+
   //| Sorts swings oldest-to-newest, assigns HH/LH/HL/LL labels, then  |
   //| reverses to newest-first so index 0 is always the most recent    |
   //+------------------------------------------------------------------+
   void              LabelSwings()
     {
      //--- Sort ascending by time—labeling must walk oldest to newest
      for(int i = 0; i < m_count - 1; i++)
         for(int j = i + 1; j < m_count; j++)
            if(m_swings[i].time > m_swings[j].time)
              {
               SSwingPoint tmp = m_swings[i];
               m_swings[i] = m_swings[j];
               m_swings[j] = tmp;
              }
      //--- Assign HH/LH/HL/LL by comparing each swing to the last of its kind
      double lastHigh = -1, lastLow = DBL_MAX;
      for(int i = 0; i < m_count; i++)
        {
         if(m_swings[i].is_high)
           {
            m_swings[i].label = (lastHigh < 0 || m_swings[i].price > lastHigh) ? "HH" : "LH";
            lastHigh = m_swings[i].price;
           }
         else
           {
            m_swings[i].label = (lastLow == DBL_MAX || m_swings[i].price > lastLow) ? "HL" : "LL";
            lastLow = m_swings[i].price;
           }
        }
      //--- Reverse to newest-first—matches the public GetSwing(0) contract
      for(int i = 0, j = m_count - 1; i < j; i++, j--)
        {
         SSwingPoint tmp = m_swings[i];
         m_swings[i] = m_swings[j];
         m_swings[j] = tmp;
        }
     }

   //+------------------------------------------------------------------+
   //| Classifies trend from the two most recent labels                 |
   //+------------------------------------------------------------------+
   void              ClassifyTrend()
     {
      if(m_count < 4)
        {
         m_trend = TREND_RANGE;
         return;
        }                                                                   // Need at least 4 swings
      SSwingPoint lastHigh = GetLastSwingHigh();                            // Most recent labeled high
      SSwingPoint lastLow  = GetLastSwingLow();                             // Most recent labeled low
      if(lastHigh.label == "HH" && lastLow.label == "HL")
         m_trend = TREND_UP;                                                // New high + higher low
      else
         if(lastHigh.label == "LH" && lastLow.label == "LL")
            m_trend = TREND_DOWN;                                           // New low + lower high
         else
            m_trend = TREND_RANGE;                                          // Mixed sequence
     }

public:
                     CSwingEngine() : m_count(0), m_strength(3), m_lookback(200),
                     m_trend(TREND_RANGE), m_swing_tf(PERIOD_H4),
                     m_last_htf_bar(0)
     { m_symbol = _Symbol; }

   //+------------------------------------------------------------------+
   //| Initialize the engine                                            |
   //| swing_tf is the timeframe structure is READ from (default H4).   |
   //| It is independent of whatever chart the calling code is on.      |
   //+------------------------------------------------------------------+
   bool              Init(int strength, int lookback,
                          ENUM_TIMEFRAMES swing_tf = PERIOD_H4,
                          const string symbol = "")
     {
      m_strength  = MathMax(1, strength);                                   // Minimum strength of 1
      m_lookback  = MathMax(m_strength * 4, lookback);                      // Minimum sensible lookback
      m_swing_tf  = swing_tf;                                               // Analysis timeframe
      m_symbol    = (symbol == "") ? _Symbol : symbol;                      // Use chart symbol if blank
      m_count     = 0;                                                      // Reset swing count
      m_last_htf_bar = 0;                                                   // Reset HTF bar tracker
      m_trend     = TREND_RANGE;                                            // Start as range
      ArrayResize(m_swings, 0);                                             // Clear swing array
      int bars_available = Bars(m_symbol, m_swing_tf);                      // Check available H4 bars
      if(bars_available < m_lookback + m_strength * 2)                      // Insufficient bars
        {
         Print("CSwingEngine: Insufficient ", EnumToString(m_swing_tf),
               " bars. Available:", bars_available,
               " Required:", m_lookback + m_strength * 2);
         return false;
        }
      Print(StringFormat(
               "CSwingEngine: Initialized | Symbol:%s | SwingTF:%s | Strength:%d | Lookback:%d",
               m_symbol, EnumToString(m_swing_tf), m_strength, m_lookback));
      return true;
     }

   //+------------------------------------------------------------------+
   //| Scans H4 bars and updates swing points and trend classification  |
   //| Gated on a NEW H4 BAR, not a new bar on the calling chart.       |
   //| Safe to call every tick from OnTick() or OnCalculate().          |
   //+------------------------------------------------------------------+
   bool              Update()
     {
      datetime htf_bar = iTime(m_symbol, m_swing_tf, 0);                    // Current H4 bar time
      if(htf_bar == 0)
         return false;                                                      // H4 data not ready
      if(htf_bar == m_last_htf_bar)
         return false;                                                      // Same H4 bar — skip
      m_last_htf_bar = htf_bar;                                             // Update HTF bar tracker
      //--- Copy H4 price data for the lookback window
      int    total = m_lookback + m_strength * 2;                           // Total H4 bars needed
      double high[], low[];
      datetime times[];
      ArraySetAsSeries(high,  true);                                        // Newest first
      ArraySetAsSeries(low,   true);                                        // Newest first
      ArraySetAsSeries(times, true);                                        // Newest first
      if(CopyHigh(m_symbol, m_swing_tf, 0, total, high)  < total)
         return false;                                                      // Copy H4 highs
      if(CopyLow(m_symbol,  m_swing_tf, 0, total, low)   < total)
         return false;                                                      // Copy H4 lows
      if(CopyTime(m_symbol, m_swing_tf, 0, total, times) < total)
         return false;                                                      // Copy H4 times
      //--- Scan for swing points in the confirmed zone
      ArrayResize(m_swings, 0);                                             // Clear and rebuild
      m_count = 0;
      for(int i = m_strength; i < total - m_strength; i++)                  // Scan confirmed zone
        {
         if(IsSwingHigh(high, i, total))                                    // Confirmed swing high
           {
            SSwingPoint sp;
            sp.is_high = true;
            sp.price = high[i];
            sp.time = times[i];
            sp.bar_index = i;
            sp.label = "";
            ArrayResize(m_swings, m_count + 1);
            m_swings[m_count] = sp;
            m_count++;
           }
         if(IsSwingLow(low, i, total))                                      // Confirmed swing low
           {
            SSwingPoint sp;
            sp.is_high = false;
            sp.price = low[i];
            sp.time = times[i];
            sp.bar_index = i;
            sp.label = "";
            ArrayResize(m_swings, m_count + 1);
            m_swings[m_count] = sp;
            m_count++;
           }
        }
      LabelSwings();                                                        // Sort, label, reverse
      ClassifyTrend();                                                      // Classify from labels
      return true;                                                          // New H4 bar processed
     }

   //+------------------------------------------------------------------+
   //| Returns the current classified market trend                      |
   //+------------------------------------------------------------------+
   ENUM_SWING_TREND  GetTrend()       { return m_trend; }

   //+------------------------------------------------------------------+
   //| Returns the timeframe swings are calculated on                   |
   //+------------------------------------------------------------------+
   ENUM_TIMEFRAMES   GetSwingTimeframe() { return m_swing_tf; }

   //+------------------------------------------------------------------+
   //| Returns the total number of confirmed swings currently stored    |
   //+------------------------------------------------------------------+
   int               GetSwingCount()  { return m_count; }

   //+------------------------------------------------------------------+
   //| Returns the swing point at the given index (0 = most recent)     |
   //+------------------------------------------------------------------+
   SSwingPoint       GetSwing(int index)
     {
      SSwingPoint empty = {false, 0, 0, 0, ""};                             // Empty result
      if(index < 0 || index >= m_count)
         return empty;                                                      // Bounds check
      return m_swings[index];                                               // Return swing
     }

   //+------------------------------------------------------------------+
   //| Returns the most recent confirmed swing high                     |
   //+------------------------------------------------------------------+
   SSwingPoint       GetLastSwingHigh()
     {
      SSwingPoint empty = {true, 0, 0, 0, ""};                              // Empty result
      for(int i = 0; i < m_count; i++)                                      // Scan from newest
         if(m_swings[i].is_high)
            return m_swings[i];                                             // Return first high
      return empty;                                                         // None found
     }

   //+------------------------------------------------------------------+
   //| Returns the most recent confirmed swing low                      |
   //+------------------------------------------------------------------+
   SSwingPoint       GetLastSwingLow()
     {
      SSwingPoint empty = {false, 0, 0, 0, ""};                             // Empty result
      for(int i = 0; i < m_count; i++)                                      // Scan from newest
         if(!m_swings[i].is_high)
            return m_swings[i];                                             // Return first low
      return empty;                                                         // None found
     }

   //+------------------------------------------------------------------+
   //| Returns a human-readable string for the current trend            |
   //+------------------------------------------------------------------+
   string            GetTrendString()
     {
      switch(m_trend)
        {
         case TREND_UP:
            return "UPTREND";
         case TREND_DOWN:
            return "DOWNTREND";
         default:
            return "RANGE";
        }
     }
  };

#endif // SWINGENGINE_MQH
//+------------------------------------------------------------------+

Update() при каждом вызове полностью перестраивает массив свингов — это гарантирует его соответствие текущим данным H4 и исключает устаревшие свинговые точки. Ключевой момент — проверка в начале метода: значение iTime(m_symbol, m_swing_tf, 0) сравнивается со временем последнего обработанного бара H4. Поэтому ресурсоемкая перестройка выполняется лишь раз в четыре часа независимо от того, как часто Update() вызывается на графике M15 или M5 с частыми тиками. Разделение таймфреймов не только обеспечивает корректность работы, но и дает дополнительное преимущество: на младшем таймфрейме с частыми тиками движок требует меньше вычислительных ресурсов, чем версия, анализирующая бары самого графика.

LabelSwings() сортирует обнаруженные свинги в хронологическом порядке и последовательно обрабатывает их от самых старых к самым новым. Каждый максимум или минимум сравнивается с предыдущим свингом того же типа и получает соответствующую метку HH/LH или HL/LL. На заключительном этапе этот движок меняет порядок элементов на обратный: сначала снова идут самые новые свинги, как и требуется для GetSwing(0) и остальных методов публичного интерфейса.

Демонстрационный индикатор

Сохраните файл по пути MQL5\Indicators\SwingDemo.mq5. Этот индикатор можно прикрепить к графику любого таймфрейма. В следующих статьях серии для обнаружения паттернов будут использоваться M15 и H1, но работающий в индикаторе движок всегда анализирует H4. Свинговые максимумы и минимумы отображаются в том же стиле, что и в инструментах Smart Money Concept: над максимумами — стрелка вниз и метка HH/LH, под минимумами — стрелка вверх и метка HL/LL. Для меток, подтверждающих направление на старшем таймфрейме (HH, HL), используются яркие цвета, для противоположных меток (LH, LL) — более приглушенные.

//+------------------------------------------------------------------+
//|                                                    SwingDemo.mq5 |
//|     Visual demonstration of CSwingEngine — H4 structure drawn on |
//|                 whatever timeframe this indicator is attached to |
//|                                Copyright 2026, Tola Moses Hector |
//|                                          https://t.me/tolahector |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Tola Moses Hector"
#property link      "https://t.me/tolahector"
#property version   "1.00"
#property indicator_chart_window
#property indicator_plots 0

#include <ChartPatterns\SwingEngine.mqh>

input int              InpStrength = 3;                       // Swing strength — H4 bars on each side
input int              InpLookback = 200;                     // Maximum H4 bars to scan
input ENUM_TIMEFRAMES  InpSwingTF  = PERIOD_H4;               // Timeframe swings are calculated on

CSwingEngine g_engine;
int          g_atr_handle = INVALID_HANDLE;                   // ATR on the swing timeframe — sets label offset

//+------------------------------------------------------------------+
//| Indicator initialization function                                |
//+------------------------------------------------------------------+
int OnInit()
  {
   if(!g_engine.Init(InpStrength, InpLookback, InpSwingTF))
      return INIT_FAILED;
   g_atr_handle = iATR(_Symbol, InpSwingTF, 14);              // ATR computed on the SAME TF as swings
   if(g_atr_handle == INVALID_HANDLE)
      return INIT_FAILED;
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Removes all objects drawn by this indicator                      |
//+------------------------------------------------------------------+
void ClearObjects()
  {
   int total = ObjectsTotal(0);
   for(int i = total - 1; i >= 0; i--)
     {
      string name = ObjectName(0, i);
      if(StringFind(name, "SWING_") == 0)
         ObjectDelete(0, name);
     }
  }

//+------------------------------------------------------------------+
//| Draws all H4 swing points on the current chart timeframe         |
//+------------------------------------------------------------------+
void DrawSwings()
  {
   ClearObjects();
   double atr_buf[];
   ArraySetAsSeries(atr_buf, true);
   double offset = 0;
   if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) >= 1)
      offset = atr_buf[0] * 0.3;                                             // Label/arrow offset

   int count = g_engine.GetSwingCount();
   for(int i = 0; i < count; i++)
     {
      SSwingPoint sp    = g_engine.GetSwing(i);                              // Get swing (time = H4 bar time)
      string      name  = "SWING_" + IntegerToString(i) + "_" +
                          TimeToString(sp.time, TIME_DATE | TIME_MINUTES);   // Unique name
      bool   is_confirm = (sp.label == "HH" || sp.label == "HL");            // Bright vs dim
      color  clr;
      int    arrow;
      double label_price, arrow_price;
      if(sp.is_high)
        {
         clr         = is_confirm ? C'100,200,255' : C'60,130,180';          // Bright = HH, dim = LH
         arrow       = 218;                                                  // Down arrow
         label_price = sp.price + offset;
         arrow_price = sp.price + offset * 0.4;
        }
      else
        {
         clr         = is_confirm ? C'100,255,120' : C'60,160,80';           // Bright = HL, dim = LL
         arrow       = 217;                                                  // Up arrow
         label_price = sp.price - offset;
         arrow_price = sp.price - offset * 0.4;
        }
      //--- sp.time is an H4 bar time. MetaTrader positions time-anchored
      //--- objects at the correct x-coordinate on ANY chart period, so
      //--- these draw correctly here even though they were computed
      //--- entirely from H4 data.
      ObjectCreate(0, name + "_A", OBJ_ARROW, 0, sp.time, arrow_price);
      ObjectSetInteger(0, name + "_A", OBJPROP_ARROWCODE, arrow);
      ObjectSetInteger(0, name + "_A", OBJPROP_COLOR, clr);
      ObjectSetInteger(0, name + "_A", OBJPROP_WIDTH, 1);
      ObjectCreate(0, name + "_L", OBJ_TEXT, 0, sp.time, label_price);
      ObjectSetString(0,  name + "_L", OBJPROP_TEXT, sp.label);
      ObjectSetInteger(0, name + "_L", OBJPROP_COLOR, clr);
      ObjectSetInteger(0, name + "_L", OBJPROP_FONTSIZE, 8);
     }
//--- Draw trend label
   string label_name = "SWING_TREND_LABEL";
   string trend_str  = g_engine.GetTrendString();
   color  trend_clr  = (g_engine.GetTrend() == TREND_UP)   ? clrLime :
                       (g_engine.GetTrend() == TREND_DOWN)  ? clrRed  : clrGray;
   ObjectCreate(0, label_name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, label_name, OBJPROP_XDISTANCE, 10);
   ObjectSetInteger(0, label_name, OBJPROP_YDISTANCE, 30);
   ObjectSetString(0,  label_name, OBJPROP_TEXT,
                   "H4 Trend: " + trend_str);
   ObjectSetInteger(0, label_name, OBJPROP_COLOR, trend_clr);
   ObjectSetInteger(0, label_name, OBJPROP_FONTSIZE, 12);
   ChartRedraw(0);
  }

//+------------------------------------------------------------------+
//| Custom indicator calculation function                            |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated,
                const datetime &time[], const double &open[],
                const double &high[], const double &low[], const double &close[],
                const long &tick_volume[], const long &volume[], const int &spread[])
  {
//--- Update() is internally gated on a new H4 bar, so calling it on
//--- every OnCalculate() pass—even on a fast chart timeframe—is cheap.
   if(g_engine.Update())
      DrawSwings();
   return rates_total;
  }

//+------------------------------------------------------------------+
//| Indicator deinitialization                                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   IndicatorRelease(g_atr_handle);
   ClearObjects();
  }
//+------------------------------------------------------------------+

Обратите внимание: при каждом выполнении обработчик событий OnCalculate вызывает g_engine.Update() без дополнительных условий. Отдельная проверка появления нового бара на таймфрейме графика не нужна: движок уже проверяет появление нового бара H4. Прикрепите индикатор к графику M15, и он будет перерисовываться не чаще раза в четыре часа — при закрытии очередной свечи H4, когда рыночная структура действительно может измениться.


Как этот движок используется в последующих статьях

В последующих статьях серии используется прежняя схема интеграции с одним дополнением: в Init() теперь явно передается таймфрейм анализа.

#include <ChartPatterns\SwingEngine.mqh>

CSwingEngine g_engine;

//--- In OnInit(), on an EA attached to M15 or H1:
if(!g_engine.Init(3, 300, PERIOD_H4))
   return INIT_FAILED;

//--- In OnTick(), once per chart bar or every tick—the engine self-gates on H4:
g_engine.Update();

//--- Before evaluating any Double Top pattern:
if(g_engine.GetTrend() != TREND_UP)
   return; // No H4 uptrend—no Double Top possible

Эта предварительная проверка — проверка результата GetTrend() перед анализом любого паттерна — остается ключевым архитектурным решением движка. Отличие в том, что предварительное условие теперь проверяется по структуре на H4, а форма паттерна по-прежнему анализируется по данным графика, на котором работает советник. Именно на этом таймфрейме определяются свинговые максимумы, пробои линии шеи и момент входа. Движок определяет контекст, а сигнал для входа формируется на таймфрейме графика.

При интеграции в следующих статьях нужно учесть одну деталь: поле bar_index каждой структуры SSwingPoint содержит индекс бара H4, а не бара текущего графика. Если алгоритм обнаружения паттернов измеряет расстояние между двумя вершинами — например, проверяет ограничение InpMaxPatternBars из второй части серии, — это расстояние и ограничение нужно либо выражать в барах H4, либо определять расстояние в барах торгового таймфрейма по значениям полей time с помощью iBarShift(). Прямое сравнение значений bar_index между таймфреймами без такого пересчета приведет к скрытой ошибке в оценке фактического расстояния между двумя вершинами.


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

Индикатор SwingDemo — основной инструмент проверки движка до реализации на его основе алгоритмов обнаружения паттернов.

Прикрепите его к графику EURUSD на H1, оставив для параметра InpSwingTF значение PERIOD_H4, и выполните следующие проверки.

Разметка свинговых точек. Каждая стрелка должна находиться в пределах временного интервала соответствующей свечи H4. На графике H1 стрелка свингового максимума должна располагаться над группой примерно из четырех последовательных свечей H1, образующих этот бар H4, на уровне его максимума. Если же стрелки привязаны к экстремумам отдельных свечей H1, скорее всего, для параметра InpSwingTF по ошибке оставлено значение PERIOD_CURRENT.

Корректность меток. Метка HH должна присваиваться только свинговому максимуму, который выше предыдущего максимума с меткой HH или LH. Метка LL должна присваиваться только свинговому минимуму, который ниже предыдущего минимума с меткой HL или LL. Прокрутите график назад до участка с четко выраженным восходящим трендом на H4 и убедитесь, что метки следуют в порядке HL, HH, HL, HH. Любое нарушение этой последовательности должно совпадать с заметной паузой в развитии восходящей структуры на самом графике H4.

Классификация тренда. Метка "H4 Trend: UPTREND" должна появляться только после того, как последний свинговый максимум получит метку HH, а последний свинговый минимум — HL. Для проверки переключитесь на график H4 и найдите на нем те же два свинга.

Периодичность обновления. Прикрепите индикатор к графику M15 и убедитесь по журналу "Эксперты" терминала, что сообщение "CSwingEngine: Initialized" выводится один раз, а последующие пересчеты выполняются примерно раз в четыре часа. Если график перерисовывается на каждом баре M15, проверка в Update() работает некорректно. Убедитесь, что параметру InpSwingTF по ошибке не присвоено значение PERIOD_CURRENT.

Параметры тестирования

Прикрепите индикатор SwingDemo к графикам со следующими настройками и визуально проверьте его работу:

Символ: EURUSD. Таймфрейм графика: H1. Таймфрейм свингов: H4. Сила свинга: 3. Глубина истории: 200. Убедитесь, что метки тренда на H4 соответствуют четко выраженной структуре на H4, хотя на самом графике отображаются свечи H1.

Demonstration on EURUSD

Рис. 4. Демонстрация на паре EURUSD

Символ: Gold. Таймфрейм графика: M15. Таймфрейм свингов: H4. Сила свинга: 3. Глубина истории: 200. На сильных направленных движениях цены золота легко убедиться, что метки, определенные по H4, остаются стабильными, несмотря на хаотичные колебания цены на M15 внутри этой структуры.

Demonstration on gold

Рис. 5. Демонстрация на золоте

Символ: GBPUSD. Таймфрейм графика: H1. Таймфрейм свингов: дневной (D1). Сила свинга: 3. Глубина истории: 150. Этот пример подтверждает, что движок работает с любой парой старшего и младшего таймфреймов, а не только с H4 для анализа и M15/H1 для торговли.

Demonstration on GBPUSD

Рис. 6. Демонстрация на паре GBPUSD


Известные ограничения

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

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

Для работы Update() необходим достаточный объем исторических данных H4 по символу независимо от того, сколько данных загружено для таймфрейма текущего графика. На только что открытом графике или для символа, по которому MetaTrader еще не загружал данные H4, первые вызовы CopyHigh/CopyLow/CopyTime могут возвращать меньше баров, чем запрошено, пока терминал не загрузит исторические данные H4 от брокера. Init() сначала проверяет результат Bars(), но при первом открытии графика нового символа все равно может потребоваться несколько секунд, прежде чем Update() выполнится успешно.

Движок полностью перестраивает массив свингов H4 при каждом закрытии бара H4. При глубине истории, используемой в этой серии (150-300 баров H4), массив перестраивается быстро, без заметного влияния на производительность. При этом перестройка требуется гораздо реже, чем в движке, анализирующем бары торгового таймфрейма.

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


Заключение

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

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

Весь код был скомпилирован и протестирован в MetaTrader 5. Сохраните файл SwingEngine.mqh по пути MQL5\Include\ChartPatterns\SwingEngine.mqh, а файл SwingDemo.mq5 — по пути MQL5\Indicators\SwingDemo.mq5. Скомпилируйте SwingDemo в MetaEditor. Дополнительные зависимости не требуются. Прикрепите индикатор к графику младшего таймфрейма, например M15 или H1, оставив для параметра InpSwingTF значение H4, и визуально проверьте правильность обнаружения свингов на H4.

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

Прикрепленные файлы |
SwingDemo.mq5 (6.31 KB)
SwingEngine.mqh (17.09 KB)
Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 1): Зачем терминалу собственный 2D-рендерер Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 1): Зачем терминалу собственный 2D-рендерер
В этой статье мы начинаем пошаговую разработку графического 2D-движка для MetaTrader. Мы введем единый стандарт представления цветов ARGB и реализуем поверхность с возможностью повторного использования — буфер пикселей типа uint, который загружается как динамический ресурс и отображается с помощью одного объекта OBJ_BITMAP_LABEL. Вы нарисуете прямоугольники и вертикальный градиент, убедитесь, что прозрачность здесь действительно работает, и освоите эффективный способ обновления с одним вызовом Flush.
Нейросети в трейдинге: Диффузионная генерация торговых планов (Планировщик) Нейросети в трейдинге: Диффузионная генерация торговых планов (Планировщик)
Продолжаем торговую адаптацию DPCC на MQL5/OpenCL: рассматриваем кадровый денойзер CNeuronPlanDenoiserOCL и отдельно — действующий цикл планировщика CNeuronDPCCOCL. Показываем подготовку контекста, предсказание шума, обратную диффузию и возврат исправленных действий в генерацию. Разбираем режимы P0/P1/P2, проверку финансовых ограничений по ценовым сценариям и выбор первого действия по спектральной оценке денежных результатов G. Материал помогает проследить движение планов и условия допуска кандидатов к выбору.
Как обнаруживать и нормализовать объекты графика в MQL5 (часть 1): Создание движка обнаружения объектов графика Как обнаруживать и нормализовать объекты графика в MQL5 (часть 1): Создание движка обнаружения объектов графика
В этой статье рассматривается интерпретационный разрыв между визуальными объектами графика и алгоритмическим исполнением. Вы создадите системный детектор, который проходит по всем объектам графика, определяет аналитические типы и нормализует их геометрические данные (временные и ценовые координаты) в структурированный массив SChartObjectInfo. В реализации используются низкоуровневые функции MQL5, конвейер "фильтрация — извлечение — сохранение" и тестовый советник (EA), управляемый таймером, что в итоге дает переиспользуемый фреймворк для торговых входных данных на основе правил.
Фильтрация новостей с помощью экономического календаря MetaTrader 5 и резервного варианта на основе CSV Фильтрация новостей с помощью экономического календаря MetaTrader 5 и резервного варианта на основе CSV
В статье представлен автономный модуль фильтрации новостей для MetaTrader 5, созданный на основе API экономического календаря платформы. Модуль реализует сопоставление торговых символов с валютами, торговые паузы перед событиями и после них, а также необязательное сокращение объёма позиции в дни с событиями высокой значимости, с резервным вариантом на основе CSV для Тестера стратегий. Демонстрационный советник и панель мониторинга на графике в реальном времени показывают интеграцию и проверку как в реальной торговой среде, так и в условиях бэктеста.