
Un esempio di come assemblare i modelli ONNX in MQL5
Introduzione
Per un trading stabile, di solito si raccomanda di diversificare sia gli strumenti negoziati che le strategie di trading. Lo stesso vale per i modelli di apprendimento automatico: è più facile creare diversi modelli semplici che uno complesso. Ma può essere difficile assemblare questi modelli in un unico modello ONNX.
Tuttavia, è possibile combinare diversi modelli ONNX addestrati in un unico programma MQL5. In questo articolo prenderemo in considerazione uno degli assemblaggi chiamato classificatore di voto. Vi mostreremo come sia facile implementare un tale assemblaggio.
Modelli per il progetto
Per il nostro esempio, utilizzeremo due semplici modelli: un modello di previsione dei prezzi a regressione e un modello di previsione del movimento dei prezzi a classificazione. La differenza principale tra i modelli è che la regressione prevede la quantità, mentre la classificazione prevede la classe.
Il primo modello è quello della regressione.
Viene addestrato utilizzando i dati EURUSD D1 dal 2003 alla fine del 2022. L'addestramento viene effettuato utilizzando serie di 10 prezzi OHLC. Per migliorare l'addestrabilità del modello, normalizziamo i prezzi e dividiamo il prezzo medio della serie per la deviazione standard della serie. In questo modo, mettiamo una serie in un certo intervallo con una media di 0 e uno spread di 1, che migliora la convergenza durante l'addestramento.
Di conseguenza, il modello dovrebbe prevedere il prezzo di chiusura del giorno successivo.
Il modello è molto semplice. Viene fornito solo a scopo dimostrativo.
# 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()Si presume che il nostro modello di regressione venga eseguito, il prezzo previsto risultante dovrebbe essere trasformato nelle seguenti classi: il prezzo scende, il prezzo non cambia, il prezzo sale. Ciò è necessario in modo da organizzare il classificatore del voto.
Il secondo modello è quello di classificazione.
È stato addestrato su EURUSD D1 dal 2010 alla fine del 2022. L'addestramento viene effettuato utilizzando una serie di 63 prezzi di chiusura. All'uscita deve essere definita una delle tre classi: il prezzo scenderà, il prezzo rimarrà entro 10 punti o il prezzo salirà. È a causa della seconda classe che abbiamo dovuto addestrare il modello utilizzando i dati a partire dal 2010 - prima di allora, nel 2009, i mercati sono passati da una precisione a 4 cifre a una a 5 cifre. Così, un vecchio punto è diventato dieci nuovi punti.
Come nel modello precedente, il prezzo è normalizzato. La normalizzazione è la stessa: dividiamo la deviazione dal prezzo medio della serie per la deviazione standard della serie. L'idea di questo modello è stata descritta nell'articolo “Financial timeseries forecasting with MLP in Keras" (in Russo). Anche questo modello è stato progettato solo a scopo dimostrativo.
# 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()I modelli sono stati addestrati con dati fino alla fine del 2022, lasciando così il periodo per dimostrare il loro funzionamento nel tester di strategie.
Un Assemblaggio di Modelli ONNX nell’Expert Advisor MQL5
Di seguito viene presentato un semplice Expert Advisor per dimostrare le possibilità di assemblaggio dei modelli. I principi principali dell'uso dei modelli ONNX in MQL5 sono stati descritti nella seconda parte dell'articolo precedente.
Dichiarazioni e definizioni avanzate
#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
Funzione 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); }
Lo eseguiremo solo con EURUSD, D1. Questo perché utilizziamo i dati del simbolo/periodo corrente, mentre i modelli vengono addestrati utilizzando i prezzi giornalieri.
I modelli sono inclusi nell'Expert Advisor come risorse.
È importante definire esplicitamente le forme dei dati di ingresso e di uscita.
Funzione 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(); }
Tutte le operazioni di trading vengono effettuate solo all'inizio della giornata.
Funzione di previsione
//+------------------------------------------------------------------+ //| 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; }
Una classe si considera selezionata quando entrambi i modelli hanno ricevuto la stessa classe. Si tratta di una votazione a maggioranza. E poiché ci sono solo due modelli nel gruppo, il voto a maggioranza significa "unanime".
Previsione del prezzo di chiusura di giornata dai 10 prezzi OHLC precedenti
//+------------------------------------------------------------------+ //| 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); }
I dati di input devono essere preparati seguendo le stesse regole che valgono per l'addestramento del modello. Dopo l'esecuzione del modello, il valore risultante viene riconvertito in prezzo. La classe viene calcolata in base alla differenza tra l'ultimo prezzo di chiusura della serie e il prezzo risultante.
La previsione del movimento dei prezzi si basa su una serie di 63 prezzi di chiusura giornalieri:
//+------------------------------------------------------------------+ //| 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())); }
I prezzi sono normalizzati secondo le stesse regole del primo modello. Tuttavia, questa volta il codice è più compatto perché l'input è un vettore e non una matrice. La classe viene selezionata in base al valore massimo delle tre probabilità.
La strategia di trading è semplice. Le operazioni di trading vengono effettuate all'inizio di ogni giornata. Se la previsione è "il prezzo salirà", compriamo; se è "il prezzo scenderà", vendiamo.
//+------------------------------------------------------------------+ //| 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(); } }
Abbiamo addestrato il nostro modello con i dati fino all'inizio del 2023. Quindi, impostiamo l'intervallo del test dall'inizio dell'anno.
Ecco i risultati dei test basati sui dati dall'inizio dell'anno.
Sarebbe interessante conoscere i risultati dei test per ogni singolo modello.
Per farlo, modifichiamo il codice sorgente dell’EA come segue:
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; } }
Abilitare il parametro "Usa solo il primo modello".
Risultati del test del primo modello
Verifichiamo ora il secondo modello. Ecco i risultati del test del secondo modello.
Il secondo modello si è rivelato molto più resistente del primo. I risultati confermano la teoria secondo cui i modelli deboli devono stare in gruppo. Tuttavia, questo articolo non riguardava la teoria dell'assemblaggio, ma l'applicazione pratica.
Nota importante: Notare che i modelli utilizzati nell'articolo sono presentati solo per dimostrare come lavorare con i modelli ONNX utilizzando il linguaggio MQL5. L'Expert Advisor non è destinato al trading su conti reali.
Conclusioni
Abbiamo presentato un esempio molto semplice ma illustrativo di un assemblaggio di due modelli ONNX. Il numero di modelli utilizzati contemporaneamente è limitato e non può superare i 256 modelli. Tuttavia, anche l'utilizzo di più di due modelli richiederà un approccio diverso nella programmazione dell'Expert Advisor, ovvero una programmazione orientata agli oggetti.
Ma questo è un argomento per un altro articolo.
Tradotto dal russo da MetaQuotes Ltd.
Articolo originale: https://www.mql5.com/ru/articles/12433





- App di trading gratuite
- Oltre 8.000 segnali per il copy trading
- Notizie economiche per esplorare i mercati finanziari
Accetti la politica del sito e le condizioni d’uso