Автоматизация торговых стратегий в MQL5 (Часть 50): Стратегия Turtle Soup на снятии ликвидности
Введение
Очевидные пробои недавних N-барных максимумов и минимумов выглядят надёжными сигналами продолжения, однако нередко оказываются снятием ликвидности: цена ненадолго выходит за экстремум, активирует скопившиеся стоп-ордера и резко возвращается обратно, оставляя трейдеров в шортах у дна и в лонгах у вершины. Для разработчика алгоритма такую неоднозначность нельзя оценивать на глаз — нужны точные, проверяемые правила.
Статья предназначена для разработчиков на MetaQuotes Language 5 (MQL5) и алгоритмических трейдеров, которые хотят торговать против ложных пробоев по чётким механическим правилам, а не на основе суждений. В предыдущей статье (Часть 49) мы автоматизировали разворотный паттерн Квазимодо на основе зигзага из свинговых точек. В этой статье мы рассмотрим задачу Turtle Soup как формализованный набор правил обнаружения сигнала и дальнейших действий: определим снятие ликвидности по абсолютной глубине в пунктах цены (points в терминологии терминала) и доле диапазона, подтверждение возврата несколькими последовательными закрытиями обратно внутри уровня и необязательному телу разворотной свечи, минимальный возраст опорного экстремума, окно снятия ликвидности и рабочие ограничения программы — работу по закрытию бара, защиту от повторных сигналов, лимит позиций по каждому направлению и соблюдение минимальных расстояний брокера. Рассмотрим следующие темы:
- Принцип стратегии Turtle Soup со снятием ликвидности
- Реализация на MQL5
- Тестирование на исторических данных
- Заключение
В результате мы получим автоматическую программу MQL5, которая отмечает N-барные пулы ликвидности, обнаруживает и подтверждает снятие ликвидности и открывает сделки с настраиваемыми стоп-лоссами, целями и необязательным трейлингом. Программа будет готова для прогонов в Тестере стратегий и дальнейшей настройки.
Принцип стратегии Turtle Soup со снятием ликвидности
Стратегия Turtle Soup возникла как торговля против классической пробойной системы Turtles. Участники системы Turtles покупали новые двадцатибарные максимумы и продавали новые двадцатибарные минимумы, рассчитывая на продолжение пробоя; Turtle Soup действует наоборот и входит против неудавшегося пробоя. Область над максимумом мы называем ликвидностью со стороны покупателей (buy-side liquidity), а область под минимумом — ликвидностью со стороны продавцов (sell-side liquidity). Цена нередко тянется к этим пулам ликвидности, выходит за уровень, активирует ожидающие стоп-ордера и затем возвращается внутрь диапазона. После снятия ликвидности над максимумом и закрытия обратно ниже него открывается продажа; после снятия ликвидности под минимумом и закрытия обратно выше него — покупка. Общий вид такого сетапа показан ниже.

Прибыльность стратегии зависит от используемых фильтров. Значимое снятие ликвидности должно выходить за уровень на достаточное расстояние, а не просто касаться его. Поэтому минимальная глубина задаётся одновременно в пунктах и как доля расчётного диапазона. Опорный экстремум также должен быть достаточно старым. Главное условие — подтверждение возврата: один или несколько баров должны закрыться обратно внутрь уровня; дополнительно можно потребовать тело разворотной свечи в сторону сделки. Это позволяет входить по подтверждённому отскоку, а не пытаться угадывать разворот прямо во время снятия ликвидности. Стоп размещается сразу за экстремумом снятия ликвидности, а цель выбирается либо по фиксированному соотношению прибыль/риск, либо по противоположной стороне диапазона.
В реальной торговле используйте N-барные максимум и минимум как ориентиры ликвидности и дождитесь снятия ликвидности. Отметьте недавний экстремум, дождитесь выхода цены за него и следите за закрытием обратно внутри диапазона, не принимая решение только по тени свечи. Входите против неудавшегося пробоя: продавайте ложный пробой максимума и покупайте ложный пробой минимума. Стоп-лосс размещайте за экстремумом снятия ликвидности, чтобы при истинном продолжении движения позиция быстро закрывалась по стопу. Фиксированное соотношение прибыль/риск делает расчёт целей более последовательным, а противоположная сторона диапазона подходит для бокового рынка.

Реализация на MQL5
Настройка: перечисления, входные параметры и глобальные переменные
Сначала подключим торговую библиотеку и зададим конфигурацию программы: перечисления для выбора режимов, сгруппированные входные параметры и глобальные переменные, которые сохраняют состояние между барами.
//+------------------------------------------------------------------+ //| TBS Turtle Body Soup.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" #property strict #include <Trade\Trade.mqh> //+------------------------------------------------------------------+ //| Перечисления | //+------------------------------------------------------------------+ enum ENUM_SLTP_METHOD { SLTP_Dynamic = 0, // Динамический режим: SL за экстремумом снятия ликвидности SLTP_Static = 1 // Статический режим: фиксированное расстояние в пунктах }; enum ENUM_TRAILING_TYPE { Trail_None = 0, // Отключён Trail_Points = 1 // По пунктам }; enum ENUM_TP_MODE { TP_RiskReward = 0, // Фиксированный множитель прибыль/риск TP_OppositeRange = 1 // Противоположная сторона диапазона анализа }; //+------------------------------------------------------------------+ //| Входные параметры | //+------------------------------------------------------------------+ input group "Setup Detection" input ENUM_TIMEFRAMES InpSignalTimeframe = PERIOD_CURRENT; // Сигнальный таймфрейм input int InpLookbackBars = 20; // Период анализа для N-барного максимума/минимума (Turtle = 20) input int InpMinExtremeAgeBars = 4; // Минимальное число баров между опорным экстремумом и снятием input int InpSweepWindowBars = 1; // Число баров, в которых допускается снятие (1 = классический вариант) input int InpConfirmCloseBars = 1; // Число последовательных закрытий обратно внутри уровня input bool InpRequireBearBullBody = true; // Требовать тело разворотной свечи в направлении сделки input group "Sweep Depth Filter" input bool InpUseSweepDepthFilter = true; // Использовать фильтр глубины снятия input double InpMinSweepDepthPoints = 30.0; // Минимальный выход за уровень в пунктах input double InpMinSweepDepthPctRange = 2.0; // Минимальная глубина снятия в процентах от диапазона input group "Risk / Orders" input double InpLotSize = 0.01; // Объём сделки в лотах input ENUM_SLTP_METHOD InpSLTPMethod = SLTP_Dynamic; // Метод расчёта стоп-лосса input ENUM_TP_MODE InpTPMode = TP_RiskReward; // Режим тейк-профита input double InpRiskRewardRatio = 1.0; // Соотношение прибыль/риск для режима цели R:R input int InpStaticSLPoints = 5000; // Размер стоп-лосса в пунктах для статического режима input double InpSLBufferPoints = 3000.0; // Дополнительный буфер стоп-лосса в пунктах для обоих режимов input int InpMaxPositionsPerDirection = 1; // Максимальное число открытых позиций в одном направлении input int InpMagicNumber = 1107; // Магический номер input group "Trailing" input ENUM_TRAILING_TYPE InpTrailingType = Trail_None; // Тип трейлинг-стопа input double InpTrailingStopPoints = 300.0; // Дистанция трейлинг-стопа в пунктах input double InpMinProfitToActivateTrail = 1000.0; // Минимальная прибыль для запуска трейлинга в пунктах input group "Visuals" input bool InpShowVisuals = true; // Рисовать графические объекты input color InpNBarHighColor = clrOrange; // Цвет N-барного максимума: ликвидность со стороны покупателей input color InpNBarLowColor = clrDodgerBlue; // Цвет N-барного минимума: ликвидность со стороны продавцов input color InpSellSetupColor = clrCrimson; // Цвет сетапа на продажу input color InpBuySetupColor = C'0,150,0'; // Цвет сетапа на покупку //+------------------------------------------------------------------+ //| Глобальные переменные | //+------------------------------------------------------------------+ CTrade g_trade; // Объект CTrade для исполнения торговых операций datetime g_lastBarTime = 0; // Время открытия последнего обработанного сигнального бара datetime g_lastSellSignalTime = 0; // Время последнего сигнального бара на продажу: защита от повторов datetime g_lastBuySignalTime = 0; // Время последнего сигнального бара на покупку: защита от повторов double g_cachedRefHigh = 0.0; // Последняя опорная цена максимума за период анализа double g_cachedRefLow = 0.0; // Последняя опорная цена минимума за период анализа string OBJ_PREFIX = "TSOUP_"; // Префикс имён графических объектов для массового удаления
Подключение стандартной торговой библиотеки предоставляет класс CTrade для отправки и изменения ордеров. Затем задаются три перечисления, превращающие переключатели режимов в именованные варианты. ENUM_SLTP_METHOD выбирает динамический стоп-лосс за экстремумом снятия ликвидности либо статический стоп фиксированной величины. ENUM_TRAILING_TYPE включает или отключает трейлинг по пунктам, а ENUM_TP_MODE выбирает цель по заданному соотношению прибыль/риск либо по противоположной стороне расчётного диапазона.
Далее входные параметры объединяются в подписанные группы, чтобы окно настроек оставалось упорядоченным. Группа обнаружения сетапа задаёт сигнальный таймфрейм, N-барное окно анализа и правила обнаружения и подтверждения снятия ликвидности. Остальные группы отвечают за фильтр глубины снятия, риск и ордера, трейлинг и визуализацию. Такое разделение позволяет настраивать программу по назначению параметров, а не искать их в одном длинном списке.
Затем объявляются глобальные переменные, связывающие все части программы. Создаётся объект CTrade, сохраняются временные метки, предотвращающие повторную обработку одного сигнального бара, и кэшируются последние N-барные максимум и минимум. Общий префикс имён позволяет удалить всю графическую разметку одним вызовом. После этого перейдём к вспомогательным функциям, поддерживающим модульную структуру программы.
Подсчёт позиций и подтверждение закрытий
Логике обнаружения помогают две небольшие проверки: первая подсчитывает открытые позиции советника в заданном направлении, а вторая подтверждает, что цена закрылась обратно внутри уровня снятия.
//+------------------------------------------------------------------+ //| Подсчитать активные позиции по символу, магику и направлению | //+------------------------------------------------------------------+ int CountActivePositions(ENUM_POSITION_TYPE positionType) { int count = 0; //--- Перебрать все открытые позиции в обратном порядке for(int pos = PositionsTotal() - 1; pos >= 0; pos--) { //--- Получить тикет позиции по текущему индексу ulong ticket = PositionGetTicket(pos); //--- Пропустить недействительные или недоступные для выбора тикеты if(ticket == 0) continue; //--- Учитывать только позиции нужного символа, магического номера и направления if(PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber && PositionGetInteger(POSITION_TYPE) == positionType) count++; } //--- Вернуть общее число подходящих позиций return count; } //+------------------------------------------------------------------+ //| Проверить N последовательных закрытых баров внутри уровня | //+------------------------------------------------------------------+ bool AllConfirmClosesInsideLevel(bool checkBelowLevel, double referenceLevel, int barsToConfirm) { //--- Проверить подтверждающие бары от последнего закрытого в прошлое for(int barIdx = 1; barIdx <= barsToConfirm; barIdx++) { //--- Получить цену закрытия текущего подтверждающего бара double closePrice = iClose(_Symbol, InpSignalTimeframe, barIdx); //--- Для продажи закрытие должно быть ниже снятого максимума if(checkBelowLevel) { //--- Сразу вернуть false, если закрытие всё ещё выше уровня if(!(closePrice < referenceLevel)) return false; } else { //--- Для покупки закрытие должно быть выше снятого минимума //--- Сразу вернуть false, если закрытие всё ещё ниже уровня if(!(closePrice > referenceLevel)) return false; } } //--- Все подтверждающие бары закрылись с нужной стороны return true; }
Функция CountActivePositions подсчитывает открытые позиции советника в заданном направлении. Ей передаётся тип позиции, после чего программа перебирает все позиции через PositionsTotal и получает их тикеты с помощью PositionGetTicket. В подсчёт входят только позиции по текущему символу, с магическим номером советника и требуемым направлением. Благодаря этому программа соблюдает установленный лимит позиций на покупку и продажу.
Функция AllConfirmClosesInsideLevel проверяет, вернулась ли цена внутрь диапазона после снятия ликвидности. Ей передаются сторона проверки, опорный уровень и количество подтверждающих баров. Затем через iClose считываются цены закрытия уже сформированных свечей. Для продажи каждое закрытие должно быть ниже снятого максимума, а для покупки — выше снятого минимума. Если хотя бы один бар закрывается с неправильной стороны, функция сразу возвращает false. Сетап подтверждается только тогда, когда все проверяемые бары закрылись с нужной стороны уровня.
Вспомогательные функции отрисовки: сегменты, области, стрелки и уровни
Для отображения каждого сетапа на графике добавим функции, которые рисуют ограниченные сегменты, заполненные зоны риска и прибыли, сигнальную стрелку с подписью и пунктирные линии N-барных уровней.
//+------------------------------------------------------------------+ //| Нарисовать горизонтальный ценовой сегмент для уровня входа | //+------------------------------------------------------------------+ void DrawHorizontalSegment(string objName, datetime startTime, double price, datetime endTime, color lineColor, ENUM_LINE_STYLE lineStyle, int lineWidth) { //--- Удалить предыдущий объект с тем же именем ObjectDelete(ChartID(), objName); //--- Создать горизонтальную трендовую линию между начальным и конечным временем ObjectCreate(ChartID(), objName, OBJ_TREND, 0, startTime, price, endTime, price); //--- Задать визуальные свойства ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, lineColor); ObjectSetInteger(ChartID(), objName, OBJPROP_STYLE, lineStyle); ObjectSetInteger(ChartID(), objName, OBJPROP_WIDTH, lineWidth); //--- Отключить луч вправо, чтобы сегмент оставался ограниченным ObjectSetInteger(ChartID(), objName, OBJPROP_RAY_RIGHT, false); //--- Запретить случайное выделение объекта пользователем ObjectSetInteger(ChartID(), objName, OBJPROP_SELECTABLE, false); } //+------------------------------------------------------------------+ //| Нарисовать прямоугольник риска или прибыли позади свечей | //+------------------------------------------------------------------+ void DrawFilledTradeBox(string objName, datetime startTime, double priceA, datetime endTime, double priceB, color fillColor) { //--- Удалить предыдущий объект с тем же именем ObjectDelete(ChartID(), objName); //--- Создать прямоугольник от цены входа до SL или TP ObjectCreate(ChartID(), objName, OBJ_RECTANGLE, 0, startTime, priceA, endTime, priceB); //--- Задать сплошную заливку и визуальные свойства ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, fillColor); ObjectSetInteger(ChartID(), objName, OBJPROP_FILL, true); //--- Разместить позади свечей, сохраняя читаемость графика ObjectSetInteger(ChartID(), objName, OBJPROP_BACK, true); ObjectSetInteger(ChartID(), objName, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(ChartID(), objName, OBJPROP_WIDTH, 1); //--- Запретить случайное выделение объекта пользователем ObjectSetInteger(ChartID(), objName, OBJPROP_SELECTABLE, false); } //+------------------------------------------------------------------+ //| Нарисовать вертикальную сигнальную стрелку с повёрнутой подписью | //+------------------------------------------------------------------+ void DrawSignalArrowAndLabel(string baseName, datetime signalTime, double anchorPrice, bool isBuy, string labelText, color signalColor) { //--- Выбрать символ Wingdings: 217 — стрелка вверх для покупки, 218 — вниз для продажи int arrowCode = isBuy ? 217 : 218; //--- Повернуть подпись на -90 градусов для покупки и +90 градусов для продажи double labelAngle = isBuy ? -90.0 : 90.0; //--- Сформировать уникальные имена объектов стрелки и текста string arrowObjName = baseName + "_ARW"; string textObjName = baseName + "_TXT"; //--- Удалить предыдущий объект стрелки ObjectDelete(ChartID(), arrowObjName); //--- Создать направленную стрелку на цене сигнального бара ObjectCreate(ChartID(), arrowObjName, OBJ_ARROW, 0, signalTime, anchorPrice); ObjectSetInteger(ChartID(), arrowObjName, OBJPROP_ARROWCODE, arrowCode); ObjectSetInteger(ChartID(), arrowObjName, OBJPROP_COLOR, signalColor); ObjectSetInteger(ChartID(), arrowObjName, OBJPROP_WIDTH, 2); //--- Разместить на переднем плане поверх свечей ObjectSetInteger(ChartID(), arrowObjName, OBJPROP_BACK, false); //--- Для покупки привязать верх стрелки под свечой, для продажи — низ над свечой ObjectSetInteger(ChartID(), arrowObjName, OBJPROP_ANCHOR, isBuy ? ANCHOR_TOP : ANCHOR_BOTTOM); ObjectSetInteger(ChartID(), arrowObjName, OBJPROP_SELECTABLE, false); //--- Удалить предыдущий объект текстовой подписи ObjectDelete(ChartID(), textObjName); //--- Создать повёрнутую подпись в координатах стрелки ObjectCreate(ChartID(), textObjName, OBJ_TEXT, 0, signalTime, anchorPrice); //--- Четыре начальных пробела отделяют текст от символа стрелки ObjectSetString(ChartID(), textObjName, OBJPROP_TEXT, " " + labelText); ObjectSetString(ChartID(), textObjName, OBJPROP_FONT, "Calibri Bold"); ObjectSetInteger(ChartID(), textObjName, OBJPROP_FONTSIZE, 11); ObjectSetInteger(ChartID(), textObjName, OBJPROP_COLOR, signalColor); //--- Применить вертикальный поворот подписи рядом со стрелкой ObjectSetDouble(ChartID(), textObjName, OBJPROP_ANGLE, labelAngle); ObjectSetInteger(ChartID(), textObjName, OBJPROP_ANCHOR, ANCHOR_LEFT); ObjectSetInteger(ChartID(), textObjName, OBJPROP_SELECTABLE, false); } //+------------------------------------------------------------------+ //| Создать/обновить пунктирную горизонтальную линию ценового уровня | //+------------------------------------------------------------------+ void RenderHorizontalLevel(string objName, double levelPrice, color levelColor, string tooltipText) { //--- Создать горизонтальную линию только при первом вызове if(ObjectFind(ChartID(), objName) < 0) { ObjectCreate(ChartID(), objName, OBJ_HLINE, 0, 0, levelPrice); ObjectSetInteger(ChartID(), objName, OBJPROP_STYLE, STYLE_DOT); ObjectSetString(ChartID(), objName, OBJPROP_TOOLTIP, tooltipText); } //--- При последующих вызовах обновлять цену и цвет ObjectSetDouble(ChartID(), objName, OBJPROP_PRICE, levelPrice); ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, levelColor); ChartRedraw(ChartID()); } //+------------------------------------------------------------------+ //| Создать/заменить текстовую подпись в координатах времени и цены | //+------------------------------------------------------------------+ void RenderPriceLabel(string objName, datetime labelTime, double labelPrice, string labelText, color textColor, int anchorType) { //--- Всегда удалять и создавать заново для точного позиционирования ObjectDelete(ChartID(), objName); ObjectCreate(ChartID(), objName, OBJ_TEXT, 0, labelTime, labelPrice); ObjectSetString(ChartID(), objName, OBJPROP_TEXT, labelText); ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, textColor); ObjectSetInteger(ChartID(), objName, OBJPROP_ANCHOR, anchorType); ObjectSetInteger(ChartID(), objName, OBJPROP_FONTSIZE, 10); ChartRedraw(ChartID()); }
Сначала создаются две простые функции для построения графических объектов. DrawHorizontalSegment рисует ограниченный горизонтальный отрезок между двумя временными точками. Функции передаются имя объекта, координаты двух опорных точек и визуальные свойства линии. Существующий объект с тем же именем удаляется через ObjectDelete, после чего ObjectCreate создаёт трендовую линию, а продолжение луча вправо отключается. DrawFilledTradeBox работает аналогично, но строит заполненный прямоугольник позади свечей, чтобы график цены оставался читаемым.
Функция DrawSignalArrowAndLabel отмечает сигнальный бар. Ей передаются базовое имя, координаты сигнала, признак покупки, текст и цвет подписи. Для покупки размещается стрелка Wingdings вверх, для продажи — вниз. Затем через ObjectSetDouble добавляется текстовая подпись, повёрнутая на девяносто градусов, чтобы она располагалась вертикально рядом со стрелкой и не перекрывала свечи.
Для постоянных N-барных уровней создаётся RenderHorizontalLevel. Ей передаются имя, цена, цвет и всплывающая подсказка. В отличие от других функций, линия создаётся только один раз, а при следующих вызовах обновляются лишь её цена и цвет, поэтому уровень обновляется вместе с диапазоном анализа. RenderPriceLabel размещает текстовую подпись в заданных координатах, каждый раз удаляя и создавая объект заново для точного позиционирования. Далее объединим эти функции в одной общей функции разметки сетапа.
Формирование полной торговой разметки
Все вспомогательные функции объединяются в одной функции, которая рисует весь сетап: область снятия ликвидности, зоны риска и прибыли, линию входа, сигнальную стрелку и ценовые подписи.
//+------------------------------------------------------------------+ //| Нарисовать область снятия, зоны R:R, сигнальную стрелку и подписи| //+------------------------------------------------------------------+ void DrawTradeSetupVisuals(bool isBuy, double sweptLevel, double sweepExtreme, double entryPrice, double stopLoss, double takeProfit, datetime signalBarTime) { //--- Сформировать уникальный суффикс имени по времени сигнального бара string timeSuffix = IntegerToString((long)signalBarTime); //--- Выбрать цвета в зависимости от направления сделки color setupColor = isBuy ? InpBuySetupColor : InpSellSetupColor; //--- Ограничить окно снятия минимум одним баром int sweepWindowSize = MathMax(InpSweepWindowBars, 1); //--- Определить начало окна снятия и время текущего бара datetime sweepStartTime = iTime(_Symbol, InpSignalTimeframe, sweepWindowSize); datetime currentBarTime = iTime(_Symbol, InpSignalTimeframe, 0); //--- Продлить область торговой разметки на восемь баров вперёд datetime tradeBoxEndTime = currentBarTime + (datetime)(PeriodSeconds(InpSignalTimeframe) * 8); //--- Задать цвета разметки color slBorderColor = clrCrimson; // Цвет подписи стоп-лосса color tpBorderColor = C'0,140,0'; // Цвет подписи тейк-профита: тёмно-зелёный color entryLineColor = C'40,40,40'; // Цвет линии и подписи входа: почти чёрный color riskBoxFill = C'255,200,200'; // Заливка зоны риска: светло-красная color rewardBoxFill = C'200,235,200'; // Заливка зоны прибыли: светло-зелёная color buyArrowColor = C'0,150,0'; // Цвет сигнальной стрелки покупки color sellArrowColor = clrCrimson; // Цвет сигнальной стрелки продажи //--- Рассчитать границы прямоугольника снятия double rectTop = MathMax(sweptLevel, sweepExtreme); double rectBot = MathMin(sweptLevel, sweepExtreme); //--- Нарисовать область снятия от начала окна до текущего бара string sweepRectName = OBJ_PREFIX + "Sweep_" + timeSuffix; ObjectCreate(ChartID(), sweepRectName, OBJ_RECTANGLE, 0, sweepStartTime, rectTop, currentBarTime, rectBot); ObjectSetInteger(ChartID(), sweepRectName, OBJPROP_COLOR, setupColor); ObjectSetInteger(ChartID(), sweepRectName, OBJPROP_FILL, false); ObjectSetInteger(ChartID(), sweepRectName, OBJPROP_BACK, true); ObjectSetInteger(ChartID(), sweepRectName, OBJPROP_STYLE, STYLE_DOT); ObjectSetInteger(ChartID(), sweepRectName, OBJPROP_WIDTH, 2); //--- Разместить подпись «Turtle Soup (снятие)» по центру левой границы области double sweepBoxMidPrice = (rectTop + rectBot) * 0.5; RenderPriceLabel(OBJ_PREFIX + "SwTxt_" + timeSuffix, sweepStartTime, sweepBoxMidPrice, "Turtle Soup (sweep) ", setupColor, ANCHOR_RIGHT); //--- Нарисовать заполненную зону риска от входа до SL и прибыли от входа до TP DrawFilledTradeBox(OBJ_PREFIX + "RiskBox_" + timeSuffix, signalBarTime, entryPrice, tradeBoxEndTime, stopLoss, riskBoxFill); DrawFilledTradeBox(OBJ_PREFIX + "RewBox_" + timeSuffix, signalBarTime, entryPrice, tradeBoxEndTime, takeProfit, rewardBoxFill); //--- Нарисовать линию входа по ширине торговой области DrawHorizontalSegment(OBJ_PREFIX + "EnLine_" + timeSuffix, signalBarTime, entryPrice, tradeBoxEndTime, entryLineColor, STYLE_SOLID, 2); //--- Привязать стрелку к минимуму бара для покупки и максимуму для продажи double signalArrowPrice = isBuy ? iLow (_Symbol, InpSignalTimeframe, 1) : iHigh(_Symbol, InpSignalTimeframe, 1); DrawSignalArrowAndLabel(OBJ_PREFIX + "Sig_" + timeSuffix, signalBarTime, signalArrowPrice, isBuy, isBuy ? " Buy" : " Sell", isBuy ? buyArrowColor : sellArrowColor); //--- Рассчитать расстояние SL в пунктах и достигнутый множитель R double slDistancePoints = MathAbs(entryPrice - stopLoss) / _Point; double achievedRMultiple = (MathAbs(entryPrice - stopLoss) > 0.0) ? MathAbs(takeProfit - entryPrice) / MathAbs(entryPrice - stopLoss) : 0.0; //--- Для покупки подпись SL снизу, TP сверху; для продажи — наоборот int slLabelAnchor = isBuy ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER; int tpLabelAnchor = isBuy ? ANCHOR_RIGHT_LOWER : ANCHOR_RIGHT_UPPER; //--- Вывести подписи входа, SL и TP у правой границы торговой области RenderPriceLabel(OBJ_PREFIX + "EnTxt_" + timeSuffix, tradeBoxEndTime, entryPrice, "Entry " + DoubleToString(entryPrice, _Digits) + " ", entryLineColor, ANCHOR_RIGHT); RenderPriceLabel(OBJ_PREFIX + "SlTxt_" + timeSuffix, tradeBoxEndTime, stopLoss, "SL " + DoubleToString(stopLoss, _Digits) + " (" + DoubleToString(slDistancePoints, 0) + " pts) ", slBorderColor, slLabelAnchor); RenderPriceLabel(OBJ_PREFIX + "TpTxt_" + timeSuffix, tradeBoxEndTime, takeProfit, "TP " + DoubleToString(takeProfit, _Digits) + " (" + DoubleToString(achievedRMultiple, 2) + "R) ", tpBorderColor, tpLabelAnchor); ChartRedraw(ChartID()); }
Функция DrawTradeSetupVisuals отображает один сетап. Ей передаются направление, уровень и экстремум снятия ликвидности, цены входа, стоп-лосса и цели, а также время сигнального бара. На графике отображаются область снятия ликвидности, зоны риска и прибыли, линия входа и сигнальная стрелка.
Затем отображается сама сделка. Через DrawFilledTradeBox рисуются зона риска от входа до стоп-лосса и зона прибыли от входа до цели; обе зоны продлеваются на несколько баров вперёд для удобства чтения. DrawHorizontalSegment добавляет линию входа на том же интервале, а DrawSignalArrowAndLabel размещает сигнальную стрелку у минимума бара для покупки или у максимума для продажи.
В завершение добавляются ценовые подписи. Рассчитываются расстояние до стоп-лосса в пунктах и достигнутое соотношение прибыль/риск, после чего у правой границы областей выводятся уровни входа, стоп-лосса и цели. Подпись стоп-лосса содержит расстояние в пунктах, а подпись цели — значение R, поэтому риск и потенциальная прибыль сразу видны на графике. После ChartRedraw можно переходить к логике поиска и торговли сетапов, но сначала создадим функцию открытия ордера.
Расчёт стоп-лосса и цели и отправка ордера
На этом этапе подтверждённый сетап превращается в рыночный ордер. Рассчитываются стоп-лосс и цель, учитываются ограничения брокера по минимальным расстояниям, отправляется ордер, записывается результат и строится разметка.
//+------------------------------------------------------------------+ //| Рассчитать SL/TP, отправить рыночный ордер и записать результат | //+------------------------------------------------------------------+ bool OpenTurtleSoupTrade(bool isBuy, double sweptLevel, double sweepExtreme, double oppositeRangeLevel, double lookbackRange, int extremeAgeBars, double sweepDepth, datetime signalBarTime) { //--- Получить текущие рыночные цены и свойства символа double askPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double bidPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); double pointSize = _Point; int priceDigits = (int)_Digits; long brokerStopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); //--- Перевести минимальный уровень стопов брокера из пунктов в цену double minStopDistance = (double)brokerStopsLevel * pointSize; double entryPrice, stopLoss, takeProfit, riskDistance; if(isBuy) { //--- Открыть покупку по текущей цене Ask entryPrice = askPrice; //--- Установить SL ниже минимума снятия с буфером либо на фиксированной дистанции if(InpSLTPMethod == SLTP_Dynamic) stopLoss = sweepExtreme - InpSLBufferPoints * pointSize; else stopLoss = entryPrice - (InpStaticSLPoints + InpSLBufferPoints) * pointSize; //--- Рассчитать исходное расстояние риска от входа до SL riskDistance = entryPrice - stopLoss; //--- Увеличить риск до минимума брокера, чтобы избежать отклонения if(riskDistance < minStopDistance || riskDistance <= 0.0) { riskDistance = minStopDistance; stopLoss = entryPrice - riskDistance; } //--- В соответствующем режиме установить цель на противоположной стороне диапазона if(InpTPMode == TP_OppositeRange) { takeProfit = oppositeRangeLevel; //--- Использовать цель R:R, если противоположный уровень слишком близок if(takeProfit - entryPrice < minStopDistance) takeProfit = entryPrice + riskDistance * InpRiskRewardRatio; } else { //--- Установить цель как фиксированный множитель R:R от риска takeProfit = entryPrice + riskDistance * InpRiskRewardRatio; } //--- Соблюсти минимальную дистанцию брокера для SL и TP if(entryPrice - stopLoss < minStopDistance) stopLoss = entryPrice - minStopDistance; if(takeProfit - entryPrice < minStopDistance) takeProfit = entryPrice + minStopDistance; //--- Округлить SL и TP до точности символа stopLoss = NormalizeDouble(stopLoss, priceDigits); takeProfit = NormalizeDouble(takeProfit, priceDigits); //--- Отправить рыночный ордер на покупку серверу if(!g_trade.Buy(InpLotSize, _Symbol, entryPrice, stopLoss, takeProfit, "Turtle Soup Buy")) { Print("Buy failed: ", g_trade.ResultRetcodeDescription()); return false; } //--- Завершить работу, если сервер не подтвердил исполнение if(g_trade.ResultRetcode() != TRADE_RETCODE_DONE) return false; //--- Записать все параметры сделки в журнал Print("BUY (Turtle Soup): swept ", DoubleToString(InpLookbackBars), "-bar low ", DoubleToString(sweptLevel, priceDigits), " by ", DoubleToString(sweepDepth / pointSize, 0), " pts (", DoubleToString(sweepDepth / lookbackRange * 100.0, 1), "% of range). Low was ", extremeAgeBars, " bars old. Entry ", DoubleToString(entryPrice, priceDigits), " SL ", DoubleToString(stopLoss, priceDigits), " TP ", DoubleToString(takeProfit, priceDigits)); } else { //--- Открыть продажу по текущей цене Bid entryPrice = bidPrice; //--- Установить SL выше максимума снятия с буфером либо на фиксированной дистанции if(InpSLTPMethod == SLTP_Dynamic) stopLoss = sweepExtreme + InpSLBufferPoints * pointSize; else stopLoss = entryPrice + (InpStaticSLPoints + InpSLBufferPoints) * pointSize; //--- Рассчитать исходное расстояние риска от входа до SL riskDistance = stopLoss - entryPrice; //--- Увеличить риск до минимума брокера, чтобы избежать отклонения if(riskDistance < minStopDistance || riskDistance <= 0.0) { riskDistance = minStopDistance; stopLoss = entryPrice + riskDistance; } //--- В соответствующем режиме установить цель на противоположной стороне диапазона if(InpTPMode == TP_OppositeRange) { takeProfit = oppositeRangeLevel; //--- Использовать цель R:R, если противоположный уровень слишком близок if(entryPrice - takeProfit < minStopDistance) takeProfit = entryPrice - riskDistance * InpRiskRewardRatio; } else { //--- Установить цель как фиксированный множитель R:R от риска takeProfit = entryPrice - riskDistance * InpRiskRewardRatio; } //--- Соблюсти минимальную дистанцию брокера для SL и TP if(stopLoss - entryPrice < minStopDistance) stopLoss = entryPrice + minStopDistance; if(entryPrice - takeProfit < minStopDistance) takeProfit = entryPrice - minStopDistance; //--- Округлить SL и TP до точности символа stopLoss = NormalizeDouble(stopLoss, priceDigits); takeProfit = NormalizeDouble(takeProfit, priceDigits); //--- Отправить рыночный ордер на продажу серверу if(!g_trade.Sell(InpLotSize, _Symbol, entryPrice, stopLoss, takeProfit, "Turtle Soup Sell")) { Print("Sell failed: ", g_trade.ResultRetcodeDescription()); return false; } //--- Завершить работу, если сервер не подтвердил исполнение if(g_trade.ResultRetcode() != TRADE_RETCODE_DONE) return false; //--- Записать все параметры сделки в журнал Print("SELL (Turtle Soup): swept ", DoubleToString(InpLookbackBars), "-bar high ", DoubleToString(sweptLevel, priceDigits), " by ", DoubleToString(sweepDepth / pointSize, 0), " pts (", DoubleToString(sweepDepth / lookbackRange * 100.0, 1), "% of range). High was ", extremeAgeBars, " bars old. Entry ", DoubleToString(entryPrice, priceDigits), " SL ", DoubleToString(stopLoss, priceDigits), " TP ", DoubleToString(takeProfit, priceDigits)); } //--- Нарисовать торговую разметку сетапа на графике if(InpShowVisuals) DrawTradeSetupVisuals(isBuy, sweptLevel, sweepExtreme, entryPrice, stopLoss, takeProfit, signalBarTime); return true; }
Функция OpenTurtleSoupTrade рассчитывает стоп-лосс и тейк-профит, отправляет рыночный ордер и сообщает результат. Ей передаются направление сделки, уровень и экстремум снятия ликвидности, противоположная граница диапазона и данные для журнала. Текущие цены Ask и Bid, а также минимально допустимая дистанция защитных уровней считываются через SymbolInfoDouble и SymbolInfoInteger, поскольку стоп-лосс и тейк-профит должны соответствовать ограничениям брокера.
Для покупки вход выполняется по Ask, а стоп-лосс размещается ниже экстремума снятия ликвидности. В динамическом режиме он устанавливается с буфером ниже снятого минимума, где повторный проход цены опровергает идею торговли против пробоя; в статическом режиме стоп располагается на фиксированном расстоянии ниже входа. Затем рассчитывается риск, и если расстояние меньше допустимого минимума брокера, оно увеличивается до этого значения, чтобы ордер не был отклонён.
Цель зависит от выбранного режима тейк-профита. В режиме противоположной стороны диапазона она устанавливается у дальней границы расчётного диапазона; если этот уровень находится слишком близко к цене входа, используется расчёт по соотношению прибыль/риск. В режиме Risk/Reward цель откладывается от цены входа как заданный множитель риска. Перед отправкой ордера ограничения брокера применяются к стоп-лоссу и тейк-профиту, а цены нормализуются с помощью NormalizeDouble.
Когда уровни готовы, рыночный ордер отправляется методом Buy объекта CTrade; при ошибке отправки или отсутствии подтверждённого исполнения функция завершает работу. После успешного открытия в журнал записываются уровень снятия, глубина снятия в пунктах и процентах диапазона и итоговые цены. Ветка продажи зеркально выполняет те же действия: вход по Bid, стоп выше снятого максимума и метод Sell. В обоих случаях в конце вызывается DrawTradeSetupVisuals. Теперь эту логику можно использовать при сканировании сигналов и открытии сделок по подтверждённым сетапам.
Поиск сетапа снятия ликвидности
Это основной модуль обнаружения. На каждом новом баре он находит N-барные максимум и минимум, проверяет снятие и возврат относительно каждого уровня и открывает соответствующую сделку, когда выполнены все условия.
//+------------------------------------------------------------------+ //| Искать сетапы Turtle Soup по последнему закрытому бару | //+------------------------------------------------------------------+ void ScanForTurtleSoupSetups() { //--- Ограничить параметры окон минимально допустимыми значениями int sweepWindow = MathMax(InpSweepWindowBars, 1); int confirmBars = MathMax(InpConfirmCloseBars, 1); //--- Рассчитать минимальное число баров для выполнения сканирования int minBarsNeeded = InpLookbackBars + sweepWindow + 2; //--- Не выполнять анализ до накопления достаточной истории if(Bars(_Symbol, InpSignalTimeframe) < minBarsNeeded) return; //--- Опорный экстремум — максимум или минимум за InpLookbackBars перед окном снятия int refWindowStartShift = 1 + sweepWindow; //--- Найти индексы баров опорных максимума и минимума в диапазоне int refHighBarShift = iHighest(_Symbol, InpSignalTimeframe, MODE_HIGH, InpLookbackBars, refWindowStartShift); int refLowBarShift = iLowest (_Symbol, InpSignalTimeframe, MODE_LOW, InpLookbackBars, refWindowStartShift); //--- Завершить работу при недействительном индексе экстремума if(refHighBarShift < 0 || refLowBarShift < 0) return; //--- Получить цены опорных максимума и минимума double refHighPrice = iHigh(_Symbol, InpSignalTimeframe, refHighBarShift); double refLowPrice = iLow (_Symbol, InpSignalTimeframe, refLowBarShift); //--- Рассчитать полную высоту диапазона анализа double lookbackRange = refHighPrice - refLowPrice; //--- Завершить работу при нулевом или отрицательном диапазоне if(lookbackRange <= 0.0) return; //--- Сохранить опорные экстремумы для возможного внешнего доступа g_cachedRefHigh = refHighPrice; g_cachedRefLow = refLowPrice; //--- Отобразить линии и подписи ликвидности N-барных максимума и минимума if(InpShowVisuals) { RenderHorizontalLevel(OBJ_PREFIX + "HighLevel", refHighPrice, InpNBarHighColor, "N-Bar High (BSL - buy-side liquidity)"); RenderHorizontalLevel(OBJ_PREFIX + "LowLevel", refLowPrice, InpNBarLowColor, "N-Bar Low (SSL - sell-side liquidity)"); //--- Получить время текущего бара для размещения подписей datetime currentTime = iTime(_Symbol, InpSignalTimeframe, 0); RenderPriceLabel(OBJ_PREFIX + "HighTxt", currentTime, refHighPrice, IntegerToString(InpLookbackBars) + "-Bar High (BSL)", InpNBarHighColor, ANCHOR_RIGHT_LOWER); RenderPriceLabel(OBJ_PREFIX + "LowTxt", currentTime, refLowPrice, IntegerToString(InpLookbackBars) + "-Bar Low (SSL)", InpNBarLowColor, ANCHOR_RIGHT_UPPER); } //--- Найти максимум и минимум внутри окна снятия с индексами от 1 до sweepWindow int sweepHighBarShift = iHighest(_Symbol, InpSignalTimeframe, MODE_HIGH, sweepWindow, 1); int sweepLowBarShift = iLowest (_Symbol, InpSignalTimeframe, MODE_LOW, sweepWindow, 1); double sweepWindowHigh = iHigh(_Symbol, InpSignalTimeframe, sweepHighBarShift); double sweepWindowLow = iLow (_Symbol, InpSignalTimeframe, sweepLowBarShift); //--- Рассчитать возраст каждого опорного экстремума в барах int refHighAgeBars = refHighBarShift - 1; int refLowAgeBars = refLowBarShift - 1; //--- Сохранить время открытия сигнального/подтверждающего бара datetime signalBarTime = iTime(_Symbol, InpSignalTimeframe, 1); //--- Проверить продажу против ложного пробоя N-барного максимума bool sellHighSwept = (sweepWindowHigh > refHighPrice); //--- Убедиться, что все подтверждающие закрытия вернулись ниже максимума bool sellClosedBelow = AllConfirmClosesInsideLevel(true, refHighPrice, confirmBars); //--- При необходимости потребовать медвежье тело подтверждающего бара bool sellBearishBody = (!InpRequireBearBullBody) || (iClose(_Symbol, InpSignalTimeframe, 1) < iOpen(_Symbol, InpSignalTimeframe, 1)); //--- Потребовать достаточный возраст опорного максимума bool sellExtremeOldEnough = (refHighAgeBars >= InpMinExtremeAgeBars); //--- Измерить глубину выхода цены выше опорного максимума double sellSweepDepth = sweepWindowHigh - refHighPrice; //--- Применить фильтр глубины в пунктах и процентах диапазона bool sellDepthPasses = (!InpUseSweepDepthFilter) || (sellSweepDepth >= InpMinSweepDepthPoints * _Point && (sellSweepDepth / lookbackRange * 100.0) >= InpMinSweepDepthPctRange); //--- Открывать продажу только при выполнении условий, отсутствии повтора и свободном лимите if(sellHighSwept && sellClosedBelow && sellBearishBody && sellExtremeOldEnough && sellDepthPasses && g_lastSellSignalTime != signalBarTime && CountActivePositions(POSITION_TYPE_SELL) < InpMaxPositionsPerDirection) { //--- Попытаться открыть продажу и отметить сигнальный бар как обработанный if(OpenTurtleSoupTrade(false, refHighPrice, sweepWindowHigh, refLowPrice, lookbackRange, refHighAgeBars, sellSweepDepth, signalBarTime)) g_lastSellSignalTime = signalBarTime; } //--- Проверить покупку против ложного пробоя N-барного минимума bool buyLowSwept = (sweepWindowLow < refLowPrice); //--- Убедиться, что все подтверждающие закрытия вернулись выше минимума bool buyClosedAbove = AllConfirmClosesInsideLevel(false, refLowPrice, confirmBars); //--- При необходимости потребовать бычье тело подтверждающего бара bool buyBullishBody = (!InpRequireBearBullBody) || (iClose(_Symbol, InpSignalTimeframe, 1) > iOpen(_Symbol, InpSignalTimeframe, 1)); //--- Потребовать достаточный возраст опорного минимума bool buyExtremeOldEnough = (refLowAgeBars >= InpMinExtremeAgeBars); //--- Измерить глубину выхода цены ниже опорного минимума double buySweepDepth = refLowPrice - sweepWindowLow; //--- Применить фильтр глубины в пунктах и процентах диапазона bool buyDepthPasses = (!InpUseSweepDepthFilter) || (buySweepDepth >= InpMinSweepDepthPoints * _Point && (buySweepDepth / lookbackRange * 100.0) >= InpMinSweepDepthPctRange); //--- Открывать покупку только при выполнении условий, отсутствии повтора и свободном лимите if(buyLowSwept && buyClosedAbove && buyBullishBody && buyExtremeOldEnough && buyDepthPasses && g_lastBuySignalTime != signalBarTime && CountActivePositions(POSITION_TYPE_BUY) < InpMaxPositionsPerDirection) { //--- Попытаться открыть покупку и отметить сигнальный бар как обработанный if(OpenTurtleSoupTrade(true, refLowPrice, sweepWindowLow, refHighPrice, lookbackRange, refLowAgeBars, buySweepDepth, signalBarTime)) g_lastBuySignalTime = signalBarTime; } }
Функция ScanForTurtleSoupSetups выполняет полный анализ по последнему закрытому бару. Сначала значения окон ограничиваются допустимым минимумом, а через Bars проверяется наличие достаточной истории. Затем программа находит опорные максимум и минимум в N-барном диапазоне, заканчивающемся перед окном снятия ликвидности. Индексы экстремумов определяются функциями iHighest и iLowest, после чего считываются их цены и рассчитывается ширина диапазона. Если диапазон имеет нулевую или отрицательную величину, дальнейшая обработка прекращается.
После определения уровней они сохраняются в кэше и при включённой визуализации отображаются как N-барные максимум и минимум. RenderHorizontalLevel рисует пунктирные линии, а RenderPriceLabel подписывает их как ликвидность со стороны покупателей и продавцов, чтобы пулы, против которых торгует стратегия, оставались видимыми.
Затем измеряется само снятие ликвидности. Внутри окна снятия находятся максимальный High и минимальный Low — бары, на которых цена могла выйти за уровень. Возраст каждого опорного экстремума рассчитывается как расстояние в барах от последнего закрытого бара. Также сохраняется время сигнального бара для защиты от повторной обработки.
Далее проверяется сетап на продажу — торговля против ложного пробоя N-барного максимума. Для сигнала должны одновременно выполняться пять условий:
- Максимум внутри окна снятия оказался выше опорного максимума.
- Все подтверждающие закрытия вернулись ниже уровня; проверка выполняется функцией AllConfirmClosesInsideLevel.
- Подтверждающий бар закрылся медвежьим телом.
- Опорный максимум имеет достаточный возраст.
- Глубина снятия достаточна и в пунктах, и в процентах от диапазона.
Когда выполнены все пять условий, сигнал не является повторным, а CountActivePositions показывает, что лимит позиций по направлению не достигнут, сделка открывается через OpenTurtleSoupTrade, после чего сигнальный бар отмечается как обработанный. Сетап на покупку зеркально проверяется ниже N-барного минимума. Теперь объединим функции в обработчиках событий для инициализации, поиска сетапов и торговли подтверждёнными сигналами.
Обнаружение нового бара и обработчики событий
Добавим небольшую функцию определения нового бара и подключим логику к трём стандартным обработчикам: проверке параметров при запуске, очистке при завершении и сканированию один раз на новом баре.
//+------------------------------------------------------------------+ //| Определить и сохранить открытие нового бара сигнального таймфрейма //+------------------------------------------------------------------+ bool IsNewSignalBarOpen() { //--- Получить время открытия текущего формирующегося бара datetime currentBarTime = iTime(_Symbol, InpSignalTimeframe, 0); //--- Вернуть false, если для символа или таймфрейма ещё нет данных if(currentBarTime == 0) return false; //--- Сравнить с последним сохранённым временем для определения нового бара if(currentBarTime != g_lastBarTime) { //--- Сохранить время нового бара и подтвердить его обнаружение g_lastBarTime = currentBarTime; return true; } //--- Текущий бар всё ещё формируется return false; } //+------------------------------------------------------------------+ //| Функция инициализации советника | //+------------------------------------------------------------------+ int OnInit() { //--- Отклонить инициализацию, если период анализа слишком мал if(InpLookbackBars < 2) { Print("ERROR: InpLookbackBars must be >= 2"); return(INIT_PARAMETERS_INCORRECT); } //--- Отклонить инициализацию при нулевом или отрицательном окне снятия if(InpSweepWindowBars < 1) { Print("ERROR: InpSweepWindowBars must be >= 1"); return(INIT_PARAMETERS_INCORRECT); } //--- Отклонить инициализацию при некорректном числе подтверждающих баров if(InpConfirmCloseBars < 1) { Print("ERROR: InpConfirmCloseBars must be >= 1"); return(INIT_PARAMETERS_INCORRECT); } //--- Отклонить инициализацию при некорректном R:R или объёме лота if(InpRiskRewardRatio <= 0.0 || InpLotSize <= 0.0) { Print("ERROR: InpRiskRewardRatio and InpLotSize must be > 0"); return(INIT_PARAMETERS_INCORRECT); } //--- Настроить объект CTrade с магическим номером и параметрами исполнения g_trade.SetExpertMagicNumber(InpMagicNumber); g_trade.SetMarginMode(); g_trade.SetTypeFillingBySymbol(_Symbol); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Функция деинициализации советника | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Удалить все графические объекты советника по общему префиксу ObjectsDeleteAll(ChartID(), OBJ_PREFIX); ChartRedraw(ChartID()); } //+------------------------------------------------------------------+ //| Обработчик тиков советника | //+------------------------------------------------------------------+ void OnTick() { //--- Выполнять обнаружение сетапов один раз на новом сигнальном баре if(!IsNewSignalBarOpen()) return; ScanForTurtleSoupSetups(); }
Функция IsNewSignalBarOpen запускает основную логику один раз на бар, а не на каждом тике. Она считывает время открытия формирующегося бара через iTime и сравнивает его с последним сохранённым значением. Если время изменилось, новое значение сохраняется и функция сообщает о новом баре; иначе текущий бар всё ещё формируется.
В OnInit входные параметры проверяются до запуска программы. Инициализация завершается с INIT_PARAMETERS_INCORRECT, если окно анализа слишком короткое для формирования диапазона, окно снятия или число подтверждающих баров меньше единицы либо соотношение прибыль/риск или объём лота имеют недопустимые значения. После успешной проверки объект CTrade настраивается с магическим номером, режимом маржи и типом исполнения, а функция возвращает INIT_SUCCEEDED.
В OnDeinit все созданные графические объекты удаляются по общему префиксу через ObjectsDeleteAll, после чего график перерисовывается. В OnTick работа выполняется один раз на бар: если функция определения нового бара не обнаружила его открытие, обработчик сразу завершается; иначе вызывается ScanForTurtleSoupSetups. После компиляции получаем следующий результат.

Программа обнаруживает, проверяет и визуализирует сетапы, а затем открывает по ним позиции. Остаётся добавить необязательный трейлинг-стоп для управления уже открытыми позициями.
//+------------------------------------------------------------------+ //| Перемещать трейлинг-стоп для всех подходящих открытых позиций | //+------------------------------------------------------------------+ void ApplyPointsBasedTrailingStop() { double pointSize = _Point; //--- Перебирать открытые позиции в обратном порядке для безопасного изменения for(int i = PositionsTotal() - 1; i >= 0; i--) { //--- Получить тикет позиции по текущему индексу ulong ticket = PositionGetTicket(i); //--- Пропустить недействительные или недоступные позиции if(ticket == 0) continue; //--- Пропустить позиции другого символа или магического номера if(PositionGetString(POSITION_SYMBOL) != _Symbol || PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue; //--- Получить текущие SL, TP и цену открытия позиции double currentSL = PositionGetDouble(POSITION_SL); double currentTP = PositionGetDouble(POSITION_TP); double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Получить текущий Bid для расчёта плавающей прибыли double currentBid = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Рассчитать новый SL на дистанции трейлинга ниже Bid double newTrailingSL = NormalizeDouble(currentBid - InpTrailingStopPoints * pointSize, _Digits); //--- Перемещать SL вверх только при улучшении и достижении порога прибыли if(newTrailingSL > currentSL && (currentBid - openPrice) > InpMinProfitToActivateTrail * pointSize) g_trade.PositionModify(ticket, newTrailingSL, currentTP); } else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Получить текущий Ask для расчёта плавающей прибыли double currentAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Рассчитать новый SL на дистанции трейлинга выше Ask double newTrailingSL = NormalizeDouble(currentAsk + InpTrailingStopPoints * pointSize, _Digits); //--- Перемещать SL вниз только при улучшении и достижении порога прибыли if((newTrailingSL < currentSL || currentSL == 0.0) && (openPrice - currentAsk) > InpMinProfitToActivateTrail * pointSize) g_trade.PositionModify(ticket, newTrailingSL, currentTP); } } } //--- Вызывать логику трейлинг-стопа на каждом тике //+------------------------------------------------------------------+ //| Обработчик тиков советника | //+------------------------------------------------------------------+ void OnTick() { //--- Применять трейлинг-стоп на каждом тике, если он включён и есть позиции if(InpTrailingType == Trail_Points && PositionsTotal() > 0) ApplyPointsBasedTrailingStop(); //--- Выполнять обнаружение сетапов один раз на новом сигнальном баре if(!IsNewSignalBarOpen()) return; ScanForTurtleSoupSetups(); }
Функция ApplyPointsBasedTrailingStop подтягивает стоп-лосс вслед за прибыльной позицией. Она перебирает открытые позиции через PositionsTotal и пропускает все позиции с другим символом или магическим номером. Для каждой позиции советника считываются текущие стоп-лосс, тейк-профит и цена открытия. Для покупки предлагается новый стоп на заданном расстоянии ниже текущего Bid, для продажи — выше текущего Ask. Стоп перемещается только при выполнении двух условий: новый уровень улучшает текущую защиту, а открытая прибыль превысила минимальный порог включения трейлинга. После этого позиция изменяется методом PositionModify объекта CTrade.
После создания трейлинга его вызов добавляется в OnTick. Если трейлинг включён и у советника есть открытые позиции, он выполняется на каждом тике, чтобы защитный стоп своевременно следовал за ценой. Обнаружение новых сетапов по-прежнему ограничивается одним запуском на бар: при отсутствии нового бара обработчик завершается, а ScanForTurtleSoupSetups вызывается только после его открытия. Такое разделение обеспечивает оперативное управление позициями и одновременно не перегружает расчётами логику поиска сигналов.

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

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

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

Заключение
В заключение мы преобразовали концепцию Turtle Soup в воспроизводимую программу MQL5 с чётким набором правил. Программа:
- Определяет N-барные максимумы и минимумы как ликвидность со стороны покупателей и продавцов.
- Обнаруживает снятие ликвидности за этими уровнями в настраиваемом окне.
- Подтверждает возврат одним или несколькими последовательными закрытиями обратно внутрь уровня и, при необходимости, проверяет направленное тело разворотной свечи.
- Применяет фильтры глубины и возраста, отбрасывая слишком мелкие снятия и слишком новые экстремумы.
Подтверждённые сигналы превращаются в рыночные ордера с динамическим или статическим стоп-лоссом за экстремумом снятия ликвидности. Предусмотрены два режима тейк-профита: по заданному соотношению прибыль/риск или по противоположной стороне диапазона. Советник учитывает ограничения брокера по минимальному расстоянию до стоп-уровней, ограничивает число позиций в каждом направлении, защищается от повторной обработки сигналов, строит графическую разметку и при необходимости применяет трейлинг по пунктам. Для тестирования вынесены все ключевые параметры: окно анализа, окно снятия ликвидности, число подтверждающих закрытий, глубина снятия в пунктах и процентах, минимальный возраст экстремума, режимы стоп-лосса и тейк-профита, размер лота и параметры трейлинга.
Предупреждение: статья предназначена исключительно для образовательных целей. Торговля связана со значительными финансовыми рисками, а результаты бэктестинга в прошлом не гарантируют будущих результатов. Перед использованием программы на реальном рынке необходимы тщательное тестирование и осторожное управление риском.
После изучения статьи вы сможете:
- Отмечать N-барные максимум и минимум как ликвидность со стороны покупателей и продавцов и наблюдать снятия с обеих сторон непосредственно на графике.
- Торговать против ложного пробоя только после того, как цена вышла за уровень на достаточную глубину и закрылась обратно внутри него при наличии подтверждающей разворотной свечи.
- Размещать стоп-лосс за экстремумом снятия, выбирать цель по заданному соотношению прибыль/риск или противоположной стороне диапазона и сопровождать позицию трейлингом по пунктам при движении в прибыльную сторону.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23155
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Нейросети в трейдинге: Долговременная память торговых сценариев (Объект верхнего уровня)
Создание торговых систем с искусственным интеллектом на MQL5 (Часть 5): Сворачиваемая боковая панель и всплывающие окна истории чатов
Моделирование рынка: Position View (XIV)
От начального до среднего уровня: Классы (I)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Большое спасибо за то, что поделились своими знаниями со всеми нами.
Спасибо вам, сэр, за эту познавательную статью об EA. Я попробую её. Она очень проста для понимания.