English
preview
Автоматизация графических паттернов в MQL5 (Часть 2): Паттерны "двойная вершина" и "двойное дно"

Автоматизация графических паттернов в MQL5 (Часть 2): Паттерны "двойная вершина" и "двойное дно"

MetaTrader 5 — Трейдинг |
50 0
Tola Moses Hector
Tola Moses Hector

Введение

В первой части данной серии мы создали CSwingEngine — класс с возможностью повторного использования, который определяет значимые свинговые точки на H4, присваивает каждой метку HH, LH, HL или LL и классифицирует рыночную структуру как восходящий тренд, нисходящий тренд или диапазон. Таймфрейм графика, на котором работает использующий этот класс советник или индикатор, на результат не влияет. В этой статье мы применим движок к первой паре паттернов данной серии — "двойной вершине" и "двойному дну". Алгоритм использует движок для подтверждения тренда на H4, а затем проверяет, соответствуют ли последние два подтвержденных свинговых максимума или минимума на H4 структурным требованиям паттерна. Триггером входа служит пробой линии шеи, который определяется на таймфрейме графика советника. Уровень тейк-профита рассчитывается по проекции высоты паттерна.

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

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

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

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

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

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

Основной вопрос этой статьи:

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

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

  1. "Двойная вершина" и "двойное дно" — теория и измерение
  2. Условия допустимости паттерна в этой реализации
  3. Архитектура — два файла, три состояния
  4. Реализация на MQL5
  5. Известные ограничения
  6. Заключение


"Двойная вершина" и "двойное дно" — теория и измерение

Паттерн "двойная вершина" состоит из пяти структурных компонентов.

Double top—structure and measurement

Рис. 1. "Двойная вершина" — структура и измерение

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

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

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

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

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

Целевой уровень рассчитывается проекцией высоты паттерна вниз от линии шеи. Высота — расстояние от линии шеи до уровня вершин. Если вершины находятся на уровне 1,1000, а линия шеи — на уровне 1,0850, высота паттерна составляет 150 пипсов. Целевой уровень — 1,0850 минус 150 пипсов, то есть 1,0700.

Double bottom—structure and measurement

Рис. 2. "Двойное дно" — структура и измерение

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


Условия допустимости паттерна в этой реализации

Перед формированием сигнала алгоритм проверяет выполнение шести условий. Все шесть условий должны быть выполнены одновременно.

  1. Условие тренда. CSwingEngine должен возвращать TREND_UP для "двойной вершины" или TREND_DOWN для "двойного дна". При этом используется структура на H4 независимо от таймфрейма графика советника. Если движок возвращает TREND_RANGE, дальнейшая проверка паттерна не выполняется.
  2. Условие количества свинговых точек. В массиве движка должно быть не менее четырех подтвержденных свинговых точек на H4 — как минимум два максимума и два минимума. Если свингов меньше, истории рыночной структуры недостаточно для проверки.
  3. Условие формирования пары свингов. Алгоритм должен быть способен найти два свинга нужного типа (два максимума для "двойной вершины» или два минимума для "двойного дна"), между которыми находится ровно один свинг противоположного типа. Для этой проверки используется отдельный булев флаг, а не сравнение поля цены с нулем. Свинговая точка с ценой 0.0 нереалистична для любого валютного символа, но использовать еще не заданную цену как признак "пока не найдено" все равно неправильно. Поэтому в приведенном ниже алгоритме поиска отдельный флаг показывает, найдена ли промежуточная свинговая точка.
  4. Условие равенства точек. Разница их цен не должна превышать произведение InpPeakTolerance и ATR на H4. Допуск по умолчанию составляет 1,5 ATR. Если разница цен превышает этот допуск, это значит, что точки тестируют разные уровни и не образуют допустимый паттерн.
  5. Условие высоты паттерна. Линия шеи должна находиться ниже обеих точек ("двойная вершина") или выше обеих точек ("двойное дно») как минимум на величину InpMinPatternHeight × ATR на H4. Это обеспечивает достаточную высоту паттерна и исключает почти плоские структуры.
  6. Условие временного порядка и ширины. Сначала должна сформироваться первая точка, затем свинг, задающий линию шеи, и только после него — вторая точка. Расстояние между первой и второй точками должно составлять от 1 до InpMaxPatternBars баров H4 и определяться по их временным меткам, а не по индексам массива. Расчет по временным меткам, а не по разности внутренних индексов освобождает эту проверку от предположений о порядке элементов в массиве движка. Она использует только поле time, предусмотренное публичным интерфейсом движка.

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

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


Архитектура — два файла, три состояния

  1. DoubleTopEA.mq5 — советник. Он отвечает за совершение сделок, управление позициями, размещение стопов, расчет размера позиции в лотах и отображение меток на графике. Он подключает файл SwingEngine.mqh из первой части и использует движок для получения текущего состояния тренда на H4 и массива свингов. Для наглядности логика проверки паттернов реализована непосредственно в советнике. В последующих статьях, по мере развития серии, она будет вынесена в отдельный подключаемый файл.
  2. Файл SwingEngine.mqh не изменился по сравнению с первой частью. Для обнаружения "двойной вершины" ничего менять в нем не требуется. Его публичный интерфейс предоставляет все необходимое советнику, в том числе две вспомогательные инфраструктурные функции GetPipValue и PipSize, используемые во всех статьях серии. В версии этой статьи из первой части серии советник содержал локальную копию PipSize(); здесь это дублирование устранено. Теперь советник DoubleTopEA.mq5 напрямую вызывает функции, объявленные в SwingEngine.mqh, поскольку этот файл и так подключается для использования движка. Это избавляет от необходимости отдельно синхронизировать еще одну функцию между файлами.

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

В советнике явно заданы три состояния.

  1. DT_STATE_SCANNING — состояние по умолчанию. После закрытия каждого бара графика выполняются обе функции: CheckDoubleTop() и CheckDoubleBottom(). Если одна из них возвращает допустимый паттерн, по которому еще не совершалась сделка с этой второй точкой, паттерн фиксируется в g_active_pattern, а советник переходит в состояние DT_STATE_PATTERN_FORMED. Паттерн идентифицируется по паре признаков: временной метке второй точки и типу паттерна. По этой паре проверяется, совершалась ли по нему сделка, поэтому один и тот же паттерн не может привести к повторному входу, даже если предыдущая позиция уже закрыта.
  2. DT_STATE_PATTERN_FORMED — в этом состоянии советник хранит один зафиксированный паттерн и выполняет три проверки на каждом баре. Советник повторно проверяет паттерн по текущим данным свингов и отменяет его фиксацию, если проверка не пройдена. Если за InpMaxBreakDelayBars баров линия шеи не пробита, срок действия паттерна истекает. Наконец, советник проверяет пробой линии шеи и пытается открыть позицию.
  3. DT_STATE_IN_TRADE — советник переходит в это состояние только после фактического открытия позиции. Советник возвращается в состояние DT_STATE_SCANNING только тогда, когда HasOpenPosition() не находит соответствующей позиции. Эта функция напрямую проверяет список открытых позиций терминала по символу и магическому числу. Отдельного булева флага "Есть ли открытая позиция?" нет: наличие позиции определяется исключительно по списку позиций терминала. Поэтому после перезапуска терминала, ручного закрытия позиции или повторного прикрепления советника к графику с уже открытой позицией корректное состояние устанавливается уже на первом тике. Для выбора начального состояния сам обработчик событий OnInit() вызывает функцию HasOpenPosition().


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

Советник создается по частям.

Подключаемые файлы, перечисления и входные параметры
//+------------------------------------------------------------------+
//|                                                  DoubleTopEA.mq5 |
//|                                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 description "Automating Chart Patterns in MQL5 Part 2"
#property description "Double Top and Double Bottom with H4 context, LTF entry timing"
#property description "Requires SwingEngine.mqh from Part 1"
#property description "Swing structure reads InpSwingTF (default H4) internally"

#include <Trade\Trade.mqh>
#include <ChartPatterns\SwingEngine.mqh>  // Swing engine + shared infrastructure helpers from Part 1

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input group "=== Swing Engine ==="
input int              InpSwingStrength   = 3;           // Swing strength — H4 bars required on each side
input int              InpSwingLookback   = 200;         // Maximum H4 bars for swing detection
input ENUM_TIMEFRAMES  InpSwingTF         = PERIOD_H4;   // Timeframe swing structure is calculated on

input group "=== Pattern Settings ==="
input double InpPeakTolerance      = 1.5;                // Peak/trough equality tolerance, in swing-TF ATR
input double InpMinPatternHeight   = 1.0;                // Minimum pattern height, in swing-TF ATR
input int    InpMaxPatternBars     = 80;                 // Maximum swing-TF bars between first and second point
input int    InpMaxBreakDelayBars  = 40;                 // Maximum CHART bars to wait for the neckline break
input int    InpATRPeriod          = 14;                 // ATR period, computed on InpSwingTF

input group "=== Entry and Risk ==="
input double InpRiskPercent      = 1.0;                  // Risk per trade as percent of balance
input double InpSLBufferATR      = 0.5;                  // Stop loss buffer beyond the pattern extreme, in ATR
input bool   InpUseMeasuredMove  = true;                 // Use measured move target (false = 2R fallback)
input double InpMaxSlipATR       = 0.5;                  // Skip entry if price is already this many ATR past the neckline

input group "=== General ==="
input int    InpMagicNumber      = 666001;               // Magic number
input int    InpSlippage         = 10;                   // Slippage in points
input bool   InpShowLabels       = true;                 // Draw pattern labels on chart
input bool   InpDebugLog         = false;                // Log the reason every rejected pattern check fails

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

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

Результат обнаружения и состояние паттерна

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

//+------------------------------------------------------------------+
//| Holds the result of a Double Top or Double Bottom check          |
//+------------------------------------------------------------------+
struct SPatternResult
  {
   bool              valid;       // true if a valid pattern was found
   bool              is_top;      // true = Double Top (short setup), false = Double Bottom (long setup)
   double            point1;      // Price of the first (older) point — a high for DT, a low for DB
   double            point2;      // Price of the second (newer) point
   double            neckline;    // Neckline price
   double            target;      // Measured move target price
   double            stop;        // Stop loss price
   datetime          point1_time; // Time of the first point
   datetime          point2_time; // Time of the second point — also this pattern's unique identifier
   datetime          neck_time;   // Time of the neckline swing
  };

//+------------------------------------------------------------------+
//| Pattern lifecycle — replaces a single "in trade" boolean         |
//+------------------------------------------------------------------+
enum ENUM_DT_STATE
  {
   DT_STATE_SCANNING,        // No locked pattern — evaluating every bar
   DT_STATE_PATTERN_FORMED,  // Pattern locked — watching for neckline break or expiry
   DT_STATE_IN_TRADE         // Position open from this pattern
  };

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

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

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CSwingEngine   g_engine;                                  // Swing engine from Part 1 — reads InpSwingTF internally
CTrade         g_trade;                                   // Trade execution object
int            g_atr_handle = INVALID_HANDLE;             // ATR handle on InpSwingTF — matches the peaks it measures
datetime       g_last_bar   = 0;                          // Last processed CHART bar time
string         g_obj_prefix = "";                         // Unique per symbol + magic number, set in OnInit()

ENUM_DT_STATE  g_state              = DT_STATE_SCANNING;  // Current pattern lifecycle state
SPatternResult g_active_pattern;                          // The one pattern currently locked in
int            g_pattern_age_bars   = 0;                  // Chart bars since the pattern was locked in

datetime       g_last_traded_dt_point2 = 0;               // Second-point time of the last DOUBLE TOP actually traded
datetime       g_last_traded_db_point2 = 0;               // Second-point time of the last DOUBLE BOTTOM actually traded

Значение g_obj_prefix формируется в обработчике событий OnInit() на основе символа и магического числа. Благодаря этому два экземпляра советника, а также советник и сторонний инструмент, который тоже использует префикс "DT_", не могут создавать графические объекты с совпадающими именами или удалять построения друг друга.

Вспомогательные функции
//+------------------------------------------------------------------+
//| Prints a message only when debug logging is enabled              |
//+------------------------------------------------------------------+
void DebugLog(string msg)
  {
   if(InpDebugLog)
      Print("DoubleTopEA[debug]: ", msg);
  }

//+------------------------------------------------------------------+
//| True if a position matching this symbol and magic number exists  |
//| The single source of truth for "are we in a trade" — no separate |
//| boolean flag is maintained anywhere else in the EA.              |
//+------------------------------------------------------------------+
bool HasOpenPosition(const string symbol, const long magic)
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(!PositionSelectByTicket(ticket))
         continue;
      if(PositionGetString(POSITION_SYMBOL)  != symbol)
         continue;
      if(PositionGetInteger(POSITION_MAGIC)  != magic)
         continue;
      return true;
     }
   return false;
  }

//+------------------------------------------------------------------+
//| Computes lot size from risk percent and stop distance in pips    |
//| If the risk-appropriate size rounds below the broker minimum,    |
//| the trade is skipped entirely rather than forced up to the       |
//| minimum lot — forcing it up would silently exceed InpRiskPercent.|
//+------------------------------------------------------------------+
double CalcLots(double sl_pips)
  {
   double balance   = AccountInfoDouble(ACCOUNT_BALANCE);
   double risk_amt  = balance * InpRiskPercent / 100.0;
   double pip_value = GetPipValue(_Symbol, 1.0);           // Money value of 1 pip at 1.0 lot
   if(pip_value <= 0 || sl_pips <= 0)
      return 0;
   double step    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   double min_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double max_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   if(step <= 0)
      return 0;
   double raw_lots = risk_amt / (sl_pips * pip_value);
//--- Round DOWN to the nearest volume step. A small epsilon guards
//--- against a step division landing just under a whole number due
//--- to ordinary floating-point rounding.
   double steps = MathFloor(raw_lots / step + 0.0000001);
   double lots  = NormalizeDouble(steps * step, 8);
   if(lots < min_lot)
      return 0;                                            // Skip rather than over-risk
   return MathMin(max_lot, lots);
  }

//+------------------------------------------------------------------+
//| True if free margin covers the position this order would open    |
//+------------------------------------------------------------------+
bool HasSufficientMargin(ENUM_ORDER_TYPE order_type, double lots, double price)
  {
   double margin_required = 0;
   if(!OrderCalcMargin(order_type, _Symbol, lots, price, margin_required))
      return false;
   return (margin_required <= AccountInfoDouble(ACCOUNT_MARGIN_FREE));
  }

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

Существующие объекты обновляются без удаления и повторного создания на каждом баре. Функция ChartRedraw() вызывается один раз в конце выполнения обработчика событий OnTick(), а не отдельно для каждого объекта.

//+------------------------------------------------------------------+
//| Creates or moves a horizontal line at the specified price level  |
//+------------------------------------------------------------------+
void DrawHLine(string name, double price, color clr, ENUM_LINE_STYLE style)
  {
   if(!InpShowLabels)
      return;
   string obj = g_obj_prefix + name;
   if(ObjectFind(0, obj) < 0)
     {
      if(!ObjectCreate(0, obj, OBJ_HLINE, 0, 0, price))
        { DebugLog("ObjectCreate failed for " + obj); return; }
     }
   ObjectSetDouble(0,  obj, OBJPROP_PRICE, price);
   ObjectSetInteger(0, obj, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, obj, OBJPROP_STYLE, style);
   ObjectSetInteger(0, obj, OBJPROP_WIDTH, 1);
  }

//+------------------------------------------------------------------+
//| Creates or moves a text label at the specified time and price    |
//+------------------------------------------------------------------+
void DrawLabel(string name, datetime time, double price, string text, color clr)
  {
   if(!InpShowLabels)
      return;
   string obj = g_obj_prefix + name;
   if(ObjectFind(0, obj) < 0)
     {
      if(!ObjectCreate(0, obj, OBJ_TEXT, 0, time, price))
        { DebugLog("ObjectCreate failed for " + obj); return; }
     }
   ObjectMove(0, obj, 0, time, price);
   ObjectSetString(0,  obj, OBJPROP_TEXT, text);
   ObjectSetInteger(0, obj, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, obj, OBJPROP_FONTSIZE, 9);
  }

//+------------------------------------------------------------------+
//| Deletes a single named object, if it exists                      |
//+------------------------------------------------------------------+
void DeleteObject(string name)
  {
   string obj = g_obj_prefix + name;
   if(ObjectFind(0, obj) >= 0)
      ObjectDelete(0, obj);
  }

//+------------------------------------------------------------------+
//| Removes every chart object created by this EA instance           |
//+------------------------------------------------------------------+
void ClearLabels()
  {
   int total = ObjectsTotal(0);
   for(int i = total - 1; i >= 0; i--)
     {
      string name = ObjectName(0, i);
      if(StringFind(name, g_obj_prefix) == 0)
         ObjectDelete(0, name);
     }
  }
Поиск свингов без сигнального значения
//+------------------------------------------------------------------+
//| Finds the two most recent swings of one type with a single swing |
//| of the opposite type between them. Returns false, rather than    |
//| relying on a price of 0.0 as a sentinel, when no such triple     |
//| exists in the current swing array.                               |
//+------------------------------------------------------------------+
bool FindTwoSwingsWithMidpoint(bool want_high, SSwingPoint &point1, SSwingPoint &point2, SSwingPoint &mid)
  {
   int  swing_count = g_engine.GetSwingCount();                     // Cached once — GetSwingCount() is not assumed to be free
   int  found = 0;
   bool mid_found = false;
   for(int i = 0; i < swing_count; i++)
     {
      SSwingPoint sp = g_engine.GetSwing(i);                        // Index 0 = most recent, per the engine's contract
      if(sp.is_high == want_high && found == 0)                     // Newest matching swing
        { point2 = sp; found++; }
      else
         if(sp.is_high != want_high && found == 1 && !mid_found)    // Opposite-type swing between the two
           { mid = sp; mid_found = true; }
         else
            if(sp.is_high == want_high && found == 1 && mid_found)  // Older matching swing
              { point1 = sp; found++; break; }
     }
   return (found >= 2 && mid_found);
  }

Обнаружение двойной вершины

//+------------------------------------------------------------------+
//| Evaluates the current H4 swing structure for a Double Top        |
//| atr must be computed on InpSwingTF, not the chart timeframe      |
//+------------------------------------------------------------------+
SPatternResult CheckDoubleTop(double atr)
  {
   SPatternResult result;
   result.valid  = false;
   result.is_top = true;

   if(g_engine.GetTrend() != TREND_UP)
     { DebugLog("DT reject: H4 trend is not TREND_UP."); return result; }
   if(g_engine.GetSwingCount() < 4)
     { DebugLog("DT reject: fewer than 4 confirmed H4 swings."); return result; }

   SSwingPoint point1 = {true, 0, 0, 0, ""};
   SSwingPoint point2 = {true, 0, 0, 0, ""};
   SSwingPoint mid    = {false, 0, 0, 0, ""};
   if(!FindTwoSwingsWithMidpoint(true, point1, point2, mid))
     { DebugLog("DT reject: could not find two highs with a low between them."); return result; }

   if(point1.time >= mid.time || mid.time >= point2.time)
     { DebugLog("DT reject: swing time ordering is invalid."); return result; }

//--- Width, measured from timestamps rather than an internal array index
   int bars_between = (int)MathRound((double)(point2.time - point1.time) / PeriodSeconds(InpSwingTF));
   if(bars_between <= 0 || bars_between > InpMaxPatternBars)
     { DebugLog(StringFormat("DT reject: bars_between=%d outside (0, %d].", bars_between, InpMaxPatternBars)); return result; }

   double point_diff = MathAbs(point1.price - point2.price);
   if(point_diff > atr * InpPeakTolerance)
     { DebugLog(StringFormat("DT reject: point difference %.5f exceeds tolerance.", point_diff)); return result; }

   double neckline     = mid.price;
   double pattern_high = MathMax(point1.price, point2.price);
   double height        = pattern_high - neckline;
   if(height < atr * InpMinPatternHeight)
     { DebugLog(StringFormat("DT reject: height %.5f below minimum.", height)); return result; }

   result.valid       = true;
   result.point1      = point1.price;
   result.point2      = point2.price;
   result.neckline    = neckline;
   result.target       = neckline - height;                            // Measured move below neckline
   result.stop         = pattern_high + atr * InpSLBufferATR;          // Above the highest point
   result.point1_time  = point1.time;
   result.point2_time  = point2.time;
   result.neck_time    = mid.time;
   Print(StringFormat(
            "DoubleTopEA: DT found | P1:%.5f | P2:%.5f | Neck:%.5f | Target:%.5f | Height:%.5f",
            result.point1, result.point2, result.neckline, result.target, height));
   return result;
  }
Обнаружение двойного дна
//+------------------------------------------------------------------+
//| Evaluates the current H4 swing structure for a Double Bottom     |
//+------------------------------------------------------------------+
SPatternResult CheckDoubleBottom(double atr)
  {
   SPatternResult result;
   result.valid  = false;
   result.is_top = false;

   if(g_engine.GetTrend() != TREND_DOWN)
     { DebugLog("DB reject: H4 trend is not TREND_DOWN."); return result; }
   if(g_engine.GetSwingCount() < 4)
     { DebugLog("DB reject: fewer than 4 confirmed H4 swings."); return result; }

   SSwingPoint point1 = {false, 0, 0, 0, ""};
   SSwingPoint point2 = {false, 0, 0, 0, ""};
   SSwingPoint mid    = {true,  0, 0, 0, ""};
   if(!FindTwoSwingsWithMidpoint(false, point1, point2, mid))
     { DebugLog("DB reject: could not find two lows with a high between them."); return result; }

   if(point1.time >= mid.time || mid.time >= point2.time)
     { DebugLog("DB reject: swing time ordering is invalid."); return result; }

   int bars_between = (int)MathRound((double)(point2.time - point1.time) / PeriodSeconds(InpSwingTF));
   if(bars_between <= 0 || bars_between > InpMaxPatternBars)
     { DebugLog(StringFormat("DB reject: bars_between=%d outside (0, %d].", bars_between, InpMaxPatternBars)); return result; }

   double point_diff = MathAbs(point1.price - point2.price);
   if(point_diff > atr * InpPeakTolerance)
     { DebugLog(StringFormat("DB reject: point difference %.5f exceeds tolerance.", point_diff)); return result; }

   double neckline    = mid.price;
   double pattern_low = MathMin(point1.price, point2.price);
   double height        = neckline - pattern_low;
   if(height < atr * InpMinPatternHeight)
     { DebugLog(StringFormat("DB reject: height %.5f below minimum.", height)); return result; }

   result.valid       = true;
   result.point1      = point1.price;
   result.point2      = point2.price;
   result.neckline    = neckline;
   result.target       = neckline + height;                            // Measured move above neckline
   result.stop         = pattern_low - atr * InpSLBufferATR;           // Below the lowest point
   result.point1_time  = point1.time;
   result.point2_time  = point2.time;
   result.neck_time    = mid.time;
   Print(StringFormat(
            "DoubleTopEA: DB found | P1:%.5f | P2:%.5f | Neck:%.5f | Target:%.5f | Height:%.5f",
            result.point1, result.point2, result.neckline, result.target, height));
   return result;
  }

Открытие позиции

Обе функции открытия позиции повторно вызывают HasOpenPosition() непосредственно перед отправкой ордера, проверяют достаточность маржи, нормализуют цены и учитывают установленные брокером минимальную дистанцию до стопов и дистанцию заморозки. Они также отклоняют вход, если цена ушла за линию шеи настолько далеко, что исполнение ордера по цене, соответствующей структуре паттерна, уже невозможно.
//+------------------------------------------------------------------+
//| Opens a short trade on a confirmed Double Top neckline break     |
//+------------------------------------------------------------------+
void TryEntryShort(const SPatternResult &pat, double atr)
  {
   if(HasOpenPosition(_Symbol, InpMagicNumber))
      return;                                                          // Belt-and-braces position check

   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

//--- Do not chase a neckline that has already been broken by a wide margin
   if(bid < pat.neckline - atr * InpMaxSlipATR)
     { DebugLog("DT entry skipped — price already too far past the neckline."); return; }

   double sl = NormalizeDouble(pat.stop, _Digits);
   double tp = NormalizeDouble(InpUseMeasuredMove ? pat.target : bid - (sl - bid) * 2.0, _Digits);

   double sl_pip = (sl - bid) / PipSize(_Symbol);
   double lots   = CalcLots(sl_pip);
   if(lots <= 0)
     { DebugLog("DT entry skipped — risk-appropriate lot size is below the broker minimum."); return; }

   long   stops_level  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   long   freeze_level = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
   double min_dist     = MathMax(stops_level, freeze_level) * _Point;
   if(sl - bid < min_dist)
      sl = NormalizeDouble(bid + min_dist + _Point, _Digits);
   if(bid - tp < min_dist)
      tp = NormalizeDouble(bid - min_dist - _Point, _Digits);

   if(!HasSufficientMargin(ORDER_TYPE_SELL, lots, bid))
     { DebugLog("DT entry skipped — insufficient free margin."); return; }

//--- Price 0.0 lets CTrade fill at the current market price rather than
//--- the bid snapshot taken a few lines above, avoiding an avoidable
//--- rejection if the price has ticked in the meantime.
   if(g_trade.Sell(lots, _Symbol, 0.0, sl, tp, "Double Top"))
     {
      DrawHLine("NECKLINE", pat.neckline, clrOrange,  STYLE_DASH);
      DrawHLine("TARGET",   tp,           clrGold,    STYLE_DOT);
      DrawHLine("STOP",     sl,           clrCrimson, STYLE_DOT);
      datetime t = iTime(_Symbol, PERIOD_CURRENT, 1);
      DrawLabel("ENTRY", t, iHigh(_Symbol, PERIOD_CURRENT, 1) + PipSize(_Symbol) * 5, "DT", clrCrimson);
      Print(StringFormat("DoubleTopEA: SHORT | Lots:%.2f | Bid:%.5f | SL:%.5f | TP:%.5f", lots, bid, sl, tp));
      g_last_traded_dt_point2 = pat.point2_time;                       // This exact pattern cannot re-trade
      g_state                 = DT_STATE_IN_TRADE;
     }
   else
      Print("DoubleTopEA: Sell failed | Retcode:", g_trade.ResultRetcode());
  }

//+------------------------------------------------------------------+
//| Opens a long trade on a confirmed Double Bottom neckline break   |
//+------------------------------------------------------------------+
void TryEntryLong(const SPatternResult &pat, double atr)
  {
   if(HasOpenPosition(_Symbol, InpMagicNumber))
      return;

   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

   if(ask > pat.neckline + atr * InpMaxSlipATR)
     { DebugLog("DB entry skipped — price already too far past the neckline."); return; }

   double sl = NormalizeDouble(pat.stop, _Digits);
   double tp = NormalizeDouble(InpUseMeasuredMove ? pat.target : ask + (ask - sl) * 2.0, _Digits);

   double sl_pip = (ask - sl) / PipSize(_Symbol);
   double lots   = CalcLots(sl_pip);
   if(lots <= 0)
     { DebugLog("DB entry skipped — risk-appropriate lot size is below the broker minimum."); return; }

   long   stops_level  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   long   freeze_level = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
   double min_dist     = MathMax(stops_level, freeze_level) * _Point;
   if(ask - sl < min_dist)
      sl = NormalizeDouble(ask - min_dist - _Point, _Digits);
   if(tp - ask < min_dist)
      tp = NormalizeDouble(ask + min_dist + _Point, _Digits);

   if(!HasSufficientMargin(ORDER_TYPE_BUY, lots, ask))
     { DebugLog("DB entry skipped — insufficient free margin."); return; }

   if(g_trade.Buy(lots, _Symbol, 0.0, sl, tp, "Double Bottom"))
     {
      DrawHLine("NECKLINE", pat.neckline, clrOrange,      STYLE_DASH);
      DrawHLine("TARGET",   tp,           clrGold,        STYLE_DOT);
      DrawHLine("STOP",     sl,           clrDodgerBlue,  STYLE_DOT);
      datetime t = iTime(_Symbol, PERIOD_CURRENT, 1);
      DrawLabel("ENTRY", t, iLow(_Symbol, PERIOD_CURRENT, 1) - PipSize(_Symbol) * 5, "DB", clrDodgerBlue);
      Print(StringFormat("DoubleTopEA: LONG | Lots:%.2f | Ask:%.5f | SL:%.5f | TP:%.5f", lots, ask, sl, tp));
      g_last_traded_db_point2 = pat.point2_time;
      g_state                 = DT_STATE_IN_TRADE;
     }
   else
      Print("DoubleTopEA: Buy failed | Retcode:", g_trade.ResultRetcode());
  }
OnInit и OnDeinit
Входные параметры проверяются в самом начале. Префикс имен объектов формируется на основе символа и магического числа. Начальное состояние советника определяется проверкой наличия открытой позиции, а не предположением, что позиции нет. Это важно при повторном прикреплении советника к графику, если до перезапуска уже была открыта позиция и она остается открытой.
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   if(InpSwingStrength <= 0 || InpSwingLookback <= 0 || InpATRPeriod <= 0 ||
      InpPeakTolerance <= 0 || InpMinPatternHeight <= 0 || InpRiskPercent <= 0 ||
      InpSLBufferATR < 0    || InpMaxPatternBars <= 0   || InpMaxBreakDelayBars <= 0 ||
      InpMaxSlipATR < 0)
     {
      Print("DoubleTopEA: one or more input parameters is out of range.");
      return INIT_PARAMETERS_INCORRECT;
     }

   g_obj_prefix = StringFormat("DT_%s_%d_", _Symbol, InpMagicNumber);

   if(!g_engine.Init(InpSwingStrength, InpSwingLookback, InpSwingTF))
     {
      Print("DoubleTopEA: swing engine initialization failed.");
      return INIT_FAILED;
     }
   g_atr_handle = iATR(_Symbol, InpSwingTF, InpATRPeriod);
   if(g_atr_handle == INVALID_HANDLE)
     {
      Print("DoubleTopEA: ATR handle creation failed.");
      return INIT_FAILED;
     }
   g_trade.SetExpertMagicNumber(InpMagicNumber);
   g_trade.SetDeviationInPoints(InpSlippage);

//--- Decide the starting state from the actual position list, not an assumption
   g_state = HasOpenPosition(_Symbol, InpMagicNumber) ? DT_STATE_IN_TRADE : DT_STATE_SCANNING;
   g_pattern_age_bars      = 0;
   g_last_bar              = 0;
   g_last_traded_dt_point2 = 0;
   g_last_traded_db_point2 = 0;

   Print(StringFormat(
            "DoubleTopEA initialized | Symbol:%s | ChartTF:%s | SwingTF:%s | Magic:%d | StartState:%s",
            _Symbol, EnumToString(Period()), EnumToString(InpSwingTF), InpMagicNumber,
            (g_state == DT_STATE_IN_TRADE ? "IN_TRADE (recovered)" : "SCANNING")));
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(g_atr_handle != INVALID_HANDLE)
      IndicatorRelease(g_atr_handle);
   ClearLabels();
  }

OnTick — машина состояний паттерна

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   datetime current_bar = iTime(_Symbol, PERIOD_CURRENT, 0);
   if(current_bar == g_last_bar)
      return;                                                            // One run per chart bar
   g_last_bar = current_bar;

   if(Bars(_Symbol, PERIOD_CURRENT) < 5)
      return;                                                            // Chart history not ready yet

   g_engine.Update();                                                    // Self-gated on a new swing-TF bar

   double atr_buf[];
   ArraySetAsSeries(atr_buf, true);
   if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) < 1)
      return;
   double atr = atr_buf[0];
   if(atr <= 0 || atr == EMPTY_VALUE)
     { DebugLog("ATR invalid on this bar — skipping."); return; }

   double close1 = iClose(_Symbol, PERIOD_CURRENT, 1);                   // Last CHART bar close

   switch(g_state)
     {
      case DT_STATE_IN_TRADE:
        {
         if(!HasOpenPosition(_Symbol, InpMagicNumber))
           {
            Print("DoubleTopEA: position closed — resuming scan.");
            g_state = DT_STATE_SCANNING;
            ClearLabels();
           }
         break;
        }

      case DT_STATE_PATTERN_FORMED:
        {
         g_pattern_age_bars++;

         //--- Revalidate: does this exact pattern still hold given the latest swing data?
         SPatternResult recheck = g_active_pattern.is_top ? CheckDoubleTop(atr) : CheckDoubleBottom(atr);
         bool same_pattern = recheck.valid &&
                             recheck.is_top      == g_active_pattern.is_top &&
                             recheck.point2_time == g_active_pattern.point2_time;
         if(!same_pattern)
           {
            DebugLog("Locked pattern no longer valid against current swing data — discarding.");
            DeleteObject("NECKLINE_WATCH");
            g_state = DT_STATE_SCANNING;
            break;
           }
         if(g_pattern_age_bars > InpMaxBreakDelayBars)
           {
            DebugLog("Locked pattern expired before the neckline broke — discarding.");
            DeleteObject("NECKLINE_WATCH");
            g_state = DT_STATE_SCANNING;
            break;
           }

         DrawHLine("NECKLINE_WATCH", g_active_pattern.neckline, clrOrange, STYLE_DASH);

         bool broken = g_active_pattern.is_top ? (close1 < g_active_pattern.neckline)
                       : (close1 > g_active_pattern.neckline);
         if(broken)
           {
            Print(StringFormat("DoubleTopEA: %s neckline broken at %.5f",
                               g_active_pattern.is_top ? "DT" : "DB", close1));
            DeleteObject("NECKLINE_WATCH");
            if(g_active_pattern.is_top)
               TryEntryShort(g_active_pattern, atr);
            else
               TryEntryLong(g_active_pattern, atr);
           }
         break;
        }

      case DT_STATE_SCANNING:
        {
         if(HasOpenPosition(_Symbol, InpMagicNumber))                    // Safety net — should be rare
           { g_state = DT_STATE_IN_TRADE; break; }

         SPatternResult dt = CheckDoubleTop(atr);
         if(dt.valid && dt.point2_time != g_last_traded_dt_point2)
           {
            g_active_pattern   = dt;
            g_pattern_age_bars = 0;
            g_state            = DT_STATE_PATTERN_FORMED;
            break;                                                       // Double Top takes priority when both qualify
           }

         SPatternResult db = CheckDoubleBottom(atr);
         if(db.valid && db.point2_time != g_last_traded_db_point2)
           {
            g_active_pattern   = db;
            g_pattern_age_bars = 0;
            g_state            = DT_STATE_PATTERN_FORMED;
           }
         break;
        }
     }

   ChartRedraw(0);                                                       // One redraw per bar, not per object
  }
//+------------------------------------------------------------------+

В состоянии DT_STATE_SCANNING советник фиксирует только один паттерн и сразу переходит в состояние DT_STATE_PATTERN_FORMED. Поэтому на графике не могут одновременно отображаться линии ожидания пробоя для "двойной вершины" и "двойного дна": активен только один паттерн и отображается только одна линия NECKLINE_WATCH.

Что дает движок свингов

Чтобы понять, что дает движок алгоритму обнаружения, рассмотрим случай, когда функция GetTrend() возвращает TREND_RANGE. Функция CheckDoubleTop() сразу завершает работу на первой строчке. Проверка шести структурных условий не выполняется. Массив свингов не просматривается. Метки не отрисовываются.

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

Чего ожидать

Паттерны "двойная вершина" и "двойное дно" на H4 встречаются нечасто. По EURUSD следует ожидать от пяти до 15 сигналов в год, соответствующих заданным условиям. Так и должно быть: фильтр из шести условий специально настроен на строгий отбор. В журнал записываются результаты каждой проверки паттерна. Когда движок возвращает TREND_UP, фиксируются две найденные вершины, уровень линии шеи и результаты проверок равенства точек и высоты паттерна. Эта диагностическая информация позволяет убедиться, что алгоритм находит полноценные паттерны, а не структуры на грани допустимости.

Если в журнале часто появляются записи "DT found", сразу после которых открываются позиции, хотя на графике не видно паттерна, уменьшите InpPeakTolerance до 1,0 или InpMaxPatternBars до 50. Если алгоритм вообще не находит паттернов, увеличьте InpSwingLookback до 400 или уменьшите InpSwingStrength до 2.


DoubleTopEA demonstration

Рис. 3. Наглядная демонстрация работы системы


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

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

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

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

Значение InpMaxPatternBars и ширина, проверяемая в CheckDoubleTop()/CheckDoubleBottom(), измеряются в барах H4 (или другого таймфрейма, заданного в InpSwingTF), а не в барах графика. Это связано с тем, что ширина определяется по временным меткам свингов на таймфрейме свингов. Параметр InpMaxBreakDelayBars, напротив, задает срок ожидания в барах графика — того таймфрейма, на котором советник отслеживает пробой. Если при настройке советника перепутать эти две единицы измерения, ограничение окажется либо гораздо более мягким, либо гораздо более жестким, чем предполагалось.

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

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


Заключение

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

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

Весь код был скомпилирован и протестирован в MetaTrader 5. Сохраните файл SwingEngine.mqh из первой части по пути MQL5\Include\ChartPatterns\SwingEngine.mqh. Сохраните файл DoubleTopEA.mq5 по пути MQL5\Experts\DoubleTopEA.mq5. Скомпилируйте советник в MetaEditor; дополнительные зависимости не требуются. Рекомендуется для EURUSD и GBPUSD на H4. Перед запуском на реальном счете всегда проводите тестирование на демо-счете.

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

Прикрепленные файлы |
SwingEngine.mqh (17.09 KB)
DoubleTopEA.mq5 (28.43 KB)
Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 2): Точки, контуры и путь Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 2): Точки, контуры и путь
Во второй части мы добавим слой геометрии в графическую библиотеку для MetaTrader 5, создаваемую по образцу Cairo. В ней определяется путь из точек с координатами двойной точности, сгруппированных в контуры, сохраняется признак того, является ли контур открытым или замкнутым, а вершины хранятся в плоском массиве с индексами начала контуров. Мы реализуем методы MoveTo, LineTo и Close, добавим базовые вспомогательные методы для построения фигур и демонстрационный пример, который отображает построенную геометрию для визуальной проверки и повторного использования.
Как обнаруживать и нормализовать объекты графика в MQL5 (часть 1): Создание движка обнаружения объектов графика Как обнаруживать и нормализовать объекты графика в MQL5 (часть 1): Создание движка обнаружения объектов графика
В этой статье рассматривается интерпретационный разрыв между визуальными объектами графика и алгоритмическим исполнением. Вы создадите системный детектор, который проходит по всем объектам графика, определяет аналитические типы и нормализует их геометрические данные (временные и ценовые координаты) в структурированный массив SChartObjectInfo. В реализации используются низкоуровневые функции MQL5, конвейер "фильтрация — извлечение — сохранение" и тестовый советник (EA), управляемый таймером, что в итоге дает переиспользуемый фреймворк для торговых входных данных на основе правил.
Алгоритм опыления цветов — Flower Pollination Algorithm (FPA) Алгоритм опыления цветов — Flower Pollination Algorithm (FPA)
Рассматривается реализация Flower Pollination Algorithm (FPA) в MQL5 с разбором операторов глобального и локального опыления и влияния ключевых параметров популяции и шага Леви, который при авторских настройках ищет почти наугад: находим причину и поднимаем результат с 26% до 49%. Трейдер получает готовый класс FPA с рабочими настройками для оптимизации советников и понимание, для каких задач он подходит.
Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 1): Зачем терминалу собственный 2D-рендерер Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 1): Зачем терминалу собственный 2D-рендерер
В этой статье мы начинаем пошаговую разработку графического 2D-движка для MetaTrader. Мы введем единый стандарт представления цветов ARGB и реализуем поверхность с возможностью повторного использования — буфер пикселей типа uint, который загружается как динамический ресурс и отображается с помощью одного объекта OBJ_BITMAP_LABEL. Вы нарисуете прямоугольники и вертикальный градиент, убедитесь, что прозрачность здесь действительно работает, и освоите эффективный способ обновления с одним вызовом Flush.