English Deutsch 日本語
preview
Машинное обучение и Data Science (Часть 32): Как поддерживать актуальность AI-моделей с онлайн-обучением

Машинное обучение и Data Science (Часть 32): Как поддерживать актуальность AI-моделей с онлайн-обучением

MetaTrader 5Статистика и анализ |
192 3
Omega J Msigwa
Omega J Msigwa

Содержание


Что такое онлайн-обучение?

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

При работе с торговыми данными всегда сложно определить оптимальный момент для обновления моделей и частоту этих обновлений. Например, если AI-модель обучалась на данных по Bitcoin, то данные из недавнего прошлого могут оказаться выбросами для алгоритма, учитывая, что недавно криптовалюта установила новый исторический максимум.

В отличие от валютного рынка, где символы исторически колеблются в определенных диапазонах, такие инструменты, как NASDAQ 100, S&P 500 и аналогичные фондовые индексы, а также акции, как правило, имеют тенденцию к росту и обновлению максимальных значений.

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


Преимущества онлайн-обучения

  • Адаптивность

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

  • Масштабируемость

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

  • Прогнозы в реальном времени

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

  • Эффективность

    Пошаговое обучение и обновление моделей позволяют сократить время обучения и снизить его стоимость, обеспечивая при этом непрерывную адаптацию.

Итак, мы разобрали ключевые преимущества этого подхода. Теперь рассмотрим, какая инфраструктура необходима для эффективной реализации онлайн-обучения в MetaTrader 5.


Инфраструктура онлайн-обучения для MetaTrader 5 

Поскольку наша конечная цель — сделать AI-модели полезными для трейдинга в MetaTrader 5, для этого требуется иная инфраструктура онлайн-обучения, чем та, которая обычно используется в приложениях на базе Python.

Инфраструктура онлайн-обучения для MetaTrader5


Шаг 01: Клиент Python

В Python-клиенте (скрипте) мы будем строить AI-модели на основе торговых данных, получаемых из MetaTrader 5.

Мы будем использовать библиотеку MetaTrader 5 Python. Начнем с инициализации платформы.

import pandas as pd
import numpy as np
import MetaTrader5 as mt5
from datetime import datetime

if not mt5.initialize(): # Initialize the MetaTrader 5 platform
    print("initialize() failed")
    mt5.shutdown()

После инициализации MetaTrader 5 получим торговые данные с помощью метода copy_rates_from_pos

def getData(start = 1, bars = 1000):

    rates = mt5.copy_rates_from_pos("EURUSD", mt5.TIMEFRAME_H1, start, bars)
  
   if len(rates) < bars: # if the received information is less than specified
        print("Failed to copy rates from MetaTrader 5, error = ",mt5.last_error())

    # create a pandas DataFrame out of the obtained data

    df_rates = pd.DataFrame(rates)
                                                
    return df_rates

Выведем полученные данные.

print("Trading info:\n",getData(1, 100)) # get 100 bars starting at the recent closed bar

Пример вывода

           time     open     high      low    close  tick_volume  spread  real_volume
0   1731351600  1.06520  1.06564  1.06451  1.06491         1688       0            0
1   1731355200  1.06491  1.06519  1.06460  1.06505         1607       0            0
2   1731358800  1.06505  1.06573  1.06495  1.06512         1157       0            0
3   1731362400  1.06512  1.06564  1.06512  1.06557         1112       0            0
4   1731366000  1.06557  1.06579  1.06553  1.06557          776       0            0
..         ...      ...      ...      ...      ...          ...     ...          ...
95  1731693600  1.05354  1.05516  1.05333  1.05513         5125       0            0
96  1731697200  1.05513  1.05600  1.05472  1.05486         3966       0            0
97  1731700800  1.05487  1.05547  1.05386  1.05515         2919       0            0
98  1731704400  1.05515  1.05522  1.05359  1.05372         2651       0            0
99  1731708000  1.05372  1.05379  1.05164  1.05279         2977       0            0

[100 rows x 8 columns]

Мы используем метод copy_rates_from_pos, так как он позволяет обращаться к последнему закрытому бару по индексу 1. Это гораздо удобнее, чем фиксированные даты, поскольку всегда возвращаются актуальные данные.

Благодаря этому мы можем быть уверены, что начиная с бара с индексом 1 мы получаем данные именно с последнего закрытого бара и далее — столько баров, сколько нужно.

После того, как нужные данные получены, мы можем переходить к стандартным шагам машинного обучения.

Создадим отдельные файлы для каждой модели. Такой подход упрощает работу — в основном файле main.py, где реализованы ключевые процессы и функции, можно будет легко подключать нужные модели.

Файл catboost_models.py

from catboost import CatBoostClassifier
from sklearn.metrics import accuracy_score

from onnx.helper import get_attribute_value
from skl2onnx import convert_sklearn, update_registered_converter
from sklearn.pipeline import Pipeline
from skl2onnx.common.shape_calculator import (
    calculate_linear_classifier_output_shapes,
)  # noqa
from skl2onnx.common.data_types import (
    FloatTensorType,
    Int64TensorType,
    guess_tensor_type,
)
from skl2onnx._parse import _apply_zipmap, _get_sklearn_operator_name
from catboost.utils import convert_to_onnx_object

# Example initial data (X_initial, y_initial are your initial feature matrix and target)

class CatBoostClassifierModel():
    def __init__(self, X_train, X_test, y_train, y_test):

        self.X_train = X_train
        self.X_test = X_test
        self.y_train = y_train
        self.y_test = y_test
        self.model = None

    def train(self, iterations=100, depth=6, learning_rate=0.1, loss_function="CrossEntropy", use_best_model=True):
        # Initialize the CatBoost model

        params = {
            "iterations": iterations,
            "depth": depth,
            "learning_rate": learning_rate,
            "loss_function": loss_function,
            "use_best_model": use_best_model
        }

        self.model = Pipeline([ # wrap a catboost classifier in sklearn pipeline | good practice (not necessary tho :))
            ("catboost", CatBoostClassifier(**params))
        ])

        # Testing the model
        
        self.model.fit(X=self.X_train, y=self.y_train, catboost__eval_set=(self.X_test, self.y_test))

        y_pred = self.model.predict(self.X_test)
        print("Model's accuracy on out-of-sample data = ",accuracy_score(self.y_test, y_pred))

    # a function for saving the trained CatBoost model to ONNX format

    def to_onnx(self, model_name):

        update_registered_converter(
            CatBoostClassifier,
            "CatBoostCatBoostClassifier",
            calculate_linear_classifier_output_shapes,
            self.skl2onnx_convert_catboost,
            parser=self.skl2onnx_parser_castboost_classifier,
            options={"nocl": [True, False], "zipmap": [True, False, "columns"]},
        )

        model_onnx = convert_sklearn(
            self.model,
            "pipeline_catboost",
            [("input", FloatTensorType([None, self.X_train.shape[1]]))],
            target_opset={"": 12, "ai.onnx.ml": 2},
        )

        # And save.
        with open(model_name, "wb") as f:
            f.write(model_onnx.SerializeToString())

Что касается этой модели CatBoost, мы подробнее обсуждали ее в отдельной статьеЭту модель CatBoost я использовал в качестве примера, вы же можете использовать любую другую модель на свое усмотрение.

Итак, выше мы написали класс, который мы будем использовать для инициализации, обучения и сохранения модели CatBoost. Развернем эту модель в файле "main.py".

Файл main.py

Как и раньше, начнем с получения данных из десктопного терминала MetaTrader 5:

data = getData(start=1, bars=1000)

Если внимательно посмотреть на используемую модель CatBoost, вы увидите, что это классификатор. Однако у нас нет целевой переменной (target) для этого классификатора. Нужно ее создать.

# Preparing the target variable

data["future_open"] = data["open"].shift(-1) # shift one bar into the future
data["future_close"] = data["close"].shift(-1)

target = []
for row in range(data.shape[0]):
    if data["future_close"].iloc[row] > data["future_open"].iloc[row]: # bullish signal
        target.append(1)
    else: # bearish signal
        target.append(0)

data["target"] = target # add the target variable to the dataframe

data = data.dropna() # drop empty rows

Мы можем удалить из двумерного массива X все будущие переменные, а также признаки, содержащие большое количество нулевых значений, и назначить переменную target в качестве одномерного массива y.

X = data.drop(columns = ["spread","real_volume","future_close","future_open","target"])
y = data["target"]

После этого разбиваем данные на обучающую и валидационную выборки, инициализируем модель CatBoost с использованием рыночных данных и обучаем ее.

X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, random_state=42)

catboost_model = catboost_models.CatBoostClassifierModel(X_train, X_test, y_train, y_test)
catboost_model.train()

В завершение сохраняем модель в формате ONNX в общий каталог MetaTrader 5.

Шаг 02: Общая папка

С помощью Python-библиотеки MetaTrader 5 можно получить путь к общему каталогу.

terminal_info_dict = mt5.terminal_info()._asdict()
common_path = terminal_info_dict["commondata_path"]

Здесь мы будем хранить все обученные AI-модели, созданные в нашем Python-клиенте.

При доступе к общему каталогу через MQL5 обычно используется подпапка Files, расположенная внутри этого общего каталога. Чтобы далее без проблем получать доступ к этим моделям из MQL5, необходимо сохранять их именно в эту подпапку.

# Save models in a specific location under the common parent folder

models_path = os.path.join(common_path, "Files")

if not os.path.exists(models_path): #if the folder exists
    os.makedirs(models_path) # Create the folder if it doesn't exist

catboost_model.to_onnx(model_name=os.path.join(models_path, "catboost.H1.onnx"))

В завершение обернем все эти строки кода в одну функцию, чтобы упростить выполнение всех процессов, когда это потребуется.

def trainAndSaveCatBoost():

    data = getData(start=1, bars=1000)

    # Check if we were able to receive some data

    if (len(data)<=0):
        print("Failed to obtain data from Metatrader5, error = ",mt5.last_error())
        mt5.shutdown()

    # Preparing the target variable

    data["future_open"] = data["open"].shift(-1) # shift one bar into the future
    data["future_close"] = data["close"].shift(-1)

    target = []
    for row in range(data.shape[0]):
        if data["future_close"].iloc[row] > data["future_open"].iloc[row]: # bullish signal
            target.append(1)
        else: # bearish signal
            target.append(0)

    data["target"] = target # add the target variable to the dataframe

    data = data.dropna() # drop empty rows

    X = data.drop(columns = ["spread","real_volume","future_close","future_open","target"])
    y = data["target"]

    X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, random_state=42)

    catboost_model = catboost_models.CatBoostClassifierModel(X_train, X_test, y_train, y_test)
    catboost_model.train()

    # Save models in a specific location under the common parent folder

    models_path = os.path.join(common_path, "Files")

    if not os.path.exists(models_path): #if the folder exists
        os.makedirs(models_path) # Create the folder if it doesn't exist

    catboost_model.to_onnx(model_name=os.path.join(models_path, "catboost.H1.onnx"))

Далее вызовем эту функцию и посмотрим, что она делает.

trainAndSaveCatBoost()
exit() # stop the script

Вывод функции

0:      learn: 0.6916088        test: 0.6934968 best: 0.6934968 (0)     total: 163ms    remaining: 16.1s
1:      learn: 0.6901684        test: 0.6936087 best: 0.6934968 (0)     total: 168ms    remaining: 8.22s
2:      learn: 0.6888965        test: 0.6931576 best: 0.6931576 (2)     total: 175ms    remaining: 5.65s
3:      learn: 0.6856524        test: 0.6927187 best: 0.6927187 (3)     total: 184ms    remaining: 4.41s
4:      learn: 0.6843646        test: 0.6927737 best: 0.6927187 (3)     total: 196ms    remaining: 3.72s
...
...
...
96:     learn: 0.5992419        test: 0.6995323 best: 0.6927187 (3)     total: 915ms    remaining: 28.3ms
97:     learn: 0.5985751        test: 0.7002011 best: 0.6927187 (3)     total: 924ms    remaining: 18.9ms
98:     learn: 0.5978617        test: 0.7003299 best: 0.6927187 (3)     total: 928ms    remaining: 9.37ms
99:     learn: 0.5968786        test: 0.7010596 best: 0.6927187 (3)     total: 932ms    remaining: 0us

bestTest = 0.6927187021
bestIteration = 3

Shrink model to first 4 iterations.
Точность модели на данных вне обучающей выборки составила 0,5

Файл .onnx доступен в папке Common\Files.

Шаг 03: MetaTrader 5

Теперь в MetaTrader 5 нужно загрузить модель, сохраненную в формате ONNX.

Начнем с импорта библиотеки для выполнения этой задачи.

Файл "Online Learning Catboost.mq5"

#include <CatBoost.mqh>
CCatBoost *catboost;

input string model_name = "catboost.H1.onnx";
input string symbol = "EURUSD";
input ENUM_TIMEFRAMES timeframe = PERIOD_H1;

string common_path;

Первое, что надо сделать в функции OnInit — проверить, есть ли файл модели в общем каталоге. Если файл отсутствует, это может означать, что модель не была обучена.

Затем инициализируем ONNX-модель с передачей флага ONNX_COMMON_FOLDER, чтобы явно указать загрузку модели из общего каталога Common folder.

int OnInit()
  {
//--- Check if the model file exists
  
   if (!FileIsExist(model_name, FILE_COMMON))
     {
       printf("%s Onnx file doesn't exist",__FUNCTION__);
       return INIT_FAILED;
     }
     
//--- Initialize a catboost model
   
  catboost = new CCatBoost(); 
  if (!catboost.Init(model_name, ONNX_COMMON_FOLDER))
    {
      printf("%s failed to initialize the catboost model, error = %d",__FUNCTION__,GetLastError());      
      return INIT_FAILED;
    }
      
//---
}

Чтобы использовать загруженную модель для прогнозирования, вернемся в скрипт Python и посмотрим, какие признаки использовались для обучения после того, как часть из них была удалена.

В MQL5 необходимо собрать те же признаки в том же порядке.

Код Python из файла "main.py".

X = data.drop(columns = ["spread","real_volume","future_close","future_open","target"])
y = data["target"]

print(X.head())

 Вывод функции

         time     open     high      low    close  tick_volume
0  1726772400  1.11469  1.11584  1.11453  1.11556         3315
1  1726776000  1.11556  1.11615  1.11525  1.11606         2812
2  1726779600  1.11606  1.11680  1.11606  1.11656         2309
3  1726783200  1.11656  1.11668  1.11590  1.11622         2667
4  1726786800  1.11622  1.11644  1.11605  1.11615         1166

Затем, внутри функции OnTick, мы получаем эти данные и вызываем функцию predict_bin, которая предсказывает классы.

В нашем случае функция возвращает два класса, соответствующие значениям целевой переменной, подготовленной в Python-клиенте: 0 — бычий рынок (bullish), 1 — медвежий рынок (bearish).

void OnTick()
  {
//---
     MqlRates rates[];
     CopyRates(symbol, timeframe, 1, 1, rates); //copy the recent closed bar information
     
     vector x = {
                 (double)rates[0].time, 
                 rates[0].open, 
                 rates[0].high, 
                 rates[0].low, 
                 rates[0].close, 
                 (double)rates[0].tick_volume};
     
     Comment(TimeCurrent(),"\nPredicted signal: ",catboost.predict_bin(x)==0?"Bearish":"Bullish");// if the predicted signal is 0 it means a bearish signal, otherwise it is a bullish signal
  }

Вывод функции


Автоматизация процесса обучения и запуска

Мы смогли обучить и загрузить модель в MetaTrader 5, однако наша цель — полностью автоматизировать этот процесс. 

Внутри виртуального окружения Python необходимо установить библиотеку schedule.

$ pip install schedule

Этот небольшой модуль позволяет планировать выполнение определенных функций. Поскольку мы уже обернули код для сбора данных, обучения и сохранения модели в одну функцию, можно настроить ее автоматический вызов каждую минуту.

schedule.every(1).minute.do(trainAndSaveCatBoost) #schedule catboost training

# Keep the script running to execute the scheduled tasks
while True:
    schedule.run_pending()
    time.sleep(60)  # Wait for 1 minute before checking again

Расписание работает как магия :)

В основном советнике мы также установим расписание — когда и как часто он должен загружать модель из общего каталога. Таким образом мы обновляем модель для нашего торгового робота.

Для этого используем функцию OnTimer, которая также прекрасно справляется с задачей.

int OnInit()
  {
//--- Check if the model file exists
  
  ....
     
//--- Initialize a catboost model
   
....
      
//---

   if (!EventSetTimer(60)) //Execute the OnTimer function after every 60 seconds
     {
       printf("%s failed to set the event timer, error = %d",__FUNCTION__,GetLastError());
       return INIT_FAILED;
     }
    
    
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
    if (CheckPointer(catboost) != POINTER_INVALID)
      delete catboost;
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
     ....
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
    if (CheckPointer(catboost) != POINTER_INVALID)
      delete catboost; 
      
//--- Load the new model after deleting the prior one from memory

     catboost = new CCatBoost(); 
     if (!catboost.Init(model_name, ONNX_COMMON_FOLDER))
       {
         printf("%s failed to initialize the catboost model, error = %d",__FUNCTION__,GetLastError());      
         return;
       }
       
     printf("%s New model loaded",TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES));
  }

Вывод функции

HO      0       13:14:00.648    Online Learning Catboost (EURUSD,D1)    2024.11.18 12:14 New model loaded
FK      0       13:15:55.388    Online Learning Catboost (GBPUSD,H1)    2024.11.18 12:15 New model loaded
JG      0       13:16:55.380    Online Learning Catboost (GBPUSD,H1)    2024.11.18 12:16 New model loaded
MP      0       13:17:55.376    Online Learning Catboost (GBPUSD,H1)    2024.11.18 12:17 New model loaded
JM      0       13:18:55.377    Online Learning Catboost (GBPUSD,H1)    2024.11.18 12:18 New model loaded
PF      0       13:19:55.368    Online Learning Catboost (GBPUSD,H1)    2024.11.18 12:19 New model loaded
CR      0       13:20:55.387    Online Learning Catboost (GBPUSD,H1)    2024.11.18 12:20 New model loaded
NO      0       13:21:55.377    Online Learning Catboost (GBPUSD,H1)    2024.11.18 12:21 New model loaded
LH      0       13:22:55.379    Online Learning Catboost (GBPUSD,H1)    2024.11.18 12:22 New model loaded

Мы увидели, как можно автоматизировать процесс обучения и поддерживать актуальность моделей в связке с советником в MetaTrader 5. Хотя этот подход легко реализуется для большинства методов машинного обучения, при работе с моделями глубокого обучения (например, рекуррентными нейронными сетями — RNN) могут возникнуть сложности. В частности, RNN не получится удобно встроить в конвейер Sklearn, который значительно упрощает работу с классическими моделями машинного обучения.

Давайте посмотрим, как можно применить этот подход при работе с управляемым рекуррентным блоком (GRU), который представляет собой особую форму рекуррентной нейронной сети.


Онлайн-обучение для AI-моделей глубокого обучения

В Python-клиенте

Выполним стандартные шаги машинного обучения внутри класса GRUClassifier. Подробнее о GRU мы уже говорили ранее в этой статье.

После обучения модели мы сохраняем ее в формате ONNX. На этот раз мы дополнительно сохраняем параметры объекта StandardScaler в бинарные файлы — это позволит впоследствии нормализовать новые данные в MQL5 точно так же, как в Python.

Файл gru_models.py

import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import GRU, Dense, Input, Dropout
from keras.callbacks import EarlyStopping
from keras.optimizers import Adam
import tf2onnx


class GRUClassifier():
    def __init__(self, time_step, X_train, X_test, y_train, y_test):

        self.X_train = X_train
        self.X_test = X_test
        self.y_train = y_train
        self.y_test = y_test
        self.model = None
        self.time_step = time_step
        self.classes_in_y = np.unique(self.y_train)


    def train(self, learning_rate=0.001, layers=2, neurons = 50, activation="relu", batch_size=32, epochs=100, loss="binary_crossentropy", verbose=0):

        self.model = Sequential()
        self.model.add(Input(shape=(self.time_step, self.X_train.shape[2]))) 
        self.model.add(GRU(units=neurons, activation=activation)) # input layer


        for layer in range(layers): # dynamically adjusting the number of hidden layers

            self.model.add(Dense(units=neurons, activation=activation))
            self.model.add(Dropout(0.5))

        self.model.add(Dense(units=len(self.classes_in_y), activation='softmax', name='output_layer')) # the output layer

        # Compile the model
        adam_optimizer = Adam(learning_rate=learning_rate)
        self.model.compile(optimizer=adam_optimizer, loss=loss, metrics=['accuracy'])
        

        early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)

        history = self.model.fit(self.X_train, self.y_train, epochs=epochs, batch_size=batch_size,
                                 validation_data=(self.X_test, self.y_test),
                                 callbacks=[early_stopping], verbose=verbose)

        val_loss, val_accuracy = self.model.evaluate(self.X_test, self.y_test, verbose=verbose)

        print("Gru accuracy on validation sample = ",val_accuracy)

            
    def to_onnx(self, model_name, standard_scaler):

        # Convert the Keras model to ONNX
        spec = (tf.TensorSpec((None, self.time_step, self.X_train.shape[2]), tf.float16, name="input"),)
        self.model.output_names = ['outputs']

        onnx_model, _ = tf2onnx.convert.from_keras(self.model, input_signature=spec, opset=13)

        # Save the ONNX model to a file
        with open(model_name, "wb") as f:
            f.write(onnx_model.SerializeToString())

        # Save the mean and scale parameters to binary files
        standard_scaler.mean_.tofile(f"{model_name.replace('.onnx','')}.standard_scaler_mean.bin")
        standard_scaler.scale_.tofile(f"{model_name.replace('.onnx','')}.standard_scaler_scale.bin")

В файле main.py мы создаем функцию, отвечающую за работу с моделью GRU. 

def trainAndSaveGRU():

    data = getData(start=1, bars=1000)

    # Preparing the target variable

    data["future_open"] = data["open"].shift(-1)
    data["future_close"] = data["close"].shift(-1)

    target = []
    for row in range(data.shape[0]):
        if data["future_close"].iloc[row] > data["future_open"].iloc[row]:
            target.append(1)
        else:
            target.append(0)

    data["target"] = target

    data = data.dropna()

    # Check if we were able to receive some data

    if (len(data)<=0):
        print("Failed to obtain data from Metatrader5, error = ",mt5.last_error())
        mt5.shutdown()

    X = data.drop(columns = ["spread","real_volume","future_close","future_open","target"])
    y = data["target"]

    X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, shuffle=False)

    ########### Preparing data for timeseries forecasting ###############

    time_step = 10 

    scaler = StandardScaler()

    X_train = scaler.fit_transform(X_train)
    X_test = scaler.transform(X_test)

    x_train_seq, y_train_seq = create_sequences(X_train, y_train, time_step)
    x_test_seq, y_test_seq = create_sequences(X_test, y_test, time_step)

    ###### One HOt encoding #######

    y_train_encoded = to_categorical(y_train_seq)
    y_test_encoded = to_categorical(y_test_seq)

    gru = gru_models.GRUClassifier(time_step=time_step,
                                    X_train= x_train_seq, 
                                    y_train= y_train_encoded, 
                                    X_test= x_test_seq, 
                                    y_test= y_test_encoded
                                    )

    gru.train(
        batch_size=64, 
        learning_rate=0.001, 
        activation = "relu",
        epochs=1000,
        loss="binary_crossentropy",
        layers = 2,
        neurons = 50,
        verbose=1
        )
    
    # Save models in a specific location under the common parent folder

    models_path = os.path.join(common_path, "Files")

    if not os.path.exists(models_path): #if the folder exists
        os.makedirs(models_path) # Create the folder if it doesn't exist

    gru.to_onnx(model_name=os.path.join(models_path, "gru.H1.onnx"), standard_scaler=scaler)

Наконец, установим, как часто нужно вызывать функцию trainAndSaveGRU. Точно так же мы устанавливали расписание в CatBoost.

schedule.every(1).minute.do(trainAndSaveGRU) #scheduled GRU training

Вывод функции

Epoch 1/1000
11/11 ━━━━━━━━━━━━━━━━━━━━ 7s 87ms/step - accuracy: 0.4930 - loss: 0.6985 - val_accuracy: 0.5000 - val_loss: 0.6958
Epoch 2/1000
11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 16ms/step - accuracy: 0.4847 - loss: 0.6957 - val_accuracy: 0.4931 - val_loss: 0.6936
Epoch 3/1000
11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 17ms/step - accuracy: 0.5500 - loss: 0.6915 - val_accuracy: 0.4897 - val_loss: 0.6934
Epoch 4/1000
11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 16ms/step - accuracy: 0.4910 - loss: 0.6923 - val_accuracy: 0.4690 - val_loss: 0.6938
Epoch 5/1000
11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 16ms/step - accuracy: 0.5538 - loss: 0.6910 - val_accuracy: 0.4897 - val_loss: 0.6935
Epoch 6/1000
11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 20ms/step - accuracy: 0.5037 - loss: 0.6953 - val_accuracy: 0.4931 - val_loss: 0.6937
Epoch 7/1000
...
...
...
11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 22ms/step - accuracy: 0.4964 - loss: 0.6952 - val_accuracy: 0.4793 - val_loss: 0.6940
Epoch 20/1000
11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 19ms/step - accuracy: 0.5285 - loss: 0.6914 - val_accuracy: 0.4793 - val_loss: 0.6949
Epoch 21/1000
11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 17ms/step - accuracy: 0.5224 - loss: 0.6935 - val_accuracy: 0.4966 - val_loss: 0.6942
Epoch 22/1000
11/11 ━━━━━━━━━━━━━━━━━━━━ 0s 21ms/step - accuracy: 0.5009 - loss: 0.6936 - val_accuracy: 0.5103 - val_loss: 0.6933
10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 19ms/step - accuracy: 0.4925 - loss: 0.6938
Gru accuracy on validation sample =  0.5103448033332825

В MetaTrader 5

Сначала загрузим библиотеки, которые позволят загрузить модель GRU и стандартный масштабатор.

#include <preprocessing.mqh>
#include <GRU.mqh>

CGRU *gru;
StandardizationScaler *scaler;

//--- Arrays for temporary storage of the scaler values
double scaler_mean[], scaler_std[];

input string model_name = "gru.H1.onnx";

string mean_file;
string std_file;

Далее, в функции OnInit, получаем имена бинарных файлов масштабатора — тот же принцип мы использовали при создании этих файлов.

 string base_name__ = model_name;
   
 if (StringReplace(base_name__,".onnx","")<0)
   {
     printf("%s Failed to obtain the parent name for the scaler files, error = %d",__FUNCTION__,GetLastError());
     return INIT_FAILED;
   }
  
  mean_file = base_name__ + ".standard_scaler_mean.bin";
  std_file = base_name__ + ".standard_scaler_scale.bin";

Наконец, загрузим модель GRU в формате ONNX из общей папки. При этом также читаем бинарные файлы масштабатора, присваивая их значения в массивах scaler_mean и scaler_std.

int OnInit()
  {
   
   string base_name__ = model_name;
   
   if (StringReplace(base_name__,".onnx","")<0) //we followed this same file patterns while saving the binary files in python client
     {
       printf("%s Failed to obtain the parent name for the scaler files, error = %d",__FUNCTION__,GetLastError());
       return INIT_FAILED;
     }
        
   mean_file = base_name__ + ".standard_scaler_mean.bin";
   std_file = base_name__ + ".standard_scaler_scale.bin";
   
//--- Check if the model file exists

   if (!FileIsExist(model_name, FILE_COMMON))
     {
       printf("%s Onnx file doesn't exist",__FUNCTION__);
       return INIT_FAILED;
     }
  
//--- Initialize the GRU model from the common folder

     gru = new CGRU(); 
     if (!gru.Init(model_name, ONNX_COMMON_FOLDER))
       {
         printf("%s failed to initialize the gru model, error = %d",__FUNCTION__,GetLastError());      
         return INIT_FAILED;
       }

//--- Read the scaler files
   
   if (!readArray(mean_file, scaler_mean) || !readArray(std_file, scaler_std))
     {
       printf("%s failed to read scaler information",__FUNCTION__);
       return INIT_FAILED;
     }  
      
   scaler = new StandardizationScaler(scaler_mean, scaler_std); //Load the scaler class by populating it with values
   
//--- Set the timer

   if (!EventSetTimer(60))
     {
       printf("%s failed to set the event timer, error = %d",__FUNCTION__,GetLastError());
       return INIT_FAILED;
     }
    
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
    if (CheckPointer(gru) != POINTER_INVALID)
      delete gru;
    if (CheckPointer(scaler) != POINTER_INVALID)
      delete scaler;
  }

В функции OnTimer зададим расписание для чтения файлов масштабатора и модели из общей папки.

void OnTimer(void)
  {
//--- Delete the existing pointers in memory as the new ones are about to be created

    if (CheckPointer(gru) != POINTER_INVALID)
      delete gru;
    if (CheckPointer(scaler) != POINTER_INVALID)
      delete scaler;
      
//---
      
   if (!readArray(mean_file, scaler_mean) || !readArray(std_file, scaler_std))
     {
       printf("%s failed to read scaler information",__FUNCTION__);
       return;
     }  
      
   scaler = new StandardizationScaler(scaler_mean, scaler_std);
   
     gru = new CGRU(); 
     if (!gru.Init(model_name, ONNX_COMMON_FOLDER))
       {
         printf("%s failed to initialize the gru model, error = %d",__FUNCTION__,GetLastError());      
         return;
         
       }
     printf("%s New model loaded",TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES));
  }

Вывод функции

II      0       14:49:35.920    Online Learning GRU (GBPUSD,H1) 2024.11.18 13:49 New model loaded
QP      0       14:50:35.886    Online Learning GRU (GBPUSD,H1) Initializing ONNX model...
MF      0       14:50:35.919    Online Learning GRU (GBPUSD,H1) ONNX model Initialized
IJ      0       14:50:35.919    Online Learning GRU (GBPUSD,H1) 2024.11.18 13:50 New model loaded
EN      0       14:51:35.894    Online Learning GRU (GBPUSD,H1) Initializing ONNX model...
JD      0       14:51:35.913    Online Learning GRU (GBPUSD,H1) ONNX model Initialized
EL      0       14:51:35.913    Online Learning GRU (GBPUSD,H1) 2024.11.18 13:51 New model loaded
NM      0       14:52:35.885    Online Learning GRU (GBPUSD,H1) Initializing ONNX model...
KK      0       14:52:35.915    Online Learning GRU (GBPUSD,H1) ONNX model Initialized
QQ      0       14:52:35.915    Online Learning GRU (GBPUSD,H1) 2024.11.18 13:52 New model loaded
DK      0       14:53:35.899    Online Learning GRU (GBPUSD,H1) Initializing ONNX model...
HI      0       14:53:35.935    Online Learning GRU (GBPUSD,H1) ONNX model Initialized
MS      0       14:53:35.935    Online Learning GRU (GBPUSD,H1) 2024.11.18 13:53 New model loaded
DI      0       14:54:35.885    Online Learning GRU (GBPUSD,H1) Initializing ONNX model...
IL      0       14:54:35.908    Online Learning GRU (GBPUSD,H1) ONNX model Initialized
QE      0       14:54:35.908    Online Learning GRU (GBPUSD,H1) 2024.11.18 13:54 New model loaded

Чтобы получить прогнозы от модели GRU, необходимо учитывать значение временного шага timestep — оно позволяет рекуррентным нейросетям (RNN) понимать временные зависимости в данных.

В функции trainAndSaveGRU устанавливает значение timestep = 10.

def trainAndSaveGRU():

    data = getData(start=1, bars=1000)

     ....
     ....

    time_step = 10 

Получим последние 10 баров (timesteps) из истории, начиная с последнего закрытого бара в MQL5. (как это и должно быть)

input int time_step = 10;
void OnTick()
  {
//---
     MqlRates rates[];
     CopyRates(symbol, timeframe, 1, time_step, rates); //copy the recent closed bar information
     
     vector classes = {0,1}; //Beware of how classes are organized in the target variable. use numpy.unique(y) to determine this array
     
     matrix X = matrix::Zeros(time_step, 6); // 6 columns
     for (int i=0; i<time_step; i++)
       {         
         vector row = {
                 (double)rates[i].time, 
                 rates[i].open, 
                 rates[i].high, 
                 rates[i].low, 
                 rates[i].close, 
                 (double)rates[i].tick_volume};
         
         X.Row(row, i);
       }     
     
     X = scaler.transform(X); //it's important to normalize the data  
     Comment(TimeCurrent(),"\nPredicted signal: ",gru.predict_bin(X, classes)==0?"Bearish":"Bullish");// if the predicted signal is 0 it means a bearish signal, otherwise it is a bullish signal
  }

Вывод функции


Инкрементальное машинное обучение

Некоторые модели более устойчивы и эффективны, чем другие, когда речь идет о методах обучения. Если поискать в интернете информацию про машинное обучение онлайн / online machine learning, чаще всего можно встретить определение, согласно которому небольшие пачки данных (mini-batches) дообучают исходную модель в рамках более крупной задачи обучения.

Проблема в том, что многие модели либо вовсе не поддерживают такой режим работы, либо показывают неудовлетворительные результаты при обучении на малых выборках.

Современные методы машинного обучения, среди которых и CatBoost, изначально проектировались с учетом такого дообучения. Благодаря чему они подходят для онлайн-обучения и при этом позволяют существенно экономить память при работе с большими данными, разделяя их на небольшие блоки и поочередно дообучая исходную модель.

def getData(start = 1, bars = 1000):

    rates = mt5.copy_rates_from_pos("EURUSD", mt5.TIMEFRAME_H1, start, bars)

    df_rates = pd.DataFrame(rates)
                                                
    return df_rates

def trainIncrementally():

    # CatBoost model
    clf = CatBoostClassifier(
        task_type="CPU",
        iterations=2000,
        learning_rate=0.2,
        max_depth=1,
        verbose=0,
    )
    
    # Get big data
    big_data = getData(1, 10000)

    # Split into chunks of 1000 samples
    chunk_size = 1000
    chunks = [big_data[i:i + chunk_size].copy() for i in range(0, len(big_data), chunk_size)]  # Use .copy() here    

    for i, chunk in enumerate(chunks):
            
        # Preparing the target variable

        chunk["future_open"] = chunk["open"].shift(-1)
        chunk["future_close"] = chunk["close"].shift(-1)

        target = []
        for row in range(chunk.shape[0]):
            if chunk["future_close"].iloc[row] > chunk["future_open"].iloc[row]:
                target.append(1)
            else:
                target.append(0)

        chunk["target"] = target

        chunk = chunk.dropna()

        # Check if we were able to receive some data

        if (len(chunk)<=0):
            print("Failed to obtain chunk from Metatrader5, error = ",mt5.last_error())
            mt5.shutdown()

        X = chunk.drop(columns = ["spread","real_volume","future_close","future_open","target"])
        y = chunk["target"]

        X_train, X_val, y_train, y_val = train_test_split(X, y, train_size=0.8, random_state=42)

        if i == 0:
            # Initial training, training the model for the first time
            clf.fit(X_train, y_train, eval_set=(X_val, y_val))

            y_pred = clf.predict(X_val)
            print(f"---> Acc score: {accuracy_score(y_pred=y_pred, y_true=y_val)}")
        else:
            # Incremental training by using the initial trained model
            clf.fit(X_train, y_train, init_model="model.cbm", eval_set=(X_val, y_val))

            y_pred = clf.predict(X_val)
            print(f"---> Acc score: {accuracy_score(y_pred=y_pred, y_true=y_val)}")
        
        # Save the model
        clf.save_model("model.cbm")
        print(f"Chunk {i + 1}/{len(chunks)} processed and model saved.")

Вывод функции

---> Acc score: 0.555
Chunk 1/10 processed and model saved.
---> Acc score: 0.505
Chunk 2/10 processed and model saved.
---> Acc score: 0.55
Chunk 3/10 processed and model saved.
---> Acc score: 0.565
Chunk 4/10 processed and model saved.
---> Acc score: 0.495
Chunk 5/10 processed and model saved.
---> Acc score: 0.55
Chunk 6/10 processed and model saved.
---> Acc score: 0.555
Chunk 7/10 processed and model saved.
---> Acc score: 0.52
Chunk 8/10 processed and model saved.
---> Acc score: 0.455
Chunk 9/10 processed and model saved.
---> Acc score: 0.535
Chunk 10/10 processed and model saved.

При этом вы можете использовать ту же архитектуру онлайн-обучения, выполняя построение модели инкрементально, а финальную версию сохранять в формате ONNX в общий каталог (Common Folder), чтобы в дальнейшем использовать ее в MetaTrader 5.


Заключительные мысли

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

Главная задача — найти баланс между автоматизацией процесса обучения и регулярной оценкой моделей, чтобы все работало так, как задумано.


Таблица вложений


Инфраструктура (папки)

Файлы Описание и использование

 Клиент Python


-  catboost_models.py
-  gru_models.py
-  main.py
-  incremental_learning.py


- Файл содержит модель CatBoost
- Файл содержит модель GRU
- Основной файл python, собирает все воедино
- В этом файле реализовано инкрементальное обучение для модели CatBoost


Общая папка (Common Folder)



- catboost.H1.onnx
- gru.H1.onnx
- gru.H1.standard_scaler_mean.bin
- gru.H1.standard_scaler_scale.bin

 Содержит все AI-модели в формате ONNX и файлы масштабатора в бинарных форматах
 
MetaTrader 5 (MQL5)


 - Experts\Online Learning Catboost.mq5
 - Experts\Online Learning GRU.mq5
 - Include\CatBoost.mqh
 - Include\GRU.mqh
 - Include\preprocessing.mqh
 
- Развертывает модель CatBoost в MQL5
- Развертывает модель GRU в MQL5
- Файл библиотеки для инициализации и развертывания модели CatBoost в формате ONNX
- Файл библиотеки для инициализации и развертывания модели GRU в формате ONNX
- Файл библиотеки, содержащий StandardScaler для нормализации данных для использования модели машинного обучения

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/16390

Прикрепленные файлы |
Attachments.zip (475 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
Phuc Hung
Phuc Hung | 14 мар. 2025 в 08:49

Здравствуйте, Omega J Msigwa

Я спросил, какую версию python вы используете для этой статьи Я установил его, и есть конфликт библиотек.

Конфликт вызван следующим:

Пользователь запросил protobuf==3.20.3

onnx 1.17.0 зависит от protobuf>=3.20.2

onnxconverter-common 1.14.0 зависит от protobuf==3.20.2


Затем я отредактировал версию, как было предложено, и получил еще одну ошибку установки.


Чтобы исправить это, вы можете попробовать:

1. расширить диапазон версий пакетов, которые вы указали

2. удалить версии пакетов, чтобы позволить pip попытаться решить конфликт зависимостей.


Конфликт вызван:

Пользователь запросил protobuf==3.20.2

onnx 1.17.0 зависит от protobuf>=3.20.2

onnxconverter-common 1.14.0 зависит от protobuf==3.20.2

tensorboard 2.18.0 зависит от protobuf!=4.24.0 и >=3.19.6

tensorflow-intel 2.18.0 зависит от protobuf!=4.21.0, !=4.21.1, !=4.21.2, !=4.21.3, !=4.21.4, !=4.21.5, <6.0.0dev и >=3.20.3


Чтобы исправить это, вы можете попробовать:

1. расширить диапазон версий пакетов, которые вы указали

2. удалить версии пакетов, чтобы позволить pip попытаться решить конфликт зависимостей



Пожалуйста, дайте больше инструкций

panovq
panovq | 13 авг. 2025 в 15:48
Можно уточнить, что именно то дает это? 
Miguel Angel Vico Alba
Miguel Angel Vico Alba | 13 авг. 2025 в 19:02
panovq # Могу ли я уточнить, что именно это дает?
Не стесняйтесь.
Моделирование рынка (Часть 04): Создание класса C_Orders (I) Моделирование рынка (Часть 04): Создание класса C_Orders (I)
В данной статье мы начнем создание класса C_Orders, чтобы иметь возможность отправлять ордеры на торговый сервер. Мы будем делать это понемногу, поскольку наша цель состоит в том, чтобы подробно объяснить, как это будет происходить с помощью системы обмена сообщениями.
Нейросетевой торговый робот на современной архитектуре нейросети Mamba с селективной SSM Нейросетевой торговый робот на современной архитектуре нейросети Mamba с селективной SSM
Статья исследует революционную архитектуру нейронной сети Mamba/SSM для прогнозирования финансовых временных рядов. Представлена полная реализация на MQL5 современной альтернативы Transformer с линейной сложностью O(N) вместо квадратичной O(N²). Детально рассмотрены селективные State Space Models, hardware-aware оптимизации, patching техники и продвинутые методы обучения AdamW. Включены практические результаты тестирования, показавшие увеличение точности с 62% до 71% при снижении времени обучения с 45 до 8 минут. Представлен готовый торговый советник с автообучением и адаптивным риск-менеджментом для MetaTrader 5.
Нейросети в трейдинге: Модель темпоральных запросов (TQNet) Нейросети в трейдинге: Модель темпоральных запросов (TQNet)
Фреймворк TQNet открывает новые возможности в моделировании и прогнозировании финансовых временных рядов, сочетая модульность, гибкость и высокую производительность. В статье раскрывается возможность реализации сложных механизмом работы с глобальными корреляциями, включая продвинутые методы инициализации параметров.
От начального до среднего уровня: Шаблон и Typename (II) От начального до среднего уровня: Шаблон и Typename (II)
В этой статье мы расскажем, как справиться с одной из самых сложных ситуаций в программировании, с которой можно столкнуться: использование разных типов в одной и той же функции или шаблоне процедуры. Хотя большую часть времени мы уделяли только функциям, всё, что мы здесь рассмотрели, полезно и может быть применено к процедурам.