Автоматизация торговых стратегий в MQL5 (Часть 49): Разворотный паттерн "Квазимодо" (QM)
Введение
Торговля на разворотах нередко приносит трейдерам самые серьёзные убытки. Слишком ранний вход означает борьбу с текущим движением, а ожидание подтверждения часто лишает лучшей цены. Паттерн Квазимодо предлагает структурированный способ распознавания разворота, однако распознавание «на глаз» остаётся субъективным. Где именно находится левое плечо? Является ли новый экстремум настоящей головой или обычным рыночным шумом? Произошёл ли пробой структуры или цена лишь временно остановилась? Когда фигура сформирована, необходимо определить точку входа, уровень инвалидации и разумную цель движения. Без формализованных правил ответы меняются от графика к графику, а паттерн, очевидный задним числом, в реальном времени превращается в догадку.
Статья предназначена для разработчиков на MetaQuotes Language 5 (MQL5) и алгоритмических трейдеров, которые хотят автоматизировать разворотный подход Price Action на основе чётких и воспроизводимых правил. В предыдущей статье (Часть 48) мы автоматизировали концепцию Smart Money, основанную на ордер-блоках, индукции и пробое структуры. Теперь сосредоточимся на одной разворотной модели и создадим программу, которая выявляет паттерн Квазимодо по последовательности подтверждённых свинговых экстремумов (pivot points), подтверждает его пробоем структуры, ждёт возврата цены к уровню входа и сопровождает позицию от открытия до закрытия. Рассмотрим следующие темы:
- Понимание разворотного паттерна Квазимодо (QM)
- Реализация в MQL5
- Тестирование в тестере стратегий
- Заключение
В результате мы получим автоматическую программу MQL5, которая обнаруживает развороты Квазимодо, переводит сетап в состояние ожидания входа после подтверждения, входит при возврате к линии QM и применяет структурный Stop Loss, расчёт объёма по риску и управление позицией. Советник будет готов к бэктесту и дальнейшей настройке.
Понимание разворотного паттерна Квазимодо (QM)
Паттерн Квазимодо, также называемый моделью «выше и ниже», представляет собой разворотную структуру, сформированную узнаваемой последовательностью свинговых точек. Он относится к тому же семейству, что и фигура «голова и плечи», однако строится вокруг снятия ликвидности и подтверждённого пробоя рыночной структуры, а не симметричной линии шеи. Представьте, что восходящий тренд подходит к вершине. Цена формирует свинговый максимум — левое плечо, откатывается к свинговому минимуму Leg, а затем обновляет максимум, образуя голову и снимая ликвидность над плечом. Вместо продолжения роста цена разворачивается и пробивает минимум точки Leg; этот пробой структуры указывает, что предыдущий восходящий тренд, вероятно, завершён. Для разворота нисходящего тренда действует зеркальная схема: минимум левого плеча, максимум точки Leg, более низкая голова и пробой структуры выше точки Leg. Общая структура паттерна показана ниже.

После подтверждения структуры паттерн даёт нам три точных уровня без необходимости гадать. Вход располагается по цене левого плеча, которая становится линией QM: предполагается, что перед продолжением разворота цена вернётся к этому уровню. Уровень отмены сценария находится за головой, поскольку закрытие цены обратно за уровень головы означает, что снятие ликвидности не завершило тренд. Целью служит пробитый структурный уровень, поэтому ориентир связан с реальной рыночной структурой. Надёжное обнаружение требует последовательного определения свингов. Точка подтверждается только после закрытия заданного числа баров с обеих сторон, затем подтверждённые точки объединяются в чередующийся зигзаг максимумов и минимумов. Если подряд появляются два максимума или два минимума, сохраняется только более экстремальный. Благодаря этому последние четыре точки образуют чистую кандидатную структуру, а фильтр предыдущего тренда проверяет, действительно ли движение перед левым плечом было направленным.
На практике линия QM используется как ориентир для входа, а риск определяется самой структурой. Для медвежьего Квазимодо дождитесь, чтобы голова сняла ликвидность выше левого плеча, а цена пробила минимум точки Leg. Затем продавайте на возврате вверх к линии QM, размещая Stop Loss выше головы, а цель — на пробитом уровне точки Leg. Для бычьего Квазимодо условия зеркальны: голова снимает ликвидность ниже левого плеча, цена пробивает максимум точки Leg, после чего покупка открывается на возврате вниз к линии QM. Предпочтение следует отдавать сетапам после выраженного тренда и пропускать варианты с соотношением прибыли к риску ниже заданного порога. Для более консервативного входа можно дождаться прокола линии QM и последующего закрытия обратно через неё: это отфильтровывает возвраты, при которых цена проходит уровень без реакции. Ниже показано, что именно мы создадим.

Реализация в MQL5
Настройка входных параметров, перечислений и состояния программы
Сначала зададим конфигурацию программы и структуры, в которых хранится её состояние. Объявим макрос версии, подключим торговую библиотеку, добавим пользовательские параметры, а также перечисления и структуры для хранения данных паттерна и сделок.
//+------------------------------------------------------------------+ //| Советник по паттерну Квазимодо (QM).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" //--- Определяем строку версии советника #define EA_VERSION "1.00" //--- Подключаем торговую библиотеку #include <Trade\Trade.mqh> //+------------------------------------------------------------------+ //| Перечисления | //+------------------------------------------------------------------+ enum LotSizingMode { LOTS_FIXED, // Фиксированный размер лота LOTS_RISK_PERCENT // Процент риска от баланса (автоматический лот) }; enum TakeProfitMode { TP_REWARD_RISK, // Множитель прибыли к риску TP_STRUCTURE // Структурный экстремум пробитого свинга }; enum TrailingMode { TRAIL_OFF, // Без трейлинг-стопа TRAIL_ATR, // Расстояние по ATR позади цены TRAIL_POINTS // Фиксированное расстояние в пунктах за ценой }; enum TradeDirection { TRADE_BOTH, // Оба направления TRADE_BUYS_ONLY, // Только бычий QM (покупки) TRADE_SELLS_ONLY // Только медвежий QM (продажи) }; //+------------------------------------------------------------------+ //| Входные параметры | //+------------------------------------------------------------------+ input group "GENERAL" input long InpMagicNumber = 1107; // Магический номер (уникальный идентификатор советника) input string InpOrderComment = "QM"; // Комментарий к ордеру input TradeDirection InpTradeDirection = TRADE_BOTH; // Разрешённое направление торговли input bool InpAllowMultipleTrades = false; // Разрешить несколько одновременно открытых сделок input bool InpShowLogs = true; // Выводить сообщения в журнал input string InpLogPrefix = "QM> "; // Префикс журнала input group "RISK & POSITION SIZING" input LotSizingMode InpLotSizingMode = LOTS_RISK_PERCENT; // Режим расчёта лота input double InpFixedLots = 0.01; // Фиксированный лот (фиксированный режим) input double InpRiskPercent = 0.5; // Риск на сделку в процентах от баланса (режим риска) input group "SWING / PATTERN DETECTION" input int InpSwingLookback = 5; // Глубина поиска свинговой точки (баров с каждой стороны) input int InpEntryBufferPoints = 0; // Число пунктов, на которое закрытие должно пройти за линию QM input bool InpWaitForRejectionClose = false; // Ждать подтверждающего закрытия обратно через линию QM input int InpMaxWaitBars = 30; // Максимальное число баров ожидания возврата, затем отмена input double InpMinRewardRisk = 1.0; // Пропускать сетапы с меньшим целевым соотношением прибыли к риску input bool InpSkipSharedShoulder = true; // Пропускать новый сетап, повторно использующий последнее левое плечо input bool InpRequirePriorTrend = true; // Требовать наличия предыдущего тренда input int InpTrendPivots = 2; // Число свинговых точек до левого плеча для подтверждения тренда input group "STOP LOSS" input int InpStopBufferPoints = 200; // Буфер за головой (в пунктах) input bool InpUseAtrStopFloor = true; // Расширить стоп-лосс до минимальной дистанции по ATR, если структурный стоп слишком близок input int InpAtrPeriod = 14; // Период ATR для минимальной дистанции стоп-лосса и трейлинга input double InpAtrStopMultiplier = 1.0; // Множитель ATR для минимальной дистанции стоп-лосса input group "TAKE PROFIT" input TakeProfitMode InpTakeProfitMode = TP_REWARD_RISK; // Режим тейк-профита input double InpRewardRiskRatio = 2.0; // Соотношение прибыли к риску (режим RR) input group "TRADE MANAGEMENT" input bool InpUseBreakeven = true; // Переносить стоп-лосс в безубыток input double InpBreakevenAtRR = 1.0; // Прибыль в R для переноса в безубыток input int InpBreakevenLockPoints = 50; // Пункты прибыли за ценой входа при безубытке input TrailingMode InpTrailingMode = TRAIL_OFF; // Метод трейлинг-стопа для оставшейся позиции input double InpTrailStartRR = 1.5; // Прибыль в R, после которой начинается трейлинг по ATR input double InpTrailAtrMultiplier = 2.0; // Множитель ATR для дистанции трейлинга input int InpTrailLockPoints = 1000; // Трейлинг в пунктах: минимальная фиксируемая прибыль после начала input int InpTrailDistancePoints = 300; // Трейлинг в пунктах: расстояние за ценой input bool InpUsePartialClose = false; // Зафиксировать часть позиции на первой цели input double InpPartialAtRR = 1.0; // Цель частичного закрытия в R input double InpPartialPercent = 50.0; // Процент позиции для частичного закрытия input group "CHART VISUALS" input bool InpDrawVisuals = true; // Рисовать структуру QM (линии, уровни, стрелки) input bool InpShowSwingMarkers = true; // Рисовать подписанные свинговые точки (H/HH/LH, L/LL/HL) input color InpBullColor = clrDodgerBlue; // Цвет бычьего QM input color InpBearColor = clrRed; // Цвет медвежьего QM input color InpQmLineColor = clrDarkViolet; // Цвет линии QM (вход) input color InpSwingHighColor = clrDarkOrange; // Цвет маркера свингового максимума input color InpSwingLowColor = clrDodgerBlue; // Цвет маркера свингового минимума input color InpTrendColor = clrGray; // Цвет линии предыдущего тренда //+------------------------------------------------------------------+ //| Свинговая точка | //+------------------------------------------------------------------+ struct SwingPivot { bool isHigh; // true, если точка является свинговым максимумом double price; // Цена точки datetime time; // Время бара точки }; //+------------------------------------------------------------------+ //| Активированный сетап QM, ожидающий входа на возврате цены | //+------------------------------------------------------------------+ struct ArmedSetup { bool isActive; // true, пока активированный сетап ожидает входа bool isBull; // true для бычьего QM (покупка) double qmLinePrice; // Цена плеча — уровень входа (линия QM) double headPrice; // Цена головы — уровень отмены сценария и опорная точка стоп-лосса double targetPrice; // Структурная цель (экстремум пробитого свинга) double legPrice; // Пробитый уровень опорной точки Leg для отрисовки BoS datetime shoulderTime; // Время бара левого плеча datetime legTime; // Время бара точки Leg datetime headTime; // Время бара головы datetime bosTime; // Время бара пробоя структуры double shoulderPrice; // Цена левого плеча datetime armedTime; // Время бара активации сетапа (начало срока ожидания) bool lineBroken; // true после закрытия цены за линией QM: первый этап подтверждающего возврата }; //+------------------------------------------------------------------+ //| Запись сделки по тикету | //+------------------------------------------------------------------+ struct TradeRecord { ulong ticket; // Тикет позиции bool isBull; // true для покупки double entryPrice; // Цена исполнения double initialStop; // Начальная цена стоп-лосса double riskDistance; // Расстояние между ценой входа и начальным стоп-лоссом bool partialTaken; // true после частичной фиксации прибыли }; //+------------------------------------------------------------------+ //| Глобальные переменные | //+------------------------------------------------------------------+ CTrade Trade; // Объект исполнения сделок int SymDigits; // Число знаков символа double SymPoint; // Размер пункта символа int AtrHandle = INVALID_HANDLE; // Дескриптор индикатора ATR datetime g_lastBarTime = 0; // Время последнего обработанного бара SwingPivot g_pivots[]; // Чередующийся зигзаг H/L подтверждённых точек int g_maxPivots = 24; // Максимальное число сохраняемых точек зигзага double g_lastLabeledHigh = 0.0; // Цена последнего размеченного свингового максимума datetime g_lastLabeledHighTime = 0; // Время последнего размеченного свингового максимума double g_lastLabeledLow = 0.0; // Цена последнего размеченного свингового минимума datetime g_lastLabeledLowTime = 0; // Время последнего размеченного свингового минимума ArmedSetup g_setup; // Текущий активированный сетап QM datetime g_lastArmedBosTime = 0; // Защита от повторной активации того же паттерна datetime g_lastArmedShoulderTime = 0; // Защита от повторного использования левого плеча TradeRecord g_trades[]; // Записи сделок по тикетам
Начинаем с макроса EA_VERSION и подключения стандартной торговой библиотеки, предоставляющей класс CTrade для отправки и изменения торговых приказов. Затем определяем четыре перечисления, которые превращают переключатели режимов в понятные именованные варианты. Они задают способ расчёта лота, метод Take Profit, режим трейлинг-стопа и разрешённое направление торговли.
Входные параметры объединены в именованные группы, чтобы настраивать советник по смысловым разделам, а не просматривать единый длинный список. Группы охватывают общие настройки, расчёт риска, обнаружение свингов, Stop Loss, Take Profit и сопровождение сделок. Далее определяются три рабочие структуры: SwingPivot хранит подтверждённую точку разворота, ArmedSetup — обнаруженный паттерн, ожидающий входа, вместе с ключевыми уровнями и временем, а TradeRecord — данные каждой открытой позиции по её тикету.
Затем объявляются глобальные переменные, связывающие все компоненты. Торговый объект, сохранённые характеристики символа и дескриптор ATR используются для исполнения сделок и чтения индикатора. Массив зигзага хранит подтверждённые точки, а активированный сетап и массив торговых записей — текущий паттерн и сопровождаемые позиции. Теперь можно определить вспомогательные функции.
Вспомогательные функции: журнал, расчёт объёма и учёт сделок
Несколько служебных функций выполняют базовые операции, от которых зависит остальная программа: выводят сообщения, определяют возможность отрисовки, рассчитывают объём позиции и отслеживают уже открытые сделки.
//+------------------------------------------------------------------+ //| Вывести сообщение с префиксом в журнал | //+------------------------------------------------------------------+ void Log(string message) { //--- Выводить только при включённом журналировании if(InpShowLogs) Print(InpLogPrefix + message); } //+------------------------------------------------------------------+ //| Определить возможность отрисовки объектов | //+------------------------------------------------------------------+ bool VisualsAllowed() { //--- Не выполнять отрисовку при невизуальном тестировании if(MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_VISUAL_MODE)) return false; //--- В остальных случаях разрешить отрисовку return true; } //+------------------------------------------------------------------+ //| Определить открытие нового бара | //+------------------------------------------------------------------+ bool IsNewBar() { //--- Считать время открытия текущего бара datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Сообщить о новом баре при изменении времени открытия if(currentBarTime != g_lastBarTime) { //--- Сохранить время и подтвердить новый бар g_lastBarTime = currentBarTime; return true; } //--- Тот же бар return false; } //+------------------------------------------------------------------+ //| Считать одно значение буфера индикатора по смещению | //+------------------------------------------------------------------+ double IndicatorValue(int handle, int buffer, int shift) { //--- Получить одно значение из буфера double values[]; //--- Вернуть ошибочное значение, если данные не скопированы if(CopyBuffer(handle, buffer, shift, 1, values) < 1) return EMPTY_VALUE; //--- Вернуть скопированное значение return values[0]; } //+------------------------------------------------------------------+ //| Проверить разрешение направления торговли | //+------------------------------------------------------------------+ bool IsDirectionAllowed(bool isBull) { //--- Разрешить оба направления if(InpTradeDirection == TRADE_BOTH) return true; //--- Разрешить только покупки if(InpTradeDirection == TRADE_BUYS_ONLY) return isBull; //--- В остальных случаях разрешить только продажи return !isBull; } //+------------------------------------------------------------------+ //| Рассчитать размер лота по проценту риска и расстоянию до стоп-лосса | //+------------------------------------------------------------------+ double CalcLotsByRisk(double entry, double stop) { //--- Рассчитать сумму риска по балансу и проценту double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * InpRiskPercent / 100.0; //--- Измерить расстояние до стоп-лосса в пунктах double stopPoints = MathAbs(entry - stop) / SymPoint; //--- Завершить работу при нулевом расстоянии до стоп-лосса if(stopPoints <= 0) return 0; //--- Считать стоимость и размер тика double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); //--- Завершить при некорректных параметрах тика if(tickValue <= 0 || tickSize <= 0) return 0; //--- Определить денежную стоимость пункта на лот double valuePerPoint = tickValue / tickSize * SymPoint; //--- Завершить при нулевой стоимости пункта if(valuePerPoint <= 0) return 0; //--- Рассчитать объём позиции по сумме риска и стоимости стоп-лосса double lots = riskMoney / (stopPoints * valuePerPoint); //--- Считать ограничения объёма брокера double volMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double volMax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double volStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); //--- Округлить лот вниз по шагу объёма if(volStep > 0) lots = MathFloor(lots / volStep) * volStep; //--- Ограничить допустимым диапазоном и нормализовать return NormalizeDouble(MathMax(volMin, MathMin(volMax, lots)), 2); } //+------------------------------------------------------------------+ //| Найти индекс записи сделки по тикету | //+------------------------------------------------------------------+ int FindTradeRecord(ulong ticket) { //--- Просмотреть все сохранённые записи for(int i = 0; i < ArraySize(g_trades); i++) //--- Вернуть индекс при совпадении тикета if(g_trades[i].ticket == ticket) return i; //--- Сообщить, что запись не найдена return -1; } //+------------------------------------------------------------------+ //| Подсчитать открытые позиции советника по символу | //+------------------------------------------------------------------+ int CountOurPositions() { //--- Инициализировать счётчик int count = 0; //--- Перебрать открытые позиции for(int i = PositionsTotal() - 1; i >= 0; i--) { //--- Выбрать позицию по индексу ulong ticket = PositionGetTicket(i); //--- Пропустить при ошибке выбора if(ticket == 0 || !PositionSelectByTicket(ticket)) continue; //--- Пропустить другой магический номер if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue; //--- Пропустить другие символы if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; //--- Учесть позицию count++; } //--- Вернуть итоговое значение return count; } //+------------------------------------------------------------------+ //| Добавить новую запись сделки | //+------------------------------------------------------------------+ void AddTradeRecord(ulong ticket, bool isBull, double entry, double stop) { //--- Пропустить пустой или уже отслеживаемый тикет if(ticket == 0 || FindTradeRecord(ticket) >= 0) return; //--- Увеличить массив записей на один элемент int n = ArraySize(g_trades); ArrayResize(g_trades, n + 1); //--- Заполнить новую запись g_trades[n].ticket = ticket; g_trades[n].isBull = isBull; g_trades[n].entryPrice = entry; g_trades[n].initialStop = stop; g_trades[n].riskDistance = MathAbs(entry - stop); g_trades[n].partialTaken = false; } //+------------------------------------------------------------------+ //| Удалить записи закрытых позиций | //+------------------------------------------------------------------+ void PruneTradeRecords() { //--- Перебирать записи с конца for(int i = ArraySize(g_trades) - 1; i >= 0; i--) { //--- Обработать отсутствующую позицию if(!PositionSelectByTicket(g_trades[i].ticket)) { //--- Сдвинуть последующие записи for(int j = i; j < ArraySize(g_trades) - 1; j++) g_trades[j] = g_trades[j + 1]; //--- Уменьшить массив на один элемент ArrayResize(g_trades, ArraySize(g_trades) - 1); } } } //+------------------------------------------------------------------+ //| Добавить в учёт открытые позиции советника, которые ещё не отслеживаются | //+------------------------------------------------------------------+ void SyncTradeRecords() { //--- Перебрать открытые позиции for(int i = PositionsTotal() - 1; i >= 0; i--) { //--- Выбрать позицию по индексу ulong ticket = PositionGetTicket(i); //--- Пропустить при ошибке выбора if(ticket == 0 || !PositionSelectByTicket(ticket)) continue; //--- Пропустить другой магический номер if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue; //--- Пропустить другие символы if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; //--- Пропустить уже отслеживаемые тикеты if(FindTradeRecord(ticket) >= 0) continue; //--- Увеличить массив записей на один элемент int n = ArraySize(g_trades); ArrayResize(g_trades, n + 1); //--- Заполнить добавленную запись данными открытой позиции g_trades[n].ticket = ticket; g_trades[n].isBull = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY); g_trades[n].entryPrice = PositionGetDouble(POSITION_PRICE_OPEN); g_trades[n].initialStop = PositionGetDouble(POSITION_SL); g_trades[n].riskDistance = MathAbs(g_trades[n].entryPrice - g_trades[n].initialStop); //--- Отметить как принятую, чтобы частичное закрытие не сработало повторно g_trades[n].partialTaken = true; } }
Начнём с небольших вспомогательных функций. Log выводит сообщение с префиксом только при включённом журналировании. VisualsAllowed возвращает false во время невизуального тестирования, чтобы не расходовать ресурсы на объекты, которые тестер не показывает. IsNewBar считывает время открытия текущего бара через iTime и возвращает true при появлении нового бара. IndicatorValue получает одно значение индикаторного буфера на заданном смещении с помощью CopyBuffer и возвращает EMPTY_VALUE, если данные не получены.
Далее реализуем проверку направления и расчёт объёма. Функция IsDirectionAllowed принимает признак бычьего сетапа и сопоставляет его с разрешённым направлением торговли. CalcLotsByRisk преобразует заданный процент риска в объём позиции: по цене входа и Stop Loss вычисляется сумма риска от баланса и расстояние до Stop Loss в пунктах. Затем по стоимости и размеру тика определяется денежная стоимость одного пункта на лот, после чего риск делится на стоимость Stop Loss. Полученный объём округляется вниз по шагу брокера и ограничивается допустимыми минимальным и максимальным значениями.
Для учёта собственных сделок добавим несколько функций. FindTradeRecord возвращает индекс записи с заданным тикетом или −1, если запись не найдена. CountOurPositions перебирает открытые позиции через PositionsTotal и PositionGetTicket и учитывает только позиции текущего символа с нашим магическим номером. После исполнения AddTradeRecord добавляет запись с ценой входа, первоначальным Stop Loss и размером риска, а PruneTradeRecords удаляет записи по уже закрытым позициям.
Отдельно обрабатывается перезапуск советника. Если программа повторно загружена на график, где уже есть её позиции, SyncTradeRecords добавляет в учёт ещё не отслеживаемые сделки. Запись восстанавливается по данным открытой позиции, а частичная фиксация прибыли отмечается как уже выполненное, чтобы после перезапуска оно не сработало повторно. Далее создадим функции построения зигзага и подтверждения предыдущего тренда.
Построение зигзага и подтверждение предыдущего тренда
Эти две функции преобразуют поток необработанных точек в чистую чередующуюся структуру и проверяют, существовал ли перед паттерном реальный тренд.
//+------------------------------------------------------------------+ //| Добавить подтверждённую свинговую точку в чередующийся зигзаг | //+------------------------------------------------------------------+ void AddPivotToZigZag(bool isHigh, double price, datetime time) { //--- Получить текущее количество свинговых точек int count = ArraySize(g_pivots); //--- Сразу добавить первую свинговую точку if(count == 0) { //--- Сохранить первую точку и завершить работу ArrayResize(g_pivots, 1); g_pivots[0].isHigh = isHigh; g_pivots[0].price = price; g_pivots[0].time = time; return; } //--- Объединить точки, если новая точка имеет тот же тип, что и последняя if(g_pivots[count - 1].isHigh == isHigh) { //--- Сохранить только более экстремальную из двух точек bool moreExtreme = isHigh ? (price > g_pivots[count - 1].price) : (price < g_pivots[count - 1].price); //--- Заменить последнюю точку, если новая является более экстремальной if(moreExtreme) { //--- Обновить цену и время последней точки g_pivots[count - 1].price = price; g_pivots[count - 1].time = time; } return; } //--- Добавить очередную точку противоположного типа ArrayResize(g_pivots, count + 1); g_pivots[count].isHigh = isHigh; g_pivots[count].price = price; g_pivots[count].time = time; //--- Ограничить размер сохраняемой истории if(ArraySize(g_pivots) > g_maxPivots) { //--- Рассчитать количество старых точек для удаления int drop = ArraySize(g_pivots) - g_maxPivots; //--- Сдвинуть оставшиеся точки в начало массива for(int i = 0; i < ArraySize(g_pivots) - drop; i++) g_pivots[i] = g_pivots[i + drop]; //--- Сократить массив до установленного лимита ArrayResize(g_pivots, g_maxPivots); } } //+------------------------------------------------------------------+ //| Подтвердить предшествующий тренд перед левым плечом | //+------------------------------------------------------------------+ bool IsPriorTrendConfirmed(bool isBull, int shoulderIndex) { //--- Считать проверку пройденной при отключённом фильтре тренда if(!InpRequirePriorTrend) return true; //--- Пропустить проверку, если требуется менее двух точек if(InpTrendPivots < 2) return true; //--- Отсчитать заданное количество точек назад от плеча int oldestIndex = shoulderIndex - InpTrendPivots; //--- Отклонить при недостатке истории перед плечом if(oldestIndex < 0) return false; //--- Сравнить пары однотипных точек через одну до уровня плеча for(int i = oldestIndex; i + 2 <= shoulderIndex; i++) { //--- Получить более раннюю однотипную точку double earlier = g_pivots[i].price; //--- Получить более позднюю однотипную точку double later = g_pivots[i + 2].price; //--- Для предшествующего восходящего тренда потребовать повышения точек (медвежий QM) if(!isBull) { if(!(later > earlier)) return false; } //--- Для предшествующего нисходящего тренда потребовать снижения точек (бычий QM) else { if(!(later < earlier)) return false; } } //--- Все сравниваемые пары образуют правильную ступенчатую структуру return true; }
Для надёжного распознавания Квазимодо история свингов должна представлять собой последовательное чередование максимумов и минимумов без двух максимумов или минимумов подряд. Это обеспечивает функция AddPivotToZigZag, которой передаются тип точки, цена и время бара. Первая точка просто добавляется в массив через ArrayResize. Для последующих точек логика зависит от того, совпадает ли их тип с типом последней сохранённой точки.
Если новая точка имеет тот же тип, что и предыдущая, она не добавляется отдельно. Вместо этого сохраняется более экстремальная точка: более высокий максимум заменяет предыдущий максимум, а более низкий минимум — предыдущий минимум. Такое объединение сводит группу однонаправленных точек к единственному структурно значимому экстремуму. Новая запись добавляется только при чередовании типа. Чтобы массив не рос бесконечно, число точек ограничивается, а при превышении лимита оставшиеся элементы сдвигаются к началу.
Квазимодо является разворотной моделью, поэтому перед ним должен существовать реальный тренд. Функция IsPriorTrendConfirmed получает направление сетапа и индекс левого плеча. Если фильтр тренда отключён или требуется менее двух подтверждающих точек, проверка сразу считается пройденной. В остальных случаях функция отступает на заданное число точек до плеча и сравнивает пары одного типа через два индекса.
Пары сравниваются через два индекса, поскольку в чередующемся зигзаге максимумы и минимумы одного типа расположены именно так. Для медвежьего сетапа, разворачивающего восходящий тренд, каждая последующая точка должна находиться выше предыдущей, подтверждая восходящую структуру. Для бычьего сетапа требуется нисходящая структура. Если хотя бы одна пара нарушает последовательность, сетап отклоняется; тренд подтверждается только при корректном расположении всех сравниваемых точек.
Вспомогательные функции отрисовки: линии, уровни, подписи и маркеры свингов
Для отображения паттерна на графике создадим набор функций, которые при первом вызове формируют нужные объекты, а при последующих лишь перемещают и обновляют их.
//+------------------------------------------------------------------+ //| Создать или переместить сегмент трендовой линии | //+------------------------------------------------------------------+ void DrawTrendline(string name, datetime time1, double price1, datetime time2, double price2, color clr, ENUM_LINE_STYLE style, int width, bool ray = false) { //--- Создать объект, если он отсутствует if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_TREND, 0, time1, price1, time2, price2); //--- Иначе переместить существующий объект else { //--- Переместить обе опорные точки линии ObjectMove(0, name, 0, time1, price1); ObjectMove(0, name, 1, time2, price2); } //--- Задать цвет, стиль и толщину ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_STYLE, style); ObjectSetInteger(0, name, OBJPROP_WIDTH, width); //--- Настроить параметры продолжения линии лучом ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, ray); ObjectSetInteger(0, name, OBJPROP_RAY_LEFT, false); //--- Запретить выделение и оставить объект на переднем плане ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); ObjectSetInteger(0, name, OBJPROP_BACK, false); } //+------------------------------------------------------------------+ //| Нарисовать горизонтальный сегмент между двумя моментами времени | //+------------------------------------------------------------------+ void DrawHLevel(string name, datetime time1, datetime time2, double price, color clr, ENUM_LINE_STYLE style, int width) { //--- Нарисовать горизонтальную трендовую линию на заданной цене DrawTrendline(name, time1, price, time2, price, clr, style, width, false); } //+------------------------------------------------------------------+ //| Создать или обновить текстовую подпись | //+------------------------------------------------------------------+ void DrawText(string name, datetime time, double price, string text, color clr, ENUM_ANCHOR_POINT anchor) { //--- Один раз создать подпись и задать её шрифт и свойства if(ObjectFind(0, name) < 0) { //--- Создать текстовый объект ObjectCreate(0, name, OBJ_TEXT, 0, time, price); ObjectSetString(0, name, OBJPROP_FONT, "Arial Bold"); ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9); ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); } //--- Обновить текст, цвет и привязку ObjectSetString(0, name, OBJPROP_TEXT, text); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor); //--- Переместить подпись ObjectMove(0, name, 0, time, price); } //+------------------------------------------------------------------+ //| Нарисовать свинговую точку с подписью: значок и текст | //+------------------------------------------------------------------+ void DrawSwingMarker(bool isHigh, datetime time, double price, string label, color clr) { //--- Сформировать уникальные имена объектов для этой точки string tag = (isHigh ? "QM_SWH_" : "QM_SWL_") + IntegerToString((int)time); string iconName = tag + "_i"; string textName = tag + "_t"; //--- Один раз создать круглый значок точки if(ObjectFind(0, iconName) < 0) { //--- Сформировать значок Wingdings ObjectCreate(0, iconName, OBJ_TEXT, 0, time, price); ObjectSetString(0, iconName, OBJPROP_FONT, "Wingdings"); ObjectSetString(0, iconName, OBJPROP_TEXT, CharToString(174)); ObjectSetInteger(0, iconName, OBJPROP_FONTSIZE, 9); ObjectSetInteger(0, iconName, OBJPROP_COLOR, clr); ObjectSetInteger(0, iconName, OBJPROP_ANCHOR, isHigh ? ANCHOR_RIGHT_LOWER : ANCHOR_RIGHT_UPPER); ObjectSetInteger(0, iconName, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, iconName, OBJPROP_HIDDEN, true); } //--- Один раз создать подпись H/HH/LH или L/LL/HL if(ObjectFind(0, textName) < 0) { //--- Сформировать текст подписи ObjectCreate(0, textName, OBJ_TEXT, 0, time, price); ObjectSetString(0, textName, OBJPROP_FONT, "Arial Bold"); ObjectSetString(0, textName, OBJPROP_TEXT, label); ObjectSetInteger(0, textName, OBJPROP_FONTSIZE, 9); ObjectSetInteger(0, textName, OBJPROP_COLOR, clr); ObjectSetInteger(0, textName, OBJPROP_ANCHOR, isHigh ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER); ObjectSetInteger(0, textName, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, textName, OBJPROP_HIDDEN, true); } //--- Перерисовать график ChartRedraw(0); }
Основой визуализации служит функция DrawTrendline, создающая или перемещающая отдельный отрезок трендовой линии. Ей передаются имя объекта, две опорные точки и свойства отображения, включая необязательный режим луча. Если объект ещё не существует, он создаётся через ObjectCreate; иначе обе его опорные точки перемещаются функцией ObjectMove. Затем задаются цвет, стиль и толщина, а возможность выделения отключается, чтобы линию нельзя было случайно перетащить.
Функция DrawHLevel строит горизонтальный отрезок между двумя точками. Чтобы не повторять код работы с объектами, вызывается DrawTrendline с одинаковой ценой обеих опорных точек.
Для подписей используется функция DrawText. Она получает имя, положение на графике, выводимый текст, цвет и точку привязки. При первом вызове создаётся текстовый объект и один раз задаётся шрифт; при последующих вызовах обновляются текст, цвет и привязка, а объект перемещается в нужную позицию.
Функция DrawSwingMarker размещает подписанный маркер у подтверждённой точки. По времени точки формируются два уникальных имени, затем создаются небольшой круглый значок Wingdings и текстовая подпись, например H, HH или LH. Маркеры максимумов располагаются над баром, минимумов — под ним и окрашиваются по типу. После создания объектов график обновляется через ChartRedraw. Эти функции будут использоваться для визуального подтверждения и отрисовки сетапа.
Отрисовка структуры QM, предыдущего тренда и торговых уровней
На основе низкоуровневых функций создадим три составные функции, отображающие полную картину: сам паттерн, предшествующий тренд и открытую сделку.
//+------------------------------------------------------------------+ //| Нарисовать полную структуру QM для активированного сетапа | //+------------------------------------------------------------------+ void DrawQMStructure() { //--- Пропустить отрисовку, если визуализация отключена if(!(InpDrawVisuals && VisualsAllowed())) return; //--- Выбрать цвет направления и сформировать базовое имя объектов color clr = g_setup.isBull ? InpBullColor : InpBearColor; string id = IntegerToString((int)g_setup.bosTime); string base = "QM_STRUCT_" + id; //--- Нарисовать сегмент зигзага от плеча к опорной точке Leg DrawTrendline(base + "_l1", g_setup.shoulderTime, g_setup.shoulderPrice, g_setup.legTime, g_setup.legPrice, clr, STYLE_SOLID, 2); //--- Нарисовать сегмент зигзага от Leg к голове DrawTrendline(base + "_l2", g_setup.legTime, g_setup.legPrice, g_setup.headTime, g_setup.headPrice, clr, STYLE_SOLID, 2); //--- Нарисовать сегмент зигзага от головы к точке BoS DrawTrendline(base + "_l3", g_setup.headTime, g_setup.headPrice, g_setup.bosTime, g_setup.targetPrice, clr, STYLE_SOLID, 2); //--- Нарисовать пробитый уровень Leg: линию BoS DrawHLevel(base + "_bosln", g_setup.legTime, g_setup.bosTime, g_setup.legPrice, clr, STYLE_DOT, 1); //--- Подписать левое плечо DrawText(base + "_ls", g_setup.shoulderTime, g_setup.shoulderPrice, "Left Shoulder", clr, g_setup.isBull ? ANCHOR_UPPER : ANCHOR_LOWER); //--- Подписать голову DrawText(base + "_head", g_setup.headTime, g_setup.headPrice, "Head", clr, g_setup.isBull ? ANCHOR_LOWER : ANCHOR_UPPER); //--- Подписать точку Leg DrawText(base + "_leg", g_setup.legTime, g_setup.legPrice, g_setup.isBull ? "Leg high" : "Leg low", clr, g_setup.isBull ? ANCHOR_LOWER : ANCHOR_UPPER); //--- Подписать пробой структуры DrawText(base + "_bos", g_setup.bosTime, g_setup.legPrice, " BOS", clr, g_setup.isBull ? ANCHOR_LOWER : ANCHOR_UPPER); //--- Рассчитать правую границу линии QM с учётом периода ожидания datetime lineEnd = g_setup.bosTime + (datetime)(PeriodSeconds(_Period) * (InpMaxWaitBars + 5)); //--- Нарисовать линию входа QM на уровне плеча DrawHLevel(base + "_qml", g_setup.shoulderTime, lineEnd, g_setup.qmLinePrice, InpQmLineColor, STYLE_DASH, 1); //--- Добавить подпись к линии входа QM DrawText(base + "_qmltxt", lineEnd, g_setup.qmLinePrice, " QM line (entry)", InpQmLineColor, ANCHOR_LEFT); //--- Перерисовать график ChartRedraw(0); } //+------------------------------------------------------------------+ //| Нарисовать линию предшествующего тренда перед плечом | //+------------------------------------------------------------------+ void DrawPriorTrend(int shoulderIndex, bool isBull, string baseName, color clr) { //--- Начать линию за заданное количество точек до плеча int startIndex = shoulderIndex - InpTrendPivots; //--- Завершить при недостатке истории if(startIndex < 0) return; //--- Соединить все точки до плеча for(int i = startIndex; i < shoulderIndex; i++) { //--- Нарисовать один соединительный сегмент string segName = baseName + "_trend" + IntegerToString(i); DrawTrendline(segName, g_pivots[i].time, g_pivots[i].price, g_pivots[i + 1].time, g_pivots[i + 1].price, clr, STYLE_DOT, 1); } //--- Разместить подпись над максимумом или под минимумом ENUM_ANCHOR_POINT labelAnchor = g_pivots[startIndex].isHigh ? ANCHOR_LOWER : ANCHOR_UPPER; //--- Подписать соединительную линию по направлению тренда DrawText(baseName + "_trendlbl", g_pivots[startIndex].time, g_pivots[startIndex].price, isBull ? "Downtrend" : "Uptrend", clr, labelAnchor); //--- Перерисовать график ChartRedraw(0); } //+------------------------------------------------------------------+ //| Нарисовать линии входа, стоп-лосса и цели, а также стрелку входа | //+------------------------------------------------------------------+ void DrawEntryLevels(bool isBull, datetime time, double entry, double stop, double takeProfit) { //--- Сформировать базовое имя и правую границу уровней string id = "QM_ENT_" + IntegerToString((int)time); datetime endTime = time + (datetime)(PeriodSeconds(_Period) * 30); //--- Нарисовать линию входа DrawHLevel(id + "_e", time, endTime, entry, clrDodgerBlue, STYLE_SOLID, 2); //--- Нарисовать линию стоп-лосса DrawHLevel(id + "_sl", time, endTime, stop, C'220,60,60', STYLE_DASH, 1); //--- Нарисовать линию цели DrawHLevel(id + "_tp", time, endTime, takeProfit, C'0,200,80', STYLE_DASH, 1); //--- Получить экстремумы последнего закрытого бара для размещения стрелки string arrowName = id + "_a"; double barHigh = iHigh(_Symbol, _Period, 1); double barLow = iLow(_Symbol, _Period, 1); //--- Один раз создать стрелку входа if(ObjectFind(0, arrowName) < 0) { //--- Разместить стрелку направления у экстремума бара ObjectCreate(0, arrowName, OBJ_ARROW, 0, time, isBull ? barLow : barHigh); ObjectSetInteger(0, arrowName, OBJPROP_ARROWCODE, isBull ? 233 : 234); ObjectSetInteger(0, arrowName, OBJPROP_COLOR, isBull ? InpBullColor : InpBearColor); ObjectSetInteger(0, arrowName, OBJPROP_WIDTH, 3); ObjectSetInteger(0, arrowName, OBJPROP_ANCHOR, isBull ? ANCHOR_TOP : ANCHOR_BOTTOM); ObjectSetInteger(0, arrowName, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, arrowName, OBJPROP_HIDDEN, true); } //--- Перерисовать график ChartRedraw(0); }
В этом фрагменте функция DrawQMStructure отображает весь паттерн после перевода сетапа в состояние ожидания входа. Сначала выбираются направление и цвет, а по времени пробоя структуры формируется базовое имя, обеспечивающее уникальный префикс всех объектов. Затем DrawTrendline рисует три участка зигзага: от плеча к точке Leg, от точки Leg к голове и от головы к точке пробоя структуры. Пробитый уровень Leg отмечается пунктирной линией через DrawHLevel, а точки Left Shoulder, Head, Leg и пробой структуры подписываются функцией DrawText.
Чтобы показать уровень срабатывания сделки, линия QM продлевается от плеча до правой границы окна ожидания. Время рассчитывается по длительности периода и максимальному числу баров ожидания через PeriodSeconds. Уровень по цене плеча отображается штриховой линией, подписывается как линия входа, после чего график обновляется через ChartRedraw.
Функция DrawPriorTrend рисует соединительную линию за плечом и показывает тренд, который разворачивает паттерн. Ей передаются индекс плеча, направление сетапа, базовое имя и цвет. Построение начинается за заданное число точек до плеча; соседние точки соединяются пунктирными отрезками, а вся последовательность подписывается как восходящий или нисходящий тренд.
Функция DrawEntryLevels отображает сделку после её открытия. Ей передаются направление, время входа, цены входа, Stop Loss и цели. Эти три уровня рисуются горизонтальными линиями. Затем у экстремума только что закрывшегося бара размещается стрелка направления: ниже бара для покупки и выше — для продажи; экстремум считывается функциями iHigh и iLow. В завершение график обновляется.
Обнаружение структуры QM и перевод сетапа в состояние ожидания входа
На этом этапе паттерн распознаётся непосредственно. Последние точки проверяются на соответствие структуре Квазимодо; при выполнении условий сетап сохраняется и отображается на графике.
//+------------------------------------------------------------------+ //| Активировать обнаруженный сетап QM и нарисовать его структуру | //+------------------------------------------------------------------+ void ArmSetup(bool isBull, const SwingPivot &shoulder, const SwingPivot &leg, const SwingPivot &head, const SwingPivot &bos, int shoulderIndex) { //--- Соблюдать правило одного активного сетапа if(!InpAllowMultipleTrades && CountOurPositions() > 0) return; //--- Активировать сетап и сохранить его направление g_setup.isActive = true; g_setup.isBull = isBull; //--- Сохранить линию QM и уровень входа на цене плеча g_setup.qmLinePrice = shoulder.price; g_setup.shoulderPrice = shoulder.price; //--- Сохранить уровни головы, цели и опорной точки Leg g_setup.headPrice = head.price; g_setup.targetPrice = bos.price; g_setup.legPrice = leg.price; //--- Сохранить время свинговых точек g_setup.shoulderTime = shoulder.time; g_setup.legTime = leg.time; g_setup.headTime = head.time; g_setup.bosTime = bos.time; //--- Сохранить время бара активации и сбросить флаг подтверждающего возврата g_setup.armedTime = iTime(_Symbol, _Period, 0); g_setup.lineBroken = false; //--- Запомнить паттерн, чтобы не активировать его повторно g_lastArmedBosTime = bos.time; g_lastArmedShoulderTime = shoulder.time; //--- Нарисовать структуру QM DrawQMStructure(); //--- При необходимости нарисовать линию предшествующего тренда if(InpRequirePriorTrend && InpDrawVisuals && VisualsAllowed()) DrawPriorTrend(shoulderIndex, isBull, "QM_STRUCT_" + IntegerToString((int)bos.time), InpTrendColor); //--- Записать сведения об активированном сетапе в журнал Log((isBull ? "BULLISH" : "BEARISH") + " QM armed | QM line=" + DoubleToString(g_setup.qmLinePrice, SymDigits) + " Head=" + DoubleToString(g_setup.headPrice, SymDigits) + " Target=" + DoubleToString(g_setup.targetPrice, SymDigits) + " -> waiting for the retrace to the QM line."); } //+------------------------------------------------------------------+ //| Проверить последние четыре свинговые точки на наличие паттерна QM | //+------------------------------------------------------------------+ void CheckForQMPattern() { //--- Для проверки требуется не менее четырёх точек int count = ArraySize(g_pivots); if(count < 4) return; //--- Получить последние четыре точки: плечо, Leg, голову и BoS SwingPivot shoulder = g_pivots[count - 4]; SwingPivot leg = g_pivots[count - 3]; SwingPivot head = g_pivots[count - 2]; SwingPivot bos = g_pivots[count - 1]; //--- Проверить медвежий QM: H, L, H, L if(shoulder.isHigh && !leg.isHigh && head.isHigh && !bos.isHigh) { //--- Потребовать Head > Shoulder, Leg < Shoulder и BoS < Leg if(head.price > shoulder.price && leg.price < shoulder.price && bos.price < leg.price) { //--- Активировать новый разрешённый сетап без повторного плеча при подтверждённом тренде if(bos.time != g_lastArmedBosTime && IsDirectionAllowed(false) && (!InpSkipSharedShoulder || shoulder.time != g_lastArmedShoulderTime) && IsPriorTrendConfirmed(false, count - 4)) ArmSetup(false, shoulder, leg, head, bos, count - 4); } //--- Завершить проверку после обработки медвежьей структуры return; } //--- Проверить бычий QM: L, H, L, H if(!shoulder.isHigh && leg.isHigh && !head.isHigh && bos.isHigh) { //--- Потребовать Head < Shoulder, Leg > Shoulder и BoS > Leg if(head.price < shoulder.price && leg.price > shoulder.price && bos.price > leg.price) { //--- Активировать новый разрешённый сетап без повторного плеча при подтверждённом тренде if(bos.time != g_lastArmedBosTime && IsDirectionAllowed(true) && (!InpSkipSharedShoulder || shoulder.time != g_lastArmedShoulderTime) && IsPriorTrendConfirmed(true, count - 4)) ArmSetup(true, shoulder, leg, head, bos, count - 4); } } }
Сначала определим функцию ArmSetup, вызываемую сразу после обнаружения корректного паттерна. Ей передаются направление, четыре точки структуры — плечо, точка Leg, голова и пробой структуры — и индекс плеча. Вначале соблюдается правило одной сделки: если множественные сделки запрещены и позиция уже открыта, что проверяется через CountOurPositions, функция завершает работу.
После успешной проверки сетап активируется и переходит в режим ожидания входа. Цена левого плеча сохраняется как линия QM и уровень входа, голова — как уровень инвалидации и опорная точка стоп-лосса, структурная цель — как экстремум пробитого свинга, а уровень Leg — для построения линии BoS. Также сохраняется время активации сетапа, от которого отсчитывается окно ожидания возврата, сбрасывается флаг lineBroken для режима входа после подтверждающего возврата через линию QM, а время BoS и левого плеча запоминается, чтобы не активировать тот же паттерн повторно. После этого программа отрисовывает структуру и записывает сведения о сетапе в журнал.
Решение о переводе в состояние ожидания входа принимает функция CheckForQMPattern, запускаемая после добавления каждой новой точки в зигзаг. Она считывает последние четыре точки — плечо, точка Leg, голову и пробой структуры — и проверяет их на соответствие двум вариантам QM.
Медвежий Квазимодо формируется последовательностью «максимум — минимум — максимум — минимум». Голова должна располагаться выше левого плеча, снимая ликвидность над ним; точка Leg находится ниже плеча, а пробой структуры подтверждается закрытием цены ниже минимума Leg. Для бычьего варианта условия зеркальны: «минимум — максимум — минимум — максимум», голова ниже плеча, точка Leg выше него, а пробой структуры подтверждается закрытием цены выше максимума Leg. Совпадения цен недостаточно: дополнительно проверяются новизна сетапа, разрешённое направление торговли, отсутствие повторного использования плеча и наличие предшествующего тренда.
//+------------------------------------------------------------------+ //| Обнаружить подтверждённую свинговую точку и добавить её в зигзаг | //+------------------------------------------------------------------+ void DetectSwingPivot() { //--- Ограничить глубину проверки минимум одним баром int lookback = InpSwingLookback; if(lookback < 1) lookback = 1; //--- Убедиться, что баров достаточно для центральной свинговой точки if(iBars(_Symbol, _Period) < lookback * 2 + 2) return; //--- Получить бар-кандидат: справа от него уже закрыто заданное число баров int pivotShift = lookback + 1; datetime pivotTime = iTime(_Symbol, _Period, pivotShift); double pivotHigh = iHigh(_Symbol, _Period, pivotShift); double pivotLow = iLow(_Symbol, _Period, pivotShift); //--- Сначала считать кандидата одновременно максимумом и минимумом bool isHigh = true, isLow = true; //--- Сравнить кандидата с барами по обе стороны for(int j = 1; j <= lookback; j++) { //--- Отклонить максимум, если соседний бар выше или равен ему if(iHigh(_Symbol, _Period, pivotShift - j) >= pivotHigh || iHigh(_Symbol, _Period, pivotShift + j) >= pivotHigh) isHigh = false; //--- Отклонить минимум, если соседний бар ниже или равен ему if(iLow(_Symbol, _Period, pivotShift - j) <= pivotLow || iLow(_Symbol, _Period, pivotShift + j) <= pivotLow) isLow = false; } //--- Обработать подтверждённый свинговый максимум if(isHigh && pivotTime != g_lastLabeledHighTime) { //--- Выбрать подпись H/HH/LH и цвет string label; color clr = InpSwingHighColor; if(g_lastLabeledHigh <= 0) label = "H"; else if(pivotHigh > g_lastLabeledHigh) label = "HH"; else { label = "LH"; clr = InpSwingLowColor; } //--- Нарисовать свинговую точку, если визуализация включена if(InpShowSwingMarkers && InpDrawVisuals && VisualsAllowed()) DrawSwingMarker(true, pivotTime, pivotHigh, label, clr); //--- Запомнить максимум и добавить его в зигзаг g_lastLabeledHigh = pivotHigh; g_lastLabeledHighTime = pivotTime; AddPivotToZigZag(true, pivotHigh, pivotTime); //--- Повторно проверить паттерн с новой точкой CheckForQMPattern(); } //--- Обработать подтверждённый свинговый минимум if(isLow && pivotTime != g_lastLabeledLowTime) { //--- Выбрать подпись L/LL/HL и цвет string label; color clr = InpSwingLowColor; if(g_lastLabeledLow <= 0) label = "L"; else if(pivotLow < g_lastLabeledLow) label = "LL"; else { label = "HL"; clr = InpSwingHighColor; } //--- Нарисовать свинговую точку, если визуализация включена if(InpShowSwingMarkers && InpDrawVisuals && VisualsAllowed()) DrawSwingMarker(false, pivotTime, pivotLow, label, clr); //--- Запомнить минимум и добавить его в зигзаг g_lastLabeledLow = pivotLow; g_lastLabeledLowTime = pivotTime; AddPivotToZigZag(false, pivotLow, pivotTime); //--- Повторно проверить паттерн с новой точкой CheckForQMPattern(); } }
Функция DetectSwingPivot находит подтверждённую точку разворота и передаёт её в зигзаг. Бар считается свинговым максимумом, только если ни один бар в заданном окне слева и справа не достигает такой же высоты; для свингового минимума ни один соседний бар не должен опускаться так же низко. Поскольку требуются закрытые бары с обеих сторон, последний бар оценить нельзя: кандидат выбирается со смещением, обеспечивающим полный набор закрытых баров справа. Глубина проверки ограничивается минимум одним баром, а при недостатке истории функция завершается; количество истории проверяется через iBars.
Для проверки кандидата его максимум и минимум считываются функциями iHigh и iLow, после чего он предварительно считается одновременно максимумом и минимумом. Затем соседние бары слева и справа сравниваются с кандидатом. Если сосед достигает такой же высоты, признак свингового максимума снимается; если такой же глубины — снимается признак минимума. Оставшийся после цикла признак определяет подтверждённую точку.
После подтверждения свингового максимума, если этот бар ещё не был размечен, его метка определяется сравнением с последним сохранённым максимумом. Первый максимум обозначается H, более высокий — HH, более низкий — LH; цвет также меняется для наглядности структуры. При включённой визуализации DrawSwingMarker рисует маркер, значение сохраняется как последний максимум и передаётся в AddPivotToZigZag. Сразу после этого CheckForQMPattern повторно проверяет структуру, чтобы новая точка могла немедленно завершить сетап.
Свинговый минимум обрабатывается зеркально. Первый минимум обозначается L, более низкий — LL, а более высокий — HL. Точка отображается, сохраняется как последний минимум, добавляется в зигзаг, после чего проверка паттерна запускается снова.
Подключение логики к обработчикам событий
После создания всех компонентов добавим логику в три стандартных обработчика терминала: запуск, завершение работы и поступление каждого тика.
//+------------------------------------------------------------------+ //| Функция инициализации советника | //+------------------------------------------------------------------+ int OnInit() { //--- Сохранить число знаков и размер пункта символа SymDigits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); SymPoint = _Point; //--- Настроить торговый объект Trade.SetExpertMagicNumber(InpMagicNumber); Trade.SetDeviationInPoints(20); //--- Создать дескриптор ATR AtrHandle = iATR(_Symbol, _Period, InpAtrPeriod); //--- Завершить инициализацию ошибкой при некорректном дескрипторе if(AtrHandle == INVALID_HANDLE) { //--- Сообщить об ошибке и прервать инициализацию Log("ERROR: failed to create the ATR handle."); return INIT_FAILED; } //--- Очистить массивы точек и сделок и сбросить активированный сетап ArrayResize(g_pivots, 0); ArrayResize(g_trades, 0); g_setup.isActive = false; //--- Добавить в учёт уже открытые позиции советника SyncTradeRecords(); //--- Сохранить время последнего бара g_lastBarTime = iTime(_Symbol, _Period, 0); //--- Сообщить о готовности Log("Quasimodo Pattern EA v" + EA_VERSION + " ready on " + _Symbol + " (" + EnumToString((ENUM_TIMEFRAMES)_Period) + ") | Magic " + IntegerToString(InpMagicNumber)); //--- Сообщить об успешной инициализации return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Функция деинициализации советника | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Освободить дескриптор ATR if(AtrHandle != INVALID_HANDLE) IndicatorRelease(AtrHandle); //--- Удалить графические объекты только при удалении советника или закрытии графика if(reason == REASON_REMOVE || reason == REASON_CHARTCLOSE || reason == REASON_CLOSE) ObjectsDeleteAll(0, "QM_"); //--- Очистить комментарий графика Comment(""); } //+------------------------------------------------------------------+ //| Обработчик тиков советника | //+------------------------------------------------------------------+ void OnTick() { //--- Выполнить побарную логику один раз при появлении нового бара if(IsNewBar()) { //--- Обнаружить свинги, проверить паттерн и условия входа DetectSwingPivot(); CheckForQMPattern(); } }
В OnInit программа подготавливается к работе на текущем графике. Сохраняются число знаков символа и размер пункта, чтобы позже нормализовать цены, торговому объекту задаются магический номер и допустимое отклонение, а через iATR создаётся дескриптор ATR. Если возвращён INVALID_HANDLE, ошибка записывается в журнал и функция возвращает INIT_FAILED. Иначе очищаются массивы точек и сделок и сбрасывается активированный сетап. Затем SyncTradeRecords подхватывает уже открытые позиции советника, задаётся время последнего бара и возвращается INIT_SUCCEEDED.
В OnDeinit освобождается дескриптор ATR через IndicatorRelease. Объекты графика удаляются функцией ObjectsDeleteAll только при фактическом удалении советника или закрытии графика, поэтому перекомпиляция и смена таймфрейма сохраняют разметку. Комментарии графика также очищаются.
В показанном фрагменте OnTick на каждом новом баре вызываются DetectSwingPivot и CheckForQMPattern. Логика работает по закрытым барам, а не на каждом тике, поэтому свинговые экстремумы остаются стабильными, а программа не реагирует на шум внутри ещё не сформированной свечи. Проверку условий входа для активированного сетапа и управление открытыми позициями мы определим далее.

На графике видно, что сетапы подтверждаются и активируются. Следующий этап — открытие сделок при возврате к линиям входа. В нашем варианте требуется подтверждение разворота; альтернативно можно потребовать, чтобы цена вошла в разворотную зону на заданную глубину, а не просто коснулась линии.
Подтверждение входа и открытие позиции
Теперь превратим активированный сетап в открытую сделку. Рассчитаем Stop Loss и цель, проверим потенциальную доходность, определим объём позиции и подтвердим вход по закрытому бару до отправки ордера.
//+------------------------------------------------------------------+ //| Рассчитать объём, сформировать SL/TP, проверить и открыть сделку QM | //+------------------------------------------------------------------+ void OpenTrade(bool isBull, double entryLevel) { //--- Соблюдать правило одной открытой позиции if(!InpAllowMultipleTrades && CountOurPositions() > 0) return; //--- Преобразовать буфер стоп-лосса в цену и получить минимальную дистанцию по ATR double stopBuffer = InpStopBufferPoints * SymPoint; double atr = (InpUseAtrStopFloor ? IndicatorValue(AtrHandle, 0, 1) : 0.0); if(atr == EMPTY_VALUE) atr = 0.0; //--- Объявить рабочие значения приказа double entry, stop, takeProfit; ENUM_ORDER_TYPE orderType; ENUM_POSITION_TYPE posType; //--- Сформировать ордер на покупку if(isBull) { //--- Использовать цену Ask для покупки entry = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), SymDigits); //--- Установить структурный стоп-лосс ниже головы double structStop = g_setup.headPrice - stopBuffer; //--- Установить минимальный стоп-лосс по ATR ниже цены входа double atrStop = (atr > 0) ? entry - InpAtrStopMultiplier * atr : structStop; //--- Выбрать более широкий из двух стоп-лоссов stop = MathMin(structStop, atrStop); //--- Задать типы ордера и позиции orderType = ORDER_TYPE_BUY; posType = POSITION_TYPE_BUY; } //--- Сформировать ордер на продажу else { //--- Использовать цену Bid для продажи entry = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), SymDigits); //--- Установить структурный стоп-лосс выше головы double structStop = g_setup.headPrice + stopBuffer; //--- Установить минимальный стоп-лосс по ATR выше цены входа double atrStop = (atr > 0) ? entry + InpAtrStopMultiplier * atr : structStop; //--- Выбрать более широкий из двух стоп-лоссов stop = MathMax(structStop, atrStop); //--- Задать типы ордера и позиции orderType = ORDER_TYPE_SELL; posType = POSITION_TYPE_SELL; } //--- Нормализовать цену стоп-лосса stop = NormalizeDouble(stop, SymDigits); //--- Рассчитать расстояние риска double riskDistance = MathAbs(entry - stop); //--- Отменить вход при некорректном расстоянии риска if(riskDistance <= 0) { CancelArmedSetup("invalid risk distance"); return; } //--- Рассчитать тейк-профит по соотношению прибыли к риску if(InpTakeProfitMode == TP_REWARD_RISK) takeProfit = isBull ? entry + InpRewardRiskRatio * riskDistance : entry - InpRewardRiskRatio * riskDistance; //--- Иначе использовать структурную цель else takeProfit = g_setup.targetPrice; //--- Нормализовать цену тейк-профита takeProfit = NormalizeDouble(takeProfit, SymDigits); //--- Измерить расстояние до цели double rewardDistance = MathAbs(takeProfit - entry); //--- Отклонить сетап, если соотношение прибыли к риску ниже минимума if(riskDistance > 0 && (rewardDistance / riskDistance) < InpMinRewardRisk) { //--- Отменить и сообщить о недостаточном значении CancelArmedSetup("R:R " + DoubleToString(rewardDistance / riskDistance, 2) + " below minimum " + DoubleToString(InpMinRewardRisk, 2)); return; } //--- Рассчитать объём позиции в выбранном режиме double lots = (InpLotSizingMode == LOTS_FIXED) ? InpFixedLots : CalcLotsByRisk(entry, stop); //--- Считать ограничения объёма брокера double volMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double volMax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double volStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); //--- Округлить лот вниз по шагу объёма if(volStep > 0) lots = MathFloor(lots / volStep) * volStep; //--- Ограничить объём допустимым диапазоном и нормализовать lots = MathMax(volMin, MathMin(volMax, lots)); lots = NormalizeDouble(lots, 2); //--- Отменить при ошибке расчёта лота if(lots <= 0) { CancelArmedSetup("lot calc error"); return; } //--- Отправить рыночный приказ if(Trade.PositionOpen(_Symbol, orderType, lots, entry, stop, takeProfit, InpOrderComment)) { //--- Добавить новую сделку в учёт ulong ticket = Trade.ResultOrder(); AddTradeRecord(ticket, isBull, entry, stop); //--- Получить время текущего бара для отрисовки datetime now = iTime(_Symbol, _Period, 0); //--- Нарисовать завершённый возврат и уровни входа if(InpDrawVisuals && VisualsAllowed()) { //--- Соединить экстремум BoS с точкой входа color clr = isBull ? InpBullColor : InpBearColor; string retraceName = "QM_STRUCT_" + IntegerToString((int)g_setup.bosTime) + "_retr"; DrawTrendline(retraceName, g_setup.bosTime, g_setup.targetPrice, now, entry, clr, STYLE_DOT, 1); //--- Нарисовать уровни входа, стоп-лосса и цели DrawEntryLevels(isBull, now, entry, stop, takeProfit); } //--- Записать сведения об исполнении Log((isBull ? "BUY" : "SELL") + " filled @ " + DoubleToString(entry, SymDigits) + " SL=" + DoubleToString(stop, SymDigits) + " TP=" + DoubleToString(takeProfit, SymDigits) + " lots=" + DoubleToString(lots, 2) + " (R:R " + DoubleToString(rewardDistance / riskDistance, 2) + ")"); } //--- Сообщить об ошибке открытия позиции else Log("Open failed: " + Trade.ResultRetcodeDescription()); //--- Снять сетап с ожидания входа, сохранив его разметку g_setup.isActive = false; } //+------------------------------------------------------------------+ //| Подтвердить вход по последнему закрытому бару с индексом 1 | //+------------------------------------------------------------------+ void CheckArmedSetupForEntry() { //--- Получить предыдущий закрытый бар и буфер уровня входа double priorClose = iClose(_Symbol, _Period, 1); double buffer = InpEntryBufferPoints * SymPoint; //--- Отменить после истечения окна ожидания int barsElapsed = iBarShift(_Symbol, _Period, g_setup.armedTime); if(barsElapsed > InpMaxWaitBars) { CancelArmedSetup("expired (no retrace in time)"); return; } //--- Соблюдать правило одной открытой позиции if(!InpAllowMultipleTrades && CountOurPositions() > 0) return; //--- Обработать бычий сетап if(g_setup.isBull) { //--- Отменить сетап, если закрытие пробило голову вниз if(priorClose < g_setup.headPrice) { CancelArmedSetup("invalidated (Head broken on close)"); return; } //--- Войти сразу после закрытия за линией if(!InpWaitForRejectionClose) { //--- Открыть покупку, если закрытие находится на линии QM или ниже неё if(priorClose <= g_setup.qmLinePrice - buffer) OpenTrade(true, g_setup.qmLinePrice); } //--- Иначе потребовать подтверждающее закрытие обратно через линию QM else { //--- Этап 1: отметить пробой, когда закрытие проходит ниже линии if(!g_setup.lineBroken) { //--- Зафиксировать пробой линии вниз if(priorClose <= g_setup.qmLinePrice) g_setup.lineBroken = true; } //--- Этап 2: открыть покупку, когда последующее закрытие вернётся выше линии else if(priorClose >= g_setup.qmLinePrice + buffer) OpenTrade(true, g_setup.qmLinePrice); } } //--- Обработать медвежий сетап else { //--- Отменить сетап, если закрытие пробило голову вверх if(priorClose > g_setup.headPrice) { CancelArmedSetup("invalidated (Head broken on close)"); return; } //--- Войти сразу после закрытия за линией if(!InpWaitForRejectionClose) { //--- Открыть продажу, если закрытие находится на линии QM или выше неё if(priorClose >= g_setup.qmLinePrice + buffer) OpenTrade(false, g_setup.qmLinePrice); } //--- Иначе потребовать подтверждающее закрытие обратно через линию QM else { //--- Этап 1: отметить пробой, когда закрытие проходит выше линии if(!g_setup.lineBroken) { //--- Зафиксировать пробой линии вверх if(priorClose >= g_setup.qmLinePrice) g_setup.lineBroken = true; } //--- Этап 2: открыть продажу, когда последующее закрытие вернётся ниже линии else if(priorClose <= g_setup.qmLinePrice - buffer) OpenTrade(false, g_setup.qmLinePrice); } } }
Функция OpenTrade рассчитывает объём, проверяет условия и отправляет приказ после подтверждения входа. Ей передаются направление и уровень входа, после чего сразу применяется правило одной сделки. Цена покупки берётся по Ask, продажи — по Bid и нормализуется через NormalizeDouble.
Stop Loss выбирается из двух вариантов, причём используется более удалённый от входа. Структурный Stop Loss размещается с буфером за головой, поскольку закрытие обратно через голову отменяет паттерн. Минимальная дистанция по ATR располагается на заданном количестве ATR от входа и считывается через IndicatorValue. Для покупки выбирается более низкий из двух уровней, для продажи — более высокий, поэтому слишком близкий структурный Stop Loss расширяется как минимум до волатильностного порога.
После определения риска рассчитывается Take Profit. В режиме соотношения прибыли к риску цель проецируется на фиксированное число рисков от входа; в структурном режиме используется сохранённый экстремум пробитого свинга. Затем вычисляется потенциальная прибыль, и сетап отклоняется через CancelArmedSetup, если соотношение прибыль/риск (R:R) ниже минимального.
Объём определяется выбранным режимом: фиксированный лот либо расчёт по риску через CalcLotsByRisk. Значение округляется вниз по шагу объёма брокера и ограничивается допустимым диапазоном; неположительный результат отменяет вход. Рыночный приказ отправляется методом PositionOpen торгового объекта. При успехе AddTradeRecord сохраняет сделку, на графике отображаются точка возврата и торговые уровни, а исполнение записывается в журнал. При ошибке выводится причина. В обоих случаях сетап снимается с ожидания входа, но его разметка остаётся на графике.
Функция CheckArmedSetupForEntry вызывается на каждом новом баре, пока сетап активен. Она считывает закрытие последнего завершённого бара через iClose и переводит буфер входа в цену. Если с момента перевода в состояние ожидания входа прошло слишком много баров, что определяется через iBarShift, сетап отменяется как просроченный. Бычий сетап также отменяется при закрытии ниже головы, а медвежий — при закрытии выше неё, поскольку это окончательно инвалидирует структуру.
Способ входа зависит от параметра подтверждения отбоя. В прямом режиме покупка открывается, когда закрытие достигает линии QM или располагается ниже неё с учётом буфера; продажа — при закрытии на линии или выше неё. В режиме отбоя используется два этапа: сначала фиксируется закрытие цены за линией, затем вход выполняется только после последующего закрытия обратно через неё в нужном направлении. Такой фильтр исключает возвраты, при которых цена проходит уровень без реакции.

Подтверждённые сетапы открывают сделки с соответствующими торговыми уровнями. Далее необходимо реализовать управление открытыми позициями.
Сопровождение открытой сделки: частичная фиксация прибыли, безубыток и трейлинг-стоп
После открытия сделки управление выполняется на каждом тике. При необходимости часть позиции фиксируется на первой цели, Stop Loss переносится в безубыток, а оставшаяся часть сопровождается трейлинг-стопом по мере движения цены.
//+------------------------------------------------------------------+ //| Сопроводить одну открытую позицию: частичное закрытие, безубыток, трейлинг | //+------------------------------------------------------------------+ void ManageOneTrade(ulong ticket) { //--- Завершить, если тикет невозможно выбрать if(!PositionSelectByTicket(ticket)) return; //--- Пропустить другой магический номер if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) return; //--- Пропустить другие символы if(PositionGetString(POSITION_SYMBOL) != _Symbol) return; //--- Получить направление и цены позиции ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); bool isBull = (posType == POSITION_TYPE_BUY); double entry = PositionGetDouble(POSITION_PRICE_OPEN); double currentStop = PositionGetDouble(POSITION_SL); double currentTP = PositionGetDouble(POSITION_TP); double volume = PositionGetDouble(POSITION_VOLUME); //--- Определить первоначальное расстояние риска сделки int recIdx = FindTradeRecord(ticket); double riskDistance = (recIdx >= 0 && g_trades[recIdx].riskDistance > 0) ? g_trades[recIdx].riskDistance : MathAbs(entry - currentStop); //--- Завершить работу при нулевом расстоянии риска if(riskDistance <= 0) return; //--- Получить текущие цены и прибыль в единицах R double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double profitR = isBull ? (bid - entry) / riskDistance : (entry - ask) / riskDistance; //--- Получить ATR для трейлинга по ATR double atr = IndicatorValue(AtrHandle, 0, 1); if(atr == EMPTY_VALUE) atr = 0.0; //--- Зафиксировать часть позиции на первой цели if(InpUsePartialClose && recIdx >= 0 && !g_trades[recIdx].partialTaken && profitR >= InpPartialAtRR) { //--- Рассчитать объём для частичного закрытия double volMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double volStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); double closeVol = MathFloor((volume * InpPartialPercent / 100.0) / volStep) * volStep; closeVol = NormalizeDouble(closeVol, 2); //--- Выполнить частичное закрытие, если закрываемый и оставшийся объёмы допустимы if(closeVol >= volMin && (volume - closeVol) >= volMin) { //--- Отправить запрос на частичное закрытие и записать результат в журнал if(Trade.PositionClosePartial(ticket, closeVol)) Log("Partial close " + DoubleToString(closeVol, 2) + " @ " + DoubleToString(profitR, 2) + "R"); } //--- Отметить частичное закрытие как выполненное g_trades[recIdx].partialTaken = true; } //--- Перенести стоп-лосс в безубыток после достижения порога прибыли if(InpUseBreakeven && profitR >= InpBreakevenAtRR) { //--- Рассчитать стоп-лосс с фиксацией прибыли относительно цены входа double lockDistance = InpBreakevenLockPoints * SymPoint; double breakevenStop = NormalizeDouble(isBull ? entry + lockDistance : entry - lockDistance, SymDigits); //--- Перемещать стоп-лосс только в выгодном направлении bool improves = isBull ? (currentStop < breakevenStop) : (currentStop == 0 || currentStop > breakevenStop); //--- Применить стоп-лосс безубытка if(improves) { //--- После успешного изменения сохранить новый стоп-лосс if(Trade.PositionModify(ticket, breakevenStop, currentTP)) currentStop = breakevenStop; } } //--- Сопровождать оставшуюся часть позиции if(InpTrailingMode != TRAIL_OFF) { //--- Хранить рассчитанный кандидат на новый трейлинг-стоп double newStop = 0; //--- Трейлинг по пунктам включается после фиксации заданной прибыли и не зависит от безубытка if(InpTrailingMode == TRAIL_POINTS) { //--- Рассчитать открытую прибыль в пунктах double profitPoints = isBull ? (bid - entry) / SymPoint : (entry - ask) / SymPoint; //--- Включить трейлинг после превышения порога фиксации и дистанции трейлинга if(profitPoints >= InpTrailLockPoints + InpTrailDistancePoints) newStop = isBull ? bid - InpTrailDistancePoints * SymPoint : ask + InpTrailDistancePoints * SymPoint; } //--- Трейлинг по ATR включается после достижения порога прибыли в R else if(InpTrailingMode == TRAIL_ATR && profitR >= InpTrailStartRR && atr > 0) newStop = isBull ? bid - InpTrailAtrMultiplier * atr : ask + InpTrailAtrMultiplier * atr; //--- Применить рассчитанный трейлинг-стоп if(newStop > 0) { //--- Нормализовать рассчитанный стоп-лосс newStop = NormalizeDouble(newStop, SymDigits); //--- Перемещать стоп-лосс только в выгодном направлении bool improves = isBull ? (newStop > currentStop) : (currentStop == 0 || newStop < currentStop); //--- Изменить позицию, если новый стоп-лосс улучшает защиту if(improves) Trade.PositionModify(ticket, newStop, currentTP); } } } //+------------------------------------------------------------------+ //| Сопровождать все отслеживаемые сделки | //+------------------------------------------------------------------+ void ManageOpenTrades() { //--- Сопровождать отслеживаемые тикеты с конца массива for(int i = ArraySize(g_trades) - 1; i >= 0; i--) ManageOneTrade(g_trades[i].ticket); }
Функция ManageOneTrade сопровождает одну позицию от тика к тику. Она получает тикет, выбирает позицию через PositionSelectByTicket и пропускает сделки с другим магическим номером или символом. Затем считываются направление, цена входа, текущий Stop Loss и объём. Исходный риск берётся из записи, созданной при исполнении, а при её отсутствии рассчитывается как текущее расстояние от входа до Stop Loss. Открытая прибыль выражается в единицах риска R, поэтому все последующие пороги измеряются относительно первоначального Stop Loss.
Первый этап — необязательное частичная фиксация прибыли. Когда функция включена и прибыль достигает заданного порога, рассчитывается закрываемый объём, округляется вниз по шагу брокера и проверяется, что как закрываемая, так и оставшаяся части не меньше минимального объёма. После этого часть позиции закрывается методом PositionClosePartial, а в записи устанавливается признак выполнения, чтобы операция не повторялась.
Затем выполняется перенос в безубыток. После достижения заданного порога прибыли Stop Loss рассчитывается на несколько пунктов дальше цены входа в сторону прибыли, а не точно по цене открытия. Новый уровень применяется только тогда, когда улучшает текущий Stop Loss и никогда не ослабляет уже более жёсткую защиту. Изменение выполняется методом PositionModify.
Последний этап — трейлинг-стоп с двумя режимами. Трейлинг в пунктах включается после того, как открытая прибыль превышает фиксируемую величину вместе с дистанцией сопровождения, и следует за ценой на заданном расстоянии. Трейлинг по ATR ждёт достижения порога прибыли в R, а затем располагается на несколько ATR за ценой, адаптируясь к волатильности. В обоих режимах кандидатный Stop Loss применяется только при усилении защиты в пользу позиции.
Функция ManageOpenTrades перебирает отслеживаемые тикеты и вызывает ManageOneTrade для каждого. Она запускается на каждом тике, поэтому сопровождение следует за ценой даже между барами. Результат показан ниже.

Сетап подтверждается и активируется и переходит в режим ожидания входа, а после проверки открывается и сопровождается сделка в соответствии с поставленными задачами. Остаётся протестировать программу — этому посвящён следующий раздел.
Тестирование в тестере стратегий
Компилируем программу и запускаем её в тестере стратегий MetaTrader 5 в визуальном режиме, чтобы наблюдать построение структуры бар за баром. Результат представлен ниже в формате GIF (Graphics Interchange Format).

Во время тестирования программа размечала подтверждённые свинговые точки и собирала их в чередующийся зигзаг. Сетап ставился в ожидание входа только после того, как голова снимала ликвидность за левым плечом, а цена пробивала уровень Leg. Каждый подготовленный сетап отображал полную структуру и пунктирную линию входа QM. Некоторые сетапы истекали без сделки, если цена не возвращалась к линии; другие открывались на возврате со стоп-лоссом за головой и целью, рассчитанной по заданному соотношению прибыли к риску. В прибыльных позициях стоп-лосс переносился в безубыток, а затем сопровождал цену по мере развития движения.
График тестирования:

Отчёт тестирования:

Заключение
В статье мы создали автоматическую программу MQL5 для обнаружения разворотного паттерна Квазимодо по зигзагу подтверждённых свинговых точек. Советник подтверждает сигнал через пробой структуры и фильтр предшествующего тренда, ставит сетап в ожидание входа и открывает позицию при возврате цены к линии QM. Управление позицией включает структурный стоп-лосс с минимальной дистанцией по ATR, расчёт объёма по заданному риску, перенос в безубыток, трейлинг-стоп и необязательное частичная фиксация прибыли.
Предупреждение: статья предназначена исключительно для образовательных целей. Торговля связана со значительными финансовыми рисками, а результаты тестирования не гарантируют будущую доходность. Перед использованием программы на реальном счёте необходимы тщательное тестирование и осторожное управление риском.
После изучения статьи вы сможете:
- Автоматически обнаруживать развороты Квазимодо по зигзагу подтверждённых свинговых точек, проверять пробой структуры и предшествующий тренд и отображать найденную структуру на графике.
- Размещать входы на линии QM, устанавливать структурный стоп-лосс за головой и цель по пробитому уровню Leg, отбрасывая сетапы с недостаточным соотношением прибыли к риску.
- Сопровождать открытые позиции с помощью переноса стоп-лосса в безубыток, трейлинга по ATR или фиксированным пунктам и необязательного частичного закрытия.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23141
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От начального до среднего уровня: Классы (I)
Моделирование рынка: Position View (XIII)
Как мы построили самую мощную торговую платформу с Machine Learning: хроника развития MQL и MetaTrader по архивам, форумам и релизам
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования