English 日本語
preview
Преодоление ограничений машинного обучения (Часть 7): Автоматический выбор стратегии

Преодоление ограничений машинного обучения (Часть 7): Автоматический выбор стратегии

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

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

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

В статье предлагается методологическая схема для решения этих вопросов с помощью двух взаимодополняющих подходов:

  1. Решение "белого ящика": Использовать матричную факторизацию — в частности, сингулярное разложение (SVD) — ожидаемой доходности, чтобы определить комбинации стратегий, на которые положительно влияют текущие рыночные условия.
  2. Решение "чёрного ящика": Использовать глубокие нейронные сети для динамического выбора стратегий, основанных на наблюдаемом поведении рынка.

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


Получение нужных нам данных

Для начала мы напишем MQL5-скрипт для получения важных рыночных данных, которые нам нужны. Мы будем извлекать обычные рыночные данные, а также данные, относящиеся к входным признакам индикаторов, чтобы гарантировать, что наши модели ONNX будут обучены на тех же расчетах индикаторов, которые они будут наблюдать в процессе работы.

//+------------------------------------------------------------------+
//|                                                      ProjectName |
//|                                      Copyright 2020, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs

//--- Define our moving average indicator
#define MA_PERIOD     5                   //--- Moving Average Period
#define MA_TYPE       MODE_SMA            //--- Type of moving average we have

#define RSI_PERIOD    15                  //--- RSI Period

#define STOCH_K       5                    //--- Stochastich K Period
#define STOCH_D       3                    //--- Stochastich D Period
#define STOCH_SLOWING 3                    //--- Stochastic slowing
#define STOCH_MODE    MODE_EMA             //--- Stochastic mode
#define STOCH_PRICE   STO_LOWHIGH          //--- Stochastic price feeds

#define HORIZON        5                   //--- Forecast horizon

//--- Our handlers for our indicators
int ma_handle,ma_o_handle,ma_h_handle,ma_l_handle,rsi_handle,stoch_handle;

//--- Data structures to store the readings from our indicators
double ma_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[],rsi_reading[],sto_reading_main[],sto_reading_signal[];

//--- File name
string file_name = Symbol() + " Market Data As Series Indicators.csv";

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

//+------------------------------------------------------------------+
//| Our script execution                                             |
//+------------------------------------------------------------------+
void OnStart()
  {
   int fetch = size + (HORIZON * 2);
//---Setup our technical indicators
   ma_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE);
   ma_o_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_OPEN);
   ma_h_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_HIGH);
   ma_l_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_LOW);
   rsi_handle  = iRSI(_Symbol,PERIOD_CURRENT,RSI_PERIOD,PRICE_CLOSE);
   stoch_handle = iStochastic(_Symbol,PERIOD_CURRENT,STOCH_K,STOCH_D,STOCH_SLOWING,STOCH_MODE,STOCH_PRICE);

//---Set the values as series
   CopyBuffer(ma_handle,0,0,fetch,ma_reading);
   ArraySetAsSeries(ma_reading,true);
   CopyBuffer(ma_o_handle,0,0,fetch,ma_o_reading);
   ArraySetAsSeries(ma_o_reading,true);
   CopyBuffer(ma_h_handle,0,0,fetch,ma_h_reading);
   ArraySetAsSeries(ma_h_reading,true);
   CopyBuffer(ma_l_handle,0,0,fetch,ma_l_reading);
   ArraySetAsSeries(ma_l_reading,true);
   CopyBuffer(rsi_handle,0,0,fetch,rsi_reading);
   ArraySetAsSeries(rsi_reading,true);
   CopyBuffer(stoch_handle,0,0,fetch,sto_reading_main);
   ArraySetAsSeries(sto_reading_main,true);
   CopyBuffer(stoch_handle,0,0,fetch,sto_reading_signal);
   ArraySetAsSeries(sto_reading_signal,true);

//---Write to file
   int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");

   for(int i=size;i>=1;i--)
     {
      if(i == size)
        {
         FileWrite(file_handle,
                  //--- Time
                  "Time",
                   //--- OHLC
                   "Open",
                   "High",
                   "Low",
                   "Close",
                   //--- MA OHLC
                   "MA O",
                   "MA H",
                   "MA L",
                   "MA C",
                   //--- RSI
                   "RSI",
                   //--- Stochastic Oscilator
                   "Stoch Main",
                   "Stoch Signal"
                  );
        }

      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),
                   //--- MA OHLC
                   ma_o_reading[i],
                   ma_h_reading[i],
                   ma_l_reading[i],
                   ma_reading[i],
                   //--- RSI
                   rsi_reading[i],
                   //--- Stochastic Oscilator
                   sto_reading_main[i],
                   sto_reading_signal[i]
                  );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef HORIZON
#undef MA_PERIOD
#undef MA_TYPE
//+------------------------------------------------------------------+


Анализ нужных нам данных

Далее мы импортируем стандартные библиотеки python, которые нам понадобятся.

#Import the standard libraries 
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt 
import seaborn as sns

Сначала загрузим набор данных, который мы создали с помощью нашего MQL5-скрипта.

data = pd.read_csv("../EURUSD Market Data As Series Indicators.csv")

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

#Drop the last 3 years of historical data
data = data.iloc[:-(365*3),:]
_ = data.iloc[-(365*3):,:]

Убедимся, что горизонт прогноза совпадает с периодом прогнозирования, определенным в скрипте.

HORIZON = 5

Затем рассчитайте реализованную рыночную доходность.

data['Return'] = data['Close'].shift(-HORIZON) - data['Close']
data.dropna(inplace=True)

Чтобы оценить доходность каждой стратегии, мы должны сначала определить направление, в котором, по ее мнению, должен двигаться рынок. Если стратегия предсказывает бычье движение цены, присвоим доходность 1; если медвежье, присвоим -1. Умножим эту ожидаемую доходность на фактическую, чтобы приблизить эффективность стратегии. Доходность будет положительной только в том случае, если стратегия правильно предугадала направление рынка.

data['MA OC Strategy'] = 0
data['MA HL Strategy'] = 0
data['RSI Strategy'] = 0
data['Stochastic Strategy'] = 0

#Moving Average Open and Close strategy
data.loc[data['MA O']<data['MA C'],'MA OC Strategy'] = 1
data.loc[data['MA O']>data['MA C'],'MA OC Strategy'] = -1

#Moving average High Low Strategy
data.loc[data['Close']>data['MA H'],'MA HL Strategy'] = 1
data.loc[data['Close']<data['MA L'],'MA HL Strategy'] = -1

#RSI Strategy
data.loc[data['RSI']>50,'RSI Strategy'] = 1
data.loc[data['RSI']<50,'RSI Strategy'] = -1

#Stoch Main Strategy
data.loc[data['Stoch Main']>80,'Stochastic Strategy'] = 1
data.loc[data['Stoch Main']<30,'Stochastic Strategy'] = -1


#Strategy Returns
for i in np.arange(4):
    data.iloc[:,-1*(i+1)]= data.iloc[:,-1*(i+1)] * data['Return']
    data.iloc[:,-1*(i+1)]= data.iloc[:,-1*(i+1)].cumsum()

data['Return'] = data['Return'].cumsum()

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

data['MA OC 1'] = data['MA OC Strategy'].shift(-1)
data['MA OC 2'] = data['MA OC Strategy'].shift(-HORIZON)

data['MA HL 1'] = data['MA HL Strategy'].shift(-1)
data['MA HL 2'] = data['MA HL Strategy'].shift(-HORIZON)

data['RSI 1'] = data['RSI Strategy'].shift(-1)
data['RSI 2'] = data['RSI Strategy'].shift(-HORIZON)


data['Stochastic 1'] = data['Stochastic Strategy'].shift(-1)
data['Stochastic 2'] = data['Stochastic Strategy'].shift(-HORIZON)


data.dropna(inplace=True)

data

Теперь отделим входные данные от целевых.

X = data.iloc[:,1:12]
y = data.iloc[:,-8:]

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

plt.plot(data.iloc[:,-12:-8])
plt.legend(data.columns[-12:-8])
plt.grid()
plt.title('Estimating The Effectiveness of Different Strategies')
plt.ylabel('Estimated Profit Level')
plt.xlabel('Historical Training Epochs')

Рисунок 1: Визуализация доходности наших независимых стратегий в их нынешнем виде

Теперь выполним сингулярное разложение (SVD) по доходности стратегии. 

#Analyze the returns
U,S,VT = np.linalg.svd(data.iloc[:,-12:-8])

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

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

Матрица S (Sigma) из функции SVD от numpy содержит сингулярные значения. Они отражают, какую долю вариации объясняет каждый главный компонент. На рисунке 2 показана кумулятивная сумма сингулярных значений, масштабированная относительно их L1-нормы. График показывает, что на первые два сингулярных значения приходится более 80% от общей вариации, что указывает на доминирование первых двух основных компонентов.

#Standardize and scale the singular values
sigma_scaled = S / np.linalg.norm(S,1)


sns.barplot(np.cumsum(sigma_scaled),color='black')
plt.axhline(0.8,linestyle='--',color='red')
plt.title('Number of Singular Values Needed To Capture 80% of Variance')
plt.ylabel('Proportion of Variance Explained')
plt.xticks([0,1,2,3],['First Total','Second Total','Third Total','Total'])
plt.xlabel('Number of Singular Values Needed To Recreate The Original Dataset')

Рисунок 2: Для охвата 80% наблюдаемой в наборе данных вариации достаточно первых двух главных компонентов

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

data.iloc[:,-12:-8].corr()

Рисунок 3: Визуализация корреляционной матрицы имеющихся у нас входных признаков рыночных данных

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

VT

array([[ 0.64587337,  0.37029478,  0.63092801,  0.21830991],

       [ 0.10444288, -0.33948578,  0.38679319, -0.85101828],

       [ 0.64575265,  0.19765237, -0.67157641, -0.30483139],

       [ 0.39362773, -0.84176287, -0.03613857,  0.36767716]])

Наконец, мы построим график доходности стратегии, генерируемый этими выбранными комбинациями, на рисунке 4.

plt.plot(data.iloc[:,13]+data.iloc[:,14]+data.iloc[:,15]+data.iloc[:,16],color='red')
plt.plot(data.iloc[:,13]+data.iloc[:,15],color='Orange')
plt.plot(data.iloc[:,13]+data.iloc[:,14],color='Green')
plt.plot(data.iloc[:,13]+data.iloc[:,16],color='Blue')
plt.legend(['High Risk','Medium Risk','Low Risk','Minimal Risk'])
plt.grid()
plt.title('Estimating The Returns Produced by Each of Our Risk Settings')
plt.ylabel('Estimated Profit')
plt.xlabel('Historical Epochs')

Рисунок 4: Визуализация новых потоков прибыли, предложенных нам посредством SVD-факторизации



Реализация нашей стратегии на MQL5

Теперь мы готовы к реализации нашего торгового приложения на MQL5. Как обычно, в наших статьях мы начинаем с определения системных констант, чтобы гарантировать, что поведение приложения соответствует ожиданиям, установленным на этапе моделирования.
//+------------------------------------------------------------------+
//|                                 Automatic Strategy Selection.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/ru/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/ru/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System definiyions                                               |
//+------------------------------------------------------------------+
#define MA_PERIOD     5                    //--- Moving Average Period
#define MA_TYPE       MODE_SMA             //--- Type of moving average 
#define RSI_PERIOD    15                   //--- RSI Period
#define STOCH_K       5                    //--- Stochastich K Period
#define STOCH_D       3                    //--- Stochastich D Period
#define STOCH_SLOWING 3                    //--- Stochastic slowing
#define STOCH_MODE    MODE_EMA             //--- Stochastic mode
#define STOCH_PRICE   STO_LOWHIGH          //--- Stochastic price feeds
#define TOTAL_STRATEGIES 4                 //--- Total strategies we have to choose from

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

//+------------------------------------------------------------------+
//| System libraries                                                 |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
CTrade Trade;

Во-первых, мы настраиваем переменные для наших технических индикаторов и их выходных данных. Затем определяем объекты Mql для хранения данных о времени и тиках. Наконец, мы объявляем массивы для хранения весов основных компонентов — напомним, что стратегиям с положительными реакциями на идентифицированные режимы был присвоен вес 1, а другим - 0.

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
int ma_c_handle,ma_o_handle,ma_h_handle,ma_l_handle,rsi_handle,stoch_handle,atr_handle;
double ma_c_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[],rsi_reading[],sto_reading_main[],sto_reading_signal[],atr_reading[];
double long_vote,short_vote;

MqlDateTime ts,tc;
MqlTick     current_tick;

double const weights_1 [] = {1,1,1,1};
double const weights_2 [] = {1,0,1,0};
double const weights_3 [] = {1,1,0,0};
double const weights_4 [] = {1,0,0,1};
double selected_weights[] = {0,0,0,0};

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

//+------------------------------------------------------------------+
//| Custom enumrations                                               |
//+------------------------------------------------------------------+
enum operation_modes
  {
   HIGH=0,     //High Risk
   MID=1,      //Medium Risk
   LOW=2,      //Low Risk
   MINIMUM=3   //Minimum Risk
  };

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

//+------------------------------------------------------------------+
//| User inputs                                                      |
//+------------------------------------------------------------------+
input group "User Risk Settings"
input operation_modes  user_mode = 1;//Define Your Risk Settings

При запуске приложения мы используем оператор `switch` для загрузки выбранных пользователем весов в массив `weights` (инициализируемый нулями). Затем мы соответствующим образом настраиваем время и технические индикаторы.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Setup our risk settings
   switch(user_mode)
     {
      case(0):
         Print("High risk mode selected");
         ArrayCopy(selected_weights,weights_1,0,0,WHOLE_ARRAY);
         break;
      case(1):
         Print("Medium risk mode selected");
         ArrayCopy(selected_weights,weights_2,0,0,WHOLE_ARRAY);
         break;
      case(2):
         Print("Low risk mode selected");
         ArrayCopy(selected_weights,weights_3,0,0,WHOLE_ARRAY);
         break;
      case(3):
         Print("Minimum risk mode selected");
         ArrayCopy(selected_weights,weights_4,0,0,WHOLE_ARRAY);
         break;
      default:
         Print("No risk mode selected! No Trades will be placed");
         break;
     }

//--- Setup the time
   TimeLocal(tc);
   TimeLocal(ts);

//---Setup our technical indicators
   ma_c_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE);
   ma_o_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_OPEN);
   ma_h_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_HIGH);
   ma_l_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_LOW);
   atr_handle = iATR(_Symbol,PERIOD_CURRENT,14);
   rsi_handle  = iRSI(_Symbol,PERIOD_CURRENT,RSI_PERIOD,PRICE_CLOSE);
   stoch_handle = iStochastic(_Symbol,PERIOD_CURRENT,STOCH_K,STOCH_D,STOCH_SLOWING,STOCH_MODE,STOCH_PRICE);
//---
   return(INIT_SUCCEEDED);
  }

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   IndicatorRelease(ma_c_handle);
   IndicatorRelease(ma_o_handle);
   IndicatorRelease(ma_h_handle);
   IndicatorRelease(ma_l_handle);
   IndicatorRelease(rsi_handle);
   IndicatorRelease(stoch_handle);
   IndicatorRelease(atr_handle);
  }

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

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

   if(ts.day != tc.day)
     {
      //--- Update the time
      TimeLocal(tc);

      //--- Update Our indicator readings
      CopyBuffer(ma_c_handle,0,0,1,ma_c_reading);
      CopyBuffer(ma_o_handle,0,0,1,ma_o_reading);
      CopyBuffer(ma_h_handle,0,0,1,ma_h_reading);
      CopyBuffer(ma_l_handle,0,0,1,ma_l_reading);
      CopyBuffer(rsi_handle,0,0,1,rsi_reading);
      CopyBuffer(stoch_handle,0,0,1,sto_reading_main);
      CopyBuffer(stoch_handle,0,0,1,sto_reading_signal);
      CopyBuffer(atr_handle,0,0,1,atr_reading);

      //--- Copy Market Data
      double close = iClose(Symbol(),PERIOD_CURRENT,0);
      SymbolInfoTick(Symbol(),current_tick);

      //--- Place a position
      if(PositionsTotal() ==0)
        {
         //--- Our strategies will vote on what should be done
         long_vote = 0;
         short_vote = 0;

         for(int i =0; i<TOTAL_STRATEGIES;i++)
           {
            //--- Is the strategy's vote valid?
            if(selected_weights[i] > 0)
              {
               //--- Moving average open close strategy
               if(i == 0)
                 {
                  if(ma_o_reading[0] > ma_c_reading[0])
                     long_vote  += selected_weights[0];

                  else
                     if(ma_o_reading[0] < ma_c_reading[0])
                        short_vote += selected_weights[0];
                 }

               //--- Moving average high low strategy
               if(i == 1)
                 {
                  if(close > ma_h_reading[0])
                     long_vote += selected_weights[1];

                  else
                     if(close < ma_l_reading[0])
                        short_vote += selected_weights[1];
                 }

               //--- RSI Strategy
               if(i == 2)
                 {
                  if(rsi_reading[0] > 50)
                     long_vote += selected_weights[2];

                  else
                     if(rsi_reading[0] < 50)
                        short_vote += selected_weights[2];

                  //--- Stochastic Strategy
                  if(i == 3)
                    {
                     if(sto_reading_main[0] > 50)
                        long_vote += selected_weights[3];

                     else
                        if(sto_reading_main[0] < 50)
                           short_vote += selected_weights[3];
                    }
                 }
              }
           }
         if(long_vote > short_vote)
            Trade.Buy(0.01,Symbol(),current_tick.ask,current_tick.ask-(1.5*atr_reading[0]),current_tick.ask+(1.5*atr_reading[0]));

         if(long_vote < short_vote)
            Trade.Sell(0.01,Symbol(),current_tick.bid,current_tick.bid+(1.5*atr_reading[0]),current_tick.bid-(1.5*atr_reading[0]));
        }

     }
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef MA_PERIOD
#undef MA_TYPE
#undef RSI_PERIOD
#undef STOCH_K
#undef STOCH_D
#undef STOCH_SLOWING
#undef STOCH_MODE
#undef STOCH_PRICE
#undef TOTAL_STRATEGIES
//+------------------------------------------------------------------+


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

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

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

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

Рисунок 6: Выбор настроек 'Random delay' гарантирует, что наш бэктест будет моделировать условия реального рынка.

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

Рисунок 7: Выбрав минимальное и максимальное значение, мы позволим генетическому оптимизатору выполнить поиск

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

Рисунок 8: Анализ результатов нашего исторического бэктеста рыночных данных



Улучшение наших результатов

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

import onnx
from sklearn.linear_model import Ridge
from sklearn.neural_network import MLPRegressor
from skl2onnx.convert import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
from sklearn.model_selection import RandomizedSearchCV,TimeSeriesSplit

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

tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)

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

dist = {
    'max_iter':[10,50,100,500,1000,5000,10000,50000,100000],
    'activation':['tanh','relu','identity','logistic'],
    'alpha':[10e0,10e-1,10e-2,10e-3,10e-4,10-5,10e-6],
    'solver':['lbfgs','adam','sgd'],
    'learning_rate':['constant','invscaling','adaptive'],
    'hidden_layer_sizes':[(11,1),(11,11),(11,11,11),(11,11,11,11),(11,22,33,44),(11,22,55,22,11),(11,100,11),(11,5,2,5,11),(11,3,9,18,9,3)]
}

Затем мы определяем основные параметры нейронной сети, которые остаются неизменными на протяжении всех экспериментов.

model = MLPRegressor(shuffle=False,early_stopping=False,random_state=0,verbose=True)

После завершения настройки мы приступаем к поиску оптимальных параметров. Обратите внимание на важность параметра `n_iter` в рандомизированном поиске — чем больше итераций, тем, как правило, улучшается качество поиска.

rscv = RandomizedSearchCV(model,dist,random_state=0,n_iter=20,scoring='neg_mean_squared_error',cv=tscv,n_jobs=-1,refit=True)

Начинаем поиск.

res = rscv.fit(X,y)

Iteration 1, loss = 0.21844802

Iteration 2, loss = 0.13287107

Iteration 3, loss = 0.08159530

Iteration 4, loss = 0.07053761

Iteration 5, loss = 0.07051259

После завершения поиска наилучшая модель сохраняется в атрибуте `best_estimator_`.

res.best_estimator_

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

Перед экспортом в ONNX мы определяем форму входных и выходных данных нашей нейронной сети.

initial_types = [('float_input',FloatTensorType([1,X.shape[1]]))]
final_types   = [('float_output',FloatTensorType([y.shape[1],1]))]

Далее сохраняем модель как прототип ONNX.

onnx_proto = convert_sklearn(model=res.best_estimator_,initial_types=initial_types,final_types=final_types,target_opset=12)

Запишем модель ONNX на диск с расширением `.onnx`.

onnx.save(onnx_proto,'Unsupervised Strategy Selection MLP.onnx')


Реализуем наши улучшения

Теперь загружаем модель ONNX. 

//+------------------------------------------------------------------+
//| System resources                                                 |
//+------------------------------------------------------------------+
#resource "\\Files\\USS\\Unsupervised Strategy Selection MLP.onnx" as const uchar onnx_buffer[];

Определяем входную и выходную формы модели ONNX.

#define ONNX_INPUTS 11                     //--- Total inputs needed by our ONNX model
#define ONNX_OUTPUTS 8                     //--- Total outputs needed by our ONNX model

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

long onnx_model;
vectorf onnx_features,onnx_targets;

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {

//--- Prepare the model's inputs and outputs
   onnx_features = vectorf::Zeros(ONNX_INPUTS);
   onnx_targets  = vectorf::Zeros(ONNX_OUTPUTS);

//--- Create the ONNX model
   onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DATA_TYPE_FLOAT);

//--- Define the I/O shape
   ulong input_shape[] = {1,ONNX_INPUTS};
   ulong output_shape[] = {ONNX_OUTPUTS,1};

   if(!OnnxSetInputShape(onnx_model,0,input_shape))
     {
      Print("Failed to define ONNX input shape");
      return(INIT_FAILED);
     }

   if(!OnnxSetOutputShape(onnx_model,0,output_shape))
     {
      Print("Failed to define ONNX output shape");
      return(INIT_FAILED);
     }

//--- Check if the model is valid
   if(onnx_model == INVALID_HANDLE)
     {
      Print("Failed to create our ONNX model from buffer");
      return(INIT_FAILED);
     }

//--- Setup the time
   TimeLocal(tc);
   TimeLocal(ts);

//---Setup our technical indicators
   ma_c_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE);
   ma_o_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_OPEN);
   ma_h_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_HIGH);
   ma_l_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_LOW);
   atr_handle = iATR(_Symbol,PERIOD_CURRENT,14);
   rsi_handle  = iRSI(_Symbol,PERIOD_CURRENT,RSI_PERIOD,PRICE_CLOSE);
   stoch_handle = iStochastic(_Symbol,PERIOD_CURRENT,STOCH_K,STOCH_D,STOCH_SLOWING,STOCH_MODE,STOCH_PRICE);
//---
   return(INIT_SUCCEEDED);
  }

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

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

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

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

   if(ts.day != tc.day)
     {
      //--- Update the time
      TimeLocal(tc);

      //--- Update Our indicator readings
      CopyBuffer(ma_c_handle,0,0,1,ma_c_reading);
      CopyBuffer(ma_o_handle,0,0,1,ma_o_reading);
      CopyBuffer(ma_h_handle,0,0,1,ma_h_reading);
      CopyBuffer(ma_l_handle,0,0,1,ma_l_reading);
      CopyBuffer(rsi_handle,0,0,1,rsi_reading);
      CopyBuffer(stoch_handle,0,0,1,sto_reading_main);
      CopyBuffer(stoch_handle,0,0,1,sto_reading_signal);
      CopyBuffer(atr_handle,0,0,1,atr_reading);

      //--- Set our model inputs
      onnx_features[0] = (float) iOpen(Symbol(),PERIOD_CURRENT,0);
      onnx_features[1] = (float) iHigh(Symbol(),PERIOD_CURRENT,0);
      onnx_features[2] = (float) iLow(Symbol(),PERIOD_CURRENT,0);
      onnx_features[3] = (float) iClose(Symbol(),PERIOD_CURRENT,0);
      onnx_features[4] = (float) ma_o_reading[0];
      onnx_features[5] = (float) ma_h_reading[0];
      onnx_features[6] = (float) ma_l_reading[0];
      onnx_features[7] = (float) ma_c_reading[0];
      onnx_features[8] = (float) rsi_reading[0];
      onnx_features[9] = (float)  sto_reading_main[0];
      onnx_features[10] = (float) sto_reading_signal[0];

      //--- Copy Market Data
      double close = iClose(Symbol(),PERIOD_CURRENT,0);
      SymbolInfoTick(Symbol(),current_tick);

      //--- Place a position
      if(PositionsTotal() ==0)
        {
         if(OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_features,onnx_targets))
           {
            Comment("Onnx Model Prediction: \n",onnx_targets);

            //--- Store our result
            vectorf res = {onnx_targets[1]-onnx_targets[0],onnx_targets[3]-onnx_targets[2],onnx_targets[5]-onnx_targets[4],onnx_targets[7]-onnx_targets[6]};

            if(res.Max() > 0)
              {
               Print("Trading oppurtunity found");
               Print(res);
               if(res.ArgMax()==0)
                 {
                  if(ma_o_reading[0]<ma_c_reading[0])
                     Buy();

                  if(ma_o_reading[0]>ma_c_reading[0])
                     Sell();
                 }

               if(res.ArgMax()==1)
                 {
                  if(close>ma_h_reading[0])
                     Buy();

                  if(close<ma_l_reading[0])
                     Sell();
                 }


               if(res.ArgMax()==2)
                 {
                  if(rsi_reading[0]>50)
                     Buy();

                  if(rsi_reading[0]<50)
                     Sell();
                 }

               if(res.ArgMax()==3)
                 {
                  if(sto_reading_main[0]>50)
                     Buy();


                  if(sto_reading_main[0]<50)
                     Sell();
                 }
              }
            else
              {
               Print("No trading oppurtunities expected.");
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Enter a long position                                            |
//+------------------------------------------------------------------+
void Buy(void)
  {
   Trade.Buy(0.01,Symbol(),current_tick.ask,current_tick.ask-(1.5*atr_reading[0]),current_tick.ask+(1.5*atr_reading[0]));
  }

//+------------------------------------------------------------------+
//| Enter a short position                                           |
//+------------------------------------------------------------------+
void Sell(void)
  {
   Trade.Sell(0.01,Symbol(),current_tick.bid,current_tick.bid+(1.5*atr_reading[0]),current_tick.bid-(1.5*atr_reading[0]));
  }
//+------------------------------------------------------------------+

Теперь мы готовы провести бэктест версии "черного ящика" нашего торгового приложения.

Рисунок 10: Выберем правильную версию приложения для нашего второго теста

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

Рисунок 11: Убедитесь, что вы тщательно определили правильные даты тестирования на истории, которые мы будем использовать для нашего теста

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

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

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

Рисунок 14: Подробные результаты, полученные с помощью нашего решения "черного ящика" 



Заключение

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

Название файлаОписание файла
Automatic Strategy Selection Baseline.mq5Наше решение "белого ящика" по четырём уникальным стратегиям, сгенерированным в результате факторизации SVD.
Automatic Strategy Selection.mq5 Наше решение "черного ящика", сгенерированное нашей глубокой нейронной сетью для рынка EURUSD.
Fetch Data Indicators.mq5 Скрипт на MQL5, который мы создали для получения необходимых нам рыночных данных и начала анализа доходности стратегии.

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

Возможности Мастера MQL5, которые вам нужно знать (Часть 73): Использование паттернов Ишимоку и ADX-Wilder Возможности Мастера MQL5, которые вам нужно знать (Часть 73): Использование паттернов Ишимоку и ADX-Wilder
Индикатор Ишимоку (Ichimoku-Kinko-Hyo) и осциллятор ADX-Wilder — это взаимодополняющая пара, которую можно использовать в составе MQL5-советника. Индикатор Ишимоку многогранен, однако в данной статье мы будем использовать его в первую очередь для определения уровней поддержки и сопротивления. Мы также применим ADX для определения тренда. Как обычно, мы используем Мастер MQL5 для построения паттернов и тестирования потенциала, который может иметь эта пара индикаторов.
Оптимизатор конкурирующего роя — Competitive Swarm Optimizer (CSO) Оптимизатор конкурирующего роя — Competitive Swarm Optimizer (CSO)
В данной статье рассматривается Competitive Swarm Optimizer — алгоритм роевой оптимизации, в основе которого лежит предельно простая идея: агенты случайным образом разбиваются на пары, проигравший учится у победителя и притягивается к центру роя. Помимо разбора CSO, в статье представлена модернизация тестового стенда: визуализация работы алгоритмов переведена в 3D - мерное пространство, что позволяет наглядно наблюдать движение популяции на поверхности тестовой функции.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Нейросети в трейдинге: Оптимизация Cross-Attention для анализа длинных последовательностей рынка (Окончание) Нейросети в трейдинге: Оптимизация Cross-Attention для анализа длинных последовательностей рынка (Окончание)
В статье рассматривается практическая реализация архитектуры STCA с интеграцией механизмов OneTrans для совместной обработки временных рядов и контекстных признаков рынка. Описаны особенности построения модели, алгоритмы прямого прохода и накопления исторического состояния. Отдельное внимание уделено процессу обучения и результатам тестирования на реальных данных, демонстрирующим поведение модели в рыночных условиях.