Evaluación de modelos ONNX usando métricas de regresión
Introducción
La regresión es una tarea que consiste en predecir un valor real a partir de un ejemplo sin etiquetar. Un ejemplo muy conocido de regresión es la estimación del valor de un diamante según características como el tamaño, el peso, el color, la claridad, etcétera.
Para evaluar la precisión de las predicciones de los modelos de regresión, se utilizan las llamadas métricas de regresión. A pesar de tener algoritmos semejantes, las métricas de regresión son semánticamente diferentes de las funciones de pérdida similares. Es importante entender la diferencia entre ellas, que podemos formular de la siguiente manera:
-
La función de pérdida surge en el momento en que reducimos el problema de construcción de un modelo a un problema de optimización. Generalmente se requiere que tenga buenas propiedades (por ejemplo, diferenciabilidad).
-
Una métrica es un criterio de calidad externo y objetivo, que normalmente no depende de los parámetros del modelo, sino solo de los valores predichos.
Métricas de regresión en MQL5
Las siguientes métricas están implementadas en el lenguaje MQL5:
- Error absoluto medio (Mean Absolute Error, MAE)
- Error cuadrático medio (Mean Squared Error, MSE)
- Raíz del error cuadrático medio (Root Mean Squared Error, RMSE)
- Cuadrado R (R-squared, R2)
- Error porcentual absoluto medio (Mean Absolute Percentage Error, MAPE)
- Error porcentual cuadrático medio (Mean Squared Percentage Error, MSPE)
- Raíz del error logarítmico cuadrático medio (Root Mean Squared Logarithmic Error, RMSLE)
Se supone que la composición de las métricas de regresión en MQL5 será ampliada.
Breves características de las métricas de regresión.
MAE estima el error absoluto, es decir, en qué medida difiere el número previsto del número real. El error se mide en las mismas unidades que el valor de la función objetivo. El valor del error se interpreta según el rango de valores posibles. Por ejemplo, si los valores objetivo están en el rango de 1 a 1,5, entonces un error absoluto promedio con un valor de 10 será un error muy grande; para el rango de 10000...15000 será bastante aceptable. No resulta adecuado para evaluar pronósticos con una gran variedad de valores.
En el MSE, debido a la elevación al cuadrado, cada error tiene su propio peso. Por ello, las grandes discrepancias entre las previsiones y la realidad se hacen mucho más evidentes.
El RMSE tiene las mismas ventajas que el MSE, pero es más fácil de entender, porque el error se mide en las mismas unidades que los valores de la función objetivo. Resulta muy sensible a anomalías y valores atípicos. El MAE y el RMSE se pueden utilizar juntos para diagnosticar la variación de error en un conjunto de pronósticos. El RMSE siempre será mayor o igual que el MAE. Cuanto mayor sea la diferencia entre ellos, mayor será la dispersión de los errores individuales en la muestra. Si RMSE = MAE, entonces todos los errores tendrán la misma magnitud.
El R2 o coeficiente de determinación muestra la fuerza de la relación entre dos variables aleatorias. Ayuda a comprender qué parte de la diversidad de datos ha logrado explicar el modelo. Si el modelo siempre predice con precisión, la métrica será 1. Para un modelo trivial - 0. El valor de la métrica puede ser negativo si el modelo predice peor que de manera trivial, si el modelo no sigue la tendencia de los datos.
El error MAPE no tiene dimensionalidad y es muy fácil de interpretar. Puede expresarse como decimal o como porcentaje. En MQL5 se expresa como decimal. Por ejemplo, un valor de 0.1 indicará que el error ha sido del 10% del valor real. La idea detrás de esta métrica es la sensibilidad respecto a las desviaciones relativas. No resulta apto para tareas en las que es necesario trabajar con unidades de medida reales.
El MSPE puede considerarse como una versión ponderada del MSE, donde el peso es inversamente proporcional al cuadrado del valor observado. Así, a medida que aumentan los valores observados, el error tenderá a disminuir.
El RMSLE se utiliza cuando los valores reales abarcan varios órdenes de magnitud. Por definición, los valores previstos y observados reales no pueden ser negativos.
Los algoritmos para calcular todas las métricas anteriores se encuentran en el archivo fuente VectorRegressionMetric.mqh
Modelos ONNX
Utilizaremos 4 modelos de regresión que predicen el precio de cierre del día (EURUSD, D1) basándose en las barras diarias anteriores. Ya hemos analizado estos modelos en artículos anteriores: "Envolviendo modelos ONNX en clases", "Ejemplo de un conjunto de modelos ONNX en MQL5" y "Uso de modelos ONNX en MQL5". Por ello, no repetiremos aquí las reglas utilizadas para entrenar estos modelos. Los scripts de entrenamiento para todos los modelos se encuentran en la subcarpeta Python del archivo zip adjunto a este artículo. Los modelos onnx entrenados (model.eurusd.D1.10, model.eurusd.D1.30, model.eurusd.D1.52 y model.eurusd.D1.63) se encuentran en dicho archivo.
Envolviendo modelos ONNX en clases
En el artículo anterior, mostramos la clase básica para los modelos ONNX y las clases derivadas para los modelos de clasificación. Hemos realizado algunos cambios menores en la clase básica para hacerla más 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); } }; //+------------------------------------------------------------------+
Así, hemos añadido el parámetro datetime date a los métodos PredictPrice y PredictClass para hacer predicciones en cualquier momento, no solo en el actual. Esto nos resultará útil para formar un vector de predicción.
Clase para el modelo D1_10
Nuestro primer modelo se llamará model.eurusd.D1.10.onnx. Es un modelo de regresión entrenado en EURUSD D1 con una serie de 10 precios 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); } }; //+------------------------------------------------------------------+
Este modelo es similar a nuestro primer modelo publicado en el proyecto público MQL5\Shared Projects\ONNX.Price.Prediction.
La serie de 10 precios OHLC debe normalizarse de la misma forma que durante el entrenamiento, es decir, la desviación del precio promedio de la serie se dividirá por la desviación estándar de la misma. Esto nos permitirá colocar la serie en un rango determinado, con una media de 0 y una variación de 1, lo cual mejorará la convergencia durante el entrenamiento.
Clase para el modelo D1_30
El segundo modelo se llamará model.eurusd.D1.30.onnx. Es un modelo de regresión entrenado en EURUSD D1 utilizando una serie de 30 precios de cierre y dos medias móviles simples con periodos de promediación de 21 y 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); } }; //+------------------------------------------------------------------+
Al igual que en la clase anterior, el método Init llamará al método de clase básica CheckInit, donde se creará una sesión para el modelo ONNX y los tamaños de los tensores de entrada y salida se establecerán de forma explícita.
En el método PredictPrice se proporcionará una serie con los 30 cierres anteriores y las medias móviles calculadas. Los datos se normalizarán de la misma forma que en el entrenamiento.
Este modelo fue desarrollado para el artículo "Envolviendo modelos ONNX en clases", y convertido de clasificatorio a regresivo para este artículo.
Clase para el modelo D1_52
El tercer modelo se llamará model.eurusd.D1.52.onnx. Es un modelo de regresión entrenado en EURUSD D1 con una serie de 52 precios de cierre.
//+------------------------------------------------------------------+ //| 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 normalización de los precios antes de suministrar el modelo a la entrada se distinguirá de las anteriores. Durante el entrenamiento se ha utilizado MinMaxScaler. Por lo tanto, tomaremos los precios mínimos y máximos para un periodo de 52 semanas antes de la fecha prevista.
Este modelo es similar al descrito en el artículo "Uso de modelos ONNX en MQL5".
Clase para el modelo D1_63
Y finalmente, el cuarto modelo se llamará model.eurusd.D1.63.onnx. Es un modelo de regresión entrenado en EURUSD D1 con una serie de 63 precios de cierre.
//+------------------------------------------------------------------+ //| 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); } }; //+------------------------------------------------------------------+
En el método PredictPrice se ofrece una serie con los 63 Close anteriores. Los datos se normalizarán de la misma forma que en el primer y segundo modelo.
Este modelo fue desarrollado para el artículo "Ejemplo de un conjunto de modelos ONNX en MQL5".
Recopilaremos todos los modelos en un solo script. Realidad, predicciones y métricas de regresión.
Para aplicar las métricas de regresión, deberemos realizar una cierta cantidad de predicciones (vector_pred) y tomar datos reales para las mismas fechas (vector_true).
Como todos nuestros modelos están envueltos en clases que se derivan de la misma clase básica, podremos evaluarlos todos a la vez.
Un script muy 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); } //+------------------------------------------------------------------+
Ejecutaremos este script en el gráfico EURUSD,D1 e indicaremos las fechas desde el 1 de enero de 2023 hasta el 31 de enero inclusive. Qué es lo que veremos
[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
Lo que se nota de inmediato es el valor del cuadrado R negativo en la segunda fila. Esto significa que el modelo no funciona. Resulta interesante mirar los gráficos de predicción.
Vemos el gráfico de D1_30 muy alejado de los precios de cierre reales y de otras previsiones. Ninguna de las métricas de este modelo es alentadora. ¡El MAE muestra una precisión de pronóstico de 1809 puntos de precio! Sin embargo, no debemos olvidar que este modelo se desarrolló inicialmente para el artículo anterior como modelo de clasificación, no como modelo de regresión. Es un ejemplo muy bueno y claro.
Vamos a analizar los otros modelos por separado.
El primer candidato al análisis es 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
Veamos el gráfico de los precios predichos por este modelo.
La métrica de RMSLE no tiene mucho sentido, ya que la variación de 1,05 a 1,09 es muy inferior a un orden de magnitud. Las métricas de MAPE y de MSPE tienen valores cercanos a MAE y MSE debido a las peculiaridades del tipo de cambio eurusd, que está próximo a uno. No obstante, al calcular las desviaciones porcentuales hay un matiz que no está presente al calcular las desviaciones absolutas.
MAPE = |(y_true-y_pred)/y_true| при y_true = 10 и y_pred = 5 MAPE = 0.5 при y_true = 5 и y_pred = 10 MAPE = 1.0
Es decir, esta métrica (al igual que MSPE) es asimétrica. Esto significa que si el pronóstico es superior a los hechos, obtendremos un error mayor.
El resultado de la métrica del cuadrado R es bueno. Y esto considerando que es un modelo simple, hecho "con prisas" y fines puramente metodológicos, para mostrar cómo se puede trabajar con modelos ONNX en MQL5.
Segundo candidato - 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
A nivel puramente visual, la previsión resulta muy similar a la anterior. Los valores de las métricas confirman la similitud
[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
A continuación veremos cuál de estos modelos funcionará mejor en el simulador durante el mismo periodo.
Y finalmente, 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
Solo lo consideraremos porque su cuadrado R es superior a 0,5
Casi todos los precios previstos están por debajo del gráfico Close, como en el peor de nuestros casos. A pesar de que los valores de las métricas no son comparables con los valores de los dos modelos anteriores, este modelo no inspira ningún optimismo, lo cual comprobaremos en el siguiente punto.
Comprobando los modelos ONNX en el simulador
Aquí tenemos un asesor muy sencillo para probar nuestros modelos en el simulador
//+------------------------------------------------------------------+ //| 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); } } //+------------------------------------------------------------------+
Y ciertamente, de acuerdo con el modelo D1_52, solo se ha abierto una transacción de venta y, según este modelo, durante todo el periodo de prueba la tendencia no ha cambiado.
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).
Como hemos mencionado en el punto anterior, el modelo D1_52 no inspira optimismo. Esto es lo que vemos en los resultados de las pruebas.
Cambiaremos solo dos líneas de código.
#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;
y ejecutaremos el modelo D1_10 para realizar las pruebas.
Vemos que los resultados son buenos. El gráfico de la prueba también es bueno.
Ahora corregiremos nuevamente dos líneas de código y probaremos el modelo D1_63.
Y el gráfico.
El gráfico de prueba es mucho peor que el del modelo D1_10.
Al comparar los dos modelos D1_10 y D1_63, hemos visto que el primer modelo tiene mejores métricas de regresión que el segundo modelo. El simulador ha mostrado lo mismo.
Importante: tenga en cuenta que los modelos usados en este artículo se presentan únicamente para mostrar el trabajo con modelos ONNX utilizando el lenguaje MQL5. El asesor no ha sido diseñado para comercia en cuentas reales.
Conclusión
La métrica más adecuada para evaluar los modelos de predicción de precios es el cuadrado R. También resulta muy útil analizar el conjunto MAE - RMSE - MAPE. En las tareas de previsión de precios, no es necesario considerar la métrica RMSLE. Resulta de mucha utilidad tener varios modelos para evaluar, incluso si es el mismo modelo pero con modificaciones.
Entendemos que una muestra de 22 valores es insuficiente para una investigación seria, pero no era nuestra intención hacer un estudio estadístico, solo estamos ofreciendo un ejemplo de uso.
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/12772
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso