Создание самооптимизирующихся советников на MQL5 (Часть 8): Анализ нескольких стратегий
Как алгоритмические трейдеры, мы сталкиваемся со многими проблемами, которые мы уже обсуждали в этой серии статей. Например, мы заметили, что нашим статистическим моделям легче прогнозировать будущие показания технических индикаторов, чем будущие уровни цен.
Мы также рассмотрели преимущества торговой системы, которая моделирует взаимосвязь между используемой стратегией и рынком, на котором эта стратегия применяется.
Наши модели неизменно демонстрировали лучшие результаты, когда мы заменяли классическую задачу прямого прогнозирования цен этими альтернативными задачами. Прямое прогнозирование цен — сложная задача, но, изменив формулировку проблемы, мы можем превзойти модели, застрявшие на классической задаче, используя при этом те же статистические инструменты.
Сегодня мы рассмотрим новую потенциальную стратегию, основанную на наших предыдущих выводах. Что если мы создадим приложение, которое будет знать три разные торговые стратегии? Может ли это приложение научиться выбирать только одну стратегию за раз, периодически переключаясь на наиболее прибыльную, вместо того чтобы следовать всем трем одновременно? Если приложение может периодически менять стратегии, сможет ли оно выгодно выбрать лучшую из трех известных ему стратегий?
Подобное приложение может оказаться полезнее, чем фиксированный торговый алгоритм, который следует всем трем стратегиям или их комбинации.
Для оценки эффективности нашей статистической модели нам сначала необходимо определить базовый уровень производительности, который наша модель должна превзойти.
Мы объединим три независимые торговые стратегии: стратегию продолжения тренда на основе пересечения скользящих средних (Moving Average Crossover Continuation Strategy), стратегию импульса индекса относительной силы (Relative Strength Index Momentum Strategy) и стратегию прорыва тренда в процентном диапазоне Уильямса (Williams Percent Range Trend Breakout Strategy). Опишем каждую стратегию более подробно.
В этой статье будут представлены некоторые мощные инструменты терминала MetaTrader 5, с акцентом на форвард-тестирование. Форвард-тестирование отличается от тестирования на истории. Мы объясним эти различия позже.
Форвард-тестирование предоставляет нам больше информации, чем простое тестирование на исторических данных, особенно в сочетании с оптимизатором, который генерирует новые параметры стратегии для тестирования. Это позволяет нам надежно находить прибыльные настройки для нашей торговой стратегии. В MetaTrader 5 эта расширенная функциональность реализована с помощью быстрых и медленных генетических оптимизаторов (Fast and Slow Genetic Optimizers).
Объединив эти мощные инструменты тестирования стратегий с надежными принципами объектно-ориентированного проектирования, которые мы рассматриваем в этой серии, мы разработаем и протестируем работоспособность потенциально сильного конкурента для наших статистических моделей.
Начало работы с MQL5
В данном обсуждении рассматривается проблема оптимального сочетания различных стратегий в единую, эффективную комбинацию. Решения, запрограммированные в коде, встречаются редко, особенно при одновременном использовании нескольких стратегий.
Сочетание различных стратегий — увлекательный процесс, требующий креативности. Но это также означает, что мы должны свести к минимуму неожиданные побочные эффекты.
Трейдеры часто используют различные стратегии одновременно. Например, одна стратегия может открывать позиции, а другая — решать, когда их закрыть. Каждая стратегия фокусируется на отдельной части проблемы. Мы хотим имитировать этот человеческий подход, одновременно демонстрируя, как использовать тестер стратегий MetaTrader 5 для поиска оптимальных настроек стратегии.
Для надежного комбинирования стратегий мы будем инкапсулировать каждую стратегию в отдельный класс. Каждый класс необходимо протестировать, чтобы доказать его работоспособность. В основе всех вариантов нашей стратегии будет единый родительский класс Parent. Этот класс будет включать в себя такие распространенные функции, как обновление параметров и проверка сигналов на покупку или продажу.
Каждый класс, наследующий от родительского класса, будет переопределять то, что считается сигналом на покупку или продажу. Это достигается за счет виртуализации общих методов, что позволяет каждой стратегии безопасно их переопределять.
Мы рассмотрели достаточно материала, чтобы начать создание первого класса: родительского класса стратегий.
В MQL5 каждый класс начинается с ключевого слова class, за которым следует имя класса. Как правило, файл именуется так же, как и класс.Некоторые члены класса будут помечены как виртуальные, чтобы сообщить компилятору, что эти функции могут быть переопределены подклассами. Это позволяет каждой стратегии определить свой собственный способ безопасного использования этих методов.
//+------------------------------------------------------------------+ //| Strategy.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" class Strategy { private: int buffer_size; bool buy; bool sell; public: //--- Class constructors and destructors Strategy(void); ~Strategy(void); //--- Check if we have any valid trading signals from our strategy virtual bool BuySignal(void); virtual bool SellSignal(void); //--- Update the technical indicators in our strategy virtual bool Update(void); //--- Get the size of the technical indicator buffers int GetIndicatorBufferSize(void); };
Our default constructor sets all the default values shared by all instances of a strategy.
//+------------------------------------------------------------------+ //| The only way to create an object of the class | //+------------------------------------------------------------------+ Strategy::Strategy(void) { //--- Upon initialization, both flags should be false buy = false; sell = false; buffer_size = 10; }
Нам понадобится несколько вспомогательных методов, обычно называемых геттерами и сеттерами (getters and setters). Для начала определим метод, который возвращает текущий размер буфера, выбранный нами для индикаторов, используемых в нашей стратегии.
//+------------------------------------------------------------------+ //| The size of our indicator buffer | //+------------------------------------------------------------------+ int Strategy::GetIndicatorBufferSize(void) { int res = buffer_size; return(res); }
Кроме того, каждая стратегия должна иметь два метода, каждый из которых будет сообщать нам о наличии сигналов на покупку или продажу соответственно. Каждый класс, наследующий от базового класса, должен реализовывать правила, определяющие его элементы. В противном случае родительский класс всегда будет возвращать false и давать указание пользователю переопределить этот метод в дочернем классе.
//+------------------------------------------------------------------+ //| Check if our strategy is giving us any buy signals | //+------------------------------------------------------------------+ bool Strategy::BuySignal(void) { //--- The user is intended to overwrite the function in the child class //--- Otherwise, failing to do so will always return false as a safety feature Print("[WARNING] This function has only been implemented in the parent: ",__FUNCSIG__,"\nKindly make the necessary corrections to the child class"); return(false); } //+------------------------------------------------------------------+ //| Check if our strategy is giving us any sell signals | //+------------------------------------------------------------------+ bool Strategy::SellSignal(void) { //--- The user is intended to overwrite the function in the child class //--- Otherwise, failing to do so will always return false as a safety feature Print("[WARNING] This function has only been implemented in the parent: ",__FUNCSIG__,"\nKindly make the necessary corrections to the child class"); return(false); }
Обновление объекта стратегии подразумевает обновление всех параметров стратегии, используемых для торговли.
//+------------------------------------------------------------------+ //| Update our strategy parameters | //+------------------------------------------------------------------+ bool Strategy::Update(void) { //--- The user is intended to overwrite the function in the child class Print("[WARNING] This function has only been implemented in the parent: ",__FUNCSIG__,"\nKindly make the necessary corrections to the child class"); return(false); }
На данный момент деструктор класса пуст.
//+------------------------------------------------------------------+ //| The class destructor is currently empty | //+------------------------------------------------------------------+ Strategy::~Strategy(void) { } //+------------------------------------------------------------------+
Начнём с определения тела нашего класса. Класс называется OpenCloseMACrossover. Это стратегия, основанная на использовании двух индикаторов скользящих средних с одинаковыми периодами, применяемых соответственно к ценам открытия и закрытия. Обратите внимание, что методы, которые были виртуальными в родительском классе, снова стали виртуальными в дочернем классе.
Сигналы на продажу генерируются, когда скользящая средняя открытия находится выше скользящей средней закрытия. В противном случае регистрируется сигнал на покупку. Логика рассуждений такова: если средняя цена закрытия превышает среднюю цену открытия, то движение цены можно рассматривать как бычье, и сильный тренд в этом направлении может сохраниться.
//+------------------------------------------------------------------+ //| OpenCloseMACrossover.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" #include<VolatilityDoctor\Strategies\Parent\Strategy.mqh> #include<VolatilityDoctor\Indicators\MA.mqh> class OpenCloseMACrossover : public Strategy { private: //--- Create 2 moving average instances MA *ma_array[2]; public: //---- Class constructors and destructor OpenCloseMACrossover(string symbol,ENUM_TIMEFRAMES time_frame,int period,int shift,ENUM_MA_METHOD ma_mode); ~OpenCloseMACrossover(); //--- Class methods virtual bool Update(void); virtual bool BuySignal(void); virtual bool SellSignal(void); };
Мы уже обсудили правила торговой стратегии, поэтому внедрение методов, проверяющих эти условия, является для нас несложной задачей.
//+------------------------------------------------------------------+ //| Check For a Buy Signal | //+------------------------------------------------------------------+ bool OpenCloseMACrossover::BuySignal(void) { //--- Our buy signal is generated if the close moving average is above the open. return(ma_array[0].GetCurrentReading()>ma_array[1].GetCurrentReading()); } //+------------------------------------------------------------------+ //| Check For a Sell Signal | //+------------------------------------------------------------------+ bool OpenCloseMACrossover::SellSignal(void) { //--- Our sell signal is generated if the open moving average is above the close. return(ma_array[0].GetCurrentReading()<ma_array[1].GetCurrentReading()); }
Наш метод обновления вызывает функции обновления индикатора, которые мы создали для класса SingleBufferIndicator. Для этого метода размер буфера необходимо передать в качестве параметра. В родительском классе мы создали метод, который возвращает нам размер буфера. Мы ссылаемся на родительский класс, используя синтаксис с двойным двоеточием "::" в вызове функции Strategy::GetIndicatorBufferSize(). В конце метода обновления будет проверено, не равны ли обновленные значения нулю, после чего управление будет возвращено контексту, из которого был вызван метод.
//+------------------------------------------------------------------+ //| Our update method | //+------------------------------------------------------------------+ bool OpenCloseMACrossover::Update(void) { //--- Copy indicator readings //--- We will always get the buffer size from the parent class ma_array[0].SetIndicatorValues(Strategy::GetIndicatorBufferSize(),true); ma_array[1].SetIndicatorValues(Strategy::GetIndicatorBufferSize(),true); //--- Make sure neither of the indicator values equal 0 if((ma_array[0].GetCurrentReading() * ma_array[1].GetCurrentReading()) != 0) return(true); //--- If one/both indicator values equal 0, something went wrong. return(false); }
Конструктор класса динамически создает 2 новых экземпляра объектов индикатора скользящей средней и сохраняет их указатели в массиве того же типа, что и указатель, то есть в нашем пользовательском типе индикатора скользящей средней.
//+------------------------------------------------------------------+ //| Our class constructor | //+------------------------------------------------------------------+ OpenCloseMACrossover::OpenCloseMACrossover(string symbol,ENUM_TIMEFRAMES time_frame,int period,int shift,ENUM_MA_METHOD ma_mode) { //--- Create two instances of our moving average indiator objects ma_array[0] = new MA(symbol,time_frame,period,shift,ma_mode,PRICE_CLOSE); ma_array[1] = new MA(symbol,time_frame,period,shift,ma_mode,PRICE_OPEN); //--- Give feedback Print("Strategy class loaded correctly"); }
Деструктор класса удаляет созданные нами динамические объекты и помогает нам управлять потребляемой памятью.
//+------------------------------------------------------------------+ //| Our class destructor | //+------------------------------------------------------------------+ OpenCloseMACrossover::~OpenCloseMACrossover() { //--- Delete the custom objects we made delete ma_array[0]; delete ma_array[1]; //--- Give feedback Print("Strategy deinitialized correctly. Goodbye"); } //+------------------------------------------------------------------+
Теперь перейдем к тестированию нашего первого класса стратегии. Помните, что в нашем итоговом приложении будет три этапа, три разные стратегии. Следовательно, как хорошие разработчики, мы должны тестировать каждый класс по отдельности, используя жестко закодированную версию идентичной стратегии. Тест считается пройденным, если обе стратегии дают одинаковые результаты при ретроспективном анализе за один и тот же период. Это может сэкономить нам часы поиска ошибок в будущем.
Сначала определим системные константы, которые будем поддерживать в обоих тестах. Если обе стратегии эквивалентны, их следует активировать в одно и то же время, открывать одинаковое количество позиций и в одинаковом соотношении покупок и продаж. Повторение этих системных констант является преднамеренной частью нашего теста, поскольку эти константы контролируют параметры стратегии.
//+------------------------------------------------------------------+ //| 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" //+------------------------------------------------------------------+ //| Define system constants | //+------------------------------------------------------------------+ #define MA_TYPE MODE_EMA #define MA_PERIOD 10 #define MA_TIME_FRAME PERIOD_D1 #define MA_SHIFT 0 #define HOLDING_PERIOD 5
Далее загрузим наши зависимости.
//+------------------------------------------------------------------+ //| Dependencies | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> #include <VolatilityDoctor\Trade\TradeInfo.mqh> #include <VolatilityDoctor\Time\Time.mqh>
Затем нам потребуется несколько глобальных переменных для контроля технических индикаторов и подсчета времени, в течение которого наша позиция открыта.
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ //--- Custom Types CTrade Trade; TradeInfo *TradeInformation; Time *TradeTime; //--- System Types double ma_open[],ma_close[]; int ma_open_handler,ma_close_handler; intn position_timer;
После инициализации нашей системы загрузим технические индикаторы и убедимся в их корректности.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Our technical indicators ma_close_handler = iMA(Symbol(),MA_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE,PRICE_CLOSE); ma_open_handler = iMA(Symbol(),MA_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE,PRICE_OPEN); //--- Create dynamic instances of our custom types TradeTime = new Time(Symbol(),MA_TIME_FRAME); TradeInformation = new TradeInfo(Symbol(),MA_TIME_FRAME); //--- Safety checks if(ma_close_handler == INVALID_HANDLE) return(false); if(ma_open_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(ma_close_handler); IndicatorRelease(ma_open_handler); delete TradeTime; delete TradeInformation; } //--- End of Deinit Scope
Если наш терминал получает новые ценовые уровни, мы сначала проверяем, сформировалась ли новая свеча, и если да, то всегда обновляем наши технические индикаторы. Затем мы проверяем, есть ли у нас открытые позиции. Если их нет, проверяем наличие торгового сигнала. Если есть, ждем истечения срока действия позиции, прежде чем закрыть ее.
//+------------------------------------------------------------------+ //| 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 our technical indicators | //+------------------------------------------------------------------+ void Update(void) { //--- Call the CopyBuffer method to get updated indicator values CopyBuffer(ma_close_handler,0,0,1,ma_close); CopyBuffer(ma_open_handler,0,0,1,ma_open); } //--- End of Update Scope
Функция проверки сигналов проверяет наличие торговых условий, которые мы определили ранее: для того, чтобы считать движение цены бычьим, скользящая средняя закрытия должна быть выше скользящей средней открытия.
//+------------------------------------------------------------------+ //| Check for a trading signal using our cross-over strategy | //+------------------------------------------------------------------+ void CheckSignal(void) { //--- Long positions when the close moving average is above the open if(ma_close[0] > ma_open[0]) { Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,""); return; } //--- Otherwise short else if(ma_close[0] < ma_open[0]) { Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,""); return; } } //--- End of CheckSignal Scope
И наконец, всегда отменяйте определение системных переменных, созданных вами в конце программы.
//+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef MA_PERIOD #undef MA_SHIFT #undef MA_TIME_FRAME #undef MA_TYPE #undef HOLDING_PERIOD //+------------------------------------------------------------------+
Давайте теперь определим базовый уровень производительности нашего класса.

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

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

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

Рис. 4. Подробные результаты проведенного нами тестирования с использованием нашей жестко закодированной версии стратегии пересечения скользящих средних
В этой версии нашего теста класса для обеспечения согласованности определенные нами системные константы будут использоваться в неизменном виде.
//+------------------------------------------------------------------+ //| 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" //+------------------------------------------------------------------+ //| Define system constants | //+------------------------------------------------------------------+ #define MA_TYPE MODE_EMA #define MA_PERIOD 10 #define MA_TIME_FRAME PERIOD_D1 #define MA_SHIFT 0 #define HOLDING_PERIOD 5
Далее загрузим наши зависимости.
//+------------------------------------------------------------------+ //| Dependencies | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> #include <VolatilityDoctor\Time\Time.mqh> #include <VolatilityDoctor\Trade\TradeInfo.mqh> #include <VolatilityDoctor\Strategies\OpenCloseMACrossover.mqh>
Нам потребуется создать новую глобальную переменную для экземпляра нашей стратегии.
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ //--- Custom Types CTrade Trade; TradeInfo *TradeInformation; Time *TradeTime; OpenCloseMACrossover *MACross; //--- System Types int position_timer;
В основном, большая часть приложения осталась без изменений. Мы изолируем воздействие сигналов, производимых нашим классом. Поэтому большая часть этого кода должна показаться читателю знакомой уже после первого реализованного нами теста.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Create dynamic instances of our custom types TradeTime = new Time(Symbol(),MA_TIME_FRAME); TradeInformation = new TradeInfo(Symbol(),MA_TIME_FRAME); MACross = new OpenCloseMACrossover(Symbol(),MA_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE); //--- 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; } //--- End of Deinit Scope //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Check if a new daily candle has formed if(TradeTime.NewCandle()) { //--- Update strategy 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 our technical indicators | //+------------------------------------------------------------------+ void Update(void) { //--- Update the strategy MACross.Update(); } //--- End of Update Scope //+------------------------------------------------------------------+ //| Check for a trading signal using our cross-over strategy | //+------------------------------------------------------------------+ void CheckSignal(void) { //--- Long positions when the close moving average is above the open if(MACross.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; } } //--- End of CheckSignal Scope //+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef MA_PERIOD #undef MA_SHIFT #undef MA_TIME_FRAME #undef MA_TYPE #undef HOLDING_PERIOD //+------------------------------------------------------------------+
Теперь мы готовы начать тестирование нашего класса стратегий пересечения скользящей средней MQL5 (MQL5 MA Crossover). Сначала мы загрузим советника, запускающего данный класс, и настроим те же даты для тестирования на истории, которые мы использовали для первого теста.

Рис. 5. Начальные даты, которые мы будем использовать при оценке нашей стратегии. На эти даты повлияет наше будущее форвард-тестирование
Убедитесь, что выбранные вами настройки совпадают с первыми настройками, которые мы использовали.

Рис. 6. Выберем Real ticks (реальные тики) и Random delay (случайная задержка) для надежного тестирования на истории
Подробные результаты тестирования класса и жестко запрограммированной стратегии практически идентичны: обе стратегии совершили по 127 сделок с одинаковым соотношением покупок и продаж и получили коэффициенты Шарпа, близкие друг к другу.

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

Рис. 8. Кривая эквити на основе данного класса активов соответствует жестко заданной стратегии
Теперь мы можем приступить к поиску оптимальных параметров стратегии, поскольку мы убедились в корректности реализации класса.
Сначала нам потребуется заменить большинство системных констант на пользовательские значения. Это позволяет нашему генетическому оптимизатору корректировать стратегию за нас. Следовательно, у нас есть только одно определение системы.
//+------------------------------------------------------------------+ //| 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 | //+------------------------------------------------------------------+ #define MA_SHIFT 0 //+------------------------------------------------------------------+ //| User Inputs | //+------------------------------------------------------------------+ input group "Strategy Parameters" input int MA_PERIOD = 10;//Moving Average Period input int HOLDING_PERIOD = 5;//Position Holding Period input ENUM_TIMEFRAMES MA_TIME_FRAME = PERIOD_D1;//Moving Average Time Frame input ENUM_MA_METHOD MA_TYPE = MODE_EMA;//Moving Average Type
Остальная часть нашей системы остается практически неизменной, поэтому давайте теперь начнем обсуждать разницу между тестированием на истории и форвард-тестированием в MetaTrader 5.
Если коротко, тестирование на истории (back test) — это проверка торговой стратегии на исторических данных. Мы можем использовать наши данные не только для простого анализа исторических данных. Разделив данные на части, мы можем использовать одну часть данных для поиска параметров стратегии, а затем проверить найденные параметры с помощью оставшейся части данных.
В этом и заключается преимущество форвард-тестирования. Мы ищем не просто подходящие настройки, но и пытаемся выяснить, насколько они стабильны.
Поэтому установите параметр Forward в значение 1/2, чтобы использовать 50% ваших данных для обучения, а оставшуюся часть — для тестирования.

Рис. 9. Установим поле Forward на 1/2, чтобы использовать половину данных для обучения, а другую половину — для тестирования
Терминал MetaTrader 5 предлагает нам различные стратегии оптимизации. Мы выберем Fast genetic-based algorithm (быстрый генетический алгоритм), потому что он не слишком требователен к нашей системе, но при этом обеспечивает надежные результаты.

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

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

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

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

Рис. 14. MetaTrader 5 также предоставляет нам подробный анализ всех параметров стратегии, которые он опробовал
Щелчок правой кнопкой мыши по этой таблице откроет контекстное меню, позволяющее выполнять множество полезных задач, таких как определение того, какие столбцы следует включить в таблицу, или даже экспорт результатов форвард-теста в файл.

Рис. 15. Загрузка контекстного меню в таблице результатов показывает, что мы можем экспортировать наши результаты в формат XML для дальнейших исследований
Мы можем подробно рассмотреть результаты тестирования на истории и форвард-тестирования по отдельности. Результаты тестирования на истории представлены ниже на рис. 16. Напомним, что нас больше интересуют результаты форвард-тестирования и то, насколько они отличаются от результатов тестирования на истории.

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

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

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

Рис. 19. Наилучшие настройки, полученные с помощью нашего генетического алгоритма поиска
Заключение
В статье продемонстрирована ценность тестера стратегий MetaTrader 5. Читатели, которые не планируют разрабатывать классы, всё равно смогут найти практические примеры интеграции принципов объектно-ориентированного программирования (ООП), заложенных в MQL5, в свой процесс разработки. Статья также способствует применению передовых методы разработки для создания и тестирования надежных классов.
Наконец, используя выделенные нами принципы объектно-ориентированного проектирования, читатели могут надежно разрабатывать свои стратегии и легко тестировать их для поиска оптимальных входных данных в различных символах и таймфреймах. В следующей статье мы объединим стратегии, основанные на RSI и скользящих средних.
| Имя файла | Описание файла |
|---|---|
| MSA Test 1 Baseline.mq5 | Жестко закодированная реализация стратегии пересечения, использованная нами в качестве результата теста, который должен эмулировать наш класс. |
| MSA Test 1 Class.mq5 | Файл, демонстрирующий тестирование нашего класса стратегий на основе скользящих средних. |
| MSA Test 1.mq5 | Мы использовали этот советник для поиска оптимальных параметров стратегии с помощью тестера стратегий MetaTrader 5. |
| OpenCloseMACrossover.mqh | Класс реализует нашу стратегию на основе скользящей средней. |
| Strategy.mqh | Базовый класс для всех наших стратегий. |
| MSA Test 1.ex5 | Скомпилированная версия нашего советника. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/18402
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Знакомство с языком MQL5 (Часть 36): Освоение API и функции WebRequest в языке MQL5 (X)
Алгоритм Стрекозы — Dragonfly Algorithm (DA)
От начального до среднего уровня: Индикатор (V)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования