English Deutsch 日本語
preview
Разметка данных в анализе временных рядов (Часть 6):Применение и тестирование советника с помощью ONNX

Разметка данных в анализе временных рядов (Часть 6):Применение и тестирование советника с помощью ONNX

MetaTrader 5Примеры | 8 мая 2024, 14:29
278 0
Yuqiang Pan
Yuqiang Pan

Введение

В предыдущей статье мы рассмотрели, как использовать WebSocket для связи между советником и сервером Python для решения проблемы тестирования на истории, а также обсудили, почему мы приняли этот метод. В этой статье мы обсудим, как использовать ONNX, который изначально поддерживается MQL5, для выполнения вывода по нашей модели. Также рассмотрим ограничения метода. Если в вашей модели используются операторы, которые не поддерживаются ONNX, вывод может закончиться неудачей, поэтому этот метод подходит не для всех моделей (конечно, вы также можете добавить операторы для поддержки вашей модели, но это требует много времени и усилий). Вот почему в предыдущей статье я уделил много места тому, чтобы представить метод сокетов.

Конечно, конвертировать общую модель в формат ONNX очень удобно и дает нам эффективную поддержку кроссплатформенных операций. В статье в основном рассматриваются некоторые базовые операции по работе с моделями ONNX в MQL5, в том числе способы сопоставления входных и выходных данных моделей torch и ONNX, а также способы преобразования подходящих форматов данных для моделей ONNX. Конечно, речь пойдет и об управлении ордерами советника.

Содержание:


Структура каталогов

При преобразовании модели мы считываем файлы модели и конфигурации. К моему стыду, я обнаружил, что не представил структуру каталогов скрипта в предыдущих статьях, из-за чего вы можете не найти местоположение вашей модели и файлы конфигурации. Итак, здесь мы разбираем структуру каталогов нашего скрипта. При использовании lightning-pytorch для обучения модели, мы не определяли место сохранения модели в обратных вызовах (обратные вызовы, отвечающие за управление контрольной точкой модели, — это класс ModelCheckpoint), мы определяли только имя модели, поэтому тренер сохранит модель в пути по умолчанию.
    ck_callback=ModelCheckpoint(monitor='val_loss',
                                mode="min",
                                save_top_k=1,  
                                filename='{epoch}-{val_loss:.2f}')

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

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

f3


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


f2

При обучении мы сохраним файл results.json для записи наилучшего пути к модели и наилучшего результата, который будет использоваться при загрузке модели. Он сохраняется в корневом каталоге папки:

f4


Преобразование модели torch в модель ONNX

Мы по-прежнему используем модель NBeats в качестве примера. Следующий код будет в основном добавлен в часть вывода Nbeats.py. Этот скрипт был создан, когда я представил модель NBeats в предыдущей статье. Из-за особого характера модели NBeats экспорт модели ONNX с использованием общего метода может быть затруднен. Нам необходимо отладить процесс вывода модели, а затем получить из него соответствующую информацию, чтобы определить соответствующие параметры, необходимые для экспорта. Но я сделал это за вас, так что просто следуйте инструкциям в статье шаг за шагом, и все проблемы будут легко решены.

1. Установим необходимые библиотеки

Прежде чем конвертировать модель, необходимо сделать еще один важный шаг — установить соответствующие библиотеки ONNX. Если мы экспортируем только модель, нам нужно будет установить только библиотеку ONNX: pip install onnx. Но так как нам также необходимо протестировать модель после ее преобразования, нам также необходимо установить библиотеку onnxruntime. Библиотека разделена на две версии: среда выполнения процессора и среда выполнения графического процессора. Если модель большая и сложная, вам может потребоваться установить версию графического процессора, чтобы ускорить процесс вывода. Поскольку нашей модели требуется только вывод о процессоре, эффект ускорения графического процессора не очевиден, поэтому я рекомендую установить версию процессора: pip install onnxruntime.


2. Получаем входные данные

Во-первых, вам нужно переключить модель из режима обучения в режим вывода: best_model.eval(). Причина этого в том, что режим обучения модели и режим вывода различаются, и нам нужен только режим вывода модели, который уменьшит сложность модели и сохранит только входные данные, необходимые для вывода. Затем нам нужно создать загрузчик данных (Dataloader) после загрузки данных, чтобы получить полные входные элементы, а также итератор из этого объекта загрузчика данных, а затем вызвать следующую функцию, чтобы получить первый пакет данных. Первый элемент содержит всю необходимую нам входную информацию. При экспорте модели torch автоматически выберет для нас необходимые входные элементы. Теперь мы используем функцию spilt_data(), определенную ранее, для непосредственного создания загрузчика данных после загрузки данных: t_loader,v_loader,training=spilt_data(dt,t_shuffle=False,t_drop_last=True,v_shuffle=False,v_drop_last=True) Создадим словарь для хранения входных данных, необходимых для экспорта модели: input_dict = {} Получим все входные объекты, здесь мы используем v_loader для их получения, потому что нам нужен процесс вывода: items = next(iter(v_loader))[0] Создадим список для хранения всех входных имен: input_names=[] Затем переберем элементы, чтобы получить все входные данные и их имена:

for item in items:
            input_dict[item] = items[item][-1:]
            # print("{}:{}".format(item,input_dict[item].shape()))
            input_names.append(item)

3. Получаем выходные данные

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

offset=1
dt=dt.iloc[-max_encoder_length-offset:-offset,:]
last_=dt.iloc[-1] 
# print(len(dt))
for i in range(1,max_prediction_length+1):
    dt.loc[dt.index[-1]+1]=last_
dt['series']=0
# dt['time_idx']=dt.apply(lambda x:x.index,args=1)
dt['time_idx']=dt.index-dt.index[0]
input_=dt.loc[:,['close','series','time_idx']]
predictions = best_model.predict(input_, mode='raw',trainer_kwargs=dict(accelerator="cpu",logger=False),return_x=True)

Информация о выводе находится в выходных данных объекта прогнозов. Мы перебираем этот объект, чтобы получить всю выходную информацию, поэтому добавляем сюда следующий оператор:
output_names=[]
for out in predictions.output._fields:
    output_names.append(out)

4. Экспортируем модель

Сначала мы определяем input_sample, необходимый для экспорта модели: input_1=(input_dict,{}) Не спрашивайте почему, просто сделайте это! Затем используем метод to_onnx() в классе NBeats для экспорта в ONNX, для которого также требуется параметр пути к файлу. Экспортируем напрямую в корневой каталог с именем "NBeats.onnx": best_model.to_onnx(file_path=‘NBeats.onnx’, input_sample=input_1, input_names=input_names, output_names=output_names). После того, как программа доработает до этого момента, найдем файл "NBeats.onnx" в корневом каталоге текущей папки:



Примечания:

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

2. В загрузчике данных входные данные включают в себя encoder_cat, encoder_cont и другие, сгенерированные кодером и декодером, тогда как в процессе вывода нам нужны только два - encoder_cont и target_scale. Поэтому не думайте, что шаг сопоставления входных данных лишний. В некоторых моделях, требующих кодеры и декодеры, этот шаг необходим. 3. Конфигурация среды, использованная автором в процессе тестирования: python-3.10;ONNX version-8; pytorch-2.1.1;operators-17.



Тестирование преобразованной модели

В предыдущем разделе мы успешно экспортировали модель torch как модель ONNX. Следующая важная задача — протестировать эту модель и посмотреть, совпадают ли выходные данные этой модели с исходной моделью. Это очень важно, поскольку в процессе экспорта у некоторых операторов могут возникнуть отклонения из-за версии torch и проблем совместимости ядра ONNX. В этом случае при экспорте модели может потребоваться ручное вмешательство.
  • Сначала импортируем библиотеку времени выполнения ONNX: import onnxruntime as ort.
  • Загрузите файл модели "NBeats.onnx": sess = ort.InferenceSession(“NBeats.onnx”).
  • Получите входные имена модели ONNX, перебирая возвращаемое значение sess.get_inputs(), которое используется для сопоставления входных данных: input_names = [input.name for input in sess.get_inputs()].
  • Нам не нужно сравнивать все выходные данные, поэтому мы получаем только первый элемент выходных данных для сравнения и проверки того, совпадают ли результаты: output_name = sess.get_outputs()[0].name.
  • Чтобы сравнить, являются ли результаты одинаковыми, входные данные должны быть одинаковыми, поэтому входные данные модели должны соответствовать данным, используемым для вывода. Но нам нужно сначала преобразовать его в формат загрузчика данных и использовать input_names для соответствия входным данным, поскольку не все входные данные будут загружены в процессе вывода. Сначала загрузим входные данные как данные временных рядов, используя метод from_parameters() класса TimeSeriesDataSet: input_ds = New_TmSrDt.from_parameters(best_model.dataset_parameters, input_,predict=True). Затем преобразуйте его в тип загрузчика данных (Dataloader), используя метод класса to_dataloader(): input_dl = input_ds.to_dataloader(train=False, batch_size=1, num_workers=0).
  • Сопоставим входные данные. Во-первых, нам нужно получить пакет данных и извлечь первый элемент: input_dict = next(iter(input_dl))[0]. Затем используем input_names, чтобы сопоставить входные данные, необходимые для ввода: input_data = [input_dict[name].numpy() for name in input_names]
  • Запустим вывод: pred_onnx = sess.run([output_name], dict(zip(input_names, input_data)))[0].
  • Распечатаем результат вывода torch и onnx и сравним их.
Теперь распечатаем результат вывода torch:

torch result: tensor([[2062.9109, 2062.6191, 2062.5283, 2062.4814, 2062.3572, 2062.1545, 2061.9824, 2061.9678, 2062.1499, 2062.4380, 2062.6680, 2062.7151, 2062.5823, 2062.3979, 2062.3254, 2062.4460, 2062.7087, 2062.9802, 2063.1643, 2063.2991]])

Распечатаем результат вывода onnx:

onnx result: [[2062.911 2062.6191 2062.5283 2062.4814 2062.3572 2062.1545 2061.9824 2061.9678 2062.15 2062.438 2062.668 2062.715 2062.5823 2062.398 2062.3254 2062.446 2062.7087 2062.9802 2063.1646 2063.299 ]]

Мы видим, что результаты вывода нашей модели не отличаются. Следующий шаг — настройка экспортированной модели в MQL5.

f6


Полный код:
# Copyright 2021, MetaQuotes Ltd.
# https://www.mql5.com



import lightning.pytorch as pl
import os
from lightning.pytorch.callbacks import EarlyStopping,ModelCheckpoint
import matplotlib.pyplot as plt
import pandas as pd
from pytorch_forecasting import TimeSeriesDataSet,NBeats
from pytorch_forecasting.data import NaNLabelEncoder
from pytorch_forecasting.data.samplers import TimeSynchronizedBatchSampler
from lightning.pytorch.tuner import Tuner
import MetaTrader5 as mt
import warnings
import json

from torch.utils.data import DataLoader
from torch.utils.data.sampler import Sampler,SequentialSampler

class New_TmSrDt(TimeSeriesDataSet):
    '''
    rewrite dataset class
    '''
    def to_dataloader(self, train: bool = True, 
                      batch_size: int = 64, 
                      batch_sampler: Sampler | str = None, 
                      shuffle:bool=False,
                      drop_last:bool=False,
                      **kwargs) -> DataLoader:

        default_kwargs = dict(
            shuffle=shuffle,
            # drop_last=train and len(self) > batch_size,
            drop_last=drop_last, #
            collate_fn=self._collate_fn,
            batch_size=batch_size,
            batch_sampler=batch_sampler,
        )
        default_kwargs.update(kwargs)
        kwargs = default_kwargs
        # print(kwargs['drop_last'])
        if kwargs["batch_sampler"] is not None:
            sampler = kwargs["batch_sampler"]
            if isinstance(sampler, str):
                if sampler == "synchronized":
                    kwargs["batch_sampler"] = TimeSynchronizedBatchSampler(
                        SequentialSampler(self),
                        batch_size=kwargs["batch_size"],
                        shuffle=kwargs["shuffle"],
                        drop_last=kwargs["drop_last"],
                    )
                else:
                    raise ValueError(f"batch_sampler {sampler} unknown - see docstring for valid batch_sampler")
            del kwargs["batch_size"]
            del kwargs["shuffle"]
            del kwargs["drop_last"]

        return DataLoader(self,**kwargs)

def get_data(mt_data_len:int):
    if not mt.initialize():
        print('initialize() failed!') 
    else:
        print(mt.version())
        sb=mt.symbols_total()
        rts=None
        if sb > 0:
            rts=mt.copy_rates_from_pos("GOLD_micro",mt.TIMEFRAME_M15,0,mt_data_len) 
        mt.shutdown()
        # print(len(rts))
    rts_fm=pd.DataFrame(rts)
    rts_fm['time']=pd.to_datetime(rts_fm['time'], unit='s') 

    rts_fm['time_idx']= rts_fm.index%(max_encoder_length+2*max_prediction_length) 
    rts_fm['series']=rts_fm.index//(max_encoder_length+2*max_prediction_length)
    return rts_fm


def spilt_data(data:pd.DataFrame,
               t_drop_last:bool,
               t_shuffle:bool,
               v_drop_last:bool,
               v_shuffle:bool):
    training_cutoff = data["time_idx"].max() - max_prediction_length #max:95
    context_length = max_encoder_length
    prediction_length = max_prediction_length
    training = New_TmSrDt(
        data[lambda x: x.time_idx <= training_cutoff],
        time_idx="time_idx",
        target="close",
        categorical_encoders={"series":NaNLabelEncoder().fit(data.series)},
        group_ids=["series"],
        time_varying_unknown_reals=["close"],
        max_encoder_length=context_length,
        # min_encoder_length=max_encoder_length//2,
        max_prediction_length=prediction_length,
        # min_prediction_length=1,
        
    )

    validation = New_TmSrDt.from_dataset(training, 
                                         data, 
                                         min_prediction_idx=training_cutoff + 1)
    
    train_dataloader = training.to_dataloader(train=True,
                                              shuffle=t_shuffle, 
                                              drop_last=t_drop_last,
                                              batch_size=batch_size, 
                                              num_workers=0,)
    val_dataloader = validation.to_dataloader(train=False, 
                                              shuffle=v_shuffle,
                                              drop_last=v_drop_last,
                                              batch_size=batch_size, 
                                              num_workers=0)
    return train_dataloader,val_dataloader,training

def get_learning_rate():
    
    pl.seed_everything(42)
    trainer = pl.Trainer(accelerator="cpu", gradient_clip_val=0.1,logger=False)
    net = NBeats.from_dataset(
        training,
        learning_rate=3e-2,
        weight_decay=1e-2,
        backcast_loss_ratio=0.1,
        optimizer="AdamW",
    )
    res = Tuner(trainer).lr_find(
        net, train_dataloaders=t_loader, val_dataloaders=v_loader, min_lr=1e-5, max_lr=1e-1
    )
    # print(f"suggested learning rate: {res.suggestion()}")
    lr_=res.suggestion()
    return lr_
def train():
    early_stop_callback = EarlyStopping(monitor="val_loss", 
                                        min_delta=1e-4, 
                                        patience=10,  
                                        verbose=True, 
                                        mode="min")
    ck_callback=ModelCheckpoint(monitor='val_loss',
                                mode="min",
                                save_top_k=1,  
                                filename='{epoch}-{val_loss:.2f}')
    trainer = pl.Trainer(
        max_epochs=ep,
        accelerator="cpu",
        enable_model_summary=True,
        gradient_clip_val=1.0,
        callbacks=[early_stop_callback,ck_callback],
        limit_train_batches=30,
        enable_checkpointing=True,
    )
    net = NBeats.from_dataset(
        training,
        learning_rate=lr,
        log_interval=10,
        log_val_interval=1,
        weight_decay=1e-2,
        backcast_loss_ratio=0.0,
        optimizer="AdamW",
        stack_types=["trend", "seasonality"],
    )
    trainer.fit(
        net,
        train_dataloaders=t_loader,
        val_dataloaders=v_loader,
        # ckpt_path='best'
    )
    return trainer

if __name__=='__main__':
    ep=200
    __train=False
    mt_data_len=80000
    max_encoder_length = 96
    max_prediction_length = 20
    # context_length = max_encoder_length
    # prediction_length = max_prediction_length
    batch_size = 128
    info_file='results.json'
    warnings.filterwarnings("ignore")
    dt=get_data(mt_data_len=mt_data_len)
    if __train:
        # print(dt)
        # dt=get_data(mt_data_len=mt_data_len)
        t_loader,v_loader,training=spilt_data(dt,
                                              t_shuffle=False,t_drop_last=True,
                                              v_shuffle=False,v_drop_last=True)
        lr=get_learning_rate()
        # lr=3e-3
        trainer__=train()
        m_c_back=trainer__.checkpoint_callback
        m_l_back=trainer__.early_stopping_callback
        best_m_p=m_c_back.best_model_path
        best_m_l=m_l_back.best_score.item()

        # print(best_m_p)
        
        if os.path.exists(info_file):
            with open(info_file,'r+') as f1:
                last=json.load(fp=f1)
                last_best_model=last['last_best_model']
                last_best_score=last['last_best_score']
                if last_best_score > best_m_l:
                    last['last_best_model']=best_m_p
                    last['last_best_score']=best_m_l
                    json.dump(last,fp=f1)
        else:               
            with open(info_file,'w') as f2:
                json.dump(dict(last_best_model=best_m_p,last_best_score=best_m_l),fp=f2)

        best_model = NBeats.load_from_checkpoint(best_m_p)
        predictions = best_model.predict(v_loader, trainer_kwargs=dict(accelerator="cpu",logger=False), return_y=True)
        raw_predictions = best_model.predict(v_loader, mode="raw", return_x=True, trainer_kwargs=dict(accelerator="cpu",logger=False))
    
        for idx in range(10):  # plot 10 examples
            best_model.plot_prediction(raw_predictions.x, raw_predictions.output, idx=idx, add_loss_to_title=True)
        plt.show()
    else:
        with open(info_file) as f:
            best_m_p=json.load(fp=f)['last_best_model']
        print('model path is:',best_m_p)
        best_model = NBeats.load_from_checkpoint(best_m_p)

        # added for input
        best_model.eval()
        t_loader,v_loader,training=spilt_data(dt,
                                t_shuffle=False,t_drop_last=True,
                                v_shuffle=False,v_drop_last=True)

        input_dict = {}
        items = next(iter(v_loader))[0]
        input_names=[]
        for item in items:
            input_dict[item] = items[item][-1:]
            # print("{}:{}".format(item,input_dict[item].shape()))
            input_names.append(item)  
# ------------------------eval----------------------------------------------

        offset=1
        dt=dt.iloc[-max_encoder_length-offset:-offset,:]
        last_=dt.iloc[-1] 
        # print(len(dt))
        for i in range(1,max_prediction_length+1):
            dt.loc[dt.index[-1]+1]=last_
        dt['series']=0
        # dt['time_idx']=dt.apply(lambda x:x.index,args=1)
        dt['time_idx']=dt.index-dt.index[0]
        input_=dt.loc[:,['close','series','time_idx']]
        predictions = best_model.predict(input_, mode='raw',trainer_kwargs=dict(accelerator="cpu",logger=False),return_x=True)
        
        output_names=[]
        for out in predictions.output._fields:
            output_names.append(out)  
# ----------------------------------------------------------------------------
        
        input_1=(input_dict,{}) 
        best_model.to_onnx(file_path='NBeats.onnx', 
                           input_sample=input_1, 
                           input_names=input_names,
                           output_names=output_names)

        import onnxruntime as ort
        sess = ort.InferenceSession("NBeats.onnx")
        input_names = [input.name for input in sess.get_inputs()]
        # for input in sess.get_inputs():
        #     print(input.name,':',input.shape) 
        output_name = sess.get_outputs()[0].name

# ------------------------------------------------------------------------------
        input_ds = New_TmSrDt.from_parameters(best_model.dataset_parameters, input_,predict=True)
        input_dl = input_ds.to_dataloader(train=False, batch_size=1, num_workers=0)
        input_dict = next(iter(input_dl))[0]
        input_data = [input_dict[name].numpy() for name in input_names]
        pred_onnx = sess.run([output_name], dict(zip(input_names, input_data)))
        print("torch result:",predictions.output[0])
        print("onnx result:",pred_onnx[0])
# -------------------------------------------------------------------------------
        
        
        best_model.plot_interpretation(predictions.x,predictions.output,idx=0)
        plt.show()


Создание советника с моделью ONNX

