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

Мы добьемся этих улучшений за счет интеграции механизма трейлинг-стопа, который динамически корректирует уровень стоп-лосса в зависимости от движения рынка, обеспечивая фиксацию прибыли и предоставляя сделкам пространство для роста. Для торговли несколькими корзинами мы внедрим систему, обрабатывающую множество экземпляров сделок, каждый из которых будет иметь свой уникальный идентификатор, что позволит нам отслеживать и управлять несколькими циклами восстановления зон одновременно без наложения. Мы планируем объединить эти функции с существующим индикатором относительной силы (RSI) и конвертами для точного определения точек входа в сделку, а трейлинг-стопы и система корзин работают вместе для оптимизации защиты прибыли и торговых возможностей, что делает стратегию более надежной и адаптируемой к различным рыночным условиям. Воплотим все эти улучшения в жизнь!
Реализация средствами MQL5
Для реализации улучшений в MQL5 добавим несколько дополнительных пользовательских входных данных для функции трейлинг-стопа и переименуем максимальный лимит ордера, поскольку теперь мы имеем дело с несколькими экземплярами восстановления.
input group "======= EA GENERAL SETTINGS =======" input TradingLotSizeOptions lotOption = UNFIXED_LOTSIZE; // Lot Size Option input double initialLotSize = 0.01; // Initial Lot Size input double riskPercentage = 1.0; // Risk Percentage (%) input int riskPoints = 300; // Risk Points input int baseMagicNumber = 123456789; // Base Magic Number input int maxInitialPositions = 1; // Maximum Initial Positions (Baskets/Signals) input double zoneTargetPoints = 600; // Zone Target Points input double zoneSizePoints = 300; // Zone Size Points input bool enableInitialTrailing = true; // Enable Trailing Stop for Initial Positions input int trailingStopPoints = 50; // Trailing Stop Points input int minProfitPoints = 50; // Minimum Profit Points to Start Trailing
Начнем усовершенствование нашей системы зонального восстановления для трендовой торговли на основе конвертов в MQL5, обновляя входные параметры в группе EA GENERAL SETTINGS (общие настройки советника) для поддержки трейлинг-стопов и торговли несколькими корзинами. Внесем четыре ключевых изменения во входные данные. Во-первых, переименуем magicNumber в baseMagicNumber, установим его в 123456789, чтобы использовать в качестве отправной точки для генерации уникальных "магических" чисел для нескольких торговых корзин, обеспечивая отслеживание каждой корзины отдельно в нашей системе. Во-вторых, заменяем maxOrders на maxInitialPositions, устанавливая значение равным 1, чтобы ограничить количество начальных торговых корзин, что позволит нам эффективно управлять несколькими торговыми сигналами.
В-третьих, мы добавляем параметр enableInitialTrailing, логическую переменную со значением true, которая позволяет включать или отключать трейлинг-стопы для начальных позиций, обеспечивая контроль над нашей новой функцией фиксации прибыли. В-четвертых, устанавливаем trailingStopPoints на 50, а minProfitPoints - на 50. Переменные определяют расстояние трейлинг-стопа и минимальную прибыль, необходимую для его активации, соответственно, для реализации динамической защиты прибыли. Эти изменения позволят нашей системе обрабатывать несколько корзин сделок и эффективно защищать прибыль, создавая основу для дальнейших усовершенствований. Мы будем выделять изменения, чтобы упростить их отслеживание. После компиляции получим следующий набор входных данных.

После добавления входных параметров мы можем объявить класс MarketZoneTrader, чтобы к нему мог получить доступ базовый класс, поскольку теперь нам нужно обрабатывать несколько экземпляров сделок.
//--- Forward Declaration of MarketZoneTrader class MarketZoneTrader;
Здесь мы вводим объявление класса MarketZoneTrader. Добавим его перед определением класса BasketManager, который мы определим сразу после этого класса, чтобы он мог ссылаться на MarketZoneTrader без необходимости сразу задавать полное определение класса. Это изменение необходимо, поскольку нашей новой системе из нескольких корзин, управляемой BasketManager, потребуется создавать и обрабатывать несколько экземпляров MarketZoneTrader для разных корзин сделок. Объявив класс MarketZoneTrader первым, мы гарантируем, что компилятор распознает его при использовании в новом классе, что позволит нашей системе эффективно поддерживать несколько одновременных торговых циклов. Затем мы можем определить управляющий класс.
//--- Basket Manager Class to Handle Multiple Traders class BasketManager { private: MarketZoneTrader* m_traders[]; //--- Array of trader instances int m_handleRsi; //--- RSI indicator handle int m_handleEnvUpper; //--- Upper Envelopes handle int m_handleEnvLower; //--- Lower Envelopes handle double m_rsiBuffer[]; //--- RSI data buffer double m_envUpperBandBuffer[]; //--- Upper Envelopes buffer double m_envLowerBandBuffer[]; //--- Lower Envelopes buffer string m_symbol; //--- Trading symbol int m_baseMagicNumber; //--- Base magic number int m_maxInitialPositions; //--- Maximum baskets (signals) //--- Initialize Indicators bool initializeIndicators() { m_handleRsi = iRSI(m_symbol, PERIOD_CURRENT, 8, PRICE_CLOSE); if (m_handleRsi == INVALID_HANDLE) { Print("Failed to initialize RSI indicator"); return false; } m_handleEnvUpper = iEnvelopes(m_symbol, PERIOD_CURRENT, 150, 0, MODE_SMA, PRICE_CLOSE, 0.1); if (m_handleEnvUpper == INVALID_HANDLE) { Print("Failed to initialize upper Envelopes indicator"); return false; } m_handleEnvLower = iEnvelopes(m_symbol, PERIOD_CURRENT, 95, 0, MODE_SMA, PRICE_CLOSE, 1.4); if (m_handleEnvLower == INVALID_HANDLE) { Print("Failed to initialize lower Envelopes indicator"); return false; } ArraySetAsSeries(m_rsiBuffer, true); ArraySetAsSeries(m_envUpperBandBuffer, true); ArraySetAsSeries(m_envLowerBandBuffer, true); return true; } }
Чтобы упростить управление сделками в составе корзины, определим класс BasketManager с приватными членами для управления несколькими экземплярами класса MarketZoneTrader и данными индикатора. Создаем m_traders, массив указателей MarketZoneTrader, для хранения отдельных торговых корзин, каждая из которых представляет собой отдельный цикл зонального восстановления. Это изменение имеет решающее значение, поскольку позволяет нам обрабатывать несколько торговых сигналов одновременно, в отличие от подхода с одним экземпляром в предыдущей версии. Также объявим массивы m_handleRsi, m_handleEnvUpper и m_handleEnvLower для хранения хэндлов индикаторов, а также массивы m_rsiBuffer, m_envUpperBandBuffer и m_envLowerBandBuffer для хранения данных RSI и конвертов, перенося управление индикаторами из MarketZoneTrader в BasketManager для централизованного управления всеми корзинами.
Также добавим параметр m_symbol для хранения торгового символа, m_baseMagicNumber для генерации уникальных магических чисел для каждой корзины и m_maxInitialPositions для ограничения количества активных корзин, что соответствует новому входному параметру maxInitialPositions. В функции initializeIndicators настроим индикатор RSI с iRSI, используя 8-периодный режим, и конверты с iEnvelopes (150-периодные с отклонением 0.1 и 95-периодный с отклонением 1.4), проверим на INVALID_HANDLE и регистрируем ошибки с помощью Print. Настроим m_rsiBuffer, m_envUpperBandBuffer и m_envLowerBandBuffer в качестве массивов временных рядов с помощью ArraySetAsSeries. Новая структура классов позволит нам эффективно координировать несколько торговых корзин, централизуя данные индикаторов для согласованного формирования сигналов по всем корзинам. Затем нам потребуется разработать логику для подсчета всех отдельных позиций корзин для упрощения отслеживания и очистки корзин.
//--- Count Active Baskets int countActiveBaskets() { int count = 0; for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL && m_traders[i].getCurrentState() != MarketZoneTrader::INACTIVE) { count++; } } return count; } //--- Cleanup Terminated Baskets void cleanupTerminatedBaskets() { int newSize = 0; for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL && m_traders[i].getCurrentState() == MarketZoneTrader::INACTIVE) { delete m_traders[i]; m_traders[i] = NULL; } if (m_traders[i] != NULL) newSize++; } MarketZoneTrader* temp[]; ArrayResize(temp, newSize); int index = 0; for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL) { temp[index] = m_traders[i]; index++; } } ArrayFree(m_traders); ArrayResize(m_traders, newSize); for (int i = 0; i < newSize; i++) { m_traders[i] = temp[i]; } ArrayFree(temp); }
Здесь мы добавляем две новые функции в класс BasketManager - countActiveBaskets и cleanupTerminatedBaskets. Начнем с функции countActiveBaskets, которая отслеживает количество активных торговых корзин. Инициализируем переменную count значением 0 и перебираем массив m_traders, используя функцию ArraySize. Для каждой ненулевой записи m_traders, проверяем, не является ли ее состояние, полученное с помощью функции getCurrentState, значением MarketZoneTrader::INACTIVE. Если условие выполняется, увеличиваем count. Возвращаем count, чтобы отслеживать количество одновременно открытых корзин, что крайне важно для того, чтобы оставаться в пределах лимита m_maxInitialPositions при открытии новых корзин.
Далее создадим функцию cleanupTerminatedBaskets для удаления неактивных корзин и оптимизации памяти. Сначала подсчитываем количество ненулевых записей в массиве m_traders, проходясь по нему в цикле. Если trader не равен null и его getCurrentState возвращает MarketZoneTrader::INACTIVE, используем delete для освобождения памяти и устанавливаем значение в NULL. Отслеживаем количество оставшихся ненулевых значений trader в newSize. Далее создаем массив temp, изменяем его размер до newSize с ArrayResize и копируем ненулевые trader из m_traders в temp, с помощью счетчика index. Очищаем m_traders с ArrayFree, меняем размер newSize и переносим trader обратно из temp. Наконец, освобождаем temp с ArrayFree. Очистка удаляет невостребованные корзины, поддерживая эффективность нашей системы и готовность к новым сделкам. Перейдем к модификатору публичного доступа, где изменим способ обработки конструктора и деструктора при инициализации и уничтожении членов и элементов класса.
public: BasketManager(string symbol, int baseMagic, int maxInitPos) { m_symbol = symbol; m_baseMagicNumber = baseMagic; m_maxInitialPositions = maxInitPos; ArrayResize(m_traders, 0); m_handleRsi = INVALID_HANDLE; m_handleEnvUpper = INVALID_HANDLE; m_handleEnvLower = INVALID_HANDLE; } ~BasketManager() { for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL) delete m_traders[i]; } ArrayFree(m_traders); cleanupIndicators(); }
Начнем с конструктора BasketManager, которая принимает в качестве параметров symbol, baseMagic и maxInitPos. Присвоим их m_symbol, m_baseMagicNumber и m_maxInitialPositions соответственно, чтобы установить торговый символ, базовое магическое число для уникальной идентификации корзины и максимальное количество активных корзин. Инициализируем массив m_traders нулевым значением, используя функцию ArrayResize и устанавливаем хэндлы индикатора — m_handleRsi, m_handleEnvUpper и m_handleEnvLower — на INVALID_HANDLE, чтобы подготовиться к последующей настройке индикатора. Конструктор имеет решающее значение для настройки системы с несколькими корзинами.
Далее создадим деструктор ~BasketManager для очистки ресурсов. Как правило, в качестве префикса для деструкторов используется знак тильды, просто для напоминания. Переберем массив m_traders, используя ArraySize, и удалим все ненулевые экземпляры MarketZoneTrader с помощью delete, чтобы освободить их память. Очистим массив m_traders с ArrayFree и вызовем cleanupIndicators для освобождения хэндлов индикаторов и буферов. Это гарантирует корректное завершение работы нашей системы, предотвращая утечки памяти при остановке советника. В предыдущей версии нам приходилось добавлять логику удаления в обработчик события OnDeinit сразу после обнаружения утечки памяти, но здесь его можно добавить на более раннем этапе, поскольку мы уже знаем, что нужно позаботиться об утечках памяти. Затем нам необходимо изменить логику инициализации таким образом, чтобы она могла загружать существующие позиции в соответствующие корзины. Вот логика, которую мы используем для достижения этой цели.
bool initialize() { if (!initializeIndicators()) return false; //--- Load existing positions into baskets int totalPositions = PositionsTotal(); for (int i = 0; i < totalPositions; i++) { ulong ticket = PositionGetTicket(i); if (PositionSelectByTicket(ticket)) { if (PositionGetString(POSITION_SYMBOL) == m_symbol) { long magic = PositionGetInteger(POSITION_MAGIC); if (magic >= m_baseMagicNumber && magic < m_baseMagicNumber + m_maxInitialPositions) { //--- Check if basket already exists for this magic bool exists = false; for (int j = 0; j < ArraySize(m_traders); j++) { if (m_traders[j] != NULL && m_traders[j].getMagicNumber() == magic) { exists = true; break; } } if (!exists && countActiveBaskets() < m_maxInitialPositions) { createNewBasket(magic, ticket); } } } } } Print("BasketManager initialized with ", ArraySize(m_traders), " existing baskets"); return true; } /* //--- PREVIOUS INITIALIZATION int initialize() { //--- Initialization Start m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); //--- Set magic number int totalPositions = PositionsTotal(); //--- Get total positions for (int i = 0; i < totalPositions; i++) { //--- Iterate positions ulong ticket = PositionGetTicket(i); //--- Get ticket if (PositionSelectByTicket(ticket)) { //--- Select position if (PositionGetString(POSITION_SYMBOL) == m_tradeConfig.marketSymbol && PositionGetInteger(POSITION_MAGIC) == m_tradeConfig.tradeIdentifier) { //--- Check symbol and magic if (activateTrade(ticket)) { //--- Activate position Print("Existing position activated: Ticket=", ticket); //--- Log activation } else { Print("Failed to activate existing position: Ticket=", ticket); //--- Log failure } } } } m_handleRsi = iRSI(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 8, PRICE_CLOSE); //--- Initialize RSI if (m_handleRsi == INVALID_HANDLE) { //--- Check RSI Print("Failed to initialize RSI indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } m_handleEnvUpper = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 150, 0, MODE_SMA, PRICE_CLOSE, 0.1); //--- Initialize upper Envelopes if (m_handleEnvUpper == INVALID_HANDLE) { //--- Check upper Envelopes Print("Failed to initialize upper Envelopes indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } m_handleEnvLower = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 95, 0, MODE_SMA, PRICE_CLOSE, 1.4); //--- Initialize lower Envelopes if (m_handleEnvLower == INVALID_HANDLE) { //--- Check lower Envelopes Print("Failed to initialize lower Envelopes indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } ArraySetAsSeries(m_rsiBuffer, true); //--- Set RSI buffer ArraySetAsSeries(m_envUpperBandBuffer, true); //--- Set upper Envelopes buffer ArraySetAsSeries(m_envLowerBandBuffer, true); //--- Set lower Envelopes buffer Print("EA initialized successfully"); //--- Log success return INIT_SUCCEEDED; //--- Return success //--- Initialization End } */
Здесь мы реализуем обновленную функцию initialize в классе BasketManager для улучшения нашей системы торговли с использованием нескольких корзин, инициализируя индикаторы и загружая существующие позиции в отдельные корзины. Для начала вызовем функцию initializeIndicators для настройки RSI и конвертов, возвращая false в случае неудачи, чтобы убедиться, что наша система располагает необходимыми рыночными данными. В отличие от предыдущей версии, где настройка индикаторов осуществлялась непосредственно в функции initialize в MarketZoneTrader, теперь мы централизуем этот процесс в BasketManager, чтобы использовать данные индикаторов для нескольких корзин. Далее проверяем существующие позиции, используя функцию PositionsTotal и проходим циклом по каждой позиции, получая ее ticket с функцией PositionGetTicket.
Если PositionSelectByTicket успешен и символ позиции совпадает с m_symbol (с помощью PositionGetString), проверяем, попадает ли его магическое число, полученное с помощью функции PositionGetInteger, в диапазон от m_baseMagicNumber до m_baseMagicNumber + m_maxInitialPositions. Затем проверяем, существует ли уже корзина для этого магического числа, проходя циклом по m_traders и вызывая функцию getMagicNumber для ненулевых записей. Если корзина отсутствует и значение countActiveBaskets меньше m_maxInitialPositions, вызываем функцию createNewBasket с магическим числом и ticket, чтобы загрузить позицию в новую корзину. Наконец, выводим количество инициализированных корзин с помощью Print, используя ArraySize m_traders и возвращаем true. При запуске программы мы получаем следующий результат.

Теперь мы можем перейти к обработке тиков, где нам необходимо обрабатывать существующие корзины на каждом тике и создавать новые корзины при подтверждении новых сигналов в функции processTick, в отличие от предыдущей версии, где нам нужно было только инициировать сделки на основе подтвержденных сигналов.
void processTick() { //--- Process existing baskets for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL) { m_traders[i].processTick(m_rsiBuffer, m_envUpperBandBuffer, m_envLowerBandBuffer); } } cleanupTerminatedBaskets(); //--- Check for new signals on new bar if (!isNewBar()) return; if (!CopyBuffer(m_handleRsi, 0, 0, 3, m_rsiBuffer)) { Print("Error loading RSI data. Reverting."); return; } if (!CopyBuffer(m_handleEnvUpper, 0, 0, 3, m_envUpperBandBuffer)) { Print("Error loading upper envelopes data. Reverting."); return; } if (!CopyBuffer(m_handleEnvLower, 1, 0, 3, m_envLowerBandBuffer)) { Print("Error loading lower envelopes data. Reverting."); return; } const int rsiOverbought = 70; const int rsiOversold = 30; int ticket = -1; ENUM_ORDER_TYPE signalType = (ENUM_ORDER_TYPE)-1; double askPrice = NormalizeDouble(SymbolInfoDouble(m_symbol, SYMBOL_ASK), Digits()); double bidPrice = NormalizeDouble(SymbolInfoDouble(m_symbol, SYMBOL_BID), Digits()); if (m_rsiBuffer[1] < rsiOversold && m_rsiBuffer[2] > rsiOversold && m_rsiBuffer[0] < rsiOversold) { if (askPrice > m_envUpperBandBuffer[0]) { if (countActiveBaskets() < m_maxInitialPositions) { signalType = ORDER_TYPE_BUY; } } } else if (m_rsiBuffer[1] > rsiOverbought && m_rsiBuffer[2] < rsiOverbought && m_rsiBuffer[0] > rsiOverbought) { if (bidPrice < m_envLowerBandBuffer[0]) { if (countActiveBaskets() < m_maxInitialPositions) { signalType = ORDER_TYPE_SELL; } } } if (signalType != (ENUM_ORDER_TYPE)-1) { //--- Create new basket with unique magic number int newMagic = m_baseMagicNumber + ArraySize(m_traders); if (newMagic < m_baseMagicNumber + m_maxInitialPositions) { MarketZoneTrader* newTrader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, zoneTargetPoints, zoneSizePoints, newMagic); ticket = newTrader.openInitialOrder(signalType); //--- Open INITIAL position if (ticket > 0 && newTrader.activateTrade(ticket)) { int size = ArraySize(m_traders); ArrayResize(m_traders, size + 1); m_traders[size] = newTrader; Print("New basket created: Magic=", newMagic, ", Ticket=", ticket, ", Type=", EnumToString(signalType)); } else { delete newTrader; Print("Failed to create new basket: Ticket=", ticket); } } else { Print("Maximum initial positions (baskets) reached: ", m_maxInitialPositions); } } }
В функции мы начинаем с перебора массива m_traders, используя функцию ArraySize. Для каждого ненулевого экземпляра MarketZoneTrader, вызываем его функцию processTick, передавая m_rsiBuffer, m_envUpperBandBuffer и m_envLowerBandBuffer для обработки логики отдельных корзин. Это отличается от предыдущей версии, где функция processTick напрямую управляла одним торговым циклом. Затем вызываем функцию cleanupTerminatedBaskets, чтобы удалить неактивные корзины, обеспечивая эффективное использование ресурсов. Далее проверяем наличие новых торговых сигналов только на новом баре, используя функцию isNewBar, и завершаем работу при false, чтобы сэкономить ресурсы.
Загрузим данные индикатора с помощью CopyBuffer для m_handleRsi, m_handleEnvUpper и m_handleEnvLower в соответствующие буферы с записью ошибок с помощью функции Print и завершением работы при возникновении проблем, в отличие от предыдущей версии, где это делалось в MarketZoneTrader. Установим rsiOverbought на 70 и rsiOversold на 30, а также инициализируем ticket и signalType. Получим askPrice и bidPrice, используя SymbolInfoDouble с SYMBOL_ASK и SYMBOL_BID, нормализованные с помощью функции NormalizeDouble.
Для сигнала на покупку, если m_rsiBuffer указывает на условия перепроданности и askPrice превышает m_envUpperBandBuffer, устанавливаем signalType в значение ORDER_TYPE_BUY, если countActiveBaskets ниже m_maxInitialPositions. Для сигнала на продажу, если m_rsiBuffer показывает условия перекупленности, а bidPrice ниже m_envLowerBandBuffer, устанавливаем signalType на ORDER_TYPE_SELL. Если существует допустимый signalType, создаем уникальное магическое число с m_baseMagicNumber плюс ArraySize(m_traders). Если оно находится в пределах m_maxInitialPositions, создаем новый MarketZoneTrader с входными параметрами и новым магическим числом.
Вызовем openInitialOrder с signalType. Если возвращенный ticket действителен и функция activateTrade выполняется успешно, добавляем новый trader в m_traders, используя ArrayResize и фиксируем успех с помощью Print и функции EnumToString. В противном случае удаляем trader и регистрируем ошибку или отмечаем, если достигнут лимит корзины. После открытия новых сделок нам потребуется создать для них новые корзины. Ниже приведена необходимая логика.
private: void createNewBasket(long magic, ulong ticket) { MarketZoneTrader* newTrader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, zoneTargetPoints, zoneSizePoints, magic); if (newTrader.activateTrade(ticket)) { int size = ArraySize(m_traders); ArrayResize(m_traders, size + 1); m_traders[size] = newTrader; Print("Existing position loaded into basket: Magic=", magic, ", Ticket=", ticket); } else { delete newTrader; Print("Failed to load existing position into basket: Ticket=", ticket); } }
Реализуем функцию createNewBasket в приватном разделе класса BasketManager. Это новое дополнение, призванное улучшить нашу систему торговли с использованием нескольких корзин путем создания и управления новыми торговыми корзинами для существующих позиций. Для начала создадим новый экземпляр MarketZoneTrader с именем newTrader, используя входные параметры lotOption, initialLotSize, riskPercentage, riskPoints, zoneTargetPoints, zoneSizePoints и предоставленное "магическое" число для настройки уникальной корзины сделок. Напомним, что в предыдущей версии этот пользовательский входной параметр присутствовал на этапе инициализации, поскольку нам требовался только один экземпляр зоны. Поэтому он применялся ко всем новым позициям, но в данном случае мы организуем его в новые экземпляры классов. Вот код для более быстрого сравнения.
//--- PREVIOUS VERSION OF NEW CLASS INSTANCE //--- Global Instance MarketZoneTrader *trader = NULL; //--- Declare trader instance int OnInit() { //--- EA Initialization Start trader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, maxOrders, restrictMaxOrders, zoneTargetPoints, zoneSizePoints); //--- Create trader instance return trader.initialize(); //--- Initialize EA //--- EA Initialization End }
Затем вызовем activateTrade для объекта newTrader с заданным ticket, чтобы добавить существующую позицию в корзину. В случае успеха мы получим текущий размер массива m_traders, используя ArraySize, увеличим его на единицу с помощью ArrayResize и добавим newTrader в новый слот. Зарегистрируем успешное выполнение с помощью функции Print, включая значения magic и ticket. Если activateTrade завершается ошибкой, удалим newTrader, чтобы освободить память, и регистрируем ошибку с помощью Print. Теперь эта функция позволит нам организовывать существующие позиции в отдельные корзины, что является ключевой особенностью нашей системы из нескольких корзин, в отличие от подхода с одним экземпляром в предыдущей версии. Этот класс позволит нам эффективно управлять корзинами сделок. Теперь перейдем к модификации базового класса таким образом, чтобы он мог содержать новые функции, такие как множественные корзины и трейлинг-стоп. Начнем с членов класса.
//--- Modified MarketZoneTrader Class class MarketZoneTrader { private: enum TradeState { INACTIVE, RUNNING, TERMINATING }; struct TradeMetrics { bool operationSuccess; double totalVolume; double netProfitLoss; }; struct ZoneBoundaries { double zoneHigh; double zoneLow; double zoneTargetHigh; double zoneTargetLow; }; struct TradeConfig { string marketSymbol; double openPrice; double initialVolume; long tradeIdentifier; string initialTradeLabel; //--- Label for initial positions string recoveryTradeLabel; //--- Label for recovery positions ulong activeTickets[]; ENUM_ORDER_TYPE direction; double zoneProfitSpan; double zoneRecoverySpan; double accumulatedBuyVolume; double accumulatedSellVolume; TradeState currentState; bool hasRecoveryTrades; //--- Flag to track recovery trades double trailingStopLevel; //--- Virtual trailing stop level }; struct LossTracker { double tradeLossTracker; }; TradeConfig m_tradeConfig; ZoneBoundaries m_zoneBounds; LossTracker m_lossTracker; string m_lastError; int m_errorStatus; CTrade m_tradeExecutor; TradingLotSizeOptions m_lotOption; double m_initialLotSize; double m_riskPercentage; int m_riskPoints; double m_zoneTargetPoints; double m_zoneSizePoints; }
В данном случае мы улучшаем нашу программу, модифицируя класс MarketZoneTrader, а именно его приватный раздел, чтобы включить новые функции, поддерживающие трейлинг-стопы и улучшенную маркировку сделок. Мы сохраняем основную структуру, но вносим ключевые изменения в структуру TradeConfig в соответствии с нашей усовершенствованной стратегией. Сохраним перечисление TradeState с состояниями INACTIVE, RUNNING и TERMINATING, а также структуры TradeMetrics, ZoneBoundaries и LossTracker неизменными по сравнению с предыдущей версией, поскольку они продолжают управлять состояниями сделок, показателями эффективности, границами зон и отслеживанием убытков.
В структуру TradeConfig добавим две новые строковые переменные - initialTradeLabel и recoveryTradeLabel. Эти метки позволяют нам отдельно помечать начальные и восстановительные сделки, улучшая идентификацию и отслеживание сделок в рамках каждой корзины, что особенно полезно для управления несколькими корзинами в нашей новой системе. Также введем hasRecoveryTrades - логическое значение, отслеживающее, включает ли корзина сделки восстановления, что крайне важно для правильного включения или отключения. Кроме того, добавим параметр trailingStopLevel типа double для хранения виртуального уровня трейлинг-стопа для каждой корзины, что обеспечивает динамическую защиту прибыли при совершении первоначальных сделок.
Среди переменных-членов мы сохраняем переменные m_tradeConfig, m_zoneBounds, m_lossTracker, m_lastError, m_errorStatus, m_tradeExecutor, m_lotOption, m_initialLotSize, m_riskPercentage, m_riskPoints, m_zoneTargetPoints и m_zoneSizePoints в их первоначальном виде, но теперь их роли поддерживают новую функциональность трейлинг-стопа и несколько корзин в каждом экземпляре MarketZoneTrader. В частности, мы удаляем из класса переменные, связанные с индикаторами, такие как m_handleRsi и m_rsiBuffer, поскольку теперь они централизованно управляются классом BasketManager, что позволяет каждому трейдеру сосредоточиться на отдельных операциях с корзиной. В конструкторе и деструкторе нам потребуется немного изменить некоторые переменные, чтобы они обрабатывали новые возможности.
public: MarketZoneTrader(TradingLotSizeOptions lotOpt, double initLot, double riskPct, int riskPts, double targetPts, double sizePts, long magic) { m_tradeConfig.currentState = INACTIVE; ArrayResize(m_tradeConfig.activeTickets, 0); m_tradeConfig.zoneProfitSpan = targetPts * _Point; m_tradeConfig.zoneRecoverySpan = sizePts * _Point; m_lossTracker.tradeLossTracker = 0.0; m_lotOption = lotOpt; m_initialLotSize = initLot; m_riskPercentage = riskPct; m_riskPoints = riskPts; m_zoneTargetPoints = targetPts; m_zoneSizePoints = sizePts; m_tradeConfig.marketSymbol = _Symbol; m_tradeConfig.tradeIdentifier = magic; m_tradeConfig.initialTradeLabel = "EA_INITIAL_" + IntegerToString(magic); //--- Label for initial positions m_tradeConfig.recoveryTradeLabel = "EA_RECOVERY_" + IntegerToString(magic); //--- Label for recovery positions m_tradeConfig.hasRecoveryTrades = false; //--- Initialize recovery flag m_tradeConfig.trailingStopLevel = 0.0; //--- Initialize trailing stop m_tradeExecutor.SetExpertMagicNumber(magic); } ~MarketZoneTrader() { ArrayFree(m_tradeConfig.activeTickets); }
Начнем с конструктора MarketZoneTrader. Теперь он принимает дополнительный параметр magic для присвоения каждой корзине сделок уникального магического числа, в отличие от предыдущей версии, где использовалось фиксированное магическое число. Для улучшения маркировки сделок мы добавляем m_tradeConfig.initialTradeLabel как EA_INITIAL плюс magic (с помощью IntegerToString) и m_tradeConfig.recoveryTradeLabel как EA_RECOVERY плюс magic, что позволяет четко идентифицировать начальные и восстановительные сделки в рамках корзины. Инициализируем m_tradeConfig.hasRecoveryTrades значением false, чтобы отслеживать статус восстановительных сделок, и устанавливаем m_tradeConfig.trailingStopLevel равным 0.0 для виртуального трейлинг-стопа. Обе функции являются новыми. Наконец, настраиваем m_tradeExecutor с помощью функции SetExpertMagicNumber, используя параметр magic. Для удобства выделили основные изменения.
Далее упростим деструктор ~MarketZoneTrader по сравнению с предыдущей версией, которая называлась cleanup. Теперь мы очищаем m_tradeConfig.activeTickets только с помощью ArrayFree, в то время как индикаторы очищаются с помощью BasketManager, область действия деструктора сужается до ресурсов, специфичных для корзины. Затем мы можем обновить функцию, отвечающую за активацию сделок, чтобы она могла инициализировать уровень трейлинг-стопа и состояние восстановления для первоначальных сделок.
bool activateTrade(ulong ticket) { m_tradeConfig.hasRecoveryTrades = false; m_tradeConfig.trailingStopLevel = 0.0; //--- THE REST OF THE LOGIC REMAINS return true; }
Здесь мы просто добавляем логику для инициализации уровня трейлинг-стопа первой сделки значением 0 и состояния восстановления значением false, чтобы указать, что это первая позиция в корзине. Наконец, мы можем добавить функцию для открытия исходной позиции.
int openInitialOrder(ENUM_ORDER_TYPE orderType) { //--- Open INITIAL position based on signal int ticket; double openPrice; if (orderType == ORDER_TYPE_BUY) { openPrice = NormalizeDouble(getMarketAsk(), Digits()); } else if (orderType == ORDER_TYPE_SELL) { openPrice = NormalizeDouble(getMarketBid(), Digits()); } else { Print("Invalid order type [Magic=", m_tradeConfig.tradeIdentifier, "]"); return -1; } double lotSize = 0; if (m_lotOption == FIXED_LOTSIZE) { lotSize = m_initialLotSize; } else if (m_lotOption == UNFIXED_LOTSIZE) { lotSize = calculateLotSize(m_riskPercentage, m_riskPoints); } if (lotSize <= 0) { Print("Invalid lot size [Magic=", m_tradeConfig.tradeIdentifier, "]: ", lotSize); return -1; } if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, orderType, lotSize, openPrice, 0, 0, m_tradeConfig.initialTradeLabel)) { ticket = (int)m_tradeExecutor.ResultOrder(); Print("INITIAL trade opened [Magic=", m_tradeConfig.tradeIdentifier, "]: Ticket=", ticket, ", Type=", EnumToString(orderType), ", Volume=", lotSize); } else { ticket = -1; Print("Failed to open INITIAL order [Magic=", m_tradeConfig.tradeIdentifier, "]: Type=", EnumToString(orderType), ", Volume=", lotSize); } return ticket; }
В публичном разделе класса MarketZoneTrader мы добавили новую функцию openInitialOrder, которая поддерживает наши улучшения в области работы с несколькими корзинами и улучшенной маркировки сделок, открывая начальные позиции для конкретной корзины сделок с уникальным идентификатором. Начнем с инициализации ticket и openPrice. Для orderType в значении ORDER_TYPE_BUY установим openPrice, используя getMarketAsk, и нормализуем его с помощью NormalizeDouble и Digits. Для ORDER_TYPE_SELL используем getMarketBid. Если orderType недействителен, регистрируем ошибку с помощью Print, включающего m_tradeConfig.tradeIdentifier, и возвращаем -1.
Определяем lotSize на основе m_lotOption: для FIXED_LOTSIZE используем m_initialLotSize; для UNFIXED_LOTSIZE вызываем функцию calculateLotSize с параметрами m_riskPercentage и m_riskPoints. При недопустимом lotSize, регистрируем ошибку с помощью Print и возвращаем -1. Затем откроем позицию с помощью m_tradeExecutor.PositionOpen с m_tradeConfig.marketSymbol, orderType, lotSize, openPrice и m_tradeConfig.initialTradeLabel для четкой маркировки первоначальных сделок. В случае успеха присвоим ticket значение ResultOrder и выведем результат с помощью Print, включая m_tradeConfig.tradeIdentifier и функцию EnumToString. При неудаче установим ticket на -1 и выведем сообщение об ошибке. Наконец, возвращаем ticket. В отличие от функции openOrder в предыдущей версии, эта функция использует новый параметр initialTradeLabel и фокусируется исключительно на начальных позициях, что соответствует нашей системе из нескольких корзин. В результате компиляции мы получаем следующий результат.

На изображении видно, что мы можем открыть первоначальную сделку и создать для нее новый экземпляр корзины. Теперь нам необходима логика трейлинг-стопа.
void evaluateMarketTick() { if (m_tradeConfig.currentState == INACTIVE) return; if (m_tradeConfig.currentState == TERMINATING) { finalizePosition(); return; } double currentPrice; double profitPoints = 0.0; //--- Handle BUY initial position if (m_tradeConfig.direction == ORDER_TYPE_BUY) { currentPrice = getMarketBid(); profitPoints = (currentPrice - m_tradeConfig.openPrice) / _Point; //--- Trailing Stop Logic for Initial Position if (enableInitialTrailing && !m_tradeConfig.hasRecoveryTrades && profitPoints >= minProfitPoints) { //--- Calculate desired trailing stop level double newTrailingStop = currentPrice - trailingStopPoints * _Point; //--- Start or update trailing stop if profit exceeds minProfitPoints + trailingStopPoints if (profitPoints >= minProfitPoints + trailingStopPoints) { if (m_tradeConfig.trailingStopLevel == 0.0 || newTrailingStop > m_tradeConfig.trailingStopLevel) { m_tradeConfig.trailingStopLevel = newTrailingStop; Print("Trailing stop updated [Magic=", m_tradeConfig.tradeIdentifier, "]: Level=", m_tradeConfig.trailingStopLevel, ", Profit=", profitPoints, " points"); } } //--- Check if price has hit trailing stop if (m_tradeConfig.trailingStopLevel > 0.0 && currentPrice <= m_tradeConfig.trailingStopLevel) { Print("Trailing stop triggered [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " <= TrailingStop=", m_tradeConfig.trailingStopLevel); finalizePosition(); return; } } //--- Zone Recovery Logic if (currentPrice > m_zoneBounds.zoneTargetHigh) { Print("Closing position [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); finalizePosition(); return; } else if (currentPrice < m_zoneBounds.zoneLow) { Print("Triggering RECOVERY trade [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice); } } //--- Handle SELL initial position else if (m_tradeConfig.direction == ORDER_TYPE_SELL) { currentPrice = getMarketAsk(); profitPoints = (m_tradeConfig.openPrice - currentPrice) / _Point; //--- Trailing Stop Logic for Initial Position if (enableInitialTrailing && !m_tradeConfig.hasRecoveryTrades && profitPoints >= minProfitPoints) { //--- Calculate desired trailing stop level double newTrailingStop = currentPrice + trailingStopPoints * _Point; //--- Start or update trailing stop if profit exceeds minProfitPoints + trailingStopPoints if (profitPoints >= minProfitPoints + trailingStopPoints) { if (m_tradeConfig.trailingStopLevel == 0.0 || newTrailingStop < m_tradeConfig.trailingStopLevel) { m_tradeConfig.trailingStopLevel = newTrailingStop; Print("Trailing stop updated [Magic=", m_tradeConfig.tradeIdentifier, "]: Level=", m_tradeConfig.trailingStopLevel, ", Profit=", profitPoints, " points"); } } //--- Check if price has hit trailing stop if (m_tradeConfig.trailingStopLevel > 0.0 && currentPrice >= m_tradeConfig.trailingStopLevel) { Print("Trailing stop triggered [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " >= TrailingStop=", m_tradeConfig.trailingStopLevel); finalizePosition(); return; } } //--- Zone Recovery Logic if (currentPrice < m_zoneBounds.zoneTargetLow) { Print("Closing position [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " < TargetLow=", m_zoneBounds.zoneTargetLow); finalizePosition(); return; } else if (currentPrice > m_zoneBounds.zoneHigh) { Print("Triggering RECOVERY trade [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " > ZoneHigh=", m_zoneBounds.zoneHigh); triggerRecoveryTrade(ORDER_TYPE_BUY, currentPrice); } } }
Здесь мы улучшаем программу, обновляя функцию evaluateMarketTick, чтобы включить логику трейлинг-стопа, сохраняя при этом существующую логику зонального восстановления. Начнем с проверки, находится ли значение m_tradeConfig.currentState в состоянии INACTIVE или TERMINATING, после чего, как и прежде, завершим работу или вызовем функцию finalizePosition. Для позиции на покупку (m_tradeConfig.direction в качестве ORDER_TYPE_BUY) получим currentPrice с помощью getMarketBid и вычислим profitPoints как разницу между currentPrice и m_tradeConfig.openPrice, деленную на _Point. Новая логика трейлинг-стопа проверяет, истинно ли значение enableInitialTrailing, ложно ли значение m_tradeConfig.hasRecoveryTrades и соответствует ли значение profitPoints значению minProfitPoints или превышает его. В этом случае мы вычисляем newTrailingStop, вычитая trailingStopPoints, умноженное на _Point, из currentPrice. Если значение profitPoints также превышает minProfitPoints плюс trailingStopPoints, и m_tradeConfig.trailingStopLevel равно 0,0 или меньше newTrailingStop, обновляем m_tradeConfig.trailingStopLevel и уведомляем об этом с помощью функции Print.
Если параметр m_tradeConfig.trailingStopLevel установлен и значение currentPrice падает ниже него, мы регистрируем срабатывание и вызываем finalizePosition для закрытия сделки. Логика зонального восстановления остается неизменной: позиция закрывается, если currentPrice превышает m_zoneBounds.zoneTargetHigh, или запускается восстановительная сделка на продажу с помощью функции triggerRecoveryTrade, если цена падает ниже m_zoneBounds.zoneLow.
Для позиции на продажу (m_tradeConfig.direction в качестве ORDER_TYPE_SELL), извлекаем currentPrice с помощью getMarketAsk и вычисляем profitPoints в обратном порядке. Логика трейлинг-стопа зеркально отражает вариант на покупку. Устанавливаем newTrailingStop, добавляя _Point trailingStopPoints раз на currentPrice и обновляя m_tradeConfig.trailingStopLevel, если условия выполнены. Позиция закрывается, если currentPrice превышает это значение. Логика зонального восстановления закрывает позицию, если currentPrice ниже m_zoneBounds.zoneTargetLow, или запускает восстановительную сделку на покупку, если она выше m_zoneBounds.zoneHigh. Мы не включаем физический трейлинг-стоп, потому что хотим иметь полный контроль над системой. Таким образом, мы можем отслеживать и управлять всеми экземплярами. Ниже приведен результат выполнения программы для функции трейлинг-стопа.

Из изображения видно, что мы можем отслеживать позицию и закрывать ее, когда цена возвращается к уровню трейлинга. Наконец, создадим экземпляр менеджера корзин и затем используем его для глобального управления.
//--- Global Instance BasketManager *manager = NULL; int OnInit() { manager = new BasketManager(_Symbol, baseMagicNumber, maxInitialPositions); if (!manager.initialize()) { delete manager; manager = NULL; return INIT_FAILED; } return INIT_SUCCEEDED; } void OnDeinit(const int reason) { if (manager != NULL) { delete manager; manager = NULL; Print("EA deinitialized"); } } void OnTick() { if (manager != NULL) { manager.processTick(); } }
Обновим глобальные обработчики экземпляров и событий, чтобы использовать новый класс BasketManager, заменив использование класса MarketZoneTrader в предыдущей версии для улучшения системы торговли несколькими корзинами путем централизации управления несколькими корзинами сделок. Начнем с объявления глобального указателя "менеджера" на класс BasketManager, инициализированный NULL, вместо прежнего указателя trader на MarketZoneTrader. Это изменение имеет решающее значение, поскольку позволяет нам управлять несколькими корзинами сделок через единый менеджер, в отличие от подхода с использованием одного экземпляра в предыдущей версии.
В обработчике событий OnInit мы создаем новый экземпляр BasketManager для manager, передавая _Symbol, baseMagicNumber и maxInitialPositions, чтобы настроить его для текущего графика, уникального идентификатора корзины и максимального количества корзин. Вызовем метод manager.initialize для настройки индикаторов и загрузки существующих позиций, и если он не срабатывает, мы удаляем manager, устанавливаем его значение в NULL и возвращаем INIT_FAILED. В случае успеха возвращаем INIT_SUCCEEDED.
В обработчике событий OnDeinit проверяем, не равен ли manager NULL, затем удаляем его с помощью delete, устанавливаем в NULL и сообщаем о деинициализации с помощью Print. В OnTick проверяем, не равен ли manager NULL и вызываем manager.processTick для обработки рыночных тиков по всем корзинам, заменяя предыдущий вызов на trader.processTick. Это централизует обработку тиков для нескольких корзин, повышая способность системы управлять одновременно поступающими торговыми сигналами. В результате компиляции мы получаем следующее.

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

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

Заключение
Мы улучшили нашу систему зонального восстановления для трендовой торговли на основе конвертов на MQL5, введя трейлинг-стопы и торговую систему на основе нескольких корзин. Приложение из Части 22 было дополнено такими новыми компонентами, как класс BasketManager и обновленные функции MarketZoneTrader. Эти улучшения обеспечивают более гибкую и надежную торговую платформу, которую можно дополнительно настроить, изменив такие параметры, как trailingStopPoints или maxInitialPositions.
Предупреждение: Статья предназначена исключительно для образовательных целей. Торговля сопряжена со значительными финансовыми рисками, а волатильность рынка может привести к убыткам. Тщательное тестирование на исторических данных и внимательное управление рисками имеют решающее значение перед внедрением этой программы на реальных рынках.
Благодаря приведенным усовершенствованиям вы можете доработать эту систему или адаптировать ее архитектуру для создания новых стратегий, повысив свой уровень знаний в области алгоритмической торговли. Удачной торговли!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/18778
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Реализация частичного закрытия позиций в MQL5
Архитектура коллективных торговых решений ИИ-агентов
Переосмысливаем классические стратегии (Часть 14): Анализ нескольких стратегий
Марковские цепи в трейдинге и прогнозировании цены
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Сэр, не открываются первоначальные сделки на продажу.
Связано ли это с логикой торговли?