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

Создание самооптимизирующихся советников на MQL5 (Часть 8): Анализ нескольких стратегий (2)

MetaTrader 5Примеры |
139 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

В этой серии статей мы исследуем творческие подходы к объединению различных торговых стратегий в единый согласованный ансамбль. Цель состоит в том, чтобы преодолеть ограничения отдельных стратегий, объединив их во что-то более мощное.

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

Используя тестер стратегий MetaTrader 5, мы смогли найти эффективные настройки параметров для этой стратегии. Самостоятельный поиск хороших параметров может быть сложной задачей. Именно поэтому генетический оптимизатор в MetaTrader 5 является таким ценным инструментом. Он помогает автоматизировать процесс, экономя время и силы. 

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

Сегодня мы идем дальше. Мы создадим вторую стратегию, основанную на индексе относительной силы (RSI), а затем объединим её со стратегией пересечения скользящих средних. Объединив их, мы стремимся создать более надежную и потенциально более прибыльную ансамблевую стратегию. Мы также будем использовать тестер стратегий MetaTrader 5 для оптимизации этой новой комбинированной стратегии. Но прежде чем углубиться в детали, важно обсудить ключевое понятие: минимизацию количества параметров.

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

Для сравнения, наша исходная стратегия на скользящих средних достигла коэффициента Шарпа 1,29 при форвардном тестировании, с прибылью $133,51 на 101 сделке. Напротив, новая стратегия на основе RSI, которую мы сегодня построим, достигла коэффициента Шарпа 2,68 и прибыли $214,08 — всего на 52 сделках.

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

Тесно взаимодействуя с тестером стратегий и разумно его используя, мы можем выявить более прибыльные настройки. Тем не менее, важно управлять ожиданиями. Тестер стратегий MetaTrader 5 не может волшебным образом исправить плохую стратегию. Если стратегия принципиально ошибочна, никакая оптимизация не сделает её прибыльной.

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


Начало работы в MQL5

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

//+------------------------------------------------------------------+
//|                                                  RSIMidPoint.mqh |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <VolatilityDoctor\Indicators\RSI.mqh>
#include <VolatilityDoctor\Strategies\Parent\Strategy.mqh>

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

class RSIMidPoint : public Strategy
  {
private:
                     //--- The instance of the RSI used in this strategy
                     RSI *my_rsi;

public:
                     //--- Class constructor 
                     RSIMidPoint(string user_symbol,ENUM_TIMEFRAMES user_timeframe,int user_period,ENUM_APPLIED_PRICE user_price);
                     
                     //--- Class destructor
                    ~RSIMidPoint();
                    
                    //--- Class overrides
                    virtual bool Update(void);
                    virtual bool BuySignal(void);
                    virtual bool SellSignal(void);
  };
 

Теперь мы можем приступить к рассмотрению того, как каждый метод будет реализован конкретно для нашей стратегии на основе RSI. Метод обновления должен только обновить значение индикатора RSI и затем убедиться, что текущее показание индикатора RSI не равно нулю. Если это так, то всё прошло нормально; в противном случае что-то пошло не так.

//+------------------------------------------------------------------+
//| Our strategy update method                                       |
//+------------------------------------------------------------------+
bool RSIMidPoint::Update(void)
   {
      //--- Set the indicator value
      my_rsi.SetIndicatorValues(Strategy::GetIndicatorBufferSize(),true);
      
      //--- Check readings are valid
      if(my_rsi.GetCurrentReading() != 0) return(true);
      
      //--- Something went wrong
      return(false);
   }  

Далее мы рассмотрим случаи для наших сигналов на покупку и продажу. Сигналы на покупку генерируются, когда значение RSI находится ниже 50, тогда как, с другой стороны, сигналы на продажу генерируются, когда RSI выше 50.

//+------------------------------------------------------------------+
//| Check for our buy signal                                         |
//+------------------------------------------------------------------+
bool RSIMidPoint::BuySignal(void)
   {
      //--- Buy signals when the RSI is below 50
      return(my_rsi.GetCurrentReading()<50);
   }

//+------------------------------------------------------------------+
//| Check for our sell signal                                        |
//+------------------------------------------------------------------+
bool RSIMidPoint::SellSignal(void)
   {
      //--- Sell signals when the RSI is above 50
      return(my_rsi.GetCurrentReading()>50);
   }

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

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

//+------------------------------------------------------------------+
//| Our class constructor                                            |
//+------------------------------------------------------------------+
RSIMidPoint::RSIMidPoint(string user_symbol,ENUM_TIMEFRAMES user_timeframe,int user_period,ENUM_APPLIED_PRICE user_price)
  {
   my_rsi = new RSI(user_symbol,user_timeframe,user_period,user_price);
   Print("RSI-Mid-Point Strategy Loaded.");
  }

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

//+------------------------------------------------------------------+
//| Our class destructor                                             |
//+------------------------------------------------------------------+
RSIMidPoint::~RSIMidPoint()
  {
   delete my_rsi;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//|                                          MSA Test 2 Baseline.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Define system constants                                          |
//+------------------------------------------------------------------+
#define RSI_PRICE        PRICE_CLOSE
#define RSI_PERIOD       15
#define RSI_TIME_FRAME   PERIOD_D1
#define HOLDING_PERIOD   5

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

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>
#include <VolatilityDoctor\Time\Time.mqh>

Кроме того, нам также потребуется определить несколько глобальных переменных. Обратите внимание, что глобальные переменные можно разделить на два типа: пользовательские и системные. Пользовательские типы определяются самим пользователем. Системные типы доступны в каждой установке MetaTrader 5; к ним относятся такие типы, как double и float. Это один из способов группировки глобальных переменных, который упрощает сопровождение кода.

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+

//--- Custom Types
CTrade    Trade;
TradeInfo *TradeInformation;
Time      *TradeTime;

//--- System Types
double rsi[],ma_close[];
int    rsi_handler;
int    position_timer;

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Our technical indicator
   rsi_handler = iRSI(Symbol(),RSI_TIME_FRAME,RSI_PERIOD,RSI_PRICE);

//--- Create dynamic instances of our custom types
   TradeTime        = new Time(Symbol(),RSI_TIME_FRAME);
   TradeInformation = new TradeInfo(Symbol(),RSI_TIME_FRAME);


//--- Safety checks
   if(rsi_handler == INVALID_HANDLE)
      return(false);
   
//--- Everything was fine
   return(INIT_SUCCEEDED);
  }
//--- End of OnInit Scope

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete the indicators and dynamic objects
   IndicatorRelease(rsi_handler);

   delete TradeTime;
   delete TradeInformation;

  }
//--- End of Deinit Scope

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new daily candle has formed
   if(TradeTime.NewCandle())
     {

      //--- Update our technical indicators
      Update();

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Reset the position timer
         position_timer = 0;

         //--- Check for a trading signal
         CheckSignal();
        }

      //--- Otherwise
      else
        {
         //--- The position has reached maturity
         if(position_timer == HOLDING_PERIOD)
            Trade.PositionClose(Symbol());

         //--- Otherwise keep holding
         else
            position_timer++;
        }
     }
  }
//--- End of OnTick Scope

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

//+------------------------------------------------------------------+
//| Update our technical indicators                                  |
//+------------------------------------------------------------------+
void Update(void)
  {
//--- Call the CopyBuffer method to get updated indicator values
   CopyBuffer(rsi_handler,0,0,1,rsi);
  }
//--- End of Update Scope

Опять же, метод check signal — метод check signal просто ищет торговые сигналы, которые мы определили ранее — что наш RSI будет покупать, когда значение ниже 50; в противном случае, мы будем продавать, если значение выше 50.

