English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
preview
Пример ансамбля ONNX-моделей в MQL5

Пример ансамбля ONNX-моделей в MQL5

MetaTrader 5Машинное обучение | 4 апреля 2023, 15:36
1 381 2
MetaQuotes
MetaQuotes

Введение

Для стабильной торговли желательно иметь диверсификацию как по торгуемым инструментам, так и по торговым стратегиям. Такая же ситуация и с моделями машинного обучения — проще создать несколько более простых моделей, чем одну сложную. Но собрать потом эти модели в одну ONNX-модель может быть непросто.

Однако можно комбинировать использование нескольких уже обученных ONNX-моделей в одной MQL5-программе. Мы покажем, как можно просто реализовать один из ансамблей, который называется классификатор голосования.


Использованные модели

Для нашего примера мы взяли 2 простые модели - регрессионная модель предсказания цены и классификационная модель предсказания движения цены. Главное отличие моделей друг от друга - регрессия предсказывает количество, классификация предсказывает метку (класс).

Первая модель - регрессионная.

Обучение проводится на EURUSD D1 c 2003 года по конец 2022 года. Для обучения используются серии из 10 цен OHLC. Для повышения обучаемости модели цены нормируются, производится деление отклонения от средней цены в серии на стандартное отклонение в серии. Это позволяет уложить серию в некий диапазон, у которого среднее 0 и разброс 1, что улучшает сходимость при обучении.

В результате должна быть спрогнозирована цена закрытия следующего дня.

Модель очень простая и была сделана только в качестве иллюстрации.

# Copyright 2023, MetaQuotes Ltd.
# https://www.mql5.com

from datetime import datetime
import MetaTrader5 as mt5
import tensorflow as tf
import numpy as np
import pandas as pd
import tf2onnx
from sklearn.model_selection import train_test_split
from tqdm import tqdm
from sys import argv

if not mt5.initialize():
    print("initialize() failed, error code =",mt5.last_error())
    quit()

# we will save generated onnx-file near the our script
data_path=argv[0]
last_index=data_path.rfind("\\")+1
data_path=data_path[0:last_index]
print("data path to save onnx model",data_path)

# input parameters
inp_model_name = "model.eurusd.D1.10.onnx"
inp_history_size = 10
inp_start_date = datetime(2003, 1, 1, 0)
inp_end_date = datetime(2023, 1, 1, 0)

# get data from client terminal
eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_D1, inp_start_date, inp_end_date)
df = pd.DataFrame(eurusd_rates)


#
# collect dataset subroutine
#
def collect_dataset(df: pd.DataFrame, history_size: int):
    """
    Collect dataset for the following regression problem:
    - input: history_size consecutive H1 bars;
    - output: close price for the next bar.

    :param df: D1 bars for a range of dates
    :param history_size: how many bars should be considered for making a prediction
    :return: features and labels
    """
    n = len(df)
    xs = []
    ys = []
    for i in tqdm(range(n - history_size)):
        w = df.iloc[i: i + history_size + 1]

        x = w[['open', 'high', 'low', 'close']].iloc[:-1].values
        y = w.iloc[-1]['close']
        xs.append(x)
        ys.append(y)

    X = np.array(xs)
    y = np.array(ys)
    return X, y
###


# get prices
X, y = collect_dataset(df, history_size=inp_history_size)

# normalize prices
m = X.mean(axis=1, keepdims=True)
s = X.std(axis=1, keepdims=True)
X_norm = (X - m) / s
y_norm = (y - m[:, 0, 3]) / s[:, 0, 3]

# split data to train and test sets
X_train, X_test, y_train, y_test = train_test_split(X_norm, y_norm, test_size=0.2, random_state=0)

# define model
model = tf.keras.Sequential([
    tf.keras.layers.LSTM(64, input_shape=(inp_history_size, 4)),
    tf.keras.layers.BatchNormalization(),
    tf.keras.layers.Dropout(0.1),
    tf.keras.layers.Dense(32, activation='relu'),
    tf.keras.layers.BatchNormalization(),
    tf.keras.layers.Dropout(0.1),
    tf.keras.layers.Dense(32, activation='relu'),
    tf.keras.layers.Dense(1)
])

model.compile(optimizer='adam', loss='mse', metrics=['mae'])

# model training for 50 epochs
lr_reduction = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, min_lr=0.000001)
history = model.fit(X_train, y_train, epochs=50, verbose=2, validation_split=0.15, callbacks=[lr_reduction])

# model evaluation
test_loss, test_mae = model.evaluate(X_test, y_test)
print(f"test_loss={test_loss:.3f}")
print(f"test_mae={test_mae:.3f}")

# save model to onnx
output_path = data_path+inp_model_name
onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path)
print(f"saved model to {output_path}")

# finish
mt5.shutdown()
Предполагается, что при исполнении нашей регрессионной модели полученная спрогнозированная цена должна быть преобразована в класс "цена идёт вниз, цена не меняется, цена идёт вверх". Это необходимо для организации классификатора голосования.

Вторая модель — классификационная.

Обучение проводится на EURUSD D1 с 2010 года по конец 2022 года. Для обучения используются серии из 63 цен Close. На выходе должен быть определён один из трёх классов — цена пойдёт вниз, цена останется в пределах 10 пунктов или цена пойдёт вверх. Именно из-за второго класса мы обучаем на данных, начиная с 2010 года, так как в 2009 году был переход с точности 4 знака на точность 5 знаков. Таким образом, один "старый" пункт стал десятью "новыми" пунктами.

Как и в предыдущей модели, цена нормируется. Нормируется таким же самым способом — делением отклонения от средней цены в серии на стандартное отклонение в серии. Идея данной модели подсмотрена в статье "Прогнозирование финансовых временных рядов с MLP в Keras". Модель является чисто демонстрационной, как и предыдущая модель.

# Copyright 2023, MetaQuotes Ltd.
# https://www.mql5.com
#
# Classification model
# 0,0,1 - predict price down
# 0,1,0 - predict price same
# 1,0,0 - predict price up
#

from datetime import datetime
import MetaTrader5 as mt5
import tensorflow as tf
import numpy as np
import pandas as pd
import tf2onnx
from sklearn.model_selection import train_test_split
from tqdm import tqdm
from keras.models import Sequential
from keras.layers import Dense, Activation,Dropout, BatchNormalization, LeakyReLU
from keras.optimizers import SGD
from keras import regularizers
from sys import argv

# initialize MetaTrader 5 client terminal
if not mt5.initialize():
    print("initialize() failed, error code =",mt5.last_error())
    quit()

# we will save the generated onnx-file near the our script
data_path=argv[0]
last_index=data_path.rfind("\\")+1
data_path=data_path[0:last_index]
print("data path to save onnx model",data_path)

# input parameters
inp_model_name = "model.eurusd.D1.63.onnx"
inp_history_size = 63
inp_start_date = datetime(2010, 1, 1, 0)
inp_end_date = datetime(2023, 1, 1, 0)

# get data from the client terminal
eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_D1, inp_start_date, inp_end_date)
df = pd.DataFrame(eurusd_rates)


#
# collect dataset subroutine
#
def collect_dataset(df: pd.DataFrame, history_size: int):
    """
    Collect dataset for the following regression problem:
    - input: history_size consecutive H1 bars;
    - output: close price for the next bar.

    :param df: H1 bars for a range of dates
    :param history_size: how many bars should be considered for making a prediction
    :return: features and labels
    """
    n = len(df)
    xs = []
    ys = []
    for i in tqdm(range(n - history_size)):
        w = df.iloc[i: i + history_size + 1]
        x = w[['close']].iloc[:-1].values

        delta = x[-1] - w.iloc[-1]['close']
        if np.abs(delta)<=0.0001:
           y = 0, 1, 0
        else:
           if delta<0:
              y = 1, 0, 0
           else:
              y = 0, 0, 1

        xs.append(x)
        ys.append(y)

    X = np.array(xs)
    Y = np.array(ys)
    return X, Y
###


# get prices
X, Y = collect_dataset(df, history_size=inp_history_size)

# normalize prices
m = X.mean(axis=1, keepdims=True)
s = X.std(axis=1, keepdims=True)
X_norm = (X - m) / s

# split data to train and test sets
X_train, X_test, Y_train, Y_test = train_test_split(X_norm, Y, test_size=0.1, random_state=0)

# define model
model = Sequential()
model.add(Dense(64, input_dim=inp_history_size, activity_regularizer=regularizers.l2(0.01)))
model.add(BatchNormalization())
model.add(LeakyReLU())
model.add(Dropout(0.3))
model.add(Dense(16, activity_regularizer=regularizers.l2(0.01)))
model.add(BatchNormalization())
model.add(LeakyReLU())
model.add(Dense(3))
model.add(Activation('softmax'))

opt = SGD(learning_rate=0.01, momentum=0.9)
model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy'])

# model training for 300 epochs
lr_reduction = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.9, patience=5, min_lr=0.00001)
history = model.fit(X_train, Y_train, epochs=300, validation_data=(X_test, Y_test), shuffle = True, batch_size=128, verbose=2, callbacks=[lr_reduction])

# model evaluation
test_loss, test_accuracy = model.evaluate(X_test, Y_test)
print(f"test_loss={test_loss:.3f}")
print(f"test_accuracy={test_accuracy:.3f}")

# save model to onnx
output_path = data_path+inp_model_name
onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path)
print(f"saved model to {output_path}")

# finish
mt5.shutdown()
Представленные модели обучались на данных до конца 2022 года, чтобы можно было продемонстрировать работу моделей в тестере стратегий.


Ансамбль ONNX-моделей в MQL5-эксперте

Представляем простой эксперт для демонстрации возможностей ансамблирования моделей. Основные принципы использования ONNX-моделей в MQL5 были описаны во второй части одноимённой статьи.

Предварительные объявления и определения

#include <Trade\Trade.mqh>

input double InpLots = 1.0;              // Lots amount to open position

#resource "Python/model.eurusd.D1.10.onnx" as uchar ExtModel1[]
#resource "Python/model.eurusd.D1.63.onnx" as uchar ExtModel2[]

#define SAMPLE_SIZE1 10
#define SAMPLE_SIZE2 63

long     ExtHandle1=INVALID_HANDLE;
long     ExtHandle2=INVALID_HANDLE;
int      ExtPredictedClass1=-1;
int      ExtPredictedClass2=-1;
int      ExtPredictedClass=-1;
datetime ExtNextBar=0;
CTrade   ExtTrade;

//--- price movement prediction
#define PRICE_UP   0
#define PRICE_SAME 1
#define PRICE_DOWN 2

Функция OnInit

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   if(_Symbol!="EURUSD" || _Period!=PERIOD_D1)
     {
      Print("model must work with EURUSD,D1");
      return(INIT_FAILED);
     }

//--- create first model from static buffer
   ExtHandle1=OnnxCreateFromBuffer(ExtModel1,ONNX_DEFAULT);
   if(ExtHandle1==INVALID_HANDLE)
     {
      Print("First model OnnxCreateFromBuffer error ",GetLastError());
      return(INIT_FAILED);
     }
//--- 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_shape1[] = {1,SAMPLE_SIZE1,4};
   if(!OnnxSetInputShape(ExtHandle1,0,input_shape1))
     {
      Print("First model OnnxSetInputShape error ",GetLastError());
      return(INIT_FAILED);
     }
   
//--- 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_shape1[] = {1,1};
   if(!OnnxSetOutputShape(ExtHandle1,0,output_shape1))
     {
      Print("First model OnnxSetOutputShape error ",GetLastError());
      return(INIT_FAILED);
     }

//--- create second model from static buffer
   ExtHandle2=OnnxCreateFromBuffer(ExtModel2,ONNX_DEFAULT);
   if(ExtHandle2==INVALID_HANDLE)
     {
      Print("Second model OnnxCreateFromBuffer error ",GetLastError());
      return(INIT_FAILED);
     }
  
//--- 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_shape2[] = {1,SAMPLE_SIZE2};
   if(!OnnxSetInputShape(ExtHandle2,0,input_shape2))
     {
      Print("Second model OnnxSetInputShape error ",GetLastError());
      return(INIT_FAILED);
     }

//--- 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 classes (up, same or down)
   const long output_shape2[] = {1,3};
   if(!OnnxSetOutputShape(ExtHandle2,0,output_shape2))
     {
      Print("Second model OnnxSetOutputShape error ",GetLastError());
      return(INIT_FAILED);
     }
//--- ok
   return(INIT_SUCCEEDED);
  }

Работаем только с EURUSD,D1. Просто потому, что используем данные текущего символа-периода при том, что модели обучены именно на дневных ценах.

Обе модели включены в эксперт в виде ресурсов.

Обязательно явно определяем формы входных и выходных данных.

Функция OnTick

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- check new bar
   if(TimeCurrent()<ExtNextBar)
      return;
//--- set next bar time
   ExtNextBar=TimeCurrent();
   ExtNextBar-=ExtNextBar%PeriodSeconds();
   ExtNextBar+=PeriodSeconds();

//--- predict price movement
   Predict();
//--- check trading according to prediction
   if(ExtPredictedClass>=0)
      if(PositionSelect(_Symbol))
         CheckForClose();
      else
         CheckForOpen();
  }

Все торговые операции производим только в начале дня.

Функция предсказания

//+------------------------------------------------------------------+
//| Voting classification                                            |
//+------------------------------------------------------------------+
void Predict(void)
  {
//--- evaluate first model
   ExtPredictedClass1=PredictPrice(ExtHandle1,SAMPLE_SIZE1);
//--- evaluate second model
   ExtPredictedClass2=PredictPriceMovement(ExtHandle2,SAMPLE_SIZE2);
//--- vote
   if(ExtPredictedClass1==ExtPredictedClass2)
      ExtPredictedClass=ExtPredictedClass1;
   else
      ExtPredictedClass=-1;
  }

Класс считается выбранным, когда обе модели получили один и тот же класс. Голосование большинством. А так как в ансамбле всего две модели, то голосование большинством означает "единогласно".

Предсказание цены закрытия дня по 10 предыдущим ценам OHLC

//+------------------------------------------------------------------+
//| Predict next price (first model)                                 |
//+------------------------------------------------------------------+
int PredictPrice(const long handle,const int sample_size)
  {
   static matrixf input_data(sample_size,4);    // matrix for prepared input data
   static vectorf output_data(1);               // vector to get result
   static matrix  mm(sample_size,4);            // matrix of horizontal vectors Mean
   static matrix  ms(sample_size,4);            // matrix of horizontal vectors Std
   static matrix  x_norm(sample_size,4);        // matrix for prices normalize

//--- prepare input data
   matrix rates;
//--- request last bars
   if(!rates.CopyRates(_Symbol,_Period,COPY_RATES_OHLC,1,sample_size))
      return(-1);
//--- 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<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(handle,ONNX_NO_CONVERSION,input_data,output_data))
      return(-1);
//--- denormalize the price from the output value
   double predicted=output_data[0]*s[3]+m[3];
//--- classify predicted price movement
   int    predicted_class=-1;
   double delta=rates[3][sample_size-1]-predicted;
   if(fabs(delta)<=0.0001)
      predicted_class=PRICE_SAME;
   else
     {
      if(delta<0)
         predicted_class=PRICE_UP;
      else
         predicted_class=PRICE_DOWN;

     }

   return(predicted_class);
  }

Входные данные готовятся по тем же самым правилам, что и при обучении модели. После исполнения модели полученное значение обратно преобразуется в цену. На основании разницы между последней в серии цены Close и полученной цены вычисляется класс.

Предсказание движения цены на основании серии из 63 дневных Close.

//+------------------------------------------------------------------+
//| Predict price movement (second model)                            |
//+------------------------------------------------------------------+
int PredictPriceMovement(const long handle,const int sample_size)
  {
   static vectorf input_data(sample_size);    // vector for prepared input data
   static vectorf output_data(3);             // vector to get result

//--- request last bars
   if(!input_data.CopyRates(_Symbol,_Period,COPY_RATES_CLOSE,1,sample_size))
      return(-1);
//--- 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(handle,ONNX_NO_CONVERSION,input_data,output_data))
      return(-1);
//--- evaluate prediction
   return(int(output_data.ArgMax()));
  }

Нормирование цен производится по тем же правилам, что и в первой модели. Но код получился компактнее из-за того, входом является вектор, а не матрица. Класс выбирается по максимальному значению из трёх вероятностей.

Торговая стратегия проста. Торговые операции совершаются в начале каждого дня. Если прогноз "цена пойдёт вверх", то покупаем, если "цена пойдёт вниз" - продаём.

//+------------------------------------------------------------------+
//| Check for open position conditions                               |
//+------------------------------------------------------------------+
void CheckForOpen(void)
  {
   ENUM_ORDER_TYPE signal=WRONG_VALUE;
//--- check signals
   if(ExtPredictedClass==PRICE_DOWN)
      signal=ORDER_TYPE_SELL;    // sell condition
   else
     {
      if(ExtPredictedClass==PRICE_UP)
         signal=ORDER_TYPE_BUY;  // buy condition
     }

//--- open position if possible according to signal
   if(signal!=WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
      ExtTrade.PositionOpen(_Symbol,signal,InpLots,
                            SymbolInfoDouble(_Symbol,signal==ORDER_TYPE_SELL ? SYMBOL_BID:SYMBOL_ASK),
                            0,0);
  }
//+------------------------------------------------------------------+
//| Check for close position conditions                              |
//+------------------------------------------------------------------+
void CheckForClose(void)
  {
   bool bsignal=false;
//--- position already selected before
   long type=PositionGetInteger(POSITION_TYPE);
//--- check signals
   if(type==POSITION_TYPE_BUY && ExtPredictedClass==PRICE_DOWN)
      bsignal=true;
   if(type==POSITION_TYPE_SELL && ExtPredictedClass==PRICE_UP)
      bsignal=true;

//--- close position if possible
   if(bsignal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
     {
      ExtTrade.PositionClose(_Symbol,3);
      //--- open opposite
      CheckForOpen();
     }
  }


Мы обучали наши модели на данных до начала 2023 года. Поэтому устанавливаем интервал тестирования с начала года.

Настройки тестирования

Вот результат тестирования с начала года.

Результаты тестирования советника


Интересно было бы знать результаты тестирования каждой из этих моделей по отдельности.

Для этого модифицируем исходный код эксперта

enum EnModels
  {
   USE_FIRST_MODEL,    // Use first model only
   USE_SECOND_MODEL,   // Use second model only
   USE_BOTH_MODELS     // Use both models
  };
input EnModels InpModels = USE_BOTH_MODELS;  // Models using
input double   InpLots   = 1.0;              // Lots amount to open position

...

//+------------------------------------------------------------------+
//| Voting classification                                            |
//+------------------------------------------------------------------+
void Predict(void)
  {
//--- evaluate first model
   if(InpModels==USE_BOTH_MODELS || InpModels==USE_FIRST_MODEL)
      ExtPredictedClass1=PredictPrice(ExtHandle1,SAMPLE_SIZE1);
//--- evaluate second model
   if(InpModels==USE_BOTH_MODELS || InpModels==USE_SECOND_MODEL)
      ExtPredictedClass2=PredictPriceMovement(ExtHandle2,SAMPLE_SIZE2);

//--- check predictions
   switch(InpModels)
     {
      case USE_FIRST_MODEL :
         ExtPredictedClass=ExtPredictedClass1;
         break;
      case USE_SECOND_MODEL :
         ExtPredictedClass=ExtPredictedClass2;
         break;
      case USE_BOTH_MODELS :
         if(ExtPredictedClass1==ExtPredictedClass2)
            ExtPredictedClass=ExtPredictedClass1;
         else
            ExtPredictedClass=-1;
     }
  }

Выставим настройку "использовать только первую модель".

Настройки советника для использования только первой модели

Результаты тестирования первой модели

Результаты тестирования первой модели


Теперь протестируем только вторую модель. Вот результаты тестирования второй модели.

Результаты тестирования второй модели

Вторая модель оказалась гораздо сильнее первой модели. Результаты подтверждают теорию — ансамблировать нужно слабые модели. Но в данной статье речь шла не о теории ансамблирования, а о практике применения.

Важно: обращаем ваше внимание, что использованные в статье модели представлены только в целях демонстрации работы с ONNX-моделями средствами языка MQL5. Советник не предназначен для торговли на реальных счетах.



Заключение

Мы представили очень простой и наглядный пример ансамбля двух ONNX-моделей. Количество одновременно используемых моделей ограничено и не может превашать 256 моделей. Но даже использование более чем двух моделей потребует иного подхода к программированию таких экспертов, а именно — объектно-ориентированного.

Но это уже тема другой статьи.

Прикрепленные файлы |
MQL5.zip (105.08 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Ivan Butko
Ivan Butko | 5 апр. 2023 в 18:26
Для обучения используются серии из 63 цен Close

Загадочное число 63 :)

Slava
Slava | 6 апр. 2023 в 07:38
Ivan Butko #:

Загадочное число 63 :)

Это число взято из другой статьи про загадочные числа. Ссылку не найду

Нейросети — это просто (Часть 37): Разреженное внимание (Sparse Attention) Нейросети — это просто (Часть 37): Разреженное внимание (Sparse Attention)
В предыдущей статье мы познакомились с реляционными моделями, в архитектуре которых используются механизмы внимания. Одной из особенностей указанных моделей является повышенное использование вычислительных ресурсов. В данной статье будет предложен один их механизмов уменьшения количества вычислительных операций внутри блока Self-Attention. Что позволит увеличить производительность модели в целом.
Торговая стратегия на индикаторе улучшенного распознавания свечей Доджи Торговая стратегия на индикаторе улучшенного распознавания свечей Доджи
Индикатор на метабарах обнаруживал больше свечей чем классический. Проверим, дает ли это реальную пользу в автоматической торговле.
Мультибот в MetaTrader: запуск множества роботов с одного графика Мультибот в MetaTrader: запуск множества роботов с одного графика
В этой статье мы рассмотрим простой шаблон для создания универсального робота в MetaTrader, который можно использовать на нескольких графиках, но прицепив его лишь к одному графику, без необходимости настройки каждого экземпляра робота на каждом отдельном графике.
Разработка торговой системы на основе индикатора Gator Oscillator Разработка торговой системы на основе индикатора Gator Oscillator
Представляю вашему вниманию новую статью из серии, в которой мы учимся строить торговые системы на основе самых популярных индикаторов. На этот раз мы поговорим об индикаторе Gator Oscillator и создадим торговую систему по простым стратегиям.