Автоматизация торговых стратегий в MQL5 (Часть 53): Разворотные модели "двойная вершина" и "двойное дно"
Введение
Паттерны "двойная вершина" и "двойное дно" легко распознать постфактум, но торговать по ним в реальном времени сложно: экстремумы редко совпадают по цене в точности, впадина между вершинами или гребень между минимумами могут быть слабо выражены. Кроме того, человеческий глаз склонен дорисовывать упорядоченные фигуры в шумном движении цены. Для разработчиков алгоритмических торговых систем проблема еще серьезнее. Без четких правил один и тот же паттерн то появляется, то исчезает в зависимости от того, кто провел линию шеи, какие бары считаются опорными точками и насколько велик допуск. Такая неоднозначность делает сигналы невоспроизводимыми в Тестере стратегий, подталкивает к подгонке параметров задним числом и препятствует надежной автоматизации.
В предыдущей статье (часть 52) мы автоматизировали модель tCISD, которая сочетает циклы Quarterly Theory с дивергенцией между коррелирующими символами. В этой статье описанная выше проблема решается за счет формализации паттерна в виде строгого и проверяемого набора правил. Создадим программу на языке MetaQuotes Language 5 (MQL5), в которой формализован каждый критерий: подтверждение свинговых опорных точек, числовой допуск сопоставления экстремумов, зависящий от высоты паттерна, определение линии шеи по промежуточной опорной точке, настраиваемые фильтры расстояния между экстремумами и баланса отрезков, а также четкие правила входа (закрытие бара за линией шеи или, по выбору, вход при откате). В программе также заданы правила размещения стоп-лосса (за более дальним из двух экстремумов с дополнительным отступом), расчета целей (по проекции высоты паттерна или по соотношению вознаграждения и риска), режимы расчета размера позиции и опциональные трейлинг-стопы. В результате получается воспроизводимый пайплайн обнаружения паттернов, входа в сделки и управления рисками. Его можно запускать в Тестере стратегий и настраивать, не полагаясь на визуальную оценку. Мы рассмотрим следующие темы:
- Принцип работы разворотных моделей "двойная вершина" и "двойное дно"
- Реализация на MQL5
- Тестирование на исторических данных
- Заключение
Изучив статью, вы сможете:
- выявлять паттерны "двойная вершина" и "двойное дно" по подтвержденным свинговым опорным точкам и проверять их с учетом допуска расхождения цен экстремумов, баланса отрезков и расстояния между экстремумами;
- отмечать на графике экстремумы и линию шеи, входить в разворотную сделку при пробое или, по выбору, при откате, размещать стоп-лосс за экстремумом паттерна, рассчитывать цель по проекции высоты паттерна или по соотношению вознаграждения и риска и сопровождать позицию трейлинг-стопом.
Принцип работы разворотных моделей "двойная вершина" и "двойное дно"
"Двойная вершина" и "двойное дно" — это ситуации, когда рынок дважды не может преодолеть один и тот же ценовой уровень. При формировании паттерна "двойная вершина" цена достигает максимума, откатывается, затем вновь поднимается примерно до того же уровня и разворачивается вниз. То, что вторая попытка роста не приводит к обновлению максимума, указывает на истощение сил покупателей. Паттерн "двойное дно" представляет собой зеркальную картину: формируются два минимума примерно на одном ценовом уровне. Со второй попытки продавцам не удается опустить цену ниже, и контроль вновь переходит к покупателям. Линия, проведенная через точку промежуточного разворота между двумя экстремумами, называется линией шеи. Это ключевой уровень: паттерн считается завершенным только после закрытия бара за ним.
Подход на основе правил эффективнее визуальной оценки, поскольку каждый субъективный критерий в нем выражается конкретным параметром. Для сопоставления экстремумов задается допуск, зависящий от высоты паттерна, а для проверки баланса сравнивается число баров на каждом из двух отрезков, что позволяет отсеивать несимметричные фигуры. Фильтр расстояния между экстремумами отсеивает слишком близко расположенные экстремумы, а опциональная проверка предшествующего тренда — паттерны в середине диапазона, где нет движения, которое могло бы развернуться. Если паттерн проходит фильтры, все опорные уровни определены однозначно: линия шеи задает уровень срабатывания сигнала на вход, более дальний из двух экстремумов служит точкой привязки стоп-лосса, а расстояние от линии шеи до этого экстремума определяет целевой уровень. Пробой линии шеи подтверждает разворот, а откат для повторного тестирования этого уровня дает альтернативную возможность входа, часто по более выгодной цене. Ниже представлена общая архитектура стратегии.

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

Реализация на 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 выполняем эту последовательность только при появлении нового бара: сначала обнаруживаем новые опорные точки, затем ищем паттерн. Именно такой порядок необходим, поскольку найти паттерн можно только после обнаружения свинговых опорных точек, из которых он состоит. При запуске этой программы мы получаем следующий результат.

На графике видно, что мы обнаружили паттерн "двойное дно". Остается либо подтвердить его на следующих барах, либо отклонить и возобновить поиск паттернов. Для сброса отмененных сетапов и сетапов, по которым уже выполнен вход, используем следующую логику.
Сброс сетапа
Определим функцию 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, получим следующий результат.

На изображении видно, что сетапы подтверждены и позиции открыты. Остается добавить сопровождение позиций трейлинг-стопом при движении цены в нашу пользу. Для этого используем следующую логику.
Сопровождение открытой позиции трейлинг-стопом
Определим функцию 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.
Ниже показан график результатов тестирования на исторических данных.

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

Заключение
Мы реализовали разворотные паттерны "двойная вершина" и "двойное дно" на языке 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 3): Ребра и первая фигура с заливкой
Алгоритм опыления цветов — Flower Pollination Algorithm (FPA)
Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 2): Точки, контуры и путь
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования