English Deutsch 日本語
preview
Автоматизация торговых стратегий на MQL5 (Часть 3): система Zone Recovery RSI для динамического управления торговлей

Автоматизация торговых стратегий на MQL5 (Часть 3): система Zone Recovery RSI для динамического управления торговлей

MetaTrader 5Трейдинг |
73 8
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

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

Мы рассмотрим процесс кодирования логики восстановления, управления позициями с динамическим размером лота и использования RSI для сигналов входа в сделку и восстановления. К концу этой статьи вы получите четкое представление о том, как реализовать систему Zone Recovery RSI, протестировать ее эффективность с помощью тестера стратегий MQL5 и оптимизировать ее для лучшего управления рисками и повышения доходности. Для облегчения понимания статья построена следующим образом.

  1. Разработка стратегии и ключевые концепции
  2. Реализация на MQL5
  3. Бэктестинг и анализ эффективности
  4. Заключение


Разработка стратегии и ключевые концепции

Система Zone Recovery RSI сочетает в себе индикатор относительной силы (RSI) для входа в сделку с механизмом Zone Recovery для управления неблагоприятными ценовыми движениями. Вход в сделку происходит, когда RSI пересекает ключевые пороги — обычно 30 для перепроданности (покупка) и 70 для перекупленности (продажа). Однако истинная сила системы заключается в ее способности восстанавливаться после убыточных сделок с помощью хорошо структурированной модели Zone Recovery.

Система Zone Recovery устанавливает четыре критических ценовых уровня для каждой сделки: Zone High, Zone Low, Target High и Target Low. При открытии сделки эти уровни рассчитываются относительно цены входа. Для сделки на покупку Zone Low устанавливается ниже цены входа, а Zone High — на уровне цены входа. И наоборот, для сделки на продажу Zone High устанавливается выше цены входа, а Zone Low — на уровне цены входа. Если рынок выходит за пределы Zone Low (для покупок) или Zone High (для продаж), запускается противоположная сделка с увеличенным размером лота на основе заранее определенного множителя. Целевой максимум и целевой минимум определяют точки фиксации прибыли для позиций на покупку и продажу, обеспечивая закрытие сделок с прибылью, как только рынок движется в благоприятном направлении. Такой подход позволяет восстановить убытки, контролируя риск за счет систематического определения размера позиции и корректировки уровней. Ниже приведена иллюстрация, обобщающая всю модель.

ZONE RECOVERY ILLUSTRATION


Реализация на MQL5

Изучив всю теорию торговой стратегии Zone Recovery, давайте автоматизируем ее и создадим советник на языке MetaQuotes Language 5 (MQL5) для MetaTrader 5.

Чтобы создать советник, в терминале MetaTrader 5 перейдите на вкладку "Сервис" и выберите "Редактор MetaQuotes Language" или просто нажмите F4 на клавиатуре. Также можно щелкнуть значок IDE (интегрированная среда разработки) на панели инструментов. Откроется среда редактора языка MetaQuotes, которая позволяет писать торговых роботов, технические индикаторы, скрипты и библиотеки функций. После открытия MetaEditor на панели инструментов перейдите на вкладку "Файл" и выберите "Новый файл" или просто нажмите CTRL + N, чтобы создать новый документ. Также можно щелкнуть значок "Создать" на панели инструментов. Откроется всплывающее окно MQL Wizard.

В появившемся мастере выберите "Советник (шаблон)" и нажмите "Далее". В общих свойствах советника в разделе "Имя" укажите имя файла вашего советника. Обратите внимание, что для указания или создания папки, если она не существует, перед именем советника необходимо использовать обратный слеш. Например, здесь по умолчанию указано "Experts\". Это означает, что наш советник будет создан в папке Experts, и мы сможем найти его там. Остальные разделы довольно просты, но вы можете перейти по ссылке внизу мастера, чтобы узнать, как точно выполнить этот процесс.

NEW EA NAME

После указания желаемого имени файла советника нажмите "Далее", затем "Далее" и "Готово". После этого мы готовы к написанию кода и программированию нашей стратегии.

Сначала мы начинаем с определения некоторых метаданных о советнике. Сюда входит название советника, информация об авторских правах и ссылка на веб-сайт MetaQuotes. Мы также указываем версию советника, которая установлена на 1.00.

//+------------------------------------------------------------------+
//|                                      1. Zone Recovery RSI EA.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

Это отобразит метаданные системы при загрузке программы. Затем мы можем перейти к добавлению некоторых глобальных переменных, которые будем использовать в программе. Сначала мы включаем экземпляр торговли, используя #include в начале исходного кода. Это дает нам доступ к классу CTrade, который мы будем использовать для создания объекта торговли. Это очень важно, так как нам нужно его для открытия сделок.

#include <Trade/Trade.mqh>
CTrade obj_Trade;

Препроцессор заменит строку #include &lt;Trade/Trade.mqh&gt; содержимым файла Trade.mqh. Угловые скобки указывают, что файл Trade.mqh будет взят из стандартного каталога (обычно это terminal_installation_directory\MQL5\Include). Текущий каталог не включается в поиск. Строка может быть размещена в любом месте программы, но обычно все включения размещаются в начале исходного кода для лучшей структуры кода и удобства ссылки. Объявление объекта obj_Trade класса CTrade даст нам легкий доступ к методам, содержащимся в этом классе, благодаря разработчикам MQL5.

CTRADE CLASS

После этого нам нужно объявить несколько важных глобальных переменных, которые мы будем использовать в торговой системе.

// Global variables for RSI logic
int rsiPeriod = 14;                //--- The period used for calculating the RSI indicator.
int rsiHandle;                     //--- Handle for the RSI indicator, used to retrieve RSI values.
double rsiBuffer[];                //--- Array to store the RSI values retrieved from the indicator.
datetime lastBarTime = 0;          //--- Holds the time of the last processed bar to prevent duplicate signals.

Для обработки генерации сигналов мы настраиваем глобальные переменные, необходимые для управления логикой индикатора относительной силы (RSI). Сначала мы определяем rsiPeriod как 14, что определяет количество ценовых баров, используемых для расчета RSI. Это стандартная настройка в техническом анализе, позволяющая нам оценивать условия перекупленности или перепроданности рынка. Затем мы создаем rsiHandle, ссылку на индикатор RSI. Этот дескриптор позволит нам запрашивать и извлекать значения RSI из платформы MetaTrader, что даст нам возможность отслеживать движения индикатора в режиме реального времени.

Для хранения этих значений RSI мы используем rsiBuffer, массив, который хранит результаты индикатора. Мы будем анализировать этот буфер, чтобы обнаружить ключевые точки пересечения, например, когда RSI опускается ниже 30 (потенциальный сигнал на покупку) или выше 70 (потенциальный сигнал на продажу). Наконец, мы вводим lastBarTime, который хранит время последнего обработанного бара. Эта переменная гарантирует, что мы обрабатываем только один сигнал на бар, предотвращая срабатывание нескольких сделок в пределах одного бара. После этого мы можем определить класс, который будет обрабатывать механизм восстановления.

// Global ZoneRecovery object
class ZoneRecovery {

//---

};

Здесь мы реализуем систему восстановления зоны с помощью класса ZoneRecovery, который служит контейнером для всех переменных, функций и логики, необходимых для управления процессом восстановления. Используя класс, мы можем организовать код в самостоятельный объект, что позволяет нам управлять сделками, отслеживать ход восстановления и рассчитывать важные уровни для каждого торгового цикла. Такой подход обеспечивает лучшую структуру, возможность повторного использования и масштабируемость для одновременной обработки нескольких торговых позиций. Класс может содержать инкапсуляции из трех членов в виде закрытых, защищенных и публичных членов. Давайте сначала определим закрытые члены.

private:
   CTrade trade;                    //--- Object to handle trading operations.
   double initialLotSize;           //--- The initial lot size for the first trade.
   double currentLotSize;           //--- The lot size for the current trade in the sequence.
   double zoneSize;                 //--- Distance in points defining the range of the recovery zone.
   double targetSize;               //--- Distance in points defining the target profit range.
   double multiplier;               //--- Multiplier to increase lot size in recovery trades.
   string symbol;                   //--- Symbol for trading (e.g., currency pair).
   ENUM_ORDER_TYPE lastOrderType;   //--- Type of the last executed order (BUY or SELL).
   double lastOrderPrice;           //--- Price at which the last order was executed.
   double zoneHigh;                 //--- Upper boundary of the recovery zone.
   double zoneLow;                  //--- Lower boundary of the recovery zone.
   double zoneTargetHigh;           //--- Upper boundary for target profit range.
   double zoneTargetLow;            //--- Lower boundary for target profit range.
   bool isRecovery;                 //--- Flag indicating whether the recovery process is active.

Здесь мы определяем закрытые переменные-члены класса ZoneRecovery, который хранит важные данные для управления процессом восстановления зоны. Эти переменные позволяют нам отслеживать состояние стратегии, рассчитывать ключевые уровни зоны восстановления и управлять логикой исполнения сделок.

Мы используем объект CTrade для обработки всех торговых операций, таких как размещение, изменение и закрытие сделок. initialLotSize представляет размер лота первой сделки, а currentLotSize отслеживает размер лота для последующих восстановительных сделок, который увеличивается в зависимости от multiplier. zoneSize и targetSize определяют критические границы системы восстановления. В частности, зона восстановления ограничена zoneHigh и zoneLow, а цель по прибыли определяется zoneTargetHigh и zoneTargetLow.

Для отслеживания потока сделок мы сохраняем lastOrderType (BUY или SELL) и lastOrderPrice, по которой была выполнена предыдущая сделка. Эта информация помогает нам определить, как позиционировать будущие сделки в ответ на движения рынка. Переменная symbol обозначает используемый торговый инструмент, а флаг isRecovery указывает, находится ли система в процессе восстановления. Сохраняя эти переменные в закрытом доступе, мы гарантируем, что только внутренняя логика класса может их изменять, сохраняя целостность и точность вычислений системы. После этого мы можем напрямую определить функции класса, а не вызывать их позже и определять, просто для упрощения. Поэтому вместо того, чтобы объявлять нужные нам функции и определять их позже, мы просто объявляем и определяем их раз и навсегда. Давайте сначала определим функцию, отвечающую за расчет зон восстановления.

// Calculate dynamic zones and targets
void CalculateZones() {
   if (lastOrderType == ORDER_TYPE_BUY) {
      zoneHigh = lastOrderPrice;                 //--- Upper boundary starts from the last BUY price.
      zoneLow = zoneHigh - zoneSize;             //--- Lower boundary is calculated by subtracting zone size.
      zoneTargetHigh = zoneHigh + targetSize;    //--- Profit target above the upper boundary.
      zoneTargetLow = zoneLow - targetSize;      //--- Buffer below the lower boundary for recovery trades.
   } else if (lastOrderType == ORDER_TYPE_SELL) {
      zoneLow = lastOrderPrice;                  //--- Lower boundary starts from the last SELL price.
      zoneHigh = zoneLow + zoneSize;             //--- Upper boundary is calculated by adding zone size.
      zoneTargetLow = zoneLow - targetSize;      //--- Buffer below the lower boundary for profit range.
      zoneTargetHigh = zoneHigh + targetSize;    //--- Profit target above the upper boundary.
   }
   Print("Zone recalculated: ZoneHigh=", zoneHigh, ", ZoneLow=", zoneLow, ", TargetHigh=", zoneTargetHigh, ", TargetLow=", zoneTargetLow);
}

Здесь мы разрабатываем функцию CalculateZones, которая играет важную роль в определении ключевых уровней для нашей стратегии Zone Recovery. Основная цель этой функции — вычислить четыре основных границы (zoneHigh, zoneLow, zoneTargetHigh и zoneTargetLow), которые определяют наши точки входа, восстановления и выхода с прибылью. Эти границы являются динамическими и корректируются в зависимости от типа и цены последнего исполненного ордера, что позволяет нам сохранять контроль над процессом восстановления.

Если наш последний ордер был ордером на ПОКУПКУ, мы устанавливаем zoneHigh на цену, по которой был исполнен ордер на ПОКУПКУ. От этой точки мы рассчитываем zoneLow, вычитая zoneSize из zoneHigh, создавая диапазон восстановления ниже первоначальной цены ПОКУПКИ. Чтобы установить наши целевые показатели прибыли, мы рассчитываем zoneTargetHigh, добавляя targetSize к zoneHigh, в то время как zoneTargetLow располагается ниже zoneLow на ту же величину targetSize. Такая структура позволит нам разместить сделки восстановления ниже первоначальной цены входа BUY и определить верхний и нижний пределы нашего диапазона прибыли.

Если наш последний ордер был SELL, мы меняем логику. Здесь мы устанавливаем zoneLow на цену последнего ордера SELL. Затем мы рассчитываем zoneHigh, добавляя zoneSize к zoneLow, формируя верхнюю границу диапазона восстановления. Цели прибыли устанавливаются путем расчета zoneTargetLow как значения ниже zoneLow, а zoneTargetHigh устанавливается выше zoneHigh, оба по targetSize. Эта настройка снова позволит нам инициировать восстановительные сделки выше исходного входа SELL, а также определить зону фиксации прибыли.

К концу этого процесса мы установили границы зоны восстановления и целевые показатели прибыли как для сделок BUY, так и для сделок SELL. Чтобы облегчить отладку и оценку стратегии, мы используем функцию Print для отображения значений zoneHigh, zoneLow, zoneTargetHigh и zoneTargetLow в журнале. Таким образом, мы можем определить еще одну функцию, которая будет отвечать за логику исполнения сделок.

// Open a trade based on the given type
bool OpenTrade(ENUM_ORDER_TYPE type) {
   if (type == ORDER_TYPE_BUY) {
      if (trade.Buy(currentLotSize, symbol)) {
         lastOrderType = ORDER_TYPE_BUY;         //--- Mark the last trade as BUY.
         lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Store the current BID price.
         CalculateZones();                       //--- Recalculate zones after placing the trade.
         Print(isRecovery ? "RECOVERY BUY order placed" : "INITIAL BUY order placed", " at ", lastOrderPrice, " with lot size ", currentLotSize);
         isRecovery = true;                      //--- Set recovery state to true after the first trade.
         return true;
      }
   } else if (type == ORDER_TYPE_SELL) {
      if (trade.Sell(currentLotSize, symbol)) {
         lastOrderType = ORDER_TYPE_SELL;        //--- Mark the last trade as SELL.
         lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Store the current BID price.
         CalculateZones();                       //--- Recalculate zones after placing the trade.
         Print(isRecovery ? "RECOVERY SELL order placed" : "INITIAL SELL order placed", " at ", lastOrderPrice, " with lot size ", currentLotSize);
         isRecovery = true;                      //--- Set recovery state to true after the first trade.
         return true;
      }
   }
   return false;                                 //--- Return false if the trade fails.
}

Здесь мы определяем функцию OpenTrade, которая возвращает булево значение. Цель этой функции — открыть сделку в зависимости от того, хотим ли мы исполнить ордер на ПОКУПКУ или ПРОДАЖУ. Сначала мы проверяем, является ли запрошенный тип ордера ПОКУПКОЙ. Если да, мы используем функцию trade.Buy, чтобы попытаться открыть позицию на покупку с текущим размером лота и указанным символом. Если сделка открыта успешно, мы устанавливаем lastOrderType на BUY, затем сохраняем текущую цену символа с помощью функции SymbolInfoDouble, чтобы получить цену предложения. Эта цена представляет собой цену, по которой мы открыли позицию. Затем мы пересчитываем зоны восстановления, вызывая функцию CalculateZones, которая корректирует уровни зон на основе новой позиции.

Далее мы выводим в журнал сообщение, указывающее, была ли это первоначальная покупка или восстановительная покупка. Мы используем тернарный оператор, чтобы проверить, является ли флаг isRecovery истинным или ложным — если он истинный, в сообщении будет указано, что это восстановительный ордер; в противном случае будет указано, что это первоначальный ордер. После этого мы устанавливаем флаг isRecovery в значение true, сигнализируя, что любые последующие сделки будут считаться частью процесса восстановления. Наконец, функция возвращает значение true, подтверждая, что сделка была размещена успешно.

Если тип ордера — ПРОДАЖА, мы выполняем те же шаги. Мы пытаемся открыть позицию ПРОДАЖИ, вызывая функцию trade.Sell с теми же параметрами, и после успешного выполнения мы сохраняем lastOrderPrice и корректируем зоны восстановления таким же образом. Мы выводим сообщение, указывающее, была ли это первоначальная ПРОДАЖА или ПРОДАЖА восстановления, снова используя тернарный оператор для проверки флага isRecovery. Затем флаг isRecovery устанавливается на значение true, и функция возвращает true, чтобы указать, что сделка была размещена успешно. Если по какой-либо причине сделка не была успешно открыта, функция возвращает false, указывая, что попытка совершения сделки не удалась. Это важные функции, которые должны быть закрытыми. Остальные функции могут быть публичными, это не вызывает проблем.

public:
   // Constructor
   ZoneRecovery(double initialLot, double zonePts, double targetPts, double lotMultiplier, string _symbol) {
      initialLotSize = initialLot;
      currentLotSize = initialLot;                 //--- Start with the initial lot size.
      zoneSize = zonePts * _Point;                 //--- Convert zone size to points.
      targetSize = targetPts * _Point;             //--- Convert target size to points.
      multiplier = lotMultiplier;
      symbol = _symbol;                            //--- Initialize the trading symbol.
      lastOrderType = ORDER_TYPE_BUY;
      lastOrderPrice = 0.0;                        //--- No trades exist initially.
      isRecovery = false;                          //--- No recovery process active at initialization.
   }

Здесь мы объявляем публичный раздел класса ZoneRecovery, который содержит конструктор. Конструктор используется для инициализации объекта класса ZoneRecovery с определенными параметрами при его создании. Конструктор принимает в качестве входных данных initialLot, zonePts, targetPts, lotMultiplier и _symbol.

Мы начинаем с присвоения значения initialLot переменным initialLotSize и currentLotSize, обеспечивая, чтобы обе переменные начинались с одинакового значения, которое представляет размер лота для первой сделки. Затем мы вычисляем zoneSize, умножая zonePts (расстояние зоны в пунктах) на _Point, которая является встроенной константой, представляющей минимальное движение цены для символа. Аналогичным образом, targetSize рассчитывается путем преобразования targetPts (целевое расстояние прибыли) в пункты с использованием того же подхода. multiplier устанавливается на lotMultiplier, который будет использоваться позже для корректировки размера лота для восстановительных сделок.

Далее текущий символ _symbol присваивается переменной symbol, чтобы указать, какой торговый инструмент будет использоваться. lastOrderType изначально устанавливается на ORDER_TYPE_BUY, предполагая, что первая сделка будет ордером на покупку. lastOrderPrice устанавливается на 0.0, поскольку еще не было выполнено ни одной сделки. Наконец, isRecovery устанавливается на false, что указывает на то, что процесс восстановления еще не активен. Этот конструктор гарантирует, что объект ZoneRecovery правильно инициализирован и подготовлен для управления сделками и процессами восстановления. Далее мы определяем функцию для запуска сделок на основе внешних сигналов.

// Trigger trade based on external signals
void HandleSignal(ENUM_ORDER_TYPE type) {
   if (lastOrderPrice == 0.0)                   //--- Open the first trade if no trades exist.
      OpenTrade(type);
}

Здесь мы определяем функцию HandleSignal, которая принимает в качестве параметра тип ENUM_ORDER_TYPE, представляющий тип сделки, которая должна быть выполнена (BUY или SELL). Сначала мы проверяем, является ли lastOrderPrice равным 0.0, что указывает на то, что предыдущая сделка не была выполнена. Если это условие выполняется, это означает, что это первая сделка, которая будет открыта, поэтому мы вызываем функцию OpenTrade и передаем ей параметр type. Затем функция OpenTrade обработает логику открытия ордера BUY или SELL на основе полученного сигнала. Теперь мы можем управлять зонами, открывая сделки восстановления с помощью приведенной ниже логики.

// Manage zone recovery positions
void ManageZones() {
   double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Get the current BID price.

   // Open recovery trades based on zones
   if (lastOrderType == ORDER_TYPE_BUY && currentPrice <= zoneLow) {
      currentLotSize *= multiplier;            //--- Increase lot size for recovery.
      OpenTrade(ORDER_TYPE_SELL);              //--- Open a SELL order for recovery.
   } else if (lastOrderType == ORDER_TYPE_SELL && currentPrice >= zoneHigh) {
      currentLotSize *= multiplier;            //--- Increase lot size for recovery.
      OpenTrade(ORDER_TYPE_BUY);               //--- Open a BUY order for recovery.
   }
}

Для управления открытыми сделками мы определяем функцию void под названием ManageZones, которая отвечает за управление сделками восстановления на основе заранее определенных ценовых зон. Внутри этой функции мы сначала извлекаем текущую цену BID для указанного символа с помощью функции SymbolInfoDouble с параметром SYMBOL_BID. Это дает нам текущую рыночную цену, по которой торгуется актив.

Затем мы проверяем тип сделки последнего исполненного ордера с помощью переменной lastOrderType. Если последняя сделка была BUY, а текущая рыночная цена упала до или ниже zoneLow (нижней границы зоны восстановления), мы увеличиваем currentLotSize, умножая его на multiplier, чтобы выделить больше капитала для сделки восстановления. После этого мы вызываем функцию OpenTrade с параметром ORDER_TYPE_SELL, указывая, что нам нужно открыть позицию SELL, чтобы управлять убытком от предыдущей сделки BUY.

Аналогично, если последняя сделка была SELL, а текущая рыночная цена поднялась до или выше zoneHigh (верхней границы зоны восстановления), мы снова увеличиваем currentLotSize, умножая его на multiplier, чтобы увеличить размер сделки для восстановления. Затем мы вызываем функцию OpenTrade с параметром ORDER_TYPE_BUY, открывая позицию BUY для восстановления от предыдущей сделки SELL. Все очень просто. Теперь, после того как мы открыли начальную сделку и сделку восстановления, нам нужна логика, чтобы закрыть их в определенный момент. Давайте определим логику закрытия или целевую логику ниже.

// Check and close trades at zone targets
void CheckCloseAtTargets() {
   double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Get the current BID price.

   // Close BUY trades at target high
   if (lastOrderType == ORDER_TYPE_BUY && currentPrice >= zoneTargetHigh) {
      for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Loop through all open positions.
         if (PositionGetSymbol(i) == symbol) { //--- Check if the position belongs to the current symbol.
            ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Retrieve the ticket number.
            int retries = 10;
            while (retries > 0) {
               if (trade.PositionClose(ticket)) { //--- Attempt to close the position.
                  Print("Closed BUY position with ticket: ", ticket);
                  break;
               } else {
                  Print("Failed to close BUY position with ticket: ", ticket, ". Retrying... Error: ", GetLastError());
                  retries--;
                  Sleep(100);                   //--- Wait 100ms before retrying.
               }
            }
            if (retries == 0)
               Print("Gave up on closing BUY position with ticket: ", ticket);
         }
      }
      Reset();                                  //--- Reset the strategy after closing all positions.
   }
   // Close SELL trades at target low
   else if (lastOrderType == ORDER_TYPE_SELL && currentPrice <= zoneTargetLow) {
      for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Loop through all open positions.
         if (PositionGetSymbol(i) == symbol) { //--- Check if the position belongs to the current symbol.
            ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Retrieve the ticket number.
            int retries = 10;
            while (retries > 0) {
               if (trade.PositionClose(ticket)) { //--- Attempt to close the position.
                  Print("Closed SELL position with ticket: ", ticket);
                  break;
               } else {
                  Print("Failed to close SELL position with ticket: ", ticket, ". Retrying... Error: ", GetLastError());
                  retries--;
                  Sleep(100);                   //--- Wait 100ms before retrying.
               }
            }
            if (retries == 0)
               Print("Gave up on closing SELL position with ticket: ", ticket);
         }
      }
      Reset();                                  //--- Reset the strategy after closing all positions.
   }
}

Здесь мы определяем функцию CheckCloseAtTargets, которая отвечает за проверку, достигли ли открытые сделки заранее заданной целевой цены, и за их закрытие в соответствии с этим.

Сначала мы получаем текущую цену BID для данного символа с помощью функции SymbolInfoDouble с параметром SYMBOL_BID. Это дает нам текущую рыночную цену символа, которую мы будем использовать для сравнения с целевыми ценовыми уровнями (либо zoneTargetHigh, либо zoneTargetLow), чтобы решить, следует ли закрывать сделки.

Затем мы проверяем, является ли последний тип ордера BUY и достигла ли текущая цена уровня zoneTargetHigh (целевого уровня цены для сделки BUY) или превысила его. Если эти условия выполняются, мы просматриваем все открытые позиции с помощью функции PositionsTotal, начиная с последней позиции. Для каждой открытой позиции мы проверяем, принадлежит ли позиция тому же символу, используя функцию PositionGetSymbol. Если символ совпадает, мы извлекаем номер тикета позиции, используя функцию PositionGetInteger с параметром POSITION_TICKET.

Затем мы пытаемся закрыть позицию, вызывая функцию trade.PositionClose с полученным тикетом. Если позиция закрыта успешно, мы выводим подтверждающее сообщение о том, что позиция BUY была закрыта, включая номер тикета. Если закрытие не удалось, мы повторяем попытку до 10 раз, каждый раз выводя сообщение об ошибке и используя функцию Sleep, чтобы подождать 100 миллисекунд перед повторной попыткой. Если после 10 попыток мы по-прежнему не можем закрыть позицию, мы выводим сообщение об ошибке и переходим к следующей открытой позиции. Как только все позиции закрыты или достигнут лимит попыток, мы вызываем функцию Reset для сброса стратегии, обеспечивая очистку состояния для любых будущих сделок.

Аналогично, если последний тип ордера — SELL, а текущая цена достигла или упала ниже zoneTargetLow (целевого уровня цены для сделки SELL), процесс повторяется для всех позиций SELL. Функция будет пытаться закрыть позиции SELL таким же образом, повторяя попытки при необходимости и выводя сообщения о состоянии на каждом шаге. Мы использовали внешнюю функцию для сброса состояния, но вот логика, которая была принята.

// Reset the strategy after hitting targets
void Reset() {
   currentLotSize = initialLotSize;             //--- Reset lot size to the initial value.
   lastOrderType = -1;                          //--- Clear the last order type.
   lastOrderPrice = 0.0;                        //--- Clear the last order price.
   isRecovery = false;                          //--- Set recovery state to false.
   Print("Strategy reset after closing trades.");
}

Мы определяем функцию под названием Reset, которая отвечает за сброс внутренних переменных стратегии и подготовку системы к следующей сделке или сценарию сброса. Мы начинаем со сброса currentLotSize до initialLotSize, что означает, что после серии восстановительных сделок или достижения целевых уровней мы возвращаем размер лота к его исходному значению. Это гарантирует, что стратегия начнет с нуля с исходным размером лота для любых новых сделок.

Затем мы очищаем lastOrderType, устанавливая его значение -1, что фактически указывает на отсутствие предыдущего типа ордера (ни BUY, ни SELL). Это помогает избежать путаницы или зависимости от предыдущего типа ордера в будущей торговой логике. Аналогичным образом, мы сбрасываем lastOrderPrice до 0,0, очищая последнюю цену, по которой была выполнена сделка. Затем мы устанавливаем флаг isRecovery в значение false, сигнализируя, что процесс восстановления больше не активен. Это особенно важно, поскольку гарантирует, что любые будущие сделки будут рассматриваться как начальные сделки, а не как часть стратегии восстановления.

Наконец, мы выводим сообщение с помощью функции Print, указывая, что стратегия была успешно сброшена после закрытия всех сделок. Это обеспечивает обратную связь в терминале, помогая трейдеру отслеживать, когда стратегия была сброшена, и обеспечивая правильное состояние для будущих операций. По сути, функция очищает все важные переменные, которые отслеживают условия торговли, состояния восстановления и размеры сделок, возвращая систему к настройкам по умолчанию для новых операций. И это все, что нам нужно, чтобы класс обрабатывал все входящие сигналы. Теперь мы можем приступить к инициализации объекта класса, передавая параметры по умолчанию.

ZoneRecovery zoneRecovery(0.1, 200, 400, 2.0, _Symbol);
//--- Initialize the ZoneRecovery object with specified parameters.

Здесь мы создаем экземпляр класса ZoneRecovery, вызывая его конструктор и передавая необходимые параметры. В частности, мы инициализируем объект zoneRecovery со следующими значениями:

  • "0.1" – начальный размер лота. Это означает, что для первой сделки будет использоваться лот размером 0,1.
  • "200" – размер зоны, который представляет собой количество пунктов, определяющих диапазон зоны восстановления. Затем он умножается на _Point, чтобы преобразовать это значение в фактические пункты для указанного символа.
  • "400" – целевой размер, определяющий расстояние в пунктах до целевого уровня прибыли. Аналогично размеру зоны, это значение также преобразуется в пункты с помощью _Point.
  • "2.0" – множитель, который будет использоваться для увеличения размера лота в последующих сделках восстановления, если это необходимо.
  • "_Symbol" используется в качестве торгового символа для данного конкретного экземпляра ZoneRecovery, который соответствует символу инструмента, используемого трейдером.

Инициализируя zoneRecovery с этими параметрами, мы настраиваем объект для обработки торговой логики для этой конкретной торговой стратегии, включая управление зонами восстановления, корректировку размера лота и целевые уровни для любых сделок, которые будут открыты или управляемы. Этот объект готов обрабатывать торговые операции на основе определенной стратегии восстановления после запуска системы. Теперь мы можем перейти к обработчикам событий, где сосредоточимся на генерации сигналов. Начнем с обработчика событий OnInit. Здесь нам просто нужно инициализировать дескриптор индикатора и установить массив хранения в качестве временной серии.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   //--- Initialize RSI indicator
   rsiHandle = iRSI(_Symbol, PERIOD_CURRENT, rsiPeriod, PRICE_CLOSE); //--- Create RSI indicator handle.
   if (rsiHandle == INVALID_HANDLE) { //--- Check if RSI handle creation failed.
      Print("Failed to create RSI handle. Error: ", GetLastError());
      return(INIT_FAILED); //--- Return failure status if RSI initialization fails.
   }
   ArraySetAsSeries(rsiBuffer, true); //--- Set the RSI buffer as a time series to align values.
   Print("Zone Recovery Strategy initialized."); //--- Log successful initialization.
   return(INIT_SUCCEEDED); //--- Return success status.
}

Здесь мы инициализируем индикатор RSI и подготавливаем систему к торговле, выполняя ряд задач настройки в функции OnInit. Сначала мы создаем дескриптор индикатора RSI, вызывая функцию iRSI и передавая текущий символ (_Symbol), таймфрейм PERIOD_CURRENT, указанный rsiPeriod и тип цены PRICE_CLOSE. Этот шаг настраивает индикатор RSI для использования в стратегии.

Затем мы проверяем, удалось ли создать дескриптор, проверяя, не равен ли он INVALID_HANDLE. Если создание не удалось, мы выводим сообщение об ошибке с конкретным кодом ошибки, используя функцию GetLastError, и возвращаем INIT_FAILED, чтобы сигнализировать об ошибке. Если создание дескриптора прошло успешно, мы продолжаем, устанавливая буфер RSI в качестве временной серии с помощью ArraySetAsSeries, чтобы выровнять буфер с временной серией графика, обеспечивая, чтобы самые последние значения находились в индексе 0. Наконец, мы выводим сообщение об успешном завершении, подтверждающее инициализацию стратегии восстановления зоны, и возвращаем INIT_SUCCEEDED, сигнализируя, что настройка прошла успешно и советник готов к работе. Вот иллюстрация.

SUCCESS INITIALIZATION

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   if (rsiHandle != INVALID_HANDLE) //--- Check if RSI handle is valid.
      IndicatorRelease(rsiHandle); //--- Release RSI indicator handle to free resources.

   Print("Zone Recovery Strategy deinitialized."); //--- Log deinitialization message.
}

Здесь мы деинициализируем стратегию и освобождаем все ресурсы, используемые индикатором RSI, когда советник удаляется или останавливается. В функции OnDeinit мы сначала проверяем, является ли rsiHandle действительным, подтверждая, что он не равен INVALID_HANDLE. Это гарантирует, что дескриптор индикатора RSI существует, прежде чем пытаться его освободить.

Если дескриптор действителен, мы используем функцию IndicatorRelease для освобождения ресурсов, связанных с индикатором RSI, обеспечивая правильное управление памятью и ее освобождение после остановки работы советника. Наконец, мы выводим сообщение "Zone Recovery Strategy deinitialized" в журнал, чтобы подтвердить, что процесс деинициализации завершен и система была правильно закрыта. Это гарантирует, что советник может быть безопасно удален без оставления ненужных выделенных ресурсов. Вот пример результата.

SUCCESS RELEASE OF RSI INDICATOR HANDLE

После обработки экземпляра программа останавливается, и мы можем перейти к окончательному обработчику событий, который является основным, где обрабатываются тики, то есть обработчику событий OnTick.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   //--- Copy RSI values
   if (CopyBuffer(rsiHandle, 0, 1, 2, rsiBuffer) <= 0) { //--- Attempt to copy RSI buffer values.
      Print("Failed to copy RSI buffer. Error: ", GetLastError()); //--- Log failure if copying fails.
      return; //--- Exit the function on failure to avoid processing invalid data.
   }

//---

}

В функции OnTick мы сначала пытаемся скопировать значения RSI в массив rsiBuffer с помощью функции CopyBuffer. Функция CopyBuffer вызывается с параметрами: дескриптор индикатора RSI rsiHandle, индекс буфера 0 (который указывает на основной буфер RSI), начальная позиция 1 (где начать копирование данных), количество значений для копирования 2 и массив rsiBuffer, в котором будут храниться скопированные данные. Эта функция извлекает два последних значения RSI и сохраняет их в буфере.

Далее мы проверяем, была ли операция копирования успешной, оценивая, больше ли возвращаемое значение 0. Если операция завершилась неудачно (т. е. возвращает значение меньше или равно 0), мы регистрируем сообщение об ошибке, указывающее, что копирование буфера RSI завершилось неудачно, с помощью функции Print и отображаем код GetLastError, чтобы предоставить подробную информацию об ошибке. После регистрации ошибки мы немедленно выходим из функции с помощью return, чтобы предотвратить дальнейшую обработку на основе недействительных или отсутствующих данных RSI. Это гарантирует, что советник не будет пытаться принимать торговые решения на основе неполных или неверных данных, что позволяет избежать потенциальных ошибок или убытков. Если мы не прекращаем процесс, это означает, что у нас есть необходимые запрошенные данные и мы можем продолжать принимать торговые решения.

//--- Check RSI crossover signals
datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); //--- Get the time of the current bar.
if (currentBarTime != lastBarTime) { //--- Ensure processing happens only once per bar.
   lastBarTime = currentBarTime; //--- Update the last processed bar time.
   if (rsiBuffer[1] > 30 && rsiBuffer[0] <= 30) { //--- Check for RSI crossing below 30 (oversold signal).
      Print("BUY SIGNAL"); //--- Log a BUY signal.
      zoneRecovery.HandleSignal(ORDER_TYPE_BUY); //--- Trigger the Zone Recovery BUY logic.
   } else if (rsiBuffer[1] < 70 && rsiBuffer[0] >= 70) { //--- Check for RSI crossing above 70 (overbought signal).
      Print("SELL SIGNAL"); //--- Log a SELL signal.
      zoneRecovery.HandleSignal(ORDER_TYPE_SELL); //--- Trigger the Zone Recovery SELL logic.
   }
}

Здесь мы проверяем сигналы пересечения RSI на каждом новом баре рынка, чтобы запустить потенциальные сделки. Мы начинаем с извлечения временной метки текущего бара с помощью функции iTime. Функция принимает символ (_Symbol), таймфрейм (PERIOD_CURRENT) и индекс бара (0 для текущего бара). Это дает currentBarTime, который представляет временную метку самого последнего завершенного бара.

Далее мы убеждаемся, что торговая логика выполняется только один раз за бар, сравнивая currentBarTime с lastBarTime. Если времена различаются, это означает, что сформировался новый бар, поэтому мы приступаем к обработке. Затем мы обновляем lastBarTime, чтобы он соответствовал currentBarTime, чтобы отслеживать последний обработанный бар и предотвратить повторное выполнение в течение одного и того же бара.

Следующим шагом является обнаружение сигналов пересечения RSI. Сначала мы проверяем, пересекло ли значение RSI уровень 30 (состояние перепроданности), сравнивая rsiBuffer[1] (значение RSI предыдущего бара) с rsiBuffer[0] (значение RSI текущего бара). Если RSI предыдущего бара был выше 30, а RSI текущего бара равен или ниже 30, это указывает на потенциальный сигнал ПОКУПКИ, поэтому мы выводим сообщение "BUY SIGNAL", а затем вызываем функцию HandleSignal объекта zoneRecovery, чтобы запустить процесс восстановления для ордера ПОКУПКИ.

Аналогичным образом мы проверяем, пересек ли RSI отметку 70 (состояние перекупленности). Если RSI предыдущего бара был ниже 70, а RSI текущего бара равен или выше 70, это сигнализирует о потенциальном сигнале SELL, и мы выводим сообщение "SELL SIGNAL". Затем мы снова вызываем HandleSignal, но на этот раз для ордера SELL, запуская соответствующую логику Zone Recovery SELL. Наконец, мы просто вызываем соответствующие функции для управления открытыми зонами и закрываем их при достижении целей.

//--- Manage zone recovery logic
zoneRecovery.ManageZones(); //--- Perform zone recovery logic for active positions.

//--- Check and close at zone targets
zoneRecovery.CheckCloseAtTargets(); //--- Evaluate and close trades when target levels are reached.

Здесь мы используем точечный оператор (".") для вызова функций, которые являются частью класса "ZoneRecovery". Сначала мы используем "zoneRecovery.ManageZones()" для выполнения метода ManageZones, который обрабатывает логику управления сделками восстановления зоны на основе текущей цены и определенных зон восстановления. Этот метод корректирует размер лота для сделок восстановления и открывает новые позиции по мере необходимости.

Затем мы вызываем zoneRecovery.CheckCloseAtTargets() для запуска метода CheckCloseAtTargets, который проверяет, достигла ли цена целевых уровней для закрытия позиций. Если условия выполнены, он пытается закрыть открытые сделки, обеспечивая соответствие стратегии целевым границам прибыли или убытка. Используя оператор точки, мы получаем доступ и выполняем эти методы на объекте zoneRecovery, чтобы эффективно управлять процессом восстановления. Чтобы убедиться, что методы успешно вызываются при каждом тике, мы запускаем программу, и вот результат.

ONTICK EVENT HANDLER RESET CONFIRMATION

На изображении мы видим, что мы успешно вызываем методы класса, чтобы подготовить программу на первом тике, что подтверждает, что наш класс программы подключен и готов к работе. Чтобы подтвердить это, мы запускаем программу, и вот подтверждения сделок.

BUY LOG AND CONFIRMATION

На изображении мы видим, что мы подтверждаем сигнал на покупку, открываем позицию, добавляем ее в систему восстановления зоны, пересчитываем уровни зоны, определяем, что это начальная позиция, и когда цель достигнута, мы закрываем позицию и сбрасываем систему для следующей сделки. Давайте попробуем посмотреть на случай, когда мы входим в систему восстановления зоны.

ZONE RECOVERY ENTERED

На изображении мы видим, что когда рынок идет против нас на 200 пунктов, мы предполагаем, что тренд является бычьим, и следуем ему, открывая позицию на покупку с более высоким размером лота, в данном случае 0,2.

FULL RECOVERY

Мы снова видим, что когда рынок достигает целевых уровней, мы закрываем сделки и сбрасываем настройки для следующей. Пока система находится в режиме восстановления, мы игнорируем любые входящие сигналы. Это подтверждает, что мы успешно достигли нашей цели, и остается только провести бэктестинг программы и проанализировать ее производительность. Это будет рассмотрено в следующем разделе.


Бэктестинг и анализ эффективности

В этом разделе мы сосредоточимся на процессе бэктестинга и анализе эффективности нашей системы Zone Recovery RSI. Бэктестинг позволяет нам оценить эффективность нашей стратегии на исторических данных, выявить потенциальные недостатки и точно настроить параметры для достижения лучших результатов в реальной торговле.

Мы начинаем с настройки тестера стратегий в платформе MetaTrader 5. Тестер стратегий позволяет нам моделировать исторические рыночные условия и выполнять сделки так, как если бы они происходили в режиме реального времени. Для запуска бэктеста мы выбираем соответствующий символ, временной интервал и период тестирования. Мы также убеждаемся, что включен визуальный режим, если мы хотим видеть сделки по мере их появления на графике.

Как только среда бэктестинга готова, мы настраиваем входные данные для нашей программы. Мы указываем начальный депозит, размер лота и конкретные параметры, связанные с логикой Zone Recovery. Ключевые входные данные включают начальный размер лота, размер зоны, целевой размер и множитель. Изменяя эти входные данные, мы можем проанализировать, как они влияют на общую прибыльность стратегии. Вот что мы изменили.

ZoneRecovery zoneRecovery(0.1, 700, 1400, 2.0, _Symbol);
//--- Initialize the ZoneRecovery object with specified parameters.

Мы настроили систему на работу с 1 января 2024 года в течение всего года, и вот результаты.

График тестера стратегий:

GRAPH 1

Отчет тестера стратегий:

REPORT 1

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

//+------------------------------------------------------------------+
//|      FUNCTION TO APPLY TRAILING STOP                             |
//+------------------------------------------------------------------+
void applyTrailingStop(double slPoints, CTrade &trade_object, int magicNo=0, double minProfitPoints=0){
   double buySl = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - slPoints*_Point, _Digits); //--- Calculate the stop loss price for BUY trades
   double sellSl = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + slPoints*_Point, _Digits); //--- Calculate the stop loss price for SELL trades
   
   for (int i = PositionsTotal() - 1; i >= 0; i--){ //--- Loop through all open positions
      ulong ticket = PositionGetTicket(i); //--- Get the ticket number of the current position
      if (ticket > 0){ //--- Check if the ticket is valid
         if (PositionSelectByTicket(ticket)){ //--- Select the position by its ticket number
            if (PositionGetString(POSITION_SYMBOL) == _Symbol && //--- Check if the position belongs to the current symbol
               (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)){ //--- Check if the position matches the given magic number or if no magic number is specified
               
               double positionOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get the opening price of the position
               double positionSl = PositionGetDouble(POSITION_SL); //--- Get the current stop loss of the position
               
               if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){ //--- Check if the position is a BUY trade
                  double minProfitPrice = NormalizeDouble(positionOpenPrice + minProfitPoints * _Point, _Digits); //--- Calculate the minimum price at which profit is locked
                  if (buySl > minProfitPrice &&  //--- Check if the calculated stop loss is above the minimum profit price
                      buySl > positionOpenPrice && //--- Check if the calculated stop loss is above the opening price
                      (buySl > positionSl || positionSl == 0)){ //--- Check if the calculated stop loss is greater than the current stop loss or if no stop loss is set
                     trade_object.PositionModify(ticket, buySl, PositionGetDouble(POSITION_TP)); //--- Modify the position to update the stop loss
                  }
               }
               else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){ //--- Check if the position is a SELL trade
                  double minProfitPrice = NormalizeDouble(positionOpenPrice - minProfitPoints * _Point, _Digits); //--- Calculate the minimum price at which profit is locked
                  if (sellSl < minProfitPrice &&  //--- Check if the calculated stop loss is below the minimum profit price
                      sellSl < positionOpenPrice && //--- Check if the calculated stop loss is below the opening price
                      (sellSl < positionSl || positionSl == 0)){ //--- Check if the calculated stop loss is less than the current stop loss or if no stop loss is set
                     trade_object.PositionModify(ticket, sellSl, PositionGetDouble(POSITION_TP)); //--- Modify the position to update the stop loss
                  }
               }
            }
         }
      }
   }
}

