English Deutsch 日本語
preview
Переосмысливаем классические стратегии (Часть 14): Анализ нескольких стратегий

Переосмысливаем классические стратегии (Часть 14): Анализ нескольких стратегий

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

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

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

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

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

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

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

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



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

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

//+------------------------------------------------------------------+
//|                                                      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  100               //--- Period for our moving average
#define MA_TYPE    MODE_EMA          //--- Type of moving average we have 
#define RSI_PERIOD 24                //--- Period For Our RSI Indicator
#define RSI_PRICE  PRICE_CLOSE       //--- Applied Price For our RSI Indicator
#define HORIZON    38                //--- Holding period
#define TF PERIOD_H3                 //--- Time Frame

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

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

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

//--- File name
string file_name = Symbol() + " Market Data As Series Multiple Strategy Analysis.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,TF,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE);
   ma_o_handle = iMA(_Symbol,TF,MA_PERIOD,0,MA_TYPE,PRICE_OPEN);
   rsi_handle = iRSI(_Symbol,TF,RSI_PERIOD,RSI_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(rsi_handle,0,0,fetch,rsi_reading);
   ArraySetAsSeries(rsi_reading,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","True Open","True High","True Low","True Close","True MA C","True MA O","True RSI","Open","High","Low","Close","MA Close","MA Open","RSI");
        }

      else
        {
         FileWrite(file_handle,
                   iTime(_Symbol,TF,i), 
                   iOpen(_Symbol,TF,i),
                   iHigh(_Symbol,TF,i),
                   iLow(_Symbol,TF,i),
                   iClose(_Symbol,TF,i),
                   ma_reading[i],
                   ma_o_reading[i],
                   rsi_reading[i],
                   iOpen(_Symbol,TF,i)   - iOpen(_Symbol,TF,(i + HORIZON)), 
                   iHigh(_Symbol,TF,i)   - iHigh(_Symbol,TF,(i + HORIZON)),
                   iLow(_Symbol,TF,i)    - iLow(_Symbol,TF,(i + HORIZON)),
                   iClose(_Symbol,TF,i)  - iClose(_Symbol,TF,(i + HORIZON)),
                   ma_reading[i] - ma_reading[(i + HORIZON)],
                   ma_o_reading[i] - ma_o_reading[(i + HORIZON)],
                   rsi_reading[i] - rsi_reading[(i + HORIZON)]
                   );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }
//+------------------------------------------------------------------+


Анализ данных в Python

Проанализируем собранные рыночные данные с использованием численных библиотек Python. Сначала загрузим pandas для чтения данных.

#Load our libraries
import pandas as pd

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

#Read in the data
data = pd.read_csv("EURUSD Market Data As Series Multiple Strategy Analysis.csv")

#The optimal holding period suggested by our MT5 Genetic optimizer
HORIZON = 38

#Calculate the true market return
data['Return'] = data['True Close'].shift(-HORIZON) - data['True Close']

#The action suggested by our first strategy, MA Cross
data['Action 1'] = 0

#The action suggested by our second strategy, RSI Strategy
data['Action 2'] = 0

#Buy conditions
data.loc[data['True MA C'] > data['True MA O'],'Action 1'] = 1
data.loc[data['True RSI'] > 50,'Action 2'] = 1

#Sell conditions
data.loc[data['True MA C'] < data['True MA O'],'Action 1'] = -1
data.loc[data['True RSI'] < 50,'Action 2'] = -1

#Perform a linear transformation of the true market return, using our trading stragies
data['Return 1'] = data['Return'] * data['Action 1']
data['Return 2'] = data['Return'] * data['Action 2']

data = data.iloc[:-HORIZON,:]

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

#Drop our back test data
_    = data.iloc[-((365 * 2 * 6)):,:]
data = data.iloc[:-((365 * 2 * 6)),:]

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

#Gether inputs
X = data.iloc[:,1:15]

#Both Strategies will earn equal reward
data['Target 1'] = 0 
data['Target 2'] = 0 

#Strategy 1 is more profitable
data.loc[data['Return 1'] > data['Return 2'],'Target 1'] = 1

#Strategy 2 is more profitable
data.loc[data['Return 2'] > data['Return 1'],'Target 2'] = 1

#Classical Target
data['Classical Target'] = 0

data.loc[data['Return'] > 0,'Classical Target'] = 1

Далее загрузим библиотеки scikit-learn для анализа числовых свойств собранных данных.

#Loading our scikit learn libraries
from sklearn.model_selection import TimeSeriesSplit,cross_val_score
from sklearn.linear_model import LinearRegression,LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.neural_network import MLPRegressor
from sklearn.model_selection import RandomizedSearchCV

Создадим объекты для валидации временных рядов. Для этого используем пять сплитов, что соответствует оптимальному горизонту, найденному генетическим оптимизатором. Затем вычислим средние значения по столбцам и стандартные отклонения, чтобы стандартизировать датасет — приведем его к нулевому среднему и единичному стандартному отклонению.

#Prepare the data for time series modelling
tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)

Z1 = X.mean()
Z2 = X.std()

X = ((X-X.mean()) / X.std())

Теперь измерим точность предсказания новых целевых переменных и сравним ее с точностью прогнозирования классической цели. Используем объекты кросс-валидации из scikit-learn, чтобы оценить точность линейного классификатора. Затем сохраним результаты в массив и построим диаграмму. Как видно, точность по классической цели близка к 50%, а точность предсказания того, какая из двух стратегий окажется более прибыльной, составляет около 90%, что значительно превосходит классический подход.

#Measuring our accuracy on our new target
res = []

model = LinearDiscriminantAnalysis()

res.append(np.mean(np.abs(cross_val_score(model,X,data['Classical Target'],cv=tscv,scoring='accuracy'))))

model = LinearDiscriminantAnalysis()

res.append(np.mean(np.abs(cross_val_score(model,X,data['Target 1'],cv=tscv,scoring='accuracy'))))

model = LinearDiscriminantAnalysis()

res.append(np.mean(np.abs(cross_val_score(model,X,data['Target 2'],cv=tscv,scoring='accuracy',n_jobs=-1))))

sns.barplot(res,color='black')
plt.xticks([0,1,2],['Classical Target','MA Cross Over Target','RSI Target'])
plt.axhline(res[0],linestyle=':',color='red')
plt.ylabel('5-Fold Percentage Accuracy %')
plt.title('Outperforming The Classical Target of Direct Price Prediction')

Рисунок 1: Улучшение по сравнению с классической задачей прямого прогнозирования цены за счет моделирования взаимосвязи между стратегией и рынком

Далее используем библиотеку случайного поиска из scikit-learn для построения нейронной сети на основе нашего набора рыночных данных. Сначала инициализируем нейросеть с параметрами по умолчанию - у нас это фиксированные параметры shuffle и early_stopping.

#Use random search to build a neural network for our market data

#Initialize the model
model = MLPRegressor(shuffle=False,early_stopping=False)


distributions = {'solver':['lbfgs','adam','sgd'],
                 'hidden_layer_sizes':[(X.shape[1],2,10,20),(X.shape[1],30,50,10),(X.shape[1],14,14,14),(X.shape[1],5,20,2),(X.shape[1],1,2,3,4,5,6,10),(X.shape[1],1,14,14,1)],
                 'activation':['relu','identity','logistic','tanh']
                 }

rscv = RandomizedSearchCV(model,distributions,n_jobs=-1,n_iter=50)

rscv.fit(X,data.loc[:,['Target 1','Target 2']])
Экспортируем обученную нейронную сеть в формат ONNX. Для начала загрузим библиотеку ONNX и необходимые конвертеры. ONNX (Open Neural Network Exchange) — это протокол с открытым исходным кодом, позволяющий удобно создавать и экспортировать модели машинного обучения в независимом от конкретной реализации формате.
#Exporting our model to ONNX 
import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

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

model = rscv.best_estimator_

model.fit(X,data.loc[:,['Target 1','Target 2']])

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

onnx.save(onnx_proto,'EURUSD NN MSA.onnx')

Чтобы можно было просмотреть граф ONNX нашей нейросети, сначала импортируем библиотеку Netron, а затем воспользуемся функцией netron.start, передав путь к модели ONNX. 

#Viewing our ONNX graph in netron
import netron
netron.start('../EURUSD NN MSA.onnx')

На рисунке 2 ниже представлены метасвойства нашей модели ONNX. Как видите, модель имеет 14 входных и 2 выходных параметра типа float. Кроме того, она содержит другую важную метаинформацию, такую как источник создания и версия ONNX.

Рисунок 2: Визуализация метаданных модели ONNX для проверки корректности размеров входных и выходных данных

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

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


Построение советника в MQL5

Первым шагом загрузим в советник модель ONNX, которую мы создали на предыдущем этапе.

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

//+------------------------------------------------------------------+
//| ONNX Model                                                       |
//+------------------------------------------------------------------+
#resource "\\Files\\EURUSD NN MSA.onnx" as uchar onnx_buffer[];

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

//+------------------------------------------------------------------+
//| ONNX Parameters                                                  |
//+------------------------------------------------------------------+
double Z1[] = { 1.18932220e+00,  1.19077958e+00,  1.18786462e+00,  1.18931542e+00,
                1.18994040e+00,  1.18994674e+00,  4.94395259e+01, -4.99204879e-04,
                -5.00701302e-04, -4.97575935e-04, -4.98995739e-04, -4.70848300e-04,
                -4.70289373e-04, -1.84697724e-02
                };

double Z2[] = {1.09599015e-01, 1.09698934e-01, 1.09479324e-01, 1.09593123e-01,
               1.09413744e-01, 1.09419007e-01, 1.00452009e+01, 1.31269558e-02,
               1.31336302e-02, 1.31513465e-02, 1.31174740e-02, 6.88794916e-03,
               6.89036979e-03, 1.28550006e+01
              };

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

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define MA_SHIFT         0
#define MA_TYPE          MODE_EMA
#define RSI_PRICE        PRICE_CLOSE
#define ONNX_INPUTS      14
#define ONNX_OUTPUTS     2
#define HORIZON 38 

Основные параметры стратегии (период скользящей средней и период RSI), также были подобраны генетическим оптимизатором, и мы также не будем их менять в ходе выполнения программы.

//+------------------------------------------------------------------+
//| Strategy Parameters                                              |
//+------------------------------------------------------------------+
int             MA_PERIOD                       =        100;  //Moving Average Period
int             RSI_PERIOD                      =         24;  //RSI Period
ENUM_TIMEFRAMES STRATEGY_TIME_FRAME             =  PERIOD_H3;  //Strategy Timeframe
int             HOLDING_PERIOD                  =         38;  //Position Maturity Period

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

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

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

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
//--- Custom Types
CTrade               Trade;
Time                 *TradeTime;
TradeInfo            *TradeInformation;
RSIMidPoint          *RSIMid;
OpenCloseMACrossover *MACross;
long                 onnx_model;
vectorf onnx_output;

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

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

//--- System Types
int                  position_timer;

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Create dynamic instances of our custom types
   TradeTime        = new Time(Symbol(),STRATEGY_TIME_FRAME);
   TradeInformation = new TradeInfo(Symbol(),STRATEGY_TIME_FRAME);
   MACross          = new OpenCloseMACrossover(Symbol(),STRATEGY_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE);
   RSIMid           = new RSIMidPoint(Symbol(),STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE);
   onnx_model       = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT);
   onnx_output      = vectorf::Zeros(ONNX_OUTPUTS);

//---Setup our technical indicators
   ma_handle        = iMA(_Symbol,STRATEGY_TIME_FRAME,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE);
   ma_o_handle      = iMA(_Symbol,STRATEGY_TIME_FRAME,MA_PERIOD,0,MA_TYPE,PRICE_OPEN);
   rsi_handle       = iRSI(_Symbol,STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE);
   if(onnx_model != INVALID_HANDLE)
     {
      Print("Preparing ONNX model");

      ulong input_shape[] = {1,ONNX_INPUTS};

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

      ulong output_shape[] = {ONNX_OUTPUTS,1};

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

//--- Everything was fine
   Print("Successfully loaded all components for our Expert Advisor");
   return(INIT_SUCCEEDED);
  }
//--- End of OnInit Scope

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

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

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

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

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

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

      //--- Otherwise
      else
        {
         //--- The position has reached maturity
         if(position_timer == HOLDING_PERIOD)
            Trade.PositionClose(Symbol());
         //--- Otherwise keep holding
         else
            position_timer++;
        }
     }
  }
//--- End of OnTick Scope

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

//+------------------------------------------------------------------+
//| Update our technical indicators                                  |
//+------------------------------------------------------------------+
void Update(void)
  {
   int fetch = (HORIZON * 2);

//--- Update the strategy
   RSIMid.Update();
   MACross.Update();

//---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(rsi_handle,0,0,fetch,rsi_reading);
   ArraySetAsSeries(rsi_reading,true);
  }
//--- End of Update Scope

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

//+------------------------------------------------------------------+
//| Get A Prediction from our ONNX model                             |
//+------------------------------------------------------------------+
void OnnxPredict(void)
  {
   vectorf   input_variables =
     {
      iOpen(_Symbol,STRATEGY_TIME_FRAME,0),
      iHigh(_Symbol,STRATEGY_TIME_FRAME,0),
      iLow(_Symbol,STRATEGY_TIME_FRAME,0),
      iClose(_Symbol,STRATEGY_TIME_FRAME,0),
      ma_reading[0],
      ma_o_reading[0],
      rsi_reading[0],
      iOpen(_Symbol,STRATEGY_TIME_FRAME,0)   - iOpen(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)),
      iHigh(_Symbol,STRATEGY_TIME_FRAME,0)   - iHigh(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)),
      iLow(_Symbol,STRATEGY_TIME_FRAME,0)    - iLow(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)),
      iClose(_Symbol,STRATEGY_TIME_FRAME,0)  - iClose(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)),
      ma_reading[0] - ma_reading[(0 + HORIZON)],
      ma_o_reading[0] - ma_o_reading[(0 + HORIZON)],
      rsi_reading[0] - rsi_reading[(0 + HORIZON)]
     };

   for(int i = 0; i < ONNX_INPUTS;i++)
     {
      input_variables[i] = ((input_variables[i] - Z1[i])/ Z2[i]);
     }

   OnnxRun(onnx_model,ONNX_DEFAULT,input_variables,onnx_output);
  }

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

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

   OnnxPredict();

