Автоматизация торговых стратегий на MQL5 (Часть 22): Создание системы зонального восстановления для трендовой торговли по индикатору Envelopes
Введение
В предыдущей статье (Часть 21), мы рассмотрели торговую стратегию на основе нейронной сети, усиленную адаптивными темпами обучения для повышения точности прогнозирования рыночных движений на MetaQuotes Language 5 (MQL5). В части 22 мы переключаем внимание на создание системы зонального восстановления (Zone Recovery System), с трендовой торговой стратегией на основе конвертов в сочетании с индексом относительной силы (RSI) и конвертами для автоматизации сделок и эффективного управления убытками. Мы рассмотрим следующие темы:
- Понимание архитектуры тренда зонального восстановления
- Реализация средствами MQL5
- Тестирование на истории
- Заключение
В итоге у вас будет надежная торговая система на основе MQL5, разработанная для динамичных рыночных условий, готовая к внедрению и тестированию.
Понимание архитектуры тренда зонального восстановления
Зональное восстановление (zone recovery) — это интеллектуальная торговая стратегия, которая помогает нам превратить потенциальные убытки в прибыль, совершая дополнительные сделки, когда рынок движется против нас, стремясь выйти в плюс или безубыток. Представьте, что вы покупаете валютную пару, ожидая роста, но она падает — в дело вступает зональное восстановление, устанавливая ценовой диапазон или "зону", в которой мы совершаем противоположные сделки, чтобы компенсировать потери, если цена вернется назад. Мы планируем разработать автоматизированную систему на языке MetaQuotes Language 5 (MQL5), которая будет использовать эту концепцию для торговли на валютных рынках, сохраняя при этом низкие риски и максимизируя прибыль.
Мы используем два технических индикатора, чтобы определить оптимальное время для входа в сделки. Один из индикаторов будет отслеживать активность рынка, гарантируя, что мы будем совершать сделки только тогда, когда наблюдается сильное движение в одном направлении, избегая слабых или нечетких сигналов. Другой (конверты) строит канал вокруг средней цены рынка, показывая, когда цены слишком сильно растут или падают, сигнализируя о вероятном моменте отскока, подходящем для входа в рынок. Эти индикаторы будут работать вместе, чтобы находить сделки с высокой вероятностью успеха, когда цена готова развернуться в рамках тренда.
Вот как мы планируем всё это объединить: мы начнем с открытия сделки, когда наши индикаторы будут сигнализировать о развороте, например, когда цена достигнет края канала конвертов с сильным импульсом. Если рынок двинется в неправильном направлении, мы активируем зональное восстановление, открыв контр-сделки в пределах установленной нами ценовой зоны, размер которой тщательно подобран с учетом баланса риска и восстановления. Мы ограничим количество сделок, чтобы избежать излишнего числа сделок и обеспечить дисциплинированность системы. Такая схема позволит нам использовать трендовые возможности и одновременно обеспечит подстраховку на случай, если что-то пойдет не по плану, адаптируясь как к нестабильному, так и к спокойному рынку. Ниже представлен план реализации.

Реализация средствами MQL5
Чтобы создать программу на MQL5, откройте MetaEditor, перейдите в "Навигатор", выберите папку Indicators, кликните "Создать" и следуйте инструкциям для создания файла. Начнем с объявления некоторых входных переменных, которые помогут нам легко контролировать ключевые параметры программы.
//+------------------------------------------------------------------+ //| Envelopes Trend Bounce with Zone Recovery EA.mq5 | //| Copyright 2025, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property strict #include <Trade/Trade.mqh> //--- Include trade library enum TradingLotSizeOptions { FIXED_LOTSIZE, UNFIXED_LOTSIZE }; //--- Define lot size options input group "======= EA GENERAL SETTINGS =======" input TradingLotSizeOptions lotOption = UNFIXED_LOTSIZE; // Lot Size Option input double initialLotSize = 0.01; // Initial Lot Size input double riskPercentage = 1.0; // Risk Percentage (%) input int riskPoints = 300; // Risk Points input int magicNumber = 123456789; // Magic Number input int maxOrders = 1; // Maximum Initial Positions input double zoneTargetPoints = 600; // Zone Target Points input double zoneSizePoints = 300; // Zone Size Points input bool restrictMaxOrders = true; // Apply Maximum Orders Restriction
Здесь мы закладываем основу для нашей системы зонального восстановления для торговли по тренду с использованием конвертов на MQL5, устанавливая необходимые компоненты и пользовательские параметры. Начнем с включения библиотеки <Trade/Trade.mqh>, которая предоставляет класс CTrade для выполнения торговых операций, таких как открытие и закрытие позиций. Это крайне важно, поскольку предоставляет нашему советнику инструменты бесперебойного взаимодействия с рынком, особенно для инициирования ордеров. Ниже вы найдете инструкцию по открытию файла.

Затем мы определяем параметры перечисления TradingLotSizeOptions с двумя значениями: FIXED_LOTSIZE и UNFIXED_LOTSIZE. Это позволяет нам предлагать пользователям выбор между постоянным размером лота и размером, динамически изменяющимся в зависимости от параметров риска, обеспечивая гибкость в определении размера сделки в соответствии с различными стилями торговли. Далее мы настраиваем параметры ввода в группе EA GENERAL SETTINGS (общие настройки советника), которые пользователи могут изменить в платформе MetaTrader 5.
Параметр lotOption, по умолчанию равный UNFIXED_LOTSIZE, определяет, будет ли использоваться фиксированный размер лота или размер лота, зависящий от риска. initialLotSize (0.01) задает размер лота для фиксированных сделок, в то время как riskPercentage (1.0%) и riskPoints (300) определяют процент от баланса счета и расстояние стоп-лосса для динамического определения размера лота. Эти настройки определяют, какой риск мы принимаем на себя за каждую сделку, обеспечивая соответствие советника допустимому уровню риска для пользователя.
Присвоим уникальный magicNumber (123456789) для идентификации сделок нашего советника, что позволяет нам отличать их от других сделок на том же счете. входные параметры maxOrders (1) и restrictMaxOrders (true) ограничивают количество начальных позиций, чтобы советник не мог открывать слишком много сделок одновременно. Наконец, параметры zoneTargetPoints (600) и zoneSizePoints (300) определяют целевой показатель прибыли и размер зонального восстановления в пунктах, задавая границы нашей стратегии. После компиляции получаем следующий результат.

