English Русский Deutsch
preview
PythonとMQL5を使用して初めてのグラスボックスモデルを作る

PythonとMQL5を使用して初めてのグラスボックスモデルを作る

MetaTrader 5トレーディングシステム | 22 4月 2024, 11:28
107 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

はじめに

グラスボックスアルゴリズムとは、完全に透明で本質的に理解しやすい機械学習アルゴリズムのことです。機械学習における予測精度と解釈可能性はトレードオフの関係にあるという従来の常識を覆し、比類ないレベルの精度と透明性を提供するからです。つまり、私たちがよく知っているブラックボックスアルゴリズムに比べ、デバッグやメンテナンス、反復の改善が飛躍的に容易です。ブラックボックスモデルとは、内部構造が複雑で容易に解釈できない機械学習モデルのことです。これらのモデルは、私たち人間には容易に理解できない、高次元で非線形な関係を表すことができます。

経験則として、ブラックボックスモデルは、グラスボックスモデルが同じレベルの精度を提供できないシナリオでのみ使用されるべきです。この記事では、グラスボックスモデルを構築し、それを採用することの潜在的な利点を理解します。グラスボックスモデルでMetaTrader 5端末を制御する2つの方法を探ります。

  1. レガシーアプローチ:これが最も簡単な方法です。MetaTrader 5に統合されたPythonライブラリを使用して、グラスボックスモデルをMetaTrader 5端末に接続するだけです。そこからMetaQuotes Language 5でエキスパートアドバイザー(EA)を構築し、グラスボックスモデルを支援し、効果を最大化します。
  2. 現代的アプローチ:これは、機械学習モデルをEAに統合するための推奨方法です。グラスボックスモデルをOpen Neural Network Exchange形式に書き出し、そのモデルをリソースとしてEAに直接読み込むことで、MetaTrader 5で利用可能なすべての便利な機能を活用し、グラスボックスモデルのパワーと融合させることができます。

AI

図1:人工知能による人間の脳の模倣


ブラックボックスモデルとグラスボックスモデル

前述のように、従来の機械学習モデルのほとんどは、解釈や説明が難しいです。このクラスのモデルはブラックボックスモデルとして知られています。ブラックボックスモデルは、複雑で解釈しにくい内部構造を持つすべてのモデルを包含します。これは、モデルの主要なパフォーマンス指標を改善しようとする私たちにとって大きな問題となります。一方、グラスボックスモデルは、内部構造が透明でわかりやすく、さらに予測精度も高くて信頼できる機械学習モデルの集合体です。 

Microsoft Researchの研究者、開発者、ドメインエクスパートのチームは、InterpretMLと呼ばれるPythonパッケージをオープンソース化し、この記事を書いている時点ではアクティブに維持しています。このパッケージには、ブラックボックスexplainerとグラスボックスモデルの包括的なスイートが含まれています。ブラックボックスexplainerは、ブラックボックスモデルの内部構造を理解しようとする一連のアルゴリズムです。InterpretMLに含まれるブラックボックスexplainer明アルゴリズムのほとんどは、モデル非依存的です。しかし、これらのブラックボックスexplainerは、ブラックボックスモデルの推定値しか与えることができません。このことがなぜ問題になるかは、この記事の次のセクションで説明します。InterpretMLにはグラスボックスモデルのスイートも含まれており、これらのモデルは、前例のない透明性でブラックボックスモデルの予測精度に匹敵します。これは、初心者であろうと専門家であろうと、機械学習を使用するすべての人に最適です。モデルの解釈可能性の価値は、ドメインや経験レベルを超越したものです。

詳細は次をご覧ください。

1.興味のある方は、InterpretMLドキュメントをご覧ください。

2.さらに、InterpretML白書を読むこともできます。 

本稿ではInterpretMLを使い、pythonでグラスボックスモデルを構築します。グラスボックスモデルが、特徴エンジニアリングプロセスを導き、モデルの内部構造の理解を向上させるために、どのように重要な洞察を与えてくれるかを見ていきます。

ブラックボックスモデルの課題:不一致問題(Disagreement Problem)

ブラックボックスモデルの使用を止めたくなる理由のひとつは、「不一致問題」と呼ばれるものです。一言で言えば、同じモデルを評価する場合でも、説明の手法が異なれば、モデルの説明が大きく異なることがあるのです。説明技法は、ブラックボックスモデルの根本的な構造を洞察しようとするものです。モデルの説明には様々な考え方があり、それぞれの説明手法はモデルの動作の異なる側面に焦点を当てることができるため、基礎となるブラックボックスモデルについてそれぞれ異なる指標を推論することができます。不一致問題は未解決の研究分野であり、認識し、積極的に軽減すべき注意点です。

本稿では、読者がこの現象を独自に観察したことがない場合に備えて、不一致問題の実際のデモンストレーションをみます。

詳細は次をご覧ください。

1.不一致問題についてもっと知りたい方は、ハーバード大学、マサチューセッツ工科大学、ドレクセル大学、カーネギーメロン大学の優秀な卒業生たちによるこの優れた論文を読むことをお勧めします。

では、早速、不一致問題を実際に見てみましょう。

まず、分析に役立つpythonパッケージをインポートします。

#Import MetaTrader5 Python package
#pip install --upgrade MetaTrader5, if you don't have it installed
import MetaTrader5 as mt5

#Import datetime for selecting data
#Standard python package, no installation required
from datetime import datetime

#Plotting Data
#pip install --upgrade matplotlib, if you don't have it installed
import matplotlib.pyplot as plt

#Import pandas for handling data
#pip install --upgrade pandas, if you don't have it installed
import pandas as pd

#Import library for calculating technical indicators
#pip install --upgrade pandas-ta, if you don't have it installed
import pandas_ta as ta

#Scoring metric to assess model accuracy
#pip install --upgrade scikit-learn, if you don't have it installed
from sklearn.metrics import precision_score

#Import mutual information, a black-box explanation technique
from sklearn.feature_selection import mutual_info_classif

#Import permutation importance, another black-box explanation technique
from sklearn.inspection import permutation_importance

#Import our model
#pip install --upgrade xgboost, if you don't have it installed
from xgboost import XGBClassifier

#Plotting model importance
from xgboost import plot_importance

ここからMetaTrader 5端末への接続に移りますが、その前にログイン認証情報を指定する必要があります。

#Enter your account number
login = 123456789

#Enter your password
password = '_enter_your_password_'

#Enter your Broker's server
server = 'Deriv-Demo'

これで、MetaTrader 5端末を初期化し、同じ手順で取引口座にログインできます。

#We can initialize the MT5 terminal and login to our account in the same step
if mt5.initialize(login=login,password=password,server=server):
    print('Logged in successfully')
else:
    print('Failed To Log in')

ログインに成功しました。

MetaTrader 5端末にフルアクセスできるようになり、チャートデータ、ティックデータ、現在の気配値などをリクエストできるようになりました。

#To view all available symbols from your broker
symbols = mt5.symbols_get()

for index,value in enumerate(symbols):
    print(value.name)

Volatility 10 Index

Volatility 25 Index

Volatility 50 Index

Volatility 75 Index

Volatility 100 Index

Volatility 10 (1s) Index

Boom 1000 Index

Boom 500 Index

Crash 1000 Index

Crash 500 Index

Step Index

...

モデル化したい銘柄を特定したら、その銘柄のチャートデータをリクエストできるが、その前に、取り出したい日付の範囲を指定する必要があります。

#We need to specify the dates we want to use in our dataset
date_from = datetime(2019,4,17)
date_to = datetime.now()

これで、その銘柄のチャートデータをリクエストできます。
#Fetching historical data
data = pd.DataFrame(mt5.copy_rates_range('Boom 1000 Index',mt5.TIMEFRAME_D1,date_from,date_to))

プロットするために、データフレームの時間列を書式設定する必要があります。

#Let's convert the time from seconds to year-month-date
data['time'] = pd.to_datetime(data['time'],unit='s')

data

時間変換後のデータフレーム

図2:データフレームでの人間が読める形式での時間表示 - real_volume列がゼロで埋められている

次に、データフレームに新しい機能を追加したり、テクニカル指標を計算したり、データフレームをクリーンアップしたりするのに役立つヘルパー関数を作成する必要があります。

#Let's create a function to preprocess our data
def preprocess(df):
    #All values of real_volume are 0 in this dataset, we can drop the column
    df.drop(columns={'real_volume'},inplace=True) 
    #Calculating 14 period ATR
    df.ta.atr(length=14,append=True)
    #Calculating the growth in the value of the ATR, the second difference
    df['ATR Growth'] = df['ATRr_14'].diff().diff()
    #Calculating 14 period RSI
    df.ta.rsi(length=14,append=True)    
    #Calculating the rolling standard deviation of the RSI
    df['RSI Stdv'] = df['RSI_14'].rolling(window=14).std()
    #Calculating the mid point of the high and low price
    df['mid_point'] = ( ( df['high'] + df['low'] ) / 2 )  
    #We will keep track of the midpoint value of the previous day
    df['mid_point - 1'] = df['mid_point'].shift(1) 
    #How far is our price from the midpoint?
    df['height'] = df['close'] - df['mid_point']  
    #Drop any rows that have missing values
    df.dropna(axis=0,inplace=True)

データフレームの前処理関数を呼び出してみましょう。

preprocess(data)

data

前処理後のデータフレーム

図3:前処理済みのデータフレーム

次の終値が今日の終値より大きいかどうかが目標となります。明日の終値が今日の終値より大きければ、目標は1になります。そうでなければ、目標は0となります。

#We want to predict whether tomorrow's close will be greater than today's close
#We can encode a dummy variable for that: 
#1 means tomorrow's close will be greater.
#0 means today's close will be greater than tomorrow's.

data['target'] = (data['close'].shift(-1) > data['close']).astype(int)

data

#The first date is 2019-05-14, and the first close price is 9029.486, the close on the next day 2019-05-15 was 8944.461
#So therefore, on the first day, 2019-05-14, the correct forecast is 0 because the close price fell the following day.


目標エンコーディング

図4:目標作成

次に、ターゲットと予測因子を明確に定義してから、データを訓練セットとテストセットに分けます。これは時系列データであるため、無作為に2つのグループに分けることはできません。

#Seperating predictors and target
predictors = ['open','high','low','close','tick_volume','spread','ATRr_14','ATR Growth','RSI_14','RSI Stdv','mid_point','mid_point - 1','height']
target     = ['target']

#The training and testing split definition
train_start = 27
train_end = 1000

test_start = 1001

次に、訓練セットとテストセットを作成します。

#Train set
train_x = data.loc[train_start:train_end,predictors]
train_y = data.loc[train_start:train_end,target]

#Test set
test_x = data.loc[test_start:,predictors]
test_y = data.loc[test_start:,target]

これでブラックボックスモデルを適合させることができます。

#Let us fit our model
black_box = XGBClassifier()
black_box.fit(train_x,train_y)

テストセットでのモデルの予測を見てみましょう。

#Let's see our model predictions
black_box_predictions = pd.DataFrame(black_box.predict(test_x),index=test_x.index)

モデルの精度を評価しましょう。

#Assesing model prediction accuracy
black_box_score = precision_score(test_y,black_box_predictions)

#Model precision score
black_box_score

0.4594594594594595

モデルの精度は45%ですが、どの特徴がこれを達成するのに役立っていて、どの特徴が役立っていないのでしょうか。幸いなことに、XGBoostには特徴の重要度を測定する関数が内蔵されており、物事が簡単になっています。しかし、これはXGBoostのこの実装に特有のものであり、すべてのブラックボックスにこのような方法で特徴の重要性を簡単に示す便利な関数が含まれているわけではありません。  例えば、ニューラルネットワークやサポートベクトルマシンにはそれに相当する関数がないので、モデルをよりよく理解するためには、自分でモデルの重みを冷静に分析し、注意深く解釈する必要があります。XGBoostのplot_importance関数を使用すれば、モデルの内部を覗き見ることができます。

plot_importance(black_box)

XGBoost特徴の重要度

図5:XGBClassifierの特徴の重要性 - 表には交互作用項が含まれていないが、これは必ずしもそれらが存在しないということではない

さて、グランドトゥルースを確立したところで、「Permutation Importance」と呼ばれる最初のブラックボックス説明技法を見てみましょう。 Permutation Importanceは、各特徴の値を無作為にシャッフルし、モデルの損失関数の変化を測定することで、各特徴の重要度を推定しようとするものです。モデルがその特徴に依存すればするほど、それらの値を無作為にシャッフルした場合、そのパフォーマンスは悪化するからです。Permutation Importanceの利点と欠点について説明しましょう。

利点

  1. モデルにとらわれない:Permutation Importanceは、モデルやPermutation Importance関数のいずれにも前処理を必要とせず、どのようなブラックボックスモデルにも使用することができます。 
  2. 解釈可能性:Permutation Importanceの結果は解釈しやすく、評価される基礎モデルに関係なく一貫して解釈されます。そのため、使いやすいツールとなっています。
  3. 非線形性に対応:Permutation Importanceが堅牢であり,予測変数と応答との間の非線形関係を捕捉するのに適しています。 
  4. 異常値に対応:Permutation importanceは,予測変数の生の値に依存せず、モデルのパフォーマンスに対する特徴の影響に関係します。このアプローチは、生データに含まれる可能性のある異常値に対して堅牢です。

欠点

  1. 計算コスト:多くの特徴を持つ大規模なデータセットの場合、Permutation Importanceを計算するのは計算コストがかかります。各特徴を繰り返し、順列を計算し、モデルを評価し、次の特徴に移ってそのプロセスを繰り返さなければならないからです。
  2. 相関性のある特徴の課題:Permutation Importanceは、相関の強い特徴を評価するときに偏った結果を与えることがあります。
  3. モデルの複雑さに敏感:Permutation Importanceはモデルを問いませんが、複雑すぎるモデルは、その特徴が並べ替えられたときに高い分散を示す可能性があり、信頼できる結論を導き出すことが難しくなります。
  4. 特徴の独立性:Permutation Importanceは、データセットの特徴が独立であり、結果なしに無作為に並べ替えられることを仮定しています。計算は簡単になりますが、現実の世界ではほとんどの特徴が互いに依存しており、Permutation Importanceでは拾いきれない相互作用があります。 

ブラックボックス分類器のPermutation Importanceを計算してみましょう。

#Now let us observe the disagreement problem
black_box_pi = permutation_importance(black_box,train_x,train_y)

# Get feature importances and standard deviations
perm_importances = black_box_pi.importances_mean
perm_std = black_box_pi.importances_std

# Sort features based on importance
sorted_idx = perm_importances.argsort()

計算されたPermutation Importanceの値をプロットしてみましょう。

#We're going to utilize a bar histogram
plt.barh(range(train_x.shape[1]), perm_importances[sorted_idx], xerr=perm_std[sorted_idx])
plt.yticks(range(train_x.shape[1]), train_x.columns[sorted_idx])
plt.xlabel('Permutation Importance')
plt.title('Permutation Importances')
plt.show()

Permutation Importance

図6:ブラックボックスのPermutation Importance

Permutation Importanceアルゴリズムによる計算によると、ATRの読み取り値は私たちが設計した中で最も情報量の多い特徴ですが、グランドトゥルースから、そうではないことがわかっています。ATRは6位にランクされています。ATR Growthが最も重要な特徴です。2番目に重要な特徴はheightでしたが、Permutation ImportanceではATR Growthの方が重要でした。3番目に重要な特徴はRSIの測定値でしたが、Permutation Importanceではheightがより重要であると算出されました。

これはブラックボックス説明技法の問題点です。ブラックボックス説明技法は、特徴の重要性の推定には非常に適していますが、せいぜい推定に過ぎないため、間違いが生じやすくなります。それだけでなく、同じモデルを評価する際にも意見が食い違うことがあります。自分の目で確かめましょう。

第2のブラックボックス説明技法として、相互情報アルゴリズムを使用します。 相互情報は、ある特徴の価値を認識することによってもたらされる不確実性の減少を測定します。

#Let's see if our black-box explainers will disagree with each other by calculating mutual information
black_box_mi = mutual_info_classif(train_x,train_y)
black_box_mi = pd.Series(black_box_mi, name="MI Scores", index=train_x.columns)
black_box_mi = black_box_mi.sort_values(ascending=False)

black_box_mi

RSI_14:              0.014579

open:                0.010044

low:                  0.005544

mid_point-1:    0.005514

close:                0.002428

tick_volume:    0.001402

high:                 0.000000

spread:             0.000000

ATRr_14:           0.000000

ATR Growth:     0.000000

RSIStdv:          0.000000

mid_point:       0.000000

height:             0.000000

Name:MI Scores, dtype: float64

ご覧のように、重要度ランキングは大きく異なっています。相互情報は、私たちのグランドトゥルースやPermutation Importance計算と比較して、ほぼ逆の順序で特徴量をランク付けしています。もし、この例のようなグランドトゥルースがなかったら、どちらのexplainerに頼ることになるでしょうか。  さらに、5つの異なる説明技法を使用して、それぞれが異なる重要度ランキングを出したとしたらどうするでしょうか。現実世界の仕組みに関する自分の信念に沿ったランキングを選択すれば、確証バイアスと呼ばれる別の問題への扉を開くことになります。確証バイアスとは、自分の既存の信念と矛盾する証拠を無視し、たとえそれが真実でなくても、自分が真実だと信じていることを積極的に正当化しようとすることです。

グラスボックスモデルの利点

グラスボックスモデルは、完全に透明で非常に分かりやすいため、ブラックボックス説明技法の必要性を完全に代替します。これらは、金融領域を含む多くの領域における不一致問題を解決する可能性を秘めています。それだけで十分な理由にならないとすれば、グラスボックスモデルのデバッグは、同レベルの柔軟性を持つブラックボックスモデルのデバッグよりも指数関数的に簡単だということです。これによって、最も重要な資源である時間を節約することができます。そして何より素晴らしいのは、グラスボックスであることでモデルの精度を損なうことがなく、両方の長所を兼ね備えていることです。経験則として、ブラックボックスは、グラスボックスでは同レベルの精度を達成できないシナリオでのみ使用されるべきです。 

さて、最初のグラスボックスモデルを作り、そのパフォーマンスを分析し、精度を向上させることに目を向けましょう。ここから、グラスボックスモデルをMetaTrader 5端末に接続し、グラスボックスモデルを使った取引を開始する方法について説明します。次に、MetaQuotes Language 5を使用して、グラスボックスモデルを支援するEAを構築します。そして最後に、MetaTrader 5とグラスボックスモデルの可能性を最大限に発揮できるように、グラスボックスモデルをOpen Neural Network Exchange形式に書き出します。

Pythonを使用した初めてのグラスボックスモデルの構築は簡単

コードを読みやすくするために、ブラックボックスモデルを構築するために使用したPythonスクリプトとは別のPythonスクリプトでグラスボックスを構築します。ただし、ログイン、データの取得、データの前処理など、ほとんどのことは変わりません。したがって、これらのステップをもう一度説明することはせず、グラスボックスモデル特有のステップのみに焦点を当てることにします。

まずはInterpretMLをインストールします。

#Installing Interpret ML
pip install --upgrade interpret

次に依存関係を読み込みます。この記事では、interpretパッケージの3つのモジュールに焦点を当てます。1つ目はグラスボックスモデルそのものであり、2つ目はモデルの内部を見ることができる便利なモジュールで、この情報をインタラクティブなGUIダッシュボードで表示します。最後のパッケージを使用すると、モデルのパフォーマンスを1つのグラフで視覚化することができます。その他のパッケージについてはすでに述べたとおりです。

#Import MetaTrader5 package
import MetaTrader5 as mt5

#Import datetime for selecting data
from datetime import datetime

#Import matplotlib for plotting
import matplotlib.pyplot as plt

#Intepret glass-box model for classification
from interpret.glassbox import ExplainableBoostingClassifier

#Intepret GUI dashboard utility
from interpret import show

#Visualising our model's performance in one graph
from interpret.perf import ROC

#Pandas for handling data
import pandas as pd

#Pandas-ta for calculating technical indicators
import pandas_ta as ta

#Scoring metric to assess model accuracy
from sklearn.metrics import precision_score

次に、ログイン認証情報を作成し、前回と同じようにMT5端末にログインします。このステップは省略されます。

そこから、先ほどと同じようにモデル化したい銘柄を選択します。このステップは省略されます。

次に、先ほどと同じように、モデル化したいデータの日付範囲を指定します。このステップは省略されます。

そして、前と同じように過去のデータを取得することができます。このステップは省略されます。

そこからは、上で説明したのと同じ前処理ステップを踏みます。このステップは省略されます。

データが前処理されたら、先ほどと同じようにターゲットを追加します。このステップは省略されます。

その後、前回と同じように訓練テストの分割をおこないます。このステップは省略されます。訓練テストの分割がランダムでないことを確認してください。自然な時間順序を守らなければ、結果は損なわれ、将来のパフォーマンスについて過度に楽観的なイメージを描くことになります。

これでグラスボックスモデルが完成しました。

#Let us fit our glass-box model
#Please note this step can take a while, depending on your computational resources
glass_box = ExplainableBoostingClassifier()
glass_box.fit(train_x,train_y)

グラスボックスモデルの内部を見ることができます。

#The show function provides an interactive GUI dashboard for us to interface with out model
#The explain_global() function helps us find what our model found important and allows us to identify potential bias or unintended flaws
show(glass_box.explain_global())


ガラスボックスの大域的な状態

図7:グラスボックスの大域的な状態

要約統計の解釈は非常に重要ですが、その前にまず重要な命名法を確認しておきましょう。「大域的なターム」または「大域的な状態」は、モデル全体の状態を要約します。これは、モデルがどの特徴を有益と判断したかの概要を示してくれます。これは「局所的なターム」や「局所的な状態」と混同してはなりません。局所的な状態は、個々のモデルの予測を説明するために使われ、モデルがなぜその予測をしたのか、どの特徴が個々の予測に影響を与えたのかを理解するのに役立ちます。

グラスボックスモデルの大域的な状態に戻ります。見てわかるように、このモデルは遅行中間点の値が非常に有益であることを発見しました。それだけでなく、ATR Growthと遅行中間点値の間に相互作用項がある可能性も発見しました。heightは3番目に重要な特徴で、終値とheightの交互作用項がそれに続きました。このグラスボックスモデルを理解するために、追加のツールは一切必要ありません。これにより、意見の不一致問題と確証バイアスに対する扉が完全に閉ざされます。大域的な状態の情報は、特徴エンジニアリングにおいて非常に貴重です。より良い機能をエンジニアリングするために、今後の取り組みをどこに向けるべきかが示されているからです。グラスボックスのパフォーマンスを見てみましょう。

グラスボックス予想を入手します。

#Obtaining glass-box predictions
glass_box_predictions = pd.DataFrame(glass_box.predict(test_x))

次にグラスボックスの精度を測定します。

glass_box_score = precision_score(test_y,glass_box_predictions)

glass_box_score

0.49095022624434387

私たちのグラスボックスの精度は49%です。XGBClassifierと比較すると、明らかにExplainable Boosting Classifierの方が自重があります。これは、明瞭度を損なうことなく高い精度を実現するグラスボックスモデルの威力を証明するものです。

また、グラスボックスモデルから各予測に対する個別の説明を取得し、どの特徴がその予測に影響を与えたかを粒度レベルで理解することができます。これらは局所的な説明と呼ばれ、Explainable Boosting Classifierから取得するのは簡単です。

#We can also obtain individual explanations for each prediction
show(glass_box.explain_local(test_x,test_y))

局所的な説明

図8:Explainable Boosting Classifierによる局所的説明

最初のドロップダウンメニューで、それぞれの予測をスクロールし、より理解したい予測を選択することができます。 

そこから、実際のクラスと予測されたクラスを見ることができます。この場合、実際のクラスは0であり、終値が下落したことを意味しますが、私たちはそれを1と分類しました。また、各クラスの推定確率もそれぞれ表示され、私たちのモデルが次のローソク足が高く閉じる確率を53%と誤って推定していることがわかります。推定確率に対する各特徴の寄与の内訳も示されています。青で示した特徴は、私たちのモデルによる予測に対して貢献しており、オレンジで示した特徴は、私たちのモデルによる予測に貢献しています。つまり、RSIがこの誤判定に最も寄与しているが、スプレッドとheightの交互作用項が正しい方向を指し示していたことを意味します。これらの特徴はさらに設計する価値があるかもしれませんが、結論に達する前に、局所的な説明をより厳密に調査する必要があります。

ここでは、ROC(Receiver Operating Characteristic、受受信者操作特性)として知られる1つのグラフを用いて、モデルのパフォーマンスを検証します。ROCグラフによって、分類器のパフォーマンスを簡単に評価することができます。曲線下面積(AUC)に注目しています。理論的には、完璧な分類器は曲線下面積の合計が1になります。このため、1つのグラフだけで簡単に分類器を評価することができます。

glass_box_performance = ROC(glass_box.predict_proba).explain_perf(test_x,test_y, name='Glass Box')
show(glass_box_performance)

ROCチャート

図9:グラスボックスモデルのROCチャート

グラスボックスモデルのAUCは0.49でした。この単純な指標で、私たち人間にとって解釈可能な単位を使用してモデルのパフォーマンスを評価することができます。さらに、この曲線はモデルに依存しないので、基礎となる分類技法に関係なく、異なる分類器を比較するために使用することができます。

ガラスボックスモデルをMT5端末に接続する

ここが踏ん張りどころで、まずはシンプルな方法でグラスボックスモデルをMT5端末に接続します。 

まず、経常収支の状況を追ってみましょう。

#Fetching account Info
account_info = mt5.account_info()

# getting specific account data
initial_balance = account_info.balance
initial_equity = account_info.equity

print('balance: ', initial_balance)
print('equity: ', initial_equity)

balance:912.11 equity:912.11

すべての銘柄を取得します。

symbols = mt5.symbols_get()

グローバル変数をいくつか設定します。

#Trading global variables
#The symbol we want to trade
MARKET_SYMBOL = 'Boom 1000 Index'

#This data frame will store the most recent price update
last_close = pd.DataFrame()

#We may not always enter at the price we want, how much deviation can we tolerate?
DEVIATION = 100

#For demonstrational purposes we will always enter at the minimum volume
#However,we will not hardcode the minimum volume, we will fetch it dynamically
VOLUME = 0
#How many times the minimum volume should our positions be
LOT_MUTLIPLE = 1

#What timeframe are we working on?
TIMEFRAME = mt5.TIMEFRAME_D1

取引数量をハードコードするのではなく、証券会社から動的に最小許容取引数量を取得し、無効な注文を送らないようにするため、それに何らかの係数を掛けます。そこで本稿では、注文の大きさを最小量との相対的な関係で考えることにします。

この場合、すべての取引を最小取引数量で、またはファクター1を使用して開始します。

for index,symbol in enumerate(symbols):
    if symbol.name == MARKET_SYMBOL:
        print(f"{symbol.name} has minimum volume: {symbol.volume_min}")
        VOLUME = symbol.volume_min * LOT_MULTIPLE

ブーム1000指数の出来高は最小:0.2

次に、取引を開始するヘルパー関数を定義します。

# function to send a market order
def market_order(symbol, volume, order_type, **kwargs):
    #Fetching the current bid and ask prices
    tick = mt5.symbol_info_tick(symbol)
    
    #Creating a dictionary to keep track of order direction
    order_dict = {'buy': 0, 'sell': 1}
    price_dict = {'buy': tick.ask, 'sell': tick.bid}

    request = {
        "action": mt5.TRADE_ACTION_DEAL,
        "symbol": symbol,
        "volume": volume,
        "type": order_dict[order_type],
        "price": price_dict[order_type],
        "deviation": DEVIATION,
        "magic": 100,
        "comment": "Glass Box Market Order",
        "type_time": mt5.ORDER_TIME_GTC,
        "type_filling": mt5.ORDER_FILLING_FOK,
    }

    order_result = mt5.order_send(request)
    print(order_result)
    return order_result

次に、チケット番号に基づいて取引をクローズするヘルパー関数を定義します。

# Closing our order based on ticket id
def close_order(ticket):
    positions = mt5.positions_get()

    for pos in positions:
        tick = mt5.symbol_info_tick(pos.symbol) #validating that the order is for this symbol
        type_dict = {0: 1, 1: 0}  # 0 represents buy, 1 represents sell - inverting order_type to close the position
        price_dict = {0: tick.ask, 1: tick.bid} #bid ask prices

        if pos.ticket == ticket:
            request = {
                "action": mt5.TRADE_ACTION_DEAL,
                "position": pos.ticket,
                "symbol": pos.symbol,
                "volume": pos.volume,
                "type": type_dict[pos.type],
                "price": price_dict[pos.type],
                "deviation": DEVIATION,
                "magic": 100,
                "comment": "Glass Box Close Order",
                "type_time": mt5.ORDER_TIME_GTC,
                "type_filling": mt5.ORDER_FILLING_FOK,
            }

            order_result = mt5.order_send(request)
            print(order_result)
            return order_result

    return 'Ticket does not exist'

サーバーに大量のデータをリクエストし続ける必要はないので、日付の範囲も更新します。

#Update our date from and date to
date_from = datetime(2023,11,1)
date_to = datetime.now()

また、グラスボックスモデルから予測を取得し、その予測を売買シグナルとして使用する関数も必要です。

#Get signals from our glass-box model
def ai_signal():
    #Fetch OHLC data
    df = pd.DataFrame(mt5.copy_rates_range(market_symbol,TIMEFRAME,date_from,date_to))
    #Process the data
    df['time'] = pd.to_datetime(df['time'],unit='s')
    df['target'] = (df['close'].shift(-1) > df['close']).astype(int)
    preprocess(df)
    #Select the last row
    last_close = df.iloc[-1:,1:]
    #Remove the target column
    last_close.pop('target')
    #Use the last row to generate a forecast from our glass-box model
    #Remember 1 means buy and 0 means sell
    forecast = glass_box.predict(last_close)
    return forecast[0]

次に、Pythonグラスボックス取引ボットの本体を定義します。

#Now we define the main body of our Python Glass-box Trading Bot
if __name__ == '__main__':
    #We'll use an infinite loop to keep the program running
    while True:
        #Fetching model prediction
        signal = ai_signal()
        
        #Decoding model prediction into an action
        if signal == 1:
            direction = 'buy'
        elif signal == 0:
            direction = 'sell'
        
        print(f'AI Forecast: {direction}')
        
        #Opening A Buy Trade
        #But first we need to ensure there are no opposite trades open on the same symbol
        if direction == 'buy':
            #Close any sell positions
            for pos in mt5.positions_get():
                if pos.type == 1:
                    #This is an open sell order, and we need to close it
                    close_order(pos.ticket)
            
            if not mt5.positions_totoal():
                #We have no open positions
                market_order(MARKET_SYMBOL,VOLUME,direction)
        
        #Opening A Sell Trade
        elif direction == 'sell':
            #Close any buy positions
            for pos in mt5.positions_get():
                if pos.type == 0:
                    #This is an open buy order, and we need to close it
                    close_order(pos.ticket)
            
            if not mt5.positions_get():
                #We have no open positions
                market_order(MARKET_SYMBOL,VOLUME,direction)
        
        print('time: ', datetime.now())
        print('-------\n')
        time.sleep(60)

AI Forecast: sell

time:  2023-12-0415:31:31.569495

-------

グラスボックス取引ボット

図10:Pythonで作られたグラスボックス取引ボットが利益を上げている

グラスボックスモデルを支援するEAの構築

次に、MQL5を使用したグラスボックスモデルのアシスタントの構築に移ります。ATRの読みに基づいて損切り(SL)と利益確定(TP)を動かすEAを作ります。Pythonの統合モジュールを使用してこのタスクを実行するのは、1分ごとや1時間ごとといった低い頻度で更新しない限り、悪夢となるでしょう。SLとTPは刻々と更新され、それ以外は私たちの厳しい要求を満たしません。エントリからSL/TPまでのギャップの大きさを指定する、ユーザーからの2つの入力が必要です。ATRの読みにユーザー入力を掛けて、SLまたはTPからエントリーポイントまでの高さを計算します。そして2つ目の入力は、単純にATRの期間です。

//Meta Properties 
#property copyright "Gamuchirai Ndawana"
#property link "https://twitter.com/Westwood267"

//Classes for managing Trades And Orders
#include  <Trade\Trade.mqh>
#include <Trade\OrderInfo.mqh>

//Instatiating the trade class and order manager
CTrade trade;
class COrderInfo;

//Input variables
input double atr_multiple =0.025;  //How many times the ATR should the SL & TP be?
input int atr_period = 200;      //ATR Period

//Global variables
double ask, bid,atr_stop; //We will use these variables to determine where we should place our ATR
double atr_reading[];     //We will store our ATR readings in this arrays
int    atr;               //This will be our indicator handle for our ATR indicator
int min_volume;

int OnInit(){     
                  //Check if we are authorized to use an EA on the terminal
                  if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)){
                           Comment("Press Ctrl + E To Give The Robot Permission To Trade And Reload The Program");
                           //Remove the EA from the terminal
                           ExpertRemove();
                           return(INIT_FAILED);
                  }
                  
                  //Check if we are authorized to use an EA on the terminal
                  else if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){
                            Comment("Reload The Program And Make Sure You Clicked Allow Algo Trading");
                            //Remove the EA from the terminal
                            ExpertRemove();
                            return(INIT_FAILED);
                  }
                  
                  //If we arrive here then we are allowed to trade using an EA on the Terminal                
                  else{
                        //Symbol information
                        //The smallest distance between our point of entry and the stop loss
                        min_volume = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);//SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN)
                        //Setting up our ATR indicator
                        atr = iATR(_Symbol,PERIOD_CURRENT,atr_period);
                        return(INIT_SUCCEEDED);
                  }                       
}

void OnDeinit(const int reason){

}

void OnTick(){
               //Get the current ask
               ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
               //Get the current bid
               bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
               //Copy the ATR reading our array for storing the ATR value
               CopyBuffer(atr,0,0,1,atr_reading);
               //Set the array as series so the natural time ordering is preserved
               ArraySetAsSeries(atr_reading,true); 
               
               //Calculating where to position our stop loss
               //For now we'll keep it simple, we'll add the minimum volume and the current ATR reading and multiply it by the ATR multiple
               atr_stop = ((min_volume + atr_reading[0]) * atr_multiple);

               //If we have open positions we should adjust the stop loss and take profit 
               if(PositionsTotal() > 0){
                        check_atr_stop();          
               }
}

//--- Functions
//This funciton will update our S/L & T/P based on our ATR reading
void check_atr_stop(){
      
      //First we iterate over the total number of open positions                      
      for(int i = PositionsTotal() -1; i >= 0; i--){
      
            //Then we fetch the name of the symbol of the open position
            string symbol = PositionGetSymbol(i);
            
            //Before going any furhter we need to ensure that the symbol of the position matches the symbol we're trading
                  if(_Symbol == symbol){
                           //Now we get information about the position
                           ulong ticket = PositionGetInteger(POSITION_TICKET); //Position Ticket
                           double position_price = PositionGetDouble(POSITION_PRICE_OPEN); //Position Open Price
                           double type = PositionGetInteger(POSITION_TYPE); //Position Type
                           double current_stop_loss = PositionGetDouble(POSITION_SL); //Current Stop loss value
                           
                           //If the position is a buy
                           if(type == POSITION_TYPE_BUY){
                                  //The new stop loss value is just the ask price minus the ATR stop we calculated above
                                  double atr_stop_loss = (ask - (atr_stop));
                                  //The new take profit is just the ask price plus the ATR stop we calculated above
                                  double atr_take_profit = (ask + (atr_stop));
                                  
                                  //If our current stop loss is less than our calculated ATR stop loss 
                                  //Or if our current stop loss is 0 then we will modify the stop loss and take profit
                                 if((current_stop_loss < atr_stop_loss) || (current_stop_loss == 0)){
                                       trade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                                 }  
                           }
                           
                            //If the position is a sell
                           else if(type == POSITION_TYPE_SELL){
                                     //The new stop loss value is just the bid price plus the ATR stop we calculated above
                                     double atr_stop_loss = (bid + (atr_stop));
                                     //The new take profit is just the bid price minus the ATR stop we calculated above
                                     double atr_take_profit = (bid - (atr_stop));
                                     
                                 //If our current stop loss is greater than our calculated ATR stop loss 
                                 //Or if our current stop loss is 0 then we will modify the stop loss and take profit 
                                 if((current_stop_loss > atr_stop_loss) || (current_stop_loss == 0)){
                                       trade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                                 }
                           }  
                  }  
            }
}

EAがグラスボックスをサポート

図11:EAはグラスボックスモデルと密接に連携している

グラスボックスモデルをOpen Neural Network Exchange (ONNX)形式に書き出します。


ONNXロゴ

図12:Open Neural Network Exchangeのロゴ

Open Neural Network Exchange (ONNX)は、あらゆる機械学習モデルを表現するためのオープンソースプロトコルです。世界中のさまざまな業種の企業による大規模で広範な集団的努力によって、広くサポートされ、維持されています。マイクロソフト、フェイスブック、MATLAB、IBM、クアルコム、ファーウェイ、インテル、AMDなどの企業が名を連ねています。本稿執筆時点では、ONNXは、どのフレームワークで開発されたかにかかわらず、あらゆる機械学習モデルを表現するための普遍的な標準形式であり、さらに、機械学習モデルを異なるプログラミング言語や環境で開発展開することを可能にしています。どうしてこんなことが可能なのか不思議に思われるかもしれませんが、核となる考え方は、どんな機械学習モデルもノードとエッジのグラフとして表現できるということです。各ノードは数学的演算を表し、各エッジはデータの流れを表します。このシンプルな表現を使用すれば、どのような機械学習モデルも、それを作ったフレームワークに関係なく表現することができます。

ONNXモデルができたら、ONNXモデルを実行するエンジンが必要です。これはONNXランタイムの責任です。ONNXランタイムは、データセンターのスーパーコンピュータからポケットの中の携帯電話、そしてその間にあるあらゆるものまで、さまざまなデバイス上でONNXモデルを効率的に実行し、展開する役割を担っています。

私たちの場合、ONNXによって機械学習モデルをEAに統合し、本質的にそれ自身の頭脳を持つEAを構築することができます。MetaTrader 5端末は、履歴データで安全かつ確実にアドバイザーをテストするための一連のツールを提供し、さらには、EAをテストする推奨方法であるウォークフォワードテストを実行するためのツールも提供します。ウォークフォワードテストとは、EAをリアルタイムで、あるいはモデルが見た最後の訓練日より前の任意の期間で実行することです。これは、訓練で見たことのないデータに扱うモデルの頑健性をテストするのに最適な方法であり、さらに、訓練したデータを使用してモデルをバックテストすることで、自分自身を欺くことを防ぐことができます。

前回と同じように、ONNXモデルを書き出しするためのコードを、この記事でこれまで使用してきた残りのコードから分離して、コードを読みやすくしておきます。さらに、実用的な実装を単純化するために、入力として必要なパラメータの数を減らします。ONNXモデルの入力として、以下の特徴だけを選択しました。

1.ラグの高さ:この場合の高さは、次のように定義されることを覚えておいてください:(((高値+安値)/2)-終値)と定義されるので、遅行高値は高値の前回の値です。

2.高さの伸び:高さの伸びは、高さの測定値の二次導関数の推定値として機能します。これは、連続する過去の高さの値の差を2回取ることによって達成されます。得られた値から、高さの変化率を知ることができます。もっと簡単に言えば、高さが時間とともに加速度的に伸びているのか、それとも減速しているのかを理解するのに役立ちます。

3.中間点:この場合の中間点は((High + Low) / 2)として定義されることを覚えておいてください。

4.中間点の伸び:中間点の伸びは、中間点の測定値の二次微分を表す派生特徴です。これは、連続する過去の中間値の差を2回取ることによって達成されます。その結果、中間点の変化率を知ることができます。具体的には、中間点が加速度的に伸びているのか、それとも減速しているのかを示します。より簡単で専門的でない言葉で言えば、中間点がゼロからどんどん離れていくのか、ゼロにどんどん近づいていくのかを理解するのに役立ちます。

記事の前半では「Boom 1000 Index」の銘柄をモデル化しましたが、今回は「Volatility 75 Index」銘柄をモデル化します。

また、EAは、前に見たようにATR読み取り値を使用して動的にSL/TPポジションを自動的に配置します。さらに、利益が特定のしきい値を超えた場合に別のポジションを自動的に追加する機能も提供します。

ONNXとebm2onnxの2つの新しいインポートを除き、ほとんどのインポートは変わりません。これら2つのパッケージで、Explainable Boosting MachineをONNX形式に変換できます。 

#Import MetaTrader5 package
import MetaTrader5 as mt5

#Import datetime for selecting data
from datetime import datetime

#Keeping track of time
import time

#Import matplotlib
import matplotlib.pyplot as plt

#Intepret glass-box model
from interpret.glassbox import ExplainableBoostingClassifier

#Intepret GUI dashboard utility
from interpret import show

#Pandas for handling data
import pandas as pd

#Pandas-ta for calculating technical indicators
import pandas_ta as ta

#Scoring metric to assess model accuracy
from sklearn.metrics import precision_score

#ONNX
import onnx

#Import ebm2onnx
import ebm2onnx

#Path handling
from sys import argv

そこから、上で説明したのと同じステップを繰り返してログインし、データを取得します。唯一の違いは、カスタム特徴を準備するためのステップです。

#Let's create a function to preprocess our data
def preprocess(data):
    data['mid_point'] = ((data['high'] + data['low']) / 2)

    data['mid_point_growth'] = data['mid_point'].diff().diff()

    data['mid_point_growth_lag'] = data['mid_point_growth'].shift(1)

    data['height'] = (data['mid_point'] - data['close'])

    data['height - 1'] = data['height'].shift(1)

    data['height_growth'] = data['height'].diff().diff()
    
    data['height_growth_lag'] = data['height_growth'].shift(1)
    
    data['time'] = pd.to_datetime(data['time'],unit='s')
    
    data.dropna(axis=0,inplace=True)
    
    data['target'] = (data['close'].shift(-1) > data['close']).astype(int)

いったんデータが収集されれば、データを訓練セットとテストセットに分割するために必要なステップと、グラスボックスモデルを適合させるために必要なステップは同じです。

グラスボックスモデルのフィッティングが完了したら、次はONNX形式への書き出しです。

まず、モデルを保存するパスを指定する必要があります。MetaTrader 5をインストールすると、端末で使用できるファイル専用のフォルダが作成されます。Pythonライブラリを使用すると、絶対パスを非常に簡単に取得できます。

terminal_info=mt5.terminal_info()
print(terminal_info)
TerminalInfo(community_account=False, community_connection=False, connected=True, dlls_allowed=False, trade_allowed=True, tradeapi_disabled=False, email_enabled=False, ftp_enabled=False, notifications_enabled=False, mqid=True, build=4094, maxbars=100000, codepage=0, ping_last=222088, community_balance=0.0, retransmission=0.030435223698894183, company='MetaQuotes Software Corp.', name='MetaTrader 5', language='English', path='C:\\Program Files\\MetaTrader 5', data_path='C:\\Users\\Westwood\\AppData\\Roaming\\MetaQuotes\\Terminal\\D0E8209F77C8CF37AD8BF550E51FF075', commondata_path='C:\\Users\\Westwood\\AppData\\Roaming\\MetaQuotes\\Terminal\\Common')

探しているパスは、上で作成したterminal_infoオブジェクトの「データパス」として保存されています。

file_path=terminal_info.data_path+"\\MQL5\\Files\\"
print(file_path)

C:\Users\Westwood\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Files\

そこから、使用するパスを準備する必要があります。コードは端末から取得したファイルパスを取り込み、ファイル名を除外してパスのディレクトリを分離します。

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\Westwood\AppData\Local\Programs\Python\Python311\Lib\site-packages\

そこからebm2onnxパッケージを使用して、グラスボックスモデルをONNXフォーマットに変換する準備をします。ebm2onnx.get_dtype_from_pandas関数を使用して動的におこなうのが望ましいので、先ほど使用した訓練データフレームを渡します。 

onnx_model = ebm2onnx.to_onnx(glass_box,ebm2onnx.get_dtype_from_pandas(train_x))
#Save the ONNX model in python
output_path = data_path+"Volatility_75_EBM.onnx"
onnx.save_model(onnx_model,output_path)
#Save the ONNX model as a file to be imported in our MetaEditor
output_path = file_path+"Volatility_75_EBM.onnx"
onnx.save_model(onnx_model,output_path)

これでMetaEditor 5でONNXファイルを扱う準備が整いました。MetaEditorは、MetaQuotes Languageを使用してコードを書くための統合開発環境です。 

最初にMetaEditor 5統合開発環境を開き、[Volatility Doctor 75 EBM]をダブルクリックすると、このように表示されます。

ONNXモデルを初めて開く

図13:ONNXモデルの入出力


EAを作成し、ONNXモデルを読み込みます。

まず、一般的なファイル情報を指定します。

//+------------------------------------------------------------------+
//|                                                         ONNX.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
//Meta properties
#property copyright "Gamuchirai Zororo Ndawana"
#property link      "https://www.mql5.com/ja/users/gamuchiraindawa"
#property version   "1.00"

そこから、いくつかのグローバル変数を指定する必要があります。

//Trade Library
#include <Trade\Trade.mqh>           //We will use this library to modify our positions

//Global variables
//Input variables
input double atr_multiple =0.025;    //How many times the ATR should the SL & TP be?
int input lot_mutliple = 1;          //How many time greater than minimum lot should we enter?
const int atr_period = 200;          //ATR Period

//Trading variables
double ask, bid,atr_stop;            //We will use these variables to determine where we should place our ATR
double atr_reading[];                //We will store our ATR readings in this arrays
int    atr;                          //This will be our indicator handle for our ATR indicator
long min_distance;                   //The smallest distance allowed between our entry position and the stop loss
double min_volume;                   //The smallest contract size allowed by the broker
static double initial_balance;       //Our initial trading balance at the beginning of the trading session
double current_balance;              //Our trading balance at every instance of trading
long     ExtHandle = INVALID_HANDLE; //This will be our model's handler
int      ExtPredictedClass = -1;     //This is where we will store our model's forecast
CTrade   ExtTrade;                   //This is the object we will call to open and modify our positions

//Reading our ONNX model and storing it into a data array
#resource "\\Files\\Volatility_75_EBM.onnx" as uchar ExtModel[] //This is our ONNX file being read into our expert advisor

//Custom keyword definitions
#define  PRICE_UP 1
#define  PRICE_DOWN 0

そこからOnInit()関数を指定します。OnInit関数を使用してONNXモデルをセットアップします。ONNXモデルをセットアップするには、3つの簡単なステップを踏むだけです。まず、ONNXモデルをリソースとして必要とするときに、上記のグローバル変数で使用したバッファからONNXモデルを作成します。それを読み込んだ後、個々の入力の形状を指定し、次に個々の出力の形状を指定する必要があります。入出力の形状を設定しようとしたときに、エラーが投げられたかどうかを確認します。すべてがうまくいったら、証券会社が許容する最小取引数量、損切りとエントリポジションの間の最小距離を取得し、ATR指標も設定します。

int OnInit()
  {
   //Check if the symbol and time frame conform to training conditions
   if(_Symbol != "Volatility 75 Index" || _Period != PERIOD_M1)
       {
            Comment("Model must be used with the Volatility 75 Index on the 1 Minute Chart");
            return(INIT_FAILED);
       }
    
    //Create an ONNX model from our data array
    ExtHandle = OnnxCreateFromBuffer(ExtModel,ONNX_DEFAULT);
    Print("ONNX Create from buffer status ",ExtHandle);
    
    //Checking if the handle is valid
    if(ExtHandle == INVALID_HANDLE)
      {
            Comment("ONNX create from buffer error ", GetLastError());
            return(INIT_FAILED);
      }
   
   //Set input shape
   long input_count = OnnxGetInputCount(ExtHandle);   
   const long input_shape[] = {1};
   Print("Total model inputs : ",input_count);
   
   //Setting the input shape of each input
   OnnxSetInputShape(ExtHandle,0,input_shape);
   OnnxSetInputShape(ExtHandle,1,input_shape);
   OnnxSetInputShape(ExtHandle,2,input_shape);
   OnnxSetInputShape(ExtHandle,3,input_shape);
   
   //Check if anything went wrong when setting the input shape
   if(!OnnxSetInputShape(ExtHandle,0,input_shape) || !OnnxSetInputShape(ExtHandle,1,input_shape) || !OnnxSetInputShape(ExtHandle,2,input_shape) || !OnnxSetInputShape(ExtHandle,3,input_shape))
      {
            Comment("ONNX set input shape error ", GetLastError());
            OnnxRelease(ExtHandle);
            return(INIT_FAILED);
      }
      
   //Set output shape
   long output_count = OnnxGetOutputCount(ExtHandle);
   const long output_shape[] = {1};
   Print("Total model outputs : ",output_count);
   //Setting the shape of each output
   OnnxSetOutputShape(ExtHandle,0,output_shape);
   //Checking if anything went wrong when setting the output shape
   if(!OnnxSetOutputShape(ExtHandle,0,output_shape))
      {
            Comment("ONNX set output shape error ", GetLastError());
            OnnxRelease(ExtHandle);
            return(INIT_FAILED);
      }
    //Get the minimum trading volume allowed  
    min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);  
    //Symbol information
    //The smallest distance between our point of entry and the stop loss
    min_distance = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
    //Initial account balance
    initial_balance = AccountInfoDouble(ACCOUNT_BALANCE);
    //Setting up our ATR indicator
    atr = iATR(_Symbol,PERIOD_CURRENT,atr_period);
    return(INIT_SUCCEEDED);
//---
  }

DeInit関数はとてもシンプルで、ONNXハンドラを削除し、使用していないリソースを占有しないようにします。

void OnDeinit(const int reason)
  {
//---
   if(ExtHandle != INVALID_HANDLE)
      {
         OnnxRelease(ExtHandle);
         ExtHandle = INVALID_HANDLE;
      }
  }

OnTick関数はEAの心臓部であり、証券会社から新しいティックを受信するたびに呼び出されます。私たちの場合、まず時間を記録することから始める。これにより、ティックごとに実行したい処理と、新しいローソク足が形成されるたびに実行したい処理を分けることができます。ティックごとに買値と売値を更新し、ティックごとに利食いと損切りのポジションを更新したいのですが、未決済のポジションがない場合は、新しいローソク足が形成された時点で一度だけモデル予測をおこないたいのです。

void OnTick()
  {
//---
   //Time trackers
   static datetime time_stamp;
   datetime time = iTime(_Symbol,PERIOD_M1,0);

   //Current bid price
   bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   //Current ask price
   ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   
   //Copy the ATR reading our array for storing the ATR value
   CopyBuffer(atr,0,0,1,atr_reading);
   
   //Set the array as series so the natural time ordering is preserved
   ArraySetAsSeries(atr_reading,true); 
   
   //Calculating where to position our stop loss
   //For now we'll keep it simple, we'll add the minimum volume and the current ATR reading and multiply it by the ATR multiple
   atr_stop = ((min_distance + atr_reading[0]) * atr_multiple);
   
   //Current Session Profit and Loss Position
   current_balance = AccountInfoDouble(ACCOUNT_BALANCE);
   Comment("Current Session P/L: ",current_balance - initial_balance);
   
   //If we have a position open we need to update our stoploss
   if(PositionsTotal() > 0){
        check_atr_stop();          
   }
   
    //Check new bar
     if(time_stamp != time)
      {
         time_stamp = time;
         
         //If we have no open positions let's make a forecast and open a new position
         if(PositionsTotal() == 0){
            Print("No open positions making a forecast");
            PredictedPrice();
            CheckForOpen();
         }
      }
   
  }
