English Русский 日本語
preview
Data label for time series mining (Part 6):Apply and Test in EA Using ONNX

Data label for time series mining (Part 6):Apply and Test in EA Using ONNX

MetaTrader 5Beispiele | 17 Mai 2024, 13:19
80 0
Yuqiang Pan
Yuqiang Pan

Einführung

Im vorigen Artikel haben wir besprochen, wie man einen Socket (Websocket) zur Kommunikation eines EAs mit einem Python-Server verwendet, um das Backtesting-Problem zu lösen, und wir haben auch erläutert, warum wir diese Technik gewählt haben. In diesem Artikel wird erörtert, wie ONNX, das von mql5 nativ unterstützt wird, verwendet werden kann, um Inferenzen mit unserem Modell durchzuführen, aber diese Methode hat einige Einschränkungen. Wenn Ihr Modell Operatoren verwendet, die von ONNX nicht unterstützt werden, kann es zu Fehlern kommen, sodass diese Methode nicht für alle Modelle geeignet ist (natürlich können Sie auch Operatoren hinzufügen, um Ihr Modell zu unterstützen, aber das erfordert viel Zeit und Mühe). Aus diesem Grund habe ich im letzten Artikel viel Platz darauf verwendet, die Socket-Methode vorzustellen und sie Ihnen zu empfehlen.

Natürlich ist die Konvertierung eines allgemeinen Modells in das ONNX-Format sehr praktisch und bietet uns eine effektive Unterstützung für plattformübergreifende Operationen. Dieser Artikel befasst sich hauptsächlich mit einigen grundlegenden Operationen zum Betrieb von ONNX-Modellen in mql5, einschließlich der Frage, wie man die Ein- und Ausgabe von Torch-Modellen und ONNX-Modellen aufeinander abstimmt und wie man geeignete Datenformate für ONNX-Modelle konvertiert. Dazu gehört natürlich auch die EA-Auftragsverwaltung. Ich werde es Ihnen im Detail erklären. Kommen wir nun zum Hauptthema dieses Artikels!

Inhaltsverzeichnis


Verzeichnisstruktur

Wenn wir die Modellkonvertierung durchführen, werden wir die Modell- und Konfigurationsdateien lesen, aber peinlicherweise habe ich in den vorherigen Artikeln nicht die Verzeichnisstruktur des Skripts vorgestellt, was dazu führen kann, dass Sie den Speicherort Ihrer Modell- und Konfigurationsdateien nicht finden. Wir sortieren hier also die Verzeichnisstruktur unseres Skripts. Wenn wir Lightning-Pytorch zum Trainieren des Modells verwenden, haben wir den Speicherort des Modells nicht in den Callbacks definiert (die Callbacks, die für die Verwaltung des Modells Checkpoint verantwortlich sind, sind die ModelCheckpoint-Klasse), sondern nur den Modellnamen definiert, sodass der Trainer das Modell im Standardpfad speichern wird.
    ck_callback=ModelCheckpoint(monitor='val_loss',
                                mode="min",
                                save_top_k=1,  
                                filename='{epoch}-{val_loss:.2f}')

Zu diesem Zeitpunkt speichert das Training das Modell im Stammverzeichnis, was vielleicht etwas vage ist, daher verwende ich einige Bilder zur Veranschaulichung, damit Sie genau wissen, welche Dateien während des Trainingsprozesses gespeichert werden und wo sich die Dateien befinden.

Dieser Pfad enthält verschiedene Versionsordner, jeder Versionsordner enthält einen Checkpoint-Ordner, eine Ereignisdatei und eine Parameterdatei, der Checkpoint-Ordner enthält die gespeicherte Modelldatei:

f3


Beim Training des Modells haben wir ein Modell verwendet, um die beste Lernrate zu finden, die im Stammverzeichnis des Ordners gespeichert wird:


f2

Beim Training wird eine results.json-Datei gespeichert, um den besten Modellpfad und die beste Punktzahl festzuhalten, die beim Laden des Modells verwendet wird:

f4


Torch-Modell in ONNX-Modell umwandeln

