Пример ансамбля ONNX-моделей в MQL5
Введение
Для стабильной торговли желательно иметь диверсификацию как по торгуемым инструментам, так и по торговым стратегиям. Такая же ситуация и с моделями машинного обучения — проще создать несколько более простых моделей, чем одну сложную. Но собрать потом эти модели в одну ONNX-модель может быть непросто.
Однако можно комбинировать использование нескольких уже обученных ONNX-моделей в одной MQL5-программе. Мы покажем, как можно просто реализовать один из ансамблей, который называется классификатор голосования.
Использованные модели
Для нашего примера мы взяли 2 простые модели - регрессионная модель предсказания цены и классификационная модель предсказания движения цены. Главное отличие моделей друг от друга - регрессия предсказывает количество, классификация предсказывает метку (класс).
Первая модель - регрессионная.
Обучение проводится на EURUSD D1 c 2003 года по конец 2022 года. Для обучения используются серии из 10 цен OHLC. Для повышения обучаемости модели цены нормируются, производится деление отклонения от средней цены в серии на стандартное отклонение в серии. Это позволяет уложить серию в некий диапазон, у которого среднее 0 и разброс 1, что улучшает сходимость при обучении.
В результате должна быть спрогнозирована цена закрытия следующего дня.
Модель очень простая и была сделана только в качестве иллюстрации.
# Copyright 2023, MetaQuotes Ltd. # https://www.mql5.com from datetime import datetime import MetaTrader5 as mt5 import tensorflow as tf import numpy as np import pandas as pd import tf2onnx from sklearn.model_selection import train_test_split from tqdm import tqdm from sys import argv if not mt5.initialize(): print("initialize() failed, error code =",mt5.last_error()) quit() # we will save generated onnx-file near the our script data_path=argv[0] last_index=data_path.rfind("\\")+1 data_path=data_path[0:last_index] print("data path to save onnx model",data_path) # input parameters inp_model_name = "model.eurusd.D1.10.onnx" inp_history_size = 10 inp_start_date = datetime(2003, 1, 1, 0) inp_end_date = datetime(2023, 1, 1, 0) # get data from client terminal eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_D1, inp_start_date, inp_end_date) df = pd.DataFrame(eurusd_rates) # # collect dataset subroutine # def collect_dataset(df: pd.DataFrame, history_size: int): """ Collect dataset for the following regression problem: - input: history_size consecutive H1 bars; - output: close price for the next bar. :param df: D1 bars for a range of dates :param history_size: how many bars should be considered for making a prediction :return: features and labels """ n = len(df) xs = [] ys = [] for i in tqdm(range(n - history_size)): w = df.iloc[i: i + history_size + 1] x = w[['open', 'high', 'low', 'close']].iloc[:-1].values y = w.iloc[-1]['close'] xs.append(x) ys.append(y) X = np.array(xs) y = np.array(ys) return X, y ### # get prices X, y = collect_dataset(df, history_size=inp_history_size) # normalize prices m = X.mean(axis=1, keepdims=True) s = X.std(axis=1, keepdims=True) X_norm = (X - m) / s y_norm = (y - m[:, 0, 3]) / s[:, 0, 3] # split data to train and test sets X_train, X_test, y_train, y_test = train_test_split(X_norm, y_norm, test_size=0.2, random_state=0) # define model model = tf.keras.Sequential([ tf.keras.layers.LSTM(64, input_shape=(inp_history_size, 4)), tf.keras.layers.BatchNormalization(), tf.keras.layers.Dropout(0.1), tf.keras.layers.Dense(32, activation='relu'), tf.keras.layers.BatchNormalization(), tf.keras.layers.Dropout(0.1), tf.keras.layers.Dense(32, activation='relu'), tf.keras.layers.Dense(1) ]) model.compile(optimizer='adam', loss='mse', metrics=['mae']) # model training for 50 epochs lr_reduction = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, min_lr=0.000001) history = model.fit(X_train, y_train, epochs=50, verbose=2, validation_split=0.15, callbacks=[lr_reduction]) # model evaluation test_loss, test_mae = model.evaluate(X_test, y_test) print(f"test_loss={test_loss:.3f}") print(f"test_mae={test_mae:.3f}") # save model to onnx output_path = data_path+inp_model_name onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path) print(f"saved model to {output_path}") # finish mt5.shutdown()Предполагается, что при исполнении нашей регрессионной модели полученная спрогнозированная цена должна быть преобразована в класс "цена идёт вниз, цена не меняется, цена идёт вверх". Это необходимо для организации классификатора голосования.
Вторая модель — классификационная.
Обучение проводится на EURUSD D1 с 2010 года по конец 2022 года. Для обучения используются серии из 63 цен Close. На выходе должен быть определён один из трёх классов — цена пойдёт вниз, цена останется в пределах 10 пунктов или цена пойдёт вверх. Именно из-за второго класса мы обучаем на данных, начиная с 2010 года, так как в 2009 году был переход с точности 4 знака на точность 5 знаков. Таким образом, один "старый" пункт стал десятью "новыми" пунктами.
Как и в предыдущей модели, цена нормируется. Нормируется таким же самым способом — делением отклонения от средней цены в серии на стандартное отклонение в серии. Идея данной модели подсмотрена в статье "Прогнозирование финансовых временных рядов с MLP в Keras". Модель является чисто демонстрационной, как и предыдущая модель.
# Copyright 2023, MetaQuotes Ltd. # https://www.mql5.com # # Classification model # 0,0,1 - predict price down # 0,1,0 - predict price same # 1,0,0 - predict price up # from datetime import datetime import MetaTrader5 as mt5 import tensorflow as tf import numpy as np import pandas as pd import tf2onnx from sklearn.model_selection import train_test_split from tqdm import tqdm from keras.models import Sequential from keras.layers import Dense, Activation,Dropout, BatchNormalization, LeakyReLU from keras.optimizers import SGD from keras import regularizers from sys import argv # initialize MetaTrader 5 client terminal if not mt5.initialize(): print("initialize() failed, error code =",mt5.last_error()) quit() # we will save the generated onnx-file near the our script data_path=argv[0] last_index=data_path.rfind("\\")+1 data_path=data_path[0:last_index] print("data path to save onnx model",data_path) # input parameters inp_model_name = "model.eurusd.D1.63.onnx" inp_history_size = 63 inp_start_date = datetime(2010, 1, 1, 0) inp_end_date = datetime(2023, 1, 1, 0) # get data from the client terminal eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_D1, inp_start_date, inp_end_date) df = pd.DataFrame(eurusd_rates) # # collect dataset subroutine # def collect_dataset(df: pd.DataFrame, history_size: int): """ Collect dataset for the following regression problem: - input: history_size consecutive H1 bars; - output: close price for the next bar. :param df: H1 bars for a range of dates :param history_size: how many bars should be considered for making a prediction :return: features and labels """ n = len(df) xs = [] ys = [] for i in tqdm(range(n - history_size)): w = df.iloc[i: i + history_size + 1] x = w[['close']].iloc[:-1].values delta = x[-1] - w.iloc[-1]['close'] if np.abs(delta)<=0.0001: y = 0, 1, 0 else: if delta<0: y = 1, 0, 0 else: y = 0, 0, 1 xs.append(x) ys.append(y) X = np.array(xs) Y = np.array(ys) return X, Y ### # get prices X, Y = collect_dataset(df, history_size=inp_history_size) # normalize prices m = X.mean(axis=1, keepdims=True) s = X.std(axis=1, keepdims=True) X_norm = (X - m) / s # split data to train and test sets X_train, X_test, Y_train, Y_test = train_test_split(X_norm, Y, test_size=0.1, random_state=0) # define model model = Sequential() model.add(Dense(64, input_dim=inp_history_size, activity_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization()) model.add(LeakyReLU()) model.add(Dropout(0.3)) model.add(Dense(16, activity_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization()) model.add(LeakyReLU()) model.add(Dense(3)) model.add(Activation('softmax')) opt = SGD(learning_rate=0.01, momentum=0.9) model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy']) # model training for 300 epochs lr_reduction = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.9, patience=5, min_lr=0.00001) history = model.fit(X_train, Y_train, epochs=300, validation_data=(X_test, Y_test), shuffle = True, batch_size=128, verbose=2, callbacks=[lr_reduction]) # model evaluation test_loss, test_accuracy = model.evaluate(X_test, Y_test) print(f"test_loss={test_loss:.3f}") print(f"test_accuracy={test_accuracy:.3f}") # save model to onnx output_path = data_path+inp_model_name onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path) print(f"saved model to {output_path}") # finish mt5.shutdown()Представленные модели обучались на данных до конца 2022 года, чтобы можно было продемонстрировать работу моделей в тестере стратегий.
Ансамбль ONNX-моделей в MQL5-эксперте
Представляем простой эксперт для демонстрации возможностей ансамблирования моделей. Основные принципы использования ONNX-моделей в MQL5 были описаны во второй части одноимённой статьи.
Предварительные объявления и определения
#include <Trade\Trade.mqh> input double InpLots = 1.0; // Lots amount to open position #resource "Python/model.eurusd.D1.10.onnx" as uchar ExtModel1[] #resource "Python/model.eurusd.D1.63.onnx" as uchar ExtModel2[] #define SAMPLE_SIZE1 10 #define SAMPLE_SIZE2 63 long ExtHandle1=INVALID_HANDLE; long ExtHandle2=INVALID_HANDLE; int ExtPredictedClass1=-1; int ExtPredictedClass2=-1; int ExtPredictedClass=-1; datetime ExtNextBar=0; CTrade ExtTrade; //--- price movement prediction #define PRICE_UP 0 #define PRICE_SAME 1 #define PRICE_DOWN 2
Функция OnInit
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if(_Symbol!="EURUSD" || _Period!=PERIOD_D1) { Print("model must work with EURUSD,D1"); return(INIT_FAILED); } //--- create first model from static buffer ExtHandle1=OnnxCreateFromBuffer(ExtModel1,ONNX_DEFAULT); if(ExtHandle1==INVALID_HANDLE) { Print("First model OnnxCreateFromBuffer error ",GetLastError()); return(INIT_FAILED); } //--- since not all sizes defined in the input tensor we must set them explicitly //--- first index - batch size, second index - series size, third index - number of series (OHLC) const long input_shape1[] = {1,SAMPLE_SIZE1,4}; if(!OnnxSetInputShape(ExtHandle1,0,input_shape1)) { Print("First model OnnxSetInputShape error ",GetLastError()); return(INIT_FAILED); } //--- since not all sizes defined in the output tensor we must set them explicitly //--- first index - batch size, must match the batch size of the input tensor //--- second index - number of predicted prices (we only predict Close) const long output_shape1[] = {1,1}; if(!OnnxSetOutputShape(ExtHandle1,0,output_shape1)) { Print("First model OnnxSetOutputShape error ",GetLastError()); return(INIT_FAILED); } //--- create second model from static buffer ExtHandle2=OnnxCreateFromBuffer(ExtModel2,ONNX_DEFAULT); if(ExtHandle2==INVALID_HANDLE) { Print("Second model OnnxCreateFromBuffer error ",GetLastError()); return(INIT_FAILED); } //--- since not all sizes defined in the input tensor we must set them explicitly //--- first index - batch size, second index - series size const long input_shape2[] = {1,SAMPLE_SIZE2}; if(!OnnxSetInputShape(ExtHandle2,0,input_shape2)) { Print("Second model OnnxSetInputShape error ",GetLastError()); return(INIT_FAILED); } //--- since not all sizes defined in the output tensor we must set them explicitly //--- first index - batch size, must match the batch size of the input tensor //--- second index - number of classes (up, same or down) const long output_shape2[] = {1,3}; if(!OnnxSetOutputShape(ExtHandle2,0,output_shape2)) { Print("Second model OnnxSetOutputShape error ",GetLastError()); return(INIT_FAILED); } //--- ok return(INIT_SUCCEEDED); }
Работаем только с EURUSD,D1. Просто потому, что используем данные текущего символа-периода при том, что модели обучены именно на дневных ценах.
Обе модели включены в эксперт в виде ресурсов.
Обязательно явно определяем формы входных и выходных данных.
Функция OnTick
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- check new bar if(TimeCurrent()<ExtNextBar) return; //--- set next bar time ExtNextBar=TimeCurrent(); ExtNextBar-=ExtNextBar%PeriodSeconds(); ExtNextBar+=PeriodSeconds(); //--- predict price movement Predict(); //--- check trading according to prediction if(ExtPredictedClass>=0) if(PositionSelect(_Symbol)) CheckForClose(); else CheckForOpen(); }
Все торговые операции производим только в начале дня.
Функция предсказания
//+------------------------------------------------------------------+ //| Voting classification | //+------------------------------------------------------------------+ void Predict(void) { //--- evaluate first model ExtPredictedClass1=PredictPrice(ExtHandle1,SAMPLE_SIZE1); //--- evaluate second model ExtPredictedClass2=PredictPriceMovement(ExtHandle2,SAMPLE_SIZE2); //--- vote if(ExtPredictedClass1==ExtPredictedClass2) ExtPredictedClass=ExtPredictedClass1; else ExtPredictedClass=-1; }
Класс считается выбранным, когда обе модели получили один и тот же класс. Голосование большинством. А так как в ансамбле всего две модели, то голосование большинством означает "единогласно".
Предсказание цены закрытия дня по 10 предыдущим ценам OHLC
//+------------------------------------------------------------------+ //| Predict next price (first model) | //+------------------------------------------------------------------+ int PredictPrice(const long handle,const int sample_size) { static matrixf input_data(sample_size,4); // matrix for prepared input data static vectorf output_data(1); // vector to get result static matrix mm(sample_size,4); // matrix of horizontal vectors Mean static matrix ms(sample_size,4); // matrix of horizontal vectors Std static matrix x_norm(sample_size,4); // matrix for prices normalize //--- prepare input data matrix rates; //--- request last bars if(!rates.CopyRates(_Symbol,_Period,COPY_RATES_OHLC,1,sample_size)) return(-1); //--- get series Mean vector m=rates.Mean(1); //--- get series Std vector s=rates.Std(1); //--- prepare matrices for prices normalization for(int i=0; i<sample_size; i++) { mm.Row(m,i); ms.Row(s,i); } //--- the input of the model must be a set of vertical OHLC vectors x_norm=rates.Transpose(); //--- normalize prices x_norm-=mm; x_norm/=ms; //--- run the inference input_data.Assign(x_norm); if(!OnnxRun(handle,ONNX_NO_CONVERSION,input_data,output_data)) return(-1); //--- denormalize the price from the output value double predicted=output_data[0]*s[3]+m[3]; //--- classify predicted price movement int predicted_class=-1; double delta=rates[3][sample_size-1]-predicted; if(fabs(delta)<=0.0001) predicted_class=PRICE_SAME; else { if(delta<0) predicted_class=PRICE_UP; else predicted_class=PRICE_DOWN; } return(predicted_class); }
Входные данные готовятся по тем же самым правилам, что и при обучении модели. После исполнения модели полученное значение обратно преобразуется в цену. На основании разницы между последней в серии цены Close и полученной цены вычисляется класс.
Предсказание движения цены на основании серии из 63 дневных Close.
//+------------------------------------------------------------------+ //| Predict price movement (second model) | //+------------------------------------------------------------------+ int PredictPriceMovement(const long handle,const int sample_size) { static vectorf input_data(sample_size); // vector for prepared input data static vectorf output_data(3); // vector to get result //--- request last bars if(!input_data.CopyRates(_Symbol,_Period,COPY_RATES_CLOSE,1,sample_size)) return(-1); //--- get series Mean float m=input_data.Mean(); //--- get series Std float s=input_data.Std(); //--- normalize prices input_data-=m; input_data/=s; //--- run the inference if(!OnnxRun(handle,ONNX_NO_CONVERSION,input_data,output_data)) return(-1); //--- evaluate prediction return(int(output_data.ArgMax())); }
Нормирование цен производится по тем же правилам, что и в первой модели. Но код получился компактнее из-за того, входом является вектор, а не матрица. Класс выбирается по максимальному значению из трёх вероятностей.
Торговая стратегия проста. Торговые операции совершаются в начале каждого дня. Если прогноз "цена пойдёт вверх", то покупаем, если "цена пойдёт вниз" - продаём.
//+------------------------------------------------------------------+ //| Check for open position conditions | //+------------------------------------------------------------------+ void CheckForOpen(void) { ENUM_ORDER_TYPE signal=WRONG_VALUE; //--- check signals if(ExtPredictedClass==PRICE_DOWN) signal=ORDER_TYPE_SELL; // sell condition else { if(ExtPredictedClass==PRICE_UP) signal=ORDER_TYPE_BUY; // buy condition } //--- open position if possible according to signal if(signal!=WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) ExtTrade.PositionOpen(_Symbol,signal,InpLots, SymbolInfoDouble(_Symbol,signal==ORDER_TYPE_SELL ? SYMBOL_BID:SYMBOL_ASK), 0,0); } //+------------------------------------------------------------------+ //| Check for close position conditions | //+------------------------------------------------------------------+ void CheckForClose(void) { bool bsignal=false; //--- position already selected before long type=PositionGetInteger(POSITION_TYPE); //--- check signals if(type==POSITION_TYPE_BUY && ExtPredictedClass==PRICE_DOWN) bsignal=true; if(type==POSITION_TYPE_SELL && ExtPredictedClass==PRICE_UP) bsignal=true; //--- close position if possible if(bsignal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { ExtTrade.PositionClose(_Symbol,3); //--- open opposite CheckForOpen(); } }
Мы обучали наши модели на данных до начала 2023 года. Поэтому устанавливаем интервал тестирования с начала года.
Вот результат тестирования с начала года.
Интересно было бы знать результаты тестирования каждой из этих моделей по отдельности.
Для этого модифицируем исходный код эксперта
enum EnModels { USE_FIRST_MODEL, // Use first model only USE_SECOND_MODEL, // Use second model only USE_BOTH_MODELS // Use both models }; input EnModels InpModels = USE_BOTH_MODELS; // Models using input double InpLots = 1.0; // Lots amount to open position ... //+------------------------------------------------------------------+ //| Voting classification | //+------------------------------------------------------------------+ void Predict(void) { //--- evaluate first model if(InpModels==USE_BOTH_MODELS || InpModels==USE_FIRST_MODEL) ExtPredictedClass1=PredictPrice(ExtHandle1,SAMPLE_SIZE1); //--- evaluate second model if(InpModels==USE_BOTH_MODELS || InpModels==USE_SECOND_MODEL) ExtPredictedClass2=PredictPriceMovement(ExtHandle2,SAMPLE_SIZE2); //--- check predictions switch(InpModels) { case USE_FIRST_MODEL : ExtPredictedClass=ExtPredictedClass1; break; case USE_SECOND_MODEL : ExtPredictedClass=ExtPredictedClass2; break; case USE_BOTH_MODELS : if(ExtPredictedClass1==ExtPredictedClass2) ExtPredictedClass=ExtPredictedClass1; else ExtPredictedClass=-1; } }
Выставим настройку "использовать только первую модель".
Результаты тестирования первой модели
Теперь протестируем только вторую модель. Вот результаты тестирования второй модели.
Вторая модель оказалась гораздо сильнее первой модели. Результаты подтверждают теорию — ансамблировать нужно слабые модели. Но в данной статье речь шла не о теории ансамблирования, а о практике применения.
Важно: обращаем ваше внимание, что использованные в статье модели представлены только в целях демонстрации работы с ONNX-моделями средствами языка MQL5. Советник не предназначен для торговли на реальных счетах.
Заключение
Мы представили очень простой и наглядный пример ансамбля двух ONNX-моделей. Количество одновременно используемых моделей ограничено и не может превашать 256 моделей. Но даже использование более чем двух моделей потребует иного подхода к программированию таких экспертов, а именно — объектно-ориентированного.
Но это уже тема другой статьи.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Загадочное число 63 :)
Загадочное число 63 :)
Это число взято из другой статьи про загадочные числа. Ссылку не найду