В предыдущей статье мы использовали две ONNX-модели для организации классификатора голосования. При этом весь исходный текст был организван в виде одного MQ5-файла. Да, весь код разбит на функции. Но попробуйте, например, поменять местами модели. А если добавить ещё модель? Исходный текст ещё более распухнет. Попробуем объектно-ориентированный подход.
1. Какие модели мы собираемся использовать
В предыдущем классификаторе голосования мы использовали одну классификационную модель и одну регрессионную модель. В регрессионной модели вместо предсказанного движения цены (вниз, вверх, не изменяется) мы получаем предсказанную цену, на основе которой и вычисляем класс. Однако, в этом случае мы не имеем распределения вероятностей по классам, что не позволяет проводить так называемое "мягкое голосование".
Мы подготовили 3 классификационные модели. Две модели уже использовались в статье "Пример ансамбля ONNX-моделей в MQL5". Первая модель — регрессионная — переделана в классификационную. Обучение проводилось на сериях из 10 цен OHLC. Вторая модель — классификационная. Обучение проводилось на сериях из 63 цен Close.
Наконец, ещё одна модель. Классификационная модель обучалась на сериях из 30 цен Close и сериях простых скользящих средних с периодами усреднения 21 и 34. Мы не делали никаких предположений по поводу пересечения скользящих средних с графиком Close и между собой — все закономерности посчитает и запомнит сеть в виде матриц коэффициентов между слоями.
Все модели обучались на данных сервера MetaQuotes-Demo, EURUSD D1 с 2010.01.01 по 2023.01.01. Тренировочные скрипты всех трёх моделей написаны на Питоне и приложены к данной статье. Мы не будем приводить их исходные коды здесь, чтобы не отвлекать внимание читателя от основной темы нашей статьи.
2. Нужен один базовый класс для всех моделей
Три модели. Каждая отличается от другой размером входных данных, подготовкой входных данных. У всех моделей есть общность. Один и тот же интерфейс. Классы всех моделей должны наследоваться от одного и того же базового класса.
Попробуем представить базовый класс.
//+------------------------------------------------------------------+//| ModelSymbolPeriod.mqh |//| Авторские права 2023, MetaQuotes Ltd. |//| https://www.mql5.com |//+------------------------------------------------------------------+//--- предсказание движения цены#definePRICE_UP0#definePRICE_SAME1#definePRICE_DOWN2//+------------------------------------------------------------------+//| Базовый класс для моделей на основе обученного символа и периода |//+------------------------------------------------------------------+classCModelSymbolPeriod {protected:long m_handle; // созданный обработчик сессии модели string m_symbol; // символ обученных данных ENUM_TIMEFRAMES m_period; // период обученных данных datetime m_next_bar; // время следующего бара (мы работаем только в начале бара)double m_class_delta; // дельта для определения "цена та же самая" в регрессионных моделяхpublic: //+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+CModelSymbolPeriod(conststring symbol,constENUM_TIMEFRAMES period,constdouble class_delta=0.0001) { m_handle=INVALID_HANDLE; m_symbol=symbol; m_period=period; m_next_bar=0; m_class_delta=class_delta; } //+------------------------------------------------------------------+ //| Деструктор | //+------------------------------------------------------------------+~CModelSymbolPeriod(void) {Shutdown(); } //+------------------------------------------------------------------+ //| Виртуальный заглушка для Init | //+------------------------------------------------------------------+virtualboolInit(conststring symbol,constENUM_TIMEFRAMES period) {return(false); } //+------------------------------------------------------------------+ //| Проверка на инициализацию, создание модели | //+------------------------------------------------------------------+boolCheckInit(conststring symbol,constENUM_TIMEFRAMES period,constuchar& model[]) { //--- проверка символа, периодаif(symbol!=m_symbol || period!=m_period) {PrintFormat("Модель должна работать с %s,%s",m_symbol,EnumToString(m_period));return(false); } //--- создание модели из статического буфера m_handle=OnnxCreateFromBuffer(model,ONNX_DEFAULT);if(m_handle==INVALID_HANDLE) {Print("Ошибка OnnxCreateFromBuffer ",GetLastError());return(false); } //--- успешноreturn(true); } //+------------------------------------------------------------------+ //| Освободить сеанс ONNX | //+------------------------------------------------------------------+voidShutdown(void) {if(m_handle!=INVALID_HANDLE) {OnnxRelease(m_handle); m_handle=INVALID_HANDLE; } } //+------------------------------------------------------------------+ //| Проверка на продолжение OnTick | //+------------------------------------------------------------------+virtualboolCheckOnTick(void) { //--- проверка на новый барif(TimeCurrent()<m_next_bar)return(false); //--- установить время следующего бара m_next_bar=TimeCurrent(); m_next_bar-=m_next_bar%PeriodSeconds(m_period); m_next_bar+=PeriodSeconds(m_period); //--- работать на новом дневном бареreturn(true); } //+------------------------------------------------------------------+ //| Виртуальная заглушка для PredictPrice (регрессионная модель) | //+------------------------------------------------------------------+virtualdoublePredictPrice(void) {return(DBL_MAX); } //+------------------------------------------------------------------+ //| Предсказать класс (регрессия -> классификация) | //+------------------------------------------------------------------+virtualintPredictClass(vector& probabilities) {double predicted_price=PredictPrice();if(predicted_price==DBL_MAX)return(-1);int predicted_class=-1;double last_close=iClose(m_symbol,m_period,1); //--- классифицировать предсказанное движение ценыdouble delta=last_close-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; } //--- установить вероятность предсказания как 1.0probabilities.Fill(0);if(predicted_class<(int)probabilities.Size())probabilities[predicted_class]=1; //--- и вернуть предсказанный классreturn(predicted_class); } };//+------------------------------------------------------------------+
Базовый класс можно использовать как для моделей регрессии, так и для моделей классификации. Надо будет только реализовать в классе-наследнике соответствующий метод — PredictPrice или PredictClass.
В базовом классе задаётся с каким символом-периодом должна работать модель (на каких данных обучалась модель). В базовом классе проводится проверка, что эксперт, использующий модель, работает на нужном символе-периоде, а также создаётся ONNX-сессия для исполнения модели. В базовом классе обеспечивается работа только в начале нового бара.
3. Класс для первой модели
Наша первая модель называется model.eurusd.D1.10.class.onnx, то есть классификационная модель, тренированная на EURUSD D1 на сериях из 10 цен OHLC.
//+------------------------------------------------------------------+//| ModelEurusdD1_10Class.mqh |//| Авторские права 2023, MetaQuotes Ltd. |//| https://www.mql5.com |//+------------------------------------------------------------------+#include"ModelSymbolPeriod.mqh"#resource "Python/model.eurusd.D1.10.class.onnx" as uchar model_eurusd_D1_10_class[]//+------------------------------------------------------------------+//| Класс-обертка для ONNX-модели |//+------------------------------------------------------------------+classCModelEurusdD1_10Class:publicCModelSymbolPeriod {private:int m_sample_size;public: //+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+CModelEurusdD1_10Class(void) :CModelSymbolPeriod("EURUSD",PERIOD_D1) { m_sample_size=10; } //+------------------------------------------------------------------+ //| Инициализация ONNX-модели | //+------------------------------------------------------------------+virtualboolInit(conststring symbol,constENUM_TIMEFRAMES period) { //--- проверка символа, периода, создание моделиif(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_10_class)) {Print("model_eurusd_D1_10_class : ошибка инициализации");return(false); } //--- так как не все размеры определены во входном тензоре, их необходимо явно установить //--- первый индекс - размер партии, второй индекс - размер серии, третий индекс - количество серий (OHLC)constlong input_shape[] = {1,m_sample_size,4};if(!OnnxSetInputShape(m_handle,0,input_shape)) {Print("model_eurusd_D1_10_class : ошибка установки формы ввода ",GetLastError());return(false); } //--- так как не все размеры определены в выходном тензоре, их необходимо явно установить //--- первый индекс - размер партии, должен совпадать с размером партии входного тензора //--- второй индекс - количество классов (вверх, тот же или вниз)constlong output_shape[] = {1,3};if(!OnnxSetOutputShape(m_handle,0,output_shape)) {Print("model_eurusd_D1_10_class : ошибка установки формы вывода ",GetLastError());return(false); } //--- успешноreturn(true); } //+------------------------------------------------------------------+ //| Предсказать класс | //+------------------------------------------------------------------+virtualintPredictClass(vector& probabilities) {static matrixf input_data(m_sample_size,4); // матрица для подготовленных входных данныхstatic vectorf output_data(3); // вектор для получения результатаstatic matrix mm(m_sample_size,4); // матрица горизонтальных векторов Meanstatic matrix ms(m_sample_size,4); // матрица горизонтальных векторов Stdstatic matrix x_norm(m_sample_size,4); // матрица для нормализации цен //--- подготовить входные данные matrix rates; //--- запросить последние барыif(!rates.CopyRates(m_symbol,m_period,COPY_RATES_OHLC,1,m_sample_size))return(-1); //--- получить Mean серии vector m=rates.Mean(1); //--- получить Std серии vector s=rates.Std(1); //--- подготовить матрицы для нормализации ценfor(int i=0; i<m_sample_size; i++) {mm.Row(m,i);ms.Row(s,i); } //--- вход модели должен быть набором вертикальных векторов OHLC x_norm=rates.Transpose(); //--- нормализовать цены x_norm-=mm; x_norm/=ms; //--- выполнить выводinput_data.Assign(x_norm);if(!OnnxRun(m_handle,ONNX_NO_CONVERSION,input_data,output_data))return(-1); //--- оценить предсказаниеprobabilities.Assign(output_data);return(int(output_data.ArgMax())); } };//+------------------------------------------------------------------+
Как было сказано выше: "Три модели. Каждая отличается от другой размером входных данных, подготовкой входных данных". И мы переопределили всего два метода — Init и PredictClass. В двух других классах для двух других моделей будут переопределены эти же самые методы.
В методе Init вызывается метод базового класса CheckInit, где создаётся сессия для нашей ONNX-модели. А также явно выставляются размеры входного и выходного тензоров. Здесь больше комментариев, чем кода.
В методе PredictClass обеспечивается точно такая же подготовка входных данных, что и при обучении модели. На вход подаётся матрица нормализованных цен OHLC.
4. Проверим, как это работает
Для проверки работоспособности нашего класса был создан очень компактный эксперт.
//+------------------------------------------------------------------+//| ONNX.eurusd.D1.Prediction.mq5 |//| Авторское право 2023, MetaQuotes Ltd. |//| https://www.mql5.com |//+------------------------------------------------------------------+#property copyright "Авторское право 2023, MetaQuotes Ltd."#property link "https://www.mql5.com"#property version "1.00"//#include "ModelEurusdD1_10Class.mqh"#include"ModelEurusdD1_63Class.mqh"//#include "ModelEurusdD1_30Class.mqh"#include<Trade\Trade.mqh>input double InpLots =1.0; // Объем лотов для открытия позиции//CModelEurusdD1_10Class ExtModel;CModelEurusdD1_63Class ExtModel;//CModelEurusdD1_30Class ExtModel;CTrade ExtTrade;//+------------------------------------------------------------------+//| Функция инициализации эксперта |//+------------------------------------------------------------------+intOnInit() {if(!ExtModel.Init(_Symbol,_Period))return(INIT_FAILED);//---return(INIT_SUCCEEDED); }//+------------------------------------------------------------------+//| Функция деинициализации эксперта |//+------------------------------------------------------------------+voidOnDeinit(constint reason) {ExtModel.Shutdown(); }//+------------------------------------------------------------------+//| Функция тика эксперта |//+------------------------------------------------------------------+voidOnTick() {if(!ExtModel.CheckOnTick())return;//--- предсказать следующее движение цены vector prob(3);int predicted_class=ExtModel.PredictClass(prob);//--- проверить торговлю в соответствии с прогнозомComment("Ответ модели =",predicted_class);if(predicted_class>=0)if(PositionSelect(_Symbol))CheckForClose(predicted_class);elseCheckForOpen(predicted_class); }//+------------------------------------------------------------------+//| Проверить условия открытия позиции |//+------------------------------------------------------------------+voidCheckForOpen(constint predicted_class) { ENUM_ORDER_TYPE signal=WRONG_VALUE;//--- проверить сигналыif(predicted_class==PRICE_DOWN) signal=ORDER_TYPE_SELL; // условие на продажуelse {if(predicted_class==PRICE_UP) signal=ORDER_TYPE_BUY; // условие на покупку }//--- открыть позицию, если это возможно согласно сигналу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); } }//+------------------------------------------------------------------+//| Проверить условия закрытия позиции |//+------------------------------------------------------------------+voidCheckForClose(constint predicted_class) {bool bsignal=false;//--- позиция уже выбрана ранееlong type=PositionGetInteger(POSITION_TYPE);//--- проверить сигналыif(type==POSITION_TYPE_BUY && predicted_class==PRICE_DOWN) bsignal=true;if(type==POSITION_TYPE_SELL && predicted_class==PRICE_UP) bsignal=true;//--- закрыть позицию, если это возможноif(bsignal &&TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) {ExtTrade.PositionClose(_Symbol,3); //--- открыть противоположнуюCheckForOpen(predicted_class); } }//+------------------------------------------------------------------+
Так как модель обучалась на ценовых данных до 2023 года, запустим тестирование с 1 января 2023 года.
И получим следующий результат:
Как видим, вполне работоспособная модель.
5. Класс для второй модели
Вторая модель называется model.eurusd.D1.30.class.onnx. Классификационная модель, тренированная на EURUSD D1 на сериях из 30 цен Close и двух простых скользящих средних с периодами усреднения 21 и 34.
//+------------------------------------------------------------------+//| ModelEurusdD1_30Class.mqh |//| Авторские права 2023, MetaQuotes Ltd. |//| https://www.mql5.com |//+------------------------------------------------------------------+#include"ModelSymbolPeriod.mqh"#resource "Python/model.eurusd.D1.30.class.onnx" as uchar model_eurusd_D1_30_class[]//+------------------------------------------------------------------+//| Класс-обертка для ONNX-модели |//+------------------------------------------------------------------+classCModelEurusdD1_30Class:publicCModelSymbolPeriod {private:int m_sample_size;int m_fast_period;int m_slow_period;int m_sma_fast;int m_sma_slow;public: //+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+CModelEurusdD1_30Class(void) :CModelSymbolPeriod("EURUSD",PERIOD_D1) { m_sample_size=30; m_fast_period=21; m_slow_period=34; m_sma_fast=INVALID_HANDLE; m_sma_slow=INVALID_HANDLE; } //+------------------------------------------------------------------+ //| Инициализация ONNX-модели | //+------------------------------------------------------------------+virtualboolInit(conststring symbol,constENUM_TIMEFRAMES period) { //--- проверка символа, периода, создание моделиif(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_30_class)) {Print("model_eurusd_D1_30_class : ошибка инициализации");return(false); } //--- так как не все размеры определены во входном тензоре, их необходимо явно установить //--- первый индекс - размер партии, второй индекс - размер серии, третий индекс - количество серий (Close, MA fast, MA slow)
constlong input_shape[] = {1,m_sample_size,3};if(!OnnxSetInputShape(m_handle,0,input_shape)) {Print("model_eurusd_D1_30_class : ошибка установки формы ввода ",GetLastError());return(false); } //--- так как не все размеры определены в выходном тензоре, их необходимо явно установить //--- первый индекс - размер партии, должен совпадать с размером партии входного тензора //--- второй индекс - количество классов (вверх, тот же или вниз)constlong output_shape[] = {1,3};if(!OnnxSetOutputShape(m_handle,0,output_shape)) {Print("model_eurusd_D1_30_class : ошибка установки формы вывода ",GetLastError());return(false); } //--- индикаторы 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_class : не удается создать индикатор");return(false); } //--- успешноreturn(true); } //+------------------------------------------------------------------+ //| Предсказать класс | //+------------------------------------------------------------------+virtualintPredictClass(vector& probabilities) {static matrixf input_data(m_sample_size,3); // матрица для подготовленных входных данныхstatic vectorf output_data(3); // вектор для получения результатаstatic matrix x_norm(m_sample_size,3); // матрица для нормализации ценstatic vector vtemp(m_sample_size);staticdouble ma_buffer[]; //--- запросить последние барыif(!vtemp.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,1,m_sample_size))return(-1); //--- получить Mean серииdouble m=vtemp.Mean(); //--- получить Std серииdouble s=vtemp.Std(); //--- нормализовать vtemp-=m; vtemp/=s;x_norm.Col(vtemp,0); //--- быстрое среднееif(CopyBuffer(m_sma_fast,0,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); //--- медленное среднееif(CopyBuffer(m_sma_slow,0,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); //--- выполнить выводinput_data.Assign(x_norm);if(!OnnxRun(m_handle,ONNX_NO_CONVERSION,input_data,output_data))return(-1); //--- оценить предсказаниеprobabilities.Assign(output_data);return(int(output_data.ArgMax())); } };//+------------------------------------------------------------------+
Как и в предыдущем классе, в методе Init вызывается метод базового класса CheckInit, где создаётся сессия для ONNX-модели и явно выставляются размеры входного и выходного тензоров
В методе PredictClass обеспечиваются серии из 30 предыдущих Close и рассчитанные скользящие средние. Данные нормируются тем же способом, что и при обучении.
Проверим, как работает эта модель. Для этого изменим всего две строки проверочного эксперта
#include"ModelEurusdD1_30Class.mqh"#include<Trade\Trade.mqh>input double InpLots =1.0; // Lots amount to open positionCModelEurusdD1_30Class ExtModel;CTrade ExtTrade;
Параметры тестирования те же самые.
Видим, что модель работает.
6. Класс для третьей модели
Последняя модель называется model.eurusd.D1.63.class.onnx. Классификационная модель, тренированная на EURUSD D1 на сериях из 63 цен Close.
//+------------------------------------------------------------------+//| ModelEurusdD1_63.mqh |//| Авторские права 2023, MetaQuotes Ltd. |//| https://www.mql5.com |//+------------------------------------------------------------------+#include"ModelSymbolPeriod.mqh"#resource "Python/model.eurusd.D1.63.class.onnx" as uchar model_eurusd_D1_63_class[]//+------------------------------------------------------------------+//| Класс-обертка для ONNX-модели |//+------------------------------------------------------------------+classCModelEurusdD1_63Class:publicCModelSymbolPeriod {private:int m_sample_size;public: //+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+CModelEurusdD1_63Class(void) :CModelSymbolPeriod("EURUSD",PERIOD_D1) { m_sample_size=63; } //+------------------------------------------------------------------+ //| Инициализация ONNX-модели | //+------------------------------------------------------------------+virtualboolInit(conststring symbol,constENUM_TIMEFRAMES period) { //--- проверка символа, периода, создание моделиif(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_63_class)) {Print("model_eurusd_D1_63_class : ошибка инициализации");return(false); } //--- так как не все размеры определены во входном тензоре, их необходимо явно установить //--- первый индекс - размер партии, второй индекс - размер серииconstlong input_shape[] = {1,m_sample_size};if(!OnnxSetInputShape(m_handle,0,input_shape)) {Print("model_eurusd_D1_63_class : ошибка установки формы ввода ",GetLastError());return(false); } //--- так как не все размеры определены в выходном тензоре, их необходимо явно установить //--- первый индекс - размер партии, должен совпадать с размером партии входного тензора //--- второй индекс - количество классов (вверх, тот же или вниз)constlong output_shape[] = {1,3};if(!OnnxSetOutputShape(m_handle,0,output_shape)) {Print("model_eurusd_D1_63_class : ошибка установки формы вывода ",GetLastError());return(false); } //--- успешноreturn(true); } //+------------------------------------------------------------------+ //| Предсказать класс | //+------------------------------------------------------------------+virtualintPredictClass(vector& probabilities) {static vectorf input_data(m_sample_size); // вектор для подготовленных входных данныхstatic vectorf output_data(3); // вектор для получения результата //--- запросить последние барыif(!input_data.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,1,m_sample_size))return(-1); //--- получить Mean серииfloat m=input_data.Mean(); //--- получить Std серииfloat s=input_data.Std(); //--- нормализовать цены input_data-=m; input_data/=s; //--- выполнить выводif(!OnnxRun(m_handle,ONNX_NO_CONVERSION,input_data,output_data))return(-1); //--- оценить предсказаниеprobabilities.Assign(output_data);return(int(output_data.ArgMax())); } };//+------------------------------------------------------------------+
Это — самая простая модель из трёх. Поэтому код метода PredictClass получился таким компактным.
Опять изменим две строки в эксперте
#include"ModelEurusdD1_63Class.mqh"#include<Trade\Trade.mqh>input double InpLots =1.0; // Lots amount to open positionCModelEurusdD1_63Class ExtModel;CTrade ExtTrade;
И запустим тестирование с теми же самыми настройками.
Модель работает
7. Собираем все модели в одном эксперте. Жёсткое голосование
Все три модели показали свою работоспособность. Теперь попробуем объединить их усилия. Устроим голосование моделей.
Предварительные объявления и определения
#include"ModelEurusdD1_10Class.mqh"#include"ModelEurusdD1_30Class.mqh"#include"ModelEurusdD1_63Class.mqh"#include<Trade\Trade.mqh>input double InpLots =1.0; // Количество лотов для открытия позицииCModelSymbolPeriod *ExtModels[3];CTrade ExtTrade;
voidOnTick() {for(long i=0; i<ArraySize(ExtModels); i++)if(!ExtModels[i].CheckOnTick())return;//--- предсказать следующее движение ценыintreturned[3]={0,0,0}; vector soft=vector::Zeros(3);//--- собрать возвращенные классыfor(long i=0; i<ArraySize(ExtModels); i++) { vector prob(3);int pred=ExtModels[i].PredictClass(prob);if(pred>=0) {returned[pred]++; soft+=prob; } }//--- получить одно предсказание для всех моделейint predicted_class=-1;//--- мягкое или жесткое голосованиеif(InpVotes==Soft) predicted_class=(int)soft.ArgMax();else { //--- подсчитать голоса за предсказанияfor(int n=0; n<3; n++) {if(returned[n]>=InpVotes) { predicted_class=n;break; } } }//--- проверить торговлю в соответствии с предсказаниемif(predicted_class>=0)if(PositionSelect(_Symbol))CheckForClose(predicted_class);elseCheckForOpen(predicted_class); }
Большинство голосов считается по формуле <общее количество голосов>/2 + 1. Для общего числа голосов 3 большинством являются 2 голоса. Это - так называемое "жёсткое голосование"
Результат тестирования всё с теми же самыми настройками.
Вспомним работу всех трёх моделей по отдельности, а именно количество прибыльных и убыточных трейдов.
Модель
количество прибыльных и убыточных трейдов.
Первая модель
11 : 3
Вторая модель
6 : 1
Третья модель
16 : 10
Похоже, при помощи жёсткого голосования мы улучшили результат — 16 : 4. Но, конечно же, необходимо смотреть полные отчёты и графики тестирования.
8. Мягкое голосование
Мягкое голосование отличается от жёсткого тем, что учитывается не количество голосов, а считается сумма вероятностей всех трёх классов от всех трёх моделей. И уже по самой высокой вероятности выбирается класс.
Для обеспечения мягкого голосования необходимо внести некоторые изменения.
В базовом классе:
virtualintPredictClass(vector& probabilities) {double predicted_price=PredictPrice();if(predicted_price==DBL_MAX)return(-1);int predicted_class=-1;double last_close=iClose(m_symbol,m_period,1); //--- классифицировать предсказанное движение ценыdouble delta=last_close-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; } //--- установить вероятность предсказания как 1.0probabilities.Fill(0);if(predicted_class<(int)probabilities.Size())probabilities[predicted_class]=1; //--- и вернуть предсказанный классreturn(predicted_class); } };
#include"ModelEurusdD1_10Class.mqh"#include"ModelEurusdD1_30Class.mqh"#include"ModelEurusdD1_63Class.mqh"#include<Trade\Trade.mqh>enumEnVotes { Two=2, // Два голоса Three=3, // Три голоса Soft=4// Мягкое голосование };input double InpLots =1.0; // Количество лотов для открытия позицииinput EnVotes InpVotes = Two; // Голоса для принятия торгового решенияCModelSymbolPeriod *ExtModels[3];CTrade ExtTrade;
voidOnTick() {for(long i=0; i<ArraySize(ExtModels); i++)if(!ExtModels[i].CheckOnTick())return;//--- предсказать следующее движение ценыintreturned[3]={0,0,0}; vector soft=vector::Zeros(3);//--- собрать возвращенные классыfor(long i=0; i<ArraySize(ExtModels); i++) { vector prob(3);int pred=ExtModels[i].PredictClass(prob);if(pred>=0) {returned[pred]++; soft+=prob; } }//--- получить одно предсказание для всех моделейint predicted_class=-1;//--- мягкое или жесткое голосованиеif(InpVotes==Soft) predicted_class=(int)soft.ArgMax();else { //--- подсчитать голоса за предсказанияfor(int n=0; n<3; n++) {if(returned[n]>=InpVotes) { predicted_class=n;break; } } }//--- проверить торговлю в соответствии с предсказаниемif(predicted_class>=0)if(PositionSelect(_Symbol))CheckForClose(predicted_class);elseCheckForOpen(predicted_class); }
Тестируем всё с теми же настройками. Во входных параметрах выбираем Soft.
Получим результат.
Прибыльных трейдов — 15, Убыточных трейдов — 3. В денежном выражении жёсткое голосование тоже оказалось лучше, чем мягкое.
9. Единогласное голосоване
Интересно посмотреть на результат единогласного голосования, то есть при количестве голосов 3.
Очень консервативная торговля. При этом единственная убыточная сделка была закрыта при окончании тестирования (возможно, она и не убыточная на самом деле).
Важно: обращаем ваше внимание, что использованные в статье модели представлены только в целях демонстрации работы с ONNX-моделями средствами языка MQL5. Советник не предназначен для торговли на реальных счетах.
Заключение
Мы показали, как объектно-ориентированное программирование позволяет упростить написание программ. Все сложности моделей (а модели могут быть гораздо более сложными, чем представленные нами в качестве примера) прячутся в своих классах. А остальная "сложность" уместилась в 45 строках функции OnTick.