Wir verwenden weiterhin das NBeats-Modell als Beispiel. Der folgende Code wird hauptsächlich in den Inferenzteil von Nbeats.py eingefügt. Dieses Skript wurde erstellt, als ich das NBeats-Modell im vorherigen Artikel vorstellte. Aufgrund der besonderen Natur des NBeats-Modells kann es schwierig sein, das ONNX-Modell mit der allgemeinen Methode zu exportieren. Sie müssen den Inferenzprozess des Modells debuggen und dann die relevanten Informationen daraus gewinnen, um die für den Export erforderlichen Parameter zu definieren. Aber ich habe diesen Prozess für Sie erledigt, also machen Sie sich keine Sorgen, folgen Sie einfach den Schritten in diesem Artikel Schritt für Schritt, und alle Probleme werden leicht gelöst werden.

1. Installieren der erforderlichen Bibliotheken

Vor der Konvertierung des Modells ist ein weiterer wichtiger Schritt zu tun, nämlich die Installation der relevanten Bibliotheken von ONNX. Wenn wir nur das Modell exportieren, müssten wir nur die ONNX-Bibliothek installieren: pip install onnx. Da wir das Modell nach der Konvertierung aber auch testen müssen, müssen wir auch die ONNX-Runtime-Bibliothek installieren. Diese Bibliothek ist in zwei Versionen unterteilt: CPU-Laufzeit und GPU-Laufzeit. Wenn das Modell groß und komplex ist, müssen Sie möglicherweise die GPU-Version installieren, um den Inferenzprozess zu beschleunigen. Da unser Modell nur CPU-Inferenz benötigt, ist der Effekt der GPU-Beschleunigung nicht offensichtlich, daher empfehle ich die Installation der CPU-Version: pip install onnxruntime.


2. Eingabeinformationen abrufen

Zunächst müssen wir das Modell vom Trainingsmodus in den Inferenzmodus umschalten: best_model.eval(). Der Grund dafür ist, dass der Trainingsmodus und der Inferenzmodus des Modells unterschiedlich sind und wir nur den Inferenzmodus des Modells benötigen, der die Komplexität des Modells reduziert und nur die für die Inferenz erforderlichen Eingaben beibehält. Dann müssen wir nach dem Laden der Daten einen Dataloader erstellen, um die vollständigen Eingabeelemente zu erhalten, einen Iterator von diesem Dataloader-Objekt erhalten und dann die nächste Funktion aufrufen, um den ersten Datenstapel zu erhalten. Das erste Element enthält alle erforderlichen Eingabedaten. Während des Exportvorgangs des Modells wählt torch automatisch die erforderlichen Eingabeelemente für uns aus. Nun verwenden wir die zuvor definierte Funktion spilt_data(), um nach dem Laden der Daten direkt einen Dataloader zu erstellen: t_loader,v_loader,training=spilt_data(dt,t_shuffle=False,t_drop_last=True,v_shuffle=False,v_drop_last=True). Nun erstellen wir ein „Wörterbuch“ zum Speichern der für den Export des Modells erforderlichen Eingaben: input_dict = {}. Wir holen alle Eingabeobjekte, hier verwenden wir v_loader, um sie zu holen, weil wir den Inferenzprozess brauchen: items = next(iter(v_loader))[0]. Wir erstellen eine Liste, um alle Eingabenamen zu speichern: input_names=[]. Dann iterieren wir durch die Elemente, um alle Eingaben und Eingabenamen zu erhalten:

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

3. Abrufen von Ausgabeinformationen

Bevor wir die Ausgabe erhalten, müssen wir zunächst eine Inferenz durchführen und dann die benötigten Informationen aus dem Ergebnis der Inferenz abrufen. Dies ist der ursprüngliche Inferenzprozess:

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)

Die Rückschlussinformationen befinden sich in der Ausgabe des Predictions-Objekts, wir iterieren durch dieses Objekt, um alle Ausgabeinformationen zu erhalten, also fügen wir hier die folgende Anweisung ein:
output_names=[]
for out in predictions.output._fields:
    output_names.append(out)

4. Exportieren des Modells

