時系列マイニングのためのデータラベル(第6回):ONNXを使用したEAへの応用とテスト
はじめに
前回の記事では、バックテストの問題を解決するために、EAとPythonサーバー間の通信にソケット(WebSocket)を使用する方法について説明しました。この記事では、MQL5がネイティブにサポートしているONNXを使用して推論をおこなう方法について説明しますが、この方法にはいくつかの制限があります。ONNXがサポートしていない演算子を使用している場合に失敗する可能性があるため、この方法はすべてのモデルに適しているわけではありません(もちろん、モデルをサポートする演算子を追加することもできるが、それには多くの時間と労力が必要である)。そのため、前回の記事では多くのスペースを割いてソケット方式を紹介し、推奨しました。もちろん、一般的なモデルをONNX形式に変換することは非常に便利であり、クロスプラットフォームでの操作を効果的にサポートしてくれます。この記事では、torchモデルとONNXモデルの入出力を一致させる方法や、ONNXモデルに適したデータ形式の変換方法など、MQL5でONNXモデルを操作するための基本的な操作を中心に解説します。もちろん、EAの注文管理も含まれています。詳しく説明します。さて、今回の本題に入りましょう。
目次
ディレクトリ構造
モデル変換をおこなう際には、モデルファイルや設定ファイルを読み込むことになるのですが、恥ずかしながら、これまでの記事でスクリプトのディレクトリ構造を紹介していなかったため、読者がモデルファイルや設定ファイルの場所がわからないことがありました。ここでスクリプトのディレクトリ構造を理解します。lightning-pytorchを使用してモデルを訓練する際、コールバックでモデルの保存場所を定義せず(モデルのCheckpointを管理するコールバックはModelCheckpointクラス)、モデル名のみを定義したため、トレーナーはモデルをデフォルトのパスに保存します。ck_callback=ModelCheckpoint(monitor='val_loss', mode="min", save_top_k=1, filename='{epoch}-{val_loss:.2f}')
この時、トレーナーはモデルをルートディレクトリに保存します。少し曖昧かもしれませんが、説明のためにいくつかの図を使います。これにより、訓練プロセス中にどのファイルが保存されるか、またそのファイルがどこにあるかが明確になります。
まず第一に、モデルの保存場所です。このパスにはさまざまなバージョンのフォルダが含まれています。各バージョンフォルダには、確認ポイントフォルダ、イベントファイル、パラメータファイルがあり、確認ポイントフォルダには保存したモデルファイルがあります。
モデルを訓練する際には、最適な学習率を求めるモデルを使用し、そのモデルはフォルダのルートディレクトリに保存されます。
訓練時には、results.jsonファイルを保存し、最適なモデルのパスと最適なスコアを記録します。これはモデルを読み込むときに使用され、フォルダのルートディレクトリに保存されます。
TorchモデルをONNXモデルに変換する
引き続きNBeatsのモデルを例として使用します。以下のコードは主にNbeats.pyの推論部分に追加されます。このスクリプトは、前回の記事でNBeatsモデルを紹介したときに作成したものです。NBeatsモデルの特殊性により、一般的な方法でONNXモデルを書き出すのは困難な場合があります。モデルの推論プロセスをデバッグし、そこから関連情報を得て、書き出しに必要な関連パラメータを定義する必要がありますが、読者のために私がこのプロセスをおこなったので、心配無用です。記事のステップに従って一歩一歩進むだけで、すべての問題は簡単に解決します。1.必要なライブラリをインストールする
モデルを変換する前に、もう1つ重要なステップがあります。これは、ONNXの関連ライブラリをインストールすることです。モデルを書き出すだけなら、ONNXライブラリをインストールするだけです(pip install onnx)。しかし、モデルを変換した後にテストする必要もあるので、onnxruntimeライブラリもインストールする必要があります。このライブラリは、CPUランタイムとGPUランタイムの2つのバージョンに分かれています。モデルが大きく複雑な場合は、推論プロセスを高速化するためにGPUバージョンをインストールする必要があるかもしれません。私たちのモデルはCPU推論しか必要としないので、GPUアクセラレーションの効果は明らかではありません。したがって、CPUバージョンをインストールすることをお勧めします(pip install onnxruntime)。
2.入力情報の取得
まず、モデルを訓練モードから推論モードに切り替える必要があります(best_model.eval())。このようにする理由は、モデルの訓練モードと推論モードは異なるため、モデルの複雑さを軽減し、推論に必要な入力だけを保持する推論モードだけが必要だからです。これにより、モデルの複雑さが軽減され、推論に必要な入力のみが保持されます。次に、データを読み込んだ後にDataloaderを作成して入力項目全体を取得し、このDataloaderオブジェクトからイテレーターを取得して、次の関数を呼び出して最初のバッチデータを取得する必要があります。最初の要素には、必要なすべての入力情報が含まれています。モデルの書き出し中に、Torchが自動的に必要な入力項目を選択してくれます。次に、モデルを書き出すために必要な入力を格納する辞書を作成します(t_loader,v_loader,training=spilt_data(dt,t_shuffle=False,t_drop_last=True,v_shuffle=False,v_drop_last=True)。モデルの書き出しに必要な入力を保存するディクショナリを作成します(input_dict = {})。すべての入力オブジェクトを取得します。推論プロセスが必要なので、ここではv_loaderを使用してそれらを取得します(items = next(iter(v_loader))[0])。すべての入力名を保存するリストを作成します(input_names=[])。次に、項目を反復処理して、すべての入力と入力名を取得します。
for item in items: input_dict[item] = items[item][-1:] # print("{}:{}".format(item,input_dict[item].shape())) input_names.append(item)
3.出力情報の取得
出力を得る前に、まず推論を実行し、推論結果から必要な出力情報を得る必要があります。これが本来の推論プロセスです。
offset=1 dt=dt.iloc[-max_encoder_length-offset:-offset,:] last_=dt.iloc[-1] # print(len(dt)) for i in range(1,max_prediction_length+1): dt.loc[dt.index[-1]+1]=last_ dt['series']=0 # dt['time_idx']=dt.apply(lambda x:x.index,args=1) dt['time_idx']=dt.index-dt.index[0] input_=dt.loc[:,['close','series','time_idx']] predictions = best_model.predict(input_, mode='raw',trainer_kwargs=dict(accelerator="cpu",logger=False),return_x=True)
推論情報はpredictionオブジェクトの出力にあり、このオブジェクトを繰り返し処理してすべての出力情報を取得します。そこで、ここに次の文を追加します。
output_names=[] for out in predictions.output._fields: output_names.append(out)
4.モデルの書き出し
まず、モデルの書き出しに必要なinput_sampleを定義します(input_1=(input_dict,{}))。理由は聞かないでください。次に、NBeatsクラスのto_onnx()メソッドを使用して、ONNXに書き出します。このメソッドには、ファイルパスのパラメータも必要ですが、ここでは、NBeats.onnxというルートディレクトリに直接書き出します(best_model.to_onnx(file_path=‘NBeatsonnx’, input_sample=input_1, input_names=input_names, output_names=output_names))。プログラムがここまで実行されると、カレントフォルダのルートディレクトリにNBeats.onnxファイルが見つかります。
注:
1.入力名が完全でない場合、書き出しモデルは書き出し時に自動的に入力名を付けますが、これは混乱を招き、どれが本当の入力なのかわからなくなるため、書き出しモデルの入力名の一貫性を確保するために、書き出し関数にはすべての入力名を入力することにしています。
2.Dataloaderの入力データには、encoder_cat、encoder_cont、その他エンコーダーとデコーダーによって生成された複数の入力が含まれますが、推論プロセスでは、encoder_contとtarget_scaleの2つだけが必要です。入力データを照合するステップが冗長であるとは考えないでください。エンコーダーとデコーダーを必要とするいくつかのモデルでは、このステップは必要なのです。3.筆者がテスト時に使用した環境設定はpython-3.10;ONNX version-8; pytorch-2.1.1;operators-17です。変換したモデルをテストする
前のセクションでは、TorchモデルをONNXモデルとして書き出すことに成功しました。次の重要な作業は、このモデルをテストし、このモデルの出力が元のモデルと同じかどうかを確認することです。これは非常に重要なことです。というのも、書き出しの過程で、TorchのバージョンとONNXランタイムのカーネル互換性の問題により、いくつかのオペレーターにずれが生じる可能性があるからです。この場合、モデルを書き出す際に手作業が必要になることがあります。- まず、ONNXランタイムライブラリをインポートします(import onnxruntime as ort)。
- モデルファイルNBeats.onnxを読み込みます(sess = ort.InferenceSession(“NBeats.onnx”))。
- sess.get_inputs()の戻り値を反復処理することで、ONNXモデルの入力名を取得します。この入力名は、入力データの照合に使用されます(input_names = [input.name for input in sess.get_inputs()])。
- すべての出力を比較する必要はないので、出力の最初の項目だけを取得して比較し、結果が同じかどうかを確認します(output_name = sess.get_outputs()[0].name)。
- 結果が同じかどうかを比較するためには、入力が同じでなければならないので、モデルの入力は推論に使われるデータと一致していなければなりません。しかし、推論プロセス中にすべての入力が読み込まれるわけではないので、まずDataloader形式に変換し、input_namesを使用して入力データと一致させる必要があります。まず、TimeSeriesDataSet クラスの from_parameters() メソッドを使用して、入力データを時系列データとして読み込みます(input_ds = New_TmSrDt.from_parameters(best_model.dataset_parameters, input_,predict=True))。次に、to_dataloader()クラスのメソッドを使用して、これをDataloader型に変換します(input_dl = input_ds.to_dataloader(train=False, batch_size=1, num_workers=0))。
- 入力データに照合します。まず、データのバッチを取得し、最初の要素を取り出す必要があります(input_dict = next(iter(input_dl))[0])。次に、input_namesを使用して、入力に必要な入力データを照合します(input_data = [input_dict[name].numpy() for name in input_names])。
- 推論を実行します(pred_onnx = sess.run([output_name], dict(zip(input_names, input_data)))[0])。
- Torchの推論結果とONNXの推論結果を出力し、比較します。
torch result: tensor([[2062.9109, 2062.6191, 2062.5283, 2062.4814, 2062.3572, 2062.1545, 2061.9824, 2061.9678, 2062.1499, 2062.4380, 2062.6680, 2062.7151, 2062.5823, 2062.3979, 2062.3254, 2062.4460, 2062.7087, 2062.9802, 2063.1643, 2063.2991]])
ONNX推論の結果を表示します。
ONNX result: [[2062.911 2062.6191 2062.5283 2062.4814 2062.3572 2062.1545 2061.9824 2061.9678 2062.15 2062.438 2062.668 2062.715 2062.5823 2062.398 2062.3254 2062.446 2062.7087 2062.9802 2063.1646 2063.299 ]]
モデル推論の結果は同じであることがわかります。次のステップは、書き出ししたモデルをMQL5で設定することです。下図のようにします。
完全なコードは、次のとおりです。
# Copyright 2021, MetaQuotes Ltd. # https://www.mql5.com import lightning.pytorch as pl import os from lightning.pytorch.callbacks import EarlyStopping,ModelCheckpoint import matplotlib.pyplot as plt import pandas as pd from pytorch_forecasting import TimeSeriesDataSet,NBeats from pytorch_forecasting.data import NaNLabelEncoder from pytorch_forecasting.data.samplers import TimeSynchronizedBatchSampler from lightning.pytorch.tuner import Tuner import MetaTrader5 as mt import warnings import json from torch.utils.data import DataLoader from torch.utils.data.sampler import Sampler,SequentialSampler class New_TmSrDt(TimeSeriesDataSet): ''' rewrite dataset class ''' def to_dataloader(self, train: bool = True, batch_size: int = 64, batch_sampler: Sampler | str = None, shuffle:bool=False, drop_last:bool=False, **kwargs) -> DataLoader: default_kwargs = dict( shuffle=shuffle, # drop_last=train and len(self) > batch_size, drop_last=drop_last, # collate_fn=self._collate_fn, batch_size=batch_size, batch_sampler=batch_sampler, ) default_kwargs.update(kwargs) kwargs = default_kwargs # print(kwargs['drop_last']) if kwargs["batch_sampler"] is not None: sampler = kwargs["batch_sampler"] if isinstance(sampler, str): if sampler == "synchronized": kwargs["batch_sampler"] = TimeSynchronizedBatchSampler( SequentialSampler(self), batch_size=kwargs["batch_size"], shuffle=kwargs["shuffle"], drop_last=kwargs["drop_last"], ) else: raise ValueError(f"batch_sampler {sampler} unknown - see docstring for valid batch_sampler") del kwargs["batch_size"] del kwargs["shuffle"] del kwargs["drop_last"] return DataLoader(self,**kwargs) def get_data(mt_data_len:int): if not mt.initialize(): print('initialize() failed!') else: print(mt.version()) sb=mt.symbols_total() rts=None if sb > 0: rts=mt.copy_rates_from_pos("GOLD_micro",mt.TIMEFRAME_M15,0,mt_data_len) mt.shutdown() # print(len(rts)) rts_fm=pd.DataFrame(rts) rts_fm['time']=pd.to_datetime(rts_fm['time'], unit='s') rts_fm['time_idx']= rts_fm.index%(max_encoder_length+2*max_prediction_length) rts_fm['series']=rts_fm.index//(max_encoder_length+2*max_prediction_length) return rts_fm def spilt_data(data:pd.DataFrame, t_drop_last:bool, t_shuffle:bool, v_drop_last:bool, v_shuffle:bool): training_cutoff = data["time_idx"].max() - max_prediction_length #max:95 context_length = max_encoder_length prediction_length = max_prediction_length training = New_TmSrDt( data[lambda x: x.time_idx <= training_cutoff], time_idx="time_idx", target="close", categorical_encoders={"series":NaNLabelEncoder().fit(data.series)}, group_ids=["series"], time_varying_unknown_reals=["close"], max_encoder_length=context_length, # min_encoder_length=max_encoder_length//2, max_prediction_length=prediction_length, # min_prediction_length=1, ) validation = New_TmSrDt.from_dataset(training, data, min_prediction_idx=training_cutoff + 1) train_dataloader = training.to_dataloader(train=True, shuffle=t_shuffle, drop_last=t_drop_last, batch_size=batch_size, num_workers=0,) val_dataloader = validation.to_dataloader(train=False, shuffle=v_shuffle, drop_last=v_drop_last, batch_size=batch_size, num_workers=0) return train_dataloader,val_dataloader,training def get_learning_rate(): pl.seed_everything(42) trainer = pl.Trainer(accelerator="cpu", gradient_clip_val=0.1,logger=False) net = NBeats.from_dataset( training, learning_rate=3e-2, weight_decay=1e-2, backcast_loss_ratio=0.1, optimizer="AdamW", ) res = Tuner(trainer).lr_find( net, train_dataloaders=t_loader, val_dataloaders=v_loader, min_lr=1e-5, max_lr=1e-1 ) # print(f"suggested learning rate: {res.suggestion()}") lr_=res.suggestion() return lr_ def train(): early_stop_callback = EarlyStopping(monitor="val_loss", min_delta=1e-4, patience=10, verbose=True, mode="min") ck_callback=ModelCheckpoint(monitor='val_loss', mode="min", save_top_k=1, filename='{epoch}-{val_loss:.2f}') trainer = pl.Trainer( max_epochs=ep, accelerator="cpu", enable_model_summary=True, gradient_clip_val=1.0, callbacks=[early_stop_callback,ck_callback], limit_train_batches=30, enable_checkpointing=True, ) net = NBeats.from_dataset( training, learning_rate=lr, log_interval=10, log_val_interval=1, weight_decay=1e-2, backcast_loss_ratio=0.0, optimizer="AdamW", stack_types=["trend", "seasonality"], ) trainer.fit( net, train_dataloaders=t_loader, val_dataloaders=v_loader, # ckpt_path='best' ) return trainer if __name__=='__main__': ep=200 __train=False mt_data_len=80000 max_encoder_length = 96 max_prediction_length = 20 # context_length = max_encoder_length # prediction_length = max_prediction_length batch_size = 128 info_file='results.json' warnings.filterwarnings("ignore") dt=get_data(mt_data_len=mt_data_len) if __train: # print(dt) # dt=get_data(mt_data_len=mt_data_len) t_loader,v_loader,training=spilt_data(dt, t_shuffle=False,t_drop_last=True, v_shuffle=False,v_drop_last=True) lr=get_learning_rate() # lr=3e-3 trainer__=train() m_c_back=trainer__.checkpoint_callback m_l_back=trainer__.early_stopping_callback best_m_p=m_c_back.best_model_path best_m_l=m_l_back.best_score.item() # print(best_m_p) if os.path.exists(info_file): with open(info_file,'r+') as f1: last=json.load(fp=f1) last_best_model=last['last_best_model'] last_best_score=last['last_best_score'] if last_best_score > best_m_l: last['last_best_model']=best_m_p last['last_best_score']=best_m_l json.dump(last,fp=f1) else: with open(info_file,'w') as f2: json.dump(dict(last_best_model=best_m_p,last_best_score=best_m_l),fp=f2) best_model = NBeats.load_from_checkpoint(best_m_p) predictions = best_model.predict(v_loader, trainer_kwargs=dict(accelerator="cpu",logger=False), return_y=True) raw_predictions = best_model.predict(v_loader, mode="raw", return_x=True, trainer_kwargs=dict(accelerator="cpu",logger=False)) for idx in range(10): # plot 10 examples best_model.plot_prediction(raw_predictions.x, raw_predictions.output, idx=idx, add_loss_to_title=True) plt.show() else: with open(info_file) as f: best_m_p=json.load(fp=f)['last_best_model'] print('model path is:',best_m_p) best_model = NBeats.load_from_checkpoint(best_m_p) # added for input best_model.eval() t_loader,v_loader,training=spilt_data(dt, t_shuffle=False,t_drop_last=True, v_shuffle=False,v_drop_last=True) input_dict = {} items = next(iter(v_loader))[0] input_names=[] for item in items: input_dict[item] = items[item][-1:] # print("{}:{}".format(item,input_dict[item].shape())) input_names.append(item) # ------------------------eval---------------------------------------------- offset=1 dt=dt.iloc[-max_encoder_length-offset:-offset,:] last_=dt.iloc[-1] # print(len(dt)) for i in range(1,max_prediction_length+1): dt.loc[dt.index[-1]+1]=last_ dt['series']=0 # dt['time_idx']=dt.apply(lambda x:x.index,args=1) dt['time_idx']=dt.index-dt.index[0] input_=dt.loc[:,['close','series','time_idx']] predictions = best_model.predict(input_, mode='raw',trainer_kwargs=dict(accelerator="cpu",logger=False),return_x=True) output_names=[] for out in predictions.output._fields: output_names.append(out) # ---------------------------------------------------------------------------- input_1=(input_dict,{}) best_model.to_onnx(file_path='NBeats.onnx', input_sample=input_1, input_names=input_names, output_names=output_names) import onnxruntime as ort sess = ort.InferenceSession("NBeats.onnx") input_names = [input.name for input in sess.get_inputs()] # for input in sess.get_inputs(): # print(input.name,':',input.shape) output_name = sess.get_outputs()[0].name # ------------------------------------------------------------------------------ input_ds = New_TmSrDt.from_parameters(best_model.dataset_parameters, input_,predict=True) input_dl = input_ds.to_dataloader(train=False, batch_size=1, num_workers=0) input_dict = next(iter(input_dl))[0] input_data = [input_dict[name].numpy() for name in input_names] pred_onnx = sess.run([output_name], dict(zip(input_names, input_data))) print("torch result:",predictions.output[0]) print("onnx result:",pred_onnx[0]) # ------------------------------------------------------------------------------- best_model.plot_interpretation(predictions.x,predictions.output,idx=0) plt.show()
ONNXモデルでEAを作成する
モデルの変換とテストが完了したので、次にonnx.mq5という名前のEAファイルを作成します。EAでは、OnTimer()を使用してモデルの推論ロジックを管理し、OnTick()を使用して注文ロジックを管理する予定です。そうすれば、クウォートが来るたびに推論を実行する(これは深刻なリソースの占有を引き起こす可能性がある)のではなく、推論を実行する頻度を設定することができます。同様に、このEAでは複雑な取引ロジックは提供せず、デモ例を提供するだけですので、このEAを直接取引に使用しないでください。
1.ONNXモデルの構造を見る
このステップは非常に重要です。EAでONNXモデルの入出力を定義する必要があるので、モデルの構造を見て、入出力の数、データ型、データ次元を決定する必要があります。ONNXモデルを見るには、MQL5エディターで直接開いてモデル構造を見ることができます。入出力のスタイルも表示されますが、編集はできません。NetronやWinMLダッシュボードツールを使用することもできますが、この記事で使用するツールはNetronです。
MQL5IDEでモデルファイルNBeats.onnxを見つけ、直接開きます。下の注釈の位置に[Open in Netron]オプションがあるので、ボタンをクリックするとモデルファイルが自動的に開かれます。
または、IDEのファイルエクスプローラーでモデルファイルを右クリックすると、[Open in Netron]オプションが表示されます。
Netronツールをお持ちでない場合は、IDEがインストールを案内します。
開かれたモデルはこんな感じです。
インターフェイスはとてもシンプルで爽やかです。そしてその機能は非常に強力です。モデルノードの編集にも使用できます。主題に戻ります。最初のノードをクリックすると、Netronがモデルの関連情報を表示します。
書き出しされたNBeatsモデルの形式はONNX v8、pytorchバージョン:pytorch 2.1.1、書き出しツール:ai.onnx v17であることがわかります。
入力は2つあります。1つ目はencoder_cont、次元は[1,96,1]、データ形式はfloat32、2つ目はtarget_scale、次元は[1,2]、データ形式はfloat32です。
出力は5つあります。最初の出力は予測で、次元は[1,20]、2番目の出力はバックキャストで、次元は[1,96]、他の3つの解釈可能な出力はトレンド、季節性、一般で次元は[1,116]です。出力データ形式はすべてfloat32です。
2.モデルの入出力を定義する
モデルの入出力形式はすでに分かっており、MQL5でONNXがサポートしている入出力形式は配列、行列、ベクトルです。EAでそれらを定義してみましょう。まず、OnTimer()で入力を定義します。両方とも配列です。
- 最初の入力:matrixfin_normf;
- 2番目の入力:floatin1[1][2];
OnTick()でモデルの出力結果を呼び出す必要があるため、OnTimer()でモデルの出力を定義するのは無理があり、グローバル変数として定義する必要があります。モデルの推論結果とモデルの読み込みハンドルもグローバル変数として定義する必要があります。
- モデルハンドル:long handle;
- 最初の推論結果:vectorf y=vector<float>::Zeros(20);
- 2番目の推論結果:vectorf backcast=vector<float>::Zeros(96);
- 3番目の推論結果:vectorf trend=vector<float>::Zeros(116);
- 4番目の推論結果:vectorf seasonality=vector<float>::Zeros(116);
- 5番目の推論結果:vectorf generic=vector<float>::Zeros(116);
- 予測結果の定義:string pre=NULL;
3.推論ロジックを定義する
初期化
まず、ONNXモデルをEAの外部リソースとしてインポートします(#resource “NBeats.onnx” as uchar ExtModel[])。OnInit()関数でタイマーを初期化します(EventSetTimer(300))。この値は自分で設定できます。モデルを読み込み、モデルハンドルを取得します(handle=OnnxCreateFromBuffer(ExtModel,ONNX_DEBUG_LOGS))。モデルの入力または出力情報を表示したい場合は、以下の文を追加できます。
long in_ct=OnnxGetInputCount(handle); OnnxTypeInfo inf; for(int i=0;i<in_ct;i++){ Print(OnnxGetInputName(handle,i)); bool re=OnnxGetInputTypeInfo(handle,i,inf); //Print("map:",inf.map,"seq:",inf.sequence,"tensor:",inf.tensor,"type:",inf.type); Print(re,GetLastError()); }
データ処理
モデルの入出力はすでに定義しましたが、次にこれらの変数の具体的な定義、またどのようなデータなのかを知る必要があります。このために、pytorch_forecastingライブラリのtimeseries.pyファイルで定義を見つける必要があります。この記事では、このファイルの詳細については説明しません。
最初の入力:
encoder_contは実際には目標変数の正規化された値です。もちろんpytorch_forecastingにはEncoderNormalizer、GroupNormalizer、MultiNormalizer、NaNLabelEncoder、TorchNormalizerといったメソッドが用意されています。これらのメソッドはMQL5で実装するのが難しい場合があるため、この記事では通常の正規化メソッドを直接使用します。まず空のMqlRatesを定義します(MqlRates rates[])。それを使用し、終値の直近96バーをコピーします(if(!CopyRates(_Symbol,_Period,0,96,rates)) return:)。コピーに失敗したらそのまま終了します。また、平均と分散を計算するために、この値を受け取る行列を定義する必要があります(matrixin0_m(96,1))。このレートの終値をin0_m行列にコピーします(for(int i=0; i<96; i++) in0_m[i][0]= rates[i].close)。平均を計算します(vector m=in0_m.Mean(0))。分散を計算します(vector s=in0_m.Std(0))。平均を格納する行列mmを作成します(matrix mm(96,1))。分散を格納する行列msを作成します(matrix ms(96,1))。平均と分散を補助行列にコピーします。
for(int i=0; i<96; i++) { mm.Row(m,i); ms.Row(s,i); }
次に正規化行列を計算します。まず平均を引きます(in0_m-=mm)。次に、標準偏差で割ります(in0_m/=ms)。そして行列を入力行列にコピーし、データ型をfloatに変換します(in_normf.Assign(in0_m))。
2つ目の入力
target_scaleは実際には目標変数のスケーリング範囲であり、その最初の値は実際には目標変数の平均(in1[0][0]=m[0])、2番目のデータは目標変数の分散(in1[0][1]=s[0])です。
推論の実行
ONNXモデル推論を実行する場合、モデル構造体に表示される入出力はすべて定義されていなければなりません。必要のない入力をパラメータとしてOnnxRun()関数に渡すことになっても、1つも欠けることはできません。これは非常に重要です。1つでも欠けていると、間違いなくエラーが報告されます。
if(!OnnxRun(handle, ONNX_DEBUG_LOGS | ONNX_NO_CONVERSION, in_normf, in1, y, backcast, trend, seasonality, generic)) { Print("OnnxRun failed, error ",GetLastError()); OnnxRelease(handle); return; }
4.推論結果
予測値の平均が現在のバーの最高値と最安値の平均より大きければ、将来は上昇トレンドになると仮定し、preを「買い」に設定し、そうでなければpreを「売り」に設定します。
if (y.Mean()>iHigh(_Symbol,_Period,0)/2+iLow(_Symbol,_Period,0)/2) pre="buy"; else pre="sell";
5.注文処理ロジック
この部分については、「時系列マイニングのためのデータラベル(第5回):ソケットを使用したEAでの応用とテスト」稿で詳しく紹介しているので、今回は詳細な紹介はおこなわず、OnTick()のメインロジックをコピーしてそのまま使用することにします。各実行後、preはNULLに設定され、予測プロセスでは、この2つの値に値を割り当てます。これは、注文操作プロセスと予測プロセスの同期を保証し、前の予測値に影響されないことに注意すべきです。このステップは非常に重要です。間違えれば、論理的な混乱を引き起こします。以下は完全な注文処理コードです。
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.リサイクル資源
EAが実行されると、タイマーを閉じてONNXモデルのインスタンスハンドルを解放する必要があるので、OnDeinit(constintreason)関数に以下のコードを追加します。
void OnDeinit(const int reason) { //--- //— destroy timer EventKillTimer(); //— complete operation OnnxRelease(handle); }
ここで基本的にコードを書き終えて、バックテストでEAを読み込んでテストする必要があります。
注:
1.ONNXモデルの入出力を設定する際には、データ形式の照合に注意する必要があります。
2.ここでは出力の最初の予測値のみを使用しますが、他の出力に価値がないという意味ではありません。本連載の「時系列マイニングのためのデータラベル(第4回):ラベルデータを用いた解釈可能性分解」では、NBeatsモデルの解釈可能性について紹介しましたが、これは他の出力を用いて実装したものです。すでにpythonで可視化を確認しているので、今回はEAでの可視化関数は追加しません。興味のある読者は、視覚化のために1つまたは複数をチャートに追加してみてはいかがでしょうか。
バックテスト
バックテストを始める前に、1つ注意しなければならないことがあります。ONNXモデルは、onnx.mq5ファイルと同じディレクトリに配置しなければなりません。そうしないと、モデルファイルの読み込みに失敗します。MQL5エディターを開き、コンパイルボタンをクリックすると、コンパイルされたファイルが生成されます。スムーズにコンパイルできたら、Ctrl+F5を押してデバッグモードでバックテストを開始します。新しいウィンドウが開き、テストプロセスが表示されます。以下は私の出力ログです。
バックテストの結果は次の通りです。
完成です。
完全なコードは、次のとおりです。
//+------------------------------------------------------------------+ //| onnx.mq5 | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #resource "NBeats.onnx" as uchar ExtModel[] long handle; vectorf y=vector<float>::Zeros(20); vectorf backcast=vector<float>::Zeros(96); vectorf trend=vector<float>::Zeros(116); vectorf seasonality=vector<float>::Zeros(116); vectorf generic=vector<float>::Zeros(116); //bool is_pre=false; string pre=NULL; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- EventSetTimer(300); handle=OnnxCreateFromBuffer(ExtModel,ONNX_DEBUG_LOGS); //— specify the shape of the input data long in_ct=OnnxGetInputCount(handle); OnnxTypeInfo inf; for(int i=0;i<in_ct;i++){ Print(OnnxGetInputName(handle,i)); bool re=OnnxGetInputTypeInfo(handle,i,inf); //Print("map:",inf.map,"seq:",inf.sequence,"tensor:",inf.tensor,"type:",inf.type); Print(re,GetLastError()); } //long in_nm=OnnxGetInputName() //— return initialization result //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- //— destroy timer EventKillTimer(); //— complete operation OnnxRelease(handle); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- MqlTradeRequest request; MqlTradeResult result; //int x=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE); if (pre!=NULL) { //Print("The predicted value is:",pre); ulong numt=0; ulong tik=0; bool sod=false; ulong tpt=-1; ZeroMemory(request); numt=PositionsTotal(); //Print("All tickets: ",numt); if (numt>0) { tik=PositionGetTicket(numt-1); sod=PositionSelectByTicket(tik); tpt=PositionGetInteger(POSITION_TYPE);//ORDER_TYPE_BUY or ORDER_TYPE_SELL if (tik==0 || sod==false || tpt==0) return; } if (pre=="buy") { if (tpt==POSITION_TYPE_BUY) return; request.action=TRADE_ACTION_DEAL; request.symbol=Symbol(); request.volume=0.1; request.deviation=5; request.type_filling=ORDER_FILLING_IOC; request.type = ORDER_TYPE_BUY; request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK); if(tpt==POSITION_TYPE_SELL) { request.position=tik; Print("Close sell order."); } else{ Print("Open buy order."); } OrderSend(request, result); } else{ if (tpt==POSITION_TYPE_SELL) return; request.action = TRADE_ACTION_DEAL; request.symbol = Symbol(); request.volume = 0.1; request.type = ORDER_TYPE_SELL; request.price = SymbolInfoDouble(Symbol(), SYMBOL_BID); request.deviation = 5; //request.type_filling=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE); request.type_filling=ORDER_FILLING_IOC; if(tpt==POSITION_TYPE_BUY) { request.position=tik; Print("Close buy order."); } else{ Print("OPen sell order."); } OrderSend(request, result); } //is_pre=false; } pre=NULL; } //+------------------------------------------------------------------+ void OnTimer() { //float in0[1][96][1]; matrixf in_normf; float in1[1][2]; //— get the last 10 bars MqlRates rates[]; if(!CopyRates(_Symbol,_Period,0,96,rates)) return; //— input a set of OHLC vectors //double out[1][20]; matrix in0_m(96,1); for(int i=0; i<96; i++) { in0_m[i][0]= rates[i].close; } //— normalize the input data // matrix x_norm=x; vector m=in0_m.Mean(0); vector s=in0_m.Std(0); in1[0][0]=m[0]; in1[0][1]=s[0]; matrix mm(96,1); matrix ms(96,1); // //— fill in the normalization matrices for(int i=0; i<96; i++) { mm.Row(m,i); ms.Row(s,i); } // //— normalize the input data in0_m-=mm; in0_m/=ms; // //— convert normalized input data to float type in_normf.Assign(in0_m); //— get the output data of the model here, i.e. the price prediction //— run the model if(!OnnxRun(handle, ONNX_DEBUG_LOGS | ONNX_NO_CONVERSION, in_normf, in1, y, backcast, trend, seasonality, generic)) { Print("OnnxRun failed, error ",GetLastError()); OnnxRelease(handle); return; } //— print the output value of the model to the log //Print(y); //is_pre=true; if (y.Mean()>iHigh(_Symbol,_Period,0)/2+iLow(_Symbol,_Period,0)/2) pre="buy"; else pre="sell"; }
まとめ
この連載は今回が最終回となる予定です。この記事では、TorchモデルをONNXモデルに変換するプロセス全体について、モデルの入出力の見つけ方、形式の定義方法、モデルとのマッチング方法、いくつかのデータ処理テクニックなどを詳しく紹介しました。この記事の難しさは、複雑な入出力を持つモデルをONNXモデルとして書き出す方法にあります。読者がインスピレーションを得て、そこから得るものがあることを願っています。もちろん、私たちのテストEAにはまだまだ改善の余地があります。例えば、NBeatsモデル出力のトレンドや季節性をチャートで可視化したり、出力トレンドから注文の方向性を判断したりすることができます。
やる限り無数の可能性があります。記事中の例は最も単純な例に過ぎませんが、核心的な内容は比較的揃っています。自由に拡張して使用することができますが、このEAを実際の取引に気軽に使用しないようご注意ください。この連載では、データセットの作成から、さまざまな時系列予測モデルの訓練、そしてバックテストでの使用方法まで、さまざまな比較的完全なソリューションを提供しています。初心者でも、ステップバイステップで全過程を完了し、練習に応用することができます。この連載を無事に終えることができます。
ご精読ありがとうございました。何かを学んでいただけたなら幸いです。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/13919
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索