Переосмысливаем классические стратегии (Часть 14): Анализ нескольких стратегий
В параллельной серии статей о самооптимизирующихся экспертных советниках мы решили создать ансамбль из нескольких стратегий и объединить их в одну, более мощную стратегию.
Там мы определили, что стратегии будут работать совместно в форме своеобразной демократии, где каждая стратегия имеет один голос. Вес каждого голоса стал настраиваемым параметром, который, как мы обсуждали ранее, настраивался генетическим оптимизатором для максимизации прибыльности торговой стратегии. Затем мы исключили стратегию, получившую наименьший вес от генетического оптимизатора, оставив две стратегии, которые теперь будем анализировать и на основе которых построим статистические модели.
Мы также получили рыночные данные с помощью 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Торговые инструменты на MQL5 (Часть 11): Панель корреляционной матрицы (Пирсон, Спирман, Кенделл) с тепловой картой и стандартным режимом
Реализация частичного закрытия позиций в MQL5
Автоматизация торговых стратегий на MQL5 (Часть 23): Зональное восстановление с трейлинг-стопом и логикой корзин
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования