English 中文 Deutsch 日本語
preview
Автоматизация торговых стратегий на MQL5 (Часть 22): Создание системы зонального восстановления для трендовой торговли по индикатору Envelopes

Автоматизация торговых стратегий на MQL5 (Часть 22): Создание системы зонального восстановления для трендовой торговли по индикатору Envelopes

MetaTrader 5Трейдинг |
420 2
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В предыдущей статье (Часть 21), мы рассмотрели торговую стратегию на основе нейронной сети, усиленную адаптивными темпами обучения для повышения точности прогнозирования рыночных движений на MetaQuotes Language 5 (MQL5). В части 22 мы переключаем внимание на создание системы зонального восстановления (Zone Recovery System), с трендовой торговой стратегией на основе конвертов в сочетании с индексом относительной силы (RSI) и конвертами для автоматизации сделок и эффективного управления убытками. Мы рассмотрим следующие темы:

  1. Понимание архитектуры тренда зонального восстановления
  2. Реализация средствами MQL5
  3. Тестирование на истории
  4. Заключение

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

ФАЙЛ ТОРГОВЫХ ОПЕРАЦИЙ MQL5

Затем мы определяем параметры перечисления 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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Sabrina Hellal
Sabrina Hellal | 9 июл. 2025 в 13:44
Большое спасибо 🙏
Allan Munene Mutiiria
Allan Munene Mutiiria | 9 июл. 2025 в 16:46
Sabrina Hellal #:
Большое спасибо 🙏

Очень рады. Спасибо

Нейросети в трейдинге: Оптимизация Cross-Attention для анализа длинных последовательностей рынка (Окончание) Нейросети в трейдинге: Оптимизация Cross-Attention для анализа длинных последовательностей рынка (Окончание)
В статье рассматривается практическая реализация архитектуры STCA с интеграцией механизмов OneTrans для совместной обработки временных рядов и контекстных признаков рынка. Описаны особенности построения модели, алгоритмы прямого прохода и накопления исторического состояния. Отдельное внимание уделено процессу обучения и результатам тестирования на реальных данных, демонстрирующим поведение модели в рыночных условиях.
Внедрение в MQL5 практических модулей из других языков (Часть 02): Создание библиотеки REQUESTS, как в Python Внедрение в MQL5 практических модулей из других языков (Часть 02): Создание библиотеки REQUESTS, как в Python
В этой статье опишем реализацию модуля, аналогичного модулю requests в Python, чтобы упростить отправку и получение веб-запросов в MetaTrader 5 с использованием MQL5.
Оптимизатор конкурирующего роя — Competitive Swarm Optimizer (CSO) Оптимизатор конкурирующего роя — Competitive Swarm Optimizer (CSO)
В данной статье рассматривается Competitive Swarm Optimizer — алгоритм роевой оптимизации, в основе которого лежит предельно простая идея: агенты случайным образом разбиваются на пары, проигравший учится у победителя и притягивается к центру роя. Помимо разбора CSO, в статье представлена модернизация тестового стенда: визуализация работы алгоритмов переведена в 3D - мерное пространство, что позволяет наглядно наблюдать движение популяции на поверхности тестовой функции.
Машинное обучение и Data Science (Часть 44): Прогнозирование OHLC-рядов Forex методом векторной авторегрессии (VAR) Машинное обучение и Data Science (Часть 44): Прогнозирование OHLC-рядов Forex методом векторной авторегрессии (VAR)
В этом материале мы познакомимся с тем, как модели векторной авторегрессии (VAR) могут прогнозировать временные ряды значений OHLC (цены открытия, максимум, минимум и цена закрытия) на форексе Поговорим о том, как реализовать VAR-модели, обучать их и строить прогнозы в MetaTrader 5 в реальном времени, чтобы анализировать взаимозависимые движения валютных курсов для получения лучших результатов в трейдинге.