English Русский Deutsch 日本語
preview
Anotação de dados na análise de série temporal (Parte 6): Aplicação e teste de EA com ONNX

Anotação de dados na análise de série temporal (Parte 6): Aplicação e teste de EA com ONNX

MetaTrader 5Exemplos | 25 junho 2024, 10:00
30 0
Yuqiang Pan
Yuqiang Pan

Introdução

No artigo anterior, discutimos como usar WebSocket para a comunicação entre o EA e o servidor Python para resolver o problema de teste histórico, e também discutimos por que adotamos esse método. Neste artigo, discutiremos como usar o ONNX, que é suportado nativamente pelo MQL5, para realizar a inferência do nosso modelo. Também abordaremos as limitações do método. Se o seu modelo usar operadores não suportados pelo ONNX, a inferência pode falhar, então esse método não é adequado para todos os modelos (claro, você também pode adicionar operadores para suportar seu modelo, mas isso requer muito tempo e esforço). É por isso que, no artigo anterior, dediquei bastante espaço para apresentar o método de sockets.

Claro, converter um modelo comum para o formato ONNX é muito conveniente e nos dá suporte eficiente para operações multiplataforma. O artigo aborda principalmente algumas operações básicas de trabalho com modelos ONNX no MQL5, incluindo maneiras de mapear os dados de entrada e saída dos modelos torch e ONNX, bem como formas de converter formatos de dados adequados para modelos ONNX. Claro, falaremos também sobre o gerenciamento de ordens do EA.

Conteúdo:


Estrutura de diretórios

Ao converter o modelo, lemos os arquivos de modelo e configuração. Para minha vergonha, percebi que não apresentei a estrutura de diretórios do script em artigos anteriores, o que pode dificultar encontrar a localização do seu modelo e arquivos de configuração. Portanto, aqui abordamos a estrutura de diretórios do nosso script. Ao usar o lightning-pytorch para treinar o modelo, não definimos o local de salvamento do modelo nos callbacks (callbacks responsáveis pelo gerenciamento do checkpoint do modelo — essa classe é a ModelCheckpoint), apenas definimos o nome do modelo, então o treinador salvará o modelo no caminho padrão.
    ck_callback=ModelCheckpoint(monitor='val_loss',
                                mode="min",
                                save_top_k=1,  
                                filename='{epoch}-{val_loss:.2f}')

Nesse momento, o treinador salvará o modelo no diretório raiz. Isso pode soar um pouco vago, então uso algumas imagens para ilustrar. Isso ajudará você a entender quais arquivos são salvos durante o treinamento e onde eles estão.

Primeiramente, o local de salvamento do nosso modelo. Esse caminho contém diferentes pastas de versões, cada pasta contém uma pasta de checkpoints, um arquivo de eventos e um arquivo de parâmetros. Na pasta de checkpoints está o arquivo de modelo que salvamos:

f3


Durante o treinamento do modelo, usamos o modelo para encontrar a melhor taxa de aprendizado, que será salva no diretório raiz da pasta:


f2

Ao treinar, salvamos o arquivo results.json para registrar o melhor caminho do modelo e o melhor resultado, que será usado ao carregar o modelo. Ele é salvo no diretório raiz da pasta:

f4


Conversão do modelo torch para o modelo ONNX

Ainda usamos o modelo NBeats como exemplo. O código a seguir será adicionado principalmente na parte de saída do Nbeats.py. Este script foi criado quando apresentei o modelo NBeats no artigo anterior. Devido à natureza específica do modelo NBeats, a exportação do modelo ONNX usando o método comum pode ser difícil. Precisamos depurar o processo de saída do modelo e depois obter as informações correspondentes para determinar os parâmetros necessários para a exportação. Mas eu fiz isso por você, então basta seguir as instruções no artigo passo a passo, e todos os problemas serão facilmente resolvidos.

1. Instalar as bibliotecas necessárias

Antes de converter o modelo, precisamos dar um passo importante — instalar as bibliotecas ONNX correspondentes. Se apenas exportarmos o modelo, precisaremos instalar apenas a biblioteca ONNX: pip install onnx. Mas como também precisamos testar o modelo após sua conversão, também precisamos instalar a biblioteca onnxruntime. A biblioteca é dividida em duas versões: ambiente de execução para CPU e ambiente de execução para GPU. Se o modelo for grande e complexo, pode ser necessário instalar a versão para GPU para acelerar o processo de inferência. Como nosso modelo só precisa de inferência na CPU, o efeito de aceleração da GPU não é evidente, então recomendo instalar a versão para CPU: pip install onnxruntime.


2. Obter dados de entrada

Primeiro, você precisa alternar o modelo do modo de treinamento para o modo de inferência: best_model.eval(). A razão para isso é que o modo de treinamento do modelo e o modo de inferência são diferentes, e só precisamos do modo de inferência, o que reduzirá a complexidade do modelo e manterá apenas os dados de entrada necessários para a inferência. Em seguida, precisamos criar um DataLoader após carregar os dados para obter todos os elementos de entrada, bem como um iterador desse objeto DataLoader, e depois chamar a função next para obter o primeiro lote de dados. O primeiro elemento contém todas as informações de entrada necessárias. Ao exportar o modelo, o torch selecionará automaticamente os elementos de entrada necessários para nós. Agora usamos a função spilt_data() definida anteriormente para criar diretamente o DataLoader após carregar os dados: t_loader, v_loader, training = spilt_data(dt, t_shuffle=False, t_drop_last=True, v_shuffle=False, v_drop_last=True) Criamos um dicionário para armazenar os dados de entrada necessários para exportar o modelo: input_dict = {} Obtivemos todos os objetos de entrada, aqui usamos v_loader para obtê-los, pois precisamos do processo de inferência: items = next(iter(v_loader))[0] Criamos uma lista para armazenar todos os nomes de entrada: input_names=[] Depois, iteramos pelos itens para obter todos os dados de entrada e seus nomes:

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

3. Obter dados de saída

Antes de obter os dados de saída, precisamos primeiro realizar a inferência e, em seguida, obter as informações de saída necessárias a partir do resultado da inferência. Este é o processo de inferência 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)

A informação de saída está nos dados de saída do objeto de previsões. Iteramos sobre este objeto para obter todas as informações de saída, então adicionamos a seguinte operação:
output_names=[]
for out in predictions.output._fields:
    output_names.append(out)

4. Exportar o modelo

Primeiro, definimos input_sample, necessário para exportar o modelo: input_1=(input_dict,{}). Não pergunte por que, apenas faça isso! Em seguida, usamos o método to_onnx() da classe NBeats para exportar para ONNX, que também requer o parâmetro do caminho do arquivo. Exportamos diretamente para o diretório raiz com o nome "NBeats.onnx": best_model.to_onnx(file_path='NBeats.onnx', input_sample=input_1, input_names=input_names, output_names=output_names). Após a execução do programa até este ponto, encontraremos o arquivo "NBeats.onnx" no diretório raiz da pasta atual:



Notas:

1. Se o nome de entrada não for completo, a exportação do modelo atribuirá automaticamente um nome a ele durante a exportação, o que pode causar alguma confusão. Não saberemos qual é o correto. Portanto, decidimos inserir todos os nomes na função de exportação para garantir a consistência dos nomes de entrada do modelo exportado.

2. No DataLoader, os dados de entrada incluem encoder_cat, encoder_cont e outros, gerados pelo codificador e decodificador, enquanto no processo de inferência precisamos apenas de dois - encoder_cont e target_scale. Portanto, não pense que o passo de mapeamento dos dados de entrada é desnecessário. Em alguns modelos que exigem codificadores e decodificadores, esse passo é necessário. 3. Configuração do ambiente utilizada pelo autor durante o teste: python-3.10; ONNX version-8; pytorch-2.1.1; operadores-17.



Teste do modelo convertido

Na seção anterior, exportamos com sucesso o modelo torch para o modelo ONNX. A próxima tarefa importante é testar esse modelo e verificar se os dados de saída desse modelo correspondem ao modelo original. Isso é muito importante, pois no processo de exportação alguns operadores podem ter desvios devido à versão do torch e problemas de compatibilidade do kernel ONNX. Nesse caso, pode ser necessário intervenção manual ao exportar o modelo.
  • Primeiro, importamos a biblioteca de tempo de execução ONNX: import onnxruntime as ort.
  • Carregue o arquivo do modelo "NBeats.onnx": sess = ort.InferenceSession('NBeats.onnx').
  • Obtenha os nomes de entrada do modelo ONNX, iterando sobre o valor retornado sess.get_inputs(), que é usado para mapear os dados de entrada: input_names = [input.name for input in sess.get_inputs()].
  • Não precisamos comparar todos os dados de saída, então obtemos apenas o primeiro elemento dos dados de saída para comparação e verificação de correspondência: output_name = sess.get_outputs()[0].name.
  • Para comparar se os resultados são iguais, os dados de entrada devem ser iguais, então os dados de entrada do modelo devem corresponder aos dados usados para a inferência. No entanto, precisamos primeiro convertê-los para o formato do DataLoader e usar input_names para corresponder aos dados de entrada, pois nem todos os dados de entrada serão carregados no processo de inferência. Primeiro, carregamos os dados de entrada como dados de séries temporais, usando o método from_parameters() da classe TimeSeriesDataSet: input_ds = New_TmSrDt.from_parameters(best_model.dataset_parameters, input_, predict=True). Em seguida, convertemos para o tipo DataLoader usando o método da classe to_dataloader(): input_dl = input_ds.to_dataloader(train=False, batch_size=1, num_workers=0).
  • Mapeamos os dados de entrada. Primeiro, precisamos obter um lote de dados e extrair o primeiro elemento: input_dict = next(iter(input_dl))[0]. Em seguida, usamos input_names para mapear os dados de entrada necessários para a inferência: input_data = [input_dict[name].numpy() for name in input_names].
  • Executamos a inferência: pred_onnx = sess.run([output_name], dict(zip(input_names, input_data)))[0].
  • Imprimimos o resultado da inferência do torch e do onnx e comparamos.
Imprimimos o resultado da inferência do 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]])

Imprimimos o resultado da inferência do 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 os resultados da inferência do nosso modelo não diferem. O próximo passo é configurar o modelo exportado no MQL5.

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


Criação do EA com o modelo ONNX

Completamos a conversão e o teste do modelo e agora criaremos o arquivo EA com o nome onnx.mq5. No EA, planejamos usar OnTimer() para gerenciar a lógica de inferência do modelo e usar OnTick() para gerenciar a lógica de ordens, para que possamos definir a frequência de execução da inferência, em vez de executar a inferência cada vez que chega uma cotação, o que resultaria em um grande aumento no consumo de recursos. Da mesma forma, neste EA, não forneceremos uma lógica de negociação complexa, mas apenas um exemplo demonstrativo. Por favor, não use este EA diretamente para negociação!

1. Visualização da estrutura do modelo ONNX

Este passo é muito importante: precisamos definir os dados de entrada e saída do modelo ONNX no EA, então precisamos visualizar a estrutura do modelo para determinar a quantidade, tipo de dados e dimensionalidade dos dados de entrada e saída. Para visualizar o modelo ONNX, você pode abri-lo diretamente no editor MQL5 e ver a estrutura do modelo. Você também verá os estilos de entrada e saída, mas eles não podem ser editados. Também podemos usar ferramentas como Netron ou WinML Dashboard. Neste artigo, usamos a ferramenta Netron.

