English Русский Deutsch 日本語 Português
preview
Marcado de datos en el análisis de series temporales (Parte 6): Aplicación y prueba en EA utilizando ONNX

Marcado de datos en el análisis de series temporales (Parte 6): Aplicación y prueba en EA utilizando ONNX

MetaTrader 5Ejemplos | 23 julio 2024, 09:51
12 0
Yuqiang Pan
Yuqiang Pan

Introducción

En el artículo anterior explicamos cómo utilizar un socket (WebSocket) para comunicarse entre el EA y el servidor Python para resolver el problema de Backtesting, y también explicamos por qué adoptamos esta técnica. En este artículo, vamos a discutir cómo utilizar ONNX, que es soportado de forma nativa por MQL5, para realizar la inferencia con nuestro modelo, pero este método tiene algunas limitaciones. Si su modelo utiliza operadores que no están soportados por ONNX, puede terminar en fracaso, por lo que este método no es adecuado para todos los modelos (por supuesto, también puede añadir operadores para apoyar su modelo, pero requiere mucho tiempo y esfuerzo). Por eso dediqué mucho espacio en el artículo anterior para presentar el método de socket y recomendarlo.

Por supuesto, convertir un modelo general a formato ONNX es muy cómodo, y nos proporciona un soporte eficaz para operaciones multiplataforma. Este artículo se centra principalmente en algunas operaciones básicas para operar modelos ONNX en MQL5, incluyendo cómo ajustar la entrada y salida de modelos Torch y ONNX, así como cómo convertir formatos de datos adecuados para modelos ONNX. Por supuesto, también incluye la gestión de órdenes del EA. Se lo explicaré detalladamente. Empecemos ahora con el tema principal de este artículo.

Tabla de contenidos:


Estructura del directorio

Cuando realicemos la conversión del modelo, implicará la lectura de los archivos de modelo y configuración, pero vergonzosamente, me encontré con que no introduje la estructura de directorios del script en los artículos anteriores, lo que puede provocar que no encuentres la ubicación de tus archivos de modelo y configuración. Así que ordenamos la estructura de directorios de nuestro script aquí. Cuando utilizamos lightning-pytorch para entrenar el modelo, no definimos la ubicación de guardado del modelo en los callbacks (los callbacks responsables de gestionar el Checkpoint del modelo son la clase ModelCheckpoint), sólo definimos el nombre del modelo, por lo que el entrenador guardará el modelo en la ruta por defecto.
    ck_callback=ModelCheckpoint(monitor='val_loss',
                                mode="min",
                                save_top_k=1,  
                                filename='{epoch}-{val_loss:.2f}')

En este momento, el entrenador guardará el modelo en el directorio raíz, que puede ser un poco vago, por lo que utilizo algunas imágenes para ilustrar, esto le hará muy claro acerca de lo que los archivos se guardan durante el proceso de formación y donde están los archivos.

En primer lugar, nuestro modelo de guardar la ubicación, esta ruta contiene diferentes carpetas de versión, cada carpeta de versión contiene carpeta de puntos de control, archivo de eventos, archivo de parámetros, en la carpeta de puntos de control contiene el archivo de modelo que hemos guardado:

f3


Al entrenar el modelo utilizamos un modelo para encontrar la mejor tasa de aprendizaje, que se guardará en el directorio raíz de la carpeta:


f2

Al entrenar, guardaremos un archivo results.json para registrar la ruta del mejor modelo y la mejor puntuación, que se utilizará cuando carguemos el modelo, se guarda en el directorio raíz de la carpeta:

f4


Convertir el modelo Torch en un modelo ONNX

Seguimos utilizando el modelo NBeats como ejemplo. El siguiente código se añadirá principalmente en la parte de inferencia de Nbeats.py. Este script se creó cuando introduje el modelo NBeats en el artículo anterior. Debido a la naturaleza especial del modelo NBeats, puede resultar difícil exportar el modelo ONNX utilizando el método general. Es necesario depurar el proceso de inferencia del modelo y obtener de él la información pertinente para definir los parámetros necesarios para la exportación. Pero he hecho este proceso por usted, así que no se preocupe, sólo tienes que seguir los pasos en el artículo paso a paso, y todos los problemas se resolverán fácilmente.

1. Instalar las bibliotecas necesarias

Antes de convertir el modelo, hay que realizar otro paso importante, que es instalar las bibliotecas pertinentes de ONNX. Si sólo exporta el modelo, sólo necesita instalar la biblioteca ONNX: pip install ONNX. Pero como también necesitamos probar el modelo después de convertirlo, también necesitamos instalar la librería ONNXruntime. Esta biblioteca se divide en dos versiones: CPU runtime y GPU runtime. Si el modelo es grande y complejo, puede que necesite instalar la versión para GPU para acelerar el proceso de inferencia. Dado que nuestro modelo sólo necesita inferencia en CPU, el efecto de aceleración en GPU no es obvio, por lo que recomiendo instalar la versión para CPU: pip install ONNXruntime.


2. Obtener información de entrada

En primer lugar, debe cambiar el modelo del modo de entrenamiento al modo de inferencia: best_model.eval(). La razón para hacer esto es que el modo de entrenamiento y el modo de inferencia del modelo son diferentes, y sólo necesitamos el modo de inferencia del modelo, lo que reducirá la complejidad del modelo y sólo retendrá la entrada necesaria para la inferencia. Luego necesitamos crear un Dataloader después de cargar los datos para obtener los elementos de entrada completos, obtener un iterador de este objeto Dataloader, y luego llamar a la siguiente función para obtener el primer lote de datos. El primer elemento contiene toda la información de entrada que necesitamos. Durante el proceso de exportación del modelo, Torch seleccionará automáticamente los elementos de entrada necesarios para nosotros. Ahora utilizamos la función spilt_data() que ha sido definida antes para crear directamente un Dataloader después de cargar los datos: t_loader, v_loader, training = spilt_data(dt, t_shuffle=False, t_drop_last=True, v_shuffle=False, v_drop_last=True). Creamos un diccionario para almacenar las entradas requeridas para exportar el modelo: input_dict = {}. Obtenemos todos los objetos de entrada; aquí usamos v_loader para obtenerlos porque necesitamos el proceso de inferencia: items = next(iter(v_loader))[0]. Creamos una lista para almacenar todos los nombres de entrada: input_names = []. Luego iteramos a través de los elementos para obtener todas las entradas y nombres de entrada.

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

3. Obtener información de salida

Antes de obtener la salida, tenemos que ejecutar una inferencia primero, y luego obtener la información de salida que necesitamos del resultado de la inferencia. Este es el proceso de inferencia original:

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)

La información de inferencia está en la salida del objeto predicciones, iteramos a través de este objeto para obtener toda la información de salida, por lo que añadimos la siguiente sentencia aquí:
output_names=[]
for out in predictions.output._fields:
    output_names.append(out)

4. Exportar el modelo

En primer lugar, definimos la input_sample necesaria para exportar el modelo: input_1=(input_dict,{}) , no preguntes por qué, ¡hazlo! A continuación, utilizamos el método to_onnx() en la clase NBeats para exportar a ONNX, que también requiere un parámetro de ruta de archivo, exportamos directamente al directorio raíz, llamado "NBeats.onnx": best_model.to_onnx(file_path='NBeats.onnx', input_sample=input_1, input_names=input_names, output_names=output_names). Después de que el programa se ejecute hasta este punto, encontraremos el archivo "NBeats.onnx" en el directorio raíz de la carpeta actual:



Nota:

1. Si el nombre de entrada no está completa, el modelo exportado lo nombrará automáticamente durante la exportación, lo cual puede causar confusión y no sabremos cuál es el verdadero nombre de entrada. Por eso elegimos ingresar todos los nombres a la función de exportación para asegurar la consistencia de los nombres de entrada del modelo exportado.