//--- MA Strategy is profitable
   if((onnx_output[0] > 0.5) && (onnx_output[1] < 0.5))
     {
      //--- Long positions when the close moving average is above the open
      if(MACross.BuySignal())
        {
         Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
         return;
        }

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

//--- RSI strategy is profitable
   else
      if((onnx_output[0] < 0.5) && (onnx_output[1] > 0.5))
        {

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

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

В завершении мы снимаем определение всех системных констант, определенных в начале программы (undefine).

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

Выбираем даты для тестирования на истории достаточно просто. Нам нужно протестировать новое приложение на том же периоде форвард-тестирования, который мы использовали ранее. Отсюда мы и выбирали даты тестирования.

Рисунок 4: Выбор дат для бэктеста с учетом предыдущего форвард-тестирования

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

Рисунок 5: Random delay для реалистичного тестирования

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

Рисунок 6: Новая кривая доходности демонстрирует ухудшение по сравнению с результатами без статистического моделирования

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

Рисунок 7: Детальный анализ новой торговой стратегии, основанной на статистических моделях


Пересмотр рыночных данных в Python

После дополнительного анализа рыночных данных в Python я решил более внимательно изучить возможные источники ошибки. Для этого я начал с импорта стандартных библиотек визуализации данных и построил график накопленной суммы доходностей, полученных двумя стратегиями. Проблема стала очевидной сразу.
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

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

fig , axs = plt.subplots(2,1,sharex=True)

fig.suptitle('Visualizing The Individual Cumulative Return of our 2 Strategies')

sns.lineplot(data['Return 1'].cumsum(),ax=axs[0],color='black')

sns.lineplot(data['Return 2'].cumsum(),ax=axs[1],color='black')

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

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

fig , axs = plt.subplots(3,1,sharex=True)

fig.suptitle('Visualizing The Risk In Our 2 Strategies')

sns.lineplot(data['Return'].rolling(window=HORIZON).var(),ax=axs[0],color='black')
axs[0].axhline(data['Return'].var(),color='red',linestyle=':')

sns.lineplot(data['Return 1'].rolling(window=HORIZON).var(),ax=axs[1],color='black')
axs[1].axhline(data['Return 1'].var(),color='red',linestyle=':')

sns.lineplot(data['Return 2'].rolling(window=HORIZON).var(),ax=axs[2],color='black')
axs[2].axhline(data['Return 2'].var(),color='red',linestyle=':')

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

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

plt.title('Correlation Between Market Return And Strategy Returns')
sns.heatmap(data.loc[:,['Return','Return 1','Return 2']].corr(),annot=True)

Рисунок 10: Корреляционная матрица торговых стратегий на EURUSD


Начинаем улучшения

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

Рисунок 11: Выбор дат бэктеста для оптимизации

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

Рисунок 12: Обязательно включаем режим Random Delay

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

Рисунок 13: На этот раз фиксируем веса всех голосов на уровне 1, поскольку предполагаем, что генетический оптимизатор хитрит

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

Рисунок 14: Результаты оптимизации значительно улучшились по сравнению с предыдущей итерацией

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

Рисунок 15: Результаты форвард-теста демонстрируют явные признаки стабильности и прибыльности

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

Рисунок 16: Кривая доходности, полученная с фиксированными весами, значительно более прибыльна


Заключение

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

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

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

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

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

Название файла Описание файла
Fetch Data MSA.mq5 Скрипт MQL5 для получения данных по двум стратегиям, выбранным генетическим оптимизатором.
MSA Test 2.1.mq5 Созданный советник на основе двух выбранных стратегий и модели ONNX.
Analyzing Multiple Strategies I.ipynb Блокнот Jupyter для анализа рыночных данных, полученных с помощью нашего скрипта MQL5.
EURUSD NN MSA.onnx Глубокая нейронная сеть, которую мы построили, используя библиотеку случайного поиска sklearn.

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

Прикрепленные файлы |
Fetch_Data_MSA.mq5 (3.46 KB)
MSA_Test_2.1.mq5 (10.97 KB)
EURUSD_NN_MSA.onnx (3.42 KB)
Торговые инструменты на MQL5 (Часть 11):  Панель корреляционной матрицы (Пирсон, Спирман, Кенделл) с тепловой картой и стандартным режимом Торговые инструменты на MQL5 (Часть 11): Панель корреляционной матрицы (Пирсон, Спирман, Кенделл) с тепловой картой и стандартным режимом
В этой статье мы создаем панель мониторинга корреляционной матрицы в MQL5 для вычисления взаимосвязей между активами с использованием методов Пирсона (Pearson), Спирмена (Spearman) и Кенделла (Kendall) за заданный таймфрейм и количество баров. Система предлагает стандартный режим с цветовыми порогами и звездочками p-значений, а также режим тепловой карты с градиентными визуальными элементами силы корреляции. Он включает в себя интерактивный пользовательский интерфейс с селекторами таймфреймов, переключателями режимов и динамической легендой для эффективного анализа взаимозависимостей символов.
Реализация частичного закрытия позиций в MQL5 Реализация частичного закрытия позиций в MQL5
В статье разрабатывается класс для управления частичным закрытием позиций в MQL5 с последующей интеграцией в советника Order Blocks. Кроме того, представлены результаты тестирования, сравнивающие стратегию с использованием частичных закрытий и без них, а также анализ того, при каких условиях их использование может обеспечивать и максимизировать прибыль. В заключение делается вывод, что в торговых стратегиях, особенно ориентированных на более широкие ценовые движения, использование частичных закрытий может быть довольно выгодным.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Автоматизация торговых стратегий на MQL5 (Часть 23): Зональное восстановление с трейлинг-стопом и логикой корзин Автоматизация торговых стратегий на MQL5 (Часть 23): Зональное восстановление с трейлинг-стопом и логикой корзин
В этой статье мы усовершенствуем нашу систему зонального восстановления (Zone Recovery System), внедрив трейлинг-стопы и возможности торговли несколькими корзинами. Мы исследуем, как усовершенствованная архитектура использует динамические трейлинг-стопы для фиксации прибыли и систему управления корзинами для эффективной обработки множества торговых сигналов. В ходе реализации и тестирования на истории мы продемонстрируем более надежную торговую систему, приспособленную к адаптивным рыночным условиям.