После загрузки входных данных мы можем приступить к определению основной логики всей системы. Начнем с объявления некоторых структур и классов, которые мы будем использовать, поскольку хотим применить объектно-ориентированное программирование (OOP).
class MarketZoneTrader { private: //--- Trade State Definition enum TradeState { INACTIVE, RUNNING, TERMINATING }; //--- Define trade lifecycle states //--- Data Structures struct TradeMetrics { bool operationSuccess; //--- Track operation success double totalVolume; //--- Sum closed trade volumes double netProfitLoss; //--- Accumulate profit/loss }; struct ZoneBoundaries { double zoneHigh; //--- Upper recovery zone boundary double zoneLow; //--- Lower recovery zone boundary double zoneTargetHigh; //--- Upper profit target double zoneTargetLow; //--- Lower profit target }; struct TradeConfig { string marketSymbol; //--- Trading symbol double openPrice; //--- Position entry price double initialVolume; //--- Initial trade volume long tradeIdentifier; //--- Magic number string tradeLabel; //--- Trade comment ulong activeTickets[]; //--- Active position tickets ENUM_ORDER_TYPE direction; //--- Trade direction double zoneProfitSpan; //--- Profit target range double zoneRecoverySpan; //--- Recovery zone range double accumulatedBuyVolume; //--- Total buy volume double accumulatedSellVolume; //--- Total sell volume TradeState currentState; //--- Current trade state }; struct LossTracker { double tradeLossTracker; //--- Track cumulative profit/loss }; };
Здесь мы определяем основную структуру нашей системы для торговли по трендам с использованием конвертов на MQL5, вводя класс MarketZoneTrader с акцентом на приватный раздел, содержащий определения торговых состояний и структуры данных. Эта логика поможет организовать критически важные компоненты, необходимые для управления сделками, отслеживания зонального восстановления и мониторинга производительности. Начнем с определения класса MarketZoneTrader, который служит основой нашего советника, инкапсулируя логику нашей торговой стратегии.
В приватном разделе добавим перечисление TradeState с тремя состояниями - INACTIVE (неактивно), RUNNING (работает) и TERMINATING (завершение). Эти состояния позволяют нам отслеживать жизненный цикл наших торговых операций, гарантируя, что мы знаем, находится ли советник в режиме ожидания, активно управляет сделками или закрывает позиции. Это крайне важно для поддержания контроля над торговым процессом, поскольку помогает нам координировать такие действия, как открытие восстановительных сделок или завершение позиций.
Далее создаем структуру TradeMetrics для хранения ключевых данных о результатах наших сделок. Она включает в себя параметр operationSuccess для отслеживания успешности торговых операций (например, закрытия позиций), параметр totalVolume для суммирования объемов закрытых сделок и параметр netProfitLoss для накопления прибыли или убытка от этих сделок. Эта структура помогает нам оценивать результаты наших торговых операций, предоставляя четкое представление о показателях эффективности в период восстановления или закрытия.
Затем определяем структуру ZoneBoundaries, которая содержит уровни цен для нашей стратегии зонального восстановления. Переменные zoneHigh и zoneLow обозначают верхнюю и нижнюю границы зонального восстановления, где мы совершаем контрсделки для минимизации потерь. Параметры zoneTargetHigh и zoneTargetLow устанавливают целевые уровни прибыли выше и ниже зоны, определяя, когда мы можем прибыльно выйти из сделок. Эти границы имеют важное значение для нашей стратегии, поскольку они определяют, когда следует инициировать действия по восстановлению или закрытию позиций. Вот как это будет выглядеть в визуализации, чтобы у вас было четкое представление о том, зачем нам нужна эта структура.

Параметры торговой конфигурации хранятся в структуре TradeConfig. Она включает в себя marketSymbol для валютной пары, openPrice для цены входа и initialVolume для размера сделки. tradeIdentifier содержит наш уникальный магический номер, а tradeLabel добавляет комментарий для идентификации сделки. Массив activeTickets отслеживает открытые позиции, а direction указывает, является ли сделка покупкой или продажей. Мы также используем zoneProfitSpan и zoneRecoverySpan для определения размеров целевой прибыли и зонального восстановления в ценовых единицах, а также параметры accumulatedBuyVolume и accumulatedSellVolume для отслеживания общего объема для каждого типа сделок. Переменная currentState, использующая перечисление TradeState, отслеживает состояние торговли, связывая все воедино.
Наконец, мы добавляем структуру LossTracker с единственной переменной tradeLossTracker для отслеживания совокупной прибыли или убытка по всем сделкам. Это помогает нам оценить финансовые последствия наших мер по восстановлению, гарантируя, что мы сможем скорректировать нашу стратегию, если убытки станут слишком большими. Затем мы можем определить несколько переменных-членов, которые помогут хранить другую, менее важную, но необходимую информацию о торговых операциях.
//--- Member Variables TradeConfig m_tradeConfig; //--- Store trade configuration ZoneBoundaries m_zoneBounds; //--- Store zone boundaries LossTracker m_lossTracker; //--- Track profit/loss string m_lastError; //--- Store error message int m_errorStatus; //--- Store error code CTrade m_tradeExecutor; //--- Manage trade execution int m_handleRsi; //--- RSI indicator handle int m_handleEnvUpper; //--- Upper Envelopes handle int m_handleEnvLower; //--- Lower Envelopes handle double m_rsiBuffer[]; //--- RSI data buffer double m_envUpperBandBuffer[]; //--- Upper Envelopes buffer double m_envLowerBandBuffer[]; //--- Lower Envelopes buffer TradingLotSizeOptions m_lotOption; //--- Lot size option double m_initialLotSize; //--- Fixed lot size double m_riskPercentage; //--- Risk percentage int m_riskPoints; //--- Risk points int m_maxOrders; //--- Maximum positions bool m_restrictMaxOrders; //--- Position restriction flag double m_zoneTargetPoints; //--- Profit target points double m_zoneSizePoints; //--- Recovery zone points
Определим ключевые переменные-члены в приватном разделе класса MarketZoneTrader для управления настройками торговли, зональным восстановлением и данными индикаторов. Используем m_tradeConfig (структура TradeConfig) для хранения сведений о сделках, таких как символ и направление, m_zoneBounds (структура ZoneBoundaries) для зонального восстановления и целевых цен прибыли, а также m_lossTracker (структура LossTracker) для отслеживания прибыли или убытков. Для регистрации ошибок используются m_lastError (строка) и m_errorStatus (целое число), объект m_tradeExecutor (класс CTrade) выполняет торговые операции.
Хэндлы индикаторов — m_handleRsi, m_handleEnvUpper, m_handleEnvLower — обеспечивают доступ к данным RSI и конвертов. Массивы m_rsiBuffer, m_envUpperBandBuffer и m_envLowerBandBuffer хранят их значения. Мы храним входные параметры в переменных m_lotOption (TradingLotSizeOptions), m_initialLotSize, m_riskPercentage, m_riskPoints, m_maxOrders, m_restrictMaxOrders, m_zoneTargetPoints и m_zoneSizePoints для управления размерами лотов, лимитами позиций и размерами зон. Эти переменные составляют основу для управления сделками и индикаторами, подготавливая нас к будущей торговой логике. Далее нам необходимо определить несколько вспомогательных функций, которые мы будем часто использовать в программе.
//--- Error Handling void logError(string message, int code) { //--- Error Logging Start m_lastError = message; //--- Store error message m_errorStatus = code; //--- Store error code Print("Error: ", message); //--- Log error to Experts tab //--- Error Logging End } //--- Market Data Access double getMarketVolumeStep() { //--- Volume Step Retrieval Start return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_VOLUME_STEP); //--- Retrieve broker's volume step //--- Volume Step Retrieval End } double getMarketAsk() { //--- Ask Price Retrieval Start return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_ASK); //--- Retrieve ask price //--- Ask Price Retrieval End } double getMarketBid() { //--- Bid Price Retrieval Start return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_BID); //--- Retrieve bid price //--- Bid Price Retrieval End }
Здесь мы добавляем важные вспомогательные функции для обработки ошибок и доступа к рыночным данным. Функция logError сохраняет message в m_lastError, code в m_errorStatus и отображает сообщение с помощью Print на вкладке "Эксперты" для отладки. Функция getMarketVolumeStep использует SymbolInfoDouble с SYMBOL_VOLUME_STEP для получения шага изменения объема у брокера для m_tradeConfig.marketSymbol, обеспечивая корректность размеров сделок. Функции getMarketAsk и getMarketBid получают цены bid и ask, используя SymbolInfoDouble с SYMBOL_ASK и SYMBOL_BID соответственно для точного ценообразования сделок.
Теперь мы можем определить основные функции для осуществления торговых операций. Начнем с тех, которые помогут нам инициализировать операции, хранить торговые ордера для отслеживания и мониторинга, а также закрывать сделки, поскольку это наименее сложная логика.
//--- Trade Initialization bool configureTrade(ulong ticket) { //--- Trade Configuration Start if (!PositionSelectByTicket(ticket)) { //--- Select position by ticket logError("Failed to select ticket " + IntegerToString(ticket), INIT_FAILED); //--- Log selection failure return false; //--- Return failure } m_tradeConfig.marketSymbol = PositionGetString(POSITION_SYMBOL); //--- Set symbol m_tradeConfig.tradeLabel = __FILE__; //--- Set trade comment m_tradeConfig.tradeIdentifier = PositionGetInteger(POSITION_MAGIC); //--- Set magic number m_tradeConfig.direction = (ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE); //--- Set direction m_tradeConfig.openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Set entry price m_tradeConfig.initialVolume = PositionGetDouble(POSITION_VOLUME); //--- Set initial volume m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); //--- Set magic number for executor return true; //--- Return success //--- Trade Configuration End } //--- Trade Ticket Management void storeTradeTicket(ulong ticket) { //--- Ticket Storage Start int ticketCount = ArraySize(m_tradeConfig.activeTickets); //--- Get ticket count ArrayResize(m_tradeConfig.activeTickets, ticketCount + 1); //--- Resize ticket array m_tradeConfig.activeTickets[ticketCount] = ticket; //--- Store ticket //--- Ticket Storage End } //--- Trade Execution ulong openMarketTrade(ENUM_ORDER_TYPE tradeDirection, double tradeVolume, double price) { //--- Trade Opening Start ulong ticket = 0; //--- Initialize ticket if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, tradeDirection, tradeVolume, price, 0, 0, m_tradeConfig.tradeLabel)) { //--- Open position ticket = m_tradeExecutor.ResultOrder(); //--- Get ticket } else { Print("Failed to open trade: Direction=", EnumToString(tradeDirection), ", Volume=", tradeVolume); //--- Log failure } return ticket; //--- Return ticket //--- Trade Opening End } //--- Trade Closure void closeActiveTrades(TradeMetrics &metrics) { //--- Trade Closure Start for (int i = ArraySize(m_tradeConfig.activeTickets) - 1; i >= 0; i--) { //--- Iterate tickets in reverse if (m_tradeConfig.activeTickets[i] > 0) { //--- Check valid ticket if (m_tradeExecutor.PositionClose(m_tradeConfig.activeTickets[i])) { //--- Close position m_tradeConfig.activeTickets[i] = 0; //--- Clear ticket metrics.totalVolume += m_tradeExecutor.ResultVolume(); //--- Accumulate volume if ((ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE) == ORDER_TYPE_BUY) { //--- Check buy position metrics.netProfitLoss += m_tradeExecutor.ResultVolume() * (m_tradeExecutor.ResultPrice() - PositionGetDouble(POSITION_PRICE_OPEN)); //--- Calculate buy profit } else { //--- Handle sell position metrics.netProfitLoss += m_tradeExecutor.ResultVolume() * (PositionGetDouble(POSITION_PRICE_OPEN) - m_tradeExecutor.ResultPrice()); //--- Calculate sell profit } } else { metrics.operationSuccess = false; //--- Mark failure Print("Failed to close ticket: ", m_tradeConfig.activeTickets[i]); //--- Log failure } } } //--- Trade Closure End } //--- Bar Detection bool isNewBar() { //--- New Bar Detection Start static datetime previousTime = 0; //--- Store previous bar time datetime currentTime = iTime(m_tradeConfig.marketSymbol, Period(), 0); //--- Get current bar time bool result = (currentTime != previousTime); //--- Check for new bar previousTime = currentTime; //--- Update previous time return result; //--- Return new bar status //--- New Bar Detection End }
Здесь мы углубимся в основную логику нашей программы, разработав функции для открытия сделок, отслеживания позиций, исполнения ордеров, закрытия сделок и определения времени выполнения действий. Начнем с создания функции configureTrade, которая подготовит сделку для заданного ticket. Сначала попробуем выбрать позицию с помощью функции PositionSelectByTicket. Если это не сработает, регистрируем проблему с помощью функции logError и завершаем работу с false. В случае успеха заполняем m_tradeConfig деталями: получаем marketSymbol с помощью функции PositionGetString, устанавливаем tradeLabel на __FILE__ и получаем tradeIdentifier и direction из PositionGetInteger, меняя последний на ENUM_ORDER_TYPE. Затем устанавливаем openPrice и initialVolume с PositionGetDouble и помечаем m_tradeExecutor с SetExpertMagicNumber, чтобы гарантировать готовность нашей сделки к выполнению.
Далее создаем функцию storeTradeTicket, чтобы упорядочить наши открытые позиции. Проверим размер m_tradeConfig.activeTickets с функцией ArraySize, увеличим размер массива на один слот, используя функцию ArrayResize, и добавим новый ticket на место, чтобы всегда знать, какие сделки активны. Далее создадим функцию openMarketTrade для совершения сделок. Вызываем m_tradeExecutor.PositionOpen, передавая ему данные из параметров tradeDirection, tradeVolume, price и m_tradeConfig. Если всё прошло успешно, мы присваиваем ticket значение ResultOrder; в противном случае регистрируем ошибку с помощью функции Print, обеспечивая тем самым надежное исполнение сделки.
Затем переходим к закрытию позиций с помощью функции closeActiveTrades. Пройдем в обратном порядке по m_tradeConfig.activeTickets, закрывая каждый действительный тикет с помощью m_tradeExecutor.PositionClose. Когда закрытие сделки удается, закрываем заявку, добавляем ResultVolume к metrics.totalVolume и вычисляем metrics.netProfitLoss, используя функции PositionGetInteger и PositionGetDouble, чтобы проверить направление сделки. Если что-то не удается, мы помечаем параметр metrics.operationSuccess как false и регистрируем с помощью Print, обеспечивая отслеживание каждого результата.
Наконец, добавим функцию isNewBar, которая позволяет совершать сделки только один раз за бар, что снижает потребление ресурсов. Извлечем время текущего бара для m_tradeConfig.marketSymbol с помощью функции iTime, сравним ее с previousTime и обновим previousTime, если его значение отличается. Это сообщит нам о появлении нового бара для проверки торговых сигналов. Наконец, нам понадобится функция для расчета объема торгов и функция для открытия сделок.
//--- Lot Size Calculation double calculateLotSize(double riskPercent, int riskPips) { //--- Lot Size Calculation Start double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * riskPercent / 100; //--- Calculate risk amount double tickSize = SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_TRADE_TICK_SIZE); //--- Get tick size double tickValue = SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_TRADE_TICK_VALUE); //--- Get tick value if (tickSize == 0 || tickValue == 0) { //--- Validate tick data Print("Invalid tick size or value"); //--- Log invalid data return -1; //--- Return invalid lot } double lotValue = (riskPips * _Point) / tickSize * tickValue; //--- Calculate lot value if (lotValue == 0) { //--- Validate lot value Print("Invalid lot value"); //--- Log invalid lot return -1; //--- Return invalid lot } return NormalizeDouble(riskMoney / lotValue, 2); //--- Return normalized lot size //--- Lot Size Calculation End } //--- Order Execution int openOrder(ENUM_ORDER_TYPE orderType, double stopLoss, double takeProfit) { //--- Order Opening Start int ticket; //--- Initialize ticket double openPrice; //--- Initialize open price if (orderType == ORDER_TYPE_BUY) { //--- Check buy order openPrice = NormalizeDouble(getMarketAsk(), Digits()); //--- Set buy price } else if (orderType == ORDER_TYPE_SELL) { //--- Check sell order openPrice = NormalizeDouble(getMarketBid(), Digits()); //--- Set sell price } else { Print("Invalid order type"); //--- Log invalid type return -1; //--- Return invalid ticket } double lotSize = 0; //--- Initialize lot size if (m_lotOption == FIXED_LOTSIZE) { //--- Check fixed lot lotSize = m_initialLotSize; //--- Use fixed lot size } else if (m_lotOption == UNFIXED_LOTSIZE) { //--- Check dynamic lot lotSize = calculateLotSize(m_riskPercentage, m_riskPoints); //--- Calculate risk-based lot } if (lotSize <= 0) { //--- Validate lot size Print("Invalid lot size: ", lotSize); //--- Log invalid lot return -1; //--- Return invalid ticket } if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, orderType, lotSize, openPrice, 0, 0, __FILE__)) { //--- Open position ticket = (int)m_tradeExecutor.ResultOrder(); //--- Get ticket Print("New trade opened: Ticket=", ticket, ", Type=", EnumToString(orderType), ", Volume=", lotSize); //--- Log success } else { ticket = -1; //--- Set invalid ticket Print("Failed to open order: Type=", EnumToString(orderType), ", Volume=", lotSize); //--- Log failure } return ticket; //--- Return ticket //--- Order Opening End }
Начнем с функции calculateLotSize, которая определяет размер сделки на основе параметров риска. Сначала рассчитаем riskMoney, беря процент от баланса на счете, используя AccountInfoDouble с ACCOUNT_BALANCE и riskPercent. Затем извлечем tickSize и tickValue для m_tradeConfig.marketSymbol, используя SymbolInfoDouble с SYMBOL_TRADE_TICK_SIZE и SYMBOL_TRADE_TICK_VALUE. Если хотя бы одно из этих значений равно нулю, регистрируем ошибку с помощью Print и возвращаем -1, чтобы избежать некорректных вычислений. Вычислим lotValue с помощью riskPips, _Point, tickSize и tickValue, при 0 регистрируем еще одну ошибку и возвращаем -1. Наконец, мы возвращаем размер лота с NormalizeDouble с точностью до двух знаков после запятой, чтобы гарантировать соответствие требованиям брокера.
Далее создадим функцию openOrder для размещения сделок. Инициализируем ticket и openPrice, затем проверяем orderType. Для ORDER_TYPE_BUY устанавливаем openPrice, используя getMarketAsk и NormalizeDouble с Digits; для ORDER_TYPE_SELL используем getMarketBid. Если значение orderType недопустимо, информируем об этом с помощью Print и возвращаем -1. Определяем lotSize на основе m_lotOption: для FIXED_LOTSIZE используем m_initialLotSize; для UNFIXED_LOTSIZE вызываем функцию calculateLotSize с параметрами m_riskPercentage и m_riskPoints. При недопустимом lotSize, регистрируем ошибку с помощью Print и возвращаем -1. Затем откроем позицию с помощью параметра m_tradeExecutor.PositionOpen, используя параметры m_tradeConfig.marketSymbol, orderType, lotSize, openPrice и FILE в качестве комментария. В случае успеха присвоим ticket значение ResultOrder и выведем результат с помощью Print; в случае неудачи установим ticket на -1 и выведем сообщение об ошибке. В заключение возвращаем значение тикета.
После этого нам необходимо инициализировать значения системы. Мы можем добиться этого с помощью специальной функции, но для простоты воспользуемся конструктором. Рекомендуется определять конструктор в модификаторе публичного доступа, чтобы он был доступен во всей программе. Определим также и деструктор.
public: //--- Constructor MarketZoneTrader(TradingLotSizeOptions lotOpt, double initLot, double riskPct, int riskPts, int maxOrds, bool restrictOrds, double targetPts, double sizePts) { //--- Constructor Start m_tradeConfig.currentState = INACTIVE; //--- Set initial state ArrayResize(m_tradeConfig.activeTickets, 0); //--- Initialize ticket array m_tradeConfig.zoneProfitSpan = targetPts * _Point; //--- Set profit target m_tradeConfig.zoneRecoverySpan = sizePts * _Point; //--- Set recovery zone m_lossTracker.tradeLossTracker = 0.0; //--- Initialize loss tracker m_lotOption = lotOpt; //--- Set lot size option m_initialLotSize = initLot; //--- Set initial lot m_riskPercentage = riskPct; //--- Set risk percentage m_riskPoints = riskPts; //--- Set risk points m_maxOrders = maxOrds; //--- Set max positions m_restrictMaxOrders = restrictOrds; //--- Set restriction flag m_zoneTargetPoints = targetPts; //--- Set target points m_zoneSizePoints = sizePts; //--- Set zone points m_tradeConfig.marketSymbol = _Symbol; //--- Set symbol m_tradeConfig.tradeIdentifier = magicNumber; //--- Set magic number //--- Constructor End } //--- Destructor ~MarketZoneTrader() { //--- Destructor Start cleanup(); //--- Release resources //--- Destructor End }
Продолжим определением конструктора и деструктора для класса MarketZoneTrader в его публичной секции. Начнем с конструктора MarketZoneTrader, который принимает параметры lotOpt, initLot, riskPct, riskPts, maxOrds, restrictOrds, targetPts и sizePts. Инициализируем торговую среду, устанавливая параметр m_tradeConfig.currentState в значение INACTIVE, что указывает на отсутствие активных сделок. Далее очистим массив m_tradeConfig.activeTickets, установив ArrayResize на ноль и подготовив его для новых тикетов. Вычислим m_tradeConfig.zoneProfitSpan и m_tradeConfig.zoneRecoverySpan, умножив targetPts и sizePts на _Point. Это позволит установить размеры целевой прибыли и зонального восстановления в ценовых единицах. Сбросим m_lossTracker.tradeLossTracker на 0.0, чтобы начать отслеживание прибыли или убытков с нуля.
Затем присвоим входные параметры переменным-членам: m_lotOption — lotOpt, m_initialLotSize — initLot, m_riskPercentage — riskPct, m_riskPoints — riskPts, m_maxOrders — maxOrds, m_restrictMaxOrders — restrictOrds, m_zoneTargetPoints — targetPts и m_zoneSizePoints — sizePts. Установим m_tradeConfig.marketSymbol на _Symbol для торговли текущим символом графика и присвоим m_tradeConfig.tradeIdentifier для magicNumber для уникальной идентификации сделки. Такая настройка гарантирует, что наш советник будет учитывать пользовательские параметры и будет готов к торговле.
Далее определим деструктор ~MarketZoneTrader для очистки ресурсов. Вызовем функцию cleanup, чтобы освободить все выделенные ресурсы, такие как хэндлы индикаторов, обеспечивая корректное завершение работы советника без утечек памяти. Стоит отметить, что конструктор и деструктор имеют одинаковую формулировку имени класса, только у деструктора в начале стоит тильда (~). Функция для уничтожения класса за ненадобностью.
//--- Cleanup void cleanup() { //--- Cleanup Start IndicatorRelease(m_handleRsi); //--- Release RSI handle ArrayFree(m_rsiBuffer); //--- Free RSI buffer IndicatorRelease(m_handleEnvUpper); //--- Release upper Envelopes handle ArrayFree(m_envUpperBandBuffer); //--- Free upper Envelopes buffer IndicatorRelease(m_handleEnvLower); //--- Release lower Envelopes handle ArrayFree(m_envLowerBandBuffer); //--- Free lower Envelopes buffer //--- Cleanup End }
Мы просто используем функцию IndicatorRelease для освобождения хэндлов индикаторов, а также функцию ArrayFree - для освобождения массивов хранения. Раз уж мы затронули индикаторы, давайте определим функцию инициализации, которую мы будем вызывать при запуске программы.
//--- Getters TradeState getCurrentState() { //--- Get Current State Start return m_tradeConfig.currentState; //--- Return trade state //--- Get Current State End } double getZoneTargetHigh() { //--- Get Target High Start return m_zoneBounds.zoneTargetHigh; //--- Return profit target high //--- Get Target High End } double getZoneTargetLow() { //--- Get Target Low Start return m_zoneBounds.zoneTargetLow; //--- Return profit target low //--- Get Target Low End } double getZoneHigh() { //--- Get Zone High Start return m_zoneBounds.zoneHigh; //--- Return recovery zone high //--- Get Zone High End } double getZoneLow() { //--- Get Zone Low Start return m_zoneBounds.zoneLow; //--- Return recovery zone low //--- Get Zone Low End } //--- Initialization int initialize() { //--- Initialization Start m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); //--- Set magic number int totalPositions = PositionsTotal(); //--- Get total positions for (int i = 0; i < totalPositions; i++) { //--- Iterate positions ulong ticket = PositionGetTicket(i); //--- Get ticket if (PositionSelectByTicket(ticket)) { //--- Select position if (PositionGetString(POSITION_SYMBOL) == m_tradeConfig.marketSymbol && PositionGetInteger(POSITION_MAGIC) == m_tradeConfig.tradeIdentifier) { //--- Check symbol and magic if (activateTrade(ticket)) { //--- Activate position Print("Existing position activated: Ticket=", ticket); //--- Log activation } else { Print("Failed to activate existing position: Ticket=", ticket); //--- Log failure } } } } m_handleRsi = iRSI(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 8, PRICE_CLOSE); //--- Initialize RSI if (m_handleRsi == INVALID_HANDLE) { //--- Check RSI Print("Failed to initialize RSI indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } m_handleEnvUpper = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 150, 0, MODE_SMA, PRICE_CLOSE, 0.1); //--- Initialize upper Envelopes if (m_handleEnvUpper == INVALID_HANDLE) { //--- Check upper Envelopes Print("Failed to initialize upper Envelopes indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } m_handleEnvLower = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 95, 0, MODE_SMA, PRICE_CLOSE, 1.4); //--- Initialize lower Envelopes if (m_handleEnvLower == INVALID_HANDLE) { //--- Check lower Envelopes Print("Failed to initialize lower Envelopes indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } ArraySetAsSeries(m_rsiBuffer, true); //--- Set RSI buffer ArraySetAsSeries(m_envUpperBandBuffer, true); //--- Set upper Envelopes buffer ArraySetAsSeries(m_envLowerBandBuffer, true); //--- Set lower Envelopes buffer Print("EA initialized successfully"); //--- Log success return INIT_SUCCEEDED; //--- Return success //--- Initialization End }
Начнем с создания простых функций-геттеров для доступа к ключевым торговым данным. Функция getCurrentState возвращает m_tradeConfig.currentState, позволяя проверить, находится ли система в состоянии INACTIVE, RUNNING или TERMINATING. Далее создадим getZoneTargetHigh и getZoneTargetLow для извлечения m_zoneBounds.zoneTargetHigh и m_zoneBounds.zoneTargetLow, которые предоставляют целевые цены прибыли для наших сделок. Затем добавим функции getZoneHigh и getZoneLow, чтобы получить значения m_zoneBounds.zoneHigh и m_zoneBounds.zoneLow, что дает нам границы зонального восстановления.
Далее создадим функцию initialize для настройки нашего советника. Начнем с присвоения m_tradeConfig.tradeIdentifier для m_tradeExecutor с помощью SetExpertMagicNumber для маркировки наших сделок. Затем проверим наличие существующих позиций с помощью функции PositionsTotal и переберем их в цикле, получая каждый ticket с помощью PositionGetTicket. Если PositionSelectByTicket отработал успешно и позиция соответствует m_tradeConfig.marketSymbol и m_tradeConfig.tradeIdentifier (с помощью PositionGetString и PositionGetInteger), используем activateTrade для управления, регистрируя успех или неудачу с помощью Print.
Далее настроим наши индикаторы. Создадим хэндл RSI с функцией iRSI для m_tradeConfig.marketSymbol с использованием 8-периодного значения на текущем таймфрейме и PRICE_CLOSE. Если m_handleRsi равен INVALID_HANDLE, регистрируем ошибку с помощью Print и вернем INIT_FAILED. Затем инициализируем индикаторы конвертов: m_handleEnvUpper с помощью функции iEnvelopes, используя 150-периодную простую скользящую среднюю с отклонением 0,1 и PRICE_CLOSE, а также m_handleEnvLower с 95-периодной шкалой и отклонением 1,4. Если хотя бы один из хэндлов имеет значение INVALID_HANDLE, регистрируем ошибку и вернем INIT_FAILED. Наконец настроим m_rsiBuffer, m_envUpperBandBuffer и m_envLowerBandBuffer как массивы временных рядов с ArraySetAsSeries, зафиксируем успешный результат с помощью Print и вернем INIT_SUCCEEDED. Теперь мы можем вызвать эту функцию в обработчике событий OnInit, но сначала нам понадобится экземпляр класса.
//--- Global Instance MarketZoneTrader *trader = NULL; //--- Declare trader instance
Здесь мы создадим глобальный экземпляр нашей системы, объявляя указатель на класс MarketZoneTrader. Создадим переменную trader как указатель на MarketZoneTrader и инициализируем его в NULL. Этот шаг гарантирует наличие единого, глобально доступного экземпляра нашей торговой системы, который мы сможем использовать во всем советнике для управления всеми торговыми операциями, такими как инициализация сделок, исполнение ордеров и обработка зонального восстановления. Начиная со значения NULL, мы подготавливаем trader для последующего корректного создания экземпляра, предотвращая преждевременный доступ до полной настройки советника. Теперь мы можем вызвать функцию.
int OnInit() { //--- EA Initialization Start trader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, maxOrders, restrictMaxOrders, zoneTargetPoints, zoneSizePoints); //--- Create trader instance return trader.initialize(); //--- Initialize EA //--- EA Initialization End }
В обработчике событий OnInit начнем с использования нового экземпляра класса MarketZoneTrader, присвоив его глобальному указателю trader. Передадим заданные пользователем входные параметры — lotOption, initialLotSize, riskPercentage, riskPoints, maxOrders, restrictMaxOrders, zoneTargetPoints и zoneSizePoints — в конструктор для настройки торговой системы с необходимыми параметрами. Далее вызовем функцию initialize на trader, чтобы настроить советника, включая присвоение меток сделкам, проверку существующих позиций и инициализацию индикаторов, и вернем результат, сигнализирующий об успешности настройки. Функция гарантирует, что наш советник полностью готов к началу торговли с указанными настройками. После компиляции получаем следующий результат.

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

На изображении видно, что присутствуют неудаленные объекты, которые приводят к утечке памяти. Для решения этой проблемы нам необходимо выполнить очистку объектов. Для этого мы используем следующую логику.
void OnDeinit(const int reason) { //--- EA Deinitialization Start if (trader != NULL) { //--- Check trader existence delete trader; //--- Delete trader trader = NULL; //--- Clear pointer Print("EA deinitialized"); //--- Log deinitialization } //--- EA Deinitialization End }
Для проведения очистки в обработчике событий OnDeinit начнем с проверки, не равен ли указатель trader значению NULL, чтобы убедиться, что экземпляр MarketZoneTrader существует. Если это так, используем оператор delete для освобождения памяти, выделенной для trader, предотвращая утечки памяти. Затем установим значение параметра trader в NULL, чтобы избежать случайного доступа к освобожденной памяти. Наконец, запишем сообщение с помощью функции Print, чтобы подтвердить деинициализацию советника. Эта функция обеспечивает чистое завершение работы нашего советника и правильное высвобождение ресурсов. Теперь мы можем продолжить определение основной логики для обработки оценок сигналов и управления открытыми сделками. Для этого нам понадобятся вспомогательные функции.
//--- Position Management bool activateTrade(ulong ticket) { //--- Position Activation Start m_tradeConfig.currentState = INACTIVE; //--- Set state to inactive ArrayResize(m_tradeConfig.activeTickets, 0); //--- Clear tickets m_lossTracker.tradeLossTracker = 0.0; //--- Reset loss tracker if (!configureTrade(ticket)) { //--- Configure trade return false; //--- Return failure } storeTradeTicket(ticket); //--- Store ticket if (m_tradeConfig.direction == ORDER_TYPE_BUY) { //--- Handle buy position m_zoneBounds.zoneHigh = m_tradeConfig.openPrice; //--- Set zone high m_zoneBounds.zoneLow = m_zoneBounds.zoneHigh - m_tradeConfig.zoneRecoverySpan; //--- Set zone low m_tradeConfig.accumulatedBuyVolume = m_tradeConfig.initialVolume; //--- Set buy volume m_tradeConfig.accumulatedSellVolume = 0.0; //--- Reset sell volume } else { //--- Handle sell position m_zoneBounds.zoneLow = m_tradeConfig.openPrice; //--- Set zone low m_zoneBounds.zoneHigh = m_zoneBounds.zoneLow + m_tradeConfig.zoneRecoverySpan; //--- Set zone high m_tradeConfig.accumulatedSellVolume = m_tradeConfig.initialVolume; //--- Set sell volume m_tradeConfig.accumulatedBuyVolume = 0.0; //--- Reset buy volume } m_zoneBounds.zoneTargetHigh = m_zoneBounds.zoneHigh + m_tradeConfig.zoneProfitSpan; //--- Set target high m_zoneBounds.zoneTargetLow = m_zoneBounds.zoneLow - m_tradeConfig.zoneProfitSpan; //--- Set target low m_tradeConfig.currentState = RUNNING; //--- Set state to running return true; //--- Return success //--- Position Activation End } //--- Tick Processing void processTick() { //--- Tick Processing Start double askPrice = NormalizeDouble(getMarketAsk(), Digits()); //--- Get ask price double bidPrice = NormalizeDouble(getMarketBid(), Digits()); //--- Get bid price if (!isNewBar()) return; //--- Exit if not new bar if (!CopyBuffer(m_handleRsi, 0, 0, 3, m_rsiBuffer)) { //--- Load RSI data Print("Error loading RSI data. Reverting."); //--- Log RSI failure return; //--- Exit } if (!CopyBuffer(m_handleEnvUpper, 0, 0, 3, m_envUpperBandBuffer)) { //--- Load upper Envelopes Print("Error loading upper envelopes data. Reverting."); //--- Log failure return; //--- Exit } if (!CopyBuffer(m_handleEnvLower, 1, 0, 3, m_envLowerBandBuffer)) { //--- Load lower Envelopes Print("Error loading lower envelopes data. Reverting."); //--- Log failure return; //--- Exit } int ticket = 0; //--- Initialize ticket const int rsiOverbought = 70; //--- Set RSI overbought level const int rsiOversold = 30; //--- Set RSI oversold level if (m_rsiBuffer[1] < rsiOversold && m_rsiBuffer[2] > rsiOversold && m_rsiBuffer[0] < rsiOversold) { //--- Check buy signal if (askPrice > m_envUpperBandBuffer[0]) { //--- Confirm price above upper Envelopes if (!m_restrictMaxOrders || PositionsTotal() < m_maxOrders) { //--- Check position limit ticket = openOrder(ORDER_TYPE_BUY, 0, 0); //--- Open buy order } } } else if (m_rsiBuffer[1] > rsiOverbought && m_rsiBuffer[2] < rsiOverbought && m_rsiBuffer[0] > rsiOverbought) { //--- Check sell signal if (bidPrice < m_envLowerBandBuffer[0]) { //--- Confirm price below lower Envelopes if (!m_restrictMaxOrders || PositionsTotal() < m_maxOrders) { //--- Check position limit ticket = openOrder(ORDER_TYPE_SELL, 0, 0); //--- Open sell order } } } if (ticket > 0) { //--- Check if trade opened if (activateTrade(ticket)) { //--- Activate position Print("New position activated: Ticket=", ticket); //--- Log activation } else { Print("Failed to activate new position: Ticket=", ticket); //--- Log failure } } //--- Tick Processing End }
Здесь мы продолжаем разработку нашей программы, реализуя функции activateTrade и processTick в классе MarketZoneTrader для управления позициями и обработки рыночных тиков. Начнем с функции activateTrade, которая активирует сделку по заданному тикету (ticket). Сначала установим m_tradeConfig.currentState на INACTIVE и очистим m_tradeConfig.activeTickets, используя функцию ArrayResize для сброса списка тикетов. Сбросим m_lossTracker.tradeLossTracker на 0.0, затем вызовем configureTrade с ticket. При неудаче вернем false. Далее сохраним ticket с storeTradeTicket. Для сделки на покупку (m_tradeConfig.direction как ORDER_TYPE_BUY), установим m_zoneBounds.zoneHigh на m_tradeConfig.openPrice, рассчитаем m_zoneBounds.zoneLow, вычитая m_tradeConfig.zoneRecoverySpan, и обновим m_tradeConfig.accumulatedBuyVolume до m_tradeConfig.initialVolume, одновременно сбрасывая m_tradeConfig.accumulatedSellVolume.
Для сделки на продажу установим m_zoneBounds.zoneLow на m_tradeConfig.openPrice, добавим значение параметра m_tradeConfig.zoneRecoverySpan равным m_zoneBounds.zoneHigh и соответствующим образом скорректируем объемы. Установим m_zoneBounds.zoneTargetHigh и m_zoneBounds.zoneTargetLow с помощью m_tradeConfig.zoneProfitSpan, изменим m_tradeConfig.currentState на RUNNING и вернем true.
Далее создадим функцию processTick для обработки рыночных тиков. Извлечем askPrice и bidPrice, используя getMarketAsk и getMarketBid, нормализованные с помощью NormalizeDouble и Digits. Если isNewBar вернет false, завершим работу для экономии ресурсов. Загрузим данные индикатора с помощью CopyBuffer для m_handleRsi в m_rsiBuffer, m_handleEnvUpper - в m_envUpperBandBuffer и m_handleEnvLower - в m_envLowerBandBuffer, регистрируя ошибки с помощью Print и завершая работу при сбое. Для торговых сигналов устанавливаем значение rsiOverought равным 70, а rsiOversold — равным 30.
Если m_rsiBuffer указывает на перепроданность и askPrice превышает m_envUpperBandBuffer, откроем ордер на покупку с помощью openOrder если m_restrictMaxOrders равен false или PositionsTotal ниже m_maxOrders. При наличии перекупленности, когда bidPrice ниже m_envLowerBandBuffer, открываем ордер на продажу. Если возвращается действительный ticket, вызываем функцию activateTrade и записываем результат в журнал. Теперь мы можем запустить функцию в обработчике событий OnTick для обработки оценки сигнала и инициализации позиции.
void OnTick() { //--- Tick Handling Start if (trader != NULL) { //--- Check trader existence trader.processTick(); //--- Process tick } //--- Tick Handling End }
В обработчике события OnTick мы начинаем с проверки того, не равен ли указатель trader, наш экземпляр класса MarketZoneTrader, значению NULL, чтобы убедиться, что торговая система инициализирована. Если объект существует, вызовем processTick в объекте trader, чтобы обработать каждый тик рынка, оценить позиции, проверить сигналы индикаторов и совершать сделки по мере необходимости. В результате компиляции мы получаем следующее.

Из изображения видно, что мы выявили сигнал, оценили его и открыли позицию на покупку. Теперь нам нужно заняться управлением открытыми позициями. Для модульности вынесем это в отдельные функции.
//--- Market Tick Evaluation void evaluateMarketTick() { //--- Tick Evaluation Start if (m_tradeConfig.currentState == INACTIVE) return; //--- Exit if inactive if (m_tradeConfig.currentState == TERMINATING) { //--- Check terminating state finalizePosition(); //--- Finalize position return; //--- Exit } }
Здесь мы реализуем функцию evaluateMarketTick в классе MarketZoneTrader для оценки рыночных условий для активных сделок. Для начала проверим, что m_tradeConfig.currentState равно INACTIVE. Если это так, немедленно завершаем процесс, чтобы избежать ненужной обработки данных, когда нет активных сделок. Далее убедимся, что m_tradeConfig.currentState равен TERMINATING. Если это так, вызовем функцию finalizePosition, чтобы закрыть все открытые позиции и завершить торговый цикл, после чего выйдем. Ниже приведена функция для закрытия сделок.
//--- Position Finalization bool finalizePosition() { //--- Position Finalization Start m_tradeConfig.currentState = TERMINATING; //--- Set terminating state TradeMetrics metrics = {true, 0.0, 0.0}; //--- Initialize metrics closeActiveTrades(metrics); //--- Close all trades if (metrics.operationSuccess) { //--- Check success ArrayResize(m_tradeConfig.activeTickets, 0); //--- Clear tickets m_tradeConfig.currentState = INACTIVE; //--- Set inactive state Print("Position closed successfully"); //--- Log success } else { Print("Failed to close position"); //--- Log failure } return metrics.operationSuccess; //--- Return status //--- Position Finalization End }
Для начала установим m_tradeConfig.currentState на TERMINATING, чтобы указать на завершение торгового цикла. Это помогает предотвратить цикл управления при закрытии сделок. Далее инициализируем структуру TradeMetrics под названием metrics с operationSuccess, равным true, totalVolume, равным 0.0, и netProfitLoss, равным 0.0, для отслеживания результатов закрытия сделок. Вызовем closeActiveTrades с metrics, чтобы закрыть все открытые позиции, перечисленные в m_tradeConfig.activeTickets. Если metrics.operationSuccess остается true, очистим m_tradeConfig.activeTickets, используя ArrayResize, чтобы сбросить список тикетов, устанавливаем m_tradeConfig.currentState на INACTIVE, чтобы пометить систему как неактивную, и зарегистрируем успешное выполнение с помощью Print.
Если закрытие не удается, регистрируем ошибку с помощью Print. В заключение возвращаем значение metrics.operationSuccess, чтобы указать, успешно ли завершился процесс. Если мы не закрыли сделки на данном этапе, это означает, что мы не находимся в процессе закрытия позиций, поэтому мы можем перейти к оценке, чтобы увидеть, достигла ли цена зонального восстановления или целевых уровней. Начнем с покупки.
double currentPrice; //--- Initialize price if (m_tradeConfig.direction == ORDER_TYPE_BUY) { //--- Handle buy position currentPrice = getMarketBid(); //--- Get bid price if (currentPrice > m_zoneBounds.zoneTargetHigh) { //--- Check profit target Print("Closing position: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); //--- Log closure finalizePosition(); //--- Close position return; //--- Exit } else if (currentPrice < m_zoneBounds.zoneLow) { //--- Check recovery trigger Print("Triggering recovery trade: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); //--- Log recovery triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice); //--- Open sell recovery } }
Далее реализуем логику в функции evaluateMarketTick класса MarketZoneTrader для обработки позиций на покупку. Для начала объявим переменную currentPrice для хранения рыночной цены. Если m_tradeConfig.direction равен ORDER_TYPE_BUY, установим currentPrice, используя функцию getMarketBid для извлечения цены bid, поскольку это цена, по которой мы можем закрыть позицию на покупку. Далее проверим, превышает ли currentPrice значение m_zoneBounds.zoneTargetHigh. Если превышает, регистрируем закрытие с помощью Print, отображая цену bid и целевое значение, затем вызываем finalizePosition для закрытия сделки и выходим с помощью return.
Если currentPrice опускается ниже m_zoneBounds.zoneLow, регистрируем сигнал восстановления с помощью Print и вызываем triggerRecoveryTrade с помощью ORDER_TYPE_SELL и currentPrice для открытия сделки на продажу и минимизации убытков. Такая логика гарантирует, что мы закрываем прибыльные сделки на покупку или начинаем восстановление убыточных, обеспечивая гибкость нашей стратегии. Ниже приведена логика работы функции, отвечающей за открытие сделок на восстановление.
//--- Recovery Trade Handling void triggerRecoveryTrade(ENUM_ORDER_TYPE tradeDirection, double price) { //--- Recovery Trade Start TradeMetrics metrics = {true, 0.0, 0.0}; //--- Initialize metrics closeActiveTrades(metrics); //--- Close existing trades for (int i = 0; i < 10 && !metrics.operationSuccess; i++) { //--- Retry closure Sleep(1000); //--- Wait 1 second metrics.operationSuccess = true; //--- Reset success flag closeActiveTrades(metrics); //--- Retry closure } m_lossTracker.tradeLossTracker += metrics.netProfitLoss; //--- Update loss tracker if (m_lossTracker.tradeLossTracker > 0 && metrics.operationSuccess) { //--- Check positive profit Print("Closing position due to positive profit: ", m_lossTracker.tradeLossTracker); //--- Log closure finalizePosition(); //--- Close position m_lossTracker.tradeLossTracker = 0.0; //--- Reset loss tracker return; //--- Exit } double tradeSize = determineRecoverySize(tradeDirection); //--- Calculate trade size ulong ticket = openMarketTrade(tradeDirection, tradeSize, price); //--- Open recovery trade if (ticket > 0) { //--- Check if trade opened storeTradeTicket(ticket); //--- Store ticket m_tradeConfig.direction = tradeDirection; //--- Update direction if (tradeDirection == ORDER_TYPE_BUY) m_tradeConfig.accumulatedBuyVolume += tradeSize; //--- Update buy volume else m_tradeConfig.accumulatedSellVolume += tradeSize; //--- Update sell volume Print("Recovery trade opened: Ticket=", ticket, ", Direction=", EnumToString(tradeDirection), ", Volume=", tradeSize); //--- Log recovery trade } //--- Recovery Trade End } //--- Recovery Size Calculation double determineRecoverySize(ENUM_ORDER_TYPE tradeDirection) { //--- Recovery Size Calculation Start double tradeSize = -m_lossTracker.tradeLossTracker / m_tradeConfig.zoneProfitSpan; //--- Calculate lot size tradeSize = MathCeil(tradeSize / getMarketVolumeStep()) * getMarketVolumeStep(); //--- Round to volume step return tradeSize; //--- Return trade size //--- Recovery Size Calculation End }
Для обработки случаев, когда рынок должен инициировать восстановление позиций, мы начнем с функции triggerRecoveryTrade, которая обрабатывает сделки восстановления, когда позиция движется против нас. Сначала мы инициализируем структуру TradeMetrics под названием metrics с operationSuccess, равным true, totalVolume, равным 0.0 и netProfitLoss, равным 0.0. Вызовем closeActiveTrades с metrics, чтобы закрыть существующие позиции. Если metrics.operationSuccess равен false, повторяем попытку до 10 раз, ожидая одну секунду с Sleep и сбрасывая operationSuccess перед каждой попыткой.
Обновляем m_lossTracker.tradeLossTracker, добавляя metrics.netProfitLoss. Если m_lossTracker.tradeLossTracker имеет положительное значение, а metrics.operationSuccess равен true, зарегистрируем закрытие с помощью Print, вызовем finalizePosition, сбросим m_lossTracker.tradeLossTracker на 0.0 и выйдем с помощью return. В противном случае рассчитаем восстановление tradeSize, используя determineRecoverySize с tradeDirection, а затем открываем новую сделку с помощью функции openMarketTrade, используя параметры tradeDirection, tradeSize и price.
Если возвращенный ticket действителен, сохраним его с storeTradeTicket, обновим m_tradeConfig.direction, скорректируем m_tradeConfig.accumulatedBuyVolume или m_tradeConfig.accumulatedSellVolume на основе tradeDirection и зарегистрируем сделку с помощью Print, используя EnumToString. Далее создадим функцию determineRecoverySize для расчета размера лота для сделок по восстановлению. Вычислим tradeSize, разделив отрицательное значение m_lossTracker.tradeLossTracker на m_tradeConfig.zoneProfitSpan, чтобы определить размер сделки для покрытия убытков. Затем округлим tradeSize до шага объема брокера, используя MathCeil и getMarketVolumeStep для обеспечения соответствия требованиям и возврата результата. Теперь обрабатываются случаи восстановления, и мы можем продолжить работу над логикой обработки зон продажи. Логика прямо противоположна логике покупки, поэтому мы не будем тратить на это много времени. В итоге, полноценная функциональность будет выглядеть следующим образом.
//--- Market Tick Evaluation void evaluateMarketTick() { //--- Tick Evaluation Start if (m_tradeConfig.currentState == INACTIVE) return; //--- Exit if inactive if (m_tradeConfig.currentState == TERMINATING) { //--- Check terminating state finalizePosition(); //--- Finalize position return; //--- Exit } double currentPrice; //--- Initialize price if (m_tradeConfig.direction == ORDER_TYPE_BUY) { //--- Handle buy position currentPrice = getMarketBid(); //--- Get bid price if (currentPrice > m_zoneBounds.zoneTargetHigh) { //--- Check profit target Print("Closing position: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); //--- Log closure finalizePosition(); //--- Close position return; //--- Exit } else if (currentPrice < m_zoneBounds.zoneLow) { //--- Check recovery trigger Print("Triggering recovery trade: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); //--- Log recovery triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice); //--- Open sell recovery } } else if (m_tradeConfig.direction == ORDER_TYPE_SELL) { //--- Handle sell position currentPrice = getMarketAsk(); //--- Get ask price if (currentPrice < m_zoneBounds.zoneTargetLow) { //--- Check profit target Print("Closing position: Ask=", currentPrice, " < TargetLow=", m_zoneBounds.zoneTargetLow); //--- Log closure finalizePosition(); //--- Close position return; //--- Exit } else if (currentPrice > m_zoneBounds.zoneHigh) { //--- Check recovery trigger Print("Triggering recovery trade: Ask=", currentPrice, " > ZoneHigh=", m_zoneBounds.zoneHigh); //--- Log recovery triggerRecoveryTrade(ORDER_TYPE_BUY, currentPrice); //--- Open buy recovery } } //--- Tick Evaluation End }
Теперь эта функция обрабатывает все направления восстановления. В результате компиляции мы получаем следующее.

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

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

Заключение
Мы разработали надежную программу на MQL5, которая реализует систему зонального восстановления для трендовой торговли на основе конвертов, сочетая индекс относительной силы (RSI) и индикатор конвертов для выявления торговых возможностей и управления убытками с помощью структурированных зон восстановления. Нами был использован объектно-ориентированный подход (OOP). Используя такие компоненты, как класс MarketZoneTrader, а также такие структуры, как TradeConfig и ZoneBoundaries, и такие функции, как processTick и triggerRecoveryTrade, мы создали гибкую систему, которую можно настраивать, изменяя параметры, включая zoneTargetPoints или riskPercentage в соответствии с различными рыночными условиями.
Предупреждение: Статья предназначена исключительно для образовательных целей. Торговля сопряжена со значительными финансовыми рисками, а рыночная волатильность может привести к убыткам. Тщательно протестируйте программу на исторических данных и отслеживайте все риски перед использованием программы на реальном рынке.
Опираясь на знания, полученные в этой статье, вы можете усовершенствовать систему зонального восстановления или адаптировать ее логику для разработки новых торговых стратегий, тем самым повысив свой уровень знаний в области алгоритмической торговли. Удачной торговли!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/18720
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Нейросети в трейдинге: Оптимизация Cross-Attention для анализа длинных последовательностей рынка (Окончание)
Внедрение в MQL5 практических модулей из других языков (Часть 02): Создание библиотеки REQUESTS, как в Python
Оптимизатор конкурирующего роя — Competitive Swarm Optimizer (CSO)
Машинное обучение и Data Science (Часть 44): Прогнозирование OHLC-рядов Forex методом векторной авторегрессии (VAR)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Большое спасибо 🙏
Очень рады. Спасибо