2. En el Dataloader, los datos de entrada incluyen "encoder_cat", "encoder_cont" y otras entradas múltiples generadas por el codificador y el decodificador, mientras que en el proceso de inferencia sólo necesitamos "encoder_cont" y "target_scale" dos. Así que no pienses que el paso de cotejar los datos de entrada es redundante, en algunos modelos que requieren codificadores y decodificadores, este paso es necesario. 3. La configuración del entorno utilizada por el autor durante el proceso de prueba: python-3.10;ONNX versión-8; pytorch-2.1.1;operators-17.



Probando el modelo convertido

En la parte anterior, hemos exportado con éxito Torch como modelo ONNX. La siguiente tarea importante es probar este modelo y ver si el resultado de este modelo es el mismo que el del modelo original. Esto es muy importante, porque durante el proceso de exportación, algunos operadores pueden tener desviaciones debidas a la versión Torch y a problemas de compatibilidad del núcleo de ejecución (kernel) de ONNX. En este caso, puede ser necesaria la intervención manual al exportar el modelo.
  • En primer lugar, importa la biblioteca de ejecución de ONNX: import ONNXruntime as ort.
  • Cargar el fichero modelo "NBeats.onnx": sess = ort.InferenceSession("NBeats.onnx").
  • Obtenga los nombres de entrada del modelo ONNX iterando sobre el valor de retorno de sess.get_inputs(), que se utilizan para emparejar los datos de entrada: input_names = [input.name for input in sess.get_inputs()].
  • No necesitamos comparar todas las salidas, así que sólo obtenemos el primer elemento de la salida para comparar y ver si los resultados son los mismos: nombre_salida = sess.get_outputs()[0].nombre.
  • Para comparar si los resultados son los mismos, la entrada debe ser la misma, por lo que la entrada del modelo debe ser coherente con los datos utilizados para la inferencia. Pero primero tenemos que convertirlo al formato Dataloader y utilizar input_names para que coincida con los datos de entrada, porque no todas las entradas se cargarán durante el proceso de inferencia. En primer lugar, cargue los datos de entrada como datos de series temporales utilizando el método from_parameters() de la clase TimeSeriesDataSet: input_ds = New_TmSrDt.from_parameters(best_model.dataset_parameters, input_,predict=True). A continuación, conviértalo a tipo Dataloader mediante el método de clase to_dataloader(): input_dl = input_ds.to_dataloader(train=False, batch_size=1, num_workers=0).
  • Haga coincidir los datos de entrada. Primero, necesitamos obtener un lote de datos y sacar el primer elemento: input_dict = next(iter(input_dl))[0]. A continuación, utilice input_names para hacer coincidir los datos de entrada requeridos por la entrada: input_data = [input_dict[nombre].numpy() para nombre en input_names].
  • Ejecuta la inferencia: pred_onnx = sess.run([output_name], dict(zip(input_names, input_data)))[0].
  • Imprima el resultado de la inferencia de Torch y el resultado de la inferencia de ONNX y compárelos.
Ahora, imprime el resultado de la inferencia de 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]])

Imprime el resultado de la inferencia 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 ]]

Podemos ver que los resultados de la inferencia de nuestro modelo son los mismos. El siguiente paso es configurar el modelo exportado a MQL5. Como se muestra en la figura:

f6


Código completo:
# 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()


Crear un EA con el modelo ONNX

Hemos completado la conversión del modelo y las pruebas, y ahora crearemos un archivo experto llamado onnx.mq5. En el EA planeamos utilizar OnTimer() para gestionar la lógica de inferencia del modelo, y utilizar OnTick() para gestionar la lógica de orden, de forma que podamos establecer cada cuánto tiempo ejecutar la inferencia, en lugar de ejecutar la inferencia cada vez que llega una cotización, lo que provocaría una grave ocupación de recursos. De manera similar, en este EA no proporcionaremos una lógica de trading compleja, solo ofreceremos un ejemplo de demostración. ¡Por favor, no utilice este EA directamente para operar!

1. Ver la estructura del modelo ONNX

Este paso es muy importante, necesitamos definir la entrada y salida para el modelo ONNX en el EA, por lo que necesitamos ver la estructura del modelo, para determinar el número, tipo de datos y dimensión de los datos de entrada y salida. Para ver el modelo ONNX, puedes abrirlo directamente en el editor MQL5 y verás la estructura del modelo. También te dará los estilos de entrada y salida, pero no es editable. También podemos utilizar las herramientas Netron o WinML Dashboard, la herramienta que utilizamos en este artículo es Netron.

Encontramos nuestro archivo modelo "NBeats.onnx" en el IDE MQL5 y lo abrimos directamente, en la posición de anotación de abajo puedes encontrar la opción "Abrir en Netron", pulsa el botón y el archivo modelo se abrirá automáticamente.

o0

O haz clic con el botón derecho del ratón sobre nuestro archivo modelo en el explorador de archivos del IDE y verás la opción "Abrir en Netron".

o1

Si no dispone de la herramienta Netron, el IDE le guiará para instalarla.

El modelo tiene este aspecto después de abrirlo:

md

Puedes ver que toda la interfaz es muy sencilla y refrescante, y la función es muy potente. Incluso podemos utilizarlo para editar los nodos del modelo. Volviendo al tema, hacemos clic en el primer nodo, y Netron nos mostrará la información relevante del modelo:

inf

Puedes ver que el formato del modelo NBeats exportado es: ONNX v8 y la versión de PyTorch es la 2.1.1. La herramienta de exportación es: ai.ONNX v17.

Hay dos entradas, la primera es: encoder_cont, la dimensión es: [1,96,1], el formato de datos es: float32; la segunda es: target_scale, la dimensión es: [1,2], el formato de datos es: float32.

Hay cinco salidas: la primera es: predicción, con dimensiones [1,20]; la segunda es: backcast, con dimensiones [1,96]; y las otras tres salidas interpretables son: tendencia, estacionalidad y dimensión genérica, todas con dimensiones [1,116]. Todos los formatos de datos de salida son float32.


2. Definir la entrada y la salida del modelo

Ya conocemos el formato de entrada y salida del modelo, y los formatos de entrada y salida soportados por ONNX en MQL5 son arrays, matrices y vectores. Ahora vamos a definirlos en el EA. Primero, define la entrada en OnTimer(), ambos son arrays:

  • La primera entrada: matrixf in_normf;
  • La segunda entrada: float in1[1][2];

Debido a que necesitamos llamar a los resultados de salida del modelo en OnTick(), no es razonable definir la salida del modelo en OnTimer(), y necesitan ser definidos como variables globales. Los resultados de la inferencia del modelo y el controlador de carga del modelo también deben definirse como variables globales:

  • Manejo del modelo: long handle;
  • El primer resultado de la inferencia: vectorf y=vector<float>::Zeros(20);
  • El segundo resultado de la inferencia: vectorf backcast=vector<float>::Zeros(96);
  • El tercer resultado de la inferencia: vectorf trend=vector<float>::Zeros(116);
  • El cuarto resultado de la inferencia: vectorf seasonality=vector<float>::Zeros(116);
  • El quinto resultado de la inferencia: vectorf generic=vector<float>::Zeros(116);
  • Definir el resultado de la predicción: cadena pre=NULL;

3. Definir la lógica de inferencia

Ⅰ Inicialización

En primer lugar, importa el modelo ONNX como recurso externo en el EA: #resource “NBeats.onnx” as uchar ExtModel[]. Inicialice el temporizador en la función OnInit(): EventSetTimer(300), este valor puede ser fijado por usted mismo. Cargar el modelo y obtener el handle del modelo: handle=OnnxCreateFromBuffer(ExtModel,ONNX_DEBUG_LOGS). Si quieres ver la información de entrada o salida del modelo, puedes añadir la siguiente sentencia:

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

Ⅱ Tratamiento de datos

Ya hemos definido antes la entrada y la salida del modelo, y a continuación necesitamos conocer la definición específica de estas variables, qué tipo de datos son. Para ello, debemos encontrar sus definiciones en el archivo timeseries.py de la biblioteca pytorch_forecasting. Este artículo no explicará este archivo en detalle, vamos a revelar la respuesta directamente.

La primera entrada:

"encoder_cont" es en realidad el valor normalizado de la variable de destino, por supuesto pytorch_forecasting proporciona diferentes métodos EncoderNormalizer, GroupNormalizer, MultiNormalizer, NaNLabelEncoder, TorchNormalizer, estos métodos pueden ser difíciles de implementar en MQL5, por lo que en este artículo utilizamos directamente el método normalize ordinario. Primero define un MqlRates vacío: MqlRates rates[], y luego úsalo para copiar las últimas 96 barras de valores de cierre: if(!CopyRates(_Symbol,_Period,0,96,rates)) return, si la copia falla, vuelve directamente. También tenemos que definir una matriz para recibir este valor, que se utiliza para calcular la media y la varianza: matriz in0_m(96,1). Copiar el valor de cierre de esta tasa en la matriz in0_m: for(int i=0; i<96; i++) in0_m[i][0]= tasas[i].cierre. Calcular la media: vector m=in0_m.Mean(0); calcular la varianza: vector s=in0_m.Std(0). Crear una matriz mm para almacenar la media: matriz mm(96,1); crear una matriz ms para almacenar la varianza: matriz ms(96,1). Copia la media y la varianza en la matriz auxiliar:

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

Ahora calculamos la matriz normalizada, primero restamos la media: in0_m-=mm, luego dividimos por la desviación estándar: in0_m/=ms, y luego copiamos la matriz a la matriz de entrada y convertimos el tipo de datos a float: in_normf.Assign(in0_m)

La segunda entrada:

"target_scale" es en realidad el rango de escala de la variable objetivo, su primer valor es en realidad la media de la variable objetivo: in1[0][0]=m[0], el segundo dato es la varianza de la variable objetivo: in1[0][1]=s[0]


Ⅲ Ejecutar Inferencia

Al ejecutar la inferencia del modelo ONNX, las entradas y salidas que aparecen en la estructura del modelo deben estar todas definidas, no puede faltar ninguna, aunque algunas entradas que no necesites también deben pasarse como parámetros a la función OnnxRun(), esto es muy importante, de lo contrario informará definitivamente de un error.

   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. Resultados de la inferencia

Hacemos una suposición simple: si la media del valor pronosticado es mayor que la media de los valores más alto y más bajo de la barra actual, suponemos que el futuro será una tendencia alcista, y establecemos "pre" en "comprar", de lo contrario establecemos "pre" en "vender":

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

5. Lógica de procesamiento de pedidos

Esta parte ya la hemos introducido en detalle en el artículo Marcado de datos en el análisis de series temporales (Parte 5): Aplicación y comprobación de asesores usando Socket, este artículo no hará una introducción detallada, solo necesitamos copiar la lógica principal a OnTick() y usarla directamente. Hay que tener en cuenta que después de cada ejecución, "pre" se pone a NULL, y durante el proceso de predicción asignaremos valores a estos dos valores, lo que asegura la sincronización del proceso de operación de la orden y el proceso de predicción, y no se verá afectado por el valor de predicción anterior. Este paso es muy importante, de lo contrario causará cierta confusión lógica, el siguiente es el código completo de procesamiento de pedidos:

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. Reciclar recursos

Cuando el EA se ejecuta, necesitamos cerrar el temporizador y liberar el "handle" de la instancia del modelo ONNX, por lo que necesitamos añadir el siguiente código a la función OnDeinit(const int reason):

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

Aquí básicamente hemos terminado de escribir el código, y luego tenemos que cargar y probar el EA en el probador de estrategias.

Nota:

1. Cuando configure la entrada y la salida del modelo ONNX, deberá prestar atención a la coincidencia del formato de los datos.

2. Aquí sólo utilizamos el primer valor previsto de la salida, lo que no significa que las demás salidas no tengan valor. En el artículo "Marcado de datos en el análisis de series temporales (Parte 4): Descomposición de la interpretabilidad usando el marcado de datos" de esta serie, introdujimos la interpretabilidad del modelo NBeats, que se implementa utilizando otras salidas. Ya hemos comprobado su visualización con Python, y no añadiremos la función de visualización en EA en este artículo. Los lectores interesados pueden intentar añadir uno o varios de ellos al gráfico para visualizarlos.


Prueba retrospectiva (Backtesting)

Antes de iniciar el backtesting hay que tener en cuenta una cosa: nuestro modelo ONNX debe colocarse en el mismo directorio que el archivo onnx.mq5, ¡de lo contrario fallará al cargar el archivo del modelo! Todo está listo, ahora abre el editor MQL5, pulsa el botón compilar, y genera el archivo compilado. Si compila sin problemas, pulse Ctrl+F5 para iniciar el backtesting en modo depuración. Se abrirá una nueva ventana para mostrar el proceso de prueba. Mi registro de salida:

lg

Resultados de las pruebas retrospectivas:

hc

Lo hicimos!

Código completo:

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


Resumen

Se espera que este artículo sea el último de esta serie. En este artículo, hemos presentado en detalle todo el proceso de conversión de un modelo Torch a un modelo ONNX, incluyendo cómo encontrar la entrada y la salida del modelo, cómo definir sus formatos, cómo emparejarlos con el modelo y algunas técnicas de procesamiento de datos. La dificultad de este artículo radica en cómo exportar un modelo con entradas y salidas complejas como modelo ONNX. Esperamos que los lectores puedan inspirarse y sacar provecho de ella. Por supuesto, nuestro EA de pruebas aún tiene mucho margen de mejora. Por ejemplo, puede visualizar la tendencia y la estacionalidad de la salida del modelo NBeats en el gráfico, o utilizar la tendencia de salida para juzgar la dirección del pedido, etc.

Hay innumerables posibilidades siempre que lo hagas. El ejemplo del artículo es sólo el más sencillo, pero el contenido básico es relativamente completo. Puede ampliarlo y utilizarlo libremente, pero tenga en cuenta que no debe utilizar este EA para operaciones reales de forma casual. Esta serie de artículos ofrece soluciones variadas y relativamente completas, desde la creación de conjuntos de datos hasta el entrenamiento de distintos modelos de predicción de series temporales y, a continuación, cómo utilizarlos en backtesting. Incluso los principiantes pueden completar todo el proceso paso a paso y aplicarlo en la práctica, por lo que esta serie puede finalizar con éxito.
Gracias por leer, espero que hayas aprendido algo. ¡Que tengas un buen día!




Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/13919

Archivos adjuntos |
NBeats.onnx (6949.02 KB)
onnx.mq5 (11.99 KB)
n_beats.py (11.07 KB)
Características del Wizard MQL5 que debe conocer (Parte 11): Muros numéricos Características del Wizard MQL5 que debe conocer (Parte 11): Muros numéricos
Los muros numéricos (Number Walls) son una variante de los registros de desplazamiento lineal hacia atrás (Linear Shift Back Registers) que pre-evalúan las secuencias para su predictibilidad mediante la comprobación de la convergencia. Veamos cómo se pueden utilizar estas ideas en MQL5.
Técnicas del Asistente MQL5 (MQL5 Wizard) que debe conocer (Parte 10). El RBM no convencional Técnicas del Asistente MQL5 (MQL5 Wizard) que debe conocer (Parte 10). El RBM no convencional
Las máquinas de Boltzmann restringidas (RBM, Restrictive Boltzmann Machines) son, en el nivel básico, una red neuronal de dos capas que es competente en la clasificación no supervisada a través de la reducción de la dimensionalidad. Tomamos sus principios básicos y examinamos si lo rediseñamos y entrenamos de forma poco ortodoxa, podríamos obtener un filtro de señal útil.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Comercio algorítmico con MetaTrader 5 y R para principiantes Comercio algorítmico con MetaTrader 5 y R para principiantes
Embárquese en una apasionante exploración en la que el análisis financiero se encuentra con el trading algorítmico mientras desentrañamos el arte de unir a la perfección R y MetaTrader 5. Este artículo es su guía para unir los reinos de la finura analítica en R con las formidables capacidades comerciales de MetaTrader 5.