Мы завершили преобразование и тестирование модели и теперь создадим файл советника с именем onnx.mq5. В советнике мы планируем использовать OnTimer() для управления логикой вывода модели и использовать OnTick() для управления логикой ордера, чтобы мы могли установить, как часто выполнять вывод, вместо того, чтобы запускать вывод каждый раз, когда приходит котировка, что приведет к серьезному росту в потреблении ресурсов. Аналогично в этом советнике мы не будем предоставлять сложную торговую логику, а просто приведем демонстрационный пример. Пожалуйста, не используйте этот советник напрямую для торговли!

1. Просмотр структуры модели ONNX

Этот шаг очень важен: нам нужно определить входные и выходные данные для модели ONNX в EA, поэтому нам нужно просмотреть структуру модели, чтобы определить количество, тип данных и размерность входных и выходных данных. Чтобы просмотреть модель ONNX, вы можете открыть ее прямо в редакторе MQL5 и увидеть структуру модели. Также вы увидите стили ввода и вывода, но их нельзя редактировать. Мы также можем использовать инструменты Netron или WinML Dashboard. В этой статье мы используем инструмент Netron.

Мы находим наш файл модели NBeats.onnx в MetaEditor и открываем его напрямую. На скриншоте ниже вы можете найти опцию Open in Netron (открыть в Netron). Нажмите кнопку, и файл модели откроется автоматически.

o0

Или щелкните правой кнопкой мыши файл нашей модели в проводнике файлов MetaEditor, и вы увидите опцию Open in Netron.

o1

Если у вас нет Netron, среда разработки поможет вам его установить.

После открытия модель выглядит так:

md

Вы можете видеть, что весь интерфейс очень простой и удобный, а функция очень мощная. Мы даже можем использовать ее для редактирования узлов модели. Но вернемся к теме - нажмем на первый узел, и Netron покажет нам соответствующую информацию о модели:

inf

Формат экспортированной модели NBeats: ONNX v8, версия pytorch: pytorch 2.1.1, инструмент экспорта: ai.onnx v17.

Есть два входа, первый: encoder_cont, размерность: [1,96,1], формат данных: float32; второй: target_scale, размерность: [1,2], формат данных: float32.

Есть пять выходных данных, первый: prediction, размерность: [1,20]; второй: backcast, размерность: [1,96]; остальные три интерпретируемых выходных данных: trend, seasonality, generic dimension: [1,116]. Все форматы выходных данных — float32.


2. Определим входные и выходные данные модели

Мы уже знаем формат ввода и вывода модели. Форматы ввода и вывода, поддерживаемые ONNX в MQL5, — это массивы, матрицы и векторы. Теперь определим их в советнике. Сначала определим входные данные в OnTimer(). Оба являются массивами:

  • Первый: matrixf in_normf;
  • Второй: float in1[1][2];

Поскольку нам нужно вызвать выходные результаты модели в OnTick(), неразумно определять выходные данные модели в OnTimer(). Их необходимо определить как глобальные переменные. Результаты вывода модели и дескриптор загрузки модели также необходимо определить как глобальные переменные:

  • Хэндл модели: long handle;
  • Первый результат вывода: vectorf y=vector<float>::Zeros(20);
  • Второй результат вывода: vectorf backcast=vector<float>::Zeros(96);
  • Третий результат вывода: vectorf trend=vector<float>::Zeros(116);
  • Четвертый результат вывода: vectorf seasonality=vector<float>::Zeros(116);
  • Пятый результат вывода: vectorf generic=vector<float>::Zeros(116);
  • Определим результат прогноза: string pre=NULL;

3. Определение логики вывода

Ⅰ Инициализация

Сначала импортируем модель ONNX как внешний ресурс в советник: #resource “NBeats.onnx” as uchar ExtModel[]. Инициализируем таймер в функции OnInit(): EventSetTimer(300), это значение можно установить самостоятельно. Загрузим модель и получим дескриптор модели: handle=OnnxCreateFromBuffer(ExtModel,ONNX_DEBUG_LOGS). Если вы хотите просмотреть входную или выходную информацию модели, вы можете добавить следующий оператор:

   long in_ct=OnnxGetInputCount(handle);
   OnnxTypeInfo inf;
   for(int i=0;i<in_ct;i++){
   
   Print(OnnxGetInputName(handle,i));
   bool re=OnnxGetInputTypeInfo(handle,i,inf);
   //Print("map:",inf.map,"seq:",inf.sequence,"tensor:",inf.tensor,"type:",inf.type);
   Print(re,GetLastError());
   }

Ⅱ Обработка данных

Ранее мы уже определили входные и выходные данные модели, а теперь нам нужно знать конкретное определение этих переменных и тип их данных. Для этого нам необходимо найти их определения в файле timeseries.py библиотеки pytorch_forecasting. В этой статье не будет подробно объясняться этот файл, давайте сразу же перейдем к ответу.

Первый входной параметр:

