
Évaluation des modèles ONNX à l'aide de mesures de régression
Introduction
La régression consiste à prédire une valeur réelle à partir d'un exemple non étiqueté. Un exemple bien connu de régression est l'estimation de la valeur d'un diamant sur la base de caractéristiques telles que la taille, le poids, la couleur, la pureté, etc.
Les mesures dites de régression sont utilisées pour évaluer la précision des prédictions des modèles de régression. Malgré des algorithmes similaires, les mesures de régression sont sémantiquement différentes des fonctions de perte similaires. Il est important de comprendre la différence entre les deux. Elle peut être formulée comme suit :
-
La fonction de perte apparaît au moment où nous réduisons le problème de la construction d'un modèle à un problème d'optimisation. Il est généralement exigé qu'elle possède de bonnes propriétés (par exemple, la différentiabilité).
-
Une métrique est un critère de qualité objectif externe, qui ne dépend généralement pas des paramètres du modèle, mais uniquement des valeurs prédites.
Métriques de régression dans MQL5
Le langage MQL5 propose les métriques suivantes :
- Erreur Absolue Moyenne, MAE
- Erreur Quadratique Moyenne, MSE
- Racine de l’Erreur Quadratique Moyenne, RMSE
- R au Carré, R2
- Erreur Moyenne Absolue en Pourcentage, MAPE
- Erreur Quadratique Moyenne en Pourcentage, MSPE
- Erreur Logarithmique Quadratique Moyenne, RMSLE
Il est prévu d'augmenter le nombre de mesures de régression dans le langage MQL5.
Caractéristiques rapides des mesures de régression
La MAE estime l'erreur absolue, c'est-à-dire l'écart entre le nombre prédit et le nombre réel. L'erreur est mesurée dans les mêmes unités que la valeur de la fonction objective. La valeur de l'erreur est interprétée en fonction de la gamme des valeurs possibles. Par exemple, si les valeurs cibles sont comprises entre 1 et 1,5, l'erreur absolue moyenne avec une valeur de 10 est une erreur très importante, alors que pour la plage de 10000...15000, elle est tout à fait acceptable. Elle n'est pas adaptée à l'évaluation de prévisions présentant un large éventail de valeurs.
Dans la MSE, chaque erreur a son propre poids en raison de la mise au carré. Les écarts importants entre les prévisions et la réalité sont donc beaucoup plus perceptibles.
La RMSE présente les mêmes avantages que la MSE, mais elle est plus facile à comprendre, puisque l'erreur est mesurée dans les mêmes unités que les valeurs de la fonction objective. Elle est très sensible aux anomalies et aux pics. La MAE et la RMSE peuvent être utilisées conjointement pour déterminer la variation de l'erreur dans un ensemble de prédictions. La RMSE est toujours supérieure ou égale à la MAE. Plus la différence est grande, plus la dispersion des erreurs individuelles dans l'échantillon est importante. Si RMSE = MAE, toutes les erreurs ont la même ampleur.
R2 - le ratio de détermination indique la force de la relation entre 2 variables aléatoires. Il permet de déterminer la part de la diversité des données que le modèle a pu expliquer. Si le modèle prédit toujours avec précision, la métrique est de 1. Pour un modèle trivial, elle est de 0. La métrique peut être négative si le modèle prédit moins bien que le modèle trivial alors que le modèle ne suit pas la tendance des données.
MAPE - l'erreur n'a pas de dimension et est très facile à interpréter. Elle peut être exprimée à la fois en décimales et en pourcentage. Dans MQL5, elle est exprimée en décimales. Par exemple, une valeur de 0,1 indique que l'erreur était de 10% de la valeur réelle. L'idée qui sous-tend cette mesure est la sensibilité aux écarts relatifs. Elle n'est pas adaptée aux tâches nécessitant l'utilisation d'unités de mesure réelles.
La MSPE peut être considérée comme une version pondérée de la MSE, où le poids est inversement proportionnel au carré de la valeur observée. A mesure que les valeurs observées augmentent, l'erreur tend donc à diminuer.
La RMSLE est utilisée lorsque les valeurs réelles s'étendent sur plusieurs ordres de grandeur. Par définition, les valeurs prédites et les valeurs observées ne peuvent pas être négatives.
Les algorithmes de calcul de toutes les mesures ci-dessus sont fournis dans le fichier source VectorRegressionMetric.mqh.
Modèles ONNX
Nous avons utilisé 4 modèles de régression prédisant le prix de clôture du jour (EURUSD, D1) à partir des barres quotidiennes précédentes. Nous avons examiné ces modèles dans les articles précédents : "Encapsuler les modèles ONNX dans des classes", "Un exemple d'ensemble de modèles ONNX dans MQL5" et "Comment utiliser les modèles ONNX dans MQL5". Nous ne répéterons donc pas ici les règles utilisées pour former les modèles. Les scripts d'entraînement de tous les modèles se trouvent dans le sous-dossier Python de l'archive zip jointe à cet article. Les modèles onnx entraînés - model.eurusd.D1.10, model.eurusd.D1.30, model.eurusd.D1.52 et model.eurusd.D1.63 sont également attachés à cet article.
Encapsuler les modèles ONNX dans des classes
Dans l'article précédent, nous avons présenté la classe de base pour les modèles ONNX et les classes dérivées pour les modèles de classification. Nous avons apporté quelques modifications mineures à la classe de base pour la rendre plus flexible.
//+------------------------------------------------------------------+ //| ModelSymbolPeriod.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ //--- price movement prediction #define PRICE_UP 0 #define PRICE_SAME 1 #define PRICE_DOWN 2 //+------------------------------------------------------------------+ //| Base class for models based on trained symbol and period | //+------------------------------------------------------------------+ class CModelSymbolPeriod { protected: string m_name; // model name long m_handle; // created model session handle string m_symbol; // symbol of trained data ENUM_TIMEFRAMES m_period; // timeframe of trained data datetime m_next_bar; // time of next bar (we work at bar begin only) double m_class_delta; // delta to recognize "price the same" in regression models public: //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CModelSymbolPeriod(const string symbol,const ENUM_TIMEFRAMES period,const double class_delta=0.0001) { m_name=""; m_handle=INVALID_HANDLE; m_symbol=symbol; m_period=period; m_next_bar=0; m_class_delta=class_delta; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ ~CModelSymbolPeriod(void) { Shutdown(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ string GetModelName(void) { return(m_name); } //+------------------------------------------------------------------+ //| virtual stub for Init | //+------------------------------------------------------------------+ virtual bool Init(const string symbol, const ENUM_TIMEFRAMES period) { return(false); } //+------------------------------------------------------------------+ //| Check for initialization, create model | //+------------------------------------------------------------------+ bool CheckInit(const string symbol, const ENUM_TIMEFRAMES period,const uchar& model[]) { //--- check symbol, period if(symbol!=m_symbol || period!=m_period) { PrintFormat("Model must work with %s,%s",m_symbol,EnumToString(m_period)); return(false); } //--- create a model from static buffer m_handle=OnnxCreateFromBuffer(model,ONNX_DEFAULT); if(m_handle==INVALID_HANDLE) { Print("OnnxCreateFromBuffer error ",GetLastError()); return(false); } //--- ok return(true); } //+------------------------------------------------------------------+ //| Release ONNX session | //+------------------------------------------------------------------+ void Shutdown(void) { if(m_handle!=INVALID_HANDLE) { OnnxRelease(m_handle); m_handle=INVALID_HANDLE; } } //+------------------------------------------------------------------+ //| Check for continue OnTick | //+------------------------------------------------------------------+ virtual bool CheckOnTick(void) { //--- check new bar if(TimeCurrent()<m_next_bar) return(false); //--- set next bar time m_next_bar=TimeCurrent(); m_next_bar-=m_next_bar%PeriodSeconds(m_period); m_next_bar+=PeriodSeconds(m_period); //--- work on new day bar return(true); } //+------------------------------------------------------------------+ //| virtual stub for PredictPrice (regression model) | //+------------------------------------------------------------------+ virtual double PredictPrice(datetime date) { return(DBL_MAX); } //+------------------------------------------------------------------+ //| Predict class (regression ~> classification) | //+------------------------------------------------------------------+ virtual int PredictClass(datetime date,vector& probabilities) { date-=date%PeriodSeconds(m_period); double predicted_price=PredictPrice(date); if(predicted_price==DBL_MAX) return(-1); double last_close[2]; if(CopyClose(m_symbol,m_period,date,2,last_close)!=2) return(-1); double prev_price=last_close[0]; //--- classify predicted price movement int predicted_class=-1; double delta=prev_price-predicted_price; if(fabs(delta)<=m_class_delta) predicted_class=PRICE_SAME; else { if(delta<0) predicted_class=PRICE_UP; else predicted_class=PRICE_DOWN; } //--- set predicted probability as 1.0 probabilities.Fill(0); if(predicted_class<(int)probabilities.Size()) probabilities[predicted_class]=1; //--- and return predicted class return(predicted_class); } }; //+------------------------------------------------------------------+
Nous avons ajouté un paramètre de type datetime aux méthodes PredictPrice et PredictClass afin de pouvoir faire des prédictions pour n'importe quel moment, et pas seulement pour le moment présent. Cela sera utile pour former un vecteur de prédiction.
Classe de modèle D1_10
Notre premier modèle s'appelle model.eurusd.D1.10.onnx. Modèle de régression entraîné sur l’EURUSD en D1 sur la série de 10 prix OHLC.//+------------------------------------------------------------------+ //| ModelEurusdD1_10.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #include "ModelSymbolPeriod.mqh" #resource "Python/model.eurusd.D1.10.onnx" as uchar model_eurusd_D1_10[] //+------------------------------------------------------------------+ //| ONNX-model wrapper class | //+------------------------------------------------------------------+ class CModelEurusdD1_10 : public CModelSymbolPeriod { private: int m_sample_size; public: //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CModelEurusdD1_10(void) : CModelSymbolPeriod("EURUSD",PERIOD_D1) { m_name="D1_10"; m_sample_size=10; } //+------------------------------------------------------------------+ //| ONNX-model initialization | //+------------------------------------------------------------------+ virtual bool Init(const string symbol, const ENUM_TIMEFRAMES period) { //--- check symbol, period, create model if(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_10)) { Print("model_eurusd_D1_10 : initialization error"); return(false); } //--- 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_shape[] = {1,m_sample_size,4}; if(!OnnxSetInputShape(m_handle,0,input_shape)) { Print("model_eurusd_D1_10 : OnnxSetInputShape error ",GetLastError()); return(false); } //--- 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 const long output_shape[] = {1,1}; if(!OnnxSetOutputShape(m_handle,0,output_shape)) { Print("model_eurusd_D1_10 : OnnxSetOutputShape error ",GetLastError()); return(false); } //--- ok return(true); } //+------------------------------------------------------------------+ //| Predict price | //+------------------------------------------------------------------+ virtual double PredictPrice(datetime date) { static matrixf input_data(m_sample_size,4); // matrix for prepared input data static vectorf output_data(1); // vector to get result static matrix mm(m_sample_size,4); // matrix of horizontal vectors Mean static matrix ms(m_sample_size,4); // matrix of horizontal vectors Std static matrix x_norm(m_sample_size,4); // matrix for prices normalize //--- prepare input data matrix rates; //--- request last bars date-=date%PeriodSeconds(m_period); if(!rates.CopyRates(m_symbol,m_period,COPY_RATES_OHLC,date-1,m_sample_size)) return(DBL_MAX); //--- 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<m_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(m_handle,ONNX_NO_CONVERSION,input_data,output_data)) return(DBL_MAX); //--- denormalize the price from the output value double predicted=output_data[0]*s[3]+m[3]; //--- return prediction return(predicted); } }; //+------------------------------------------------------------------+
Ce modèle est similaire à notre tout premier modèle publié dans le projet public MQL5\Shared Projects\ONNX.Price.Prediction.
La série de 10 prix OHLC doit être normalisée comme lors de l'entraînement, c'est-à-dire que l'écart par rapport au prix moyen de la série est divisé par l'écart-type de la série. Nous plaçons ainsi les séries dans un certain intervalle avec une moyenne de 0 et un écart de 1, ce qui améliore la convergence pendant l'apprentissage.
Classe de modèle D1_30
Le second modèle est appelé model.eurusd.D1.30.onnx. Le modèle de régression formé sur l’EURUSD en D1 sur la série de 30 prix de clôture et 2 moyennes mobiles simples avec des périodes de moyenne de 21 et 34.
//+------------------------------------------------------------------+ //| ModelEurusdD1_30.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #include "ModelSymbolPeriod.mqh" #resource "Python/model.eurusd.D1.30.onnx" as uchar model_eurusd_D1_30[] //+------------------------------------------------------------------+ //| ONNX-model wrapper class | //+------------------------------------------------------------------+ class CModelEurusdD1_30 : public CModelSymbolPeriod { private: int m_sample_size; int m_fast_period; int m_slow_period; int m_sma_fast; int m_sma_slow; public: //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CModelEurusdD1_30(void) : CModelSymbolPeriod("EURUSD",PERIOD_D1) { m_name="D1_30"; m_sample_size=30; m_fast_period=21; m_slow_period=34; m_sma_fast=INVALID_HANDLE; m_sma_slow=INVALID_HANDLE; } //+------------------------------------------------------------------+ //| ONNX-model initialization | //+------------------------------------------------------------------+ virtual bool Init(const string symbol, const ENUM_TIMEFRAMES period) { //--- check symbol, period, create model if(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_30)) { Print("model_eurusd_D1_30 : initialization error"); return(false); } //--- 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 (Close, MA fast, MA slow) const long input_shape[] = {1,m_sample_size,3}; if(!OnnxSetInputShape(m_handle,0,input_shape)) { Print("model_eurusd_D1_30 : OnnxSetInputShape error ",GetLastError()); return(false); } //--- 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 const long output_shape[] = {1,1}; if(!OnnxSetOutputShape(m_handle,0,output_shape)) { Print("model_eurusd_D1_30 : OnnxSetOutputShape error ",GetLastError()); return(false); } //--- indicators m_sma_fast=iMA(m_symbol,m_period,m_fast_period,0,MODE_SMA,PRICE_CLOSE); m_sma_slow=iMA(m_symbol,m_period,m_slow_period,0,MODE_SMA,PRICE_CLOSE); if(m_sma_fast==INVALID_HANDLE || m_sma_slow==INVALID_HANDLE) { Print("model_eurusd_D1_30 : cannot create indicator"); return(false); } //--- ok return(true); } //+------------------------------------------------------------------+ //| Predict price | //+------------------------------------------------------------------+ virtual double PredictPrice(datetime date) { static matrixf input_data(m_sample_size,3); // matrix for prepared input data static vectorf output_data(1); // vector to get result static matrix x_norm(m_sample_size,3); // matrix for prices normalize static vector vtemp(m_sample_size); static double ma_buffer[]; //--- request last bars date-=date%PeriodSeconds(m_period); if(!vtemp.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,date-1,m_sample_size)) return(DBL_MAX); //--- get series Mean double m=vtemp.Mean(); //--- get series Std double s=vtemp.Std(); //--- normalize vtemp-=m; vtemp/=s; x_norm.Col(vtemp,0); //--- fast sma if(CopyBuffer(m_sma_fast,0,date-1,m_sample_size,ma_buffer)!=m_sample_size) return(-1); vtemp.Assign(ma_buffer); m=vtemp.Mean(); s=vtemp.Std(); vtemp-=m; vtemp/=s; x_norm.Col(vtemp,1); //--- slow sma if(CopyBuffer(m_sma_slow,0,date-1,m_sample_size,ma_buffer)!=m_sample_size) return(-1); vtemp.Assign(ma_buffer); m=vtemp.Mean(); s=vtemp.Std(); vtemp-=m; vtemp/=s; x_norm.Col(vtemp,2); //--- run the inference input_data.Assign(x_norm); if(!OnnxRun(m_handle,ONNX_NO_CONVERSION,input_data,output_data)) return(DBL_MAX); //--- denormalize the price from the output value double predicted=output_data[0]*s+m; //--- return prediction return(predicted); } }; //+------------------------------------------------------------------+
Comme dans la classe précédente, la méthode CheckInit de la classe de base est appelée dans la méthode Init. Dans la méthode de la classe de base, une session est créée pour le modèle ONNX et les tailles des tenseurs d'entrée et de sortie sont explicitement définies.
La méthode PredictPrice fournit la série des 30 clôtures précédentes et les moyennes mobiles calculées. Les données sont normalisées de la même manière que pour la formation.
Le modèle a été développé pour l'article "Encapsulation des modèles ONNX dans des classes" et converti de la classification à la régression pour cet article.
Classe de modèle D1_52
Le troisième modèle est appelé model.eurusd.D1.52.onnx. Le modèle de régression entraîné sur l’EURUSD en D1 sur la série de 52 prix de clôture.
//+------------------------------------------------------------------+ //| ModelEurusdD1_52.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #include "ModelSymbolPeriod.mqh" #resource "Python/model.eurusd.D1.52.onnx" as uchar model_eurusd_D1_52[] //+------------------------------------------------------------------+ //| ONNX-model wrapper class | //+------------------------------------------------------------------+ class CModelEurusdD1_52 : public CModelSymbolPeriod { private: int m_sample_size; public: //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CModelEurusdD1_52(void) : CModelSymbolPeriod("EURUSD",PERIOD_D1,0.0001) { m_name="D1_52"; m_sample_size=52; } //+------------------------------------------------------------------+ //| ONNX-model initialization | //+------------------------------------------------------------------+ virtual bool Init(const string symbol, const ENUM_TIMEFRAMES period) { //--- check symbol, period, create model if(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_52)) { Print("model_eurusd_D1_52 : initialization error"); return(false); } //--- 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 (only Close) const long input_shape[] = {1,m_sample_size,1}; if(!OnnxSetInputShape(m_handle,0,input_shape)) { Print("model_eurusd_D1_52 : OnnxSetInputShape error ",GetLastError()); return(false); } //--- 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_shape[] = {1,1}; if(!OnnxSetOutputShape(m_handle,0,output_shape)) { Print("model_eurusd_D1_52 : OnnxSetOutputShape error ",GetLastError()); return(false); } //--- ok return(true); } //+------------------------------------------------------------------+ //| Predict price | //+------------------------------------------------------------------+ virtual double PredictPrice(datetime date) { static vectorf output_data(1); // vector to get result static vector x_norm(m_sample_size); // vector for prices normalize //--- set date to day begin date-=date%PeriodSeconds(m_period); //--- check for calculate min and max double price_min=0; double price_max=0; GetMinMaxClose(date,price_min,price_max); //--- check for normalization possibility if(price_min>=price_max) return(DBL_MAX); //--- request last bars if(!x_norm.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,date-1,m_sample_size)) return(DBL_MAX); //--- normalize prices x_norm-=price_min; x_norm/=(price_max-price_min); //--- run the inference if(!OnnxRun(m_handle,ONNX_DEFAULT,x_norm,output_data)) return(DBL_MAX); //--- denormalize the price from the output value double predicted=output_data[0]*(price_max-price_min)+price_min; //--- return prediction return(predicted); } private: //+------------------------------------------------------------------+ //| Get minimal and maximal Close for last 52 weeks | //+------------------------------------------------------------------+ void GetMinMaxClose(const datetime date,double& price_min,double& price_max) { static vector close; close.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,date,m_sample_size*7+1); price_min=close.Min(); price_max=close.Max(); } }; //+------------------------------------------------------------------+
La normalisation des prix avant la soumission du modèle diffère des précédentes. MinMaxScaler a été utilisé pendant la formation. Nous prenons donc les prix minimum et maximum pour la période de 52 semaines précédant la date de prévision.
Le modèle est similaire à celui décrit dans l'article "Comment utiliser les modèles ONNX dans MQL5".
Classe de modèle D1_63
Enfin, le quatrième modèle est appelé model.eurusd.D1.63.onnx. Le modèle de régression entraîné sur l’EURUSD en D1 sur la série de 63 prix de Clôture.
//+------------------------------------------------------------------+ //| ModelEurusdD1_63.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #include "ModelSymbolPeriod.mqh" #resource "Python/model.eurusd.D1.63.onnx" as uchar model_eurusd_D1_63[] //+------------------------------------------------------------------+ //| ONNX-model wrapper class | //+------------------------------------------------------------------+ class CModelEurusdD1_63 : public CModelSymbolPeriod { private: int m_sample_size; public: //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CModelEurusdD1_63(void) : CModelSymbolPeriod("EURUSD",PERIOD_D1) { m_name="D1_63"; m_sample_size=63; } //+------------------------------------------------------------------+ //| ONNX-model initialization | //+------------------------------------------------------------------+ virtual bool Init(const string symbol, const ENUM_TIMEFRAMES period) { //--- check symbol, period, create model if(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_63)) { Print("model_eurusd_D1_63 : initialization error"); return(false); } //--- 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_shape[] = {1,m_sample_size}; if(!OnnxSetInputShape(m_handle,0,input_shape)) { Print("model_eurusd_D1_63 : OnnxSetInputShape error ",GetLastError()); return(false); } //--- 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 const long output_shape[] = {1,1}; if(!OnnxSetOutputShape(m_handle,0,output_shape)) { Print("model_eurusd_D1_63 : OnnxSetOutputShape error ",GetLastError()); return(false); } //--- ok return(true); } //+------------------------------------------------------------------+ //| Predict price | //+------------------------------------------------------------------+ virtual double PredictPrice(datetime date) { static vectorf input_data(m_sample_size); // vector for prepared input data static vectorf output_data(1); // vector to get result //--- request last bars date-=date%PeriodSeconds(m_period); if(!input_data.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,date-1,m_sample_size)) return(DBL_MAX); //--- 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(m_handle,ONNX_NO_CONVERSION,input_data,output_data)) return(DBL_MAX); //--- denormalize the price from the output value double predicted=output_data[0]*s+m; //--- return prediction return(predicted); } }; //+------------------------------------------------------------------+
La méthode PredictPrice fournit la série des 63 clôtures précédentes. Les données sont normalisées de la même manière que dans les premier et deuxième modèles.
Le modèle a déjà été développé pour l'article "Un exemple d’assemblage de modèles ONNX en MQL5".
Combinaison de tous les modèles dans un seul script. Réalité, prévisions et mesures de régression
Pour appliquer les métriques de régression, nous devons faire un certain nombre de prédictions (vector_pred) et prendre des données réelles pour les mêmes dates (vector_true).
Comme tous nos modèles sont enveloppés dans des classes qui dérivent de la même classe de base, nous pouvons les évaluer tous en même temps.
Le script est très simple :
//+------------------------------------------------------------------+ //| ONNX.eurusd.D1.4M.Metrics.mq5 | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #define MODELS 4 #include "ModelEurusdD1_10.mqh" #include "ModelEurusdD1_30.mqh" #include "ModelEurusdD1_52.mqh" #include "ModelEurusdD1_63.mqh" #property script_show_inputs input datetime InpStartDate = D'2023.01.01'; input datetime InpStopDate = D'2023.01.31'; CModelSymbolPeriod *ExtModels[MODELS]; struct PredictedPrices { string model; double pred[]; }; PredictedPrices ExtPredicted[MODELS]; double ExtClose[]; struct Metrics { string model; double mae; double mse; double rmse; double r2; double mape; double mspe; double rmsle; }; Metrics ExtMetrics[MODELS]; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- init section if(!Init()) return; //--- predictions test loop datetime dates[]; if(CopyTime(_Symbol,_Period,InpStartDate,InpStopDate,dates)<=0) { Print("Cannot get data from ",InpStartDate," to ",InpStopDate); return; } for(uint n=0; n<dates.Size(); n++) GetPredictions(dates[n]); CopyClose(_Symbol,_Period,InpStartDate,InpStopDate,ExtClose); CalculateMetrics(); //--- deinit section Deinit(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool Init() { ExtModels[0]=new CModelEurusdD1_10; ExtModels[1]=new CModelEurusdD1_30; ExtModels[2]=new CModelEurusdD1_52; ExtModels[3]=new CModelEurusdD1_63; for(long i=0; i<ExtModels.Size(); i++) { if(!ExtModels[i].Init(_Symbol,_Period)) { Deinit(); return(false); } } for(long i=0; i<ExtModels.Size(); i++) ExtPredicted[i].model=ExtModels[i].GetModelName(); return(true); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void Deinit() { for(uint i=0; i<ExtModels.Size(); i++) delete ExtModels[i]; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void GetPredictions(datetime date) { //--- collect predicted prices for(uint i=0; i<ExtModels.Size(); i++) ExtPredicted[i].pred.Push(ExtModels[i].PredictPrice(date)); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CalculateMetrics() { vector vector_pred,vector_true; vector_true.Assign(ExtClose); for(uint i=0; i<ExtModels.Size(); i++) { ExtMetrics[i].model=ExtPredicted[i].model; vector_pred.Assign(ExtPredicted[i].pred); ExtMetrics[i].mae =vector_pred.RegressionMetric(vector_true,REGRESSION_MAE); ExtMetrics[i].mse =vector_pred.RegressionMetric(vector_true,REGRESSION_MSE); ExtMetrics[i].rmse =vector_pred.RegressionMetric(vector_true,REGRESSION_RMSE); ExtMetrics[i].r2 =vector_pred.RegressionMetric(vector_true,REGRESSION_R2); ExtMetrics[i].mape =vector_pred.RegressionMetric(vector_true,REGRESSION_MAPE); ExtMetrics[i].mspe =vector_pred.RegressionMetric(vector_true,REGRESSION_MSPE); ExtMetrics[i].rmsle=vector_pred.RegressionMetric(vector_true,REGRESSION_RMSLE); } ArrayPrint(ExtMetrics); } //+------------------------------------------------------------------+
Exécutons le script sur le graphique EURUSD D1 et fixons les dates du 1er janvier au 31 janvier 2023 inclus. Que voyons-nous ?
[model] [mae] [mse] [rmse] [r2] [mape] [mspe] [rmsle] [0] "D1_10" 0.00381 0.00003 0.00530 0.77720 0.00356 0.00002 0.00257 [1] "D1_30" 0.01809 0.00039 0.01963 -2.05545 0.01680 0.00033 0.00952 [2] "D1_52" 0.00472 0.00004 0.00642 0.67327 0.00440 0.00004 0.00311 [3] "D1_63" 0.00413 0.00003 0.00559 0.75230 0.00385 0.00003 0.00270
La valeur négative du R au carré est immédiatement perceptible dans la deuxième ligne. Cela signifie que le modèle ne fonctionne pas. Il est intéressant d'examiner les graphiques de prédiction.
Nous constatons que le graphique D1_30 est très éloigné des prix de clôture réels et d'autres prévisions. Aucune des mesures de ce modèle n'est encourageante. Le MAE montre une précision de prévision de 1809 points de prix ! Il faut toutefois garder à l'esprit que le modèle a été initialement développé pour l'article précédent en tant que modèle de classification, et non de régression. L'exemple est assez clair.
Examinons les autres modèles séparément.
Le premier candidat à l'analyse est D1_10
[model] [mae] [mse] [rmse] [r2] [mape] [mspe] [rmsle] [0] "D1_10" 0.00381 0.00003 0.00530 0.77720 0.00356 0.00002 0.00257
Examinons le tableau des prix prédits par ce modèle.
La métrique RMSLE n'a pas beaucoup de sens, puisque l'écart entre 1,05 et 1,09 est bien inférieur à un ordre de grandeur. Les valeurs des mesures MAPE et MSPE sont proches de celles des mesures MAE et MSE en raison des particularités du taux de change EURUSD, qui est proche de un. Mais le calcul des écarts en pourcentage comporte une nuance qui n'existe pas dans le calcul des écarts absolus.
MAPE = |(y_true-y_pred)/y_true| if y_true = 10 and y_pred = 5 MAPE = 0.5 if y_true = 5 and y_pred = 10 MAPE = 1.0
En d'autres termes, cette mesure (comme le MSPE) est asymétrique. Cela signifie que dans le cas où la prévision est plus élevée que la réalité, l'erreur est plus importante.
Un bon résultat de la métrique R-carré a été obtenu pour le modèle simple élaboré à des fins purement méthodologiques, pour montrer comment vous pouvez travailler avec des modèles ONNX dans MQL5.
Deuxième candidat - D1_63
[model] [mae] [mse] [rmse] [r2] [mape] [mspe] [rmsle] [3] "D1_63" 0.00413 0.00003 0.00559 0.75230 0.00385 0.00003 0.00270
Les prévisions sont visuellement très proches des précédentes. Les métriques confirment cette similarité :
[0] "D1_10" 0.00381 0.00003 0.00530 0.77720 0.00356 0.00002 0.00257 [3] "D1_63" 0.00413 0.00003 0.00559 0.75230 0.00385 0.00003 0.00270
Nous verrons ensuite lequel de ces modèles sera le plus performant dans le testeur au cours de la même période.
Maintenant, pour D1_52 :
[model] [mae] [mse] [rmse] [r2] [mape] [mspe] [rmsle] [2] "D1_52" 0.00472 0.00004 0.00642 0.67327 0.00440 0.00004 0.00311
Nous ne le prenons en compte que si son carré R est supérieur à 0,5.
Presque tous les prix prédits sont inférieurs au graphique de clôture, comme dans notre cas le plus défavorable. Malgré des valeurs des métriques comparables à celles des deux modèles précédents, ce modèle n'inspire pas l'optimisme. Nous le vérifierons dans le paragraphe suivant.
Exécution des modèles ONNX dans le testeur
Voici un EA très simple pour vérifier nos modèles dans le testeur
//+------------------------------------------------------------------+ //| ONNX.eurusd.D1.Prediction.mq5 | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "ModelEurusdD1_10.mqh" #include "ModelEurusdD1_30.mqh" #include "ModelEurusdD1_52.mqh" #include "ModelEurusdD1_63.mqh" #include <Trade\Trade.mqh> input double InpLots = 1.0; // Lots amount to open position //CModelEurusdD1_10 ExtModel; //CModelEurusdD1_30 ExtModel; CModelEurusdD1_52 ExtModel; //CModelEurusdD1_63 ExtModel; CTrade ExtTrade; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if(!ExtModel.Init(_Symbol,_Period)) return(INIT_FAILED); Print("model ",ExtModel.GetModelName()); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { ExtModel.Shutdown(); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { if(!ExtModel.CheckOnTick()) return; //--- predict next price movement vector prob(3); int predicted_class=ExtModel.PredictClass(TimeCurrent(),prob); Print("predicted class ",predicted_class); //--- check trading according to prediction if(predicted_class>=0) if(PositionSelect(_Symbol)) CheckForClose(predicted_class); else CheckForOpen(predicted_class); } //+------------------------------------------------------------------+ //| Check for open position conditions | //+------------------------------------------------------------------+ void CheckForOpen(const int predicted_class) { ENUM_ORDER_TYPE signal=WRONG_VALUE; //--- check signals if(predicted_class==PRICE_DOWN) signal=ORDER_TYPE_SELL; // sell condition else { if(predicted_class==PRICE_UP) signal=ORDER_TYPE_BUY; // buy condition } //--- open position if possible according to signal if(signal!=WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { double price=SymbolInfoDouble(_Symbol,(signal==ORDER_TYPE_SELL) ? SYMBOL_BID : SYMBOL_ASK); ExtTrade.PositionOpen(_Symbol,signal,InpLots,price,0,0); } } //+------------------------------------------------------------------+ //| Check for close position conditions | //+------------------------------------------------------------------+ void CheckForClose(const int predicted_class) { bool bsignal=false; //--- position already selected before long type=PositionGetInteger(POSITION_TYPE); //--- check signals if(type==POSITION_TYPE_BUY && predicted_class==PRICE_DOWN) bsignal=true; if(type==POSITION_TYPE_SELL && predicted_class==PRICE_UP) bsignal=true; //--- close position if possible if(bsignal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { ExtTrade.PositionClose(_Symbol,3); //--- open opposite CheckForOpen(predicted_class); } } //+------------------------------------------------------------------+
En effet, selon le modèle D1_52, une seule transaction de vente a été ouverte et la tendance, selon ce modèle, n'a pas changé pendant toute la période de test
2023.06.09 16:18:31.967 Symbols EURUSD: symbol to be synchronized 2023.06.09 16:18:31.968 Symbols EURUSD: symbol synchronized, 3720 bytes of symbol info received 2023.06.09 16:18:32.023 History EURUSD: load 27 bytes of history data to synchronize in 0:00:00.001 2023.06.09 16:18:32.023 History EURUSD: history synchronized from 2011.01.03 to 2023.04.07 2023.06.09 16:18:32.124 History EURUSD,Daily: history cache allocated for 283 bars and contains 260 bars from 2022.01.03 00:00 to 2022.12.30 00:00 2023.06.09 16:18:32.124 History EURUSD,Daily: history begins from 2022.01.03 00:00 2023.06.09 16:18:32.126 Tester EURUSD,Daily (MetaQuotes-Demo): 1 minutes OHLC ticks generating 2023.06.09 16:18:32.126 Tester EURUSD,Daily: testing of Experts\article_2\ONNX.eurusd.D1.Prediction.ex5 from 2023.01.01 00:00 to 2023.02.01 00:00 started with inputs: 2023.06.09 16:18:32.126 Tester InpLots=1.0 2023.06.09 16:18:32.161 ONNX api version 1.16.0 initialized 2023.06.09 16:18:32.180 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.01 00:00:00 model D1_52 2023.06.09 16:18:32.194 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.02 07:02:00 predicted class 2 2023.06.09 16:18:32.194 Trade 2023.01.02 07:02:00 instant sell 1 EURUSD at 1.07016 (1.07016 / 1.07023 / 1.07016) 2023.06.09 16:18:32.194 Trades 2023.01.02 07:02:00 deal #2 sell 1 EURUSD at 1.07016 done (based on order #2) 2023.06.09 16:18:32.194 Trade 2023.01.02 07:02:00 deal performed [#2 sell 1 EURUSD at 1.07016] 2023.06.09 16:18:32.194 Trade 2023.01.02 07:02:00 order performed sell 1 at 1.07016 [#2 sell 1 EURUSD at 1.07016] 2023.06.09 16:18:32.195 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.02 07:02:00 CTrade::OrderSend: instant sell 1.00 EURUSD at 1.07016 [done at 1.07016] 2023.06.09 16:18:32.196 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.03 00:00:00 predicted class 2 2023.06.09 16:18:32.199 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.04 00:00:00 predicted class 2 2023.06.09 16:18:32.201 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.05 00:00:30 predicted class 2 2023.06.09 16:18:32.203 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.06 00:00:00 predicted class 2 2023.06.09 16:18:32.206 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.09 00:02:00 predicted class 2 2023.06.09 16:18:32.208 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.10 00:00:00 predicted class 2 2023.06.09 16:18:32.210 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.11 00:00:00 predicted class 2 2023.06.09 16:18:32.213 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.12 00:00:00 predicted class 2 2023.06.09 16:18:32.215 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.13 00:00:00 predicted class 2 2023.06.09 16:18:32.217 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.16 00:03:00 predicted class 2 2023.06.09 16:18:32.220 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.17 00:00:00 predicted class 2 2023.06.09 16:18:32.222 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.18 00:00:30 predicted class 2 2023.06.09 16:18:32.224 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.19 00:00:00 predicted class 2 2023.06.09 16:18:32.227 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.20 00:00:30 predicted class 2 2023.06.09 16:18:32.229 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.23 00:02:00 predicted class 2 2023.06.09 16:18:32.231 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.24 00:00:00 predicted class 2 2023.06.09 16:18:32.234 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.25 00:00:00 predicted class 2 2023.06.09 16:18:32.236 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.26 00:00:00 predicted class 2 2023.06.09 16:18:32.238 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.27 00:00:00 predicted class 2 2023.06.09 16:18:32.241 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.30 00:03:00 predicted class 2 2023.06.09 16:18:32.243 ONNX.eurusd.D1.Prediction (EURUSD,D1) 2023.01.31 00:00:00 predicted class 2 2023.06.09 16:18:32.245 Trade 2023.01.31 23:59:59 position closed due end of test at 1.08621 [#2 sell 1 EURUSD 1.07016] 2023.06.09 16:18:32.245 Trades 2023.01.31 23:59:59 deal #3 buy 1 EURUSD at 1.08621 done (based on order #3) 2023.06.09 16:18:32.245 Trade 2023.01.31 23:59:59 deal performed [#3 buy 1 EURUSD at 1.08621] 2023.06.09 16:18:32.245 Trade 2023.01.31 23:59:59 order performed buy 1 at 1.08621 [#3 buy 1 EURUSD at 1.08621] 2023.06.09 16:18:32.245 Tester final balance 8366.00 USD 2023.06.09 16:18:32.249 Tester EURUSD,Daily: 123499 ticks, 22 bars generated. Environment synchronized in 0:00:00.043. Test passed in 0:00:00.294 (including ticks preprocessing 0:00:00.016).
Comme mentionné dans la section précédente, le modèle D1_52 n'incite pas à l'optimisme. Les résultats des tests le confirment.
Modifions seulement 2 lignes de code
#include "ModelEurusdD1_10.mqh" #include "ModelEurusdD1_30.mqh" #include "ModelEurusdD1_52.mqh" #include "ModelEurusdD1_63.mqh" #include <Trade\Trade.mqh> input double InpLots = 1.0; // Lots amount to open position CModelEurusdD1_10 ExtModel; //CModelEurusdD1_30 ExtModel; //CModelEurusdD1_52 ExtModel; //CModelEurusdD1_63 ExtModel; CTrade ExtTrade;
et lançons le modèle D1_10 pour le tester.
Les résultats sont bons. Le graphique du test est également prometteur :
Corrigeons à nouveau 2 lignes de code et testons le modèle D1_63 :
Le graphique :
Le graphique du test est beaucoup plus mauvais que celui du modèle D1_10.
En comparant les deux modèles D1_10 et D1_63, on constate que le premier modèle a de meilleures mesures de régression que le second. Le testeur montre la même chose.
Remarque importante : Veuillez noter que les modèles utilisés dans l'article sont présentés uniquement pour démontrer comment travailler avec les modèles ONNX en utilisant le langage MQL5. L'Expert Advisor n'est pas destiné à être utilisé sur des comptes réels.
Conclusion
La mesure la plus appropriée pour évaluer les modèles de prévision des prix est le R au carré. Il peut être très utile de considérer l'agrégat MAE - RMSE - MAPE. La métrique RMSLE ne peut pas être prise en compte dans les tâches de prévision des prix. Il est très utile de disposer de plusieurs modèles pour l'évaluation, même s'il s'agit du même modèle avec des modifications.
Nous comprenons qu'un échantillon de 22 valeurs est insuffisant pour une recherche sérieuse, mais notre intention n'était pas de faire une étude statistique. Nous n'avons fourni que le cas d'utilisation à la place.
Traduit du russe par MetaQuotes Ltd.
Article original : https://www.mql5.com/ru/articles/12772





- Applications de trading gratuites
- Plus de 8 000 signaux à copier
- Actualités économiques pour explorer les marchés financiers
Vous acceptez la politique du site Web et les conditions d'utilisation