
Bewertung von ONNX-Modellen anhand von Regressionsmetriken
Einführung
Bei der Regression geht es um die Prognose eines realen Wertes anhand eines unbekannten Beispiels. Ein typisches Beispiel für eine Regression ist die Schätzung des Wertes eines Diamanten anhand von Merkmalen wie Größe, Gewicht, Farbe, Reinheit usw.
Die so genannten Regressionsmetriken werden verwendet, um die Genauigkeit der Vorhersagen des Regressionsmodells zu bewerten. Trotz ähnlicher Algorithmen unterscheiden sich die Regressionsmetriken semantisch von den ähnlichen Verlustfunktionen. Es ist wichtig, den Unterschied zwischen ihnen zu verstehen. Sie kann wie folgt formuliert werden:
-
Die Verlustfunktion entsteht in dem Moment, in dem wir das Problem der Erstellung eines Modells auf ein Optimierungsproblem reduzieren. In der Regel wird verlangt, dass sie gute Eigenschaften hat (z. B. Differenzierbarkeit).
-
Eine Metrik ist ein externes, objektives Qualitätskriterium, das in der Regel nicht von den Modellparametern, sondern nur von den prognostizierten Werten abhängt.
Regressionsmetriken in MQL5
Die Sprache MQL5 verfügt über die folgenden Metriken:
- Mittlerer absoluter Fehler (Mean Absolute Error, MAE)
- Mittlerer quadratischer Fehler (Mean Squared Error, MSE)
- Wurzel des mittleren, quadratischen Fehlers (Root Mean Squared Error, RMSE)
- R-Quadrat (R-squared, R2)
- Mittlerer absoluter prozentualer Fehler (Mean Absolute Percentage Error, MAPE)
- Mittlerer quadrierter prozentualer Fehler (Mean Squared Percentage Error, MSPE)
- Wurzel des logarithmischen quadratischen Fehlers (Root Mean Squared Logarithmic Error, RMSLE)
Es wird erwartet, dass die Anzahl der Regressionsmetriken in MQL5 erhöht wird.
Kurzcharakteristik der Regressionsmetriken
MAE schätzt den absoluten Fehler — wie stark die prognostizierte Zahl von der tatsächlichen Zahl abweicht. Der Fehler wird in denselben Einheiten gemessen wie der Wert der Zielfunktion. Der Fehlerwert wird auf der Grundlage des möglichen Wertebereichs interpretiert. Wenn die Zielwerte beispielsweise im Bereich von 1 bis 1,5 liegen, dann ist der durchschnittliche absolute Fehler mit einem Wert von 10 ein sehr großer Fehler; aber für einen Bereich von 10000...15000 wäre er durchaus akzeptabel. Sie eignet sich nicht für die Auswertung von Prognosen mit einer großen Streuung der Werte.
Beim MSE hat jeder Fehler aufgrund der Quadrierung sein eigenes Gewicht. Große Diskrepanzen zwischen Prognose und Realität werden dadurch viel deutlicher.
Der RMSE hat dieselben Vorteile wie der MSE, ist aber leichter zu verstehen, da der Fehler in denselben Einheiten gemessen wird wie die Werte der Zielfunktion. Es ist sehr empfindlich gegenüber Anomalien und Spitzen. MAE und RMSE können zusammen verwendet werden, um die Fehlerabweichung in einer Reihe von Vorhersagen zu bestimmen. Der RMSE ist immer größer als oder gleich dem MAE. Je größer die Differenz zwischen ihnen ist, desto größer ist die Streuung der einzelnen Fehler in der Stichprobe. Wenn RMSE = MAE, haben alle Fehler die gleiche Größe.
R2 — Bestimmtheitsmaß zeigt die Stärke der Beziehung zwischen zwei Zufallsvariablen. Damit lässt sich der Anteil der Datenvielfalt bestimmen, den das Modell erklären konnte. Wenn das Modell immer genau vorhersagt, ist die Metrik 1. Für das triviale Modell ist er 0. Der metrische Wert kann negativ sein, wenn das Modell eine schlechtere Prognose als die triviale Prognose liefert und das Modell nicht dem Trend der Daten folgt.
MAPE — der Fehler hat keine Dimension und ist sehr einfach zu interpretieren. Er kann sowohl als Dezimalzahl als auch als Prozentsatz ausgedrückt werden. In MQL5 wird er in Dezimalzahlen ausgedrückt. Ein Wert von 0,1 bedeutet zum Beispiel, dass der Fehler 10 % des tatsächlichen Wertes beträgt. Die Idee hinter dieser Metrik ist die Empfindlichkeit gegenüber relativen Abweichungen. Es ist nicht für Aufgaben geeignet, bei denen Sie mit echten Maßeinheiten arbeiten müssen.
MSPE kann als eine gewichtete Version von MSE betrachtet werden, wobei das Gewicht umgekehrt proportional zum Quadrat des beobachteten Wertes ist. Wenn also die beobachteten Werte steigen, nimmt der Fehler tendenziell ab.
RMSLE wird verwendet, wenn sich die tatsächlichen Werte über mehrere Größenordnungen erstrecken. Per Definition können prognostizierte und tatsächlich beobachtete Werte nicht negativ sein.
Die Algorithmen zur Berechnung aller oben genannten Metriken sind in der Quelldatei VectorRegressionMetric.mqh enthalten
ONNX-Modelle
Wir haben 4 Regressionsmodelle verwendet, die den Schlusskurs des Tages (EURUSD, D1) aus den vorherigen Tagesbalken vorhersagen. Wir haben diese Modelle in den vorangegangenen Artikeln berücksichtigt: „ONNX-Modelle in Klassen packen“, „Ein Beispiel für die Zusammenstellung von ONNX-Modellen in MQL5“ und „Wie man ONNX-Modelle in MQL5 verwendet“. Daher werden wir hier nicht die Regeln wiederholen, die zum Trainieren der Modelle verwendet wurden. Die Skripte für das Training aller Modelle befinden sich im Unterordner Python des Zip-Archivs, das diesem Artikel beigefügt ist. Die trainierten onnx-Modelle — model.eurusd.D1.10, model.eurusd.D1.30, model.eurusd.D1.52 und model.eurusd.D1.63 — befinden sich ebenfalls dort.
ONNX-Modelle in Klassen packen
Im vorherigen Artikel haben wir die Basisklasse für ONNX-Modelle und abgeleitete Klassen für Klassifizierungsmodelle vorgestellt. Wir haben einige kleinere Änderungen an der Basisklasse vorgenommen, um sie flexibler zu machen.
//+------------------------------------------------------------------+ //| 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); } }; //+------------------------------------------------------------------+
Wir haben den Methoden PredictPrice und PredictClass einen Datetime-Parameter hinzugefügt, damit wir Vorhersagen für jeden beliebigen Zeitpunkt treffen können, nicht nur für den aktuellen. Dies wird für die Bildung eines Vorhersagevektors nützlich sein.
D1_10 Modellklasse
Unser erstes Modell heißt model.eurusd.D1.10.onnx. Regressionsmodell trainiert mit EURUSD D1 auf der Serie von 10 OHLC-Preisen.//+------------------------------------------------------------------+ //| 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); } }; //+------------------------------------------------------------------+
Dieses Modell ähnelt unserem allerersten Modell, das in dem öffentlichen Projekt MQL5\Shared Projects\ONNX.Price.Prediction.
Die Serie von 10 OHLC-Kursen sollte auf die gleiche Weise wie beim Training normalisiert werden, d. h. die Abweichung vom Durchschnittspreis in der Serie wird durch die Standardabweichung in der Serie geteilt. So legen wir die Reihen in einen bestimmten Bereich mit einem Mittelwert von 0 und einer Streuung von 1, was die Konvergenz beim Training verbessert.
Die Modellklasse D1_30
Das zweite Modell heißt model.eurusd.D1.30.onnx. Das Regressionsmodell, das auf EURUSD D1 anhand der Serie von 30 Schlusskursen und zwei einfachen gleitenden Durchschnitten mit Mittelungszeiträumen von 21 und 34 trainiert wurde.
//+------------------------------------------------------------------+ //| 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); } }; //+------------------------------------------------------------------+
Wie in der vorherigen Klasse wird die Methode der Basisklasse CheckInit in der Init-Methode aufgerufen. In der Basisklassenmethode wird eine Sitzung für das ONNX-Modell erstellt und die Größen der Eingabe- und Ausgabetensoren werden explizit festgelegt.
Die Methode PredictPrice liefert eine Reihe von 30 vorherigen Schlusskursen und berechneten gleitenden Durchschnitten. Die Daten werden auf die gleiche Weise normalisiert wie beim Training.
Das Modell wurde für den Artikel „ONNX-Modelle in Klassen packen“ entwickelt und für diesen Artikel von Klassifikation auf Regression umgestellt.
Die Modellklasse D1_52
Das dritte Modell heißt model.eurusd.D1.52.onnx. Das Regressionsmodell, das auf EURUSD D1 anhand der Serie von 52 Schlusskursen trainiert wurde.
//+------------------------------------------------------------------+ //| 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(); } }; //+------------------------------------------------------------------+
Die Preisnormalisierung vor der Einreichung des Modells unterscheidet sich von den vorherigen. Beim Training wurde der MinMaxScaler verwendet. Daher nehmen wir die Mindest- und Höchstpreise für den Zeitraum von 52 Wochen vor dem Prognosetermin.
Das Modell ähnelt dem in dem Artikel „Wie man ONNX-Modelle in MQL5 verwendet“ beschriebenen Modell.
Die Modellklasse D1_63
Das vierte Modell schließlich heißt model.eurusd.D1.63.onnx. Das Regressionsmodell, das auf EURUSD D1 anhand der Serie von 63 Schlusskursen trainiert wurde.
//+------------------------------------------------------------------+ //| 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); } }; //+------------------------------------------------------------------+
Die Methode PredictPrice liefert die Serie von 63 vorherigen Schlusskursen. Die Daten werden auf die gleiche Weise normalisiert wie beim ersten und zweiten Modell.
Das Modell wurde bereits für den Artikel „Ein Beispiel für die Zusammenstellung von ONNX-Modellen in MQL5“ entwickelt.
Alle Modelle in einem Skript zusammenfassen. Realität, Vorhersagen und Regressionsmetriken
Um Regressionsmetriken anzuwenden, sollten wir eine bestimmte Anzahl von Vorhersagen (vector_pred) machen und tatsächliche Daten für die gleichen Daten (vector_true) nehmen.
Da alle unsere Modelle in Klassen verpackt sind, die von derselben Basisklasse abgeleitet sind, können wir sie alle auf einmal auswerten.
Das Skript ist sehr einfach
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Führen wir das Skript im EURUSD-D1-Chart aus und legen wir die Daten vom 1. Januar bis einschließlich 31. Januar 2023 fest. Was sehen wir?
[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
Sofort fällt der negative R-Quadrat-Wert in der zweiten Zeile auf. Das bedeutet, dass das Modell nicht funktioniert. Es ist interessant, sich die Vorhersagediagramme anzusehen.
Wir sehen das Diagramm D1_30 weit entfernt von den aktuellen Schlusskursen und anderen Prognosen. Keine der Kennzahlen dieses Modells ist ermutigend. MAE zeigt die Prognosegenauigkeit von 1809 Preispunkten! Beachten Sie jedoch, dass das Modell ursprünglich für den vorhergehenden Artikel als Klassifizierungsmodell und nicht als Regressionsmodell entwickelt wurde. Das Beispiel ist ziemlich eindeutig.
Gehen wir einzeln auf die anderen Modelle ein.
Der erste Kandidat für die Analyse ist 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
Werfen wir einen Blick auf das Diagramm der von diesem Modell prognostizierten Preise.
Die RMSLE-Metrik ist nicht sehr sinnvoll, da die Spanne von 1,05 bis 1,09 viel geringer als eine Größenordnung ist. Die MAPE- und MSPE-Metriken liegen in ihren Werten nahe bei MAE und MSE, was auf die Besonderheiten des EURUSD-Wechselkurses zurückzuführen ist, da dieser nahe bei eins liegt. Bei der Berechnung der prozentualen Abweichungen gibt es jedoch eine Nuance, die bei der Berechnung der absoluten Abweichungen nicht vorhanden ist.
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
Mit anderen Worten: Diese Metrik ist (wie MSPE) asymmetrisch. Das bedeutet, dass in dem Fall, in dem die Prognose größer ist als der tatsächliche Wert, ein größerer Fehler auftritt.
Ein gutes Ergebnis der R-Quadrat-Metrik wurde für das einfache Modell erzielt, das aus rein methodischen Gründen zusammengeschustert wurde, nämlich um zu zeigen, wie man mit ONNX-Modellen in MQL5 arbeiten kann.
Zweiter Kandidat — 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
Die Prognose ähnelt visuell sehr der vorherigen. Metrische Werte bestätigen Ähnlichkeit
[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
Als Nächstes werden wir sehen, welches dieser Modelle im gleichen Zeitraum im Tester besser abschneidet.
Jetzt für 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
Wir berücksichtigen ihn nur, weil sein R-Quadrat größer als 0,5 ist.
Fast alle prognostizierten Preise liegen unterhalb des Charts der Schlusskurse, wie in unserem Worst-Case-Fall. Trotz vergleichbarer metrischer Werte wie bei den beiden vorangegangenen Modellen gibt dieses Modell keinen Anlass zu Optimismus. Wir werden dies im nächsten Abschnitt überprüfen.
Ausführen von ONNX-Modellen im Testgerät
Nachfolgend finden Sie ein sehr einfachen EA zur Überprüfung unserer Modelle im Tester
//+------------------------------------------------------------------+ //| 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); } } //+------------------------------------------------------------------+
Nach dem Modell D1_52 wurde nur ein einziger Verkauf eröffnet, und der Trend hat sich nach diesem Modell während des gesamten Testzeitraums nicht geändert.
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).
Wie bereits im vorangegangenen Abschnitt erwähnt, gibt das Modell D1_52 keinen Anlass zu Optimismus. Dies wird durch die Testergebnisse bestätigt.
Ändern wir nur zwei Codezeilen
#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;
und starten das Modell D1_10 für einen Test.
Die Ergebnisse sind gut. Auch das Testdiagramm ist vielversprechend.
Korrigieren wir noch einmal zwei Codezeilen und testen das Modell D1_63.
Die Grafik.
Das Testdiagramm ist viel schlechter als das des Modells D1_10.
Vergleicht man die beiden Modelle D1_10 und D1_63, so zeigt sich, dass das erste Modell bessere Regressionskennzahlen aufweist als das zweite. Der Tester zeigt das Gleiche an.
Wichtiger Hinweis: Bitte beachten Sie, dass die in diesem Artikel verwendeten Modelle nur zur Demonstration der Arbeit mit ONNX-Modellen in der Sprache MQL5 dienen. Der Expert Advisor ist nicht für einen Handel auf realen Konten gedacht.
Schlussfolgerung
Die am besten geeignete Metrik zur Bewertung von Preisvorhersagemodellen ist R-Quadrat. Die Betrachtung des Aggregats aus MAE — RMSE — MAPE kann sehr hilfreich sein. Die Metrik von RMSLE sollte bei Preisprognosen nicht berücksichtigt werden. Es ist sehr nützlich, mehrere Modelle zur Bewertung zu haben, auch wenn es sich um das gleiche Modell mit Änderungen handelt.
Wir verstehen, dass eine Stichprobe von 22 Werten für eine seriöse Untersuchung nicht ausreicht, aber es war nicht unsere Absicht, eine statistische Studie durchzuführen. Wir haben stattdessen nur die Verwendung veranschaulicht.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/12772





- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.