encoder_cont на самом деле является нормализованным значением целевой переменной. Конечно, pytorch_forecasting предоставляет различные методы EncoderNormalizer, GroupNormalizer, MultiNormalizer, NaNLabelEncoder, TorchNormalizer. Эти методы может быть сложно реализовать в MQL5, поэтому в этой статье мы напрямую используем обычный метод нормализации. Сначала определим пустой MqlRates: MqlRates rates[], а затем используем его для копирования последних 96 баров значений закрытия: if(!CopyRates(_Symbol,_Period,0,96,rates)) return, если копирование не удалось, вернем напрямую. Нам также необходимо определить матрицу для получения этого значения, которое используется для вычисления среднего значения и дисперсии: matrix in0_m(96,1). Скопируйте значение закрытия в матрицу: for(int i=0; i<96; i++) in0_m[i][0]= rates[i].close. Вычислим среднее: vector m=in0_m.Mean(0); рассчитаем дисперсию: vector s=in0_m.Std(0). Создадим матрицу mm для хранения среднего значения: matrix mm(96,1); создадим матрицу ms для хранения дисперсии: matrix ms(96,1). Скопируем среднее значение и дисперсию во вспомогательную матрицу:

    for(int i=0; i<96; i++) 
     { 
        mm.Row(m,i); 
        ms.Row(s,i); 
         } 

Теперь мы вычисляем нормализованную матрицу. Сначала вычитаем среднее значение: in0_m-=mm, затем делим на стандартное отклонение: in0_m/=ms и копируем матрицу во входную матрицу и преобразуем тип данных в float: in_normf.Assign(in0_m)

Второй входной параметр:

"target_scale" на самом деле является диапазоном масштабирования целевой переменной. Его первое значение фактически является средним значением целевой переменной: in1[0][0]=m[0], второе значение - дисперсия целевой переменной: in1[0][1]=s[0]


Ⅲ Вывод

При выполнении вывода модели ONNX все без исключений входные и выходные данные, отображаемые в структуре модели, должны быть определены. Даже входные данные, которые вам не нужны, также должны быть переданы в качестве параметров функции OnnxRun(). В противном случае ошибка неизбежна.

   if(!OnnxRun(handle,
      ONNX_DEBUG_LOGS | ONNX_NO_CONVERSION,
      in_normf,
      in1,
      y,
      backcast,
      trend,
      seasonality,
      generic)) 
    { 
      Print("OnnxRun failed, error ",GetLastError()); 
      OnnxRelease(handle);
      return; 
      } 

4. Результаты вывода

Мы делаем простое предположение: если среднее значение прогнозируемого значения больше, чем среднее значение самого высокого и самого низкого значений текущего бара, мы предполагаем, что в будущем будет восходящий тренд, и устанавливаем pre на buy (покупка), в противном случае - на sell (продажа):

   if (y.Mean()>iHigh(_Symbol,_Period,0)/2+iLow(_Symbol,_Period,0)/2)
      pre="buy";
   else
      pre="sell";

5. Логика обработки ордеров

Эту часть мы уже подробно рассмотрели в статье "Разметка данных в анализе временных рядов (Часть 5):Применение и тестирование советника с помощью Socket". В этой статье мы лишь скопируем основную логику в OnTick() и использовать ее напрямую. Следует отметить, что после каждого выполнения pre устанавливается в NULL, и в процессе прогнозирования мы будем присваивать значения этим двум значениям, что обеспечивает синхронизацию процессов обработки ордера и прогнозирования и не будет зависеть от предыдущего значения прогноза. Этот шаг очень важен. Его отсутствие вызовет некоторую логическую путаницу. Ниже приведен полный код обработки ордера:

void OnTick()
  {
//---
   MqlTradeRequest request;
   MqlTradeResult result;
   //int x=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE);

    if (pre!=NULL)
    {
        //Print("The predicted value is:",pre);
        ulong numt=0;
        ulong tik=0;
        bool sod=false;
        ulong tpt=-1;
        ZeroMemory(request); 
        numt=PositionsTotal();
        //Print("All tickets: ",numt);
        if (numt>0)
         {  tik=PositionGetTicket(numt-1);    
            sod=PositionSelectByTicket(tik);
            tpt=PositionGetInteger(POSITION_TYPE);//ORDER_TYPE_BUY or ORDER_TYPE_SELL
            if (tik==0 || sod==false || tpt==0) return; 
            }
        if (pre=="buy")
        {  
           
           if (tpt==POSITION_TYPE_BUY)
               return;
               
            request.action=TRADE_ACTION_DEAL;
            request.symbol=Symbol();
            request.volume=0.1;
            request.deviation=5;
            request.type_filling=ORDER_FILLING_IOC;
            request.type = ORDER_TYPE_BUY;  
            request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
           if(tpt==POSITION_TYPE_SELL)
             {
               request.position=tik;
               Print("Close sell order.");
                    }
           else{     
  
            Print("Open buy order.");
                     }
            OrderSend(request, result);
               }
        else{
           if (tpt==POSITION_TYPE_SELL)
               return;
               
            request.action = TRADE_ACTION_DEAL;      
            request.symbol = Symbol();  
            request.volume = 0.1;  
            request.type = ORDER_TYPE_SELL;  
            request.price = SymbolInfoDouble(Symbol(), SYMBOL_BID);  
            request.deviation = 5; 
            //request.type_filling=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE);
            request.type_filling=ORDER_FILLING_IOC;
           if(tpt==POSITION_TYPE_BUY)
               {
               request.position=tik;
               Print("Close buy order.");
                    }
           else{

               Print("OPen sell order.");
                    }
            
            OrderSend(request, result);
              }
        //is_pre=false;
        }
    pre=NULL;

  }


6. Возврат ресурсов

При работе советника нам нужно закрыть таймер и освободить хэндл экземпляра модели ONNX, поэтому нам нужно добавить следующий код в функцию OnDeinit(const int Reason):

void OnDeinit(const int reason)
  {
//---
   //— destroy timer 
  EventKillTimer(); 
  //— complete operation 
  OnnxRelease(handle); 
  }

На этом мы практически закончили писать код. Далее нам нужно загрузить и протестировать советник на истории.

Примечания:

1. При настройке ввода и вывода модели ONNX необходимо обратить внимание на соответствие формата данных.

2. Здесь мы используем только первое прогнозируемое значение выходных данных, что не означает, что другие выходные данные не имеют значения. В статье "Разметка данных в анализе временных рядов (Часть 4): Декомпозиция интерпретируемости с использованием разметки данных" мы представили интерпретируемость модели NBeats, которая реализуется с использованием других выходных данных. Их визуализацию мы уже проверили на Python, и в этой статье функцию визуализации в советнике мы добавлять не будем. Заинтересованные читатели могут попробовать добавить один или несколько из них на график для визуализации.


Тестирование на истории

Прежде чем приступить к тестированию на истории, следует отметить одну вещь: наша модель ONNX должна быть размещена в том же каталоге, что и файл onnx.mq5, иначе загрузить файл модели не удастся! Все готово, теперь открываем редактор MQL5, нажимаем кнопку "Компилировать" и генерируем скомпилированный файл. Если компиляция прошла успешно, нажмите Ctrl+F5, чтобы начать тестирование в режиме отладки. Откроется новое окно, демонстрирующее процесс тестирования. Так выглядит твой журнал:

lg

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

hc

Мы сделали это!

Полный код:

//+------------------------------------------------------------------+
//|                                                         onnx.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#resource "NBeats.onnx" as uchar ExtModel[] 



long handle;
vectorf y=vector<float>::Zeros(20); 
vectorf backcast=vector<float>::Zeros(96);
vectorf trend=vector<float>::Zeros(116);
vectorf seasonality=vector<float>::Zeros(116);
vectorf generic=vector<float>::Zeros(116);
//bool is_pre=false;
string pre=NULL;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   EventSetTimer(300); 
   handle=OnnxCreateFromBuffer(ExtModel,ONNX_DEBUG_LOGS); 
   //— specify the shape of the input data 

   long in_ct=OnnxGetInputCount(handle);
   OnnxTypeInfo inf;
   for(int i=0;i<in_ct;i++){
   
   Print(OnnxGetInputName(handle,i));
   bool re=OnnxGetInputTypeInfo(handle,i,inf);
   //Print("map:",inf.map,"seq:",inf.sequence,"tensor:",inf.tensor,"type:",inf.type);
   Print(re,GetLastError());
   }
   //long in_nm=OnnxGetInputName()
   
   


//— return initialization result 
 
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   //— destroy timer 
  EventKillTimer(); 
  //— complete operation 
  OnnxRelease(handle); 
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   MqlTradeRequest request;
   MqlTradeResult result;
   //int x=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE);

    if (pre!=NULL)
    {
        //Print("The predicted value is:",pre);
        ulong numt=0;
        ulong tik=0;
        bool sod=false;
        ulong tpt=-1;
        ZeroMemory(request); 
        numt=PositionsTotal();
        //Print("All tickets: ",numt);
        if (numt>0)
         {  tik=PositionGetTicket(numt-1);    
            sod=PositionSelectByTicket(tik);
            tpt=PositionGetInteger(POSITION_TYPE);//ORDER_TYPE_BUY or ORDER_TYPE_SELL
            if (tik==0 || sod==false || tpt==0) return; 
            }
        if (pre=="buy")
        {  
           
           if (tpt==POSITION_TYPE_BUY)
               return;
               
            request.action=TRADE_ACTION_DEAL;
            request.symbol=Symbol();
            request.volume=0.1;
            request.deviation=5;
            request.type_filling=ORDER_FILLING_IOC;
            request.type = ORDER_TYPE_BUY;  
            request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
           if(tpt==POSITION_TYPE_SELL)
             {
               request.position=tik;
               Print("Close sell order.");
                    }
           else{     
  
            Print("Open buy order.");
                     }
            OrderSend(request, result);
               }
        else{
           if (tpt==POSITION_TYPE_SELL)
               return;
               
            request.action = TRADE_ACTION_DEAL;      
            request.symbol = Symbol();  
            request.volume = 0.1;  
            request.type = ORDER_TYPE_SELL;  
            request.price = SymbolInfoDouble(Symbol(), SYMBOL_BID);  
            request.deviation = 5; 
            //request.type_filling=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE);
            request.type_filling=ORDER_FILLING_IOC;
           if(tpt==POSITION_TYPE_BUY)
               {
               request.position=tik;
               Print("Close buy order.");
                    }
           else{

               Print("OPen sell order.");
                    }
            
            OrderSend(request, result);
              }
        //is_pre=false;
        }
    pre=NULL;

  }
//+------------------------------------------------------------------+
void OnTimer() 
{ 
   //float in0[1][96][1];
   matrixf in_normf; 
   float in1[1][2];
//— get the last 10 bars 
   MqlRates rates[]; 
   if(!CopyRates(_Symbol,_Period,0,96,rates)) return; 
  //— input a set of OHLC vectors 


   //double out[1][20];
   matrix in0_m(96,1);
   for(int i=0; i<96; i++) 
     { 
       in0_m[i][0]= rates[i].close;
       } 
   //— normalize the input data 
   // matrix x_norm=x; 
    vector m=in0_m.Mean(0);  
    vector s=in0_m.Std(0); 
    
    in1[0][0]=m[0];
    in1[0][1]=s[0];
    matrix mm(96,1); 
    matrix ms(96,1); 
   //    //— fill in the normalization matrices 
    for(int i=0; i<96; i++) 
     { 
        mm.Row(m,i);  
        ms.Row(s,i); 
         } 
   //    //— normalize the input data 
   in0_m-=mm;  
   in0_m/=ms; 
   // //— convert normalized input data to float type 
   
   in_normf.Assign(in0_m); 
    //— get the output data of the model here, i.e. the price prediction 
    
    //— run the model 
   if(!OnnxRun(handle,
      ONNX_DEBUG_LOGS | ONNX_NO_CONVERSION,
      in_normf,
      in1,
      y,
      backcast,
      trend,
      seasonality,
      generic)) 
    { 
      Print("OnnxRun failed, error ",GetLastError()); 
      OnnxRelease(handle);
      return; 
      } 
    //— print the output value of the model to the log 
   //Print(y); 
   //is_pre=true;
   if (y.Mean()>iHigh(_Symbol,_Period,0)/2+iLow(_Symbol,_Period,0)/2)
      pre="buy";
   else
      pre="sell";
}


Особенности

Это последняя статья в серии. В этой статье мы подробно представили весь процесс преобразования модели torch в модель ONNX, в том числе, как найти входные и выходные данные модели, как определить их форматы, как сопоставить их с моделью, а также методы обработки данных. Сложность этой статьи заключается в том, как экспортировать модель со сложным вводом и выводом в виде модели ONNX. Надеюсь, статья натолкнет читателей на новые находки. Конечно, нашему тестовому советнику еще есть куда совершенствоваться. Например, вы можете визуализировать тренд и сезонность выходных данных модели NBeats на графике, использовать выходной тренд для определения направления ордера и т. д.

Возможностей очень много. Пример в статье очень прост, но основное содержание является относительно полным. Вы можете свободно расширять и использовать советник, но только не применяйте его в реальной торговле! В серии представлены разнообразные и относительно полные решения: от создания наборов данных до обучения различных моделей прогнозирования временных рядов, а затем их использования при тестировании на истории. Даже новички могут пройти весь процесс шаг за шагом и применить полученные знания на практике.
Спасибо за внимание! Надеюсь, вы узнали что-то новое для себя.




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

Прикрепленные файлы |
NBeats.onnx (6949.02 KB)
onnx.mq5 (11.99 KB)
n_beats.py (11.07 KB)
Алгоритм эволюции панциря черепахи (Turtle Shell Evolution Algorithm, TSEA) Алгоритм эволюции панциря черепахи (Turtle Shell Evolution Algorithm, TSEA)
Уникальный алгоритм оптимизации, вдохновленный эволюцией панциря черепахи. Алгоритм TSEA эмулирует постепенное формирование ороговевших участков кожи, которые представляют собой оптимальные решения задачи. Лучшие решения становятся более "твердыми" и располагаются ближе к внешней поверхности, в то время как менее удачные решения остаются "мягкими" и находятся внутри. Алгоритм использует кластеризацию решений по качеству и расстоянию, позволяя сохранять менее успешные варианты и обеспечивая гибкость и адаптивность.
Нейросети — это просто (Часть 89): Трансформер частотного разложения сигнала (FEDformer) Нейросети — это просто (Часть 89): Трансформер частотного разложения сигнала (FEDformer)
Все рассмотренные нами ранее модели анализируют состояние окружающей среды в виде временной последовательности. Однако, тот же временной ряд можно представить и в виде частотных характеристик. В данной статье я предлагаю вам познакомиться с алгоритмом, который использует частотные характеристики временной последовательности для прогнозирования будущих состояний.
Разрабатываем мультивалютный советник (Часть 10): Создание объектов из строки Разрабатываем мультивалютный советник (Часть 10): Создание объектов из строки
План разработки советника предусматривает несколько этапов с сохранением промежуточных результатов в базе данных. Заново достать их оттуда можно только в виде строк или чисел, а не объектов. Поэтому нам нужен способ воссоздания в советнике нужных объектов из строк, прочитанных из базы данных.
Возможности Мастера MQL5, которые вам нужно знать (Часть 10): Нетрадиционная RBM Возможности Мастера MQL5, которые вам нужно знать (Часть 10): Нетрадиционная RBM
Ограниченные машины Больцмана (Restrictive Boltzmann Machines, RBM) представляют собой на базовом уровне двухслойную нейронную сеть, способную выполнять неконтролируемую классификацию посредством уменьшения размерности. Мы используем ее основные принципы и посмотрим что случится, если мы перепроектируем и обучим ее нестандартно. Сможем ли мы получить полезный фильтр сигналов?