Автоматизация торговых стратегий на MQL5 (Часть 4): Построение многоуровневой системы зонального восстановления
Введение
В предыдущей статье (часть 3 серии) мы изучили систему зонального восстановления RSI, продемонстрировав, как интегрировать генерацию сигналов на основе RSI с динамическим механизмом зонального восстановления для управления сделками и восстановления после неблагоприятных движений рынка с использованием MetaQuotes Language 5 (MQL5). В этой статье (часть 4) мы развиваем эти основы, представляя многоуровневую систему зонального восстановления, сложный подход к управлению сделками, способный одновременно обрабатывать несколько независимых сигналов.
Система использует индикатор относительной силы (RSI) для генерации торговых сигналов и динамически добавляет каждый сигнал в массив, обеспечивая бесшовную интеграцию с логикой Zone Recovery. Основная цель — масштабировать механизм зонального восстановления для эффективного управления несколькими торговыми настройками, сокращая общие просадки и улучшая торговые результаты.
Мы рассмотрим процесс разработки стратегии, кодирования системы в MQL5 и тестирования ее эффективности. Для облегчения понимания и организации мы разделили этапы на следующие темы:
- План стратегии
- Реализация в MQL5
- Тестирование на исторических данных
- Заключение
К концу курса вы получите практическое понимание того, как построить и оптимизировать многоуровневую систему зонального восстановления для динамичного и надежного управления торговлей.
План стратегии
Многоуровневая система зонального восстановления будет использовать хорошо организованную структуру для эффективного управления несколькими торговыми сигналами. Для этого мы определим структуру (struct), которая будет служить чертежом для создания отдельных торговых корзин. Каждый торговый сигнал, генерируемый индикатором RSI, связан с уникальной корзиной, хранящейся в виде элемента массива. Например, когда система генерирует сигнал 1, мы создадим корзину 1, которая будет не только хранить исходные данные о сделке, но и управлять всеми позициями восстановления, связанными с этим сигналом. Аналогично, сигнал 2 инициирует корзину 2, и эта корзина будет независимо отслеживать и выполнять все сделки восстановления на основе параметров сигнала 2. Ниже представлена визуализация свойств корзины и сигнала.

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

Структурируя систему таким образом, мы обеспечим высокую степень масштабируемости и гибкости. Каждая корзина будет действовать как самостоятельная единица, позволяя системе динамически реагировать на рыночные условия для каждого сигнала. Такая структура упростит отслеживание и управление сложными торговыми настройками, поскольку каждый сигнал и связанные с ним сделки восстановления будут четко организованы. Массивная корзина станет основой для построения надежной и адаптируемой многоуровневой системы зонального восстановления, способной обрабатывать различные торговые сценарии, сохраняя при этом эффективность и ясность. Итак, приступим к реализации.
Реализация в MQL5
Изучив все теории о торговой стратегии Multi-Level Zone Recovery, давайте автоматизируем теорию и создадим советник на языке MetaQuotes Language 5 (MQL5) для MetaTrader 5.
Чтобы создать советника, в терминале MetaTrader 5 перейдите на вкладку "Сервис" и выберите пункт "Редактор MetaQuotes Language" или просто нажмите F4 на клавиатуре. Также можно нажать на иконку IDE (интегрированная среда разработки) на панели инструментов. Откроется среда редактора MetaQuotes Language, которая позволяет писать торговых роботов, технические индикаторы, скрипты и библиотеки функций. После открытия MetaEditor в строке меню перейдите на вкладку "Файл" и выберите пункт "Новый файл" или просто нажмите CTRL + N, чтобы создать новый документ. Также можно нажать на иконку "Создать" на панели инструментов. Откроется всплывающее окно Мастера MQL.
В появившемся мастере выберите "Советник (шаблон)" и нажмите "Далее". В общих свойствах советника в разделе "Имя" укажите имя файла вашего советника. Обратите внимание, что для указания или создания папки, если она не существует, перед именем советника необходимо использовать обратный слеш. Например, здесь по умолчанию задано "Experts\". Это означает, что советник будет создан в папке Experts, и мы сможем найти его там. Остальные разделы довольно просты, но вы можете перейти по ссылке внизу мастера, чтобы узнать, как точно выполнить этот процесс.

После указания имени файла нажмите "Далее", затем "Далее" и "Готово". После этого мы готовы к написанию кода и программированию нашей стратегии.
Сначала мы начинаем с определения некоторых метаданных о советнике. Они включают название советника, информацию об авторских правах и ссылку на сайт MetaQuotes. Мы также указываем версию советника – "1.00".
//+------------------------------------------------------------------+ //| 1. Zone Recovery RSI EA Multi-Zone.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00"
Это отобразит метаданные системы при загрузке программы. Затем мы можем перейти к добавлению некоторых входных переменных, которые будут отображаться в пользовательском интерфейсе следующим образом.
sinput group "General EA Settings" input double inputlot = 0.01; input double inputzonesizepts = 200; input double inputzonetragetpts = 400; input double inputlotmultiplier = 2.0; input double inputTrailingStopPts = 50; // Trailing stop distance in points input double inputMinimumProfitPts = 50; // Minimum profit points before trailing stop starts input bool inputTrailingStopEnabled = true; // Enable or disable trailing stop
Мы определяем группу входных параметров в категории General EA Settings, позволяя пользователям настраивать основные параметры советника перед его запуском. Эти входные данные объявляются с использованием типа данных input в MQL5, что позволяет настраивать их непосредственно на панели настроек советника без изменения кода. Каждый входной параметр имеет свое специфическое назначение в управлении поведением советника и управлении рисками.
Параметр inputlot определяет начальный размер лота для открытия сделок, его значение по умолчанию составляет 0.01, что позволяет точно контролировать объем торгов. Параметр inputzonesizepts указывает размер зоны восстановления в пунктах, по умолчанию установленный на 200, который определяет расстояние между сделками восстановления. Параметр inputzonetragetpts со значением по умолчанию 400 устанавливает целевое расстояние прибыли в пунктах, указывая советнику когда закрывать позиции с прибылью.
Для расчета лотов восстановительных сделок используется параметр inputlotmultiplier, по умолчанию установленный на 2.0, что позволяет советнику динамически рассчитывать увеличивающиеся размеры лотов для восстановительных сделок на основе множителя. Кроме того, функция трейлинг-стопа реализуется с помощью трех параметров. Параметр inputTrailingStopPts определяет расстояние трейлинг-стопа в пунктах, по умолчанию установленное на 50, которое корректирует стоп-лосс по мере движения рынка в пользу сделки. Параметр inputMinimumProfitPts, также установленный на 50, гарантирует, что трейлинг-стоп активируется только после того, как сделка достигнет минимального порога прибыли.
Наконец, параметр inputTrailingStopEnabled, тип данных bool, позволяет пользователям включать или отключать функцию трейлинг-стопа по мере необходимости. Такая гибкость гарантирует, что советник способен адаптироваться к различным торговым стратегиям, профилям риска и рыночным условиям, предоставляя настраиваемую структуру для эффективного управления торговлей и рисками. Далее, поскольку мы будем открывать сделки, нам необходимо включить несколько дополнительных файлов, чтобы мы могли добавить экземпляр сделки через директиву #include. Это дает нам доступ к классу CTrade, который мы будем использовать для создания объекта сделки. Это очень важно, поскольку нам это необходимо для открытия сделок.
#include <Trade/Trade.mqh> //--- Includes the MQL5 Trade library for handling trading operations.
Препроцессор заменит строку #include <Trade/Trade.mqh> содержимым файла Trade.mqh. Угловые скобки указывают, что файл Trade.mqh будет взят из стандартного каталога (обычно это terminal_installation_directory\MQL5\Include). Текущий каталог не включается в поиск. Строка может быть размещена в любом месте программы, но обычно все включения размещаются в начале исходного кода для лучшей структуры кода и удобства ссылки. Вот конкретный файл из раздела навигатора.

После этого нам нужно объявить несколько важных глобальных переменных, которые мы будем использовать в торговой системе.
//--- 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. struct PositionRecovery {
Здесь мы определяем набор глобальных переменных для управления логикой индикатора RSI, который будет управлять торговыми сигналами советника. Эти переменные предназначены для эффективного вычисления, извлечения и обработки значений RSI. Объявляя их глобальными, мы обеспечиваем их доступность во всем советнике, что позволяет генерировать сигналы последовательно и эффективно. Мы присваиваем переменной rsiPeriod типа int значение 14, указывая период обратного просмотра для расчета индикатора RSI. Это значение определяет количество баров, которые советник будет анализировать для вычисления значений RSI, что дает нам контроль над чувствительностью индикатора. Далее мы объявляем переменную rsiHandle, еще одну переменную типа int, которую будем использовать для хранения дескриптора индикатора RSI. Этот дескриптор получается при инициализации RSI с помощью функции iRSI, что позволяет нам извлекать значения RSI непосредственно из буфера индикаторов терминала.
Для хранения этих значений RSI мы создаем rsiBuffer[], динамический массив типа double. Этот массив будет хранить рассчитанные значения RSI для каждого бара, которые мы будем использовать для определения состояний перекупленности или перепроданности на рынке. Кроме того, мы определяем переменную lastBarTime типа datetime для хранения времени последнего обработанного бара. Отслеживая это значение, мы гарантируем, что советник обрабатывает новый сигнал только при появлении нового бара, предотвращая дублирование сигналов для одного и того же бара. Теперь мы можем определить общие параметры корзины (Basket) в структуре, которую мы будем связывать с каждым сгенерированным сигналом. Для этого мы используем логику struct, общий синтаксис которой выглядит следующим образом:
//--- Struct to track individual position recovery states //--- Member 1 //--- Member 2 //--- Member 3 //--- Method 1 //... };
Чтобы объединить связанные переменные данных, нам нужна структура; ниже приведен ее общий прототип. Мы определяем структуру с именем PositionRecovery, которая служит чертежом для организации и управления данными, связанными с отдельными состояниями восстановления позиции в советнике. Структура действует как пользовательский тип данных, позволяя нам группировать связанные переменные (члены) и функции (методы) в единое целое.
Объяснение синтаксиса:
"struct" "PositionRecovery { ... };"
Это определяет структуру PositionRecovery. Ключевое слово struct используется для определения структуры, а фигурные скобки { ... } заключают в себя члены и методы структуры. Точка с запятой (;) в конце определения является обязательной в MQL5.
- Члены
Члены — это переменные, определенные в структуре, которые хранят данные, специфичные для каждого экземпляра PositionRecovery.
"//--- Member 1": Заполнитель для переменной, такой как начальный размер лота или цена входа в сделку.
"//--- Member 2": может представлять такие параметры, как размер зоны восстановления или текущее состояние сделки.
"//--- Member 3": Дополнительные данные, такие как количество выполненных сделок восстановления или флаг, указывающий на завершение восстановления.
Эти члены позволяют нам инкапсулировать всю информацию, необходимую для отслеживания и управления отдельным процессом восстановления.
- Методы
Методы — это функции, определенные внутри структуры, которые работают с ее членами.
"//--- Method 1": Заполнитель для метода, такого как расчет размера следующего лота восстановительной сделки или проверка достижения цели восстановления.
Благодаря объединению данных (членов) и логики (методов) структура становится более универсальной и самодостаточной. Поняв это, мы можем приступить к определению членов структуры.
//--- Struct to track individual position recovery states struct PositionRecovery { CTrade trade; //--- Object to handle trading operations. double initialLotSize; //--- Initial lot size for this position. double currentLotSize; //--- Current lot size in the recovery sequence. double zoneSize; //--- Distance in points defining the recovery zone size. double targetSize; //--- Distance in points defining the profit target range. double multiplier; //--- Lot size multiplier for recovery trades. string symbol; //--- Trading symbol. ENUM_ORDER_TYPE lastOrderType; //--- Type of the last order (BUY or SELL). double lastOrderPrice; //--- Price of the last executed order. double zoneHigh; //--- Upper boundary of the recovery zone. double zoneLow; //--- Lower boundary of the recovery zone. double zoneTargetHigh; //--- Upper boundary of the target range. double zoneTargetLow; //--- Lower boundary of the target range. bool isRecovery; //--- Whether the recovery is active. ulong tickets[]; //--- Array to store tickets of positions associated with this recovery. double trailingStop; //--- Trailing stop level double initialEntryPrice; //--- Initial entry price for trailing stop calculation //--- };
Здесь мы создаем структуру с именем PositionRecovery для организации и управления всеми данными, необходимыми для отслеживания состояний восстановления отдельных позиций. Используя эту структуру, мы обеспечиваем независимую обработку каждого процесса восстановления, что позволяет нам эффективно управлять несколькими сигналами.
Мы определяем объект класса CTrade с именем trade, который будем использовать для выполнения торговых операций, таких как открытие, изменение и закрытие ордеров, связанных с этим восстановлением. Мы настраиваем initialLotSize для хранения размера первой сделки в последовательности, а currentLotSize помогает нам отслеживать размер лота последней сделки в процессе восстановления. Для управления стратегией восстановления мы указываем расстояние зоны восстановления в пунктах с помощью zoneSize и определяем диапазон целевой прибыли с помощью targetSize.
Для управления динамическим размером лота мы включаем multiplier (множитель), который будем использовать для расчета размера лота для каждой последующей сделки восстановления. Мы добавляем symbol для идентификации торгового инструмента для этого восстановления, что обеспечивает выполнение советником сделок по нужному символу. Мы используем тип данных перечисления ENUM_ORDER_TYPE для объявления переменной lastOrderType для хранения типа последнего выполненного ордера (например, BUY или SELL) и lastOrderPrice для записи его цены исполнения, что помогает нам отслеживать текущее состояние восстановления. Для мониторинга зон восстановления мы определяем zoneHigh и zoneLow как верхнюю и нижнюю границы зоны восстановления, а zoneTargetHigh и zoneTargetLow обозначают диапазон целевой прибыли.
Чтобы определить, активна ли восстановление, мы используем isRecovery, флаг, которому присваиваем значение true или false, сообразно необходимости. Мы также включаем tickets[], массив, в котором храним номера билетов всех сделок в последовательности восстановления, что позволяет нам отслеживать и управлять ими индивидуально. Наконец, мы включаем trailingStop для указания расстояния трейлинг-стопа и initialEntryPrice для записи цены входа первой сделки, которую мы будем использовать для расчета трейлинг-стопа. Эти компоненты позволяют нам динамически защищать прибыль во время восстановления.
После определения переменных-членов нам необходимо инициализировать их в каждом созданном экземпляре, или, скорее, в каждой корзине. Для этого мы можем создать метод, который будет плавно обрабатывать инициализацию.
//--- Initialize position recovery void Initialize(double lot, double zonePts, double targetPts, double lotMultiplier, string _symbol, ENUM_ORDER_TYPE type, double price) { initialLotSize = lot; //--- Assign initial lot size. currentLotSize = lot; //--- Set current lot size equal to initial lot size. zoneSize = zonePts * _Point; //--- Calculate zone size in points. targetSize = targetPts * _Point; //--- Calculate target size in points. multiplier = lotMultiplier; //--- Assign lot size multiplier. symbol = _symbol; //--- Assign the trading symbol. lastOrderType = type; //--- Set the type of the last order. lastOrderPrice = price; //--- Record the price of the last executed order. isRecovery = false; //--- Set recovery as inactive initially. ArrayResize(tickets, 0); //--- Initialize the tickets array. trailingStop = 0; //--- Initialize trailing stop initialEntryPrice = price; //--- Set initial entry price CalculateZones(); //--- Calculate recovery and target zones. } void CalculateZones() {
Мы определяем метод Initialize, который выполняет инициализацию всех необходимых параметров, а также состояния для восстановления отдельной позиции. Этот метод гарантирует, что каждый экземпляр восстановления настроен правильно и готов динамически управлять сделками на основе предоставленных входных значений. Мы начинаем с присвоения переменной initialLotSize значения lot, которая определяет размер первой сделки в последовательности восстановления. В то же время мы присваиваем переменной currentLotSize значение initialLotSize, поскольку первая сделка использует тот же размер лота. Далее мы рассчитываем размер зоны восстановления и диапазон целевой прибыли в пунктах, используя входы zonePts и targetPts соответственно, умножая их на константу _Point, чтобы учесть значение пункта символа. Эти вычисления определяют пороговые значения расстояния для управления сделками восстановления и их целями.
Мы присваиваем lotMultiplier переменной multiplier, которая определяет, как размер лота будет увеличиваться в последующих сделках восстановления. Торговый символ присваивается symbol, чтобы гарантировать, что все сделки в этом случае восстановления будут выполняться на правильном рыночном инструменте. Мы присваиваем lastOrderType значение параметра type, а lastOrderPrice присваиваем значение price, записывая информацию о самом последнем ордере. Эти значения помогают нам отслеживать состояние текущего восстановления. Кроме того, мы инициализируем isRecovery как false, указывая, что процесс восстановления не активен при первом создании.
Мы устанавливаем размер массива tickets равным нулю с помощью функции ArrayResize, очищая все существующие данные и подготавливая его для хранения номеров билетов сделок, связанных с этим экземпляром восстановления. Для дополнительной гибкости мы присваиваем trailingStop значение 0, а переменной initialEntryPrice присваиваем значение price, обеспечивая базовую линию для расчетов трейлинг-стопа. Наконец, мы вызываем метод CalculateZones, который вычисляет верхнюю и нижнюю границы зоны восстановления и целевого диапазона. Этот шаг гарантирует, что советник имеет все необходимые данные для эффективного управления сделками. Используя метод Initialize, мы установим полную и четко определенную отправную точку для каждого процесса восстановления, гарантируя, что все соответствующие параметры установлены правильно для эффективного управления сделками. Затем мы можем приступить к определению функции CalculateZones, которая отвечает за расчет уровней диапазона восстановления.
//--- Calculate dynamic zones and targets if (lastOrderType == ORDER_TYPE_BUY) { //--- If the last order was a BUY... zoneHigh = lastOrderPrice; //--- Set upper boundary at the last order price. zoneLow = zoneHigh - zoneSize; //--- Set lower boundary below the last order price. zoneTargetHigh = zoneHigh + targetSize; //--- Define target range above recovery zone. zoneTargetLow = zoneLow - targetSize; //--- Define target range below recovery zone. } else if (lastOrderType == ORDER_TYPE_SELL) { //--- If the last order was a SELL... zoneLow = lastOrderPrice; //--- Set lower boundary at the last order price. zoneHigh = zoneLow + zoneSize; //--- Set upper boundary above the last order price. zoneTargetLow = zoneLow - targetSize; //--- Define target range below recovery zone. zoneTargetHigh = zoneHigh + targetSize; //--- Define target range above recovery zone. } }
Здесь мы определяем метод CalculateZones, который динамически рассчитывает границы зоны восстановления и целевые диапазоны прибыли на основе типа последнего исполненного ордера и его цены. Этот метод гарантирует, что каждый процесс восстановления имеет четко определенные уровни, которые служат ориентиром для последующих торговых решений, позволяя системе адекватно реагировать на движения рынка.
Мы начинаем с проверки lastOrderType, чтобы определить, был ли последний ордер BUY или SELL. Если lastOrderType равен ORDER_TYPE_BUY, мы присваиваем переменной lastOrderPrice значение zoneHigh, устанавливая верхнюю границу зоны восстановления по цене входа последнего ордера BUY. Затем нижняя граница zoneLow рассчитывается путем вычитания zoneSize (преобразованного в пункты) из zoneHigh. Кроме того, мы определяем диапазон целевой прибыли: zoneTargetHigh рассчитывается путем добавления targetSize к zoneHigh, а zoneTargetLow — путем вычитания targetSize из zoneLow. Эти вычисления гарантируют, что зона восстановления и диапазон целевой прибыли расположены относительно цены ордера BUY.
Если lastOrderType равен ORDER_TYPE_SELL, мы меняем логику на противоположную. В этом случае мы присваиваем zoneLow значение lastOrderPrice, устанавливая нижнюю границу зоны восстановления на цене входа последнего ордера SELL. Верхняя граница, zoneHigh, рассчитывается путем прибавления zoneSize к zoneLow. Для диапазона целевой прибыли мы рассчитываем zoneTargetLow путем вычитания targetSize из zoneLow, а zoneTargetHigh – путем добавления targetSize к zoneHigh. Эти границы устанавливаются относительно цены ордера SELL. Эти определения уровней отражают изображение, визуализированное ниже:

После определения уровней зоны мы можем приступить к открытию позиций. Мы инкапсулируем логику открытия позиций в методе для более удобного использования в структуре кода.
//--- Open a trade with comments for position type bool OpenTrade(ENUM_ORDER_TYPE type, string comment) { if (type == ORDER_TYPE_BUY) { //--- For a BUY order... if (trade.Buy(currentLotSize, symbol, 0, 0, 0, comment)) { //--- Attempt to place a BUY trade. lastOrderType = ORDER_TYPE_BUY; //--- Update the last order type. lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Record the current price. ArrayResize(tickets, ArraySize(tickets) + 1); //--- Resize the tickets array. tickets[ArraySize(tickets) - 1] = trade.ResultOrder(); //--- Store the new ticket. CalculateZones(); //--- Recalculate zones. isRecovery = false; //--- Ensure recovery is inactive for initial trade. Print("Opened BUY Position, Ticket: ", tickets[ArraySize(tickets) - 1]); return true; //--- Return success. } } else if (type == ORDER_TYPE_SELL) { //--- For a SELL order... if (trade.Sell(currentLotSize, symbol, 0, 0, 0, comment)) { //--- Attempt to place a SELL trade. lastOrderType = ORDER_TYPE_SELL; //--- Update the last order type. lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Record the current price. ArrayResize(tickets, ArraySize(tickets) + 1); //--- Resize the tickets array. tickets[ArraySize(tickets) - 1] = trade.ResultOrder(); //--- Store the new ticket. CalculateZones(); //--- Recalculate zones. isRecovery = false; //--- Ensure recovery is inactive for initial trade. Print("Opened SELL Position, Ticket: ", tickets[ArraySize(tickets) - 1]); return true; //--- Return success. } } return false; //--- If the trade was not placed, return false. }
В этой булевой функции OpenTrade мы обрабатываем логику открытия новой сделки указанного типа (BUY или SELL) и управляем необходимыми обновлениями системы восстановления. Эта функция обеспечивает правильное открытие сделок и обновление всех связанных данных для поддержания синхронизации с процессом восстановления. Когда параметр type имеет значение ORDER_TYPE_BUY, мы пытаемся открыть сделку BUY с помощью метода trade.Buy. Метод использует параметры currentLotSize, symbol и comment для выполнения сделки, оставляя уровни стоп-лосса и тейк-профита равными нулю (не указаны), поскольку мы определяем их динамически в соответствии с целевыми диапазонами зон. Если сделка BUY успешно размещена, мы обновляем lastOrderType до ORDER_TYPE_BUY, указывая тип последней сделки, а lastOrderPrice устанавливается на текущую рыночную цену, полученную с помощью функции SymbolInfoDouble с параметром SYMBOL_BID.
Затем мы изменяем размер массива tickets с помощью функции ArrayResize, чтобы освободить место для новой сделки, и сохраняем номер билета успешно размещенной сделки с помощью trade.ResultOrder(). Это гарантирует, что все сделки, связанные с этим экземпляром восстановления, отслеживаются и хранятся эффективно. Затем мы вызываем функцию CalculateZones, чтобы пересчитать зоны восстановления и целевые зоны на основе последней сделки. Наконец, мы присваиваем isRecovery значение false, так как это первоначальная сделка и она не является частью процесса восстановления. В лог выводится сообщение об успешном выполнении, и функция возвращает true, чтобы указать, что сделка была успешно открыта.
Если параметр type имеет значение ORDER_TYPE_SELL, мы следуем аналогичной логике. Вызывается метод trade.Sell для размещения сделки SELL с указанными параметрами. В случае успеха мы обновляем lastOrderType до ORDER_TYPE_SELL и записываем lastOrderPrice как текущую рыночную цену. Массив tickets изменяется, и новый билет сохраняется, как и в случае с ордером BUY. Зоны пересчитываются с помощью CalculateZones, а isRecovery присваивается значение false. Выводится сообщение об успешном выполнении, и функция возвращает значение true.
Если сделка не удалась для любого из типов ордеров, функция возвращает false, сигнализируя о неудачной операции. Такая структура обеспечивает систематическое управление сделками и правильное обновление всех данных, связанных с восстановлением, для бесперебойного управления сделками. После открытия позиций и расчета уровней зон мы можем продолжать управлять этими зонами при каждом тике, чтобы открывать позиции восстановления при достижении любого из уровней.
//--- Manage zone recovery void ManageZones() { double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Get the current price. if (lastOrderType == ORDER_TYPE_BUY && currentPrice <= zoneLow) { //--- If price drops below the recovery zone for a BUY... double previousLotSize = currentLotSize; //--- Store the current lot size temporarily. currentLotSize *= multiplier; //--- Tentatively increase lot size. if (OpenTrade(ORDER_TYPE_SELL, "Recovery Position")) { //--- Attempt to open a SELL recovery trade. isRecovery = true; //--- Mark recovery as active if trade is successful. } else { currentLotSize = previousLotSize; //--- Revert the lot size if the trade fails. } } else if (lastOrderType == ORDER_TYPE_SELL && currentPrice >= zoneHigh) { //--- If price rises above the recovery zone for a SELL... double previousLotSize = currentLotSize; //--- Store the current lot size temporarily. currentLotSize *= multiplier; //--- Tentatively increase lot size. if (OpenTrade(ORDER_TYPE_BUY, "Recovery Position")) { //--- Attempt to open a BUY recovery trade. isRecovery = true; //--- Mark recovery as active if trade is successful. } else { currentLotSize = previousLotSize; //--- Revert the lot size if the trade fails. } } }
Мы объявляем функцию ManageZones для мониторинга рыночной цены зон восстановления и принятия мер, если цена движется в противоположном направлении по сравнению с первоначальной сделкой. Сначала мы получаем текущую рыночную цену с помощью функции SymbolInfoDouble, чтобы получить последнюю цену предложения. Затем мы проверяем, вышла ли цена за пределы зоны восстановления, которая определяется значением zoneLow для ордера BUY и zoneHigh для ордера SELL.
Если последний ордер был BUY (обозначенный как lastOrderType == ORDER_TYPE_BUY) и текущая цена опускается ниже zoneLow, мы увеличиваем размер лота для восстановительной сделки. Мы сохраняем текущий размер лота в previousLotSize, затем умножаем currentLotSize на multiplier, чтобы увеличить его. После этого мы пытаемся открыть восстановительную сделку SELL с помощью функции OpenTrade. Если восстановительная сделка успешно размещена, мы присваиваем isRecovery значение true, чтобы отметить, что восстановление активно. Если сделка не удалась, мы возвращаем размер лота к исходному значению, сохраненному в previousLotSize.
Аналогично, если последний ордер был SELL (обозначенный как lastOrderType == ORDER_TYPE_SELL) и цена поднимается выше zoneHigh, мы применяем ту же логику для открытия восстановительной сделки BUY. Размер лота увеличился, и мы пытаемся открыть сделку BUY. В случае успеха переменной isRecovery присваивается значение true, но если сделка не удалась, размер лота возвращается к исходному значению. Это гарантирует, что система эффективно управляет сделками восстановления, корректируя размеры позиций и принимая корректирующие меры в зависимости от рыночных условий. Наконец, нам нужно закрыть позиции, когда цена достигнет заданных целевых уровней, поэтому нам понадобится функция для обработки этой логики.
//--- Check and close trades at targets void CheckCloseAtTargets() { double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Get the current price. if (lastOrderType == ORDER_TYPE_BUY && currentPrice >= zoneTargetHigh) { //--- If price reaches the target for a BUY... ClosePositionsAtTarget(); //--- Close positions that meet the target criteria. } else if (lastOrderType == ORDER_TYPE_SELL && currentPrice <= zoneTargetLow) { //--- If price reaches the target for a SELL... ClosePositionsAtTarget(); //--- Close positions that meet the target criteria. } }
Здесь мы определяем функцию CheckCloseAtTargets типа void, чтобы проверить, достигла ли рыночная цена заранее определенных целевых уровней, и закрыть позиции, которые соответствуют целевым критериям. Сначала мы получаем текущую рыночную цену предложения с помощью SymbolInfoDouble (symbol, SYMBOL_BID). Затем мы сравниваем эту цену с целевыми уровнями, определенными zoneTargetHigh для ордера на ПОКУПКУ и zoneTargetLow для ордера на ПРОДАЖУ.
Если последняя сделка была BUY (обозначена как lastOrderType == ORDER_TYPE_BUY) и текущая цена поднимается до или выше zoneTargetHigh, мы считаем, что позиция достигла желаемой цели по прибыли. В этом случае мы вызываем функцию ClosePositionsAtTarget, чтобы закрыть все позиции, которые соответствуют целевым критериям. Аналогично, если последний ордер был SELL (обозначенный как lastOrderType == ORDER_TYPE_SELL) и цена упала до или ниже zoneTargetLow, система снова вызывает ClosePositionsAtTarget для закрытия позиций. Эта функция гарантирует, что сделки будут закрыты, когда рынок достигнет заданной цели по прибыли, фиксируя прибыль и завершая процесс восстановления.
Для закрытия позиций мы использовали функцию ClosePositionsAtTarget, чтобы можно было повторно использовать ее. Вот фрагмент кода этой функции.
//--- Close positions that have reached the target void ClosePositionsAtTarget() { for (int i = ArraySize(tickets) - 1; i >= 0; i--) { //--- Iterate through all tickets. ulong ticket = tickets[i]; //--- Get the position ticket. int retries = 10; //--- Set retry count. while (retries > 0) { //--- Retry until successful or retries exhausted. if (trade.PositionClose(ticket)) { //--- Attempt to close the position. Print("CLOSED # ", ticket, " Trailed and closed: ", (trailingStop != 0)); ArrayRemove(tickets, i); //--- Remove the ticket from the array on success. retries = 0; //--- Exit the loop on success. } else { retries--; //--- Decrement retries on failure. Sleep(100); //--- Wait before retrying. } } } if (ArraySize(tickets) == 0) { //--- If all tickets are closed... Reset(); //--- Reset recovery state after closing the target positions. } }
В функции ClosePositionsAtTarget мы просматриваем все открытые позиции, хранящиеся в массиве tickets, и пытаемся закрыть те, которые достигли целевых уровней. Мы начинаем с цикла по массиву tickets в обратном порядке, чтобы не пропустить ни одной позиции при их удалении после закрытия. Для каждого билета мы устанавливаем количество повторных попыток retries, чтобы система повторила попытку, если позиция не закрылась с первой попытки.
Для каждой позиции мы пытаемся закрыть ее с помощью функции trade.PositionClose(ticket). Если позиция успешно закрыта, мы выводим сообщение о том, что билет был закрыт, и был ли он трейлинг-стоп или нет, используя trailingStop != 0 для проверки, был ли применен трейлинг-стоп. После закрытия позиции мы удаляем билет из массива tickets с помощью функции ArrayRemove и выходим из цикла повторных попыток, присваивая переменной retries значение 0. Если позиция не закрывается, мы уменьшаем счетчик retries, ждем некоторое время с помощью функции Sleep, а затем снова пытаемся закрыть позицию, следя за тем, чтобы не перегрузить функцию.
После попытки закрыть все позиции мы проверяем, пуст ли массив tickets, используя функцию ArraySize. Если все позиции закрыты, мы вызываем функцию Reset, чтобы сбросить состояние восстановления, очистив все оставшиеся данные, связанные с восстановлением, и подготовившись к будущим сделкам. Это все. Однако, поскольку мы не можем быть полностью уверены, что рынок достигнет наших целевых уровней, мы можем улучшить систему, отслеживая позиции, которые достигли нашей минимальной прибыли, вместо того, чтобы ждать, пока уровни будут достигнуты. Эта логика снова реализована в методе.
//--- Apply trailing stop logic to initial positions void ApplyTrailingStop() { if (inputTrailingStopEnabled && ArraySize(tickets) == 1) { // Ensure trailing stop is enabled and there is only one position (initial position) ulong ticket = tickets[0]; // Get the ticket of the initial position double entryPrice = GetPositionEntryPrice(ticket); // Get the entry price of the position by ticket double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); // Get the current price double newTrailingStop; if (lastOrderType == ORDER_TYPE_BUY) { if (currentPrice > entryPrice + (inputMinimumProfitPts + inputTrailingStopPts) * _Point) { newTrailingStop = currentPrice - inputTrailingStopPts * _Point; // Calculate new trailing stop for BUY if (newTrailingStop > trailingStop) { trailingStop = newTrailingStop; // Update trailing stop if the new one is higher Print("Trailing BUY Position, Ticket: ", ticket, " New Trailing Stop: ", trailingStop); } } if (trailingStop != 0 && currentPrice <= trailingStop) { Print("Trailed and closing BUY Position, Ticket: ", ticket); ClosePositionsAtTarget(); // Close position if the price falls below the trailing stop } } else if (lastOrderType == ORDER_TYPE_SELL) { if (currentPrice < entryPrice - (inputMinimumProfitPts + inputTrailingStopPts) * _Point) { newTrailingStop = currentPrice + inputTrailingStopPts * _Point; // Calculate new trailing stop for SELL if (newTrailingStop < trailingStop) { trailingStop = newTrailingStop; // Update trailing stop if the new one is lower Print("Trailing SELL Position, Ticket: ", ticket, " New Trailing Stop: ", trailingStop); } } if (trailingStop != 0 && currentPrice >= trailingStop) { Print("Trailed and closing SELL Position, Ticket: ", ticket); ClosePositionsAtTarget(); // Close position if the price rises above the trailing stop } } } }
В методе void ApplyTrailingStop, который мы определяем, мы реализуем логику трейлинг-стопа для начальной позиции в зависимости от того, включены ли трейлинг-стопы и есть ли только одна активная позиция. Сначала мы проверяем, включена ли функция трейлинг-стопа с помощью inputTrailingStopEnabled и есть ли только одна открытая позиция (что обеспечивается ArraySize(tickets) == 1). Затем мы извлекаем билет начальной позиции и используем его для получения цены входа через функцию GetPositionEntryPrice. Мы также извлекаем текущую рыночную цену с помощью функции SymbolInfoDouble.
Для позиции BUY мы проверяем, превысила ли текущая цена цену входа на определенную величину (учитывая как минимальную прибыль, так и расстояние трейлинг-стопа, рассчитанное с помощью inputMinimumProfitPts + inputTrailingStopPts), и соответствующим образом устанавливаем новый трейлинг-стоп. Если рассчитанный трейлинг-стоп выше текущего trailingStop, мы обновляем значение трейлинг-стопа и выводим сообщение с указанием нового уровня трейлинг-стопа. Если текущая цена опускается до уровня трейлинг-стопа или ниже, мы закрываем позицию с помощью функции ClosePositionsAtTarget.
Для позиции SELL мы следуем аналогичному процессу, но в обратном порядке. Мы проверяем, находится ли текущая цена ниже цены входа на определенную величину, и при необходимости устанавливаем трейлинг-стоп ниже. Если рассчитанный трейлинг-стоп ниже текущего trailingStop, мы обновляем трейлинг-стоп и выводим сообщение с указанием нового уровня. Если текущая цена поднимается до уровня трейлинг-стопа или выше, позиция закрывается. Эта функция обеспечивает динамическое применение трейлинг-стопа в зависимости от рыночных условий, что позволяет зафиксировать прибыль и одновременно защитить позицию от значительных убытков. Если цена движется в благоприятном направлении, трейлинг-стоп корректируется; если цена разворачивается и достигает уровня трейлинг-стопа, позиция закрывается.
Вы, возможно, заметили, что мы используем пользовательскую функцию для получения цен входа. Вот логика этой функции.
//--- Get the entry price of a position by ticket double GetPositionEntryPrice(ulong ticket) { if (PositionSelectByTicket(ticket)) { return PositionGetDouble(POSITION_PRICE_OPEN); } else { Print("Failed to select position by ticket: ", ticket); return 0.0; } }
Здесь мы определяем функцию GetPositionEntryPrice, которая извлекает цену входа в позицию по предоставленному номеру билета. Сначала мы пытаемся выбрать позицию, связанную с данным билетом, с помощью функции PositionSelectByTicket. Если позиция выбрана успешно, мы извлекаем цену входа в позицию, вызывая PositionGetDouble (POSITION_PRICE_OPEN), которая дает нам цену, по которой была открыта позиция. Если позицию не удается выбрать (например, если билет недействителен или позиция больше не существует), мы выводим сообщение об ошибке, указывающее на сбой, и возвращаем значение 0.0, чтобы обозначить, что цена входа не может быть получена.
Теперь, после открытия и закрытия позиций, нам необходимо сбросить систему и удалить связанную торговую корзину в качестве метода очистки. Вот как мы обрабатываем эту логику очистки в функции Reset.
//--- Reset recovery state void Reset() { currentLotSize = inputlot; //--- Reset lot size to initial value. lastOrderType = -1; //--- Clear the last order type. lastOrderPrice = 0.0; //--- Reset the last order price. isRecovery = false; //--- Mark recovery as inactive. ArrayResize(tickets, 0); //--- Clear the tickets array. trailingStop = 0; //--- Reset trailing stop initialEntryPrice = 0.0; //--- Reset initial entry price Print("Strategy BASKET reset after closing trades."); }
В функции Reset мы сбрасываем состояние восстановления, чтобы подготовиться к новому торговому циклу. Сначала мы возвращаем currentLotSize к начальному значению, определенному inputlot, обеспечивая сброс размера лота к начальному значению, заданному пользователем. Мы также очищаем информацию о последнем ордере, присваиваем lastOrderType значение -1 (что указывает на отсутствие активного типа ордера) и сбрасываем lastOrderPrice на 0.0, эффективно удаляя любую предыдущую информацию о цене ордера.
Затем мы помечаем восстановление как неактивное, присваивая isRecovery значение false, что гарантирует, что при сбросе не будет применяться логика восстановления. Затем мы очищаем массив tickets с помощью функции ArrayResize, удаляя все сохраненные билеты позиций, которые были частью предыдущего процесса восстановления. Кроме того, мы сбрасываем trailingStop до 0 и initialEntryPrice до 0.0, очищая все настройки трейлинг-стопа и значения цены входа из предыдущих сделок. Наконец, мы выводим сообщение Strategy BASKET reset after closing trades» (Стратегия BASKET сброшена после закрытия сделок), чтобы уведомить о завершении сброса и очистке состояния восстановления. Эта функция гарантирует, что система находится в чистом состоянии и готова к следующему торговому циклу.
После определения свойств структуры мы можем генерировать сигналы и добавлять их в определенную структуру. Однако, поскольку нам потребуется управлять множеством динамических сигналов, нам нужно будет определить структуру массива, которая будет действовать как целая корзина, в которой мы будем определять подкорзины для каждого сгенерированного сигнала. Вот как мы этого добиваемся.
//--- Dynamic list to track multiple positions PositionRecovery recoveryArray[]; //--- Dynamic array for recovery instances.
Здесь мы объявляем динамический массив с именем recoveryArray, который предназначен для отслеживания и управления несколькими экземплярами восстановления позиций. Массив основан на структуре PositionRecovery, что позволяет ему независимо хранить отдельные состояния восстановления для нескольких сделок. Каждый элемент массива представляет собой отдельную настройку восстановления, включая все соответствующие атрибуты, такие как размер лота, границы зоны и связанные торговые билеты.
Сделав массив динамическим, мы можем расширять или сокращать его по мере необходимости во время выполнения с помощью таких функций, как ArrayResize. Это позволяет нам динамически добавлять новые экземпляры восстановления для новых торговых сигналов или удалять завершенные восстановления, обеспечивая эффективное использование памяти и адаптируемость к различным торговым сценариям. Такой подход необходим для одновременного управления несколькими сделками, поскольку он позволяет логике восстановления каждой сделки работать независимо в рамках своей собственной "корзины" данных.
После определения массива мы можем приступить к логике генерации сигналов. Нам нужно инициализировать дескриптор индикатора в обработчике событий OnInit, который вызывается при каждой инициализации программы.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { rsiHandle = iRSI(_Symbol, PERIOD_CURRENT, rsiPeriod, PRICE_CLOSE); //--- Create RSI indicator handle. if (rsiHandle == INVALID_HANDLE) { //--- Check if handle creation failed. Print("Failed to create RSI handle. Error: ", GetLastError()); //--- Print error message. return(INIT_FAILED); //--- Return initialization failure. } ArraySetAsSeries(rsiBuffer, true); //--- Set RSI buffer as a time series. Print("Multi-Zone Recovery Strategy initialized."); //--- Log initialization success. return(INIT_SUCCEEDED); //--- Return initialization success. }
В функции обработчика событий OnInit мы инициализируем основные компоненты, необходимые для работы советника. Мы начинаем с создания дескриптора индикатора RSI с помощью функции iRSI, которая вычисляет индекс относительной силы для текущего символа и периода. Этот дескриптор позволяет советнику динамически получать доступ к значениям RSI. Если создание дескриптора завершилось неудачей, о чем свидетельствует значение INVALID_HANDLE, мы регистрируем сообщение об ошибке с подробностями с помощью функции Print и возвращаем INIT_FAILED, чтобы прервать процесс инициализации. Затем мы настраиваем массив rsiBuffer как временную серию с помощью ArraySetAsSeries, обеспечивая хронологическую организацию данных для точной обработки. После успешной инициализации мы выводим подтверждающее сообщение, указывающее, что стратегия готова, и возвращаем INIT_SUCCEEDED, чтобы сигнализировать о готовности советника к работе. Затем в обработчике событий OnDeinit мы уничтожаем дескриптор, чтобы сэкономить ресурсы.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if (rsiHandle != INVALID_HANDLE) //--- Check if RSI handle is valid. IndicatorRelease(rsiHandle); //--- Release the RSI handle. Print("Multi-Zone Recovery Strategy deinitialized."); //--- Log deinitialization. }
Здесь мы обрабатываем очистку и управление ресурсами для советника при его удалении или деактивации. Сначала мы проверяем, является ли rsiHandle для индикатора RSI действительным, убедившись, что он не равен INVALID_HANDLE. Если дескриптор действителен, мы освобождаем его с помощью функции IndicatorRelease, чтобы освободить ресурсы и избежать утечки памяти. Наконец, мы регистрируем сообщение с помощью Print, чтобы указать, что стратегия восстановления Multi-Zone Recovery Strategy была успешно деинициализирована. Эта функция обеспечивает чистое и упорядоченное завершение работы программы, не оставляя никаких остаточных ресурсов или процессов.
После этого мы можем перейти к обработчику событий OnTick, который будет обрабатывать всю основную логику системы, используя ранее определенную структуру. Сначала нам нужно получить данные индикатора, чтобы использовать их для дальнейшего анализа.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { if (CopyBuffer(rsiHandle, 0, 1, 2, rsiBuffer) <= 0) { //--- Copy the RSI buffer values. Print("Failed to copy RSI buffer. Error: ", GetLastError()); //--- Print error on failure. return; //--- Exit on failure. } //--- }
Здесь, в функции OnTick, мы обрабатываем логику, которая выполняется при каждом новом тике, представляющем обновление цены для торгового символа. Первый шаг включает копирование значений индикатора RSI в массив rsiBuffer с помощью функции CopyBuffer. Мы указываем rsiHandle для идентификации индикатора RSI, устанавливаем индекс буфера равным 0 и запрашиваем два значения, начиная с самого последнего бара. Если операция завершается с ошибкой (т. е. возвращаемое значение меньше или равно 0), мы выводим сообщение об ошибке с помощью Print, чтобы уведомить пользователя о проблеме, и включаем подробности ошибки, полученные с помощью функции GetLastError. После регистрации ошибки мы немедленно выходим из функции с помощью return. Это гарантирует, что остальная часть логики не будет выполняться, если извлечение данных RSI завершится с ошибкой, что обеспечивает целостность и стабильность советника.
Если мы успешно извлекаем данные, мы можем использовать их для логики генерации сигналов следующим образом.
datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); //--- Get the time of the current bar. if (currentBarTime != lastBarTime) { //--- Check if a new bar has formed. lastBarTime = currentBarTime; //--- Update the last processed bar time. if (rsiBuffer[1] > 30 && rsiBuffer[0] <= 30) { //--- Check for oversold RSI crossing up. Print("BUY SIGNAL"); PositionRecovery newRecovery; //--- Create a new recovery instance. newRecovery.Initialize(inputlot, inputzonesizepts, inputzonetragetpts, inputlotmultiplier, _Symbol, ORDER_TYPE_BUY, SymbolInfoDouble(_Symbol, SYMBOL_BID)); //--- Initialize the recovery. newRecovery.OpenTrade(ORDER_TYPE_BUY, "Initial Position"); //--- Open an initial BUY position. ArrayResize(recoveryArray, ArraySize(recoveryArray) + 1); //--- Resize the recovery array. recoveryArray[ArraySize(recoveryArray) - 1] = newRecovery; //--- Add the new recovery to the array. } else if (rsiBuffer[1] < 70 && rsiBuffer[0] >= 70) { //--- Check for overbought RSI crossing down. Print("SELL SIGNAL"); PositionRecovery newRecovery; //--- Create a new recovery instance. newRecovery.Initialize(inputlot, inputzonesizepts, inputzonetragetpts, inputlotmultiplier, _Symbol, ORDER_TYPE_SELL, SymbolInfoDouble(_Symbol, SYMBOL_BID)); //--- Initialize the recovery. newRecovery.OpenTrade(ORDER_TYPE_SELL, "Initial Position"); //--- Open an initial SELL position. ArrayResize(recoveryArray, ArraySize(recoveryArray) + 1); //--- Resize the recovery array. recoveryArray[ArraySize(recoveryArray) - 1] = newRecovery; //--- Add the new recovery to the array. } }
Здесь мы сосредоточимся на обнаружении новых баров и генерации торговых сигналов на основе пересечения индикатором определенных уровней. Сначала мы извлекаем время текущего бара с помощью функции iTime, которое хранится в переменной currentBarTime. Затем мы сравниваем currentBarTime и lastBarTime, чтобы проверить, сформировался ли новый бар. Если два значения различаются, это означает, что сформировался новый бар, поэтому мы обновляем lastBarTime до значения currentBarTime, чтобы предотвратить многократную обработку одного и того же бара.
Далее мы оцениваем условия для сигналов на основе RSI. Если значение RSI в rsiBuffer[1] (предыдущий бар) больше 30, а текущее значение в rsiBuffer[0] (текущий бар) меньше или равно 30, это означает состояние перепроданности с пересечением вверх. В этом случае мы выводим сообщение "BUY SIGNAL" и инициируем новый экземпляр PositionRecovery с именем newRecovery. Затем мы вызываем метод Initialize для newRecovery, чтобы настроить параметры восстановления, включая inputlot, inputzonesizepts, inputzonetragetpts, inputlotmultiplier, символ, тип ордера как ORDER_TYPE_BUY и текущую цену предложения из функции SymbolInfoDouble. После инициализации мы открываем начальную позицию BUY с помощью метода OpenTrade, передавая ORDER_TYPE_BUY и описательный комментарий.
Аналогично, если значение RSI в rsiBuffer[1] меньше 70, а текущее значение в rsiBuffer[0] больше или равно 70, это указывает на состояние перекупленности с пересечением вниз. В этом сценарии мы выводим сообщение "SELL SIGNAL" и создаем новый экземпляр PositionRecovery. После инициализации с теми же параметрами, но с установкой типа ордера ORDER_TYPE_SELL, мы открываем начальную позицию SELL с помощью метода OpenTrade.
Наконец, как для сигналов BUY, так и для сигналов SELL мы добавляем инициализированный экземпляр PositionRecovery в recoveryArray. Размер массива изменяется с помощью функции ArrayResize, и новый экземпляр назначается последней позиции массива, что обеспечивает независимое отслеживание каждого восстановления. Эта логика теперь отвечает за инициирование корзин позиций с начальными позициями и условиями. Для управления позициями нам нужно будет пройти по корзинам в основной корзине и применять логику управления, как в основной структуре, при каждом тике. Вот эта логика.
for (int i = 0; i < ArraySize(recoveryArray); i++) { //--- Iterate through all recovery instances. recoveryArray[i].ManageZones(); //--- Manage zones for each recovery instance. recoveryArray[i].CheckCloseAtTargets(); //--- Check and close positions at targets. recoveryArray[i].ApplyTrailingStop(); //--- Apply trailing stop logic to initial positions. }
Для независимого управления позициями мы используем цикл for для итерации по всем экземплярам восстановления, хранящимся в recoveryArray. Этот цикл гарантирует, что каждый экземпляр восстановления управляется отдельно, что позволяет системе поддерживать независимый контроль над несколькими сценариями восстановления. Цикл начинается с индекса i, установленного на 0, и продолжается до тех пор, пока все элементы в recoveryArray не будут обработаны, как определено функцией ArraySize.
Внутри цикла для каждого экземпляра восстановления вызываются три основных метода. Сначала с помощью оператора точка вызывается метод ManageZones, который отслеживает движения цены относительно определенных зон восстановления. Если цена выходит за пределы зоны, этот метод предпринимает действия, пытаясь открыть позицию восстановления, динамически корректируя размер лота в соответствии с указанным множителем.
Затем выполняется метод CheckCloseAtTargets для оценки того, достигла ли цена целевых уровней для экземпляра восстановления. Если целевые условия выполнены, этот метод закрывает все связанные позиции и сбрасывает экземпляр восстановления, обеспечивая фиксацию прибыли и готовность экземпляра к новому циклу.
Наконец, применяется метод ApplyTrailingStop, который обеспечивает логику трейлинг-стопа для начальной позиции экземпляра восстановления. Этот метод динамически корректирует уровень трейлинг-стопа по мере благоприятного движения цены, фиксируя прибыль. Если цена разворачивается и достигает трейлинг-стопа, метод обеспечивает закрытие позиции, защищая от потенциальных убытков.
Обрабатывая каждый экземпляр восстановления таким образом, система эффективно управляет несколькими независимыми позициями, обеспечивая динамическую обработку всех сценариев восстановления в соответствии с заранее определенными стратегиями. Чтобы убедиться, что программа работает правильно, мы запускаем ее, и вот результат.

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

На изображении видно, что все еще существуют экземпляры восстановления, и два из них уже находятся в режиме восстановления. Их легко отличить по комментариям, добавленным к ним в правой части, которые указывают, являются ли они начальными или восстановительными позициями. Это подтверждает, что мы успешно достигли нашей цели, и осталось только провести тестирование программы на исторических данных и проанализировать ее производительность. Это будет рассмотрено в следующем разделе.
Тестирование на исторических данных
Чтобы оценить эффективность и надежность программы, мы должны сначала смоделировать исторические рыночные условия. Таким образом, мы сможем определить, насколько хорошо программа справляется со сценариями восстановления, адаптируется к движениям цен и управляет сделками. Тестирование на исторических данных дает нам важную информацию о прибыльности стратегии, уровнях просадки и управлении рисками. Программа генерирует сигналы на покупку и продажу на основе установленных пороговых значений (например, перепроданность при 30 и перекупленность при 70; в соответствии с предпочтениями пользователя), обрабатывая исторические ценовые данные тик за тиком, чтобы воспроизвести реальные рыночные условия. Когда генерируется сигнал, советник инициализирует новый экземпляр восстановления, выполняет первую сделку и отслеживает движения цен в пределах обозначенных зон восстановления.
Мы тщательно тестируем динамический механизм восстановления системы, который корректирует размеры лотов с помощью множителя и открывает хеджирующие позиции при необходимости, в различных рыночных условиях. Программа оценивает сценарии восстановления независимо для каждого сигнала, обеспечивая изолированное управление всеми сделками, что отражается в обработке recoveryArray. Это гарантирует, что даже при наличии нескольких активных экземпляров восстановления стратегия остается организованной и адаптируемой. Мы тестировали программу в течение предыдущих 5 месяцев, используя следующие настройки:

По завершении мы получили следующие результаты:
График тестера стратегий:

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

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

На изображении видно, что при включении функции трейлинг-стопа количество сделок уменьшается, а процент выигрышей увеличивается. Мы можем еще больше ограничить количество позиций, включив логику ограничения количества сделок, при которой, если уже есть несколько открытых позиций, не следует рассматривать возможность открытия дополнительных ордеров на основе сгенерированных сигналов. Для этого мы определяем дополнительные входные переменные следующим образом:
input bool inputEnablePositionsRestriction = true; // Enable Maximum positions restriction input int inputMaximumPositions = 11; // Maximum number of positions
Эти входные переменные содержат флаг для включения или отключения опции ограничения, а вторая содержит максимальное количество позиций, которые могут быть открыты в системе при включенном ограничении. Затем мы применяем логику в обработчике событий OnTick при подтверждении сигнала, добавляя дополнительный уровень ограничения торговли.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { if (CopyBuffer(rsiHandle, 0, 1, 2, rsiBuffer) <= 0) { //--- Copy the RSI buffer values. Print("Failed to copy RSI buffer. Error: ", GetLastError()); //--- Print error on failure. return; //--- Exit on failure. } datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); //--- Get the time of the current bar. if (currentBarTime != lastBarTime) { //--- Check if a new bar has formed. lastBarTime = currentBarTime; //--- Update the last processed bar time. if (rsiBuffer[1] > 30 && rsiBuffer[0] <= 30) { //--- Check for oversold RSI crossing up. Print("BUY SIGNAL"); if (inputEnablePositionsRestriction == false || inputMaximumPositions > PositionsTotal()){ PositionRecovery newRecovery; //--- Create a new recovery instance. newRecovery.Initialize(inputlot, inputzonesizepts, inputzonetragetpts, inputlotmultiplier, _Symbol, ORDER_TYPE_BUY, SymbolInfoDouble(_Symbol, SYMBOL_BID)); //--- Initialize the recovery. newRecovery.OpenTrade(ORDER_TYPE_BUY, "Initial Position"); //--- Open an initial BUY position. ArrayResize(recoveryArray, ArraySize(recoveryArray) + 1); //--- Resize the recovery array. recoveryArray[ArraySize(recoveryArray) - 1] = newRecovery; //--- Add the new recovery to the array. } else { Print("FAILED: Maximum positions threshold hit!"); } } else if (rsiBuffer[1] < 70 && rsiBuffer[0] >= 70) { //--- Check for overbought RSI crossing down. Print("SELL SIGNAL"); if (inputEnablePositionsRestriction == false || inputMaximumPositions > PositionsTotal()){ PositionRecovery newRecovery; //--- Create a new recovery instance. newRecovery.Initialize(inputlot, inputzonesizepts, inputzonetragetpts, inputlotmultiplier, _Symbol, ORDER_TYPE_SELL, SymbolInfoDouble(_Symbol, SYMBOL_BID)); //--- Initialize the recovery. newRecovery.OpenTrade(ORDER_TYPE_SELL, "Initial Position"); //--- Open an initial SELL position. ArrayResize(recoveryArray, ArraySize(recoveryArray) + 1); //--- Resize the recovery array. recoveryArray[ArraySize(recoveryArray) - 1] = newRecovery; //--- Add the new recovery to the array. } else { Print("FAILED: Maximum positions threshold hit!"); } } } for (int i = 0; i < ArraySize(recoveryArray); i++) { //--- Iterate through all recovery instances. recoveryArray[i].ManageZones(); //--- Manage zones for each recovery instance. recoveryArray[i].CheckCloseAtTargets(); //--- Check and close positions at targets. recoveryArray[i].ApplyTrailingStop(); //--- Apply trailing stop logic to initial positions. } }
Здесь мы реализуем механизм управления ограничениями позиций и контроля количества сделок, которые советник может открыть в любой момент времени. Логика начинается с оценки того, отключены ли ограничения позиций (inputEnablePositionsRestriction == false) или общее количество открытых в данный момент позиций (PositionsTotal) ниже максимального значения, заданного пользователем (inputMaximumPositions). Если выполняется любое из этих условий, советник приступает к открытию новой сделки, обеспечивая ее соответствие предпочтениям пользователя в отношении неограниченной или ограниченной торговли.
Однако, если оба условия не выполняются, что указывает на то, что ограничения по позициям включены и максимально допустимое количество позиций достигнуто, советник не открывает новую сделку. Вместо этого он регистрирует сообщение об ошибке в терминале: "FAILED: Maximum positions threshold hit!". Это сообщение служит информативным механизмом обратной связи, помогая пользователю понять, почему дополнительные сделки не были выполнены. Для наглядности мы выделили изменения светло-желтым цветом. После тестирования мы получаем следующие результаты.

На изображении видно, что количество сделок еще больше сокращается, а процент выигрышных сделок еще больше увеличивается. Это подтверждает, что мы достигли нашей цели по созданию многозонной системы восстановления. В визуализации в формате GIF (Graphic Interchange Format) мы имеем следующую симуляцию, подтверждающую достижение нашей цели.

Заключение
В заключение, в этой статье был описан процесс создания надежного советника MQL5 на основе многоуровневой стратегии Zone Recovery. Используя такие основные концепции, как автоматическое обнаружение сигналов, динамическое управление восстановлением и механизмы обеспечения прибыли, такие как трейлинг-стопы, мы создали гибкую систему, способную обрабатывать несколько независимых случаев восстановления. Ключевыми компонентами этой реализации являются генерация торговых сигналов, логика ограничения позиций и эффективная обработка стратегий восстановления и выхода.
Отказ от ответственности: эта статья предназначена в качестве образовательного ресурса по программированию на MQL5. Хотя представленная многоуровневая система Zone Recovery обеспечивает структурированную основу для управления сделками, поведение рынка по своей природе неопределенно. Торговля сопряжена с финансовым риском, и исторический успех не гарантирует будущих результатов. Перед внедрением любой стратегии на реальных рынках необходимо провести всестороннее тестирование и обеспечить эффективное управление рисками.
Следуя методологиям, описанным в этом руководстве, вы можете расширить свои знания в области алгоритмической торговли и применить эти принципы для создания еще более сложных торговых систем. Удачного программирования и успехов в торговле!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17001
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Нейросети в трейдинге: Спайково-семантический подход к пространственно-временной идентификации (S3CE-Net)
Автоматизация торговых стратегий на MQL5 (Часть 12): Реализация стратегии смягчения ордер-блоков (MOB)
От начального до среднего уровня: Индикатор (III)
Создание самооптимизирующихся советников на MQL5 (Часть 5): Самоадаптирующиеся торговые правила
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования