
Оценка ONNX-моделей при помощи регрессионных метрик
Введение
Регрессия — это задача предсказания вещественной величины по непомеченному примеру. Широко известный пример регрессии — оценка стоимости бриллианта на основе таких его характеристик, как размеры, вес, цвет, чистота и т. д.
Для оценки точности предсказаний регрессионных моделей предназначены так называемые метрики регрессии. Несмотря на схожие алгоритмы, регрессионные метрики семантически отличаются от аналогичных функций потерь. Важно понимать разницу между ними. Её можно сформулировать следующим образом:
-
Функция потерь возникает в тот момент, когда мы сводим задачу построения модели к задаче оптимизации. Обычно требуется, чтобы она обладала хорошими свойствами (например, дифференцируемостью).
-
Метрика — внешний, объективный критерий качества, обычно зависящий не от параметров модели, а только от предсказанных значений.
Регрессионные метрики в MQL5
В языке MQL5 реализованы следующие метрики:
- Средняя абсолютная ошибка (Mean Absolute Error, MAE)
- Среднеквадратичная ошибка (Mean Squared Error, MSE)
- Корень из среднеквадратичной ошибки (Root Mean Squared Error, RMSE)
- Коэффициент детерминации R-квадрат (R-squared, R2)
- Средняя абсолютная процентная ошибка (Mean Absolute Percentage Error, MAPE)
- Среднеквадратичная процентная ошибка (Mean Squared Percentage Error, MSPE)
- Корень из среднеквадратичной логарифмической ошибки (Root Mean Squared Logarithmic Error, RMSLE)
Предполагается, что состав регрессионных метрик в MQL5 будет расширен.
Краткие характеристики регрессионных метрик
MAE оценивает абсолютную ошибку — то, насколько спрогнозированное число разошлось с фактическим числом. Погрешность измеряется в тех же единицах, что и значение целевой функции. Значение ошибки интерпретируется, исходя из диапазона возможных значений. Например, если целевые значения находятся в диапазоне от 1 до 1.5, то средняя абсолютная ошибка со значением 10 — это очень большая ошибка, для диапазона 10000...15000 — вполне приемлемая. Не подходит для оценки прогнозов с большим разбросом значений.
В MSE за счёт возведения в квадрат каждая ошибка имеет свой вес. Большие расхождения между прогнозом и действительностью заметны гораздо сильнее из-за этого же.
RMSE имеет те же преимущества, что и MSE, но более удобна для понимания, так как погрешность измеряется в тех же единицах, что и значения целевой функции. Очень чувствительна к аномалиям и выбросам. MAE и RMSE могут использоваться вместе для диагностики вариации ошибок в наборе прогнозов. RMSE всегда будет больше или равно MAE. Чем больше разница между ними, тем больше разброс индивидуальных ошибок в выборке. Если RMSE = MAE, то все ошибки имеют одинаковую величину.
R2 — коэффициент детерминации показывает силу связи между двумя случайными величинами. Помогает понять, какую долю разнообразия данных модель смогла объяснить. Если модель всегда предсказывает точно, метрика равна 1. Для тривиальной модели — 0. Значение метрики может быть отрицательно, если модель предсказывает хуже, чем тривиальная, если модель не соответствует тренду данных.
MAPE ошибка не имеет размерности и очень проста в интерпретации. Её можно выражать как в долях, так и в процентах. В MQL5 выражается в долях. Например, значение 0.1 говорит о том, что ошибка составила 10% от фактического значения. Идея этой метрики — это чувствительность к относительным отклонениям. Не подходит для задач, где нужно работать с реальными единицами измерения.
MSPE можно рассматривать как взвешенную версию MSE, где вес обратно пропорционален квадрату наблюдаемого значения. Таким образом, при возрастании наблюдаемых значений ошибка имеет тенденцию уменьшаться.
RMSLE используется, когда фактические значения простираются на несколько порядков величины. По определнию прогнозные и фактические наблюдаемые значения не могут быть отрицательными.
Алгоритмы расчётов всех вышеуказанных метрик представлены в исходном файле VectorRegressionMetric.mqh
ONNX-модели
Мы использовали 4 регрессионные модели, прогнозирующие цену закрытия дня (EURUSD, D1) по предыдущим дневным барам. Эти модели мы рассматривали в предыдущих статьях: "Оборачиваем ONNX-модели в классы", "Пример ансамбля ONNX-моделей в MQL5" и "Использование ONNX-моделей в MQL5". Поэтому не будем повторять здесь правила, по которым обучались эти модели. Скрипты обучения всех моделей расположены в подпапке Python zip-архива, прицепленного к данной статье. Обученные onnx-модели — model.eurusd.D1.10, model.eurusd.D1.30, model.eurusd.D1.52 и model.eurusd.D1.63 — находятся там же.
Оборачиваем ONNX-модели в классы
В предыдущей статье мы показали базовый класс для ONNX-моделей и производные классы для моделей классификации. Мы внесли небольшие изменения в базовый класс, сделав его гибче.
//+------------------------------------------------------------------+ //| 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); } }; //+------------------------------------------------------------------+
Мы добавили в методы PredictPrice и PredictClass параметр datetime date для того, чтобы можно было делать предсказания на любой момент времени, а не только на текущий. Это пригодится нам для формирования вектора предсказаний.
Класс для модели D1_10
Наша первая модель называется model.eurusd.D1.10.onnx. Регрессионная модель, тренированная на EURUSD D1 на сериях из 10 цен 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); } }; //+------------------------------------------------------------------+
Это модель аналогична нашей самой первой модели, опубликованной в публичном проекте MQL5\Shared Projects\ONNX.Price.Prediction.
Серия из 10 цен OHLC должна быть нормирована так же, как и при обучении, а именно производится деление отклонения от средней цены в серии на стандартное отклонение в серии. Это позволяет уложить серию в некий диапазон, у которого среднее 0 и разброс 1, что улучшает сходимость при обучении.
Класс для модели D1_30
Вторая модель называется model.eurusd.D1.30.onnx. Регрессионная модель, тренированная на EURUSD D1 на сериях из 30 цен Close и двух простых скользящих средних с периодами усреднения 21 и 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); } }; //+------------------------------------------------------------------+
Как и в предыдущем классе, в методе Init вызывается метод базового класса CheckInit, где создаётся сессия для ONNX-модели и явно выставляются размеры входного и выходного тензоров.
В методе PredictPrice обеспечиваются серии из 30 предыдущих Close и рассчитанные скользящие средние. Данные нормируются тем же способом, что и при обучении.
Данная модель была разработана для статьи "Оборачиваем ONNX-модели в классы", преобразована из классификационной в регрессионную для данной статьи.
Класс для модели D1_52
Третья модель называется model.eurusd.D1.52.onnx. Регрессионная модель, тренированная на EURUSD D1 на сериях из 52 цен Close.
//+------------------------------------------------------------------+ //| 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(); } }; //+------------------------------------------------------------------+
Нормирование цены перед подачей на вход модели отличается от предыдущих. При тренировке использовался MinMaxScaler. Поэтому мы берём минимум и максимум цен за период 52 недели до даты прогноза.
Эта модель аналогична описанной в статье "Использование ONNX-моделей в MQL5".
Класс для модели D1_63
И наконец, четвёртая модель называется model.eurusd.D1.63.onnx. Регрессионная модель, тренированная на EURUSD D1 на сериях из 63 цен Close.
//+------------------------------------------------------------------+ //| 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); } }; //+------------------------------------------------------------------+
В методе PredictPrice обеспечиваются серии из 63 предыдущих Close. Данные нормируются тем же способом, что и в первой и второй моделях.
Данная модель была разработана для статьи "Пример ансамбля ONNX-моделей в MQL5".
Собираем все модели в одном скрипте. Действительность, прогнозы и регрессионные метрики
Для того, чтобы применить регрессионные метрики мы должны сделать некоторое число прогнозов (vector_pred) и взять фактические данные для тех же дат (vector_true).
Так как все наши модели обёрнуты в классы, происходящие от одного и того же базового класса, то мы можем за один раз их все оценить.
Очень простой скрипт
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Запустим данный скрипт на графике EURUSD,D1 и укажем даты с 1 января 2023 по 31 января включительно. И что же мы видим
[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
Сразу же бросается в глаза отрицательное значение R-квадрат во второй строке. Это означает неработоспособность модели. Интересно посмотреть на графики предсказаний.
Мы видим график D1_30 далеко в стороне от действительных цен Close и других прогнозов. Ни одна из метрик этой модели не радует. MAE показывает точность прогноза 1809 пунктов цены! Но не забываем, что данная модель изначально разрабатывалась для предыдущей статьи как классификационная, а не регрессионная. Очень хороший и наглядный пример.
Рассмотрим другие модели по отдельности.
Первый кандидат на анализ — 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
Посмотрим на график предсказанных этой моделью цен.
Метрика RMSLE особого смысла не имеет, поскольку разброс от 1.05 до 1.09 — это гораздо меньше одного порядка. Метрики MAPE и MSPE по своим значениям близки к MAE и MSE из-за особенностей курса eurusd — он близок к единице. Однако, при расчёте процентных отклонений есть нюанс, которого нет при расчёте абсолютных оклонений.
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
То есть, данная метрика (как и MSPE) — несимметрична. Это значит, что в случае, когда прогноз выше факта, мы получаем бОльшую ошибку.
Хороший результат метрики R-квадрат. И это для простой модели, сделанной "на коленке" в чисто методических целях — показать, как можно работать с ONNX-моделями в MQL5.
Второй кандидат - 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
Чисто визуально прогноз очень похож на предыдущий. Значения метрик подтверждают похожесть
[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
Дальше мы посмотрим, какая же из этих моделей покажет себя лучше в тестере на этом же периоде.
И наконец, 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
Мы его рассматриваем только потому, что его R-квадрат больше 0.5
Практически все спрогнозированные цены ниже графика Close, как и в нашем самом худшем случае. Несмотря на сопоставимые значения метрик с метриками предыдущих двух моделей, данная модель не внушает никакого оптимизма. Что мы и проверим в следующем пункте.
Проверяем ONNX-модели в тестере
Очень простой эксперт для проверки наших моделей в тестере
//+------------------------------------------------------------------+ //| 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); } } //+------------------------------------------------------------------+
И действительно, согласно модели D1_52 была открыта всего одна сделка на продажу и на протяжении всего периода тестирования тренд, согласно этой модели, не менялся
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).
Как было сказано в предыдущем пункте, модель D1_52 не внушает оптимизма. Что мы и видим по результатам тестирования.
Поменяем всего две строчки кода
#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;
и запустим на тестирование модель модель D1_10.
Мы видим хороший результат. График тестирования тоже неплох.
Опять исправим две строчки кода и протестируем модель D1_63.
И график.
График тестирования гораздо хуже, чем у модели D1_10.
Сравнивая две модели D1_10 и D1_63, мы видели, что у первой модели регрессионные метрики лучше, чем у второй модели. Тестер показал то же самое.
Важно: обращаем ваше внимание, что использованные в статье модели представлены только в целях демонстрации работы с ONNX-моделями средствами языка MQL5. Советник не предназначен для торговли на реальных счетах.
Заключение
Наиболее подходящей метрикой для оценки моделей прогнозирования цены является R-квадрат. Очень полезно рассматривать совокупность MAE - RMSE - MAPE. В задачах прогнозирования цены метрику RMSLE можно не рассматривать. Очень полезно иметь для оценки несколько моделей, даже если это будет одна и та же модель, но с модификациями.
Мы понимаем, что для серьёзных исследований выборка из 22 значений недостаточна, но у нас не было намерения сделать статистическое исследование. Только пример использования.





- Бесплатные приложения для трейдинга
- Форексный VPS бесплатно на 24 часа
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования