Автоматизация графических паттернов в MQL5 (Часть 1): Мультитаймфреймовый движок для анализа свинговой структуры
Введение
У большинства алгоритмов распознавания графических паттернов есть одно и то же ограничение. Они распознают форму. Но не учитывают контекст. Паттерн "голова и плечи", обнаруженный на вершине подтвержденного восходящего тренда, — сигнал разворота с высокой вероятностью отработки. Та же фигура в середине бокового диапазона — лишь шум. Паттерн "двойная вершина" после трех и более последовательно повышающихся максимумов — предупреждающий структурный сигнал. "Двойная вершина" после одной волны роста, которую едва можно считать трендом, в лучшем случае вызывает сомнения. Каждый опытный технический аналитик знает это различие. Но почти ни одна автоматизированная реализация не делает это обязательной проверкой.
Есть и вторая, связанная с первой проблема — столь же распространенная, но гораздо реже обсуждаемая. Большинство советников, использующих свинги, определяют рыночную структуру на том же таймфрейме, на котором торгуют. В результате скальпинговый советник на M15 определяет "тренд" по свингам M15, которым свойственен высокий уровень шума, и десятки раз в день пересматривает его направление. Трейдер, анализирующий график, никогда бы так не поступил: сначала он посмотрел бы на H4 или дневной график, чтобы определить предпочтительное направление торговли, а затем перешел бы на младший таймфрейм для выбора момента входа. Описанный в статье движок реализует ту же дисциплину в коде: рыночная структура всегда определяется по H4 независимо от таймфрейма графика, на котором запущен советник или индикатор.
В серии статей "Автоматизация графических паттернов в MQL5" мы создадим полноценную систему обнаружения паттернов, в которой каждый паттерн проверяется с учетом рыночного контекста, придающего ему смысл. Паттерн "двойная вершина" принимается только в том случае, когда движок анализа свингов подтверждает предшествующий восходящий тренд. Паттерн "флаг" принимается только в том случае, когда сила предшествующего импульса превышает заданный минимальный порог. Паттерн "голова и плечи" считается допустимым только в том случае, если последовательность свингов соответствует структурным требованиям паттерна распределения. Весь этот контекст определяется на таймфрейме старше торгового.
В этой статье закладывается основа: CSwingEngine — самодостаточный, пригодный для повторного использования класс, который можно подключить к любому советнику или индикатору одной строчкой кода. Движок определяет свинговые точки на H4 с учетом настраиваемых глубины истории и силы свинга, присваивает каждому свингу метку HH, LH, HL или LL по мере его формирования и определяет рыночную структуру по последовательности этих меток. Он также предоставляет понятный публичный интерфейс для получения текущего состояния тренда и массива свингов. При этом объекты корректно отображаются на любом графике младшего таймфрейма, на котором запущена использующая движок программа.
Главный вопрос, на который призван ответить CSwingEngine, звучит так:
Какова рыночная структура на старшем таймфрейме и что последовательность свинговых точек с присвоенными метками говорит о том, где на моем торговом таймфрейме может сформироваться допустимый паттерн?
Прежде чем показать, что делает движок, разберем, почему упрощенное обнаружение свингов на торговом таймфрейме дает сбои и к чему приводит отсутствие проверок рыночного контекста и таймфрейма в алгоритмах распознавания паттернов. Каждое архитектурное решение в CSwingEngine обусловлено конкретным недостатком более простых подходов.
Мы рассмотрим следующие темы:
- Почему обнаружение паттернов без учета контекста не работает
- Что делает свинговую точку значимой
- Как последовательности свингов определяют рыночную структуру
- Архитектура — расчеты на H4, отрисовка на таймфрейме графика
- Реализация на MQL5
- Как этот движок используется в последующих статьях
- Тестирование на исторических данных и проверка корректности
- Известные ограничения
- Заключение
Почему обнаружение паттернов без учета контекста не работает

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

Рис. 2. Метки свингов и рыночная структура
После присвоения меток свинговым максимумам и минимумам на H4 рыночная структура определяется по последней метке максимума и последней метке минимума. Отдельный алгоритм сравнения цен не требуется.
Восходящий тренд подтверждается, когда последний свинговый максимум имеет метку HH, а последний свинговый минимум — HL. Это означает, что на последнем восходящем отрезке цена обновила максимум, а минимум последнего отката остался выше предыдущего минимума. Это классическое определение восходящего тренда по теории Доу, но здесь оно выражено через сочетание меток, а не прямое сравнение цен.
Для нисходящего тренда ситуация зеркальная: последний свинговый максимум имеет метку LH, а последний свинговый минимум — LL.
К боковому рынку относятся все остальные случаи — сочетание HH и LL, сочетание LH и HL или недостаточная история свингов. Смешанное сочетание меток означает, что рынок обновил экстремум в одном направлении, но не в другом, — это и есть признак переходного состояния или бокового диапазона.
Эта классификация определяет работу алгоритмов в последующих статьях, а ее результат всегда получается по данным H4. Когда во второй части серии проверяется паттерн "двойная вершина", например на графике H1, сначала по-прежнему проверяется, возвращает ли CSwingEngine значение TREND_UP. Но этот результат отражает структуру на H4, а не шум на H1. Если движок не возвращает TREND_UP, проверка паттерна немедленно прекращается. Форма потенциального паттерна по-прежнему определяется по движению цены на H1 (или на другом таймфрейме графика, на котором запущен советник). Но ее проверка начинается только тогда, когда рыночный контекст на H4 допускает такой паттерн.
Архитектура — расчеты на H4, отрисовка на таймфрейме графика
Реализация CSwingEngine организована в двух файлах.
- SwingEngine.mqh содержит самодостаточный класс движка. Класс обнаруживает свинговые точки на H4, присваивает им метки, классифицирует рыночную структуру и предоставляет доступ к массиву свингов и состоянию тренда через простой публичный интерфейс. Класс не содержит логики распознавания конкретных графических паттернов, а его таймфрейм анализа не зависит от графика, на котором запущена использующая его программа.
- 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, которую не нужно отдельно реализовывать в движке. О ней стоит сказать прямо: именно благодаря ей архитектура остается простой, без ненадежных дополнительных механизмов.

Рис. 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.

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

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

Рис. 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 1): Зачем терминалу собственный 2D-рендерер
Нейросети в трейдинге: Диффузионная генерация торговых планов (Планировщик)
Как обнаруживать и нормализовать объекты графика в MQL5 (часть 1): Создание движка обнаружения объектов графика
Фильтрация новостей с помощью экономического календаря MetaTrader 5 и резервного варианта на основе CSV
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования