English
preview
Автоматизация торговых стратегий в MQL5 (Часть 53): Разворотные модели "двойная вершина" и "двойное дно"

Автоматизация торговых стратегий в MQL5 (Часть 53): Разворотные модели "двойная вершина" и "двойное дно"

MetaTrader 5 — Трейдинг |
29 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

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

В предыдущей статье (часть 52) мы автоматизировали модель tCISD, которая сочетает циклы Quarterly Theory с дивергенцией между коррелирующими символами. В этой статье описанная выше проблема решается за счет формализации паттерна в виде строгого и проверяемого набора правил. Создадим программу на языке MetaQuotes Language 5 (MQL5), в которой формализован каждый критерий: подтверждение свинговых опорных точек, числовой допуск сопоставления экстремумов, зависящий от высоты паттерна, определение линии шеи по промежуточной опорной точке, настраиваемые фильтры расстояния между экстремумами и баланса отрезков, а также четкие правила входа (закрытие бара за линией шеи или, по выбору, вход при откате). В программе также заданы правила размещения стоп-лосса (за более дальним из двух экстремумов с дополнительным отступом), расчета целей (по проекции высоты паттерна или по соотношению вознаграждения и риска), режимы расчета размера позиции и опциональные трейлинг-стопы. В результате получается воспроизводимый пайплайн обнаружения паттернов, входа в сделки и управления рисками. Его можно запускать в Тестере стратегий и настраивать, не полагаясь на визуальную оценку. Мы рассмотрим следующие темы:

  1. Принцип работы разворотных моделей "двойная вершина" и "двойное дно"
  2. Реализация на MQL5
  3. Тестирование на исторических данных
  4. Заключение

Изучив статью, вы сможете:

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


Принцип работы разворотных моделей "двойная вершина" и "двойное дно"

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

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

DOUBLE TOP AND DOUBLE BOTTOM

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

DOUBLE TOP AND DOUBLE BOTTOM EXPERT ADVISOR


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

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

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

//+------------------------------------------------------------------+
//|                                 Double Top and Double Bottom.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

//--- Include the standard library for order execution
#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum LotSizingMode
  {
   LOTS_FIXED,        // Fixed lot size
   LOTS_RISK_PERCENT  // Risk percent of balance (auto lot)
  };

enum TakeProfitMode
  {
   TP_MEASURED_MOVE,  // Project the pattern height from the neckline
   TP_REWARD_RISK     // Multiple of the stop distance
  };

enum SetupState
  {
   STATE_IDLE,        // No active pattern
   STATE_ARMED,       // Pattern found, waiting for the neckline break
   STATE_RETEST       // Break confirmed, waiting for the pullback
  };

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "GENERAL"
input long            InpMagicNumber        = 1107;             // Magic number (unique ID for this EA)
input LotSizingMode   InpLotSizingMode      = LOTS_RISK_PERCENT; // Lot sizing mode
input double          InpFixedLots          = 0.01;             // Fixed lot
input double          InpRiskPercent        = 0.5;              // Risk per trade (percent of balance)
input string          InpOrderComment       = "DTB";            // Order comment

input group "PATTERN"
input ENUM_TIMEFRAMES InpEntryTimeframe     = PERIOD_CURRENT;   // Working timeframe
input int             InpSwingLength        = 5;                // Swing pivot length (bars each side)
input double          InpPeakTolerancePercent = 10.0;           // Max peak difference as percent of pattern height
input int             InpMinPatternBars     = 10;               // Minimum bars from first to second peak (0 = off)
input double          InpLegBalancePercent  = 50.0;             // Shorter leg as percent of longer leg, minimum (0 = off)
input bool            InpIncludeBreakLeg    = false;            // Fold the break leg into the balance test at breakout
input bool            InpRequirePriorTrend  = false;             // Require a prior trend into the pattern

input group "STOP LOSS"
input int             InpStopBufferPoints   = 3000;               // Buffer beyond the pattern extreme (points)
input int             InpMinStopPoints      = 0;                // Skip the trade if the stop is closer than this (points, 0 = off)

input group "TAKE PROFIT"
input TakeProfitMode  InpTakeProfitMode     = TP_MEASURED_MOVE; // Target mode
input double          InpRewardRiskRatio    = 1.0;              // Reward to risk ratio (R:R mode)

input group "ENTRY"
input bool            InpTradePullback      = false;             // Wait for the pullback to the neckline after the breakout
input int             InpMaxConfirmBars     = 50;               // Bars to wait for the neckline break
input int             InpMaxRetestBars      = 24;               // Bars to wait for the pullback

input group "TRAILING STOP"
input bool            InpUseTrailingStop    = true;            // Use trailing stop
input int             InpMinProfitPoints    = 1000;             // Minimum profit to activate trailing (points)
input int             InpTrailPoints        = 300;              // Trailing distance (points)

input group "LOGGING"
input bool            InpShowLogs           = true;             // Print messages to the Journal
input string          InpLogPrefix          = "DTB> ";          // Log prefix

input group "VISUALS"
input bool            InpDrawVisuals        = true;             // Draw patterns on the chart
input int             InpMarkerSize         = 10;               // Peak marker size (Wingdings 3)
input color           InpBullColor          = clrDodgerBlue;    // Double bottom color
input color           InpBearColor          = clrRed;           // Double top color
input color           InpNecklineColor      = clrGoldenrod;     // Neckline color
input color           InpTrendColor         = clrGray;          // Leading trend leg color

//+------------------------------------------------------------------+
//| Swing pivot point (high or low)                                  |
//+------------------------------------------------------------------+
struct SwingPoint
  {
   bool     isHigh;  // True for a swing high, false for a swing low
   double   price;   // Pivot price
   datetime time;    // Pivot bar time
  };

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade   trade;                        // Trade execution object
int      symbolDigits;                 // Cached symbol digits
double   symbolPoint;                  // Cached symbol point size
datetime lastBarTime = 0;              // Last processed bar time

SwingPoint swings[];                   // Rolling buffer of detected swing pivots

//--- Pattern state machine
int        setupState           = STATE_IDLE; // Current setup stage
int        patternDirection     = 0;          // Pattern direction: -1 top, +1 bottom
double     necklineLevel        = 0.0;        // Neckline price
double     patternExtreme       = 0.0;        // Pattern high (top) or low (bottom)
datetime   peakOneTime          = 0;          // Time of the first peak
datetime   peakTwoTime          = 0;          // Time of the second peak
double     peakOnePrice         = 0.0;        // Price of the first peak
double     peakTwoPrice         = 0.0;        // Price of the second peak
datetime   necklineTime         = 0;          // Time of the neckline pivot
bool       hasLead              = false;      // True when a leading trend leg exists
datetime   leadTime             = 0;          // Time of the leading trend start
double     leadPrice            = 0.0;        // Price of the leading trend start
datetime   necklineStartTime    = 0;          // Time the neckline line starts drawing
datetime   lastPatternEndTime   = 0;          // Second-peak time of the last pattern
datetime   breakoutTime         = 0;          // Time of the neckline breakout bar
datetime   pullbackExtremeTime  = 0;          // Time of the pullback extreme
double     pullbackExtremePrice = 0.0;        // Price of the pullback extreme
int        barsInState          = 0;          // Bars elapsed in the current stage

Сначала подключим стандартную библиотеку Trade.mqh для исполнения ордеров, затем объявим три перечисления, определяющие поведение программы. Перечисление LotSizingMode задает выбор между фиксированным количеством лотов и расчетом размера позиции по риску. Перечисление TakeProfitMode определяет способ расчета цели: по проекции высоты паттерна или с заданным множителем вознаграждения к риску. Перечисление SetupState задает три состояния сетапа: исходное состояние, активированный сетап с ожиданием пробоя линии шеи и ожидание отката. Именно перечисление SetupState служит основой конечного автомата, который управляет всей последовательностью действий до входа в позицию.

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

Отрисовка паттерна и хранение свинговых опорных точек

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

//+------------------------------------------------------------------+
//| Draw the full double top or bottom pattern                       |
//+------------------------------------------------------------------+
void DrawPattern()
  {
//--- Skip when visuals are disabled
   if(!VisualsAllowed()) return;
//--- Build a per-pattern id from the second peak time
   string id = IntegerToString((int)peakTwoTime);
//--- Pick the color and orientation for the pattern side
   color patternColor = (patternDirection < 0) ? InpBearColor : InpBullColor;
   bool topPattern = (patternDirection < 0);
//--- Extend drawing to the current bar
   datetime now = iTime(_Symbol, InpEntryTimeframe, 0);
//--- Draw the leading trend leg when present
   if(hasLead)
      DrawTrend("DTB_Lead_" + id, leadTime, leadPrice, peakOneTime, peakOnePrice, InpTrendColor, STYLE_DOT, 1);
//--- Draw the first leg from peak one to the neckline
   DrawTrend("DTB_LegA_" + id, peakOneTime, peakOnePrice, necklineTime, necklineLevel, patternColor, STYLE_SOLID, 2);
//--- Draw the second leg from the neckline to peak two
   DrawTrend("DTB_LegB_" + id, necklineTime, necklineLevel, peakTwoTime, peakTwoPrice, patternColor, STYLE_SOLID, 2);
//--- Draw the horizontal neckline
   DrawTrend("DTB_Neck_" + id, necklineStartTime, necklineLevel, now, necklineLevel, InpNecklineColor, STYLE_DASH, 1);
//--- Label the neckline
   DrawLabel("DTB_Neckt_" + id, necklineStartTime, necklineLevel, " Neckline", InpNecklineColor,
             topPattern ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
//--- Mark the first peak
   DrawMarker("DTB_P1_" + id, peakOneTime, peakOnePrice, !topPattern, patternColor, topPattern ? ANCHOR_LOWER : ANCHOR_UPPER);
//--- Mark the second peak
   DrawMarker("DTB_P2_" + id, peakTwoTime, peakTwoPrice, !topPattern, patternColor, topPattern ? ANCHOR_LOWER : ANCHOR_UPPER);
//--- Mark the neckline pivot
   DrawMarker("DTB_N_"  + id, necklineTime, necklineLevel, topPattern, InpNecklineColor, topPattern ? ANCHOR_UPPER : ANCHOR_LOWER);
//--- Label the first peak
   DrawLabel("DTB_P1t_" + id, peakOneTime, peakOnePrice, topPattern ? " First Top" : " First Bottom",
             patternColor, topPattern ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
//--- Label the second peak
   DrawLabel("DTB_P2t_" + id, peakTwoTime, peakTwoPrice, topPattern ? " Second Top" : " Second Bottom",
             patternColor, topPattern ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
  }

//+------------------------------------------------------------------+
//| Extend the neckline line to the current bar                      |
//+------------------------------------------------------------------+
void ExtendNeckline()
  {
//--- Skip when visuals are disabled
   if(!VisualsAllowed()) return;
//--- Build the per-pattern id and current bar time
   string id = IntegerToString((int)peakTwoTime);
   datetime now = iTime(_Symbol, InpEntryTimeframe, 0);
//--- Redraw the neckline out to the current bar
   DrawTrend("DTB_Neck_" + id, necklineStartTime, necklineLevel, now, necklineLevel, InpNecklineColor, STYLE_DASH, 1);
  }

//+------------------------------------------------------------------+
//| Draw the pullback path back to the neckline                      |
//+------------------------------------------------------------------+
void DrawPullback()
  {
//--- Skip when visuals are disabled
   if(!VisualsAllowed()) return;
//--- Build the per-pattern id and the touch bar time
   string id = IntegerToString((int)peakTwoTime);
   datetime touchTime = iTime(_Symbol, InpEntryTimeframe, 1);
//--- Pick the pattern color
   color patternColor = (patternDirection < 0) ? InpBearColor : InpBullColor;
//--- Draw the leg from the breakout to the pullback extreme
   DrawTrend("DTB_PBa_" + id, breakoutTime, necklineLevel, pullbackExtremeTime, pullbackExtremePrice, patternColor, STYLE_DASH, 1);
//--- Draw the leg from the pullback extreme back to the neckline
   DrawTrend("DTB_PBb_" + id, pullbackExtremeTime, pullbackExtremePrice, touchTime, necklineLevel, patternColor, STYLE_DASH, 1);
//--- Label the pullback
   DrawLabel("DTB_PBt_" + id, touchTime, necklineLevel, " Pullback", patternColor,
             patternDirection < 0 ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
  }

//+------------------------------------------------------------------+
//| Append a swing pivot and cap the rolling buffer                  |
//+------------------------------------------------------------------+
void AddSwing(bool isHigh, double price, datetime time)
  {
//--- Skip if this exact pivot is already stored
   int n = ArraySize(swings);
   for(int i = 0; i < n; i++)
      if(swings[i].time == time && swings[i].isHigh == isHigh) return;
//--- Append the new pivot
   ArrayResize(swings, n + 1);
   swings[n].isHigh = isHigh; swings[n].price = price; swings[n].time = time;
//--- Drop the oldest pivot once the buffer exceeds 80 entries
   if(ArraySize(swings) > 80)
     {
      //--- Shift entries down over the gap and shrink the array
      for(int i = 0; i < ArraySize(swings) - 1; i++) swings[i] = swings[i + 1];
      ArrayResize(swings, ArraySize(swings) - 1);
     }
  }

Определим функцию DrawPattern, которая после обнаружения паттерна отрисовывает всю фигуру: два отрезка, горизонтальную линию шеи, опциональный отрезок предшествующего тренда, а также маркеры экстремумов и линии шеи с подписями. Ориентация и цвет элементов зависят от типа паттерна — двойная вершина или двойное дно. Функция ExtendNeckline во время ожидания продлевает линию шеи до текущего бара, чтобы уровень срабатывания сигнала на вход оставался видимым по мере приближения цены. Функция DrawPullback отображает траекторию движения от пробоя до ретеста, чтобы вход при откате было легко распознать постфактум. Единственная из этих функций, которая нужна не только для визуализации, — AddSwing. Она добавляет каждую подтвержденную опорную точку в массив истории swings, игнорирует дубликаты и ограничивает размер буфера 80 записями, удаляя самые старые. Благодаря этому поиск паттернов использует ограниченный набор последних опорных точек, а буфер не растет бесконечно при длительной работе программы.

Обнаружение свинговых опорных точек

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

//+------------------------------------------------------------------+
//| Detect a confirmed swing high or low and store it                |
//+------------------------------------------------------------------+
void DetectSwings()
  {
//--- Center on the candidate bar with length bars each side
   int length = InpSwingLength;
   int candidate = length + 1;
//--- Require enough history to test both sides
   if(iBars(_Symbol, InpEntryTimeframe) < 2 * length + 2) return;
//--- Read the candidate bar high and low
   double candidateHigh = iHigh(_Symbol, InpEntryTimeframe, candidate);
   double candidateLow  = iLow(_Symbol, InpEntryTimeframe, candidate);
//--- Assume both a high and a low pivot until a neighbor breaks it
   bool isHigh = true, isLow = true;
//--- Compare the candidate against length bars on each side
   for(int k = 1; k <= length; k++)
     {
      //--- Reject the high candidate when a neighboring bar breaks it
      if(iHigh(_Symbol, InpEntryTimeframe, candidate - k) >= candidateHigh ||
         iHigh(_Symbol, InpEntryTimeframe, candidate + k) >  candidateHigh) isHigh = false;
      //--- Reject the low candidate when a neighboring bar breaks it
      if(iLow(_Symbol, InpEntryTimeframe, candidate - k) <= candidateLow ||
         iLow(_Symbol, InpEntryTimeframe, candidate + k) <  candidateLow) isLow = false;
     }
//--- Store the confirmed pivot(s)
   datetime candidateTime = iTime(_Symbol, InpEntryTimeframe, candidate);
   if(isHigh) AddSwing(true, candidateHigh, candidateTime);
   if(isLow)  AddSwing(false, candidateLow, candidateTime);
  }

Определим функцию DetectSwings для проверки закрытого бара-кандидата, достаточно удаленного от правого края графика, чтобы с каждой стороны от него можно было проверить одинаковое число баров. Его максимум подтверждается как опорная точка, если в этом окне нет более высокого максимума, а минимум — если нет более низкого минимума. На одном баре могут подтвердиться и свинговый максимум, и свинговый минимум. Данные каждой подтвержденной опорной точки передаются в функцию AddSwing для сохранения. Выбор бара-кандидата на достаточном расстоянии от текущего формирующегося бара позволяет считать опорную точку окончательно подтвержденной, а не предварительной. Поэтому детектор не отмечает свинги, которые могли бы отмениться с появлением следующих баров. Именно на этой надежности основано сопоставление паттернов.

Активация обнаруженного паттерна

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

//+------------------------------------------------------------------+
//| Store a detected pattern and arm it for the neckline break       |
//+------------------------------------------------------------------+
void ArmPattern(int direction, double neckline, datetime neckTime, double extreme, datetime t1, double p1, datetime t2, double p2, bool lead, datetime lTime, double lPrice)
  {
//--- Store the pattern direction
   patternDirection = direction;
//--- Store the neckline level and its pivot time
   necklineLevel = neckline; necklineTime = neckTime;
//--- Store the pattern extreme used as the stop anchor
   patternExtreme = extreme;
//--- Store the two peak times and prices
   peakOneTime = t1; peakOnePrice = p1;
   peakTwoTime = t2; peakTwoPrice = p2;
//--- Store the leading trend leg details
   hasLead = lead; leadTime = lTime; leadPrice = lPrice;
//--- Remember this pattern's second peak as the last pattern end
   lastPatternEndTime = t2;
//--- Default the neckline drawing to start at the first peak
   necklineStartTime = t1;
//--- Move the neckline start onto the leading leg where it crosses
   if(hasLead && MathAbs(p1 - lPrice) > 0)
     {
      //--- Interpolate the crossing point along the leading leg
      double ratio = (neckline - lPrice) / (p1 - lPrice);
      if(ratio > 0 && ratio < 1)
         necklineStartTime = lTime + (datetime)((double)(t1 - lTime) * ratio);
     }
//--- Arm the setup and reset the bar counter
   setupState = STATE_ARMED; barsInState = 0;
//--- Log the armed pattern
   Log((direction < 0 ? "Double top" : "Double bottom") + " armed | neckline " +
       DoubleToString(neckline, symbolDigits) + " | waiting for the break");
//--- Draw the pattern
   DrawPattern();
  }

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

Поиск двойной вершины или двойного дна

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

//+------------------------------------------------------------------+
//| Search the recent swings for a double top or bottom              |
//+------------------------------------------------------------------+
void TryDetectPattern()
  {
//--- Only search while idle
   if(setupState != STATE_IDLE) return;
//--- Require at least three swings to form a pattern
   int n = ArraySize(swings);
   if(n < 3) return;
//--- Take the most recent swing as the second peak
   SwingPoint latest = swings[n - 1];
//--- Convert the peak tolerance percent to a fraction
   double tolerance = InpPeakTolerancePercent / 100.0;
//--- Double top: the latest swing is a high
   if(latest.isHigh)
     {
      //--- Find the previous swing high before the latest
      int priorHigh = -1;
      for(int i = n - 2; i >= 0; i--) if(swings[i].isHigh) { priorHigh = i; break; }
      if(priorHigh < 0) return;
      //--- Reject overlap with the previous pattern
      if(swings[priorHigh].time < lastPatternEndTime) return;
      //--- Take the lowest low between the two highs as the neckline
      double neckline = 0; datetime neckTime = 0; bool found = false;
      for(int i = priorHigh + 1; i < n - 1; i++)
         if(!swings[i].isHigh && (!found || swings[i].price < neckline)) { neckline = swings[i].price; neckTime = swings[i].time; found = true; }
      if(!found) return;
      //--- Measure the pattern height above the neckline
      double peak1 = swings[priorHigh].price, peak2 = latest.price;
      double height = (peak1 + peak2) / 2.0 - neckline;
      if(height <= 0) return;
      //--- Reject when the two peaks differ by more than the tolerance
      if(MathAbs(peak1 - peak2) > tolerance * height) return;
      //--- Resolve the bar shifts of the two peaks and the neckline
      int shiftPeakOne = iBarShift(_Symbol, InpEntryTimeframe, swings[priorHigh].time, false);
      int shiftNeck    = iBarShift(_Symbol, InpEntryTimeframe, neckTime, false);
      int shiftPeakTwo = iBarShift(_Symbol, InpEntryTimeframe, latest.time, false);
      //--- Reject when the two peaks are too close together
      if(InpMinPatternBars > 0 && (shiftPeakOne - shiftPeakTwo) < InpMinPatternBars) return;
      //--- Reject when the two legs are too unbalanced
      if(InpLegBalancePercent > 0)
        {
         //--- Measure the two legs in bars
         double legA = (double)(shiftPeakOne - shiftNeck);
         double legB = (double)(shiftNeck - shiftPeakTwo);
         if(legA <= 0 || legB <= 0) return;
         //--- Require the shorter leg to meet the balance percent
         if(100.0 * MathMin(legA, legB) / MathMax(legA, legB) < InpLegBalancePercent) return;
        }
      //--- Find a leading low before the first peak for the trend leg
      bool lead = false; datetime lTime = 0; double lPrice = 0;
      for(int i = priorHigh - 1; i >= 0; i--) if(!swings[i].isHigh) { lead = true; lTime = swings[i].time; lPrice = swings[i].price; break; }
      //--- Require a prior downtrend into the pattern when enabled
      if(InpRequirePriorTrend && (!lead || lPrice >= neckline)) return;
      //--- Arm the double top
      ArmPattern(-1, neckline, neckTime, MathMax(peak1, peak2), swings[priorHigh].time, peak1, latest.time, peak2, lead, lTime, lPrice);
     }
//--- Double bottom: the latest swing is a low
   else
     {
      //--- Find the previous swing low before the latest
      int priorLow = -1;
      for(int i = n - 2; i >= 0; i--) if(!swings[i].isHigh) { priorLow = i; break; }
      if(priorLow < 0) return;
      //--- Reject overlap with the previous pattern
      if(swings[priorLow].time < lastPatternEndTime) return;
      //--- Take the highest high between the two lows as the neckline
      double neckline = 0; datetime neckTime = 0; bool found = false;
      for(int i = priorLow + 1; i < n - 1; i++)
         if(swings[i].isHigh && (!found || swings[i].price > neckline)) { neckline = swings[i].price; neckTime = swings[i].time; found = true; }
      if(!found) return;
      //--- Measure the pattern height below the neckline
      double trough1 = swings[priorLow].price, trough2 = latest.price;
      double height = neckline - (trough1 + trough2) / 2.0;
      if(height <= 0) return;
      //--- Reject when the two troughs differ by more than the tolerance
      if(MathAbs(trough1 - trough2) > tolerance * height) return;
      //--- Resolve the bar shifts of the two troughs and the neckline
      int shiftTroughOne = iBarShift(_Symbol, InpEntryTimeframe, swings[priorLow].time, false);
      int shiftNeck      = iBarShift(_Symbol, InpEntryTimeframe, neckTime, false);
      int shiftTroughTwo = iBarShift(_Symbol, InpEntryTimeframe, latest.time, false);
      //--- Reject when the two troughs are too close together
      if(InpMinPatternBars > 0 && (shiftTroughOne - shiftTroughTwo) < InpMinPatternBars) return;
      //--- Reject when the two legs are too unbalanced
      if(InpLegBalancePercent > 0)
        {
         //--- Measure the two legs in bars
         double legA = (double)(shiftTroughOne - shiftNeck);
         double legB = (double)(shiftNeck - shiftTroughTwo);
         if(legA <= 0 || legB <= 0) return;
         //--- Require the shorter leg to meet the balance percent
         if(100.0 * MathMin(legA, legB) / MathMax(legA, legB) < InpLegBalancePercent) return;
        }
      //--- Find a leading high before the first trough for the trend leg
      bool lead = false; datetime lTime = 0; double lPrice = 0;
      for(int i = priorLow - 1; i >= 0; i--) if(swings[i].isHigh) { lead = true; lTime = swings[i].time; lPrice = swings[i].price; break; }
      //--- Require a prior uptrend into the pattern when enabled
      if(InpRequirePriorTrend && (!lead || lPrice <= neckline)) return;
      //--- Arm the double bottom
      ArmPattern(1, neckline, neckTime, MathMin(trough1, trough2), swings[priorLow].time, trough1, latest.time, trough2, lead, lTime, lPrice);
     }
  }

Функция TryDetectPattern ищет паттерн среди последних свингов только при отсутствии активного сетапа. Функция принимает последнюю опорную точку за второй экстремум и просматривает историю в обратном порядке в поисках подходящей опорной точки того же типа. Линия шеи определяется по наиболее выраженной опорной точке противоположного типа между ними: по наименьшему минимуму между двумя максимумами для двойной вершины или по наибольшему максимуму между двумя минимумами для двойного дна. Расстояние от линии шеи до парных экстремумов определяет высоту паттерна. Высота паттерна позволяет заменить приблизительную визуальную оценку численными критериями: разница цен двух экстремумов не должна превышать допуск, выраженный как доля высоты; число баров на двух отрезках должно соответствовать требованию баланса; расстояние между экстремумами должно быть достаточным; опциональная проверка предшествующего тренда должна подтверждать наличие направленного движения перед формированием фигуры. Для двойной вершины и двойного дна направления зеркально противоположны. В функцию ArmPattern передается только паттерн, прошедший все включенные фильтры. Для привязки стоп-лосса выбирается более дальний из двух экстремумов — больший максимум для двойной вершины или меньший минимум для двойного дна, чтобы стоп-лосс располагался за пределами всей фигуры. Инициализируем систему и вызовем функцию поиска паттернов в обработчике событий OnTick, чтобы увидеть промежуточный результат. Для этого используем следующий подход.

Подключение обработчиков событий

Реализовав обнаружение паттернов, добавим код инициализации в обработчик событий OnInit, код очистки — в обработчик событий OnDeinit, а последовательность действий для каждого нового бара — в обработчик событий OnTick.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Cache the symbol digits and point size
   symbolDigits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   symbolPoint  = _Point;
//--- Configure the trade object magic number and slippage
   trade.SetExpertMagicNumber(InpMagicNumber);
   trade.SetDeviationInPoints(20);
//--- Clear the swing buffer
   ArrayResize(swings, 0);
//--- Reset the setup state
   setupState = STATE_IDLE; patternDirection = 0;
//--- Reset the pattern history anchors
   lastPatternEndTime = 0; necklineStartTime = 0;
//--- Seed the bar-time guard
   lastBarTime = iTime(_Symbol, InpEntryTimeframe, 0);
//--- Log a ready banner with the key settings
   Log("Double Top and Double Bottom EA ready on " + _Symbol + " | Magic " + IntegerToString(InpMagicNumber));
//--- Report successful initialization
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete our chart objects on a real removal or chart close
   if(reason == REASON_REMOVE || reason == REASON_CHARTCLOSE || reason == REASON_CLOSE)
      ObjectsDeleteAll(0, "DTB_");
//--- Clear any chart comment
   Comment("");
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Run per-bar logic only on a new bar
   if(IsNewBar())
     {
      //--- Detect new swing pivots
      DetectSwings();
      //--- Try to detect and arm a pattern
      TryDetectPattern();
      if(VisualsAllowed()) ChartRedraw(0);
     }
  }

В обработчике событий OnInit подготовим программу к первому тику: сохраним число знаков после запятой в цене символа и размер пункта, зададим торговому объекту магическое число и допустимое отклонение цены, очистим буфер свингов и сбросим состояние паттерна, а также сохраним время текущего бара для проверки появления нового. После записи сообщения о готовности в Журнал вернем INIT_SUCCEEDED. Обработчик событий OnDeinit при удалении советника или закрытии графика удаляет только графические объекты программы, определяя их по общему префиксу. При обычной перекомпиляции визуальные элементы сохраняются. В обработчике событий OnTick выполняем эту последовательность только при появлении нового бара: сначала обнаруживаем новые опорные точки, затем ищем паттерн. Именно такой порядок необходим, поскольку найти паттерн можно только после обнаружения свинговых опорных точек, из которых он состоит. При запуске этой программы мы получаем следующий результат.

DETECTED DOUBLE BOTTOM PATTERN

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

Сброс сетапа

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

//+------------------------------------------------------------------+
//| Reset the setup state machine to idle                            |
//+------------------------------------------------------------------+
void ResetSetup(string reason)
  {
//--- Log the reset with its direction and reason when active
   if(setupState != STATE_IDLE)
      Log((patternDirection < 0 ? "Double top" : "Double bottom") + " reset: " + reason + ".");
//--- Clear the state, direction and bar counter
   setupState = STATE_IDLE;
   patternDirection = 0;
   barsInState = 0;
  }

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

Открытие сделки

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

//+------------------------------------------------------------------+
//| Size, build SL and TP, and open the trade                        |
//+------------------------------------------------------------------+
void OpenTrade(bool isBull)
  {
//--- Compute the stop buffer in price
   double buffer = InpStopBufferPoints * symbolPoint;
//--- Enter at the market on the correct side
   double entry = isBull ? NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), symbolDigits)
                         : NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), symbolDigits);
//--- Place the stop beyond the pattern extreme
   double stop = isBull ? patternExtreme - buffer : patternExtreme + buffer;
//--- Reject the trade when the stop is closer than the minimum
   if(InpMinStopPoints > 0 && MathAbs(entry - stop) / symbolPoint < InpMinStopPoints)
     { ResetSetup("stop below minimum"); return; }
//--- Read the broker minimum stop distance (stops level or spread)
   long   stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   long   spread     = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
   double minPoints  = (double)MathMax(stopsLevel, spread);
   if(minPoints < 1) minPoints = 1;
//--- Convert the minimum distance to price
   double minDistance = minPoints * symbolPoint;
//--- Push the stop out to the broker minimum when too tight
   if(isBull) { if(entry - stop < minDistance) stop = entry - minDistance; }
   else       { if(stop - entry < minDistance) stop = entry + minDistance; }
//--- Normalize the stop and measure the risk distance
   stop = NormalizeDouble(stop, symbolDigits);
   double riskDistance = MathAbs(entry - stop);
//--- Abort on an invalid risk distance
   if(riskDistance <= 0) { ResetSetup("invalid risk distance"); return; }
//--- Measure the pattern height for the measured-move target
   double height = MathAbs(patternExtreme - necklineLevel);
//--- Build the take profit by the selected mode
   double takeProfit;
   if(InpTakeProfitMode == TP_MEASURED_MOVE)
      //--- Measured move: project the height from the neckline
      takeProfit = isBull ? necklineLevel + height : necklineLevel - height;
   else
      //--- Reward-to-risk: multiple of the stop distance
      takeProfit = isBull ? entry + InpRewardRiskRatio * riskDistance
                          : entry - InpRewardRiskRatio * riskDistance;
//--- Push the target out to the broker minimum when too tight
   if(isBull) { if(takeProfit - entry < minDistance) takeProfit = entry + minDistance; }
   else       { if(entry - takeProfit < minDistance) takeProfit = entry - minDistance; }
//--- Normalize the target
   takeProfit = NormalizeDouble(takeProfit, symbolDigits);
//--- Resolve the lot size for this trade
   double lots = ResolveLots(entry, stop);
//--- Abort on a lot sizing error
   if(lots <= 0) { ResetSetup("lot calc error"); return; }
//--- Send the market order on the correct side
   bool ok = isBull ? trade.Buy(lots, _Symbol, entry, stop, takeProfit, InpOrderComment)
                    : trade.Sell(lots, _Symbol, entry, stop, takeProfit, InpOrderComment);
//--- Annotate and log a successful fill
   if(ok)
     {
      //--- Draw the entry, stop and target levels
      datetime now = iTime(_Symbol, InpEntryTimeframe, 0);
      DrawTradeLevels(isBull, now, entry, stop, takeProfit);
      //--- Log the fill details
      Log((isBull ? "BUY" : "SELL") + " filled @ " + DoubleToString(entry, symbolDigits) +
          "  SL=" + DoubleToString(stop, symbolDigits) + "  TP=" + DoubleToString(takeProfit, symbolDigits) +
          "  lots=" + DoubleToString(lots, 2));
     }
   else
      //--- Log the failure reason
      Log("Open failed: " + trade.ResultRetcodeDescription());
//--- Reset the setup after the attempt
   ResetSetup("filled");
  }

//+------------------------------------------------------------------+
//| Test the three legs including the break leg for balance          |
//+------------------------------------------------------------------+
bool BreakLegBalanced()
  {
//--- Resolve the bar shifts of the two peaks and the neckline
   int shiftPeakOne = iBarShift(_Symbol, InpEntryTimeframe, peakOneTime, false);
   int shiftNeck    = iBarShift(_Symbol, InpEntryTimeframe, necklineTime, false);
   int shiftPeakTwo = iBarShift(_Symbol, InpEntryTimeframe, peakTwoTime, false);
//--- Measure the three legs in bars, including the break leg to now
   double legA = (double)(shiftPeakOne - shiftNeck);
   double legB = (double)(shiftNeck - shiftPeakTwo);
   double legC = (double)(shiftPeakTwo - 1);
//--- Fail on any non-positive leg
   if(legA <= 0 || legB <= 0 || legC <= 0) return false;
//--- Compare the shortest and longest legs
   double shortest = MathMin(legA, MathMin(legB, legC));
   double longest  = MathMax(legA, MathMax(legB, legC));
//--- Pass when the shortest leg meets the balance percent
   return (100.0 * shortest / longest >= InpLegBalancePercent);
  }

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

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

Обработка сетапа до входа в позицию

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

//+------------------------------------------------------------------+
//| Advance the setup through the break and pullback to entry        |
//+------------------------------------------------------------------+
void ProgressSetup()
  {
//--- Do nothing while idle
   if(setupState == STATE_IDLE) return;
//--- Count another bar in the current state
   barsInState++;
//--- Read the just-closed bar close, high and low
   double closePrice = iClose(_Symbol, InpEntryTimeframe, 1);
   double highPrice  = iHigh(_Symbol, InpEntryTimeframe, 1);
   double lowPrice   = iLow(_Symbol, InpEntryTimeframe, 1);
//--- Armed stage: wait for the neckline break
   if(setupState == STATE_ARMED)
     {
      //--- Reset if price broke through the pattern extreme first
      if(patternDirection < 0 && highPrice > patternExtreme) { ResetSetup("price broke above the pattern"); return; }
      if(patternDirection > 0 && lowPrice < patternExtreme) { ResetSetup("price broke below the pattern"); return; }
      //--- Reset if the neckline break never arrived in time
      if(barsInState > InpMaxConfirmBars) { ResetSetup("no neckline break in time"); return; }
      //--- Keep the neckline extended to the current bar
      ExtendNeckline();
      //--- Test for a close through the neckline
      bool confirmed = (patternDirection < 0) ? (closePrice < necklineLevel) : (closePrice > necklineLevel);
      //--- Handle a confirmed break
      if(confirmed)
        {
         //--- Optionally reject when the break leg fails the balance test
         if(InpIncludeBreakLeg && InpLegBalancePercent > 0 && !BreakLegBalanced())
           { ResetSetup("break leg fails balance"); return; }
         //--- Log the confirmed break
         Log((patternDirection < 0 ? "Double top" : "Double bottom") + " confirmed: neckline broken at " +
             DoubleToString(necklineLevel, symbolDigits));
         //--- Record the breakout bar time
         breakoutTime = iTime(_Symbol, InpEntryTimeframe, 1);
         //--- Draw the breakout leg and label
         if(VisualsAllowed())
           {
            //--- Draw the leg from peak two to the breakout
            string bid = IntegerToString((int)peakTwoTime);
            color bcolor = (patternDirection < 0) ? InpBearColor : InpBullColor;
            DrawTrend("DTB_Break_" + bid, peakTwoTime, peakTwoPrice, breakoutTime, necklineLevel, bcolor, STYLE_SOLID, 2);
            DrawLabel("DTB_Breakt_" + bid, breakoutTime, necklineLevel, " Breakout", bcolor,
                      patternDirection < 0 ? ANCHOR_LEFT_UPPER : ANCHOR_LEFT_LOWER);
           }
         //--- Enter immediately unless a pullback is required
         if(!InpTradePullback) { OpenTrade(patternDirection > 0); return; }
         //--- Seed the pullback extreme and wait for the retest
         pullbackExtremeTime  = breakoutTime;
         pullbackExtremePrice = (patternDirection < 0) ? lowPrice : highPrice;
         setupState = STATE_RETEST; barsInState = 0;
        }
      //--- Stop after the armed stage
      return;
     }
//--- Retest stage: wait for the pullback to the neckline
   if(setupState == STATE_RETEST)
     {
      //--- Reset if the pattern extreme is violated before the pullback
      if(patternDirection < 0 && highPrice > patternExtreme) { ResetSetup("pattern extreme violated before pullback"); return; }
      if(patternDirection > 0 && lowPrice < patternExtreme) { ResetSetup("pattern extreme violated before pullback"); return; }
      //--- Reset if the pullback never arrived in time
      if(barsInState > InpMaxRetestBars) { ResetSetup("no pullback in time"); return; }
      //--- Keep the neckline extended to the current bar
      ExtendNeckline();
      //--- Track the deepest pullback extreme for a top
      if(patternDirection < 0 && lowPrice < pullbackExtremePrice)
        { pullbackExtremePrice = lowPrice; pullbackExtremeTime = iTime(_Symbol, InpEntryTimeframe, 1); }
      //--- Track the highest pullback extreme for a bottom
      if(patternDirection > 0 && highPrice > pullbackExtremePrice)
        { pullbackExtremePrice = highPrice; pullbackExtremeTime = iTime(_Symbol, InpEntryTimeframe, 1); }
      //--- Enter short once price pulls back up to the neckline
      if(patternDirection < 0 && highPrice >= necklineLevel) { DrawPullback(); OpenTrade(false); }
      //--- Enter long once price pulls back down to the neckline
      else if(patternDirection > 0 && lowPrice <= necklineLevel) { DrawPullback(); OpenTrade(true); }
     }
  }

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

CONFIRMED DOUBLE BOTTOM SETUP

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

Сопровождение открытой позиции трейлинг-стопом

Определим функцию ManageTrailing, которая подтягивает стоп-лосс вслед за ценой по открытым позициям программы, когда цена движется в нашу пользу.

//+------------------------------------------------------------------+
//| Trail the stop on this EA's open positions                       |
//+------------------------------------------------------------------+
void ManageTrailing()
  {
//--- Do nothing when trailing is disabled
   if(!InpUseTrailingStop) return;
//--- Walk every open position from last to first
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      //--- Select the position by its ticket
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket)) continue;
      //--- Skip positions from another EA
      if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
      //--- Skip positions on another symbol
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      //--- Read the position side, entry, stop and target
      bool   isBull  = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
      double entry   = PositionGetDouble(POSITION_PRICE_OPEN);
      double curStop = PositionGetDouble(POSITION_SL);
      double curTP   = PositionGetDouble(POSITION_TP);
      //--- Read the current bid and ask
      double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      //--- Measure open profit in points
      double profitPoints = isBull ? (bid - entry) / symbolPoint : (entry - ask) / symbolPoint;
      //--- Trail only past the activation threshold
      if(profitPoints >= InpMinProfitPoints + InpTrailPoints)
        {
         //--- Compute the trailed stop behind price
         double newStop = isBull ? bid - InpTrailPoints * symbolPoint : ask + InpTrailPoints * symbolPoint;
         newStop = NormalizeDouble(newStop, symbolDigits);
         //--- Move the stop only when it improves protection
         bool improves = isBull ? (newStop > curStop) : (curStop == 0 || newStop < curStop);
         if(improves) trade.PositionModify(ticket, newStop, curTP);
        }
     }
  }

Определим функцию ManageTrailing для перебора открытых позиций и обработки только тех, у которых совпадают магическое число и символ программы. Позиции других программ она не затрагивает. Для каждой позиции программы рассчитываем текущую прибыль в пунктах. Только после превышения порога активации, равного сумме минимальной прибыли и дистанции трейлинга, определяем уровень стоп-лосса на заданном расстоянии позади цены. Переносим стоп-лосс с помощью метода PositionModify торгового объекта только тогда, когда новый уровень усиливает защиту, и никогда — если он ее ослабляет. Поэтому при благоприятном движении цены стоп-лосс подтягивается только вперед, без обратного смещения. Мы вызываем эту функцию при каждом тике в обработчике событий OnTick. Остается протестировать программу; это рассматривается в следующем разделе.


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

Мы протестировали программу на символе XAUUSDm с пятиминутным таймфреймом за период с 01.01.2025 по 22.07.2025 с моделированием всех тиков и начальным депозитом 10 000 долларов. В приложенном к статье файле настроек SET сохранены точные значения входных параметров, поэтому тестирование можно повторить с теми же настройками. Два из этих параметров имеют большее значение, чем остальные. Проверка предшествующего тренда отключена, поэтому паттерн "двойное дно" в середине диапазона принимается на тех же условиях, что и паттерн, завершающий нисходящий тренд. Режим входа при откате также отключен, поэтому каждый вход выполняется на баре, закрывающемся за линией шеи, а состояние ожидания ретеста не используется.

В ходе тестирования было совершено 53 сделки, из которых 50 закрылись с прибылью. Профит фактор составил 2,90, чистая прибыль — 147,08 доллара, а максимальная просадка эквити — 37,11 доллара, или 0,37%. Характер этих результатов говорит больше, чем сама доля прибыльных сделок. Средняя прибыльная сделка принесла 4,49 доллара, а средняя убыточная — убыток в размере 25,81 доллара. Это результат работы трейлинг-стопа: он активируется при прибыли в один доллар, затем следует за ценой на расстоянии 30 центов, поэтому большинство сделок закрываются раньше с небольшой прибылью, не достигая цели с соотношением вознаграждения и риска 2:1.

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

GRAPH

Ниже показан отчет о тестировании на исторических данных.

REPORT


Заключение

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

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

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

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

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


    Проект поддерживается в Algo Forge: https://forge.mql5.io/29210372/Article-23576-Automating-Trading-Strategies-MQL5-Double-Top-Double-Bottom

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

    Прикрепленные файлы |
    Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 3): Ребра и первая фигура с заливкой Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 3): Ребра и первая фигура с заливкой
    В третьей части реализован построчный растеризатор, написанный целиком на языке MQL5. Мы преобразуем отрезки пути в ребра, записанные в направлении сверху вниз, проходим по каждой строке, сортируем точки пересечения и применяем ненулевое правило заливки, чтобы определить, какие пиксели находятся внутри фигуры. В результате получается метод Fill(path, color, buffer, w, h), который рисует прямоугольники, звезды, глифы или фигуры с 2 000 ребрами без отдельного кода для каждого типа фигуры. Его можно сразу использовать в ваших проектах на языке MQL5.
    Алгоритм опыления цветов — Flower Pollination Algorithm (FPA) Алгоритм опыления цветов — Flower Pollination Algorithm (FPA)
    Рассматривается реализация Flower Pollination Algorithm (FPA) в MQL5 с разбором операторов глобального и локального опыления и влияния ключевых параметров популяции и шага Леви, который при авторских настройках ищет почти наугад: находим причину и поднимаем результат с 26% до 49%. Трейдер получает готовый класс FPA с рабочими настройками для оптимизации советников и понимание, для каких задач он подходит.
    Особенности написания экспертов Особенности написания экспертов
    Написание и тестирование экспертов в торговой системе MetaTrader 4.
    Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 2): Точки, контуры и путь Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 2): Точки, контуры и путь
    Во второй части мы добавим слой геометрии в графическую библиотеку для MetaTrader 5, создаваемую по образцу Cairo. В ней определяется путь из точек с координатами двойной точности, сгруппированных в контуры, сохраняется признак того, является ли контур открытым или замкнутым, а вершины хранятся в плоском массиве с индексами начала контуров. Мы реализуем методы MoveTo, LineTo и Close, добавим базовые вспомогательные методы для построения фигур и демонстрационный пример, который отображает построенную геометрию для визуальной проверки и повторного использования.