English Русский 中文 Español Deutsch Português
preview
古典的な戦略を再構築する(第5回):USDZARの多銘柄分析

古典的な戦略を再構築する(第5回):USDZARの多銘柄分析

MetaTrader 5 | 22 10月 2024, 09:46
115 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

はじめに

AIを取引戦略に組み込む方法は無数にありますが、残念ながら、どの戦略に資本を託すかを決める前に、それぞれの戦略を評価することはできません。本日は、人気の高い取引戦略である多銘柄分析について再考し、AIを使って戦略を改善できるかどうかを検証します。この戦略が読者の投資家プロファイルに適しているかどうかを、十分な情報に基づいた判断を下すために必要な情報を提供します。


取引戦略の概要

複数の銘柄を分析する取引戦略は、主にそれらの銘柄間に見られる相関関係に基づいています。相関関係とは、2つの変数間の線形依存関係の尺度です。相関関係はしばしば2つの変数の関係を示していると誤解されますが、必ずしもそうとは限りません。   

世界中のトレーダーが、投資判断の指針やリスクレベルの測定、さらにはエグジットシグナルとして、相関資産の基本的理解を活用してきました。例えば、USDZARの通貨ペアを考えてみましょう。米政府は世界有数の石油輸入国であり、一方、南アフリカ政府は世界最大の金輸出国です。

これらの商品は両国の国内総生産に大きな割合を占めているため、当然、これらの商品の価格水準がUSDZAR通貨ペアの変動の一部を説明する可能性があると予想されます。つまり、現物市場で原油が金よりも良好なパフォーマンスを示している場合、ドルがランドより強くなることが予想され、逆の場合も同様です。


方法論の概要

この関係を評価するために、MQL5で書かれたスクリプトを使用して、MetaTrader 5端末からすべての市場データをエクスポートしました。モデルに対する2つの可能な入力グループを使って様々なモデルを訓練しました。

  1. USDZARの通常のOHLC相場
  2. 原油価格と金価格の組み合わせ

収集されたデータから、原油は金よりもUDZAR通貨ペアとの相関水準が強いようです。

データのスケールが異なっていたため、訓練前にデータを標準化および正規化しました。異なる入力セット間で精度を比較するために、ランダムシャッフルなしで10分割交差検証を実施しました。

結果は、最初のグループが最も誤差が少ないことを示唆しています。最も優れたモデルは、通常のOHLCデータを用いた線形回帰でした。しかし、後者のグループ2において、最も良いパフォーマンスを示したモデルはKNeigborsRegressorアルゴリズムでした。

モデルの5つのパラメータについて、ランダムサーチを500回繰り返し、ハイパーパラメータのチューニングに成功しました。過剰適合を検証するために、最適化中に保持した検証セットで、カスタマイズしたモデルの誤差レベルとデフォルトモデルの誤差レベルを比較しました。両モデルを同等の訓練セットで訓練した結果、検証セットにおいてデフォルトモデルを上回るパフォーマンスを発揮しました。

最後に、カスタマイズしたモデルをONNX形式にエクスポートし、MQL5のEAに統合しました。


データ抽出

MetaTrader 5端末から必要なデータを抽出するのに便利なスクリプトを作成しました。スクリプトを希望の銘柄にドラッグ&ドロップするだけで、データが抽出されパス「\MetaTrader 5\MQL5\Files\..”」に配置されます。

//+------------------------------------------------------------------+
//|                                                      ProjectName |
//|                                      Copyright 2020, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Zororo Ndawana"
#property link      "https://www.mql5.com/ja/users/gamuchiraindawa"
#property version   "1.00"
#property script_show_inputs

//---Amount of data requested
input int size = 100000; //How much data should we fetch?

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
//---File name
   string file_name = "Market Data " + Symbol() + ".csv";

//---Write to file
   int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");

   for(int i= size;i>=0;i--)
     {
      if(i == size)
        {
         FileWrite(file_handle,"Time","Open","High","Low","Close");
        }

      else
        {
         FileWrite(file_handle,iTime(Symbol(),PERIOD_CURRENT,i),
                   iOpen(Symbol(),PERIOD_CURRENT,i),
                   iHigh(Symbol(),PERIOD_CURRENT,i),
                   iLow(Symbol(),PERIOD_CURRENT,i),
                   iClose(Symbol(),PERIOD_CURRENT,i));
        }
     }

  }
//+------------------------------------------------------------------+


Pythonによる探索的データ分析

まずは標準ライブラリをインポートします。

#Libraries
import pandas as pd
import numpy as np
import seaborn as sns

次に先ほど抽出したデータを読み込みます。

#Dollar VS Rand
USDZAR = pd.read_csv("/home/volatily/market_data/Market Data USDZAR.csv")
#US Oil
USOIL = pd.read_csv("/home/volatily/market_data/Market Data US Oil.csv")
#SA Gold
SAGOLD = pd.read_csv("/home/volatily/market_data/Market Data XAUUSD.csv")

データを検査します。

USOIL

USDZARオイル

図1:時間的に逆方向に進んでいるデータ

タイムスタンプが現在に近いところから過去に向かって進んでいます。これは、機械学習のタスクには望ましくありません。過去ではなく未来を予測するために、データの順序を逆にします。

#Format the data
USDZAR = USDZAR[::-1]
USOIL = USOIL[::-1]
SAGOLD = SAGOLD[::-1]

データセットを結合する前に、まずデータセットがすべて日付列をインデックスとして使っていることを確認しましょう。そうすることで、すべてのデータセットが共有する日だけを、正しい時系列で選択することができます。

#Set the indexes
USOIL = USOIL.set_index("Time")
SAGOLD = SAGOLD.set_index("Time")
USDZAR = USDZAR.set_index("Time")

データセットを結合します。

#Merge the dataframes
merged_df = pd.merge(USOIL,SAGOLD,how="inner",left_index=True,right_index=True,suffixes=(" US OIL"," SA GOLD"))
merged_df = pd.merge(merged_df,USDZAR,how="inner",left_index=True,right_index=True)

予測期間を定義します。

#Define the forecast horizon
look_ahead = 10

ターゲットはUSDZARペアの将来の終値とし、可視化のためにバイナリターゲットも含めます。

#Label the data
merged_df["Target"] = merged_df["Close"].shift(-look_ahead)
merged_df["Binary Target"] = 0
merged_df.loc[merged_df["Close"] < merged_df["Target"],"Binary Target"] = 1

空の行はすべて削除します。

#Drop empty rows
merged_df.dropna(inplace=True)

相関レベルを観察します。

#Let's observe the correlation levels
merged_df.corr()

データセットの相関レベル

図2:データセットの相関レベル

原油はUSD/ZAR通貨ペアとおおよそ-0.4の相関レベルを示しており、比較的強い相関関係が見られます。一方、金はこの通貨ペアとの相関レベルが約0.1と比較的弱いことがわかります。相関関係が常に変数間の関係を示すわけではないことを忘れないことが重要です。時には、相関が両方の変数に影響を与える共通の原因から生じることもあります。

例えば、歴史的に金とドルの関係は逆転していました。ドルが下落するたびに、トレーダーはドルから資金を引き上げ、代わりに金に投資しました。このため、歴史的にドルのパフォーマンスが悪いときには金価格が上昇する傾向がありました。つまり、この単純な例では、両方の市場に参加しているトレーダーが共通の原因ということになります。

散布図は2つの変数の関係を可視化するのに役立つので、金価格に対する原油価格の散布図を作成し、USDZARの価格水準が上昇(赤)したか下落(緑)したかによってポイントを色分けしました。見てわかるように、データに明確な分離レベルはありません。実際、作成された散布図はどれも強い関係を示唆していません。

原油価格と金価格の散布図

図3:原油価格と金価格の散布図

原油価格とUSDZARの終値の散布図

図4:原油価格とUSDZARの終値の散布図

USDZAR終値と金価格の散布図

図5:USDZAR終値と金価格の散布図


関係のモデル化

交差検証をおこなうために、データセットのインデックスをリセットします。

#Reset the index
merged_df.reset_index(inplace=True)

次に、データの関係をモデル化するために必要なライブラリをインポートします。

#Import the libraries we need
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Lasso
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.ensemble import AdaBoostRegressor
from sklearn.ensemble import BaggingRegressor
from sklearn.neighbors import KNeighborsRegressor
from sklearn.svm import LinearSVR
from sklearn.neural_network import MLPRegressor
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import root_mean_squared_error
from sklearn.preprocessing import RobustScaler

予測因子とターゲットを定義します。

#Define the predictors
normal_predictors = ["Open","High","Low","Close"]
oil_gold_predictors = ["Open US OIL","High US OIL","Low US OIL","Close US OIL","Open SA GOLD","High SA GOLD","Low SA GOLD","Close SA GOLD"]
target = "Target"

データをスケーリングします。

#Scale the data
all_predictors = normal_predictors + oil_gold_predictors
scaler = RobustScaler()
scaled_data = pd.DataFrame(scaler.fit_transform(merged_df.loc[:,all_predictors]),columns=all_predictors,index=np.arange(0,merged_df.shape[0]))

モデルを初期化します。

#Now prepare the models
models = [
        LinearRegression(),
        Lasso(),
        GradientBoostingRegressor(),
        RandomForestRegressor(),
        AdaBoostRegressor(),
        BaggingRegressor(),
        KNeighborsRegressor(),
        LinearSVR(),
        MLPRegressor(hidden_layer_sizes=(10,5),early_stopping=True),
        MLPRegressor(hidden_layer_sizes=(50,15),early_stopping=True)
]

columns = [
        "Linear Regression",
        "Lasso",
        "Gradient Boosting Regressor",
        "Random Forest Regressor",
        "AdaBoost Regressor",
        "Bagging Regressor",
        "KNeighbors Regressor",
        "Linear SVR",
        "Small Neural Network",
        "Large Neural Network"
]

時系列交差検証オブジェクトをインスタンス化します。

#Prepare the time-series split object
splits = 10
tscv = TimeSeriesSplit(n_splits=splits,gap=look_ahead)

誤差レベルを保存するデータフレームを作成します。

#Prepare the dataframes to store the error levels
normal_error = pd.DataFrame(columns=columns,index=np.arange(0,splits))
new_error = pd.DataFrame(columns=columns,index=np.arange(0,splits))

ネストされたforループを使って交差検証を実行します。最初のループはモデルのリストを繰り返し、2番目のループは各モデルをクロス検証し、誤差レベルを保存します。

#First we iterate over all the models we have available
for j in np.arange(0,len(models)):
        #Now we have to perform cross validation with each model
        for i,(train,test) in enumerate(tscv.split(scaled_data)):
        #Get the data
        X_train = scaled_data.loc[train[0]:train[-1],oil_gold_predictors]
        X_test = scaled_data.loc[test[0]:test[-1],oil_gold_predictors]
        y_train = merged_df.loc[train[0]:train[-1],target]
        y_test = merged_df.loc[test[0]:test[-1],target]
        #Fit the model
        models[j].fit(X_train,y_train)
        #Measure the error
        new_error.iloc[i,j] = root_mean_squared_error(y_test,models[j].predict(X_test))

通常のモデル入力を用いた誤差レベルです。

normal_error

OHLC予測因子を使って予測したときの誤差レベル

図6:OHLC予測因子を使って予測した場合の誤差レベル

titleOHLC予測因子を使って予測した場合の誤差レベル IItitlealtOHLC予測因子を使って予測した場合の誤差レベル II.alt

図7:OHLC予測因子を使って予測した場合の誤差レベル II

では、原油価格と金価格だけを使った場合の誤差レベルを見てみましょう。

new_error

原油価格と金価格を用いて予測した場合の精度レベル

図8:原油価格と金価格を用いた予測の精度レベル

原油価格と金価格を使って予測した場合の精度レベル II.

図9:原油価格と金価格を用いて予測した場合の精度レベル II

通常の予測因子を使った各モデルの平均パフォーマンスを見てみましょう。

#Let's see our average performance on the normal dataset
for i in (np.arange(0,normal_error.shape[0])):
        print(f"{models[i]} normal error {((normal_error.iloc[:,i].mean()))}")
LinearRegression() normal error 0.01136361865358375
Lasso() normal error 0.11138143304314707
GradientBoostingRegressor() normal error 0.03472997520534606
RandomForestRegressor() normal error 0.03616484012058101
AdaBoostRegressor() normal error 0.037484107657877755
BaggingRegressor() normal error 0.03670486223028821
KNeighborsRegressor() normal error 0.035113189373409175
LinearSVR() normal error 0.01085610361276552
MLPRegressor(early_stopping=True, hidden_layer_sizes=(10, 5)) normal error 2.558754334716706
MLPRegressor(early_stopping=True, hidden_layer_sizes=(50, 15)) normal error 1.0544369296125597

次に、新しい予測因子を用いて平均的なパフォーマンスを評価します。

#Let's see our average performance on the new dataset
for i in (np.arange(0,normal_error.shape[0])):
        print(f"{models[i]} normal error {((new_error.iloc[:,i].mean()))}")

LinearRegression() normal error 0.13404065973045615
Lasso() normal error 0.11138143304314707
GradientBoostingRegressor() normal error 0.0893855335909897
RandomForestRegressor() normal error 0.08957454602573789
AdaBoostRegressor() normal error 0.08658796789785872
BaggingRegressor() normal error 0.08887059320664067
KNeighborsRegressor() normal error 0.07696901077705855
LinearSVR() normal error 0.15463529064256165
MLPRegressor(early_stopping=True, hidden_layer_sizes=(10, 5)) normal error 3.8970873719426784
MLPRegressor(early_stopping=True, hidden_layer_sizes=(50, 15)) normal error 0.6958177634524169

精度の変化を観察してみましょう。

#Let's see our average performance on the normal dataset
for i in (np.arange(0,normal_error.shape[0])):
    print(f"{models[i]} changed by {((normal_error.iloc[:,i].mean()-new_error.iloc[:,i].mean()))/normal_error.iloc[:,i].mean()}%")
LinearRegression() changed by -10.795596439535894%
Lasso() changed by 0.0%
GradientBoostingRegressor() changed by -1.573728690057642%
RandomForestRegressor() changed by -1.4768406476311784%
AdaBoostRegressor() changed by -1.3099914419240863%
BaggingRegressor() changed by -1.421221271695885%
KNeighborsRegressor() changed by -1.1920256220116057%
LinearSVR() changed by -13.244087580439862%
MLPRegressor(early_stopping=True, hidden_layer_sizes=(10, 5)) changed by -0.5230408480672479%
MLPRegressor(early_stopping=True, hidden_layer_sizes=(50, 15)) changed by 0.34010489967561475%


特徴量の選択

原油と金の予測子から得られた最も優れたモデルはKNeighbors回帰です。どの特徴量が最も重要であるかを見てみましょう。

#Our best performing model was the KNeighbors Regressor
#Let us perform feature selection to test how stable the relationship is
from mlxtend.feature_selection import SequentialFeatureSelector as SFS

モデルの新しいインスタンスを作成します。

#Let us select our best model
model = KNeighborsRegressor()

前方選択を使用して、モデルにとって最も重要な特徴量を特定します。次に、モデルにすべての予測因子に一度にアクセスできるようにします。

#Create the sequential selector object
sfs1 = SFS(
        model,
        k_features=(1,len(all_predictors)),
        forward=True,
        scoring="neg_mean_squared_error",
        cv=10,
        n_jobs=-1
)

逐次特徴量選択器の適合

#Fit the sequential selector
sfs1 = sfs1.fit(scaled_data.loc[:,all_predictors],merged_df.loc[:,"Target"])
アルゴリズムによって選択された最良の特徴量を観察すると、石油や金の価格はUSDZARの予測にはあまり役立たないと結論付けるかもしれません。なぜなら、アルゴリズムはUSDZARの始値、安値、終値の3つの特徴量だけを選択したからです。
#Now let us see which predictors were selected
sfs1.k_feature_names_
('Open', 'Low', 'Close')


ハイパーパラメータの調整

scikit-learnのRandomizedSearchCVモジュールを使用して、ハイパーパラメータのチューニングをおこなってみましょう。このアルゴリズムは、完全にサンプリングするには大きすぎる応答曲面をサンプリングするのに役立ちます。多数のパラメータを持つモデルを使う場合、入力の組み合わせの総計はかなり速い速度で増えていきます。したがって、多くの可能な値を持つ多くのパラメータを扱う場合には、ランダムサーチアルゴリズムが好ましくなります。

このアルゴリズムでは、許容する反復回数を調整することで、結果の精度と計算時間のトレードオフをコントロールできます。アルゴリズムのランダムな性質のため、この記事で示された結果を正確に再現するのは難しいかもしれません。

Import the scikit-learn module.
#Now we will load the libraries we need
from sklearn.model_selection import RandomizedSearchCV

専用の訓練セットとテストセットを準備します。

