English Deutsch 日本語
preview
Переосмысливаем классические стратегии (Часть 21): Разработка комбинированной стратегии на основе полос Боллинджера и RSI

Переосмысливаем классические стратегии (Часть 21): Разработка комбинированной стратегии на основе полос Боллинджера и RSI

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

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

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

Одним из возможных решений является объединение полос Боллинджера с другим техническим индикатором, чтобы помочь отфильтровать движения возврата к среднему значению и трендовые условия. На эту роль прекрасно подходит Индекс относительной силы (Relative Strength Index, RSI). Благодаря сочетанию этих двух индикаторов длинные сделки рассматриваются только тогда, когда цена пробивается ниже нижней экстремальной полосы, а RSI одновременно входит в зону перепроданности. Это служит дополнительным подтверждением того, что цена, скорее всего, вернется к равновесию. Аналогично, когда цена пробивает верхнюю экстремальную полосу, короткие сделки рассматриваются только в том случае, если RSI также входит в зоны перекупленности, что увеличивает вероятность возврата к среднему значению.

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


Важная информация, на которую следует обратить внимание

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

Рисунок 1: Файловая структура, которой мы будем следовать на протяжении всей этой статьи

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

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

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

Рисунок 3: Указанные выше условия бэктеста будут одинаковы во всех наших тестах


Установление базового уровня

Начнем с создания базовой версии нашей торговой системы, чтобы установить пороговые значения прибыльности, которые мы хотим превзойти при всех последующих реализациях торговой системы. Прежде всего загрузим необходимые торговые библиотеки, которые нужны для нашей торговой системы. Начнем с загрузки торговой библиотеки, которая поможет нам управлять своими позициями. Кроме того, загрузим пользовательскую библиотеку TradeInfo, которую мы написали, чтобы помочь себе управлять такими задачами, как получение текущих цен Bid и Ask и минимального объема, разрешенного на рынке.
//+------------------------------------------------------------------+
//|                                                    Version 1.mq5 |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade/Trade.mqh>
#include <VolatilityDoctor/Trade/TradeInfo.mqh>
CTrade Trade;
TradeInfo *TradeHelper;

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

//+------------------------------------------------------------------+
//| System definitions                                               |
//+------------------------------------------------------------------+
#define ATR_PERIOD    14
#define ATR_MULTIPLE  2
#define BB_PERIOD     30
#define BB_SD         2
#define BB_PRICE      PRICE_CLOSE
#define RSI_PERIOD    15
#define RSI_PRICE     PRICE_CLOSE
#define RSI_LEVEL_MAX 70
#define RSI_LEVEL_MIN 30
#define SYMBOL        "EURUSD"
#define TF_MAIN       PERIOD_D1
#define TF_TRADING    PERIOD_H4
#define SHIFT         0

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

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
int      bb_handler,rsi_handler,atr_handler;
double   bb_upper[],bb_mid[],bb_lower[],rsi[],atr[];

Когда наша торговая система инициализируется в первый раз, мы начинаем с определения обработчиков наших индикаторов. После загрузки соответствующих технических индикаторов наша следующая задача - убедиться, что каждый из наших технических индикаторов загружен корректно и что ни один из них не является недействительным. Это можно сделать просто проверяя, соответствует ли каждый хэндл макросу `INVALID_HANDLE`, который определен в API MQL5. Если какой-либо из индикаторов не загрузится должным образом, мы сообщим об этом пользователю, а затем завершим процесс инициализации. В противном случае, если все пройдет хорошо, мы загрузим свой пользовательский класс и вернем сообщение об успешной инициализации.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Setup the technical indicators
   bb_handler  = iBands(SYMBOL,TF_MAIN,BB_PERIOD,SHIFT,BB_SD,BB_PRICE);
   rsi_handler = iRSI(SYMBOL,TF_MAIN,RSI_PERIOD,RSI_PRICE);
   atr_handler = iATR(SYMBOL,TF_MAIN,ATR_PERIOD);

//--- Validate the indicators were setup correctly
   if(bb_handler == INVALID_HANDLE)
     {
      //--- Failed to sertup the Bollinger Bands
      Comment("Failed to setup the Bollinger Bands Indicator: ",GetLastError());
      return(INIT_FAILED);
     }

   else
      if(rsi_handler == INVALID_HANDLE)
        {
         //--- Failed to setup the RSI indicator
         Comment("Failed to setup the RSI Indicator: ",GetLastError());
         return(INIT_FAILED);
        }

      else
         if(atr_handler == INVALID_HANDLE)
           {
            //--- Failed to setup the ATR indicator
            Comment("Failed to setup the ATR Indicator: ",GetLastError());
            return(INIT_FAILED);
           }

         else
           {
            //--- User defined types
            TradeHelper = new TradeInfo(SYMBOL,TF_MAIN);

            //--- Good news: no errors
            return(INIT_SUCCEEDED);

           }
  }

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Release the indicators
   IndicatorRelease(bb_handler);
   IndicatorRelease(rsi_handler);
   IndicatorRelease(atr_handler);
   delete TradeHelper;
  }

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

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Keep track of the time
   static datetime time_stamp;
   datetime time_current = iTime(SYMBOL,TF_TRADING,0);

//--- Check if a new candle has formed
   if(time_stamp != time_current)
     {
      //--- Update the time
      time_stamp = time_current;

      //--- Update our indicator readings
      CopyBuffer(bb_handler,0,0,1,bb_mid);
      CopyBuffer(bb_handler,1,0,1,bb_upper);
      CopyBuffer(bb_handler,2,0,1,bb_lower);
      CopyBuffer(rsi_handler,0,0,1,rsi);
      CopyBuffer(atr_handler,0,0,1,atr);

      //--- Update current price levels
      double close = iClose(SYMBOL,TF_MAIN,SHIFT);

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Check for our trading signal
         if((close > bb_upper[0]) && (rsi[0] > RSI_LEVEL_MAX))
           {
            Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),"");
           }

         else
            if((close < bb_lower[0]) && (rsi[0] < RSI_LEVEL_MIN))
              {
               Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),"");
              }
        }
     }
  }
//+------------------------------------------------------------------+

На этом создание нашей торговой системы завершено. Последний шаг - удалить определение всех системных определений, которые мы ввели в заголовке торговой системы. Опять же, это хорошая практика программирования на MQL5.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef ATR_PERIOD
#undef ATR_MULTIPLE
#undef BB_PERIOD
#undef BB_SD
#undef BB_PRICE
#undef RSI_PERIOD
#undef RSI_PRICE
#undef SYMBOL
#undef TF_MAIN
#undef SHIFT
#undef TF_TRADING
//+------------------------------------------------------------------+

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

Рисунок 4: Кривая эквити, полученная в результате нашей первоначальной попытки сочетать полосы Боллинджера и RSI

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

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

Рисунок 5: Подробная статистика, полученная в результате первой итерации нашей торговой системы


Усовершенствования базового уровня

В попытке улучшить наши первоначальные показатели, мы испробовали множество различных написанных от руки правил, чтобы улучшить как соотношение сигнал/шум, так и частоту сделок. Наша интуиция подсказывала, что, если искать сильный импульс на рынке, можно обнаружить высококачественные торговые возможности. Многие трейдеры, опирающиеся на фундаментальный анализ, используют свечные паттерны для оценки рыночных настроений. Поэтому мы искали свечные паттерны, указывающие на сильное движение рынка.
//--- Update current price levels
double close = iClose(SYMBOL,TF_MAIN,SHIFT);
      
double open_current       = iOpen(SYMBOL,TF_MAIN,SHIFT);
double open_previous      = iOpen(SYMBOL,TF_MAIN,1);
      
double low_current       = iLow(SYMBOL,TF_MAIN,SHIFT);
double low_previous      = iLow(SYMBOL,TF_MAIN,1);

double high_current      = iHigh(SYMBOL,TF_MAIN,SHIFT);
double high_previous     = iHigh(SYMBOL,TF_MAIN,1);

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

//--- If we have no open positions
if(PositionsTotal() == 0)
        {
         //--- Check for our trading signal
         if(((close > bb_upper[0]) && (rsi[0] > RSI_LEVEL_MAX)) || ((low_current<low_previous) && (high_current>high_previous) && (open_current<open_previous) && (close < bb_mid[0])))
           {
            Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),"");
           }

         else
            if(((close < bb_lower[0]) && (rsi[0] < RSI_LEVEL_MIN)) || ((high_current>high_previous) && (low_current<low_previous) && (open_current>open_previous) && (close > bb_mid[0])))
              {
               Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),"");
              }
        }

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

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

Рисунок 6: Кривая эквити, полученная в результате второй итерации нашего советника, не оправдала наших ожиданий

Анализируя подробную статистику новой торговой стратегии, мы видим, что общее количество сделок увеличилось с первоначальных 14 до 29. Это представляет собой 100%-ное улучшение и подтверждает, что нам удалось обнаружить дополнительный сигнал. Однако общая чистая прибыль снизилась с 61 до 35 долларов, что указывает на то, что в стратегию был внесен дополнительный шум. Несмотря на то, что стратегия осталась прибыльной, ее нельзя считать успешной по сравнению с эталонными показателями.

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


Получение исторических рыночных данных

Хотя были представлены только две версии торговой стратегии, читатель должен быть уверен, что было протестировано множество дополнительных итераций. После исчерпания ручных подходов, основанных на правилах, мы пришли к выводу, что использование статистических моделей может помочь нам обнаружить торговые правила, выходящие за рамки того, что может дать одна интуиция. Чтобы добиться этого, мы сначала написали скрипт для извлечения исторических данных из терминала в CSV-файл. Используя те же системные определения, что и раньше, мы записали исторические значения цены открытия, максимума, минимума, цены закрытия и технического индикатора на диск.
//+------------------------------------------------------------------+
//|                          Fetch Data Bollinger Bands RSI Strategy |
//|                                      Copyright 2026, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs

//+------------------------------------------------------------------+
//| System definitions                                               |
//+------------------------------------------------------------------+
#define BB_PERIOD     30
#define BB_SD         2
#define BB_PRICE      PRICE_CLOSE
#define RSI_PERIOD    15
#define RSI_PRICE     PRICE_CLOSE
#define RSI_LEVEL_MAX 70
#define RSI_LEVEL_MIN 30
#define TF_MAIN       PERIOD_D1
#define SHIFT         0

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
double   bb_upper[],bb_mid[],bb_lower[],rsi[];

//--- Setup the technical indicators
int bb_handler  = iBands(Symbol(),TF_MAIN,BB_PERIOD,SHIFT,BB_SD,BB_PRICE);
int rsi_handler = iRSI(Symbol(),TF_MAIN,RSI_PERIOD,RSI_PRICE);

//--- File name
string file_name = Symbol() + " Bollinger Band RSI Data.csv";

//--- Amount of data requested
input int size = 365;

//+------------------------------------------------------------------+
//| Our script execution                                             |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Write to file
   int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");
   
   CopyBuffer(bb_handler,0,0,size,bb_mid);
   ArraySetAsSeries(bb_mid,true);
   CopyBuffer(bb_handler,1,0,size,bb_upper);
   ArraySetAsSeries(bb_upper,true);
   CopyBuffer(bb_handler,2,0,size,bb_lower);
   ArraySetAsSeries(bb_lower,true);
   CopyBuffer(rsi_handler,0,0,size,rsi);
   ArraySetAsSeries(rsi,true);
   
   for(int i=size;i>=1;i--)
     {
      if(i == size)
        {
        
         FileWrite(file_handle,
                  //--- Time
                  "Time",
                   //--- OHLC
                   "Open",
                   "High",
                   "Low",
                   "Close",
                   //--- Technical Indicators
                   "BB Upper",
                   "BB Mid",
                   "BB Lower",
                   "RSI"
                  );
        }

      else
        {
         FileWrite(file_handle,
                   iTime(_Symbol,PERIOD_CURRENT,i),
                   //--- OHLC
                   iOpen(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i),
                   iLow(_Symbol,PERIOD_CURRENT,i),
                   iClose(_Symbol,PERIOD_CURRENT,i),
                   //--- Technical Indicators
                   bb_upper[i],
                   bb_mid[i],
                   bb_lower[i],
                   rsi[i]
                   );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }
//+------------------------------------------------------------------+


Достижение новых уровней эффективности

После сохранения данные проанализированы с использованием статистических библиотек на Python. На первом этапе были загружены необходимые аналитические библиотеки, а затем прочитан CSV-файл, сгенерированный скриптом в MQL5.

#Load the analytical libraries
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

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

#Read in the data
data = pd.read_csv("/ENTER/YOUR/PATH/HERE/EURUSD Bollinger Band RSI Data.csv")

Для справки предоставляется снимок полученных данных обучения.

#Drop the dates that overlap with our backtest
train = data.iloc[:((-365 * 2) - 90),:]
test  = data.iloc[((-365 * 2) - 90):,:]

#Check the dates left
train

Рисунок 8: Мы отфильтровали все наблюдения, которые совпадают с нашим периодом бэктеста. Обязательно сделайте то же самое для лучшей практики

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

plt.plot(data['Close'],color='green')
plt.plot(data['BB Upper'],color='red')
plt.plot(data['BB Lower'],color='blue')
plt.grid()
plt.title('Visualizing Historical EURUSD Exchange Rates')
plt.ylabel('Exchange Rate')
plt.xlabel('Historical Time')
plt.legend(['EURUSD Close','BB Upper','BB Lower'])

Рисунок 9: Визуально проверяем, корректно ли наш MQL5-скрипт отобразил предполагаемые исторические данные по паре EURUSD

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

#Load our machine learning training libraries
from sklearn.linear_model import LinearRegression,Ridge,Lasso,ARDRegression
from sklearn.neighbors    import KNeighborsRegressor,RadiusNeighborsRegressor
from sklearn.svm          import LinearSVR
from sklearn.ensemble     import RandomForestRegressor,BaggingRegressor,AdaBoostRegressor
from sklearn.model_selection import TimeSeriesSplit,cross_val_score

Мы определили горизонт прогноза.

#Define our forecast horizon
HORIZON = 5

А затем нанесли метки на данные, используя будущую цену закрытия.

#Label the data
train['Target'] = train['Close'].shift(-HORIZON)

Создан словарь моделей-кандидатов.

#List all the models we wish to evaluate
models = [LinearRegression(),Ridge(),Lasso(),ARDRegression(),KNeighborsRegressor(),RadiusNeighborsRegressor(),LinearSVR(),RandomForestRegressor(),BaggingRegressor(),AdaBoostRegressor()]

Их эффективность оценивалась с помощью кросс-валидации временных рядов. Поскольку перетасовка наблюдений неприемлема для прогнозирования временных рядов, мы использовали объект `TimeSeriesSplit` из библиотеки scikit-learn.

#Define a time series cross validation object
tscv = TimeSeriesSplit(
  n_splits=5,
  gap=HORIZON
)

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

#Store the performance of each model
scores = []

Была рассчитана и записана среднеквадратичная ошибка каждой модели. 

#Evaluate each model
for model in models:
  #User feedback
  print("Evaluating model: ",model)
  #Store the current score
  current_score = np.mean(np.abs(cross_val_score(model,train.iloc[:,1:-1],train.iloc[:,-1],cv=tscv,scoring='neg_mean_squared_error')))
  scores.append(current_score)

После визуализации результатов модель 3 оказалась лучшей. Хотя оказалось, что модель 5 выдала ошибку, близкую к нулю, она не прошла кросс-валидацию и вернула значения NaN. Эта информация подробно представлена читателю в ниже приведенной таблице.

sns.barplot(scores)
plt.ylabel('Cross Validated RMSE')
plt.xlabel('Model')
plt.title('Model Selection For The EURUSD Market')
plt.axhline(scores[3],linestyle=':',color='red')

Рисунок 10: Модель ARDRegression была наиболее эффективной моделью, которую мы определили в этом упражнении

 Модель 3 соответствует регрессору ARD. Соответственно, подготовимся к экспорту этой модели в формат ONNX.

Модель Ошибка
Линейная регрессия 0.0001957533746919363
Ридж 0.000550907245398377
Лассо 0.014059369238373157
ARDRegression 0.00018190369036281064
KNeighborsRegressor 0.005387854064255319
RadiusNeighborsRegressor nan
LinearSVR 0.0002872914823846638
RandomForestRegressor 0.0015833296216492855
BaggingRegressor 0.0016147744161974461
AdaBoostRegressor 0.0018082307134142561


Экспорт нашей модели в ONNX формат

Модель ARD экспортирована с помощью библиотеки ONNX. Библиотека Open Neural Network Exchange (ONNX) позволяет развертывать модели машинного обучения в формате, не зависящем от языка программирования. Это упрощает разработчикам быстрое создание прототипов и развертывание моделей машинного обучения любой сложности.

import onnx
from skl2onnx.common.data_types import FloatTensorType
from skl2onnx import convert_sklearn
Форма входа модели была определена как число с плавающей запятой размером один на восемь. 
initial_types = [('float_input',FloatTensorType([1,8]))]

Затем наша модель ARD была обучена на полном обучающем наборе. 

model = ARDRegression()
model.fit(train.iloc[:,1:-1],train.iloc[:,-1])

Далее, мы преобразовали модель в её прототип для ONNX.

onnx_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12)

Наконец, мы сохранили ONNX-файл на диск.

onnx.save(onnx_proto,"EURUSD D1 ARDRegression.onnx")


Реализация предложенных усовершенствований

Модель ONNX, которую мы экспортировали из Python, затем загружена в советник в качестве ресурса. 

//+------------------------------------------------------------------+
//| System resources                                                 |
//+------------------------------------------------------------------+
#resource "\\Files\\EURUSD D1 ARDRegression.onnx" as const uchar onnx_buffer[];

Кроме того, были введены новые системные определения для определения входных и выходных параметров модели. 

#define ONNX_INPUTS   8
#define ONNX_OUTPUTS  1

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

long     onnx_model;
vectorf  onnx_outputs;

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
  
//--- Set up the ONNX model
   onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DATA_TYPE_FLOAT);

//--- Define the model I/O shapes
   ulong onnx_input_shape[] = {1,ONNX_INPUTS};
   ulong onnx_output_shape[] = {1,ONNX_OUTPUTS};
   
//--- Validate the ONNX model
   else
      if(!OnnxSetInputShape(onnx_model,0,onnx_input_shape))
        {
         Comment("Failed to define the ONNX model input shape: ",GetLastError());
         return(INIT_FAILED);
        }

      else
         if(!OnnxSetOutputShape(onnx_model,0,onnx_output_shape))
           {
            Comment("Failed to define the ONNX model output shape: ",GetLastError());
            return(INIT_FAILED);
           }

         else
            if(onnx_model == INVALID_HANDLE)
              {
               Comment("Error occured setting up the ONNX model: ",GetLastError());
               return(INIT_FAILED);
              }

            //--- Final settings
            else
              {
              //--- Initialize the ONNX model outputs with a zero
              onnx_outputs = vectorf::Zeros(ONNX_OUTPUTS);

               //--- Good news: no errors
               return(INIT_SUCCEEDED);
              }
  }

Когда торговая система завершает работу, модель ONNX и выделенные ресурсы освобождаются, а комментарии терминала удаляются.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   OnnxRelease(onnx_model);
   Comment("");
  }

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

//--- Check if a new candle has formed
   if(time_stamp != time_current)
     {
      
      //--- Prepare our ONNX model inputs
      vectorf onnx_inputs = {(float)iOpen(SYMBOL,TF_MAIN,SHIFT),
                             (float)iHigh(SYMBOL,TF_MAIN,SHIFT),
                             (float)iLow(SYMBOL,TF_MAIN,SHIFT),
                             (float)iClose(SYMBOL,TF_MAIN,SHIFT),
                             (float)bb_upper[0],
                             (float)bb_mid[0],
                             (float)bb_lower[0],
                             (float)rsi[0]};
                             
      //--- Obtain a forecast from our ONNX model
      OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_outputs);
      Comment("EURUSD Model Forecast: ",onnx_outputs[0]);

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Check for our trading signal
         if(((close > bb_upper[0]) && (rsi[0] > RSI_LEVEL_MAX)) || (onnx_outputs[0] < close))  
           {
            Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),"");
           }

         else
            if(((close < bb_lower[0]) && (rsi[0] < RSI_LEVEL_MIN)) || (onnx_outputs[0] > close))
              {
               Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),"");
              }
        }
     }
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef ONNX_INPUTS
#undef ONNX_OUTPUTS
//+------------------------------------------------------------------+

К сожалению, полученная кривая эквити показала дальнейшее ухудшение показателей. 

Рисунок 11: Кривая эквити, полученная в результате третьей итерации нашего советника, заставляет нас более тщательно подходить к своей методологии

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

Рисунок 12: Наша подробная статистика ясно показывает, что предложенные нами изменения пока не привели к каким-либо улучшениям


Более глубокий анализ для улучшения результатов

Из-за низкой эффективности, которую мы наблюдали, нам пришлось пересмотреть свой подход к моделированию с нуля. В своем предыдущем обсуждении статистического моделирования финансовых рынков мы эмпирически обнаружили, что можно получать более высокие уровни точности при моделировании определенных технических индикаторов вместо непосредственного моделирования необработанных уровней цен. Ссылка на эту статью приведена здесь для удобства читателя. В результате мы сместили фокус с прямого прогнозирования цены на прогнозирование технических индикаторов, используемых в нашей торговой стратегии. 
#Label the data
train['Target 1'] = train['BB Upper'].shift(-HORIZON)
train['Target 2'] = train['BB Mid'].shift(-HORIZON)
train['Target 3'] = train['BB Lower'].shift(-HORIZON)
train['Target 4'] = train['RSI'].shift(-HORIZON)

#Drop missing labels
train = train.iloc[:-HORIZON,:]

Новая модель создала четыре выходных сигнала вместо одного.

final_types = [('float_output',FloatTensorType([1,4]))]
Кроме того, мы потратили время на то, чтобы более тщательно снизить уровень шума в нашей системе. Мы достигли этой цели, применив нормализацию по Z-score для соответствующего масштабирования данных.
Z1 = train.iloc[:,1:-4].mean()
Z2 = train.iloc[:,1:-4].std()
train.iloc[:,1:-4] = ((train.iloc[:,1:-4] - Z1) / Z2)

Поскольку модель ARD оказалась недостаточно обученной, мы выбрали регрессор случайного леса (Random Forest Regressor) для учета нелинейных взаимосвязей.

model = RandomForestRegressor()

model.fit(train.iloc[:,1:-4],train.iloc[:,-4:])

 Модель была преобразована в ONNX и сохранена с описательными соглашениями об именовании.

onnx_proto = convert_sklearn(model,initial_types=initial_types,final_types=final_types,target_opset=12)
onnx.save(onnx_proto,"EURUSD D1 RandomForestRegressor.onnx")


Реализация усовершенствований

После этого мы перезагрузили новую модель ONNX в торговую систему.
//+------------------------------------------------------------------+
//|                                                    Version 4.mq5 |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System resources                                                 |
//+------------------------------------------------------------------+
#resource "\\Files\\EURUSD D1 RandomForestRegressor.onnx" as const uchar onnx_buffer[];

Полученные нами в ходе анализа исторических рыночных данных на языке Python средние значения и стандартные отклонения были тщательно сохранены в виде чисел с плавающей запятой, чтобы предотвратить усечение. 

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
//--- Column Mean Values
const float Z1[] = { (float)1.18132371,  (float)1.18577335,  (float)1.17706596,  (float)1.1812953 ,  (float)1.20514458,
                     (float)1.18303579,  (float)1.16092701,  (float)48.60276562};

//--- Column Standard Deviation
const float Z2[] = { (float)0.09684736,  (float)0.09665192,  (float)0.09686825,  (float)0.09684589,  (float)0.09614994,
                     (float)0.09556366,  (float)0.09612185,  (float)11.10783131};

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

//--- Check if a new candle has formed
   if(time_stamp != time_current)
     {
      //--- Update the time
      time_stamp = time_current;

      //--- Prepare our ONNX model inputs
      vectorf onnx_inputs = {(float)iOpen(SYMBOL,TF_MAIN,SHIFT),
                             (float)iHigh(SYMBOL,TF_MAIN,SHIFT),
                             (float)iLow(SYMBOL,TF_MAIN,SHIFT),
                             (float)iClose(SYMBOL,TF_MAIN,SHIFT),
                             (float)bb_upper[0],
                             (float)bb_mid[0],
                             (float)bb_lower[0],
                             (float)rsi[0]};
      
      //--- Scale the model inputs appropriately
      for(int i = 0; i < ONNX_INPUTS;i++)
         {
            onnx_inputs[i] = ((onnx_inputs[i]-Z1[i])/Z2[i]);
         }
                  
      //--- Obtain a forecast from our ONNX model
      OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_outputs);
      Comment("EURUSD Model Forecast: ",onnx_outputs);

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Check for our trading signal
         if(((close > bb_upper[0]) && (rsi[0] > RSI_LEVEL_MAX)) || ((onnx_outputs[3] < rsi[0]) && (onnx_outputs[1] < bb_mid[0])))  
           {
            Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),"");
           }

         else
            if(((close < bb_lower[0]) && (rsi[0] < RSI_LEVEL_MIN)) || ((onnx_outputs[3] > rsi[0]) && (onnx_outputs[1] > bb_mid[0])))
              {
               Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),"");
              }
        }
     }
  }
//+------------------------------------------------------------------+

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

Рисунок 13: Кривая эквити, полученная в результате нашей четвертой итерации торговой системы, наконец-то дает желаемые результаты

Общее количество сделок увеличилось до 78, а чистая прибыль выросла до 95 долларов. Несмотря на то, что винрейт снизился с 64% до 55%, обе цели — увеличение частоты сделок и прибыльности — были достигнуты.

Рисунок 14: Наша подробная статистика существенно улучшилась по сравнению с установленными нами эталонными показателями эффективности



Анализ наших усовершенствований

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

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

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

sns.scatterplot(data=train,x='Close',y='RSI',hue='Bin Target Threshold Price')
plt.axhline(data['RSI'].mean()+data['RSI'].std(),color='red')
plt.axhline(data['RSI'].mean()-data['RSI'].std(),color='green')
plt.grid()
plt.ylabel('RSI Reading')
plt.xlabel('EURUSD Close Price')
plt.title('Relationship Between RSI & EURUSD Return')

Рисунок 15: Индикатор RSI не смог естественным образом различить бычье и медвежье ценовое движение

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

sns.scatterplot(data=train,y=train['Close']-train['BB Upper'],x=np.arange(train.shape[0]),hue='Bin Target Threshold Price')
plt.grid()
plt.axhline(0,color='red')
plt.ylabel('Difference Between Price & BB Upper')
plt.xlabel('Historical Time')
plt.title('Relationship Between EURUSD Close & BB Upper')

Рисунок 16: Верхняя полоса Боллинджера, по-видимому, определяет для нас лучшую границу принятия решения, чем RSI

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

sns.scatterplot(data=train,y=train['Close']-train['BB Lower'],x=np.arange(train.shape[0]),hue='Bin Target Threshold Price')
plt.axhline(0,color='green')
plt.grid()
plt.ylabel('Difference Between Price & BB Lower')
plt.xlabel('Historical Time')
plt.title('Relationship Between EURUSD Close & BB Lower')

Рисунок 17: Как верхняя, так и нижняя полосы Боллинджера, по-видимому, определяет  лучшие границы принятия решения, чем RSI


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

На данном этапе мы можем начать рассматривать возможность того, что поведение рынка определяется большим количеством измерений изменчивости, чем мы можем себе представить. Будучи людьми, мы отчаянно пытаемся мыслить за пределами трех измерений, которые обычно визуализируются по осям X, Y и Z. Однако рыночные данные, использованные в этом исследовании, охватывают восемь измерений: цена открытия, максимум, минимум, цена закрытия, верхняя, средняя и нижняя полосы Боллинджера, а также RSI. Это повышает вероятность того, что существует высокоразмерная торговая стратегия, которую невозможно непосредственно наблюдать или интуитивно понять.
Алгоритмы машинного обучения без учителя хорошо подходят для обнаружения и представления такой высокоразмерной структуры в форме, доступной для интерпретации человеком. Хотя существует множество неконтролируемых методов, наше обсуждение сосредоточено на мощном методе нелинейной проекции, известном как изометрическое отображение (Isomap). Эти методы обычно называют алгоритмами обучения многообразию или снижения размерности. Их цель состоит в том, чтобы тщательно сгруппировать сходные наблюдения, одновременно максимально четко разделяя непохожие наблюдения.

Широко известным методом снижения размерности является метод главных компонент (Principal Component Analysis, PCA), который широко обсуждался в предыдущих работах. Однако PCA - это линейный метод, который может не учитывать сложные нелинейные взаимосвязи, присутствующие в рыночных данных. В отличие от этого, Isomap, реализованный с помощью библиотеки sklearn.manifold, может выявлять нелинейные высокоразмерные взаимосвязи, которые могут представлять собой эффективную торговую стратегию, недоступную человеческой интуиции.

Для начала загрузим библиотеку Isomap. 

from sklearn.manifold import Isomap

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

enc = Isomap()

manifold = pd.DataFrame(enc.fit_transform(train.iloc[:,1:9]))

manifold

Рисунок 18: Мы использовали изометрическое отображение, чтобы свести все наши рыночные данные всего к двум столбцам

Затем эти полученные компоненты многообразия добавляются к исходному обучающему набору.

train['Iso 1'] = manifold.iloc[:,0]
train['Iso 2'] = manifold.iloc[:,1]

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

sns.heatmap(train.iloc[:,1:].corr())
plt.title('EURUSD Training Data Correlation Heatmap')

Рисунок 19: Корреляционная матрица нашего нового обучающего набора данных

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

sns.scatterplot(data=train,x='Iso 1',y='Iso 2',hue='Bin Target Threshold Price')
plt.grid()
plt.title('Visualizing Our High Dimensional Data in 2 Dimensions')

Рисунок 20: Алгоритмы снижения размерности позволяют нам визуализировать многомерные данные, которые в противном случае было бы невозможно полностью визуализировать

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

scores = []

from sklearn.ensemble import RandomForestClassifier

scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target Threshold Price'],cv=tscv,scoring='accuracy'))))
scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target Threshold BB Mid'],cv=tscv,scoring='accuracy'))))
scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target Threshold RSI'],cv=tscv,scoring='accuracy'))))
scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target 1'],cv=tscv,scoring='accuracy'))))
scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target 2'],cv=tscv,scoring='accuracy'))))

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

sns.barplot(scores)
plt.xticks([0,1,2,3,4],['Price','BB Mid','RSI','Iso 1','Iso 2'])
plt.axhline(scores[0],color='red',linestyle=':')
plt.ylabel('Cross Validation Accuracy 100%')
plt.xlabel('Candidate Target')
plt.title('Our Accuracy Predicting Different Targets Related to The EURUSD')

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

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

initial_types = [('float_input',FloatTensorType([1,8]))]

final_types = [('float_output',FloatTensorType([1,1]))]

model = RandomForestRegressor()

model.fit(train.iloc[:,1:9],train['Bin Target 1'])

onnx_proto = convert_sklearn(model,initial_types=initial_types,final_types=final_types,target_opset=12)

onnx.save(onnx_proto,"EURUSD D1 Iso 1 RandomForestRegressor.onnx")


Последние попытки усовершенствований

После реализации этих изменений протестируем обновленную торговую систему. Начнем с загрузки новой экспортированной модели случайного леса. 
//+------------------------------------------------------------------+
//| System resources                                                 |
//+------------------------------------------------------------------+
#resource "\\Files\\EURUSD D1 Iso 1 RandomForestRegressor.onnx" as const uchar onnx_buffer[];

Затем мы должны указать входные и выходные сигналы модели. Вспомним, что в отличие от предыдущей итерации, в которой использовалось 4 выходных сигнала, эта модель имеет один выходной сигнал. 

#define ONNX_INPUTS   8
#define ONNX_OUTPUTS  1

Торговые правила корректируются соответствующим образом: когда прогноз превышает 0,5, мы открываем длинные позиции; когда он падает ниже 0,5, мы открываем короткие позиции.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

//--- Check if a new candle has formed
   if(time_stamp != time_current)
     {
      
      //--- Prepare our ONNX model inputs
      vectorf onnx_inputs = {(float)iOpen(SYMBOL,TF_MAIN,SHIFT),
                             (float)iHigh(SYMBOL,TF_MAIN,SHIFT),
                             (float)iLow(SYMBOL,TF_MAIN,SHIFT),
                             (float)iClose(SYMBOL,TF_MAIN,SHIFT),
                             (float)bb_upper[0],
                             (float)bb_mid[0],
                             (float)bb_lower[0],
                             (float)rsi[0]};
      
      //--- Scale the model inputs appropriately
      for(int i = 0; i < ONNX_INPUTS;i++)
         {
            onnx_inputs[i] = ((onnx_inputs[i]-Z1[i])/Z2[i]);
         }
                  
      //--- Obtain a forecast from our ONNX model
      OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_outputs);
      Comment("EURUSD Model Forecast: ",onnx_outputs);

      //--- Update current price levels
      double close = iClose(SYMBOL,TF_MAIN,SHIFT);

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Check for our trading signal
         if(onnx_outputs[0] < 0.5)  
           {
            Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),"");
           }

         else
            if(onnx_outputs[0] > 0.5)
              {
               Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),"");
              }
        }
     }
  }
//+------------------------------------------------------------------+

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

Рисунок 22: Кривая эквити, полученная в результате пятой итерации нашей торговой системы, показывает слабые стороны нашей высокоразмерной торговой стратегии

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

Рисунок 23: Подробный анализ окончательной версии нашей торговой системы раскрывает присутствие неприемлемых уровней шума


Заключение

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

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


Название файла Описание файла
Version_1.mq5 Первая предпринятая нами попытка, основанная на правилах, заключалась в объединении полос Боллинджера и индикатора относительной силы. Эта торговая система позволила получить торговые сигналы с высокой вероятностью успеха, но сигналы получались с низкой частотой.
Version_2.mq5
Во второй итерации первоначальной стратегии мы использовали больше правил, написанных от руки, но получили нежелательные результаты и значительно снизили эффективность нашей торговой стратегии.
Version_3.mq5
Третья итерация нашего советника не смогла использовать статистические модели для выявления надлежащих торговых сигналов.
Version_4.mq5
Наиболее прибыльная версия торговой системы, которую нам удалось создать в ходе исследования.
Version_5.mq5
В финальной итерации нашей торговой системы была предпринята попытка обучить высокоразмерные торговые стратегии на основе исторических данных, но это не принесло прибыли.
Fetch_Data_Bollinger_Bands_RSI_Strategy.mq5 Ноутбук Jupyter, который мы использовали для анализа рыночных данных.
Bollinger_Band_RSI_Strategy.ipynb  Скрипт на MQL5, который мы использовали для записи исторических рыночных данных в CSV-файл для дальнейшего анализа на Python.

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

Прикрепленные файлы |
Version_1.mq5 (5.62 KB)
Version_2.mq5 (6.23 KB)
Version_3.mq5 (7.87 KB)
Version_4.mq5 (9.24 KB)
Version_5.mq5 (8.7 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Осциллятор Parafrac: Комбинация индикаторов Parabolic SAR и Fractals Осциллятор Parafrac: Комбинация индикаторов Parabolic SAR и Fractals
Мы рассмотрим, как объединить Parabolic SAR и индикатор Fractals для создания нового индикатора осцилляторного типа. Используя сильные стороны обоих инструментов, трейдеры могут разработать более точную и эффективную торговую стратегию.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Нейросети в трейдинге: Принятие торговых решений с учётом неопределённости (Окончание) Нейросети в трейдинге: Принятие торговых решений с учётом неопределённости (Окончание)
В статье мы доводим адаптацию фреймворка UncAD до цельной торговой архитектуры. Ранее реализованные блоки плотности рыночных состояний, оценки неопределённости, прогнозирования и планирования объединяются в модуль CNeuronUncAD. Затем система обучается на исторических данных EURUSD H1 и проходит проверку в MetaTrader 5. Итоги показывают практический потенциал подхода, но честно указывают на главный вызов — контроль просадки и усиление риск-менеджмента.