そこから、ATRの利食いと損切りのポジションを更新する関数を定義します。この関数は、開いているすべてのポジションを繰り返し、そのポジションが取引中の銘柄と一致するかどうかを確認します。そうすれば、ポジションに関する詳細な情報を取得し、そこからポジションの方向性に応じて、ポジションの損切りと利食いを調整します。取引がポジションと反対に動いている場合、利食いと損切りはそのままにされることに注意してください。
//--- Functions
//This function will update our S/L & T/P based on our ATR reading
void check_atr_stop(){
      
      //First we iterate over the total number of open positions                      
      for(int i = PositionsTotal() -1; i >= 0; i--){
      
            //Then we fetch the name of the symbol of the open position
            string symbol = PositionGetSymbol(i);
            
            //Before going any further we need to ensure that the symbol of the position matches the symbol we're trading
                  if(_Symbol == symbol){
                           //Now we get information about the position
                           ulong ticket = PositionGetInteger(POSITION_TICKET); //Position Ticket
                           double position_price = PositionGetDouble(POSITION_PRICE_OPEN); //Position Open Price
                           long type = PositionGetInteger(POSITION_TYPE); //Position Type
                           double current_stop_loss = PositionGetDouble(POSITION_SL); //Current Stop loss value
                           
                           //If the position is a buy
                           if(type == POSITION_TYPE_BUY){
                                  //The new stop loss value is just the ask price minus the ATR stop we calculated above
                                  double atr_stop_loss = (ask - (atr_stop));
                                  //The new take profit is just the ask price plus the ATR stop we calculated above
                                  double atr_take_profit = (ask + (atr_stop));
                                  
                                  //If our current stop loss is less than our calculated ATR stop loss 
                                  //Or if our current stop loss is 0 then we will modify the stop loss and take profit
                                 if((current_stop_loss < atr_stop_loss) || (current_stop_loss == 0)){
                                       ExtTrade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                                 }  
                           }
                           
                            //If the position is a sell
                           else if(type == POSITION_TYPE_SELL){
                                     //The new stop loss value is just the bid price plus the ATR stop we calculated above
                                     double atr_stop_loss = (bid + (atr_stop));
                                     //The new take profit is just the bid price minus the ATR stop we calculated above
                                     double atr_take_profit = (bid - (atr_stop));
                                     
                                 //If our current stop loss is greater than our calculated ATR stop loss 
                                 //Or if our current stop loss is 0 then we will modify the stop loss and take profit 
                                 if((current_stop_loss > atr_stop_loss) || (current_stop_loss == 0)){
                                       ExtTrade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                                 }
                           }  
                  }  
            }
}
新しいポジションを建てるための別の関数も必要です。上で宣言したグローバル変数bidとaskを使用することにご注意ください。これにより、プログラム全体が同じ価格を使用していることが保証されます。さらに、check_atr_stop関数によって管理されるため、損切りと利食いをともに0に設定します。
void CheckForOpen(void)
   {
      ENUM_ORDER_TYPE signal = WRONG_VALUE;
      
      //Check signals
      if(ExtPredictedClass == PRICE_DOWN)
         {
            signal = ORDER_TYPE_SELL;
         }
      else if(ExtPredictedClass == PRICE_UP)
         {
            signal = ORDER_TYPE_BUY;
         }
         
      if(signal != WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
         {
            double price, sl = 0 , tp = 0;
            
            if(signal == ORDER_TYPE_SELL)
               {
                  price = bid;
               }
               
           else
               {
                  price = ask;
               }
               
            Print("Opening a new position: ",signal);  
            ExtTrade.PositionOpen(_Symbol,signal,min_volume,price,0,0,"ONNX Order");
         }
   }
   

最後に、EA内でONNXモデルを使用して予測をおこなう関数が必要です。この関数は、訓練時に前処理されたのと同じように、データの前処理も担当します。この点はいくら強調してもしすぎることはありません。訓練でも本番でも、一貫した方法でデータが処理されるように注意しなければなりません。モデルへの各入力はそれぞれのベクトルに格納され、各ベクトルは訓練中にモデルに渡されたのと同じ順番でONNXRun関数に渡されます。プロジェクト全体を通して一貫性を保つことが最も重要です。そうでなければ、モデルをコンパイルするときに例外をスローしないかもしれない実行時エラーが発生することがあります。各入力ベクトルのデータ型が、モデルが期待する入力型と一致していること、さらに出力型がモデルの出力型と一致していることを確認します。

void PredictedPrice(void)
   {
      long output_data[] = {1};
      
      double lag_2_open = double(iOpen(_Symbol,PERIOD_M1,3));
      double lag_2_high = double(iOpen(_Symbol,PERIOD_M1,3));
      double lag_2_close = double(iClose(_Symbol,PERIOD_M1,3));
      double lag_2_low = double(iLow(_Symbol,PERIOD_M1,3));
      double lag_2_mid_point = double((lag_2_high + lag_2_low) / 2);
      double lag_2_height = double(( lag_2_mid_point - lag_2_close));
      
      double lag_open = double(iOpen(_Symbol,PERIOD_M1,2));
      double lag_high = double(iOpen(_Symbol,PERIOD_M1,2));
      double lag_close = double(iClose(_Symbol,PERIOD_M1,2));
      double lag_low = double(iLow(_Symbol,PERIOD_M1,2));
      double lag_mid_point = double((lag_high + lag_low) / 2);
      double lag_height = double(( lag_mid_point - lag_close));
      
      double   open  =  double(iOpen(_Symbol,PERIOD_M1,1));
      double   high  = double(iHigh(_Symbol,PERIOD_M1,1));
      double   low   = double(iLow(_Symbol,PERIOD_M1,1));
      double   close = double(iClose(_Symbol,PERIOD_M1,1));
      double   mid_point = double( (high + low) / 2 );
      double   height =  double((mid_point - close)); 
      
      double first_height_delta = (height - lag_height);
      double second_height_delta = (lag_height - lag_2_height);
      double height_growth = first_height_delta - second_height_delta;
      
      double first_midpoint_delta = (mid_point - lag_mid_point);
      double second_midpoint_delta = (lag_mid_point - lag_2_mid_point);
      double mid_point_growth = first_midpoint_delta - second_midpoint_delta;
      
      vector input_data_lag_height = {lag_height};
      vector input_data_height_grwoth = {height_growth};
      vector input_data_midpoint_growth = {mid_point_growth};
      vector input_data_midpoint = {mid_point};
      
       if(OnnxRun(ExtHandle,ONNX_NO_CONVERSION,input_data_lag_height,input_data_height_grwoth,input_data_midpoint_growth,input_data_midpoint,output_data))
         {
            Print("Model Inference Completed Successfully");
            Print("Model forecast: ",output_data[0]);
         }
       else
       {
            Print("ONNX run error : ",GetLastError());
            OnnxRelease(ExtHandle);
       }
        
       long predicted = output_data[0];
       
       if(predicted == 1)
         {
            ExtPredictedClass = PRICE_UP;
         }
         
       else if(predicted == 0)
         {
            ExtPredictedClass = PRICE_DOWN;
         }
   }

これが完了したら、モデルをコンパイルし、MetaTrader 5端末でデモ口座を使用してフォワードテストする準備が整いました。

ONNXモデルのフォワードテスト

図14:グラスボックスONNXEAのフォワードテスト

EAタブと操作ログタブをチェックして、モデルがエラーなく動作していることを確認します。

エラーチェック

図15:EAタブでエラーを確認する

操作ログタブのエラーチェック

図16:操作ログタブでのエラーチェック

見ての通り、モデルは問題なく動いています。EAの設定はいつでも調整できることを覚えておいてください。

モデルの設定を調整する

図17:EAの設定を調整する

よく遭遇する課題

このセクションでは、初めてセットアップをおこなう際に遭遇する可能性のあるエラーのいくつかを再現します。何がエラーを引き起こしているのかを検証し、最後にそれぞれの問題に対する解決策を1つずつ説明します。

入力または出力形状を正しく設定できない

最もよく発生する問題は、入力と出力の形状を正しく設定しなかったことに起因します。モデルが期待する各特徴の入力形状を定義しなければならないことを覚えておいてください。 各インデックスを繰り返し、そのインデックスの各特徴の入力形状を定義します。各特徴の形状を指定しなかった場合でも、以下のデモのようにエラーは発生せずにモデルはコンパイルされますが、そのモデルで推論を実行しようとするとエラーが発生します。エラーコードは5808で、MQL5のドキュメントには「Tensor dimension not set or invalid」と記述されています。この例では4つの入力がありますが、以下のコード例では1つの入力シェイプしか設定していないことを覚えておいてください。 

入力シェイプの設定に失敗

図18:例外をスローせずにコンパイルするEA

[エキスパート]タブを調べたときにエラーがどのように表示されるかを示すスクリーンショットも含まれています。正しいコードが記事に添付されていることを忘れないでください。

エラーメッセージ5808

図19:エラーメッセージ5808

不適切なタイプキャスティング

タイプキャストを誤ると、データが完全に失われたり、EAがクラッシュしたりすることがあります。以下の例では、ONNXモデルの出力を格納するために整数配列を使用しています。ONNXモデルの出力がint64型であることを覚えておいてください。なぜエラーになると思いますか。 int型がモデルの出力を保存するのに十分なメモリを持っていないためにエラーが発生し、モデルが失敗してしまうからです。モデルの出力には8バイトが必要ですが、int配列には4バイトしかありません。解決策は簡単で、入力と出力を格納するために正しいデータ型を使用していることを確認し、タイプキャストする必要がある場合は、MQL5のドキュメントで指定されているタイプキャストルールに準拠していることを確認することです。エラーコードは5807で、説明は「Invalid parameter size」です。

タイプキャスティングエラー

図20:不適切なタイプキャスティング

エラーメッセージ5807

図21:エラーメッセージ5807

ONNX Runの呼び出しに失敗

ONNX Run関数は、各モデル入力がそれぞれ独立した配列で渡されることを想定しています。以下のコード例では、すべての入力を1つの配列に結合し、その1つの配列をONNX Run関数に渡しています。コンパイル時には例外は発生しませんが、実行時に[エクスパート]タブでエラーが発生します。エラーコードは5804で、ドキュメントでは「Invalid number of parameters passed to OnnxRun」と簡潔に説明されています。

ONNX Runの呼び出しに失敗

図22:ONNX Run関数の呼び出しに失敗

エラーメッセージ5804

図23:エラーメッセージ5804

結論

グラスボックスモデルがなぜ私たちファイナンシャルエンジニアにとって有用なのかがお分かりいただけたと思います。ブラックボックスモデルから同じ情報を忠実に抽出するのにかかる労力に比べれば、グラスボックスモデルはわずかな労力で貴重な洞察を与えてくれます。さらに、グラスボックスモデルは、デバッグ、保守、解釈、説明が容易です。いわば「ボンネットの下を覗き込む」ことで、モデルが意図した通りに動いていることを検証しなければならないのです。 

グラスボックスモデルには、これまで取り上げてこなかった大きな欠点があります。ブラックボックスモデルほど柔軟性がないということです。グラスボックスモデルは未開拓の研究分野であり、時が経つにつれて、将来的にはより柔軟なグラスボックスモデルが登場するかもしれませんが、この記事の執筆時点では、それらはそれほど柔軟ではありません。つまり、ブラックボックスモデルでより適切にモデル化される可能性のある関係が存在することということです。さらに、現在のグラスボックスモデルの実装は決定木をベースにしているため、InterpretMLのExplainableBoostingClassifiersの実装は決定木の欠点をすべて受け継いでいます。

また会う日まで、平和、愛、調和、そして有益な取引を祈ります。

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

母集団最適化アルゴリズム:焼きなまし(SA)アルゴリズム(第1部) 母集団最適化アルゴリズム:焼きなまし(SA)アルゴリズム(第1部)
焼きなましアルゴリズムは、金属の焼きなまし過程にヒントを得たメタヒューリスティックです。この記事では、このアルゴリズムを徹底的に分析し、この広く知られている最適化方法を取り巻く多くの一般的な信念や神話を暴露します。この記事の後半では、カスタムの等方的焼きなまし(Simulated Isotropic Annealing、SIA)アルゴリズムについて説明します。
母集団最適化アルゴリズム:ネルダー–ミード法、またはシンプレックス(NM)検索法 母集団最適化アルゴリズム:ネルダー–ミード法、またはシンプレックス(NM)検索法
この記事では、ネルダー–ミード法の完全な探求を提示し、最適解を達成するために各反復でシンプレックス(関数パラメータ空間)がどのように修正され、再配置されるかを説明し、この方法がどのように改善されるかを説明します。
ニューラルネットワークが簡単に(第67回):過去の経験を活かした新しい課題の解決 ニューラルネットワークが簡単に(第67回):過去の経験を活かした新しい課題の解決
この記事では、訓練セットにデータを収集する方法について引き続き説明します。明らかに、学習プロセスには環境との絶え間ない相互作用が必要です。しかし、状況はさまざまです。
ニューラルネットワークが簡単に(第66回):オフライン学習における探索問題 ニューラルネットワークが簡単に(第66回):オフライン学習における探索問題
モデルは、用意された訓練データセットのデータを使用してオフラインで訓練されます。一定の利点がある反面、環境に関する情報が訓練データセットのサイズに大きく圧縮されてしまうというマイナス面もあります。それが逆に、探求の可能性を狭めています。この記事では、可能な限り多様なデータで訓練データセットを埋めることができる方法について考えます。