//+------------------------------------------------------------------+
//| Check for a trading signal using our cross-over strategy         |
//+------------------------------------------------------------------+
void CheckSignal(void)
  {
//--- Buy signals when the RSI is below 50
   if(rsi[0] < 50)
     {
      Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
      return;
     }

//--- Sell signals when the RSI is above 50
   else
      if(rsi[0] > 50)
        {
         Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
         return;
        }

  }
//--- End of CheckSignal Scope

Наконец, мы унифицируем наши системные константы, которые мы определяем в начале.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef RSI_PRICE        
#undef RSI_PERIOD       
#undef RSI_TIME_FRAME   
#undef HOLDING_PERIOD   
//+------------------------------------------------------------------+

Начиная наш бэктест, мы сначала выберем период обучения. Наш тест будет проходить с 1 января 2022 года по 1 мая 2025 года. Обратите внимание, что мы будем проводить это упражнение на дневном таймфрейме.

Рисунок 1: Дни бэк-тестирования, выбранные нами для базовых измерений. 

Наше моделирование даст нам надежные результаты, если мы установим для параметра «Delays» значение "Random delay". Это имитирует задержки на реальном рынке, проскальзывание и другие факторы, которые учитывают задержки, проскальзывание и другие факторы реальной торговли.

Рисунок 2: Настройки теста, которые мы использовали для нашего бэк-теста. 

Кривая капитала, представленная на рис. 3 ниже, является целью, которую должен воспроизвести класс торговой стратегии RSI.

Рисунок 3: Кривая капитала, построенная с помощью жестко запрограммированной версии нашей торговой стратегии. 

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


Рисунок 4: Подробная статистика результативности нашей торговой стратегии. 

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

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Time\Time.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>
#include <VolatilityDoctor\Strategies\RSIMidPoint.mqh>

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+

//--- Custom Types
CTrade               Trade;
TradeInfo            *TradeInformation;
Time                 *TradeTime;
RSIMidPoint          *RSIMid;

//--- System Types
int    position_timer;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Create dynamic instances of our custom types
   TradeTime        = new Time(Symbol(),RSI_TIME_FRAME);
   TradeInformation = new TradeInfo(Symbol(),RSI_TIME_FRAME);
   RSIMid           = new RSIMidPoint(Symbol(),RSI_TIME_FRAME,RSI_PERIOD,RSI_PRICE);

//--- Everything was fine
   return(INIT_SUCCEEDED);
  }
//--- End of OnInit Scope
Кроме того, нам необходимо обновить функцию OnDeinit, чтобы обеспечить правильное удаление вновь созданного объекта.
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete the dynamic objects
   delete TradeTime;
   delete TradeInformation;
   delete RSIMid;
  }
//--- End of Deinit Scope
И, наконец, нужно убедиться, что метод CheckSignal вызывается надлежащим образом.
//+------------------------------------------------------------------+
//| Check for a trading signal using our cross-over strategy         |
//+------------------------------------------------------------------+
void CheckSignal(void)
  {
//--- Long positions when RSI is below 50
   if(RSIMid.BuySignal())
     {
      Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
      return;
     }

//--- Otherwise short positions when the RSI is above 50
   else
      if(RSIMid.SellSignal())
        {
         Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
         return;
        }
  }
//--- End of CheckSignal Scope

Теперь мы приступим к тестированию созданного нами класса стратегии RSI на том же периоде, который использовался в нашем предыдущем базовом тесте.

Рисунок 5: Выбор дат тестирования для нашей версии стратегии RSI, реализованной через класс. 

Оба теста проводились в режиме "Случайной Задержки", как показано на Рисунке 2, и при данных настройках бэктеста дали схожие кривые капитала.

Рисунок 6: Кривая капитала, полученная с помощью классовой версии нашей торговой стратегии, соответствует базовому результату, полученному с помощью жестко запрограммированного кода. 

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

Рисунок 7: Подробная статистика, анализирующая эффективность версии нашей торговой стратегии, реализованной на основе классов, соответствует базовому показателю. 

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

//+------------------------------------------------------------------+
//|                                                   MSA Test 1.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
//--- Fix any parameters that can afford to remain fixed
#define MA_SHIFT         0
#define MA_TYPE          MODE_EMA
#define RSI_PRICE        PRICE_CLOSE

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

Соответственно, мы создали перечислитель под названием Strategy Mode, который имеет три возможных настройки. Первая настройка, обозначенная 0, — это политика голосования. В рамках этой политики сделка открывается только в том случае, если стратегии скользящего среднего и RSI согласны в отношении одного и того же направления. Другими словами, для открытия длинной позиции обе стратегии должны сигнализировать о длинной позиции.

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

//+------------------------------------------------------------------+
//| User defined enumerator                                          |
//+------------------------------------------------------------------+

enum STRATEGY_MODE
  {
   MODE_ONE   = 0, //Voting Policy
   MODE_TWO   = 1, //RSI Buy & MA Sell
   MODE_THREE = 2  //MA Sell & RSI Buy
  };
Структурировав стратегию таким образом, мы сократили общее количество входных параметров до удобного набора из пяти. Затем мы загружаем ранее обсуждавшиеся зависимости и инициализируем уже знакомые нам переменные. 

//+------------------------------------------------------------------+
//| User Inputs                                                      |
//+------------------------------------------------------------------+
input   group          "Moving Average Strategy Parameters"
input   int             MA_PERIOD                       =        10;//Moving Average Period


input   group          "RSI Strategy Parameters"
input   int             RSI_PERIOD                      =         15;//RSI Period

input   group          "Global Strategy Parameters"
input   ENUM_TIMEFRAMES STRATEGY_TIME_FRAME             = PERIOD_D1;//Strategy Timeframe
input   int             HOLDING_PERIOD                  =         5;//Position Maturity Period
input   STRATEGY_MODE   USER_MODE                       =         0;//Operation Mode For Our Strategy

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Time\Time.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>
#include <VolatilityDoctor\Strategies\OpenCloseMACrossover.mqh>
#include <VolatilityDoctor\Strategies\RSIMidPoint.mqh>

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+

//--- Custom Types
CTrade               Trade;
Time                 *TradeTime;
TradeInfo            *TradeInformation;
RSIMidPoint          *RSIMid;
OpenCloseMACrossover *MACross;

//--- System Types
int                  position_timer;

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Create dynamic instances of our custom types
   TradeTime        = new Time(Symbol(),STRATEGY_TIME_FRAME);
   TradeInformation = new TradeInfo(Symbol(),STRATEGY_TIME_FRAME);
   MACross          = new OpenCloseMACrossover(Symbol(),STRATEGY_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE);
   RSIMid           = new RSIMidPoint(Symbol(),STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE);
//--- Everything was fine
   return(INIT_SUCCEEDED);
  }
//--- End of OnInit Scope

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete the dynamic objects
   delete TradeTime;
   delete TradeInformation;
   delete MACross;
   delete RSIMid;
  }
//--- End of Deinit Scope

В функции OnTick мы применяем ту же логику, что и ранее. Поскольку эта логика уже была подробно рассмотрена, функция OnTick опущена в данном разделе обсуждения. Функция update была слегка модифицирована, так как теперь она вызывает два отдельных метода обновления — по одному для каждой стратегии. Наконец, метод CheckSignal претерпел наиболее значительные изменения. Теперь он должен сначала определить текущий режим стратегии, прежде чем инициировать какие-либо сделки.

//+------------------------------------------------------------------+
//| Update our technical indicators                                  |
//+------------------------------------------------------------------+
void Update(void)
  {
//--- Update the strategy
   RSIMid.Update();
   MACross.Update();
  }
//--- End of Update Scope

//+------------------------------------------------------------------+
//| Check for a trading signal using our cross-over strategy         |
//+------------------------------------------------------------------+
void CheckSignal(void)
  {

//--- Both Stratetgies Should Cast The Same Vote
   if(USER_MODE == 0)
     {
      //--- Long positions when the close moving average is above the open
      if(MACross.BuySignal() && RSIMid.BuySignal())
        {
         Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
         return;
        }

      //--- Otherwise short
      else
         if(MACross.SellSignal()  && RSIMid.SellSignal())
           {
            Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
            return;
           }
     }

//--- RSI Opens All Long Positions & The Moving Average Opens Short Positions
   else
      if(USER_MODE == 1)
        {

         if(RSIMid.BuySignal())
           {
            Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
            return;
           }

         //--- Otherwise short
         else
            if(MACross.SellSignal())
              {
               Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
               return;
              }
        }

      //--- RSI Opens All Short Positions & The Moving Average Opens Long Positions
      else
         if(USER_MODE == 2)
           {

            if(MACross.BuySignal())
              {
               Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
               return;
              }

            //--- Otherwise short
            else
               if(RSIMid.SellSignal())
                 {
                  Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
                  return;
                 }
           }

  }
//--- End of CheckSignal Scope

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

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef MA_SHIFT
#undef RSI_PRICE
#undef MA_TYPE
//+------------------------------------------------------------------+

Давайте приступим к поиску оптимальных настроек параметров для нашего приложения. Выберите только что созданное приложение, а затем в поле "Forward" установите значение "1/2". Это позволит нашему генетическому оптимизатору использовать половину имеющихся исторических данных для обучения оптимальным параметрам, а вторая половина будет скрыта от оптимизатора и использована для проверки настроек, которые оптимизатор счел приемлемыми. 

Рисунок 8: Настройка прямого теста.

После установки "Random delay", как мы делали в предыдущих двух тестах, помните, что на этот раз нам также нужно выбрать оптимизатор. Если у вас в распоряжении есть больше вычислительной мощности, то не стесняйтесь попробовать и другие предлагаемые оптимизаторы, такие как настройка "Slow Complete".

Рисунок 9: Выберите настройку "Fast Genetic Based Algorithm" в поле "Optimization".

Выберите вкладку "inputs" внизу экрана, чтобы открыть панель управления параметрами нашей стратегии, которые будут тестироваться. Поставьте галочки рядом с каждым параметром, а затем выберите начальное и конечное значения для диапазона поиска. Обратите внимание, что параметр шага также очень важно учитывать. Если вы выбрали размер шага 1, ваши результаты действительно будут очень подробными, однако в результате общее количество необходимых шагов может оказаться астрономическим. Как правило, рекомендуются интервалы, такие как 2, 4, 5 или даже 10 для особенно обширных поисков.

Рисунок 10: Выбор интервалов, в которых следует искать наши параметры.

Когда вы запускаете тест, результаты теста Forward отобразятся в виде точечного графика, где каждая точка на графике представляет собой полный тест, выполненный с уникальным набором параметров стратегии.

Рисунок 11: Мы можем визуализировать результаты, полученные тестером стратегий, в виде двумерного точечного графика с прибылью по оси Y.

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

Рисунок.12: Кривая капитала, построенная нашей торговой стратегией, демонстрирует положительную динамику вне выборки.

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

Рисунок.13: Производительность нашего приложения улучшилась после добавления дополнительной стратегии для работы.

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

Рисунок.14: Лучшие результаты, полученные нами, когда наше приложение использовало только одну стратегию.



Заключение

В данной статье читателю были представлены многочисленные преимущества применения принципов объектно-ориентированного программирования (ООП) в MQL5 в сочетании с человеческим творческим потенциалом. Было продемонстрировано, что существует множество способов интегрировать различные стратегии в единую целостную систему. Объектно-ориентированное программирование является мощным и универсальным инструментом для таких творческих задач, поскольку обеспечивает структурированную, контролируемую и предсказуемую разработку.

Опираясь на представленные здесь базовые концепции, читатели теперь могут доработать и оптимизировать свои собственные торговые стратегии. С помощью встроенного в MetaTrader 5 тестера стратегий читатели могут приступить к тестированию и совершенствованию своих решений.

В ходе обсуждения также была подчеркнута важность минимизации количества входных параметров в стратегии. Стратегию со слишком большим количеством параметров становится все сложнее эффективно оптимизировать. Хотя часть вычислительной нагрузки можно переложить на сервисы MQL5 Cloud Network, сохранение простоты конструкции часто приводит к лучшим результатам и более надежным стратегиям.

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

Название файла Описание файла 
MSA Test 2 Baseline.mq5 Тестовая версия нашей торговой стратегии RSI с жестко запрограммированным кодом. 
MSA Test 2 Class.mq5 Тестовая версия нашего класса стратегии RSI.
MSA Test 2.ex5 Скомпилированная версия нашей ансамблевой торговой стратегии. 
MSA Test 2.mq5 Исходный код разработанного нами приложения для ансамблевой торговли с использованием MA и RSI.
RSIMidPoint.mqh Класс MQL5 для нашей стратегии RSI midpoint. 

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

Прикрепленные файлы |
MSA_Test_2.ex5 (47.2 KB)
MSA_Test_2.mq5 (7.64 KB)
RSIMidPoint.mqh (3.51 KB)
Оптимизация и форвард-анализ стратегий (Часть 1): Метод Пардо — базовая модель Оптимизация и форвард-анализ стратегий (Часть 1): Метод Пардо — базовая модель
Статья показывает, как выстроить воспроизводимый процесс разработки и проверки торговых систем в MetaTrader 5: от формализации правил входа/выхода и риск‑менеджмента до пост‑оптимизационной валидации. В основу положен "Метод Пардо": разбиение истории на in‑sample/out‑of‑sample, форвард‑тестирование, мульти‑рынки/таймфреймы и выбор устойчивых "плато" параметров вместо единичных пиков. На примерах PardoSystem и советников PardoEA / Breakout_Bounce показан практический тест‑план для тестера стратегий MetaTrader 5.
Архитектура системы машинного обучения в MetaTrader5 (Часть 5): Последовательный бутстреппинг— устранение смещения меток и повышение доходности Архитектура системы машинного обучения в MetaTrader5 (Часть 5): Последовательный бутстреппинг— устранение смещения меток и повышение доходности
Последовательный бутстреппинг меняет подход к бутстреп-выборке в финансовом машинном обучении, активно избегая временных перекрытий в метках. Это обеспечивает более независимые обучающие выборки, более точные оценки неопределенности и более надежные торговые модели. В этом практическом руководстве объясняется интуитивная основа метода, пошагово разбирается алгоритм, приводятся оптимизированные паттерны кода для работы с большими массивами данных, а также демонстрируется измеримый прирост эффективности с помощью симуляций и реальных бэктестов.
Оптимизатор ястребов Харриса — Harris Hawks Optimization (HHO) Оптимизатор ястребов Харриса — Harris Hawks Optimization (HHO)
Мы реализуем в MQL5 алгоритм Harris Hawks Optimization и разбираем пять режимов движения агентов, управляемых единственным параметром — убывающей энергией побега E. Представлен класс C_AO_HHO, совместимый с унифицированным тестовым стендом, с воспроизводимой реализацией полёта Леви. Алгоритм протестирован на функциях Hilly, Forest и Megacity при 5, 25 и 500 координатах — результаты указывают на аномальное поведение.
Нейросети в трейдинге: Многодоменная архитектура анализа финансовых данных (MDL) Нейросети в трейдинге: Многодоменная архитектура анализа финансовых данных (MDL)
Статья знакомит с фреймворком MDL, который предлагает токенизацию признаков, сценариев и задач для системной организации модели и эффективного формирования контекста. В практической части реализованы CNeuronPerTokenFFN для локальной обработки токенов и CNeuronScenariosToken, генерирующий сценарные токены через 2D‑SSM и FieldPatternEmbedding. Такой подход ускоряет анализ длинных временных рядов и повышает точность интерпретации рыночных данных.