Zuerst definieren wir das input_sample, das für den Export des Modells benötigt wird: input_1=(input_dict,{}), fragen Sie nicht warum, tun Sie es einfach! Dann verwenden wir die Methode to_onnx() der Klasse NBeats, um nach ONNX zu exportieren, die ebenfalls einen Dateipfad-Parameter erfordert. Wir exportieren direkt in das Stammverzeichnis mit dem Namen „NBeats.onnx“: best_model.to_onnx(file_path='NBeats.onnx', input_sample=input_1, input_names=input_names, output_names=output_names). Nachdem das Programm bis zu diesem Punkt gelaufen ist, finden wir die Datei „NBeats.onnx“ im Stammverzeichnis des aktuellen Ordners:



Anmerkung:

1. Wenn der Eingabename nicht vollständig ist, wird das Exportmodell ihn beim Exportieren automatisch benennen, was zu einer gewissen Verwirrung führt, sodass wir nicht wissen, welcher der echte Eingaben ist. Daher entscheiden wir uns, alle Namen in die Exportfunktion einzugeben, um die Konsistenz der Eingabenamen des exportierten Modells zu gewährleisten.

2. Im Dataloader umfassen die Eingabedaten „encoder_cat“, „encoder_cont“ und andere mehrfache Eingaben, die vom Encoder und Decoder erzeugt werden, während wir für den Inferenzprozess nur zwei Eingaben benötigen: „encoder_cont“ und „target_scale“. Denken Sie also nicht, dass der Schritt des Abgleichs der Eingabedaten überflüssig ist, bei einigen Modellen, die Encoder und Decoder erfordern, ist dieser Schritt notwendig. 3. Die vom Autor während des Testprozesses verwendete Umgebungskonfiguration: python-3.10;ONNX Version-8; pytorch-2.1.1;operators-17.



Testen des konvertierten Modells

Im vorangegangenen Teil haben wir das Torch-Modell erfolgreich als ONNX-Modell exportiert. Die nächste wichtige Aufgabe besteht darin, dieses Modell zu testen und festzustellen, ob die Ausgabe dieses Modells mit der des ursprünglichen Modells übereinstimmt. Dies ist sehr wichtig, denn während des Exportvorgangs kann es bei einigen Operatoren zu Abweichungen aufgrund von Kompatibilitätsproblemen zwischen der Torch-Version und dem ONNX-Laufzeitkern kommen. In diesem Fall kann beim Exportieren des Modells ein manueller Eingriff erforderlich sein.
  • Importieren wir zunächst die ONNX-Laufzeitbibliothek: import onnxruntime as ort.
  • Wir laden die Modelldatei „NBeats.onnx“: sess = ort.InferenceSession(“NBeats.onnx“).
  • Ermitteln der Eingabenamen des ONNX-Modells durch Iteration über den Rückgabewert von sess.get_inputs(), die zur Anpassung der Eingabedaten verwendet werden: input_names = [input.name for input in sess.get_inputs()].
  • Wir müssen nicht alle Ausgaben vergleichen, also holen wir nur das erste Element der Ausgabe, um zu sehen, ob die Ergebnisse gleich sind: output_name = sess.get_outputs()[0].name.
  • Um vergleichen zu können, ob die Ergebnisse gleich sind, muss die Eingabe gleich sein, d. h. die Modelleingabe muss mit den für die Schlussfolgerung verwendeten Daten übereinstimmen. Wir müssen sie jedoch zunächst in das Dataloader-Format konvertieren und input_names verwenden, um die Eingabedaten abzugleichen, da nicht alle Eingaben während des Inferenzprozesses geladen werden. Zunächst laden wir die Eingabedaten als Zeitreihendaten mit der Methode from_parameters() der Klasse TimeSeriesDataSet: input_ds = New_TmSrDt.from_parameters(best_model.dataset_parameters, input_,predict=True). Dann konvertieren wir sie in den Dataloader-Typ mit der to_dataloader()-Klassenmethode: input_dl = input_ds.to_dataloader(train=False, batch_size=1, num_workers=0).
  • Passen wir noch die Eingabedaten an. Zunächst müssen wir einen Datenstapel abrufen und das erste Element herausnehmen: input_dict = next(iter(input_dl))[0]. Dann verwenden wir input_names, um die für die Eingabe erforderlichen Daten zu finden: input_data = [input_dict[name].numpy() for name in input_names]
  • Starten wir die Inferenz: pred_onnx = sess.run([output_name], dict(zip(input_names, input_data)))[0].
  • Wir drucken das Ergebnis der Torch-Schlussfolgerung und das Ergebnis der OnnxSchlussfolgerung aus und vergleichen sie.
Ausdruck des Ergebnisses der Torch-Inferenz:

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]])

Ausdruck des Ergebnisses der ONNX-Inferenz:

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 ]]

Wir sehen, dass die Ergebnisse unserer Modellinferenz gleich sind. Der nächste Schritt ist die Konfiguration des exportierten Modells in mql5. Wie in der Abbildung dargestellt:

f6


Hier der vollständiger Code:
# 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()


EA mit ONNX-Modell erstellen

Wir haben die Modellkonvertierung und -prüfung abgeschlossen und werden nun eine Expertendatei namens onnx.mq5 erstellen. Wir planen im EA, OnTimer() zu verwenden, um die Inferenzlogik des Modells zu verwalten, und OnTick() zu verwenden, um die Auftragslogik zu verwalten, sodass wir festlegen können, wie oft die Inferenz ausgeführt werden soll, anstatt die Inferenz jedes Mal auszuführen, wenn ein Angebot kommt, was zu einer ernsthaften Ressourcenauslastung führt. Auch in diesem EA gibt es keine komplexe Handelslogik, es ist nur eine Demonstration, ein Beispiel, bitte verwenden Sie ihn nicht direkt für Ihren Handel!

1. Ansicht der ONNX-Modellstruktur

Dieser Schritt ist sehr wichtig, da wir die Eingabe und Ausgabe für das ONNX-Modell in EA definieren müssen. Wir müssen also die Modellstruktur betrachten, um die Anzahl, den Datentyp und die Datendimension der Eingabe und Ausgabe zu bestimmen. Um das ONNX-Modell zu betrachten, können Sie es direkt im mql5-Editor öffnen, wo Sie die Modellstruktur sehen. Sie zeigt Ihnen auch die Eingabe- und Ausgabestile an, kann aber nicht bearbeitet werden. Wir können auch Netron oder WinML Dashboard-Tools verwenden, das Tool, das wir in diesem Artikel verwenden, ist Netron.

Wir finden unsere Modelldatei „NBeats.onnx“ in der mql5 IDE und öffnen sie direkt, in der Anmerkungsposition unten finden Sie die Option „Open in Netron“, klicken Sie auf die Schaltfläche und die Modelldatei wird automatisch geöffnet.

o0

Oder klicken Sie mit der rechten Maustaste auf unsere Modelldatei im Dateiexplorer der IDE und Sie werden die Option „Open in Netron“ sehen.

o1

Wenn Sie das Netron-Tool nicht haben, wird die IDE Sie bei der Installation anleiten.

Das Modell sieht nach dem Öffnen wie folgt aus:

md

Sie können sehen, dass die gesamte Schnittstelle sehr einfach und erfrischend ist, und die Funktion ist sehr leistungsfähig. Wir können sie sogar zum Bearbeiten der Modellknoten verwenden. Nun zurück zum Thema, wir klicken auf den ersten Knoten, und Netron zeigt uns die relevanten Informationen des Modells:

inf

Sie können sehen, dass das Format des exportierten NBeats-Modells wie folgt lautet: ONNX v8, pytorch Version ist: pytorch 2.1.1, Import ist: ai.onnx v17.

Es gibt zwei Eingabevariablen, die erste ist: encoder_cont, Dimension ist: [1,96,1], Datenformat ist: float32; die zweite ist: target_scale, Dimension ist: [1,2], Datenformat ist: float32.

Es gibt fünf Outputs, der erste ist: Vorhersage, Dimension: [1,20]; der zweite ist: Backcast, Dimension: [1,96]; die anderen drei interpretierbaren Outputs Trend, Saisonalität, generische Dimension sind [1,116]. Alle Ausgabedatenformate sind float32.


2. Definieren Sie die Eingabe und Ausgabe des Modells

Wir kennen bereits das Eingabe- und Ausgabeformat des Modells, und die von ONNX in mql5 unterstützten Eingabe- und Ausgabeformate sind Arrays, Matrizen und Vektoren. Nun wollen wir sie im EA definieren. Zunächst definieren wir die Eingabe in OnTimer(), beides sind Arrays:

  • Die erste Eingabe: matrixf in_normf;
  • Die zweite Eingabe: float in1[1][2];

Da wir die Ausgabeergebnisse des Modells in OnTick() aufrufen müssen, ist es unvernünftig, die Ausgabe des Modells in OnTimer() zu definieren, und sie müssen als globale Variablen definiert werden. Die Ergebnisse der Modellinferenz und der Modellladegriff müssen ebenfalls als globale Variablen definiert werden:

  • Modell Griff: langer Griff;
  • Das erste Inferenz-Ergebnis: vectorf y=vector<float>::Zeros(20);
  • Das zweite Inferenz-Ergebnis: vectorf backcast=vector<float>::Zeros(96);
  • Das dritte Inferenz-Ergebnis: vectorf trend=vector<float>::Zeros(116);
  • Das vierte Inferenz-Ergebnis: vectorf seasonality=vector<float>::Zeros(116);
  • Das fünfte Inferenz-Ergebnis: vectorf generic=vector<float>::Zeros(116);
  • Wir definieren das Vorhersageergebnis: string pre=NULL;

3. Definieren der Inferenzlogik

Ⅰ Initialisierung

Zunächst importieren wir das ONNX-Modell als externe Ressource in EA: #resource „NBeats.onnx“ as uchar ExtModel[], initialisieren den Timer mit der Funktion OnInit(): EventSetTimer(300), dieser Wert kann von Ihnen selbst festgelegt werden, laden das Modells und holen uns das Handle des Modells: handle=OnnxCreateFromBuffer(ExtModel,ONNX_DEBUG_LOGS). Wenn wir uns die Eingabe- oder Ausgabeinformationen des Modells anzeigen möchten, können wir die folgende Anweisung hinzufügen:

   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());
   }

Ⅱ Datenverarbeitung

Wir haben bereits die Ein- und Ausgaben des Modells definiert, und als nächstes müssen wir die spezifische Definition dieser Variablen kennen, d.h. welche Art von Daten sie sind. Dazu müssen wir ihre Definitionen in der Datei timeseries.py in der Bibliothek pytorch_forecasting finden. In diesem Artikel wird diese Datei nicht im Detail erklärt, sondern die Antwort wird direkt verraten.

Die erste Eingabe:

„encoder_cont“ ist eigentlich der normalisierte Wert der Zielvariable, natürlich bietet pytorch_forecasting verschiedene Methoden: EncoderNormalizer, GroupNormalizer, MultiNormalizer, NaNLabelEncoder, TorchNormalizer, aber es könnte schwierig sein, diese Methoden in mql5 zu implementieren, daher verwenden wir in diesem Artikel gleich die gewöhnliche Methode der Normalisierung. Definieren wir zunächst ein leeres MqlRates: MqlRates rates[], und verwenden es dann, um die letzten 96 Balken der Close-Preise zu kopieren: if(!CopyRates(_Symbol,_Periode,0,96,rates)) return, wenn das Kopieren fehlschlägt, direkt return. Außerdem muss eine Matrix definiert werden, die diesen Wert aufnimmt und zur Berechnung von Mittelwert und Varianz verwendet wird: matrix in0_m(96,1). Wir kopieren die Close-Werte in die in0_m-Matrix: for(int i=0; i<96; i++) in0_m[i][0]= rates[i].close und berechnen den Mittelwert: Vektor m=in0_m.Mean(0); und die Varianz: Vektor s=in0_m.Std(0). Wir erstellen eine Matrix mm zur Speicherung des Mittelwerts: matrix mm(96,1); und erstellen eine Matrix ms zur Speicherung der Varianz: matrix ms(96,1). Wir kopieren den Mittelwert und die Varianz in die Hilfsmatrix:

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

Nun berechnen wir die normalisierte Matrix, indem wir zuerst den Mittelwert abziehen: in0_m-=mm, dann durch die Standardabweichung dividieren: in0_m/=ms, und dann die Matrix in die Eingabematrix kopieren und den Datentyp in Float umwandeln: in_normf.Assign(in0_m).

Die zweite Eingabe:

„target_scale“ ist eigentlich der Skalierungsbereich der Zielvariablen, sein erster Wert ist eigentlich der Mittelwert der Zielvariablen: in1[0][0]=m[0], der zweite Wert ist die Varianz der Zielvariablen: in1[0][1]=s[0].


Ⅲ Lauf-Inferenz

Beim Ausführen der ONNX-Modellinferenz müssen alle in der Modellstruktur angezeigten Eingaben und Ausgaben definiert sein, keine darf fehlen, auch wenn einige nicht benötigte Eingaben als Parameter an die Funktion OnnxRun() übergeben werden müssen, dies ist sehr wichtig, sonst wird definitiv ein Fehler gemeldet.

   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. Ergebnisse der Inferenz

Wir gehen von einer einfachen Annahme aus: Wenn der Mittelwert des vorhergesagten Wertes größer ist als der Durchschnitt des höchsten und des niedrigsten Wertes des aktuellen Balkens, gehen wir davon aus, dass die Zukunft einen Aufwärtstrend aufweist, und setzen pre auf „buy“ (kaufen), andernfalls setzen wir pre auf „sell“ (verkaufen):

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

5. Logik der Auftragsabwicklung

Diesen Teil haben wir bereits im Artikel „Datenkennzeichnung für die Zeitreihenanalyse (Teil 5):Anwendung und Test in einem EA mit Socket“ ausführlich vorgestellt. In diesem Artikel werden wir keine detaillierte Einführung vornehmen, sondern lediglich die Hauptlogik in OnTick() kopieren und direkt verwenden. Es sollte beachtet werden, dass pre nach jeder Ausführung auf NULL gesetzt wird und wir während des Vorhersageprozesses diesen beiden Werten Werte zuweisen, was die Synchronisierung des Bestellvorgangs und des Vorhersageprozesses sicherstellt und nicht durch den vorherigen Vorhersagewert beeinflusst wird. Dieser Schritt ist sehr wichtig, da es sonst zu einer logischen Verwirrung kommt. Im Folgenden finden Sie den vollständigen Code für die Auftragsabwicklung:

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. Ressourcen recyceln

Wenn der EA läuft, müssen wir den Timer schließen und das Handle der ONNX-Modellinstanz freigeben, also müssen wir den folgenden Code zur Funktion OnDeinit(const int reason) hinzufügen:

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

Hier haben wir im Grunde den Code fertig geschrieben, und dann müssen wir den EA im Backtest laden und testen.

Anmerkung:

1. Bei der Einstellung der Ein- und Ausgabe des ONNX-Modells müssen Sie darauf achten, dass das Datenformat übereinstimmt.

2. Wir verwenden hier nur den ersten vorhergesagten Wert der Ausgabe, was nicht bedeutet, dass andere Ausgaben keinen Wert haben. Im Artikel „Datenkennzeichnung für die Zeitreihenanalyse (Teil 4):Deutung der Datenkennzeichnungen durch Aufgliederung“ dieser Serie haben wir die Interpretierbarkeit des NBeats-Modells vorgestellt, das mit anderen Outputs implementiert wird. Wir haben ihre Visualisierung mit Python bereits überprüft und werden die Visualisierungsfunktion in EA in diesem Artikel nicht hinzufügen. Interessierte Leser können versuchen, eine oder mehrere von ihnen zur Visualisierung in das Diagramm einzufügen.


Backtesting

Bevor wir mit dem Backtesting beginnen, ist eines zu beachten: Unser ONNX-Modell muss sich im gleichen Verzeichnis wie die Datei onnx.mq5 befinden, sonst wird die Modelldatei nicht geladen! Alles ist fertig, öffnen Sie nun den mql5-Editor, klicken Sie auf die Schaltfläche Kompilieren und erzeugen Sie die kompilierte Datei. Wenn die Kompilierung reibungslos verläuft, drücken Sie Strg+F5, um das Backtesting im Debug-Modus zu starten. Es öffnet sich ein neues Fenster, in dem der Testvorgang angezeigt wird. Mein Ausgabeprotokoll:

lg

Backtesting-Ergebnisse:

hc

Wir haben es geschafft!

Hier der vollständiger Code:

//+------------------------------------------------------------------+
//|                                                         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";
}


Zusammenfassung

Dieser Artikel wird voraussichtlich der letzte in dieser Reihe sein. In diesem Artikel haben wir den gesamten Prozess der Konvertierung eines Torch-Modells in ein ONNX-Modell im Detail vorgestellt, einschließlich der Frage, wie man die Eingabe und Ausgabe des Modells findet, wie man ihre Formate definiert, wie man sie mit dem Modell abgleicht, und einiger Datenverarbeitungstechniken. Die Schwierigkeit dieses Artikels liegt darin, wie man ein Modell mit komplexem Input und Output als ONNX-Modell exportiert. Wir hoffen, dass die Leser sich inspirieren lassen und davon profitieren können! Natürlich hat unser Test-EA noch viel Raum für Verbesserungen. So können Sie z. B. den Trend und die Saisonalität der NBeats-Modellausgabe im Diagramm visualisieren oder den Ausgabetrend nutzen, um die Auftragsrichtung zu beurteilen, usw.

Es gibt unzählige Möglichkeiten, solange Sie es tun. Das Beispiel in dem Artikel ist nur ein einfaches Beispiel, aber der Kerninhalt ist relativ vollständig. Sie können ihn frei erweitern und verwenden, aber bitte beachten Sie, dass Sie diesen EA nicht für den realen Handel zwanglos verwenden sollten! Diese Artikelserie bietet eine Vielzahl von relativ vollständigen Lösungen, die von der Erstellung von Datensätzen über das Training verschiedener Zeitreihenvorhersagemodelle bis hin zu deren Verwendung beim Backtesting reichen. Auch Anfänger können den gesamten Prozess Schritt für Schritt durchlaufen und in der Praxis anwenden, sodass diese Serie erfolgreich beendet werden kann!
Vielen Dank für die Lektüre, ich hoffe, Sie haben etwas gelernt, und ich wünsche Ihnen einen schönen Tag!





Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/13919

Beigefügte Dateien |
NBeats.onnx (6949.02 KB)
onnx.mq5 (11.99 KB)
n_beats.py (11.07 KB)
Neural networks made easy (Part 68): Offline Preference-guided Policy Optimization Neural networks made easy (Part 68): Offline Preference-guided Policy Optimization
Since the first articles devoted to reinforcement learning, we have in one way or another touched upon 2 problems: exploring the environment and determining the reward function. Recent articles have been devoted to the problem of exploration in offline learning. In this article, I would like to introduce you to an algorithm whose authors completely eliminated the reward function.
Data label for time series mining (Part 5):Apply and Test in EA Using Socket Data label for time series mining (Part 5):Apply and Test in EA Using Socket
This series of articles introduces several time series labeling methods, which can create data that meets most artificial intelligence models, and targeted data labeling according to needs can make the trained artificial intelligence model more in line with the expected design, improve the accuracy of our model, and even help the model make a qualitative leap!
Популяционные алгоритмы оптимизации: Гибридный алгоритм оптимизации бактериального поиска с генетическим алгоритмом (Bacterial Foraging Optimization - Genetic Algorithm, BFO-GA) Популяционные алгоритмы оптимизации: Гибридный алгоритм оптимизации бактериального поиска с генетическим алгоритмом (Bacterial Foraging Optimization - Genetic Algorithm, BFO-GA)
В статье представлен новый подход к решению оптимизационных задач, путём объединения идей алгоритмов оптимизации бактериального поиска пищи (BFO) и приёмов, используемых в генетическом алгоритме (GA), в гибридный алгоритм BFO-GA. Он использует роение бактерий для глобального поиска оптимального решения и генетические операторы для уточнения локальных оптимумов. В отличие от оригинального BFO бактерии теперь могут мутировать и наследовать гены.
Developing an MQL5 RL agent with RestAPI integration (Part 2): MQL5 functions for HTTP interaction with the tic-tac-toe game REST API Developing an MQL5 RL agent with RestAPI integration (Part 2): MQL5 functions for HTTP interaction with the tic-tac-toe game REST API
In this article we will talk about how MQL5 can interact with Python and FastAPI, using HTTP calls in MQL5 to interact with the tic-tac-toe game in Python. The article discusses the creation of an API using FastAPI for this integration and provides a test script in MQL5, highlighting the versatility of MQL5, the simplicity of Python, and the effectiveness of FastAPI in connecting different technologies to create innovative solutions.