Преодоление ограничений машинного обучения (Часть 7): Автоматический выбор стратегии
Когда большинство трейдеров начинают свой путь, им рекомендуется выбирать стратегии, соответствующие их профилю риска. Хотя это разумно, в этой статье мы утверждаем, что торговые стратегии следует прежде всего подбирать исходя из ожидаемой результативности, а не из идеализированных предпочтений. Определение прибыльных стратегий - универсальная задача для алгоритмических трейдеров, независимо от их опыта. Сложность усугубляется постоянным потоком новых стратегий, индикаторов и советников, появляющихся в нашем быстро растущем глобальном сообществе алгоритмических трейдеров.
Мы живем в эпоху беспрецедентной взаимосвязанности — информационной революции. Но что происходит, когда новые идеи появляются и распространяются быстрее, чем любой трейдер может их оценить? Учитывая бесчисленное множество возможных стратегий, как мы можем автоматически определить короткий список, который, по нашему мнению, стоит протестировать? Можем ли мы обнаружить потенциально прибыльные конфигурации стратегий, не прибегая к грубому перебору всех возможных комбинаций?
В статье предлагается методологическая схема для решения этих вопросов с помощью двух взаимодополняющих подходов:
- Решение "белого ящика": Использовать матричную факторизацию — в частности, сингулярное разложение (SVD) — ожидаемой доходности, чтобы определить комбинации стратегий, на которые положительно влияют текущие рыночные условия.
- Решение "чёрного ящика": Использовать глубокие нейронные сети для динамического выбора стратегий, основанных на наблюдаемом поведении рынка.
Наше решение основано на нашей возможности оценить прибыль, которую можно было бы получить, следуя имеющимся торговым стратегиям. Затем используем своё понимание численных вычислений, чтобы оценить ожидаемые потоки доходности наших стратегий. Приблизительный расчет доходности, получаемой с помощью любой указанной стратегии, дает ценную информацию.
Получение нужных нам данных
Для начала мы напишем 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Возможности Мастера MQL5, которые вам нужно знать (Часть 73): Использование паттернов Ишимоку и ADX-Wilder
Оптимизатор конкурирующего роя — Competitive Swarm Optimizer (CSO)
Нейросети в трейдинге: Оптимизация Cross-Attention для анализа длинных последовательностей рынка (Окончание)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования