Автоматизация торговых стратегий с помощью MQL5 (Часть 47): Торговая система Nick Rypock Trailing Reverse (NRTR) с поддержкой хеджирования
Введение
В нашей предыдущей статье (часть 46) мы создали торговую систему Liquidity Sweep on Break of Structure на языке MQL5, которая определяет зоны ликвидности, выявляет пробои структуры и исполняет сделки с настраиваемыми параметрами риска и визуальными индикаторами. В части 47 мы создаем систему Nick Rypock Trailing Reverse (NRTR), которая использует канальные сигналы разворота для входов по тренду; также в нее добавлены хеджирование, динамический трейлинг-стоп и функции управления рисками. Мы рассмотрим следующие темы:
- Разбор стратегии Nick Rypock Trailing Reverse и ее компонентов
- Реализация на MQL5
- Тестирование на исторических данных
- Заключение
В итоге у вас будет рабочая программа на языке MQL5 для торговли на разворотах по NRTR с хеджированием, готовая к дальнейшей настройке – приступим.
Разбор стратегии Nick Rypock Trailing Reverse и ее компонентов
Стратегия Nick Rypock Trailing Reverse выявляет потенциальные развороты тренда, отслеживая появление динамических уровней поддержки в канале NRTR, который строится на основе значений Average True Range (среднего истинного диапазона), умноженных на заданный пользователем коэффициент, чтобы сформировать верхнюю и нижнюю границы, адаптирующиеся к волатильности. Сигнал на вход возникает при появлении нового уровня поддержки: покупка открывается в начале восходящей поддержки (восходящего канала), а продажа – в начале нисходящей поддержки (нисходящего канала). При включенном режиме хеджирования допускается удержание противоположных позиций. Управление рисками здесь встроено в систему: размер лота рассчитывается автоматически пропорционально балансу, средствам счета или свободной марже, чтобы ограничить риск по каждой сделке, а защиту с учетом волатильности обеспечивают фиксированные или основанные на ATR уровни стоп-лосса и тейк-профита.
Наш план таков: загрузить канальный индикатор NRTR для выявления смены уровней поддержки и формирования сигналов – это бесплатный индикатор, доступный в MQL5, и ключевой элемент данной системы. Затем мы задаем правила входа с учетом лимита по позициям и режима хеджирования, рассчитываем адаптивный размер лота и уровни стопов по параметрам риска, а также реализуем трейлинг и процедуры виртуального закрытия. В итоге получается система, ориентированная на развороты и сочетающая следование за трендом с дисциплинированным контролем риска. Вкратце, этот подход дает торговую модель, адаптированную к волатильности и основанную на логике разворотов, а настраиваемые защитные механизмы обеспечивают стабильную работу системы. Ниже показана схема стратегии.

Реализация на MQL5
Чтобы создать программу на языке MQL5, откройте MetaEditor, перейдите в Навигатор, найдите папку Experts, нажмите кнопку "Создать" и следуйте подсказкам для создания файла. После этого в среде разработки нужно объявить входные параметры и глобальные переменные, которые будут использоваться во всей программе.
//+------------------------------------------------------------------+ //| NRTR - Nick Rypock Trailing Reverse - EA.mq5 | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #include <Trade\Trade.mqh> #include <Trade\PositionInfo.mqh> #include <Trade\SymbolInfo.mqh> //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ enum ENUM_RISK_BASE { RISK_BASE_EQUITY = 1, // Equity RISK_BASE_BALANCE = 2, // Balance RISK_BASE_FREEMARGIN = 3 // Free Margin }; enum ENUM_RISK_DEFAULT_SIZE { RISK_DEFAULT_FIXED = 1, // Fixed RISK_DEFAULT_AUTO = 2 // Auto }; enum ENUM_MODE_SL { SL_FIXED = 0, // Fixed SL_AUTO = 1 // Auto }; enum ENUM_MODE_TP { TP_FIXED = 0, // Fixed TP_AUTO = 1 // Auto }; enum ENUM_TRAILING_TYPE { TRAILING_NONE = 0, // None TRAILING_POINTS = 1, // Points TRAILING_SUPPORT_LEVELS = 2 // Support Levels }; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "Risk Management Settings" input ENUM_RISK_DEFAULT_SIZE RiskDefaultSize = RISK_DEFAULT_FIXED; // Default Risk Size input double DefaultLotSize = 0.01; // Default Lot Size input ENUM_RISK_BASE RiskBase = RISK_BASE_BALANCE; // Risk Base input int MaxRiskPerTrade = 5; // Max Risk Per Trade (%) double MinLotSize = 0.01; // Min Lot Size double MaxLotSize = 100; // Max Lot Size input int MaxPositions = 1; // Max Positions input bool HedgeMode = true; // Hedge Mode bool CloseOnReversalSignal = false; // Close On Reversal Signal input group "Stop-Loss and Take-Profit Settings" input int DefaultStopLossPoints = 300; // Default Stop Loss (Points) input int DefaultTakeProfitPoints = 300; // Default Take Profit (Points) input bool CloseOnStopLossHit = false; // Close On Stop Loss Hit input bool CloseOnTakeProfitHit = false; // Close On Take Profit Hit input group "Additional Settings" input int MagicNumber = 1234567890; // Magic Number input group "Trailing Stop Loss" input ENUM_TRAILING_TYPE TrailingType = TRAILING_POINTS; // Trailing Type input ushort TrailingFrequencySeconds = 10; // Trailing Frequency (Seconds) input ushort SignalCheckFrequencySeconds = 10; // Signal Check Frequency (Seconds) input ushort TrailingStopPoints = 120; // Trailing Stop (Points) input ushort TrailingStepPoints = 100; // Trailing Step (Points) input ushort BreakEvenPoints = 10; // Break Even (Points) input ushort BreakEvenTriggerPoints = 30; // Break Even Trigger (Points) input group "NRTR Channel Settings" input int NRTR_ATR_Period = 40; // NRTR ATR Period input double NRTR_Multiplier = 2.0; // NRTR Multiplier input bool NRTR_Show_Price_Label = true; // Show Price Label //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ double CurrentStopLossPoints = DefaultStopLossPoints; //--- Initialize current SL points double CurrentTakeProfitPoints = DefaultTakeProfitPoints; //--- Initialize current TP points double CurrentTrailingStopPoints = TrailingStopPoints; //--- Initialize current trailing stop points double CurrentTrailingStepPoints = TrailingStepPoints; //--- Initialize current trailing step points bool PrintLog = false; //--- Set print log flag datetime LastTrailingTime = 0; //--- Initialize last trailing time ENUM_MODE_SL StopLossMode = SL_FIXED; //--- Set SL mode ENUM_MODE_TP TakeProfitMode = TP_FIXED; //--- Set TP mode double AtrMultiplierForStopLoss = 2; //--- Set ATR multiplier for SL double AtrMultiplierForTakeProfit = 3; //--- Set ATR multiplier for TP CTrade TradeObject; //--- Declare trade object CSymbolInfo SymbolInfo; //--- Declare symbol info int TrendIndicatorHandle = -1; //--- Initialize trend handle double CurrentTrendValue, PreviousTrendValue; //--- Declare trend values double CurrentTrendDirection, PreviousTrendDirection; //--- Declare trend directions double CurrentUpSupport, PreviousUpSupport; //--- Declare up supports double CurrentDownSupport, PreviousDownSupport; //--- Declare down supports double CurrentNrtrAtr, PreviousNrtrAtr; //--- Declare NRTR ATR values
Реализация начинается с подключения основных библиотек MQL5: "#include <Trade\Trade.mqh>", "#include <Trade\PositionInfo.mqh>" и "#include <Trade\SymbolInfo.mqh>". Они дают доступ к классам для торговых операций, информации о позициях и данным по символу. Затем определяются несколько перечислений, которые позволяют систематизировать пользовательские настройки. Перечисление ENUM_RISK_BASE задает варианты расчета риска на основе средств счета, баланса или свободной маржи. Перечисление ENUM_RISK_DEFAULT_SIZE используется для выбора между фиксированным и автоматическим расчетом лота. Для стоп-лосса и тейк-профита используются ENUM_MODE_SL и ENUM_MODE_TP с вариантами фиксированного и автоматического режимов. Кроме того, ENUM_TRAILING_TYPE задает тип трейлинг-стопа: отсутствует, по пунктам или по уровням поддержки.
Далее пользовательские входные параметры распределяются по группам для удобства работы. В разделе управления рисками используются RiskDefaultSize для выбора метода расчета размера, DefaultLotSize как фиксированное значение объема, RiskBase как база расчета, MaxRiskPerTrade как процентное ограничение риска, минимальный и максимальный размер лота (заданные как глобальные переменные), MaxPositions для ограничения числа открытых сделок, HedgeMode для разрешения хеджирования и CloseOnReversalSignal для выхода по противоположному сигналу. Для настройки стоп-лосса и тейк-профита добавляются DefaultStopLossPoints и DefaultTakeProfitPoints в пунктах, а также булевы флаги для включения закрытия при достижении этих уровней.
В дополнительных настройках задается MagicNumber (магическое число) для уникальной идентификации сделок. Далее задаются параметры трейлинг-стопа: TrailingType, интервалы в секундах для обновления трейлинга и проверки сигналов, TrailingStopPoints, TrailingStepPoints, BreakEvenPoints и BreakEvenTriggerPoints. В группу параметров канала NRTR входят NRTR_ATR_Period, NRTR_Multiplier и NRTR_Show_Price_Label, позволяющие настраивать индикатор прямо из программы.
Наконец, объявляются глобальные переменные для хранения ключевых значений: текущих значений в пунктах для стопов и трейлинга, флага логирования, времени последнего трейлинга, режимов стоп-лосса и тейк-профита, множителей ATR для динамических уровней, а также объектов CTrade и CSymbolInfo. Изначально хэндл трендового индикатора устанавливается в -1; также объявляются переменные типа double для хранения текущих и предыдущих значений тренда, направлений, уровней поддержки и значений NRTR ATR. Далее инициализируется индикатор. Для того чтобы код оставался организованным и модульным, далее логика по возможности выносится в отдельные функции.
//+------------------------------------------------------------------+ //| Initialize expert | //+------------------------------------------------------------------+ int OnInit() { if (!PreInitChecks()) return INIT_FAILED; //--- Perform pre-init checks SymbolInfo.Name(Symbol()); //--- Set symbol name if (!InitIndicatorHandles()) return INIT_FAILED; //--- Initialize indicator handles InitTradeObject(); //--- Initialize trade object return INIT_SUCCEEDED; //--- Return success } //+------------------------------------------------------------------+ //| Perform pre-init checks | //+------------------------------------------------------------------+ bool PreInitChecks() { if (MaxLotSize < MinLotSize) { //--- Check lot sizes Print("MaxLotSize cannot be less than MinLotSize"); //--- Print error return false; //--- Return failure } return true; //--- Return success } //+------------------------------------------------------------------+ //| Initialize indicator handles | //+------------------------------------------------------------------+ bool InitIndicatorHandles() { TrendIndicatorHandle = iCustom(_Symbol, _Period, "Free Indicators\\NRTR Channel", NRTR_ATR_Period, NRTR_Multiplier, NRTR_Show_Price_Label); //--- Create NRTR handle if (TrendIndicatorHandle == INVALID_HANDLE) { //--- Check handle PrintFormat("Error creating NRTR handle - %d", GetLastError()); //--- Print error return false; //--- Return failure } return true; //--- Return success } //+------------------------------------------------------------------+ //| Initialize trade object | //+------------------------------------------------------------------+ void InitTradeObject() { TradeObject.SetExpertMagicNumber(MagicNumber); //--- Set magic number }
В обработчике событий OnInit сначала вызывается PreInitChecks для проверки начальных условий. Если проверка завершается неуспешно, возвращается INIT_FAILED , чтобы программа не продолжала работу с некорректными настройками. Затем в объекте SymbolInfo задается имя текущего символа. Далее вызывается InitIndicatorHandles для загрузки индикатора канала NRTR; при ошибке возвращается INIT_FAILED. Затем вызовом InitTradeObject инициализируется торговый объект, а в конце возвращается INIT_SUCCEEDED, что подтверждает успешную инициализацию.
Функция PreInitChecks проверяет, что MaxLotSize не меньше MinLotSize. Если условие нарушено, выводится сообщение об ошибке и возвращается false; иначе возвращается true, и инициализация продолжается. Внутри InitIndicatorHandles создается хэндл индикатора TrendIndicatorHandle с помощью iCustom с передачей символа, периода, пути к индикатору и параметров NRTR_ATR_Period, NRTR_Multiplier и NRTR_Show_Price_Label. Если хэндл равен INVALID_HANDLE, выводится ошибка с помощью GetLastError, и функция возвращает false; в противном случае возвращается true. Функция InitTradeObject настраивает объект TradeObject, устанавливая для него значение MagicNumber, чтобы идентифицировать сделки, открытые данной программой. Очень важно запустить программу и убедиться, что индикатор загружен успешно, поскольку именно он служит основным источником сигналов.

В нашем случае индикатор загружается успешно. Теперь можно перейти к чтению буферов индикатора и определению логики генерации сигнала. Начнем с логики получения данных.
//+------------------------------------------------------------------+ //| Fetch indicator data | //+------------------------------------------------------------------+ bool FetchIndicatorData() { double atrBuffer[]; //--- Declare ATR buffer as dynamic double trendBuffer[]; //--- Declare trend buffer as dynamic double upSupportBuffer[]; //--- Declare up support buffer as dynamic double downSupportBuffer[]; //--- Declare down support buffer as dynamic ArrayResize(atrBuffer, 2); //--- Resize to 2 ArrayResize(trendBuffer, 2); //--- Resize to 2 ArrayResize(upSupportBuffer, 2); //--- Resize to 2 ArrayResize(downSupportBuffer, 2); //--- Resize to 2 ArraySetAsSeries(atrBuffer, true); //--- Set as series ArraySetAsSeries(trendBuffer, true); //--- Set as series ArraySetAsSeries(upSupportBuffer, true); //--- Set as series ArraySetAsSeries(downSupportBuffer, true); //--- Set as series int copyCount; //--- Declare copy count bool dataReady = false; //--- Set data ready flag int maxAttempts = 5; //--- Set max attempts int delayMs = 200; //--- Set delay ms int attempt = 0; //--- Initialize attempt while (!dataReady && attempt < maxAttempts) { //--- Loop until ready dataReady = true; //--- Assume ready copyCount = CopyBuffer(TrendIndicatorHandle, 5, 1, 2, atrBuffer); //--- Copy ATR if (copyCount < 2 || atrBuffer[0] == EMPTY_VALUE) { //--- Check ATR copy dataReady = false; //--- Set not ready } else { //--- Set ATR values CurrentNrtrAtr = atrBuffer[0]; //--- Set current ATR (recent) PreviousNrtrAtr = atrBuffer[1]; //--- Set previous ATR (older) } copyCount = CopyBuffer(TrendIndicatorHandle, 4, 1, 2, trendBuffer); //--- Copy trend if (copyCount < 2) { //--- Check trend copy dataReady = false; //--- Set not ready } else { //--- Set trend directions CurrentTrendDirection = trendBuffer[0]; //--- Set current direction (recent) PreviousTrendDirection = trendBuffer[1]; //--- Set previous direction (older) } copyCount = CopyBuffer(TrendIndicatorHandle, 1, 1, 2, upSupportBuffer); //--- Copy up support if (copyCount < 2) { //--- Check up copy dataReady = false; //--- Set not ready } else { //--- Set up supports CurrentUpSupport = upSupportBuffer[0]; //--- Set current up (recent) PreviousUpSupport = upSupportBuffer[1]; //--- Set previous up (older) } copyCount = CopyBuffer(TrendIndicatorHandle, 2, 1, 2, downSupportBuffer); //--- Copy down support if (copyCount < 2) { //--- Check down copy dataReady = false; //--- Set not ready } else { //--- Set down supports CurrentDownSupport = downSupportBuffer[0]; //--- Set current down (recent) PreviousDownSupport = downSupportBuffer[1]; //--- Set previous down (older) } if (dataReady) { //--- Check ready CurrentTrendValue = (CurrentTrendDirection > 0) ? CurrentUpSupport : ((CurrentTrendDirection < 0) ? CurrentDownSupport : EMPTY_VALUE); //--- Set current trend value PreviousTrendValue = (PreviousTrendDirection > 0) ? PreviousUpSupport : ((PreviousTrendDirection < 0) ? PreviousDownSupport : EMPTY_VALUE); //--- Set previous trend value if (CurrentTrendValue == EMPTY_VALUE || PreviousTrendValue == EMPTY_VALUE) dataReady = false; //--- Check values } attempt++; //--- Increment attempt Sleep(delayMs); //--- Delay } if (!dataReady) { //--- Check final ready Print("Failed to fetch indicator data"); //--- Print error return false; //--- Return failure } return true; //--- Return success }
Функция FetchIndicatorData используется для надежного получения и сохранения значений из буферов индикатора канала NRTR. Сначала объявляются динамические массивы atrBuffer, trendBuffer, upSupportBuffer и downSupportBuffer. Затем с помощью ArrayResize размер каждого из них устанавливается равным двум элементам, а через ArraySetAsSeries, они переводятся в режим временного ряда, чтобы самые новые данные находились по индексу 0. Затем инициализируются переменные для числа скопированных элементов, флага готовности данных, максимального числа повторных попыток, равного 5, задержки в 200 миллисекунд и счетчика попыток, начинающегося с 0. В цикле while, который выполняется до готовности данных или исчерпания числа попыток, сначала предполагается, что данные готовы, а затем через CopyBuffer считываются данные двух последних баров из нужных буферов индикатора: буфер 5 – для значений ATR, буфер 4 – для направлений тренда, буфер 1 – для уровней восходящей поддержки и буфер 2 – для уровней нисходящей поддержки.
Если любое копирование завершается неудачно (то есть получено меньше 2 элементов) или последнее значение ATR равно EMPTY_VALUE, данные помечаются как неготовые. В противном случае текущие значения (индекс 0) и предыдущие значения (индекс 1) присваиваются глобальным переменным CurrentNrtrAtr, PreviousNrtrAtr, CurrentTrendDirection, PreviousTrendDirection, CurrentUpSupport, PreviousUpSupport, CurrentDownSupport и PreviousDownSupport. После успешного копирования всех буферов значение CurrentTrendValue вычисляется так: CurrentUpSupport – при положительном направлении, CurrentDownSupport – при отрицательном, иначе EMPTY_VALUE. Аналогично вычисляется и PreviousTrendValue. Если любое из значений тренда равно EMPTY_VALUE, флаг готовности сбрасывается.
Затем счетчик попыток увеличивается, и перед новой попыткой выполняется пауза через Sleep. Если после повторных попыток данные по-прежнему недоступны, выводится ошибка и возвращается false; в противном случае возвращается true, что подтверждает успешное получение данных. После получения данных можно перейти к их анализу для генерации сигнала. Сначала определим несколько вспомогательных функций.
//+------------------------------------------------------------------+ //| Count open positions | //+------------------------------------------------------------------+ int CountOpenPositions() { int count = 0; //--- Initialize count int totalPositions = PositionsTotal(); //--- Get total positions for (int i = 0; i < totalPositions; i++) { //--- Loop positions if (PositionGetSymbol(i) != Symbol()) continue; //--- Skip wrong symbol if (MagicNumber != 0 && PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; //--- Skip wrong magic count++; //--- Increment count } return count; //--- Return count } //+------------------------------------------------------------------+ //| Count open positions by type | //+------------------------------------------------------------------+ int CountOpenPositionsByType(ENUM_POSITION_TYPE positionType) { int count = 0; //--- Initialize count int totalPositions = PositionsTotal(); //--- Get total positions for (int i = 0; i < totalPositions; i++) { //--- Loop positions if (PositionGetSymbol(i) != Symbol()) continue; //--- Skip wrong symbol if (MagicNumber != 0 && PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; //--- Skip wrong magic if (PositionGetInteger(POSITION_TYPE) == positionType) count++; //--- Increment if match } return count; //--- Return count } //+------------------------------------------------------------------+ //| Open buy position | //+------------------------------------------------------------------+ bool OpenBuyPosition(string positionType) { double askPrice = SymbolInfoDouble(Symbol(), SYMBOL_ASK); //--- Get ask price double stopLossPrice = CalculateStopLoss(ORDER_TYPE_BUY, askPrice); //--- Calculate SL double takeProfitPrice = CalculateTakeProfit(ORDER_TYPE_BUY, askPrice); //--- Calculate TP double positionSize = CalculatePositionSize(stopLossPrice, askPrice); //--- Calculate size string orderComment = positionType + " Buy"; //--- Set comment if (!TradeObject.Buy(positionSize, Symbol(), 0, stopLossPrice, takeProfitPrice, orderComment)) { //--- Open buy PrintFormat("Failed to open BUY: %d", TradeObject.ResultRetcode()); //--- Print error return false; //--- Return failure } PrintFormat("%s Buy Position Opened Successfully", positionType); //--- Print success return true; //--- Return success } //+------------------------------------------------------------------+ //| Open sell position | //+------------------------------------------------------------------+ bool OpenSellPosition(string positionType) { double bidPrice = SymbolInfoDouble(Symbol(), SYMBOL_BID); //--- Get bid price double stopLossPrice = CalculateStopLoss(ORDER_TYPE_SELL, bidPrice); //--- Calculate SL double takeProfitPrice = CalculateTakeProfit(ORDER_TYPE_SELL, bidPrice); //--- Calculate TP double positionSize = CalculatePositionSize(stopLossPrice, bidPrice); //--- Calculate size string orderComment = positionType + " Sell"; //--- Set comment if (!TradeObject.Sell(positionSize, Symbol(), 0, stopLossPrice, takeProfitPrice, orderComment)) { //--- Open sell PrintFormat("Failed to open SELL: %d", TradeObject.ResultRetcode()); //--- Print error return false; //--- Return failure } PrintFormat("%s Sell Position Opened Successfully", positionType); //--- Print success return true; //--- Return success } //+------------------------------------------------------------------+ //| Close all buy positions | //+------------------------------------------------------------------+ void CloseAllBuyPositions() { int total = PositionsTotal(); //--- Get total positions for (int i = total - 1; i >= 0; i--) { //--- Loop backward if (PositionGetSymbol(i) != Symbol()) continue; //--- Skip wrong symbol if (PositionGetInteger(POSITION_TYPE) != POSITION_TYPE_BUY) continue; //--- Skip non-buy if (PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; //--- Skip wrong magic TradeObject.PositionClose(PositionGetInteger(POSITION_TICKET)); //--- Close position } Print("All Buy Positions Closed"); //--- Print closed } //+------------------------------------------------------------------+ //| Close all sell positions | //+------------------------------------------------------------------+ void CloseAllSellPositions() { int total = PositionsTotal(); //--- Get total positions for (int i = total - 1; i >= 0; i--) { //--- Loop backward if (PositionGetSymbol(i) != Symbol()) continue; //--- Skip wrong symbol if (PositionGetInteger(POSITION_TYPE) != POSITION_TYPE_SELL) continue; //--- Skip non-sell if (PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; //--- Skip wrong magic TradeObject.PositionClose(PositionGetInteger(POSITION_TICKET)); //--- Close position } Print("All Sell Positions Closed"); //--- Print closed } //+------------------------------------------------------------------+ //| Close all positions | //+------------------------------------------------------------------+ void CloseAllPositions() { int total = PositionsTotal(); //--- Get total positions for (int i = total - 1; i >= 0; i--) { //--- Loop backward if (PositionGetSymbol(i) != Symbol()) continue; //--- Skip wrong symbol if (PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; //--- Skip wrong magic TradeObject.PositionClose(PositionGetInteger(POSITION_TICKET)); //--- Close position } Print("All Positions Closed"); //--- Print closed } //+------------------------------------------------------------------+ //| Calculate stop loss | //+------------------------------------------------------------------+ double CalculateStopLoss(ENUM_ORDER_TYPE orderType, double entryPrice) { double stopLossPrice = 0; //--- Initialize SL price if (StopLossMode == SL_FIXED) { //--- Check fixed mode if (DefaultStopLossPoints == 0) return 0; //--- Return zero if none double pointValue = SymbolInfoDouble(Symbol(), SYMBOL_POINT); //--- Get point value if (orderType == ORDER_TYPE_BUY) stopLossPrice = entryPrice - DefaultStopLossPoints * pointValue; //--- Set buy SL if (orderType == ORDER_TYPE_SELL) stopLossPrice = entryPrice + DefaultStopLossPoints * pointValue; //--- Set sell SL } else { //--- Handle auto mode if (orderType == ORDER_TYPE_BUY) stopLossPrice = entryPrice - PreviousNrtrAtr * AtrMultiplierForStopLoss; //--- Set buy SL if (orderType == ORDER_TYPE_SELL) stopLossPrice = entryPrice + PreviousNrtrAtr * AtrMultiplierForStopLoss; //--- Set sell SL } return NormalizeDouble(stopLossPrice, _Digits); //--- Normalize SL } //+------------------------------------------------------------------+ //| Calculate take profit | //+------------------------------------------------------------------+ double CalculateTakeProfit(ENUM_ORDER_TYPE orderType, double entryPrice) { double takeProfitPrice = 0; //--- Initialize TP price if (TakeProfitMode == TP_FIXED) { //--- Check fixed mode if (DefaultTakeProfitPoints == 0) return 0; //--- Return zero if none double pointValue = SymbolInfoDouble(Symbol(), SYMBOL_POINT); //--- Get point value if (orderType == ORDER_TYPE_BUY) takeProfitPrice = entryPrice + DefaultTakeProfitPoints * pointValue; //--- Set buy TP if (orderType == ORDER_TYPE_SELL) takeProfitPrice = entryPrice - DefaultTakeProfitPoints * pointValue; //--- Set sell TP } else { //--- Handle auto mode if (orderType == ORDER_TYPE_BUY) takeProfitPrice = entryPrice + PreviousNrtrAtr * AtrMultiplierForTakeProfit; //--- Set buy TP if (orderType == ORDER_TYPE_SELL) takeProfitPrice = entryPrice - PreviousNrtrAtr * AtrMultiplierForTakeProfit; //--- Set sell TP } return NormalizeDouble(takeProfitPrice, _Digits); //--- Normalize TP } //+------------------------------------------------------------------+ //| Calculate position size | //+------------------------------------------------------------------+ double CalculatePositionSize(double stopLossPrice, double entryPrice) { double size = DefaultLotSize; //--- Set default size if (RiskDefaultSize == RISK_DEFAULT_AUTO) { //--- Check auto risk if (stopLossPrice == 0) stopLossPrice = 200; //--- Set default SL if zero double riskBaseAmount = 0; //--- Initialize base amount double tickValue = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_VALUE); //--- Get tick value if (RiskBase == RISK_BASE_BALANCE) riskBaseAmount = AccountInfoDouble(ACCOUNT_BALANCE); //--- Set balance base else if (RiskBase == RISK_BASE_EQUITY) riskBaseAmount = AccountInfoDouble(ACCOUNT_EQUITY); //--- Set equity base else if (RiskBase == RISK_BASE_FREEMARGIN) riskBaseAmount = AccountInfoDouble(ACCOUNT_MARGIN_FREE); //--- Set free margin base double stopLossDistancePoints = MathAbs(entryPrice - stopLossPrice) / SymbolInfoDouble(Symbol(), SYMBOL_POINT); //--- Compute distance size = (riskBaseAmount * MaxRiskPerTrade / 100) / (stopLossDistancePoints * tickValue); //--- Compute size } double lotStep = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP); //--- Get lot step double maxLot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MAX); //--- Get max lot double minLot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); //--- Get min lot size = MathFloor(size / lotStep) * lotStep; //--- Step size if (size > MaxLotSize) size = MaxLotSize; //--- Clamp max size if (size > maxLot) size = maxLot; //--- Clamp symbol max if (size < MinLotSize || size < minLot) size = 0; //--- Clamp min size return size; //--- Return size }
Для вспомогательных функций создаем CountOpenPositions для подсчета всех открытых позиций, соответствующих текущему символу и магическому числу. Внутри инициализируется счетчик, через PositionsTotal получаем общее число позиций и в цикле от 0 до этого значения используем PositionGetSymbol для пропуска несовпадающих символов, а PositionGetInteger с POSITION_MAGIC – для фильтрации по MagicNumber, если оно задано; для подходящих позиций счетчик увеличивается, после чего функция возвращает результат. Аналогично, CountOpenPositionsByType подсчитывает позиции заданного типа, например BUY или SELL: цикл остается тем же, но добавляется проверка через PositionGetInteger по POSITION_TYPE на совпадение со входным параметром positionType.
Для открытия сделок определяем функцию OpenBuyPosition, которая принимает строку positionType для формирования комментария. Через SymbolInfoDouble с SYMBOL_ASK получаем цену ask, отдельными функциями рассчитываем стоп-лосс и тейк-профит, определяем размер позиции и формируем комментарий вида "positionType + ' Buy'". Затем пытаемся открыть покупку через TradeObject.Buy: если попытка неудачна, выводим сообщение с кодом возврата, иначе сообщаем об успехе, после чего возвращаем результат. OpenSellPosition работает по той же схеме для продаж: использует цену bid из SYMBOL_BID, соответствующим образом рассчитывает стопы и выполняет TradeObject.Sell с комментарием "Sell".
Для закрытия позиций CloseAllBuyPositions безопасно проходит цикл в обратном порядке – от PositionsTotal - 1 до 0, пропуская несовпадающие символы, позиции с типом, отличным от BUY (через POSITION_TYPE_BUY), и несовпадающие магические числа; подходящие позиции закрываются через TradeObject.PositionClose по тикету из POSITION_TICKET, после чего выводится подтверждение. CloseAllSellPositions делает то же самое для продаж, проверяя POSITION_TYPE_SELL. CloseAllPositions закрывает все позиции, соответствующие символу и магическому числу, без фильтрации по типу, и выводит результат.
В CalculateStopLoss вычисляется цена стоп-лосса на основе типа ордера и цены входа. В фиксированном режиме StopLossMode, если число пунктов равно нулю, функция возвращает ноль; иначе она получает значение pointValue и для BUY или SELL вычитает либо прибавляет количество пунктов, умноженное на pointValue. В автоматическом режиме значение корректируется вычитанием или прибавлением PreviousNrtrAtr * AtrMultiplierForStopLoss с последующей нормализацией по числу знаков. CalculateTakeProfit работает по тому же принципу, используя TakeProfitMode и AtrMultiplierForTakeProfit для автоматической корректировки. Наконец, CalculatePositionSize начинает с лота по умолчанию, но при автоматическом риске задает расстояние по умолчанию, если стоп-лосс равен нулю, определяет базовую сумму риска, получает значение тика tickValue, вычисляет расстояние в пунктах и рассчитывает размер позиции как сумму риска, деленную на произведение расстояния и tickValue. Далее размер позиции округляется в соответствии с шагом лота через MathFloor, ограничивается допустимыми минимальными и максимальными значениями с учетом ограничений символа и возвращается. Теперь мы можем уверенно использовать эти функции для проверки сигналов и открытия позиций.
//+------------------------------------------------------------------+ //| Check for entry signals | //+------------------------------------------------------------------+ void CheckForEntrySignals() { bool buySignal = (CurrentUpSupport != EMPTY_VALUE) && (PreviousUpSupport == EMPTY_VALUE); //--- Check buy signal (start of long support) bool sellSignal = (CurrentDownSupport != EMPTY_VALUE) && (PreviousDownSupport == EMPTY_VALUE); //--- Check sell signal (start of short support) if (buySignal) { //--- Handle buy signal string currUpStr = DoubleToString(CurrentUpSupport, _Digits); string prevUpStr = (PreviousUpSupport == EMPTY_VALUE) ? "EMPTY" : DoubleToString(PreviousUpSupport, _Digits); PrintFormat("Buy Signal Detected: Start of Long Support (Buffer 1 Recent: %s, Older: %s)", currUpStr, prevUpStr); //--- Print buy signal if ((CountOpenPositionsByType(POSITION_TYPE_BUY) == 0 && HedgeMode) || CountOpenPositions() < MaxPositions) { //--- Check open buys OpenBuyPosition("Initial Signal"); //--- Open buy } else { //--- Handle rejected Print("Buy Trade Rejected: Maximum positions reached or hedge mode restricts"); //--- Print rejected } } if (sellSignal) { //--- Handle sell signal string currDownStr = DoubleToString(CurrentDownSupport, _Digits); string prevDownStr = (PreviousDownSupport == EMPTY_VALUE) ? "EMPTY" : DoubleToString(PreviousDownSupport, _Digits); PrintFormat("Sell Signal Detected: Start of Short Support (Buffer 2 Recent: %s, Older: %s)", currDownStr, prevDownStr); //--- Print sell signal if ((CountOpenPositionsByType(POSITION_TYPE_SELL) == 0 && HedgeMode) || CountOpenPositions() < MaxPositions) { //--- Check open sells OpenSellPosition("Initial Signal"); //--- Open sell } else { //--- Handle rejected Print("Sell Trade Rejected: Maximum positions reached or hedge mode restricts"); //--- Print rejected } } }
Здесь реализуем функцию CheckForEntrySignals для выявления и обработки потенциальных входов на основе переходов канала NRTR. В этой функции сигнал BUY считается сформированным, когда CurrentUpSupport не равен EMPTY_VALUE, а PreviousUpSupport равен EMPTY_VALUE, то есть начинается уровень поддержки для восходящего канала. Аналогичным образом, сигнал SELL срабатывает, если CurrentDownSupport имеет корректное значение, а PreviousDownSupport равен EMPTY_VALUE, что указывает на начало уровня поддержки для нисходящего канала.
Если обнаружен сигнал BUY, мы преобразуем значения поддержки в строки, где для предыдущей поддержки EMPTY_VALUE заменяется на "EMPTY", выводим отформатированное сообщение с деталями обнаружения из буфера 1 и проверяем условия: если хеджирование включено и открытых покупок нет либо если общее число позиций меньше MaxPositions, вызываем OpenBuyPosition с комментарием "Initial Signal"; в противном случае выводим сообщение об отклонении сигнала. Для сигнала SELL схема та же: форматируем и выводим детали из буфера 2, проверяем, что при включенном хеджировании открытых продаж нет либо что лимит по позициям еще не достигнут, после чего открываем продажу через OpenSellPosition или выводим сообщение об отклонении сигнала. Теперь эту функцию можно вызывать в обработчике тиков, чтобы получать результаты проверки.
//+------------------------------------------------------------------+ //| Handle tick event | //+------------------------------------------------------------------+ void OnTick() { ProcessEachTick(); //--- Process tick } //+------------------------------------------------------------------+ //| Process each tick | //+------------------------------------------------------------------+ void ProcessEachTick() { if (!FetchIndicatorData()) return; //--- Fetch indicator data static datetime lastBarTime = WRONG_VALUE; //--- Initialize last bar time datetime currentBarTime = iTime(Symbol(), Period(), 0); //--- Get current bar time static int newBarTicks = 0; //--- Initialize new bar ticks if (currentBarTime == lastBarTime) { //--- Check same bar newBarTicks++; //--- Increment ticks if (newBarTicks > 1) return; //--- Skip if more than 1 } else { //--- New bar newBarTicks = 0; //--- Reset ticks lastBarTime = currentBarTime; //--- Update last time } CheckForEntrySignals(); //--- Check entry signals }
В обработчике событий OnTick, мы просто вызываем ProcessEachTick, централизуя логику обработки ценовых обновлений. В ProcessEachTick сначала пытаемся получить свежие данные индикатора через FetchIndicatorData и сразу выходим при ошибке, чтобы решения принимались только на основе корректной информации. Чтобы сократить лишнюю обработку и сосредоточиться на новых барах, используем статическую переменную lastBarTime, инициализированную WRONG_VALUE, и через iTime получаем время открытия текущего бара для символа и периода. Статическая переменная newBarTicks инициализируется нулем и отслеживает число тиков внутри одного бара.
Если время текущего бара совпадает с lastBarTime, то есть это тот же бар, увеличиваем newBarTicks и без дальнейших действий выходим, если значение стало больше 1, чтобы не выполнять лишние проверки на последующих тиках. Иначе, при появлении нового бара, сбрасываем newBarTicks в 0, обновляем lastBarTime и продолжаем обработку. Затем вызываем CheckForEntrySignals, чтобы оценить обновленные данные и при необходимости выполнить новые входы в рынок. После компиляции получаем следующий результат.

По изображению видно, что сигнал формируется и сделка исполняется в момент его появления. Теперь остается обработать стратегию выхода: закрытие по противоположным сигналам и виртуальные закрытия.
//+------------------------------------------------------------------+ //| Check for exit signals | //+------------------------------------------------------------------+ void CheckForExitSignals() { bool exitBuySignal = false; //--- Initialize buy exit bool exitSellSignal = false; //--- Initialize sell exit if (CloseOnReversalSignal) { //--- Check reversal close exitBuySignal = (CurrentDownSupport != EMPTY_VALUE) && (PreviousDownSupport == EMPTY_VALUE); //--- Set buy exit (start of short) exitSellSignal = (CurrentUpSupport != EMPTY_VALUE) && (PreviousUpSupport == EMPTY_VALUE); //--- Set sell exit (start of long) } if (exitBuySignal) { //--- Handle buy exit string currDownStr = DoubleToString(CurrentDownSupport, _Digits); string prevDownStr = (PreviousDownSupport == EMPTY_VALUE) ? "EMPTY" : DoubleToString(PreviousDownSupport, _Digits); PrintFormat("Exit Buy Signal Detected (Reversal): Start of Short Support (Buffer 2 Recent: %s, Older: %s)", currDownStr, prevDownStr); //--- Print buy exit CloseAllBuyPositions(); //--- Close buys } if (exitSellSignal) { //--- Handle sell exit string currUpStr = DoubleToString(CurrentUpSupport, _Digits); string prevUpStr = (PreviousUpSupport == EMPTY_VALUE) ? "EMPTY" : DoubleToString(PreviousUpSupport, _Digits); PrintFormat("Exit Sell Signal Detected (Reversal): Start of Long Support (Buffer 1 Recent: %s, Older: %s)", currUpStr, prevUpStr); //--- Print sell exit CloseAllSellPositions(); //--- Close sells } } //+------------------------------------------------------------------+ //| Close on virtual take profit | //+------------------------------------------------------------------+ void CloseOnVirtualTakeProfit(ulong ticket) { if (!PositionSelectByTicket(ticket)) return; //--- Select position double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get entry price double virtualTakeProfit = 0; //--- Initialize virtual TP double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK); //--- Get ask double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID); //--- Get bid if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy virtualTakeProfit = entryPrice + DefaultTakeProfitPoints * SymbolInfoDouble(Symbol(), SYMBOL_POINT); //--- Set buy TP if (virtualTakeProfit <= bid && CloseOnTakeProfitHit) { //--- Check hit TradeObject.PositionClose(ticket); //--- Close position PrintFormat("Position Closed on Virtual TP: Ticket %llu, Price Hit %.5f", ticket, virtualTakeProfit); //--- Print closed } } else { //--- Handle sell virtualTakeProfit = entryPrice - DefaultTakeProfitPoints * SymbolInfoDouble(Symbol(), SYMBOL_POINT); //--- Set sell TP if (virtualTakeProfit >= ask && CloseOnTakeProfitHit) { //--- Check hit TradeObject.PositionClose(ticket); //--- Close position PrintFormat("Position Closed on Virtual TP: Ticket %llu, Price Hit %.5f", ticket, virtualTakeProfit); //--- Print closed } } } //+------------------------------------------------------------------+ //| Close on virtual stop loss | //+------------------------------------------------------------------+ void CloseOnVirtualStopLoss(ulong ticket) { if (!PositionSelectByTicket(ticket)) return; //--- Select position double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get entry price double virtualStopLoss = 0; //--- Initialize virtual SL double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK); //--- Get ask double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID); //--- Get bid if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy virtualStopLoss = entryPrice - DefaultStopLossPoints * SymbolInfoDouble(Symbol(), SYMBOL_POINT); //--- Set buy SL if (virtualStopLoss >= ask && CloseOnStopLossHit) { //--- Check hit TradeObject.PositionClose(ticket); //--- Close position PrintFormat("Position Closed on Virtual SL: Ticket %llu, Price Hit %.5f", ticket, virtualStopLoss); //--- Print closed } } else { //--- Handle sell virtualStopLoss = entryPrice + DefaultStopLossPoints * SymbolInfoDouble(Symbol(), SYMBOL_POINT); //--- Set sell SL if (virtualStopLoss <= bid && CloseOnStopLossHit) { //--- Check hit TradeObject.PositionClose(ticket); //--- Close position PrintFormat("Position Closed on Virtual SL: Ticket %llu, Price Hit %.5f", ticket, virtualStopLoss); //--- Print closed } } } //+------------------------------------------------------------------+ //| Handle virtual closures | //+------------------------------------------------------------------+ void HandleVirtualClosures() { int total = PositionsTotal(); //--- Get total positions for (int i = total - 1; i >= 0; i--) { //--- Loop backward if (PositionGetSymbol(i) == "") continue; //--- Skip invalid ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Get ticket if (PositionGetString(POSITION_SYMBOL) != Symbol() || PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; //--- Skip wrong symbol/magic if (CloseOnStopLossHit) CloseOnVirtualStopLoss(ticket); //--- Check virtual SL if (CloseOnTakeProfitHit) CloseOnVirtualTakeProfit(ticket); //--- Check virtual TP } }
Сначала разрабатываем CheckForExitSignals для отслеживания условий, при которых нужно закрывать уже открытые позиции. Здесь инициализируем булевы флаги exitBuySignal и exitSellSignal значением false. Если включен CloseOnReversalSignal, то при обнаружении начала поддержки для продаж, когда текущий нижний уровень поддержки корректен, а предыдущий пуст, устанавливаем exitBuySignal = true, а при обнаружении начала поддержки для покупок – exitSellSignal = true. Когда возникает сигнал на выход из покупок, мы форматируем строки значений нижней поддержки, где EMPTY_VALUE заменяется на "EMPTY", выводим подробное сообщение из буфера 2 и вызываем CloseAllBuyPositions для закрытия всех длинных позиций. Аналогично, при сигнале на выход из продаж выводим данные из буфера 1 и вызываем CloseAllSellPositions.
CloseOnVirtualTakeProfit управляет программным закрытием по виртуальному тейк-профиту для заданного тикета. Сначала через PositionSelectByTicket выбираем позицию и, если это не удается, сразу выходим; затем получаем цену входа. Для покупок вычисляем virtualTakeProfit, прибавляя DefaultTakeProfitPoints * point к цене входа; для продаж вычитаем это значение. Получаем цены ask и bid, и если виртуальный уровень достигнут, то есть когда для покупки TP <= bid, для продажи TP >= ask, а опция CloseOnTakeProfitHit включена, закрываем позицию через TradeObject.PositionClose и выводим подтверждение с тикетом и ценой срабатывания.
Аналогичным образом, CloseOnVirtualStopLoss обрабатывает виртуальные стоп-лоссы. После выбора позиции и получения цены входа рассчитываем virtualStopLoss, вычитая пункты для покупок или прибавляя их для продаж. Используя ask и bid, при срабатывании уровня, то есть когда для покупки SL >= ask, для продажи SL <= bid, а опция CloseOnStopLossHit включена, закрываем позицию и выводим подробности. В HandleVirtualClosures выполняется обратный перебор всех позиций от PositionsTotal - 1 до 0 для безопасной итерации. Для каждой позиции, если не удается получить корректный символ, она пропускается; затем получаем тикет и продолжаем цикл, если символ или магическое число не совпадают. Если опция включена CloseOnStopLossHit, вызываем CloseOnVirtualStopLoss; если включена опция CloseOnTakeProfitHit, вызываем CloseOnVirtualTakeProfit, чтобы применять виртуальные проверки ко всем открытым сделкам. Также при включении нужно обрабатывать типы трейлинга – либо по пунктам, либо по уровням поддержки.
//+------------------------------------------------------------------+ //| Adjust to break even | //+------------------------------------------------------------------+ bool AdjustToBreakEven(ulong ticket) { if (TrailingType == TRAILING_NONE) return false; //--- Check trailing type if (!PositionSelectByTicket(ticket)) return false; //--- Select position MqlTradeRequest request = {}; //--- Declare request MqlTradeResult result = {}; //--- Declare result request.action = TRADE_ACTION_SLTP; //--- Set action SLTP request.position = ticket; //--- Set position ticket long positionType = PositionGetInteger(POSITION_TYPE); //--- Get type double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get entry price double currentStopLoss = PositionGetDouble(POSITION_SL); //--- Get current SL double currentTakeProfit = PositionGetDouble(POSITION_TP); //--- Get current TP string symbol = PositionGetString(POSITION_SYMBOL); //--- Get symbol double point = SymbolInfoDouble(symbol, SYMBOL_POINT); //--- Get point double currentPrice = (positionType == POSITION_TYPE_BUY) ? SymbolInfoDouble(symbol, SYMBOL_BID) : SymbolInfoDouble(symbol, SYMBOL_ASK); //--- Get current price double currentProfitPoints = (positionType == POSITION_TYPE_BUY) ? (currentPrice - entryPrice) / point : (entryPrice - currentPrice) / point; //--- Compute profit points double newStopLoss = 0; //--- Initialize new SL if (currentProfitPoints <= 0) return false; //--- Skip if no profit switch (TrailingType) { //--- Switch trailing type case TRAILING_POINTS: { //--- Handle points if (currentProfitPoints >= BreakEvenTriggerPoints) { //--- Check BE trigger double bePoints = BreakEvenPoints; //--- Set BE points newStopLoss = (positionType == POSITION_TYPE_BUY) ? NormalizeDouble(entryPrice + (bePoints * point), _Digits) : NormalizeDouble(entryPrice - (bePoints * point), _Digits); //--- Compute new SL if ((positionType == POSITION_TYPE_BUY && newStopLoss > currentStopLoss) || (positionType == POSITION_TYPE_SELL && newStopLoss < currentStopLoss)) { //--- Check improvement if (NormalizeDouble(newStopLoss, _Digits) != NormalizeDouble(currentStopLoss, _Digits)) { //--- Check different request.sl = newStopLoss; //--- Set SL request.tp = currentTakeProfit; //--- Set TP if (OrderSend(request, result)) { //--- Send order PrintFormat("Breakeven SL Adjusted for %s Position: Ticket %llu, New SL = %.5f (Triggered at %.0f points profit)", (positionType == POSITION_TYPE_BUY) ? "Buy" : "Sell", ticket, newStopLoss, currentProfitPoints); //--- Print adjusted return true; //--- Return success } } } } if (currentProfitPoints >= CurrentTrailingStopPoints) { //--- Check trailing trigger double trailPoints = currentProfitPoints - CurrentTrailingStepPoints; //--- Compute trail points newStopLoss = (positionType == POSITION_TYPE_BUY) ? NormalizeDouble(currentPrice - (CurrentTrailingStopPoints * point), _Digits) : NormalizeDouble(currentPrice + (CurrentTrailingStopPoints * point), _Digits); //--- Compute new SL if ((positionType == POSITION_TYPE_BUY && newStopLoss > currentStopLoss) || (positionType == POSITION_TYPE_SELL && newStopLoss < currentStopLoss)) { //--- Check improvement if (NormalizeDouble(newStopLoss, _Digits) != NormalizeDouble(currentStopLoss, _Digits)) { //--- Check different request.sl = newStopLoss; //--- Set SL request.tp = currentTakeProfit; //--- Set TP if (OrderSend(request, result)) { //--- Send order PrintFormat("Trailing SL Adjusted for %s Position: Ticket %llu, New SL = %.5f (Current Profit = %.0f points)", (positionType == POSITION_TYPE_BUY) ? "Buy" : "Sell", ticket, newStopLoss, currentProfitPoints); //--- Print adjusted return true; //--- Return success } } } } break; } case TRAILING_SUPPORT_LEVELS: { //--- Handle support levels double currentSupport = (positionType == POSITION_TYPE_BUY) ? CurrentUpSupport : CurrentDownSupport; //--- Get current support double previousSupport = (positionType == POSITION_TYPE_BUY) ? PreviousUpSupport : PreviousDownSupport; //--- Get previous support if (currentSupport == EMPTY_VALUE) return false; //--- Skip if invalid support if (currentProfitPoints >= BreakEvenTriggerPoints) { //--- Check BE trigger newStopLoss = currentSupport; //--- Set new SL to support bool isProfitable = (positionType == POSITION_TYPE_BUY && newStopLoss >= entryPrice) || (positionType == POSITION_TYPE_SELL && newStopLoss <= entryPrice); //--- Ensure beyond entry if (isProfitable && ((positionType == POSITION_TYPE_BUY && newStopLoss > currentStopLoss) || (positionType == POSITION_TYPE_SELL && newStopLoss < currentStopLoss))) { //--- Check improvement and profitable if (NormalizeDouble(newStopLoss, _Digits) != NormalizeDouble(currentStopLoss, _Digits)) { //--- Check different request.sl = newStopLoss; //--- Set SL request.tp = currentTakeProfit; //--- Set TP if (OrderSend(request, result)) { //--- Send order PrintFormat("Breakeven SL Adjusted to Support Level for %s Position: Ticket %llu, New SL = %.5f (Triggered at %.0f points profit)", (positionType == POSITION_TYPE_BUY) ? "Buy" : "Sell", ticket, newStopLoss, currentProfitPoints); //--- Print adjusted return true; //--- Return success } } } } if ((positionType == POSITION_TYPE_BUY && currentSupport > previousSupport) || (positionType == POSITION_TYPE_SELL && currentSupport < previousSupport)) { //--- Check support moved newStopLoss = currentSupport; //--- Set new SL to support bool isProfitable = (positionType == POSITION_TYPE_BUY && newStopLoss >= entryPrice) || (positionType == POSITION_TYPE_SELL && newStopLoss <= entryPrice); //--- Ensure beyond entry if (isProfitable && ((positionType == POSITION_TYPE_BUY && newStopLoss > currentStopLoss) || (positionType == POSITION_TYPE_SELL && newStopLoss < currentStopLoss))) { //--- Check improvement and profitable if (NormalizeDouble(newStopLoss, _Digits) != NormalizeDouble(currentStopLoss, _Digits)) { //--- Check different request.sl = newStopLoss; //--- Set SL request.tp = currentTakeProfit; //--- Set TP if (OrderSend(request, result)) { //--- Send order PrintFormat("SL Trailed to Support Level for %s Position: Ticket %llu, New SL = %.5f (Support Moved from %.5f to %.5f)", (positionType == POSITION_TYPE_BUY) ? "Buy" : "Sell", ticket, newStopLoss, previousSupport, currentSupport); //--- Print trailed return true; //--- Return success } } } } break; } default: break; } return false; //--- Return no adjustment } //+------------------------------------------------------------------+ //| Adjust all stop losses | //+------------------------------------------------------------------+ void AdjustAllStopLosses() { for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Loop backward if (PositionGetSymbol(i) == "") continue; //--- Skip invalid ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Get ticket AdjustToBreakEven(ticket); //--- Adjust to BE } }
Здесь создаем функцию AdjustToBreakEven для управления трейлингом и переносом в безубыток по конкретной позиции, определяемой ее тикетом. Если TrailingType равен TRAILING_NONE или позицию не удается выбрать через PositionSelectByTicket, сразу возвращаем false. Подготавливаем структуры MqlTradeRequest и MqlTradeResult, задаем для запроса действие TRADE_ACTION_SLTP для изменения стоп-лосса и тейк-профита и присваиваем тикет позиции. Затем получаем тип позиции, цену входа, текущие стоп-лосс и тейк-профит, символ и значение пункта. Текущая цена – bid для покупок и ask для продаж; далее вычисляем currentProfitPoints как разницу относительно цены входа, нормализованную по point. Если прибыли нет, пропускаем дальнейшую обработку и возвращаем false.
В операторе switch по TrailingType для TRAILING_POINTS сначала проверяем, достигает ли прибыль уровня BreakEvenTriggerPoints; если да, вычисляем новый стоп-лосс на уровне цены входа плюс или минус – для покупок или продаж соответственно – BreakEvenPoints * point с последующей нормализацией. Затем проверяем, улучшает ли это текущий стоп, то есть выше ли он для покупок и ниже ли для продаж, и действительно ли новое значение отличается; после этого задаем в запросе стоп-лосс и тейк-профит, отправляем его через OrderSend и при успехе выводим сообщение, возвращая true. Далее, все в том же режиме points, если прибыль превышает CurrentTrailingStopPoints, рассчитываем трейлинг-стоп как текущую цену минус или плюс CurrentTrailingStopPoints * point, проверяем улучшение и отличие нового значения, отправляем изменение, при успехе выводим сообщение и возвращаем true.
Для TRAILING_SUPPORT_LEVELS получаем текущие и предыдущие уровни поддержки в зависимости от типа позиции и пропускаем обработку, если текущий уровень пуст. Если прибыль достигла порога безубытка, устанавливаем новый стоп на текущий уровень поддержки, убеждаемся, что он не хуже цены входа, проверяем улучшение и, если значение изменилось, отправляем запрос, после чего при успехе выводим сообщение. Если уровень поддержки сместился в благоприятную сторону, то есть выше для покупок и ниже для продаж, переносим стоп на текущую поддержку, убеждаемся, что новый уровень сохраняет прибыль, и при отличии от текущего значения отправляем запрос, выводим детали трейлинга и возвращаем true. Во всех остальных случаях ничего не делаем и возвращаем false. Функция AdjustAllStopLosses выполняет обратный перебор всех позиций от PositionsTotal - 1 до 0, пропускает недействительные символы, получает тикет каждой позиции и вызывает для нее AdjustToBreakEven, чтобы применить корректировки ко всем открытым сделкам. Теперь все эти функции можно вызывать из управляющей функции для обработки каждого тика. Итоговая функция выглядит следующим образом.
//+------------------------------------------------------------------+ //| Process each tick | //+------------------------------------------------------------------+ void ProcessEachTick() { if (!FetchIndicatorData()) return; //--- Fetch indicator data int positionCount = CountOpenPositions(); //--- Count positions if (positionCount > 0) { //--- Check positions exist CheckForExitSignals(); //--- Check exit signals } static datetime lastBarTime = WRONG_VALUE; //--- Initialize last bar time datetime currentBarTime = iTime(Symbol(), Period(), 0); //--- Get current bar time static int newBarTicks = 0; //--- Initialize new bar ticks if (currentBarTime == lastBarTime) { //--- Check same bar newBarTicks++; //--- Increment ticks if (newBarTicks > 1) return; //--- Skip if more than 1 } else { //--- New bar newBarTicks = 0; //--- Reset ticks lastBarTime = currentBarTime; //--- Update last time } CheckForEntrySignals(); //--- Check entry signals datetime currentTime = TimeCurrent(); //--- Get current time if (currentTime - LastTrailingTime >= TrailingFrequencySeconds) { //--- Check trailing time AdjustAllStopLosses(); //--- Adjust SLs LastTrailingTime = currentTime; //--- Update trailing time } HandleVirtualClosures(); //--- Handle virtual closures }
Далее вызываем эти функции по соответствующим условиям и после компиляции получаем следующий результат.

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

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

Заключение
В итоге мы разработали торговую систему Nick Rypock Trailing Reverse (NRTR) на языке MQL5, которая выявляет сигналы разворота через динамические каналы, поддерживает входы по тренду, хеджирование покупок и продаж, а также ограничения по числу позиций для ограничения объема открытого риска. Мы добавили функции управления рисками: автоматический расчет лота на основе средств счета, баланса или свободной маржи, а также фиксированные или скорректированные по ATR уровни стоп-лосса и тейк-профита.
Отказ от ответственности: Эта статья предназначена исключительно для образовательных целей. Торговля связана со значительными финансовыми рисками, и рыночная волатильность может привести к убыткам. Перед запуском этой программы на реальном рынке критически важны тщательное тестирование на исторических данных и строгое управление рисками.
Данная стратегия разворота NRTR позволяет работать с трендовыми движениями, используя хеджирование и защитные механизмы трейлинга, и служит хорошей основой для дальнейшей доработки вашей торговой системы. Удачной торговли!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21096
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От начального до среднего уровня: События в объектах (II)
Советник для размещения ордеров на основе риска с графическим интерфейсом на графике (Часть 2): Добавление интерактивности и логики
Осваиваем графики Kagi в MQL5 (Часть I): Создание движка графика Kagi
От начального до среднего уровня: События в объектах (I)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования