English Русский 中文 Español Deutsch Português 한국어 Français Italiano Türkçe
preview
MQL5でONNXモデルを使用する方法

MQL5でONNXモデルを使用する方法

MetaTrader 5機械学習 | 16 5月 2023, 12:30
453 0
MetaQuotes
MetaQuotes

はじめに

株価を予測するためのCNN-LSTMベースのモデル(WenjieLu、JiazhengLi、YifanLi、AijunSun、JingyangWang、Complexityマガジン、vol.2020、記事ID6622927、10ページ、2020年)稿(英語)の著者は、さまざまな株価予測モデルを比較しました。

株価データには時系列の特徴があります。

同時に、記憶機能により時系列データ間の関係を分析できる利点を持つ機械学習のLSTM (long short-term memory)に基づき、CNN-LSTMに基づく株価の予測手法を提案します。

その間、MLP、CNN、RNN、LSTM、CNN-RNNなどの予測モデルを使用して、それぞれで株価を予測します。さらに、これらのモデルの予測結果を分析し、比較します。 
この調査で使用されたデータは、1991年7月1日から2020年8月31日までの毎日の株価に関するもので、7127取引日を含みます。

履歴データに関しては、始値、高値、安値、終値、ボリューム、出来高、浮き沈み、変化の8つの特徴を選択します。 
まず、過去10日間の項目であるデータから効率的に特徴を抽出するためにCNNを採用します。そして、LSTMを採用し、抽出した特徴データから株価を予測します。

実験結果によると、CNN-LSTMは最高の予測精度で信頼性の高い株価予測を提供できます。
この予測方法は、株価予測のための新しい研究アイデアを提供するだけでなく、学者が金融時系列データを研究するための実践的な経験も提供します。

検討されたすべてのモデルの中で、CNN-LSTMモデルが実験中に最良の結果を生成しました。この記事では、そのようなモデルを作成して金融時系列を予測する方法と、MQL5 EAで作成したONNXモデルを使用する方法を検討します。


1.モデルの構築

Pythonは一連の特殊なライブラリを備えており、MLモデルを操作するための広範な機能を提供します。データの準備と処理はライブラリで非常に容易になります。

GPUリソースを使用して、MLプロジェクトの効率を最大化することをお勧めします。多くのWindowsユーザーは、現在のTensorFlowバージョンをインストールしようとして問題に遭遇しました(ビデオガイドとその テキスト版コメントを参照してください)。TensorFlow2.10.0はテストしたので、このバージョンを使用することをお勧めします。GPU計算は、CUDA11.2およびCUDNN8.1.0.7ライブラリを使用して、NVIDIA GeForce RTX 2080 Tiグラフィックスカードで実行されました。


1.1.Pythonとライブラリをインストールする

Pythonがない場合は、インストールする必要があります。バージョン3.9.16を使用しました。

また、ライブラリをインストールします(Conda/Anacondaを使用している場合は、Anacondaプロンプトで次のコマンドを実行します)。

python.exe -m pip install --upgrade pip
pip install --upgrade pandas
pip install --upgrade scikit-learn
pip install --upgrade matplotlib
pip install --upgrade tqdm
pip install --upgrade metatrader5
pip install --upgrade onnx==1.12
pip install --upgrade tf2onnx
pip install --upgrade tensorflow==2.10.0


1.2.TensorFlowのバージョンとGPUを確認する

以下のコードは、インストールされているTensorFlowのバージョンをチェックし、GPUを使用してモデルを計算できるかどうかを確認します。

#check tensorflow version
print(tf.__version__)
#check GPU support
print(len(tf.config.list_physical_devices('GPU'))>0)

必要なバージョンが正しくインストールされている場合、次の結果が表示されます。

2.10.0
True

モデルの構築と訓練にはPythonスクリプトを使用しました。このプロセスの手順を以下に簡単に説明します。


1.3.モデルの構築と訓練

スクリプトは、モデルで使用されるPythonライブラリをインポートすることから始まります。

#Python libraries
import matplotlib.pyplot as plt 
import MetaTrader5 as mt5
import tensorflow as tf
import numpy as np
import pandas as pd
import tf2onnx
from sklearn.model_selection import train_test_split
from sys import argv

TensorFlowのバージョンとGPUの可用性を確認します。

#check tensorflow version
print(tf.__version__)

2.10.0

#check GPU support
print(len(tf.config.list_physical_devices('GPU'))>0)

True

Pythonからの操作のためにMetaTrader 5を初期化します。

#initialize MetaTrader5 for history data
if not mt5.initialize():
    print("initialize() failed, error code =",mt5.last_error())
    quit()

MetaTrader 5ターミナルに関する情報:

#show terminal info
terminal_info=mt5.terminal_info()
print(terminal_info)

TerminalInfo(community_account=True, community_connection=True, connected=True, dlls_allowed=False, trade_allowed=False, tradeapi_disabled=False, email_enabled=False, ftp_enabled=False, notifications_enabled=False, mqid=False, build=3640, maxbars=100000, codepage=0, ping_last=58768, community_balance=1.0, retransmission=0.015296317559440137, company='MetaQuotes Software Corp.', name='MetaTrader 5', language='English', path='C:\\Program Files\\MetaTrader 5', data_path='C:\\Users\\user\\AppData\\Roaming\\MetaQuotes\\Terminal\\D0E8209F77C8CF37AD8BF550E51FF075', commondata_path='C:\\Users\\user\\AppData\\Roaming\\MetaQuotes\\Terminal\\Common')

#show file path
file_path=terminal_info.data_path+"\\MQL5\\Files\\"
print(file_path)
C:\Users\user\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Files\

モデルを保存するためのパスを出力します(この例では、スクリプトはJupyter Notebookで実行)。

#data path to save the model
data_path=argv[0]
last_index=data_path.rfind("\\")+1
data_path=data_path[0:last_index]
print("data path to save onnx model",data_path)

onnxモデルを保存するデータパスC:\Users\user\AppData\Roaming\Python\Python39\site-packages\

履歴データを要求する日付を準備します。この例では、現在の日付から120のEURUSDH1バーをリクエストします。

#set start and end dates for history data
from datetime import timedelta,datetime
end_date = datetime.now()
start_date = end_date - timedelta(days=120)

#print start and end dates
print("data start date=",start_date)
print("data end date=",end_date)
データ開始日=2022-11-2812:28:39.870685
データ終了日=2023-03-2812:28:39.870685

EURUSDの履歴データをリクエストします。

#get EURUSD rates (H1) from start_date to end_date
eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, start_date, end_date)

ダウンロードしたデータを出力します。

#check
print(eurusd_rates)


#create dataframe
df = pd.DataFrame(eurusd_rates)

データフレームの開始と終了を表示します。

#show dataframe head
df.head()


#show dataframe tail
df.tail()


#show dataframe shape (the number of rows and columns in the data set)
df.shape

(2045, 8)

終値のみを選択します。

#prepare close prices only
data = df.filter(['close']).values

データを出力します。

#show close prices
plt.figure(figsize = (18,10))
plt.plot(data,'b',label = 'Original')
plt.xlabel("Hours")
plt.ylabel("Price")
plt.title("EURUSD_H1")
plt.legend()

EURUSD H1終値チャート

MinMaxScalerを使用してソース価格データを[0,1]の範囲にスケーリングします。

#scale data using MinMaxScaler
from sklearn.preprocessing import MinMaxScaler
scaler=MinMaxScaler(feature_range=(0,1))
scaled_data = scaler.fit_transform(data)

データの最初の80%が訓練に使用されます。

#training size is 80% of the data
training_size = int(len(scaled_data)*0.80) 
print("training size:",training_size)

訓練サイズ:1636

#create train data and check size
train_data_initial = scaled_data[0:training_size,:]
print(len(train_data_initial))

1636

#create test data and check size
test_data_initial= scaled_data[training_size:,:1]
print(len(test_data_initial))

409

次の関数は訓練シーケンスを作成します。

#split a univariate sequence into samples
def split_sequence(sequence, n_steps):
    X, y = list(), list()
    for i in range(len(sequence)):
       #find the end of this pattern
       end_ix = i + n_steps
       #check if we are beyond the sequence
       if end_ix > len(sequence)-1:
          break
       #gather input and output parts of the pattern
       seq_x, seq_y = sequence[i:end_ix], sequence[end_ix]
       X.append(seq_x)
       y.append(seq_y)
    return np.array(X), np.array(y)

セットを構築します。

#split into samples
time_step = 120
x_train, y_train = split_sequence(train_data_initial, time_step)
x_test, y_test = split_sequence(test_data_initial, time_step)
#reshape input to be [samples, time steps, features] which is required for LSTM
x_train =x_train.reshape(x_train.shape[0],x_train.shape[1],1)
x_test = x_test.reshape(x_test.shape[0],x_test.shape[1],1)

訓練とテスト用のテンソル形状:

#show shape of train data
x_train.shape

(1516, 120, 1)

#show shape of test data
x_test.shape

(289, 120, 1)

#import keras libraries for the model
import math
from keras.models import Sequential
from keras.layers import Dense,Activation,Conv1D,MaxPooling1D,Dropout
from keras.layers import LSTM
from keras.utils.vis_utils import plot_model
from keras.metrics import RootMeanSquaredError as rmse
from keras import optimizers

モデルを設定します。

#define the model
model = Sequential()
model.add(Conv1D(filters=256, kernel_size=2,activation='relu',padding = 'same',input_shape=(120,1)))
model.add(MaxPooling1D(pool_size=2))
model.add(LSTM(100, return_sequences = True))
model.add(Dropout(0.3))
model.add(LSTM(100, return_sequences = False))
model.add(Dropout(0.3))
model.add(Dense(units=1, activation = 'sigmoid'))
model.compile(optimizer='adam', loss= 'mse' , metrics = [rmse()])

モデルプロパティを表示します。

#show model
model.summary()

モデルプロパティ

モデル訓練:

#measure time
import time 
time_calc_start = time.time()

#fit model with 300 epochs
history=model.fit(x_train,y_train,epochs=300,validation_data=(x_test,y_test),batch_size=32,verbose=1)

#calculate time
fit_time_seconds = time.time() - time_calc_start
print("fit time =",fit_time_seconds," seconds.")

Epoch 1/300
48/48 [==============================] - 8s 49ms/step - loss:0.0129 - root_mean_squared_error:0.1136 - val_loss:0.0065-val_root_mean_squared_error:0.0804

...

Epoch 299/300
48/48 [==============================] - 2s 35ms/step - loss:4.5197e-04-root_mean_squared_error:0.0213 - val_loss:4.2535e-04-val_root_mean_squared_error:0.0206
Epoch 300/300
48/48 [==============================] - 2s 32ms/step - loss:4.2967e-04-root_mean_squared_error:0.0207 - val_loss:4.4040e-04-val_root_mean_squared_error:0.0210

fit time = 467.4918096065521  seconds.

訓練にかかった時間は約8分です。

#show training history keys
history.history.keys()

dict_keys(['loss','root_mean_squared_error','val_loss','val_root_mean_squared_error'])

訓練およびテストデータセットにおける最適化ダイナミクス:

#show iteration-loss graph for training and validation
plt.figure(figsize = (18,10))
plt.plot(history.history['loss'],label='Training Loss',color='b')
plt.plot(history.history['val_loss'],label='Validation-loss',color='g')
plt.xlabel("Iteration")
plt.ylabel("Loss")
plt.title("LOSS")
plt.legend()

損失関数の反復グラフ

#show iteration-rmse graph for training and validation
plt.figure(figsize = (18,10))
plt.plot(history.history['root_mean_squared_error'],label='Training RMSE',color='b')
plt.plot(history.history['val_root_mean_squared_error'],label='Validation-RMSE',color='g')
plt.xlabel("Iteration")
plt.ylabel("RMSE")
plt.title("RMSE")
plt.legend()


RMSE反復グラフ

#evaluate training data
model.evaluate(x_train,y_train, batch_size = 32)

48/48 [==============================] - 1s 22ms/step - loss:2.9911e-04-root_mean_squared_error:0.0173
[0.00029911252204328775,0.01729486882686615]
#evaluate testing data
model.evaluate(x_test,y_test, batch_size = 32)

10/10 [==============================] - 0s 31ms/step - loss:4.4040e-04-root_mean_squared_error:0.0210

[0.00044039846397936344,0.020985672250390053]

訓練データセットに対する予測の形成:

#prediction using training data
train_predict = model.predict(x_train)
plot_y_train = y_train.reshape(-1,1)

48/48 [==============================] - 2s 18ms/step

訓練間隔の実際のグラフと予測されたグラフを出力します。

#show actual vs predicted (training) graph
plt.figure(figsize=(18,10))
plt.plot(scaler.inverse_transform(plot_y_train),color = 'b', label = 'Original')
plt.plot(scaler.inverse_transform(train_predict),color='red', label = 'Predicted')
plt.title("Prediction Graph Using Training Data")
plt.xlabel("Hours")
plt.ylabel("Price")
plt.legend()
plt.show()

訓練データセットの予測のグラフ:

テストデータセットでの予測の形成:

#prediction using testing data
test_predict = model.predict(x_test)
plot_y_test = y_test.reshape(-1,1)

11/11 [==============================] - 0s 11ms/step

メトリックを計算するには、間隔[0,1]からデータを変換する必要があります。繰り返しますが、MinMaxScalerを使用します。

#calculate metrics
from sklearn import metrics
from sklearn.metrics import r2_score
#transform data to real values
value1=scaler.inverse_transform(plot_y_test)
value2=scaler.inverse_transform(test_predict)
#calc score
score = np.sqrt(metrics.mean_squared_error(value1,value2))
print("RMSE         : {}".format(score))
print("MSE          :", metrics.mean_squared_error(value1,value2))
print("R2 score     :",metrics.r2_score(value1,value2))

RMSE:0.0015151631684117558
MSE:2.295719426911551e-06
R2スコア:0.9683533377809039

#show actual vs predicted (testing) graph
plt.figure(figsize=(18,10))
plt.plot(scaler.inverse_transform(plot_y_test),color = 'b',  label = 'Original')
plt.plot(scaler.inverse_transform(test_predict),color='g', label = 'Predicted')
plt.title("Prediction Graph Using Testing Data")
plt.xlabel("Hours")
plt.ylabel("Price")
plt.legend()
plt.show()

テストデータベースセットの予測グラフ:


モデルをonnxファイルにエクスポートします。

# save model to ONNX
output_path = data_path+"model.eurusd.H1.120.onnx"
onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path)
print(f"model saved to {output_path}")

output_path = file_path+"model.eurusd.H1.120.onnx"
onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path)
print(f"saved model to {output_path}")

# finish
mt5.shutdown()

Pythonスクリプトの完全なコードは、Jupyter Notebookの記事に添付されています。

A CNN-LSTM-Based Model to Forecast Stock Prices」稿では、CNN-LSTMを使用したモデルで最良の結果R^2=0.9646が得られました。この例では、CNN-LSTMネットワークがR^2=0.9684という最良の結果を生成しました。結果によると、このタイプのモデルは予測問題を効率的に解決できます。

金融務時系列を予測するためにCNN-LSTMモデルを構築および訓練するPythonスクリプトの例を検討しました。


2.MetaTrader 5でモデルを使用する

2.1.始める前に知っておくとよいこと

モデルを作成するには、次の2つの方法があります。OnnxCreateを使用してonnxファイルからモデルを作成するか、OnnxCreateFromBufferを使用してデータ配列からモデルを作成することができます。

ONNXモデルがEAのリソースとして使用されている場合、モデルを変更するたびにEAを再コンパイルする必要があります。

すべてのモデルに完全に定義されたサイズの入力および/または出力テンソルがあるわけではありません。これは通常、パッケージサイズを決定する最初の次元です。モデルを実行する前に、OnnxSetInputShapeおよびOnnxSetOutputShape関数を使用してサイズを明示的に指定する必要があります。モデルの入力データは、モデルの訓練時に行ったのと同じ方法で準備する必要があります。

入力データと出力データについては、モデルで使用されているのと同じ型の配列、行列、および/またはベクトルを使用することをお勧めします。この場合、モデルの実行時にデータを変換する必要はありません。データが必要な型で表現できない場合、データは自動的に変換されます。

OnnxRunを使用して、モデルを推論(実行)します。モデルは複数回実行できます。モデルを使用した後、OnnxRelease関数を使用してモデルを解放します。

MQL5のONNXモデルの完全なドキュメント


2.2.onnxファイルを読み取り、入力と出力に関する情報を取得する

モデルを使用するには、モデルの場所、入力データ型と形状、出力データ型と形状を知る必要があります。以前に作成したスクリプトによると、model.eurusd.H1.120.onnxは、onnxファイルを生成したPythonスクリプトと同じフォルダにあります。入力はfloat32、120の正規化された終値(バッチサイズが1の場合)です。出力はfloat32で、これはモデルによって予測された1つの正規化された価格です。

また、MQL5スクリプトを使用してモデルの入力データと出力データを取得するために、MQL5\Filesフォルダにonnxファイルを作成しました。

//+------------------------------------------------------------------+
//|                                                OnnxModelInfo.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"

#define UNDEFINED_REPLACE 1

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   string file_names[];
   if(FileSelectDialog("Open ONNX model",NULL,"ONNX files (*.onnx)|*.onnx|All files (*.*)|*.*",FSD_FILE_MUST_EXIST,file_names,NULL)<1)
      return;

   PrintFormat("Create model from %s with debug logs",file_names[0]);

   long session_handle=OnnxCreate(file_names[0],ONNX_DEBUG_LOGS);
   if(session_handle==INVALID_HANDLE)
     {
      Print("OnnxCreate error ",GetLastError());
      return;
     }

   OnnxTypeInfo type_info;

   long input_count=OnnxGetInputCount(session_handle);
   Print("model has ",input_count," input(s)");
   for(long i=0; i<input_count; i++)
     {
      string input_name=OnnxGetInputName(session_handle,i);
      Print(i," input name is ",input_name);
      if(OnnxGetInputTypeInfo(session_handle,i,type_info))
         PrintTypeInfo(i,"input",type_info);
     }

   long output_count=OnnxGetOutputCount(session_handle);
   Print("model has ",output_count," output(s)");
   for(long i=0; i<output_count; i++)
     {
      string output_name=OnnxGetOutputName(session_handle,i);
      Print(i," output name is ",output_name);
      if(OnnxGetOutputTypeInfo(session_handle,i,type_info))
         PrintTypeInfo(i,"output",type_info);
     }

   OnnxRelease(session_handle);
  }
//+------------------------------------------------------------------+
//| PrintTypeInfo                                                    |
//+------------------------------------------------------------------+
void PrintTypeInfo(const long num,const string layer,const OnnxTypeInfo& type_info)
  {
   Print("   type ",EnumToString(type_info.type));
   Print("   data type ",EnumToString(type_info.element_type));

   if(type_info.dimensions.Size()>0)
     {
      bool   dim_defined=(type_info.dimensions[0]>0);
      string dimensions=IntegerToString(type_info.dimensions[0]);
      for(long n=1; n<type_info.dimensions.Size(); n++)
        {
         if(type_info.dimensions[n]<=0)
            dim_defined=false;
         dimensions+=", ";
         dimensions+=IntegerToString(type_info.dimensions[n]);
        }
      Print("   shape [",dimensions,"]");
      //--- not all dimensions defined
      if(!dim_defined)
         PrintFormat("   %I64d %s shape must be defined explicitly before model inference",num,layer);
      //--- reduce shape
      uint reduced=0;
      long dims[];
      for(long n=0; n<type_info.dimensions.Size(); n++)
        {
         long dimension=type_info.dimensions[n];
         //--- replace undefined dimension
         if(dimension<=0)
            dimension=UNDEFINED_REPLACE;
         //--- 1 can be reduced
         if(dimension>1)
           {
            ArrayResize(dims,reduced+1);
            dims[reduced++]=dimension;
           }
        }
      //--- all dimensions assumed 1
      if(reduced==0)
        {
         ArrayResize(dims,1);
         dims[reduced++]=1;
        }
      //--- shape was reduced
      if(reduced<type_info.dimensions.Size())
        {
         dimensions=IntegerToString(dims[0]);
         for(long n=1; n<dims.Size(); n++)
           {
            dimensions+=", ";
            dimensions+=IntegerToString(dims[n]);
           }
         string sentence="";
         if(!dim_defined)
            sentence=" if undefined dimension set to "+(string)UNDEFINED_REPLACE;
         PrintFormat("   shape of %s data can be reduced to [%s]%s",layer,dimensions,sentence);
        }
     }
   else
      PrintFormat("no dimensions defined for %I64d %s",num,layer);
  }
//+------------------------------------------------------------------+

ファイル選択ウィンドウで、MQL5\Filesに保存されているonnxファイルを選択し、OnnxCreateを使用してファイルからモデルを作成し、次の情報を取得しました。

Create model from model.eurusd.H1.120.onnx with debug logs
ONNX: Creating and using per session threadpools since use_per_session_threads_ is true
ONNX: Dynamic block base set to 0
ONNX: Initializing session.
ONNX: Adding default CPU execution provider.
ONNX: Total shared scalar initializer count: 0
ONNX: Total fused reshape node count: 0
ONNX: Removing NodeArg 'Gather_out0'. It is no longer used by any node.
ONNX: Removing NodeArg 'Gather_token_1_out0'. It is no longer used by any node.
ONNX: Total shared scalar initializer count: 0
ONNX: Total fused reshape node count: 0
ONNX: Removing initializer 'sequential/conv1d/Conv1D/ExpandDims_1:0'. It is no longer used by any node.
ONNX: Use DeviceBasedPartition as default
ONNX: Saving initialized tensors.
ONNX: Done saving initialized tensors
ONNX: Session successfully initialized.
model has 1 input(s)
0 input name is conv1d_input
   type ONNX_TYPE_TENSOR
   data type ONNX_DATA_TYPE_FLOAT
   shape [-1, 120, 1]
   0 input shape must be defined explicitly before model inference
   shape of input data can be reduced to [120] if undefined dimension set to 1
model has 1 output(s)
0 output name is dense
   type ONNX_TYPE_TENSOR
   data type ONNX_DATA_TYPE_FLOAT
   shape [-1, 1]
   0 output shape must be defined explicitly before model inference
   shape of output data can be reduced to [1] if undefined dimension set to 1

デバッグモードが有効になっているので

   long session_handle=OnnxCreate(file_names[0],ONNX_DEBUG_LOGS);

ONNXプレフィックスを持つログがあります。

モデルには実際には1つの入力と1つの出力があることがわかります。ここで、入力テンソルの最初の次元と出力テンソルの最初の次元は定義されていません。これらのディメンションがバッチサイズの原因であると想定されます。したがって、モデルを推論する前に、使用するサイズ(OnnxSetInputShapeおよびOnnxSetOutputShape)を明示的に指定する必要があります。通常、モデルに入力されるデータセットは1つだけです。詳細な例は、次の段落「取引EAでのONNXモデルの使用例」に記載されています。

データを準備するとき、次元が[1,120,1]の配列を使用する必要はありません。1次元配列または120要素のベクトルを入力できます。


2.3.取引EAでのONNXモデルの使用例

宣言と定義

#include <Trade\Trade.mqh>

input double InpLots = 1.0;    // Lots amount to open position

#resource "Python/model.120.H1.onnx" as uchar ExtModel[]

#define SAMPLE_SIZE 120

long     ExtHandle=INVALID_HANDLE;
int      ExtPredictedClass=-1;
datetime ExtNextBar=0;
datetime ExtNextDay=0;
float    ExtMin=0.0;
float    ExtMax=0.0;
CTrade   ExtTrade;

//--- price movement prediction
#define PRICE_UP   0
#define PRICE_SAME 1
#define PRICE_DOWN 2

OnInit関数

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   if(_Symbol!="EURUSD" || _Period!=PERIOD_H1)
     {
      Print("model must work with EURUSD,H1");
      return(INIT_FAILED);
     }

//--- create a model from static buffer
   ExtHandle=OnnxCreateFromBuffer(ExtModel,ONNX_DEFAULT);
   if(ExtHandle==INVALID_HANDLE)
     {
      Print("OnnxCreateFromBuffer error ",GetLastError());
      return(INIT_FAILED);
     }

//--- since not all sizes defined in the input tensor we must set them explicitly
//--- first index - batch size, second index - series size, third index - number of series (only Close)
   const long input_shape[] = {1,SAMPLE_SIZE,1};
   if(!OnnxSetInputShape(ExtHandle,ONNX_DEFAULT,input_shape))
     {
      Print("OnnxSetInputShape error ",GetLastError());
      return(INIT_FAILED);
     }

//--- since not all sizes defined in the output tensor we must set them explicitly
//--- first index - batch size, must match the batch size of the input tensor
//--- second index - number of predicted prices (we only predict Close)
   const long output_shape[] = {1,1};
   if(!OnnxSetOutputShape(ExtHandle,0,output_shape))
     {
      Print("OnnxSetOutputShape error ",GetLastError());
      return(INIT_FAILED);
     }
//---
   return(INIT_SUCCEEDED);
  }

現在の銘柄/期間データを使用するため、EURUSD、H1のみを使用します。

私たちのモデルはリソースとしてEAに含まれています。EAは完全に自己完結型であり、外部のonnxファイルを読み取る必要はありません。リソース配列からモデルが作成されます。

入力および出力のデータ形状は、明示的に定義する必要があります。

OnTick関数:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- check new day
   if(TimeCurrent()>=ExtNextDay)
     {
      GetMinMax();
      //--- set next day time
      ExtNextDay=TimeCurrent();
      ExtNextDay-=ExtNextDay%PeriodSeconds(PERIOD_D1);
      ExtNextDay+=PeriodSeconds(PERIOD_D1);
     }

//--- check new bar
   if(TimeCurrent()<ExtNextBar)
      return;
//--- set next bar time
   ExtNextBar=TimeCurrent();
   ExtNextBar-=ExtNextBar%PeriodSeconds();
   ExtNextBar+=PeriodSeconds();
//--- check min and max
   double close=iClose(_Symbol,_Period,0);
   if(ExtMin>close)
      ExtMin=close;
   if(ExtMax<close)
      ExtMax=close;

//--- predict next price
   PredictPrice();
//--- check trading according to prediction
   if(ExtPredictedClass>=0)
      if(PositionSelect(_Symbol))
         CheckForClose();
      else
         CheckForOpen();
  }

新しい1日の始まりを定義します。1日の始まりは、120時間系列の価格を正規化するために、120日系列の安値と高値を更新するために使用されます。モデルは、入力データを準備するときに従わなければならないこれらの条件下で訓練されました。

//+------------------------------------------------------------------+
//| Get minimal and maximal Close for last 120 days                  |
//+------------------------------------------------------------------+
void GetMinMax(void)
  {
   vectorf close;
   close.CopyRates(_Symbol,PERIOD_D1,COPY_RATES_CLOSE,0,SAMPLE_SIZE);
   ExtMin=close.Min();
   ExtMax=close.Max();
  }

必要に応じて、1日を通してLowとHighを変更できます。

次は予測関数です。

//+------------------------------------------------------------------+
//| Predict next price                                               |
//+------------------------------------------------------------------+
void PredictPrice(void)
  {
   static vectorf output_data(1);            // vector to get result
   static vectorf x_norm(SAMPLE_SIZE);       // vector for prices normalize

//--- check for normalization possibility
   if(ExtMin>=ExtMax)
     {
      ExtPredictedClass=-1;
      return;
     }
//--- request last bars
   if(!x_norm.CopyRates(_Symbol,_Period,COPY_RATES_CLOSE,1,SAMPLE_SIZE))
     {
      ExtPredictedClass=-1;
      return;
     }
   float last_close=x_norm[SAMPLE_SIZE-1];
//--- normalize prices
   x_norm-=ExtMin;
   x_norm/=(ExtMax-ExtMin);
//--- run the inference
   if(!OnnxRun(ExtHandle,ONNX_NO_CONVERSION,x_norm,output_data))
     {
      ExtPredictedClass=-1;
      return;
     }
//--- denormalize the price from the output value
   float predicted=output_data[0]*(ExtMax-ExtMin)+ExtMin;
//--- classify predicted price movement
   float delta=last_close-predicted;
   if(fabs(delta)<=0.00001)
      ExtPredictedClass=PRICE_SAME;
   else
     {
      if(delta<0)
         ExtPredictedClass=PRICE_UP;
      else
         ExtPredictedClass=PRICE_DOWN;
     }
  }

まず、正規化できるかどうかを確認します。正規化は、MinMaxScalerPython関数のように実装されます。

#scale data
from sklearn.preprocessing import MinMaxScaler
scaler=MinMaxScaler(feature_range=(0,1))
scaled_data = scaler.fit_transform(data)

したがって、正規化コードは非常に単純で簡単です。

入力データと結果を受け取るためのベクトルは、静的として編成されます。これにより、プログラムの存続期間全体にわたって存在する再配置不可能なバッファが保証されます。したがって、ONNXモデルの入力テンソルと出力テンソルは、モデルを実行するたびに再作成されるわけではありません。

主要関数はOnnxRunです。ONNX_NO_CONVERSIONフラグは、MQL5float型がONNX_DATA_TYPE_FLOATに正確に対応するため、入力および出力データを変換してはならないことを示します。ONNX_DEBUGフラグが設定されていません。

その後、取得したデータを予測価格に非正規化し、価格が上がるか下がるか、変わらないかのクラスを決定します。


取引戦略は簡単です。各時間の開始時に、その時間の終わりの価格予測を確認します。予想価格が上がれば、買います。モデルが下向きの動きを予測する場合、売ります。

//+------------------------------------------------------------------+
//| Check for open position conditions                               |
//+------------------------------------------------------------------+
void CheckForOpen(void)
  {
   ENUM_ORDER_TYPE signal=WRONG_VALUE;
//--- check signals
   if(ExtPredictedClass==PRICE_DOWN)
      signal=ORDER_TYPE_SELL;    // sell condition
   else
     {
      if(ExtPredictedClass==PRICE_UP)
         signal=ORDER_TYPE_BUY;  // buy condition
     }

//--- open position if possible according to signal
   if(signal!=WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
     {
      double price;
      double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID);
      double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      if(signal==ORDER_TYPE_SELL)
         price=bid;
      else
         price=ask;
      ExtTrade.PositionOpen(_Symbol,signal,InpLots,price,0.0,0.0);
     }
  }
//+------------------------------------------------------------------+
//| Check for close position conditions                              |
//+------------------------------------------------------------------+
void CheckForClose(void)
  {
   bool bsignal=false;
//--- position already selected before
   long type=PositionGetInteger(POSITION_TYPE);
//--- check signals
   if(type==POSITION_TYPE_BUY && ExtPredictedClass==PRICE_DOWN)
      bsignal=true;
   if(type==POSITION_TYPE_SELL && ExtPredictedClass==PRICE_UP)
      bsignal=true;

//--- close position if possible
   if(bsignal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
     {
      ExtTrade.PositionClose(_Symbol,3);
      //--- open opposite
      CheckForOpen();
     }
  }

それでは、ストラテジーテスターでEAのパフォーマンスを確認してみましょう。年初からEAをテストするには、以前のデータを使用してモデルを訓練する必要があります。そのため、未使用の部分を削除し、訓練の終了日をテスト期間と重ならないように変更するなど、Pythonスクリプトをわずかに変更しました。

ONNX.eurusd.H1.120.Training.pyスクリプトはPythonサブフォルダにあり、MetaEditorで直接実行されます。結果のONNXモデルは同じPythonサブフォルダに保存され、EAのコンパイル中にリソースとして使用されます。

# Copyright 2023, MetaQuotes Ltd.
# https://www.mql5.com

# python libraries
import MetaTrader5 as mt5
import tensorflow as tf
import numpy as np
import pandas as pd
import tf2onnx

# input parameters
inp_model_name = "model.eurusd.H1.120.onnx"
inp_history_size = 120

if not mt5.initialize():
    print("initialize() failed, error code =",mt5.last_error())
    quit()

# we will save generated onnx-file near our script to use as resource
from sys import argv
data_path=argv[0]
last_index=data_path.rfind("\\")+1
data_path=data_path[0:last_index]
print("data path to save onnx model",data_path)

# set start and end dates for history data
from datetime import timedelta, datetime
#end_date = datetime.now()
end_date = datetime(2023, 1, 1, 0)
start_date = end_date - timedelta(days=inp_history_size)

# print start and end dates
print("data start date =",start_date)
print("data end date =",end_date)

# get rates
eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, start_date, end_date)

# create dataframe
df = pd.DataFrame(eurusd_rates)

# get close prices only
data = df.filter(['close']).values

# scale data
from sklearn.preprocessing import MinMaxScaler
scaler=MinMaxScaler(feature_range=(0,1))
scaled_data = scaler.fit_transform(data)

# training size is 80% of the data
training_size = int(len(scaled_data)*0.80) 
print("Training_size:",training_size)
train_data_initial = scaled_data[0:training_size,:]
test_data_initial = scaled_data[training_size:,:1]

# split a univariate sequence into samples
def split_sequence(sequence, n_steps):
    X, y = list(), list()
    for i in range(len(sequence)):
       # find the end of this pattern
       end_ix = i + n_steps
       # check if we are beyond the sequence
       if end_ix > len(sequence)-1:
          break
       # gather input and output parts of the pattern
       seq_x, seq_y = sequence[i:end_ix], sequence[end_ix]
       X.append(seq_x)
       y.append(seq_y)
    return np.array(X), np.array(y)

# split into samples
time_step = inp_history_size
x_train, y_train = split_sequence(train_data_initial, time_step)
x_test, y_test = split_sequence(test_data_initial, time_step)

# reshape input to be [samples, time steps, features] which is required for LSTM
x_train =x_train.reshape(x_train.shape[0],x_train.shape[1],1)
x_test = x_test.reshape(x_test.shape[0],x_test.shape[1],1)

# define model
from keras.models import Sequential
from keras.layers import Dense, Activation, Conv1D, MaxPooling1D, Dropout, Flatten, LSTM
from keras.metrics import RootMeanSquaredError as rmse
model = Sequential()
model.add(Conv1D(filters=256, kernel_size=2, activation='relu',padding = 'same',input_shape=(inp_history_size,1)))
model.add(MaxPooling1D(pool_size=2))
model.add(LSTM(100, return_sequences = True))
model.add(Dropout(0.3))
model.add(LSTM(100, return_sequences = False))
model.add(Dropout(0.3))
model.add(Dense(units=1, activation = 'sigmoid'))
model.compile(optimizer='adam', loss= 'mse' , metrics = [rmse()])

# model training for 300 epochs
history = model.fit(x_train, y_train, epochs = 300 , validation_data = (x_test,y_test), batch_size=32, verbose=2)

# evaluate training data
train_loss, train_rmse = model.evaluate(x_train,y_train, batch_size = 32)
print(f"train_loss={train_loss:.3f}")
print(f"train_rmse={train_rmse:.3f}")

# evaluate testing data
test_loss, test_rmse = model.evaluate(x_test,y_test, batch_size = 32)
print(f"test_loss={test_loss:.3f}")
print(f"test_rmse={test_rmse:.3f}")

# save model to ONNX
output_path = data_path+inp_model_name
onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path)
print(f"saved model to {output_path}")

# finish
mt5.shutdown()


ONNXモデルベースのEAのテスト

ここで、ストラテジーテスターの履歴データでEAをテストしてみましょう。モデルの訓練に使用したのと同じパラメータを指定します(:EURUSD銘柄とH1時間枠)。

テスト間隔には訓練期間は含まれません。年初(2023年1月1日)から開始されます。 

EAテスト設定


ストラテジーによれば、シグナルは各時間の開始時に1回チェックされます(EAは新しいバーの出現を監視します)。したがって、ティックモデリングモードは重要ではありません。OnTickはテスターでバーごとに1回処理されます。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- check new day
   if(TimeCurrent()>=ExtNextDay)
     {
      GetMinMax();
      //--- set next day time
      ExtNextDay=TimeCurrent();
      ExtNextDay-=ExtNextDay%PeriodSeconds(PERIOD_D1);
      ExtNextDay+=PeriodSeconds(PERIOD_D1);
     }

//--- check new bar
   if(TimeCurrent()<ExtNextBar)
      return;
//--- set next bar time
   ExtNextBar=TimeCurrent();
   ExtNextBar-=ExtNextBar%PeriodSeconds();
   ExtNextBar+=PeriodSeconds();
//--- check min and max
   float close=(float)iClose(_Symbol,_Period,0);
   if(ExtMin>close)
      ExtMin=close;
   if(ExtMax<close)
      ExtMax=close;

//--- predict next price
   PredictPrice();
//--- check trading according to prediction
   if(ExtPredictedClass>=0)
      if(PositionSelect(_Symbol))
         CheckForClose();
      else
         CheckForOpen();
  }


この処理により、3か月の期間テストはわずか数秒で完了します。結果は以下のとおりです。

EAテスト結果



ここで、シグナルによるポジションオープンとストップロス(SL)またはテイクプロフィット(TP)による決済を可能にするように取引戦略を変更しましょう。

input double InpLots       = 1.0;    // Lots amount to open position
input bool   InpUseStops   = true;   // Use stops in trading
input int    InpTakeProfit = 500;    // TakeProfit level
input int    InpStopLoss   = 500;    // StopLoss level

//+------------------------------------------------------------------+
//| Check for open position conditions                               |
//+------------------------------------------------------------------+
void CheckForOpen(void)
  {
   ENUM_ORDER_TYPE signal=WRONG_VALUE;
//--- check signals
   if(ExtPredictedClass==PRICE_DOWN)
      signal=ORDER_TYPE_SELL;    // sell condition
   else
     {
      if(ExtPredictedClass==PRICE_UP)
         signal=ORDER_TYPE_BUY;  // buy condition
     }

//--- open position if possible according to signal
   if(signal!=WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
     {
      double price,sl=0,tp=0;
      double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID);
      double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      if(signal==ORDER_TYPE_SELL)
        {
         price=bid;
         if(InpUseStops)
           {
            sl=NormalizeDouble(bid+InpStopLoss*_Point,_Digits);
            tp=NormalizeDouble(ask-InpTakeProfit*_Point,_Digits);
           }
        }
      else
        {
         price=ask;
         if(InpUseStops)
           {
            sl=NormalizeDouble(ask-InpStopLoss*_Point,_Digits);
            tp=NormalizeDouble(bid+InpTakeProfit*_Point,_Digits);
           }
        }
      ExtTrade.PositionOpen(_Symbol,signal,InpLots,price,sl,tp);
     }
  }
//+------------------------------------------------------------------+
//| Check for close position conditions                              |
//+------------------------------------------------------------------+
void CheckForClose(void)
  {
//--- position should be closed by stops
   if(InpUseStops)
      return;

   bool bsignal=false;
//--- position already selected before
   long type=PositionGetInteger(POSITION_TYPE);
//--- check signals
   if(type==POSITION_TYPE_BUY && ExtPredictedClass==PRICE_DOWN)
      bsignal=true;
   if(type==POSITION_TYPE_SELL && ExtPredictedClass==PRICE_UP)
      bsignal=true;

//--- close position if possible
   if(bsignal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
     {
      ExtTrade.PositionClose(_Symbol,3);
      //--- open opposite
      CheckForOpen();
     }
  }

InpUseStops=trueは、SLとTPレベルがポジションを開くときに設定されることを意味します。

SL/TPを使用したEA入力パラメータ


同じ期間のSL/TPレベルでのテスト結果:

SLおよびTPレベルを使用したEAのテスト結果


EAの完全なソースコードと訓練済みモデル(2023年の初めまで)は、添付ファイルにあります。


結論

この記事では、MQL5プログラムでONNXモデルを使用するのに難しいことは何もないことが分かります。実際、モデルの適用は最も簡単な部分ですが、適切なONNXモデルを取得するのははるかに困難です。

この記事で使用されているモデルは、MQL5言語を使用してONNXモデルを操作する方法を示すために、デモンストレーションのみを目的として提供されているので、ご注意ください。この記事で紹介するEAは、実際の取引を目的としたものではありません。

MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/12373

添付されたファイル |
MQL5.zip (1243.88 KB)
MQL5でONNXモデルをアンサンブルする方法の例 MQL5でONNXモデルをアンサンブルする方法の例
ONNX (Open Neural Network eXchange)は、ニューラルネットワークを表現するために構築されたオープンフォーマットです。この記事では、1つのエキスパートアドバイザー(EA)で2つのONNXモデルを同時に使用する方法を示します。
ニューラルネットワークが簡単に(第36回):関係強化学習 ニューラルネットワークが簡単に(第36回):関係強化学習
前回の記事で説明した強化学習モデルでは、元のデータ内のさまざまなオブジェクトを識別できる畳み込みネットワークのさまざまなバリアントを使用しました。畳み込みネットワークの主な利点は、場所に関係なくオブジェクトを識別できることです。同時に、畳み込みネットワークは、オブジェクトやノイズのさまざまな変形がある場合、常にうまく機能するとは限りません。これらは、関係モデルが解決できる問題です。
母集団最適化アルゴリズム:ハーモニーサーチ(HS) 母集団最適化アルゴリズム:ハーモニーサーチ(HS)
今回は、完璧な音のハーモニーを見つける過程に着想を得た、最も強力な最適化アルゴリズムであるハーモニーサーチ(HS)を研究し、検証してみます。私たちの評価でトップになるのはどのアルゴリズムでしょうか。
ビル・ウィリアムズのMFIによる取引システムの設計方法を学ぶ ビル・ウィリアムズのMFIによる取引システムの設計方法を学ぶ
これは、人気のあるテクニカル指標に基づいて取引システムを設計する方法を学ぶための連載の新しい記事です。今回はビル・ウィリアムズの「マーケット・ファシリテーション・インデックス(BW MFI、Bill Williams' Market Facilitation Index)」を取り上げます。