Здесь мы создаем функцию под названием applyTrailingStop, которая позволяет нам применять трейлинг-стоп ко всем открытым позициям BUY и SELL. Цель этого трейлинг-стопа — защитить и зафиксировать прибыль, когда рынок движется в пользу наших сделок. Мы используем объект CTrade для автоматического изменения уровней стоп-лосса для сделок. Чтобы трейлинг-стоп не срабатывал слишком рано, мы включаем условие, требующее достижения минимальной прибыли, прежде чем стоп-лосс начнет трейлинг. Такой подход предотвращает преждевременную корректировку стоп-лосса и гарантирует, что мы обеспечим определенную прибыль перед трейлингом.

В этой функции мы определяем четыре ключевых параметра. Параметр slPoints указывает расстояние в пунктах от текущей рыночной цены до нового уровня стоп-лосса. Параметр trade_object относится к объекту CTrade, который позволяет нам управлять открытыми позициями, изменять стоп-лосс и корректировать тейк-профит. Параметр magicNo служит уникальным идентификатором для фильтрации сделок. Если magicNo установлен на 0, мы применяем трейлинг-стоп ко всем сделкам, независимо от их магического числа. Наконец, параметр minProfitPoints определяет минимальную прибыль (в пунктах), которая должна быть достигнута, прежде чем сработает трейлинг-стоп. Это гарантирует, что мы корректируем стоп-лосс только после того, как позиция принесет достаточную прибыль.

Здесь мы начинаем с расчета цен трейлинг-стоп-лосса для сделок BUY и SELL. Для сделок BUY мы рассчитываем новую цену стоп-лосса, вычитая slPoints из текущей цены BID. Для сделок SELL мы рассчитываем ее, прибавляя slPoints к текущей цене ASK. Эти цены стоп-лоссов нормализуются с помощью _Digits для обеспечения точности на основе точности цены символа. Этот этап нормализации гарантирует, что цены соответствуют правильному количеству десятичных знаков для конкретного финансового инструмента.

Затем мы просматриваем все открытые позиции, начиная с последней и переходя к первой. Этот подход с обратным циклом необходим, поскольку изменение позиций во время прямого цикла может привести к ошибкам в индексации позиций. Для каждой позиции мы получаем ее «тикет», который является уникальным идентификатором этой позиции. Если тикет действителен, мы используем функцию PositionSelectByTicket для выбора и доступа к деталям позиции.

После выбора позиции мы проверяем, соответствует ли она текущему символу и соответствует ли ее магическое число заданному magicNo. Если magicNo установлено на 0, мы применяем трейлинг-стоп ко всем сделкам, независимо от их магического числа. После определения соответствующей позиции мы определяем, является ли она сделкой ПОКУПКИ или ПРОДАЖИ.

Если позиция является сделкой BUY, мы рассчитываем минимальную цену, которую рынок должен достичь, прежде чем стоп-лосс начнет следовать. Это значение получается путем добавления minProfitPoints к цене открытия позиции. Затем мы проверяем, превышает ли рассчитанная цена трейлинг-стопа как цену открытия позиции, так и текущий стоп-лосс. Если эти условия выполняются, мы изменяем позицию с помощью «trade_object.PositionModify», обновляя цену стоп-лосса для сделки BUY.

Если позиция является сделкой SELL, мы следуем аналогичному процессу. Мы рассчитываем минимальную цену прибыли, вычитая «minProfitPoints» из цены открытия позиции. Мы проверяем, находится ли рассчитанная цена трейлинг-стопа ниже как цены открытия позиции, так и текущего стоп-лосса. Если эти условия выполняются, мы изменяем позицию с помощью «trade_object.PositionModify», обновляя стоп-лосс для сделки SELL.

Теперь, вооружившись этой функцией, нам нужна логика, чтобы сначала найти начальные позиции, и к этим функциям мы можем добавить логику трейлинг-стопа. Для этого нам нужно будет определить булеву переменную в классе восстановления зоны, но важно сделать ее доступной в любой точке программы, сделав ее публичной.

public:
   bool isFirstPosition;

Здесь у нас есть публичная переменная isFirstPosition внутри класса ZoneRecovery. Эта переменная имеет тип булева (bool), что означает, что она может содержать только два возможных значения: true или false. Цель этой функции — отслеживать, находится ли текущая сделка на первой позиции в процессе восстановления зоны. Когда isFirstPosition имеет значение true, это означает, что предыдущие сделки не были открыты, и это начальная позиция. Это различие очень важно, потому что логика обработки первой сделки изменится, поскольку мы хотим применить к ней логику трейлинг-стопа.

Поскольку мы объявляем isFirstPosition как public, к ней можно получить доступ и изменить ее извне класса «ZoneRecovery». Это позволяет другим частям программы проверять, является ли позиция первой в серии, или обновлять ее статус соответствующим образом. Теперь, внутри функции, отвечающей за открытие сделок, нам нужно присвоить булевы флаги для того, является ли это первой позицией или нет, как только позиция открыта.

if (trade.Buy(currentLotSize, symbol)) {
   lastOrderType = ORDER_TYPE_BUY;         //--- Mark the last trade as BUY.
   lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Store the current BID price.
   CalculateZones();                       //--- Recalculate zones after placing the trade.
   Print(isRecovery ? "RECOVERY BUY order placed" : "INITIAL BUY order placed", " at ", lastOrderPrice, " with lot size ", currentLotSize);
   isFirstPosition = isRecovery ? false : true;
   isRecovery = true;                      //--- Set recovery state to true after the first trade.
   return true;
}

Здесь мы устанавливаем переменную isFirstPosition в значение false, если позиция зарегистрирована как позиция восстановления, или в значение true, если переменная isRecovery имеет значение false. Опять же, в конструкторе и функциях сброса мы по умолчанию устанавливаем переменную target в значение false. Отсюда мы можем перейти к обработчику событий «OnTick» и применить трейлинг-стоп, когда у нас есть начальная позиция.

if (zoneRecovery.isFirstPosition == true){ //--- Check if this is the first position in the Zone Recovery process
   applyTrailingStop(100, obj_Trade, 0, 100); //--- Apply a trailing stop with 100 points, passing the "obj_Trade" object, a magic number of 0, and a minimum profit of 100 points
}

Здесь мы проверяем, является ли переменная zoneRecovery.isFirstPosition истинной, что указывает на то, что это первая позиция в процессе восстановления зоны. Если да, мы вызываем функцию applyTrailingStop. Передаваемые параметры: "100" пунктов для дистанции трейлинг-стопа, «obj_Trade» в качестве объекта торговли, магическое число «0» для идентификации сделки и минимальная прибыль «100» пунктов. Это гарантирует, что как только сделка достигнет прибыли в 100 пунктов, будет применен трейлинг-стоп для защиты прибыли путем отслеживания стоп-лосса по мере движения цены в пользу сделки. Однако, когда мы закрываем сделки с помощью трейлинг-стопа, у нас все еще остаются остатки логики восстановления зоны, поскольку мы их не сбрасываем. Это приводит к тому, что система открывает сделки восстановления, даже когда у нас нет существующих сделок. Вот что мы имеем в виду.

FAULTY TRAILING STOP LOGIC GIF

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

if (zoneRecovery.isFirstPosition == true && PositionsTotal() == 0){ //--- Check if this is the first position and if there are no open positions
   zoneRecovery.Reset(); //--- Reset the Zone Recovery system, restoring initial settings and clearing previous trade data
}

Здесь мы проверяем, является ли переменная isFirstPosition истинной и нет ли существующих позиций. Если оба условия выполняются, это означает, что у нас была начальная позиция, которая закрылась по какой-либо причине, и теперь, когда ее больше нет, мы вызываем функцию «zoneRecovery.Reset()». Это сбрасывает систему Zone Recovery, восстанавливая ее начальные настройки и очищая все предыдущие торговые данные, обеспечивая новый старт процесса восстановления. Эти изменения делают систему идеальной. После проведения окончательных тестов мы получили следующие результаты.

График тестера стратегий:

FINAL TESTER GRAPH

Отчет тестера стратегий:

FINAL TESTER REPORT

На изображении видно, что мы сократили количество позиций восстановления, что значительно увеличивает нашу частоту попаданий. Это подтверждает, что мы достигли нашей цели по созданию системы восстановления зоны с динамической логикой управления торговлей.


Заключение

В заключение мы продемонстрировали, как построить советник на языке MetaQuotes Language 5 (MQL5) с использованием стратегии Zone Recovery. Комбинируя индикатор относительной силы (RSI) с логикой «Zone Recovery», мы создали систему, способную обнаруживать торговые сигналы, управлять позициями восстановления и фиксировать прибыль с помощью трейлинг-стопов. Ключевыми элементами были идентификация сигналов, автоматическое исполнение сделок и динамическое восстановление позиций.

Отказ от ответственности: данная статья служит в качестве образовательного руководства по разработке программ MQL5. Хотя стратегия Zone Recovery RSI предлагает структурированный подход к управлению торговлей, рыночные условия остаются непредсказуемыми. Торговля сопряжена с финансовым риском, и прошлые результаты не гарантируют будущих результатов. Перед началом реальной торговли необходимо провести надлежащее тестирование и обеспечить управление рисками.

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

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/16705

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (8)
Allan Munene Mutiiria
Allan Munene Mutiiria | 30 янв. 2025 в 16:00
Amir Jafary #:

Я не могу найти файлы tradq mq, где они находятся

Это прекрасно объясняется даже с помощью изображения.

Silk Road Trading LLC
Ryan L Johnson | 30 янв. 2025 в 20:38
wupan123898 контролировать большие потери, автор имеет какой-либо метод, чтобы избежать больших потерь?

Хорошо сказано. Стратегия предполагает, что цена выйдет из первоначально проторгованного диапазона, чтобы возместить убытки. Как мы знаем, по крайней мере, на рынке Форекс, цена чаще колеблется, чем движется. Методом уменьшения катастрофических потерь по Мартингейлу было бы добавление фильтра волатильности для начальных сделок.

Allan Munene Mutiiria
Allan Munene Mutiiria | 30 янв. 2025 в 21:52
Ryan L Johnson #:

Хорошо сказано. Стратегия предполагает, что цена выйдет из первоначально проторгованного диапазона, чтобы восстановить потери. Как мы знаем, по крайней мере на рынке Форекс, цена чаще колеблется, чем развивается. Для уменьшения катастрофических потерь по Мартингейлу следует добавить фильтр волатильности для начальных сделок.

Конечно.

Silk Road Trading LLC
Ryan L Johnson | 16 мар. 2025 в 18:43
Allan Munene Mutiiria функцией тоже несложно, и она будет работать лучше, быстрее и более гибко, чем встроенный ATR ...
ENUM_TIMEFRAMES _atrTimeFrame = PERIOD_D1;
int             _atrPeriod    = 20;
double          _atrValue     = 0;
   MqlRates _rates[]; int _ratesCopied = CopyRates(_Symbol,_atrTimeFrame,0,_atrPeriod+1,_rates);
                      if (_ratesCopied>0)
                           for (int i=1; i<_ratesCopied; i++) _atrValue += MathMax(_rates[i].high,_rates[i-1].close)-MathMin(_rates[i].low,_rates[i-1].close);
                                                              _atrValue /= MathMax(_ratesCopied,1);

masuchai ma
masuchai ma | 21 мар. 2025 в 23:51

Я просмотрел код и задался вопросом, почему бы не использовать цену спроса для вычисления в случае покупки? Есть ли какой-нибудь конфликт, если я изменю это? Спасибо.

Модификация Алгоритма оптимизации динго — Dingo Optimization Algorithm M (DOAm) Модификация Алгоритма оптимизации динго — Dingo Optimization Algorithm M (DOAm)
Представленная в статье авторская модификация алгоритма динго высоко подняла планку для поиска лучшего из лучших алгоритма оптимизации. Возможны ли еще более высокие результаты?
Нейросети в трейдинге: Устойчивые торговые сигналы в любых режимах рынка (Окончание) Нейросети в трейдинге: Устойчивые торговые сигналы в любых режимах рынка (Окончание)
В статье подробно рассмотрена интеграция подходов фреймворка ST-Expert в архитектуру Extralonger, позволяющая одновременно анализировать временные и пространственные представления данных. Представлены результаты тестирования на реальных исторических данных, демонстрирующие эффективность модели и её устойчивость к рыночным аномалиям. Описана модульная структура фреймворка, обеспечивающая воспроизводимость, гибкость для исследований и возможность поэтапной оптимизации компонентов.
Разработка инструментария для анализа движения цен (Часть 8): Панель метрик Разработка инструментария для анализа движения цен (Часть 8): Панель метрик
Будучи одним из самых мощных наборов инструментов для анализа движения цен, панель метрик (Metrics Board) разработана для упрощения анализа рынка путем мгновенного предоставления основных рыночных показателей всего одним нажатием кнопки. Каждая кнопка выполняет определенную функцию: анализирует силу тренда, объем и другие ключевые показатели. Этот инструмент предоставляет точные данные в реальном времени, когда они вам больше всего нужны. Давайте подробнее рассмотрим его особенности в этой статье.
Как упростить ручное тестирование стратегий с помощью MQL5: строим свой набор инструментов Как упростить ручное тестирование стратегий с помощью MQL5: строим свой набор инструментов
В этой статье разрабатываем пользовательский набор инструментов MQL5 для удобного ручного тестирования на исторических данных в Тестере стратегий. Объясним его конструкцию и реализацию, уделив особое внимание интерактивным средствам управления сделками. Затем покажем, как использовать его для эффективного тестирования стратегий