Encontramos nosso arquivo de modelo NBeats.onnx no MetaEditor e o abrimos diretamente. Na captura de tela abaixo, você pode encontrar a opção "Open in Netron" (abrir no Netron). Clique no botão, e o arquivo do modelo será aberto automaticamente.

o0

Ou clique com o botão direito do mouse no arquivo do nosso modelo no explorador de arquivos do MetaEditor, e você verá a opção Open in Netron.

o1

Se você não tiver o Netron, o ambiente de desenvolvimento ajudará você a instalá-lo.

Após a abertura, o modelo se parece com isto:

md

Você pode ver que toda a interface é muito simples e conveniente, e a funcionalidade é muito poderosa. Podemos até usá-la para editar os nós do modelo. Mas voltando ao tema - clique no primeiro nó, e o Netron nos mostrará as informações correspondentes do modelo:

inf

Formato do modelo exportado NBeats: ONNX v8, versão do pytorch: pytorch 2.1.1, ferramenta de exportação: ai.onnx v17.

Existem duas entradas, a primeira: encoder_cont, dimensionalidade: [1,96,1], formato de dados: float32; a segunda: target_scale, dimensionalidade: [1,2], formato de dados: float32.

Existem cinco saídas, a primeira: prediction, dimensionalidade: [1,20]; a segunda: backcast, dimensionalidade: [1,96]; as outras três são saídas interpretáveis: trend, seasonality, generic dimension: [1,116]. Todos os formatos de saída são float32.


2. Definir dados de entrada e saída do modelo

Já conhecemos o formato de entrada e saída do modelo. Os formatos de entrada e saída suportados pelo ONNX no MQL5 são arrays, matrizes e vetores. Agora vamos defini-los no EA. Primeiro, definimos os dados de entrada em OnTimer(). Ambos são arrays:

  • Primeiro: matrixf in_normf;
  • Segundo: float in1[1][2];

Como precisamos chamar os resultados da inferência do modelo em OnTick(), não é razoável definir os dados de saída do modelo em OnTimer(). Eles precisam ser definidos como variáveis globais. Os resultados da inferência do modelo e o descritor de carga do modelo também precisam ser definidos como variáveis globais:

  • Handle do modelo: long handle;
  • Primeiro resultado da inferência: vectorf y=vector<float>::Zeros(20);
  • Segundo resultado da inferência: vectorf backcast=vector<float>::Zeros(96);
  • Terceiro resultado da inferência: vectorf trend=vector<float>::Zeros(116);
  • Quarto resultado da inferência: vectorf seasonality=vector<float>::Zeros(116);
  • Quinto resultado da inferência: vectorf generic=vector<float>::Zeros(116);
  • Definir o resultado da previsão: string pre=NULL;

3. Definição da lógica de inferência

Ⅰ Inicialização

Primeiro, importamos o modelo ONNX como um recurso externo no EA: #resource "NBeats.onnx" as uchar ExtModel[]. Inicializamos o timer na função OnInit(): EventSetTimer(300), esse valor pode ser configurado conforme desejado. Carregamos o modelo e obtemos o descritor do modelo: handle=OnnxCreateFromBuffer(ExtModel,ONNX_DEBUG_LOGS). Se você quiser visualizar as informações de entrada ou saída do modelo, você pode adicionar a seguinte operação:

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

Ⅱ Processamento de dados

Anteriormente, já definimos os dados de entrada e saída do modelo, e agora precisamos conhecer a definição específica dessas variáveis e seu tipo de dados. Para isso, precisamos encontrar suas definições no arquivo timeseries.py da biblioteca pytorch_forecasting. Este artigo não explicará este arquivo em detalhes, vamos direto à resposta.

Primeiro parâmetro de entrada:

encoder_cont é, na verdade, o valor normalizado da variável alvo. Claro, o pytorch_forecasting fornece vários métodos como EncoderNormalizer, GroupNormalizer, MultiNormalizer, NaNLabelEncoder e TorchNormalizer. Esses métodos podem ser difíceis de implementar no MQL5, então neste artigo usamos diretamente o método de normalização comum. Primeiro, definimos um MqlRates vazio: MqlRates rates[], e depois usamos isso para copiar os valores de fechamento dos últimos 96 barras: if(!CopyRates(_Symbol, _Period, 0, 96, rates)) return, se a cópia falhar, retornamos diretamente. Também precisamos definir uma matriz para obter esse valor, que é usado para calcular a média e a variância: matrix in0_m(96,1). Copiamos o valor de fechamento para a matriz: for(int i=0; i<96; i++) in0_m[i][0]= rates[i].close. Calculamos a média: vector m=in0_m.Mean(0); calculamos a variância: vector s=in0_m.Std(0). Criamos uma matriz mm para armazenar a média: matrix mm(96,1); criamos uma matriz ms para armazenar a variância: matrix ms(96,1). Copiamos a média e a variância para a matriz auxiliar:

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

Agora calculamos a matriz normalizada. Primeiro subtraímos a média: in0_m-=mm, depois dividimos pelo desvio padrão: in0_m/=ms e copiamos a matriz para a matriz de entrada e convertemos o tipo de dados para float: in_normf.Assign(in0_m)

Segundo parâmetro de entrada:

"target_scale" é, na verdade, o intervalo de escala da variável alvo. O primeiro valor é a média da variável alvo: in1[0][0]=m[0], o segundo valor é a variância da variável alvo: in1[0][1]=s[0]


Ⅲ Inferência

Ao realizar a inferência do modelo ONNX, todos os dados de entrada e saída, exibidos na estrutura do modelo, devem ser definidos. Mesmo os dados de entrada que você não precisa também devem ser passados como parâmetros para a função OnnxRun(). Caso contrário, um erro será inevitável.

   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 da inferência

Fazemos uma suposição simples: se o valor médio do valor previsto for maior que a média dos valores mais alto e mais baixo da barra atual, supomos que haverá uma tendência de alta no futuro e definimos pre para buy (compra), caso contrário, para sell (venda):

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

5. Lógica de processamento de ordens

Esta parte já foi detalhadamente discutida no artigo "Anotação de dados na análise de série temporal (Parte 5):Aplicação e teste de EA com Socket". Neste artigo, apenas copiamos a lógica principal para OnTick() e a usamos diretamente. Vale notar que após cada execução, pre é definido como NULL, e durante o processo de previsão, atribuímos valores a essas duas variáveis, garantindo a sincronização dos processos de processamento de ordens e previsão e não dependendo do valor de previsão anterior. Este passo é muito importante. Sua ausência causará alguma confusão lógica. Abaixo está o código completo de processamento de ordens:

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. Retorno de recursos

Ao trabalhar com o EA, precisamos fechar o timer e liberar o handle da instância do modelo ONNX, então precisamos adicionar o seguinte código na função OnDeinit(const int Reason):

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

Concluímos praticamente o código. Agora precisamos carregar e testar o EA com dados históricos.

Notas:

1. Ao configurar os dados de entrada e saída do modelo ONNX, é necessário prestar atenção à correspondência do formato dos dados.

2. Aqui, usamos apenas o primeiro valor previsto dos dados de saída, o que não significa que os outros dados de saída não sejam importantes. No artigo "Anotação de dados na análise de série temporal (Parte 4): Decomposição da interpretabilidade usando anotação de dados", apresentamos a interpretabilidade do modelo NBeats, que é realizada usando outros dados de saída. Já verificamos a visualização deles no Python, e nesta série não adicionaremos a função de visualização no EA. Os leitores interessados podem tentar adicionar um ou mais deles ao gráfico para visualização.


Teste histórico

Antes de iniciar o teste histórico, vale ressaltar uma coisa: nosso modelo ONNX deve estar no mesmo diretório que o arquivo onnx.mq5, caso contrário, o arquivo do modelo não será carregado! Tudo pronto, agora abra o editor MQL5, clique no botão "Compilar" e gere o arquivo compilado. Se a compilação for bem-sucedida, pressione Ctrl+F5 para iniciar o teste no modo de depuração. Uma nova janela será aberta, mostrando o processo de teste. É assim que seu log se parecerá:

lg

Resultados do teste histórico:

hc

Conseguimos!

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


Características

Esta é a última parte da série. Nesta série, apresentamos detalhadamente todo o processo de conversão de um modelo torch para um modelo ONNX, incluindo como encontrar os dados de entrada e saída do modelo, como definir seus formatos, como mapeá-los para o modelo, além dos métodos de processamento de dados. A complexidade deste artigo reside em como exportar um modelo com entrada e saída complexas como um modelo ONNX. Espero que o artigo inspire novas descobertas nos leitores. Claro, nosso EA de teste ainda tem muito espaço para melhorias. Por exemplo, você pode visualizar a tendência e a sazonalidade dos dados de saída do modelo NBeats no gráfico, usar a tendência de saída para determinar a direção da ordem, etc.

As possibilidades são muitas. O exemplo no artigo é muito simples, mas o conteúdo principal é relativamente completo. Você pode expandir e usar o EA à vontade, mas não o use em negociações reais! A série apresenta soluções variadas e relativamente completas: desde a criação de conjuntos de dados até o treinamento de vários modelos de previsão de séries temporais, e depois o uso deles em testes históricos. Mesmo iniciantes podem seguir todo o processo passo a passo e aplicar o conhecimento adquirido na prática.
Obrigado pela atenção! Espero que você tenha aprendido algo novo.




Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/13919

Arquivos anexados |
NBeats.onnx (6949.02 KB)
onnx.mq5 (11.99 KB)
n_beats.py (11.07 KB)
Desenvolvendo um sistema de Replay (Parte 54): O nascimento do primeiro módulo Desenvolvendo um sistema de Replay (Parte 54): O nascimento do primeiro módulo
Neste artigo, iremos ver como construir o primeiro dos módulos, realmente funcional a fim de ser utilizado no sistema de replay / simulador. Além de ter como proposito geral servir para outras coisas também. O módulo que será construído aqui será o do indicador de mouse.
Funcionalidades do assistente MQL5 que você precisa conhecer (Parte 10): RBM não convencional Funcionalidades do assistente MQL5 que você precisa conhecer (Parte 10): RBM não convencional
As máquinas de Boltzmann restritas (Restrictive Boltzmann Machines, RBM) são, em um nível básico, uma rede neural de duas camadas capaz de realizar classificação não supervisionada através da redução de dimensionalidade. Vamos usar seus princípios básicos e ver o que acontece se a desenharmos e a treinarmos de forma não convencional. Será que conseguiremos obter um filtro de sinais útil?
EA de grid-hedge modificado em MQL5 (Parte II): Criando um EA de grade simples EA de grid-hedge modificado em MQL5 (Parte II): Criando um EA de grade simples
O artigo aborda a estratégia clássica de grade, descrevendo detalhadamente sua automação com um EA em MQL5 e analisando os resultados iniciais dos testes históricos. Também enfatiza a necessidade de manter posições por um longo período e considera a possibilidade de otimização de parâmetros-chave (como distância, take-profit e tamanhos de lotes) em futuras partes. O objetivo desta série de artigos é aumentar a eficiência da estratégia de negociação e sua adaptabilidade a diferentes condições de mercado.
Negociação algorítmica com MetaTrader 5 e R para iniciantes Negociação algorítmica com MetaTrader 5 e R para iniciantes
Neste artigo, vamos combinar análise financeira com negociação algorítmica, além de ver como integrar R e MetaTrader 5. Este artigo é um guia para unir a flexibilidade analítica do R com as enormes possibilidades de negociação do MetaTrader 5.