#Let us see if we can tune the model
#First we will create train test splits
train_X = scaled_data.loc[:(scaled_data.shape[0]//2),:]
train_y = merged_df.loc[:(merged_df.shape[0]//2),"Target"]

test_X = scaled_data.loc[(scaled_data.shape[0]//2):,:]
test_y = merged_df.loc[(merged_df.shape[0]//2):,"Target"]

パラメータチューニングをおこなうためには、scikit-learnインターフェイスを実装した推定器を渡し、その後に推定器のパラメータに対応するキーと、各パラメータの許可される入力範囲に対応する値を含む辞書を渡す必要があります。そこから、5分割交差検証を実施したいことを指定し、次にスコアリングメトリックとして負の平均二乗誤差を指定する必要があります。

#Create the tuning object
rs = RandomizedSearchCV(KNeighborsRegressor(n_jobs=-1),{
        "n_neighbors": [1,2,3,4,5,8,10,16,20,30,60,100],
        "weights":["uniform","distance"],
        "leaf_size":[1,2,3,4,5,10,15,20,40,60,90],
        "algorithm":["ball_tree","kd_tree"],
        "p":[1,2,3,4,5,6,7,8]
},cv=5,n_iter=500,return_train_score=False,scoring="neg_mean_squared_error")

訓練セットでパラメータチューニングをおこないます。

#Let's perform the hyperparameter tuning
rs.fit(train_X,train_y)

得られた結果を、良いものから悪いものまで見ていきます。

#Let's store the results from our hyperparameter tuning
tuning_results = pd.DataFrame(rs.cv_results_)
tuning_results.loc[:,["param_n_neighbors","param_weights","param_leaf_size","param_algorithm","param_p","mean_test_score"]].sort_values(by="mean_test_score",ascending=False)

最良モデルのチューニングした結果

図10:最良のモデルをチューニングした結果

以下が、見つかった最良のパラメータです。

#The best parameters we came across
rs.best_params_

{'weights': 'distance',
 'p':1,
 'n_neighbors':4,
 'leaf_size':15,
 'algorithm': 'ball_tree'}


過剰適合の確認

カスタマイズモデルとデフォルトモデルを比較する準備をしましょう。両モデルは同じ訓練セットで学習します。検証セットにおいてデフォルトモデルがカスタマイズモデルを上回った場合、訓練データが過剰適合している可能性があります。しかし、カスタマイズモデルがより良い結果を出す場合、これは過剰適合せずにモデルパラメータのチューニングに成功したことを示唆しているかもしれません。

#Create instances of the default model and the custmoized model
default_model = KNeighborsRegressor()
customized_model = KNeighborsRegressor(p=rs.best_params_["p"],weights=rs.best_params_["weights"],n_neighbors=rs.best_params_["n_neighbors"],leaf_size=rs.best_params_["leaf_size"],algorithm=rs.best_params_["algorithm"])

デフォルトモデルの精度を測ってみましょう。

#Measure the accuracy of the default model
default_model.fit(train_X,train_y)
root_mean_squared_error(test_y,default_model.predict(test_X))
0.06633226373900612

次に、カスタマイズされたモデルの精度です。

#Measure the accuracy of the customized model
customized_model.fit(train_X,train_y)
root_mean_squared_error(test_y,customized_model.predict(test_X))
0.04334616246844129

過剰適合することなく、うまくモデルをチューニングできたようです。それでは、カスタマイズしたモデルをONNX形式にエクスポートする準備をしましょう。


ONNX形式へのエクスポート

Open Neural Network Exchange (ONNX)は、言語にとらわれずに機械学習モデルを構築展開するための相互運用可能なフレームワークです。ONNXを使用することで、ONNX APIをサポートしている言語であれば、どのようなプログラミング言語でも機械学習モデルを簡単に使用することができます。本稿執筆時点では、ONNX APIは世界最大の企業で構成されるコンソーシアムによって開発維持されています。

Import the libraries we need
#Let's prepare to export the customized model to ONNX format
import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

MetaTrader 5端末で再現できるように、データがスケーリングされ、正規化されていることを確認する必要があります。したがって、後で端末でいつでも実行できる標準的な変換を実行します。各列の平均値を引いて、データの中心を決めます。そして、各値をその列の標準偏差で割ります。こうすることで、モデルが異なるスケールの変数間の変化をよりよく理解できるようになります。

#Train the model on all the data we have
#But before doing that we need to first scale the data in a way we can repeat in MQL5
scale_factors = pd.DataFrame(columns=all_predictors,index=["mean","standard deviation"])
for i in np.arange(0,len(all_predictors)):
        scale_factors.iloc[0,i] = merged_df.loc[:,all_predictors[i]].mean()
        scale_factors.iloc[1,i] = merged_df.loc[:,all_predictors[i]].std()
scale_factors

スケーリング係数

図12:データのスケーリングと標準化に使用する値の抜粋

正規化と標準化をおこないます。

for i in all_predictors:
        merged_df.loc[:,i] = (merged_df.loc[:,i] - merged_df.loc[:,i].mean()) / merged_df.loc[:,i].std()

では、データを見てみましょう。

merged_df

スケールデータ

図11:スケーリング後のデータの抜粋

カスタマイズしたモデルを初期化します。

customized_model = KNeighborsRegressor(p=rs.best_params_["p"],weights=rs.best_params_["weights"],n_neighbors=rs.best_params_["n_neighbors"],leaf_size=rs.best_params_["leaf_size"],algorithm=rs.best_params_["algorithm"])
customized_model.fit(merged_df.loc[:,all_predictors],merged_df.loc[:,"Target"])

モデルの入力形状を定義します。

#Define the input shape and type
initial_type = [("float_tensor_type",FloatTensorType([1,train_X.shape[1]]))]

ONNX表現を作成します。

#Create an ONNX representation
onnx_model = convert_sklearn(customized_model,initial_types=initial_type)

ONNXモデルを保存します。

#Store the ONNX model
onnx_model_name = "USDZAR_FLOAT_M1.onnx"
onnx.save_model(onnx_model,onnx_model_name)


ONNXモデルの可視化

Netronは、機械学習モデルのためのオープンソースのビジュアライザーです。NetronはONNX以外にもKerasのような様々なフレームワークへのサポートを拡張しています。netronを使って、ONNXモデルが期待通りの入出力形状を持つことを確認します。

netronモジュールをインポートします。

#Let's visualize the model in netron
import netron

これでnetronを使ってモデルを可視化できます。

#Run netron
netron.start(onnx_model_name)

ONNXモデルのメタデータ

図12:ONNXモデルの仕様

ONNXモデルの構造

図13:ONNXモデルの構造

ONNXモデルは期待に応えており、入出力の形状は期待するところに正確に収まっています。次に、ONNXモデルの上にEAを構築します。


MQL5での実装

まずONNXモデルをアプリケーションに統合することから始めましょう。ONNXファイルをリソースとして指定することで、ONNXファイルは.ex5の拡張子でコンパイルされたプログラムに含まれます。

//+------------------------------------------------------------------+
//|                                                       USDZAR.mq5 |
//|                                        Gamuchirai Zororo Ndawana |
//|                          https://www.mql5.com/ja/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Zororo Ndawana"
#property link      "https://www.mql5.com/ja/gamuchiraindawa"
#property version   "1.00"

//+-----------------------------------------------------------------+
//| Require the ONNX file                                           |
//+-----------------------------------------------------------------+
#resource "\\Files\\USDZAR_FLOAT_M1.onnx" as const uchar onnx_model_buffer[];

では、取引ライブラリをインポートしましょう。

//+-----------------------------------------------------------------+
//| Libraries we need                                               |
//+-----------------------------------------------------------------+
#include <Trade/Trade.mqh>
CTrade Trade;

エンドユーザーがコントロールできる入力を定義しましょう。

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
double input sl_width = 0.4;              //How tight should our stop loss be?
int input lot_multiple = 10;              //How many times bigger than minimum lot should we enter?
double input max_risk = 10;               //After how much profit/loss should we close?

次に、いくつかのグローバル変数が必要です。

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
long onnx_model;                          //Our onnx model
double mean_values[12],std_values[12];    //The scaling factors we used for our data
vector model_inputs = vector::Zeros(12);  //Our model's inputs
vector model_forecast = vector::Zeros(1); //Our model's output
double bid,ask;                           //Market prices
double minimum_volume;                    //Smallest lot size
double state = 0;                         //0 means we have no positions, 1 means we have buy position, 2 means we have sell position.

ここで、繰り返し実行する必要のあるタスクのためのヘルパー関数を定義してみましょう。まず、リスクレベルをコントロールしましょう。利益と損失の合計が、定義したリスクレベルを超えた場合、自動的にポジションを決済します。

//+------------------------------------------------------------------+
//| Check if we have reached our risk level                          |
//+------------------------------------------------------------------+
void check_risk_level(void)
  {
//--- Check if we have surpassed our maximum risk level
   if(MathAbs(PositionGetDouble(POSITION_PROFIT)) > max_risk)
     {
      //--- We should close our positions
      Trade.PositionClose("USDZAR");
     }
  }

せっかく統合AIシステムがあるのだから、それを使って反転を検知してみましょう。システムが、価格が当社に対して不利に動くと予測した場合、ポジションを決済し、エンドユーザーに反転の可能性が検出されたことを警告します。

//+------------------------------------------------------------------+
//| Check if there is a reversal may be coming                       |
//+------------------------------------------------------------------+
void check_reversal(void)
  {
   if(((state == 1) && (model_forecast[0] < iClose("USDZAR",PERIOD_M1,0))) ||((state == 2) && (model_forecast[0] > iClose("USDZAR",PERIOD_M1,0))))
     {
      //--- There may be a reversal coming
      Trade.PositionClose("USDZAR");
      //--- Give the user feedback
      Alert("Potential reversal detected");
     }
  }

エントリ機会を見つける関数が必要です。モデルの予測が上位時間枠での価格水準の変化と一致した場合のみ、エントリが有効であると判断します。 

//+------------------------------------------------------------------+
//| Find an entry opportunity                                        |
//+------------------------------------------------------------------+
void find_entry(void)
  {
//---Check for the change in price on higher timeframes
   if(iClose("USDZAR",PERIOD_D1,0) > iClose("USDZAR",PERIOD_D1,21))
     {
      //--- We're looking for buy oppurtunities
      if(model_forecast[0] > iClose("USDZAR",PERIOD_M1,0))
        {
         //--- Open the position
         Trade.Buy(minimum_volume,"USDZAR",ask,(ask - sl_width),(ask + sl_width),"USDZAR AI");
         //--- Update the system state
         state = 1;
        }
     }
//---Check for the change in price on higher timeframes
   else
      if(iClose("USDZAR",PERIOD_D1,0) < iClose("USDZAR",PERIOD_D1,21))
        {
         //--- We're looking for sell oppurtunities
         if(model_forecast[0] < iClose("USDZAR",PERIOD_M1,0))
           {
            //--- Open sell position
            Trade.Sell(minimum_volume,"USDZAR",bid,(bid + sl_width),(bid - sl_width),"USDZAR AI");
            //--- Update the system state
            state = 2;
           }
        }
  }

次に、モデルから予測を取得する関数が必要です。そのためには、まず現在の市場価格を取得し、平均を引いて標準偏差で割ることで変換する必要があります。

//+------------------------------------------------------------------+
//| Obtain a forecast from our model                                 |
//+------------------------------------------------------------------+
void model_predict(void)
  {
//Let's fetch our model's inputs
//--- USDZAR
   model_inputs[0] = ((iOpen("USDZAR",PERIOD_M1,0) - mean_values[0]) / std_values[0]);
   model_inputs[1] = ((iHigh("USDZAR",PERIOD_M1,0) - mean_values[1]) / std_values[1]);
   model_inputs[2] = ((iLow("USDZAR",PERIOD_M1,0) - mean_values[2]) / std_values[2]);
   model_inputs[3] = ((iClose("USDZAR",PERIOD_M1,0) - mean_values[3]) / std_values[3]);
//--- XTI OIL US
   model_inputs[4] = ((iOpen("XTIUSD",PERIOD_M1,0) - mean_values[4]) / std_values[4]);
   model_inputs[5] = ((iHigh("XTIUSD",PERIOD_M1,0) - mean_values[5]) / std_values[5]);
   model_inputs[6] = ((iLow("XTIUSD",PERIOD_M1,0) - mean_values[6]) / std_values[6]);
   model_inputs[7] = ((iClose("XTIUSD",PERIOD_M1,0) - mean_values[7]) / std_values[7]);
//--- GOLD SA
   model_inputs[8] = ((iOpen("XAUUSD",PERIOD_M1,0) - mean_values[8]) / std_values[8]);
   model_inputs[9] = ((iHigh("XAUUSD",PERIOD_M1,0) - mean_values[9]) / std_values[9]);
   model_inputs[10] = ((iLow("XAUUSD",PERIOD_M1,0) - mean_values[10]) / std_values[10]);
   model_inputs[11] = ((iClose("XAUUSD",PERIOD_M1,0) - mean_values[11]) / std_values[11]);
//--- Get a prediction
   OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_forecast);
  }

複数の銘柄を分析するので、気配値表示に追加する必要があります。

//+------------------------------------------------------------------+
//| Load the symbols we need and add them to the market watch        |
//+------------------------------------------------------------------+
void load_symbols(void)
  {
   SymbolSelect("XAUUSD",true);
   SymbolSelect("XTIUSD",true);
   SymbolSelect("USDZAR",true);
  }

スケーリング係数、各列の平均と標準偏差をロードする関数が必要です。

//+------------------------------------------------------------------+
//| Load the scale values                                            |
//+------------------------------------------------------------------+
void load_scale_values(void)
  {
//--- Mean
//--- USDZAR
   mean_values[0] = 18.14360511919699;
   mean_values[1] = 18.145737421580925;
   mean_values[2] = 18.141568574864074;
   mean_values[3] = 18.14362306984525;
//--- XTI US OIL
   mean_values[4] = 80.76956702216644;
   mean_values[5] = 80.7864452112087;
   mean_values[6] = 80.75236177331661;
   mean_values[7] = 80.76923546633206;
//--- GOLD SA
   mean_values[8] = 2430.5180384776245;
   mean_values[9] = 2430.878959640318;
   mean_values[10] = 2430.1509598494354;
   mean_values[11] = 2430.5204140526976;
//--- Standard Deviation
//--- USDZAR
   std_values[0] = 0.11301636249300206;
   std_values[1] = 0.11318116432297631;
   std_values[2] = 0.11288670156099372;
   std_values[3] = 0.11301994613848391;
//--- XTI US OIL
   std_values[4] = 0.9802409859148413;
   std_values[5] = 0.9807944310705999;
   std_values[6] = 0.9802449355481064;
   std_values[7] = 0.9805961626626833;
//--- GOLD SA
   std_values[8] = 26.397404261230328;
   std_values[9] = 26.414599597905003;
   std_values[10] = 26.377605644853944;
   std_values[11] = 26.395208330942864;
  }

最後に、ONNXファイルをロードする関数が必要です。

//+------------------------------------------------------------------+
//| Load the onnx file from buffer                                   |
//+------------------------------------------------------------------+
bool load_onnx_file(void)
  {
//--- Create the model from the buffer
   onnx_model = OnnxCreateFromBuffer(onnx_model_buffer,ONNX_DEFAULT);

//--- The input size for our onnx model
   ulong input_shape [] = {1,12};

//--- Check if we have the right input size
   if(!OnnxSetInputShape(onnx_model,0,input_shape))
     {
      Comment("Incorrect input shape, the model has input shape ",OnnxGetInputCount(onnx_model));
      return(false);
     }

//--- The output size for our onnx model
   ulong output_shape [] = {1,1};

//--- Check if we have the right output size
   if(!OnnxSetOutputShape(onnx_model,0,output_shape))
     {
      Comment("Incorrect output shape, the model has output shape ",OnnxGetOutputCount(onnx_model));
      return(false);
     }

//--- Everything went fine
   return(true);
  }
//+------------------------------------------------------------------+

これらのヘルパー関数を定義したので、EAで使い始めることができます。まず、アプリケーションが初めてロードされたときの動作を定義しましょう。ONNXモデルをロードし、スケーリング係数を準備し、その後市場データを取得します。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Load the onnx file
   if(!load_onnx_file())
     {
      return(INIT_FAILED);
     }

//--- Load our scaling values
   load_scale_values();

//--- Add the symbols we need to the market watch
   load_symbols();

//--- The smallest lotsize we can use
   minimum_volume = SymbolInfoDouble("USDZAR",SYMBOL_VOLUME_MIN) * lot_multiple;

//--- Everything went fine
   return(INIT_SUCCEEDED);
  }

プログラムが停止されるたびに、使わなくなったリソースを解放する必要があります。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Release the resources we used for our onnx model
   OnnxRelease(onnx_model);

//--- Remove the expert advisor
   ExpertRemove();
  }

最後に、価格が変わるたびに、モデルから新しい予測を取得し、更新された市場価格を取得し、新しいポジションを建てるか、現在開いているポジションを管理する必要があります。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- We always need a forecast from our model
   model_predict();
//--- Fetch market prices
   bid = SymbolInfoDouble("USDZAR",SYMBOL_BID);
   ask = SymbolInfoDouble("USDZAR",SYMBOL_ASK);

//--- If we have no open positions, find an entry
   if(PositionsTotal() == 0)
     {
      //--- Find an entry
      find_entry();
      //--- Reset the system state
      state = 0;
     }

//--- If we have open postitions, manage them
   else
     {
      //--- Check for a reveral warning from our AI
      check_reversal();
      //--- Check if we have not reached our max risk levels
      check_risk_level();
     }

  }

これらすべてをまとめると、プログラムの動作を観察することができます。 

EA

図17:EA

システムの動作

図14:EAの入力

システムの動作

図15:プログラムの動作


結論

この記事では、AIを搭載した多銘柄EAを構築する方法を示しました。通常のOHLCデータを使用することで、より低い誤差レベルを得ることができましたが、必ずしもMetaTrader 5端末にあるすべての銘柄に同じことが当てはまるわけではありません。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/15570

添付されたファイル |
USDZAR_FLOAT_M1.onnx (524.58 KB)
USDZAR.ipynb (694.01 KB)
FetchData.mq5 (2.05 KB)
USDZAR.mq5 (10.54 KB)
古典的な戦略を再構築する(第6回):多時間枠分析 古典的な戦略を再構築する(第6回):多時間枠分析
この連載では、古典的な戦略を再検討し、AIを使って改善できるかどうかを検証します。本日の記事では、人気の高い多時間枠分析という戦略を検証し、AIによって戦略が強化されるかどうかを判断します。
MQL5における動的時間伸縮を用いたパターン認識 MQL5における動的時間伸縮を用いたパターン認識
本稿では、金融時系列における予測パターンを特定する手段として、動的時間伸縮の概念について論じます。その仕組みと、純粋なMQL5での実装を紹介します。
どんな市場でも優位性を得る方法(第3回):VISA消費指数 どんな市場でも優位性を得る方法(第3回):VISA消費指数
ビッグデータの世界では、取引戦略を向上させる可能性を秘めた数百万もの代替データセットが存在します。この連載では、最も有益な公共データセットを特定するお手伝いをします。
古典的な戦略を再構築する(第4回):SP500と米財務省中期証券 古典的な戦略を再構築する(第4回):SP500と米財務省中期証券
この連載では、最新のアルゴリズムを用いて古典的な取引戦略を分析し、AIによって戦略を改善できるかどうかを検証します。本日の記事では、SP500と米財務省中期証券との関係を活用した古典的な取引手法を再考します。