
機械学習の限界を克服する(第2回):再現性の欠如
私はこれまで読者の皆さまから励ましのお言葉を数多くいただいておりますが、繰り返し寄せられるプライベートメッセージやコメントの中には、「記事で提示された結果を再現することが難しい」というご意見が含まれています。最初は私自身も戸惑いましたが、熟考した結果、その理由のひとつとして有力だと思われるものに行き当たりました。
グローバル金融市場は巨大かつ分散的なネットワークとして機能しています。世界には数多くのブローカーが存在し、日々新しいブローカーが登録されていますが、これらを統一的に規制する国際的な権威機関や価格フィードを調整する仕組みは存在しません。そのため、各ブローカーはロイターのような外部データサービスを含む、独自の価格フィードを自由に利用しています。
その結果、同じEURUSDのパフォーマンスを比較しても、ブローカーAとブローカーBの間で逆方向に動いている場合さえあります。たとえば、ブローカーAがEURUSDの日次変動を+0.12%と報告する一方で、ブローカーBは-0.65%と報告する、といった具合です。
問題の核心:ブローカー間のデータ不一致
今回の議論にあたり、私が個人的に取引で利用している2社のブローカーを無作為に選びました。コミュニティのガイドライン(ブローカー名の宣伝禁止)に従い、ここでは「ブローカーA」と「ブローカーB」と記載します。
MetaTrader 5のPythonライブラリを用いて、両ブローカーからEURUSDの日次データを4年分取得しました。その結果、タイムスタンプが一致していないことに気づきました。一方のブローカーは2019年9月まで遡るデータを返したのに対し、もう一方は2020年8月までしか遡れませんでした。しかし、どちらも1,460行のデータを返しており、形式的にはリクエスト通りでした。
ブローカーが異なれば運営上のタイムゾーンが異なることは想定内です。しかし、夏時間の適用、認識する祝日の違い、その他細かな相違が、タイムスタンプのずれをさらに複雑にします。
その後、両ブローカーのデータで10日間のEURUSDリターンを算出したところ、その数値的性質が一貫していないことが判明しました。ブローカーAの平均10日リターンは+0.000267、ブローカーBでは-0.000352となり、同一資産にもかかわらず期待リターンが約232%も異なる結果となったのです。
さらに問題を悪化させているのは、ブローカーAの期待リターンにはブローカーBよりも21%高いリスクが伴っていることです。これはリターン分散の比較により確認されました。
初心者向け注釈:リターンの分散は金融において「リスク」を意味します。この考え方はポートフォリオ理論の入門書で広く解説されており、初心者の方もすぐに確認できる基本概念です。
統計学では、2つの変数が同じ方向に動くのか、あるいは独立して動くのかを、その相関の度合いを測定することで調べることができます。標準的な相関の尺度は-1から1の範囲をとります。1であれば変数が完全に同じ方向に動くことを意味し、-1であれば完全に逆方向に動くことを意味します。私が2つのブローカー間でピアソン相関係数を比較したとき、正直なところ1に近い結果を予想していました。しかし、データが示した相関水準はわずか0.41にすぎませんでした。
これはつまり、EURUSD通貨ペアの価格水準が異なるブローカー間で調和して動くという信念は、数学的には根拠がないということを示しています。むしろ、私たちのテスト結果が示しているのは、市場が半分以上の時間において異なる方向に動いているということです。
2つのブローカーのクオートにおけるその他の重要な数値的特性も、この問題の深刻さを強調するものでした。以前の記事で、AIの限界について取り上げた際、回帰モデルの構築に一般的に用いられるRMSEのような指標の落とし穴を読者に示しました。その記事はこちらにリンクしています。
簡単に言えば、私たちは読者に対し、RMSEを単独の指標として解釈するのではなく、使用したいモデルのパフォーマンス(残差平方和、RSS)を、市場の平均リターンを常に予測する単純なモデルが生み出す誤差(全平方和、TSS)と比較することで、その数値を解釈するよう勧めました。重要なのは、単純モデルを上回ることがどれほど難しいかを読者が理解できるようにすることです。RSSとTSSの比率は、その単純モデルをどれほど効率的に上回っているかを示します。
同一の銘柄であれば、この比率は異なるブローカー間でもほぼ一定であることが期待されます。 しかし、ブローカーを変更するだけで、市場平均リターンを予測するモデルを上回る効率が7%改善しました。これはつまり、ブローカーBでは10日間のEURUSDリターンを直接予測することが、ブローカーAよりも約7%容易であることを意味します。
統計学者は分布の中心と標準偏差を比較することで、その分布の裾の特徴を調べることがあります。この操作を10日間のEURUSDリターンに適用すると、どちらのブローカーが極端なリターンを生みやすいかを比較する数値的方法を得ることができます。この考え方に従うと、ブローカーBの10日間のEURUSDリターンは147%膨らんでいるように見えました。
ここまで読めば、私たち直面している問題は明らかでしょう。同じ銘柄であっても、重要な数値的特性はブローカー間で一貫していないのです。その結果、ある取引戦略の収益性は、ブローカーをまたいで常に再現できるとは限りません。
ONNX APIを用いて構築されたAIモデルや、MQL5でネイティブに構築されたモデルを組み込んだ取引戦略は、投資家の期待を満たすことに一貫して失敗するかもしれません。AIを対象のブローカーに固有に調整するための追加作業が広く採用されなければ、その可能性は高まります。これは時間のかかる作業ですが、明らかに重要な作業です。
この記事を読み進めるにあたり、私たちは多くのMQL5開発者が実際に辿っているであろうプロダクションサイクルを、段階的に再現していきます。本記事を通じて示したいのは、開発者が自分の利用するブローカーBを基盤にアプリケーションを構築・最適化し、それを顧客が異なるブローカーAで実行する場合、開発者と顧客の双方にとって問題がすぐに生じるということです。このようなプロダクションサイクルを採用すれば、製品に対して賛否の入り混じったレビューを受ける可能性が高くなります。
このような不満足なパフォーマンス水準を避けるためには、信頼性の高いサービスを提供したいMQL5開発者は、マーケットプレイスで消費者の安全を守るために、戦略やアプリケーションを特定のブローカーに合わせて調整する必要があると考えられます。
はじめに
まず、標準の数値ライブラリをインポートします。
#Load our libraries import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns import MetaTrader5 as mt5
注目する時間軸と通貨ペア、および必要なデータ行数を定義します。
#Let us define certain constants TF = mt5.TIMEFRAME_D1 DATA = (365 * 4) START = 1 PAIR = "EURUSD"
ターミナルを起動します。
#Log in to the terminal if mt5.initialize(): print('Logged in successfully') else: print('Failed To Log In')
ログイン成功です。
ブローカーAでEURUSDを予測することの難しさについて分析してみましょう。
EURUSD_BROKER_A = pd.DataFrame(mt5.copy_rates_from_pos(PAIR,TF,START,DATA)) #Store the data we retrieved from broker A EURUSD_BROKER_A.to_csv("EURUSD BROKER A.csv")
ここで、ブローカーBに対して同じ手順を繰り返します。
#I have manually changed brokers using the MT5 terminal, you should also do the same on your side EURUSD_BROKER_B = pd.DataFrame(mt5.copy_rates_from_pos(PAIR,TF,START,DATA)) #Store the data we retrieved from broker B EURUSD_BROKER_B.to_csv("EURUSD BROKER B.csv")
これで準備が整いました。両ブローカーからEURUSDの過去データを収集できたので、ここからはそれぞれのデータセットの経験的な特性を確認していきましょう。異なるブローカー間でEURUSD通貨ペアが一貫しているのかどうかを見極めるためです。あわせて、どの程度先の未来まで予測をおこなうのかを定義する必要があります。
#Our forecasting horizon HORIZON = 10
両方のデータセットを読み取ります。
EURUSD_BROKER_A = pd.read_csv("EURUSD BROKER A.csv") EURUSD_BROKER_B = pd.read_csv("EURUSD BROKER B.csv")
現在、これらのデータセットの時間列は秒単位で記録されていますが、私たちは人間が読みやすい日付―月―年形式の時刻列に変換したいと考えています。そのためのメソッドを構築してみましょう。
def format_data(f_data): #First make a copy of the data, so we always preserve the original data f_data_copy = f_data.copy() #Format the time correctly, form seconds to human readable formats f_data_copy['time'] = pd.to_datetime(f_data_copy['time'],unit='s') return(f_data_copy)
データセットをフォーマットします。
A = format_data(EURUSD_BROKER_A) B = format_data(EURUSD_BROKER_B)
すべての列名を適切に変更し、それぞれの列名の末尾にデータを提供したブローカーを示す文字を付け加えましょう。つまり、ブローカーA由来の列名は末尾に「A」、ブローカーB由来の列名は末尾に「B」を付けます。それでは、両ブローカーから取得したEURUSDの過去データを注意深く検証していきましょう。両方のデータセットが正確に1,460行の日次データを持っている点に注目してください。これは、各ブローカーが正確に4年分の日次データを返していることを意味します。では、読者の皆さんは他にどのような違いを観察できるでしょうか。ティックボリュームには目を通しましたか。
# Rename all columns (except the join key) B = B.rename(columns=lambda col: col + ' B' if col != 'id' else col) A = A.rename(columns=lambda col: col + ' A' if col != 'id' else col)
図1:ブローカーAから受け取った、2019年9月までのタイムスタンプを持つ過去の日次EURUSDデータ
図2:ブローカーBから受け取ったEURUSDの日次過去データは、図1のタイムスタンプとは一致していないが、いずれも正確に4年間分のデータである
それでは、2つのデータセットを結合してみましょう。
combined = pd.concat([A,B],axis=1)
0のみで満たされた列を作成します。
combined['Null'] = 0
入力を定義します。
inputs = ['open A','high A','low A','close A','tick_volume A','spread A','open B','high B','low B','close B','tick_volume B','spread B']
10日間のEURUSDリターンを計算します。
#Label the data combined['A Target'] = combined['close A'].shift(-HORIZON) - combined['close A'] combined['B Target'] = combined['close B'].shift(-HORIZON) - combined['close B'] #Drop the last HORIZON rows of data combined = combined.iloc[:-HORIZON,:]
ティックボリュームは、定期的に観測された価格変動の数を示します。活発な取引期間は高いティックボリュームとして表れ、市場で多くの活動があったことを示します。一方、ティックボリュームが低い場合は、市場の動きが比較的静かであったことを示唆します。ブローカーAのティックボリュームデータを見ると、長期的には上昇傾向が見られ、EURUSDの未決済建玉への投資家の関心が時間とともに増加していることが示唆されます。プロット上には時折大きなスパイクがあり、これは特に取引が活発な期間に、EURUSDの未決済建玉が最大値に近づくことに対応している可能性があります。
plt.title('Broker A Daily EURUSD Tick Volume') plt.plot(combined['tick_volume A'],color='black') plt.ylabel('Tick Volume') plt.xlabel('Historical Day') plt.grid()
図3:ブローカーAから受け取ったEURUSDの日次ティックボリューム
図4に示されたブローカーBのティックボリュームを図3と比較すると、報告されている市場活動の水準に大きな違いがあることが明確にわかります。ブローカーBのティックボリュームにはほとんどトレンドが見られず、ブローカーAと比較して顕著な上昇傾向はありません。図4は密集しており、ランダムなスパイクが見られ、図3で観察されたような周期的なスパイクとは異なる傾向を示しています。
plt.title('Broker B Daily EURUSD Tick Volume') plt.plot(combined['tick_volume B'],color='black') plt.ylabel('Tick Volume') plt.xlabel('Historical Day') plt.grid()
図4:ブローカーBから受け取ったEURUSDの日次ティックボリューム
各ブローカーでEURUSDを保有した場合に投資家が期待できる平均リターンを考えると、両ブローカーは同じ銘柄であっても異なるバリエーションを提供していることがわかります。もしこれら2つのブローカーが完全に同一の銘柄を提供しているのであれば、期待リターンの水準も一致するはずではないでしょうか。
#What's the average 10-Day EURUSD return from both brokers delta_return = str(((combined.iloc[:,-2:].mean()[0]-combined.iloc[:,-2:].mean()[1]) / combined.iloc[:,-2:].mean()[0]) * 100) t = 'The Expected 10-Day EURUSD Return Differes by ' + delta_return[:5] + '% Between Our Brokers' sns.barplot(combined.iloc[:,-2:].mean(),color='black') plt.axhline(0,color='grey',linestyle='--') plt.title(t) plt.ylabel('Return')
図5:ブローカー間の平均市場リターンは0を挟んで反対側に位置している
2つの市場から得られたリターンを重ねてプロットしてみましょう。各リターンはスケーリングし、両方の線が0を中心に表示されるようにします。0からのずれは、平均市場リターンから何標準偏差逸脱しているかを示します。すぐにわかるのは、多くの場面で2つの線が0の線の反対側に位置していることです。一方で、線が互いに追従している場面もあります。一般的に私たちは、この2本の線は常に同じ方向に動くと考えがちですが、図6を見ると、これはあくまで一部の時間に限った話であることがわかります。
plt.plot(((combined.iloc[:,-1]-combined.iloc[:,-1].mean())/combined.iloc[:,-1].std()),color='red') plt.plot(((combined.iloc[:,-2]-combined.iloc[:,-2].mean())/combined.iloc[:,-2].std()),color='black') plt.grid() plt.axhline(0,color='black',linestyle='--') plt.ylabel('Std. Deviations From Expected 10-Day EURUSD Return') plt.xlabel('Historical Days') plt.title('EURUSD Returns from Different Brokers May Not Always Allign') plt.legend(['Broker A','Broker B'])
図6:2つの異なるブローカーによって生成された10日間のEURUSDリターン
ブローカーが提供するリターンの分散量を比較することで、どのブローカーの方がリスクが高いか、どのブローカーのリターンがより確実であるかを評価できます。この尺度で見ると、ブローカーAのEURUSDは、ブローカーBと比べてリターンに伴うリスクがより高いことがわかります
#The variance of returns is not the same across both brokers, broker A is riskier delta_var = str(((combined.iloc[:,-2:].var()[0]-combined.iloc[:,-2:].var()[1]) / combined.iloc[:,-2:].var()[0]) * 100) t = 'Broker A EURUSD Returns Appear to Carry '+ delta_var[:5]+'% Additional Risk.' sns.barplot(combined.iloc[:,-2:].var(),color='black') plt.axhline(np.min(combined.iloc[:,-2:].var()),color='red',linestyle=':') plt.title(t) plt.ylabel('Vriance of Returns')
図7:ブローカーAのリターンはブローカーBより21%多くのリスクを伴うが、この時点でこれらの銘柄を「同じ」と考えられるだろうか
両ブローカーが記録した最大ドローダウンに注目してみても、一貫した結論を導き出すことはできません。両市場で示された最大ドローダウンは、2つのブローカー間で約37%も異なっていました。これらの結果は、ブローカーBがEURUSD市場のボラティリティから顧客を巧みに保護するために、外国為替市場の視点を制限して提供していることを示唆しているように思われます。
#Broker A also demonstrated the largest drawdown ever in our 4 year sample window delta = (((combined.iloc[:,-2:].min()[0]-combined.iloc[:,-2:].min()[1]) / combined.iloc[:,-2:].min()[0]) *100) delta_s = str(delta) t = 'The Largest Negative 10-Day EURUSD Return Grew By: ' + delta_s[:5] + ' %' sns.barplot(combined.iloc[:,-2:].min(),color='black') plt.axhline(np.max(combined.iloc[:,-2:].min()),color='red',linestyle=':') plt.title(t) plt.ylabel('Return')
図8:ブローカーAのリターンは最大ドローダウン36.79%を記録し、ブローカーBの最大ドローダウンを大きく上回る
両ブローカーが生成した10日間のEURUSDリターンの分布を重ねてみると、両ブローカーが市場について同じ見解を提供しているわけではないことが明確にわかります。議論の冒頭で説明した通り、各ブローカーは価格フィードを自由に取得できるソースを選択できます。この分散型の価格設定スキームにより、各ブローカーは特定の市場に対して任意に異なる見解を提供している可能性があるのです。
sns.histplot(((combined.iloc[:,-2]-combined.iloc[:,-2].mean())/combined.iloc[:,-2].std()),color='black') sns.histplot(((combined.iloc[:,-1]-combined.iloc[:,-1].mean())/combined.iloc[:,-1].std()),color='red') plt.xlabel('Std. Deviations From The Expected Return') plt.ylabel('Frequency') plt.title('Comparing The Distribution of 10-Day EURUSD Returns Between 2 Brokers') plt.grid() plt.legend(['Broker A','Broker B'])
図9:2つの市場で生み出される収益の分布を比較する
さらに、ブローカー間の相関水準を分析すると、市場価格同士の相関は低いことがわかります。つまり、先に述べた通り、これら2つの特定のブローカー間では、半分以上の時間において価格水準が逆方向に動く可能性があるということです。
sns.heatmap(combined.loc[:,inputs].corr(),annot=True)
図10:相関水準を可視化すると、両ブローカーの銘柄は、ほとんどの時間においてほぼ独立して動いていることがわかる
では、ブローカー間で予測能力が一貫しているかどうかを確認してみましょう。
from sklearn.model_selection import train_test_split,TimeSeriesSplit,cross_val_score from sklearn.linear_model import Ridge
時系列検証オブジェクトを作成します。
tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)
使用する新しいモデルを返すメソッドを記述します。
def get_model(): return(Ridge())データを分割し、シャッフルされないように注意してください。
train , test = train_test_split(combined,shuffle=False,test_size=0.5)
意図的にすべて0で埋めた列を使用した場合の誤差レベルを記録します。これにより、モデルは常にターゲットの平均値を予測することになります。すべての入力が0の場合、線形モデルは切片を予測することを思い出してください。簡単に言えば、このモデルは「常に市場平均リターンを予測した場合に、この市場でどの程度のパフォーマンスが可能か」を示してくれます。このモデルを上回れない場合、私たちには市場を予測するスキルがないことを意味します。
このベンチマークはTSSと呼ばれます。TSSについては、議論の冒頭で定義しました。ここでの目的は、両ブローカーにおけるTSSを測定し、さらにこのベンチマークを上回る能力をブローカー間で比較することです。
broker_a_tss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,['Null']],train.loc[:,'A Target'],scoring='neg_mean_squared_error',n_jobs=-1,cv=tscv))) broker_a_rss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,inputs[0:(len(inputs)//2)]],train.loc[:,'A Target'],scoring='neg_mean_squared_error',n_jobs=-1,cv=tscv))) broker_b_tss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,['Null']],train.loc[:,'B Target'],scoring='neg_mean_squared_error',n_jobs=-1,cv=tscv))) broker_b_rss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,inputs[(len(inputs)//2):]],train.loc[:,'B Target'],scoring='neg_mean_squared_error',n_jobs=-1,cv=tscv)))
驚くべきことに、ブローカーBではブローカーAよりもTSSを上回ることが容易です。これは、将来の10日間のEURUSDリターンは、ブローカーを移るごとに必ずしも効率的ではないことを意味します
res = [(broker_a_rss/broker_a_tss),(broker_b_rss/broker_b_tss)] eff = str(((res[0] - res[1])/res[1]) * 100) t = 'The EURUSD Appears ' + eff[0:4] + '% Easier To Forecast With Broker B' sns.barplot(res,color='black') plt.axhline(np.min(res),color='red',linestyle=':') plt.ylabel('5-Fold Cross Valiated Ratio of RSS/TSS ') plt.title(t) plt.xticks([0,1],['Broker A','Broker B'])
図11:ブローカーBを使用すると、10日間の将来のEURUSDリターンを予測しやすくなる
どのブローカーに注目するかを決定したので、ブローカーBから受け取った入力データを選択します。
b_inputs = inputs[len(inputs)//2:]
それでは、まったく新しいモデルを構築してみましょう。
from sklearn.ensemble import GradientBoostingRegressor model = GradientBoostingRegressor()
ブローカーBから取得したすべてのデータにモデルを適合させます。
model.fit(train.loc[:,b_inputs[:-2]],train['B Target'])それでは、AIモデルをONNX形式でエクスポートできるように準備しましょう。これにより、作成したモデルをMQL5アプリケーションに容易に統合できるようになります。
import skl2onnx,onnxONNXモデルが受け入れる入力の数を定義します。
initial_types = [('float_input',skl2onnx.common.data_types.FloatTensorType([1,4]))]ONNXモデルをONNXプロトタイプに変換します。
onnx_proto = skl2onnx.convert_sklearn(model,initial_types=initial_types,target_opset=12)
ONNXプロトタイプをディスクに保存します。
onnx.save(onnx_proto,"EURUSD GBR D1.onnx")
MQL5の始め方
ONNXモデルの準備ができたので、MQL5アプリケーションの構築を開始できます。まず、必要なライブラリをロードします。//+------------------------------------------------------------------+ //| EURUSD.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System Constants Definitions | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade;
また、システム定数も必要です。これは、議論の前半で定義した重要なパラメータ、たとえば10日間リターン期間などが、アプリケーションに正しく反映されるようにするためです。
//+------------------------------------------------------------------+ //| System Constants Definitions | //+------------------------------------------------------------------+ #define ONNX_INPUT_SHAPE 4 #define ONNX_OUTPUT_SHAPE 1 #define SYSTEM_TIME_FRAME PERIOD_D1 #define RETURN_PERIOD 10 #define TRADING_VOLUME SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN)ONNXファイルをシステムリソースとして読み込み、アプリケーションでコンパイルされるようにします。
//+------------------------------------------------------------------+ //| System Resources | //+------------------------------------------------------------------+ #resource "\\Files\\Broker Manipulation\\EURUSD GBR D1.onnx" as const uchar onnx_proto[];
取引戦略を実行するには、いくつかのグローバル変数が必要になります。
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ long model; int position_timer; double bid,ask; double o,h,l,c; bool bullish; double sl_width;
システムを初めて初期化するときに、ONNXモデルを設定し、取引戦略の重要なグローバル変数をリセットします。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- model = OnnxCreateFromBuffer(onnx_proto,ONNX_DATA_TYPE_FLOAT); ulong input_shape[] = {1,ONNX_INPUT_SHAPE}; ulong output_shape[] = {1,ONNX_OUTPUT_SHAPE}; if(model == INVALID_HANDLE) { Comment("Failed To Load EURUSD Auto-Encoder-Decoder: ",GetLastError()); return(INIT_FAILED); } if(!OnnxSetInputShape(model,0,input_shape)) { Comment("Failed To Set EURUSD Auto-Encoder-Decoder Input Shape: ",GetLastError()); return(INIT_FAILED); } else if(!OnnxSetOutputShape(model,0,output_shape)) { Comment("Failed To Set EURUSD Auto-Encoder-Decoder Output Shape: ",GetLastError()); return(INIT_FAILED); } position_timer = 0; sl_width = 30; //--- return(INIT_SUCCEEDED); }
取引戦略を使用しなくなった場合は、ONNXモデルによって消費されていたリソースを解放します。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- OnnxRelease(model); }
価格が更新されるたびに、新しい価格水準を1日ごとに記録します。その後、保有ポジションがない場合は、モデルから予測を取得し、それに従って取引をおこないます。一方、すでにポジションを保有している場合は、可能であればストップロスをトレールしつつ、各取引の10日間の保有期間をカウントダウンします
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- static datetime time_stamp; datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0); if(time_stamp != current_time) { time_stamp = current_time; ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); o = iOpen(Symbol(),SYSTEM_TIME_FRAME,1); h = iHigh(Symbol(),SYSTEM_TIME_FRAME,1); l = iLow(Symbol(),SYSTEM_TIME_FRAME,1); c = iClose(Symbol(),SYSTEM_TIME_FRAME,1); bullish = (o < c) && (c > iClose(Symbol(),SYSTEM_TIME_FRAME,2)); if(PositionsTotal() == 0) { position_timer = 0; find_setup(); } else if(PositionsTotal() > 0) { if(PositionSelect(Symbol())) { long position_type = PositionGetInteger(POSITION_TYPE); double current_sl = PositionGetDouble(POSITION_SL); double new_sl; //--- Buy Trades if(position_type == POSITION_TYPE_BUY) { new_sl = bid - ((h-l)*sl_width); if(new_sl > current_sl) Trade.PositionModify(Symbol(),new_sl,0); } //--- Sell Trades else if(position_type == POSITION_TYPE_SELL) { new_sl = ask + ((h-l)*sl_width); if(new_sl < current_sl) Trade.PositionModify(Symbol(),new_sl,0); } } if(position_timer < RETURN_PERIOD) position_timer+=1; else Trade.PositionClose(Symbol()); } } }
最後に、この関数はモデルから予測を取得し、有効な取引機会があるかどうかを確認します。
//+------------------------------------------------------------------+ //| Find A Trading Setup | //+------------------------------------------------------------------+ void find_setup(void) { vectorf model_inputs(ONNX_INPUT_SHAPE); model_inputs[0] = (float) iOpen(Symbol(),SYSTEM_TIME_FRAME,0); model_inputs[1] = (float) iHigh(Symbol(),SYSTEM_TIME_FRAME,0); model_inputs[2] = (float) iLow(Symbol(),SYSTEM_TIME_FRAME,0); model_inputs[3] = (float) iClose(Symbol(),SYSTEM_TIME_FRAME,0); vectorf model_output(ONNX_OUTPUT_SHAPE); if(!OnnxRun(model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_output)) { Comment("Failed To Get A Prediction From Our Model: ",GetLastError()); return; } else { Comment("Prediction: ",model_output[0]); vector open,close; open.CopyRates(Symbol(),SYSTEM_TIME_FRAME,COPY_RATES_OPEN,1,2); close.CopyRates(Symbol(),SYSTEM_TIME_FRAME,COPY_RATES_CLOSE,1,2); if(open.Mean() < close.Mean()) { if((model_output[0] > 0) && (bullish)) Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid - ((h-l) * sl_width)),0); } else if(open.Mean() > close.Mean()) { if((model_output[0] < 0) && (!bullish)) Trade.Sell(TRADING_VOLUME,Symbol(),bid,(ask + ((h-l) * sl_width)),0); } } }
アプリケーションで作成したシステム定数を必ず定義解除してください。
//+------------------------------------------------------------------+ //| Undefine System Constants | //+------------------------------------------------------------------+ #undef ONNX_INPUT_SHAPE #undef ONNX_OUTPUT_SHAPE #undef SYSTEM_TIME_FRAME #undef TRADING_VOLUME #undef RETURN_PERIOD //+------------------------------------------------------------------+
バックテストに使用する日付期間は、モデルの学習期間とはアウトオブサンプルになります。これらの日付は、ブローカーAとブローカーBの両方のテストで一定に保持されます。ブローカーBはMQL5開発者がアプリケーションを構築する際に使用するブローカーを象徴し、ブローカーAは顧客が最終的にアプリケーションを展開する可能性のあるブローカーを象徴することを思い出してください
図12:テスト期間の入力日を選択する
上の図12および下の図13で指定された両方の設定は、実施する両方のテストで固定されます。
図13:戦略の能力を現実的に予測するために、挑戦的なモデリング設定を選択する
図14に示すように、ブローカーBでテストした場合、戦略は有望に見えます。アウトオブサンプルデータに対してもうまく対応しており、戦略をさらに洗練させて最大限のパフォーマンスを引き出すために時間をかける価値があることを示唆しています。しかし、読者に伝えたいポイントは、あるブローカーで改善した内容が、他のブローカーでも同様に効果を発揮するとは限らないということです。
図14:戦略を意図したブローカーに適用した場合、生成されたエクイティカーブは上昇傾向を示している
しかし、同じ戦略をブローカーAに適用したところ、ブローカーBで観察した口座残高の上昇トレンドはもはや見られませんでした。基盤となるモデルを変更せずにブローカーを変えるだけでは、この戦略は明らかにほとんど価値を提供しません。開発者は、これが必ずしも自分の責任ではないことを理解する必要があります。存在するすべてのブローカーに対してモデルをカスタマイズすることは、どの開発者にとっても非常に困難な作業です。
しかし、これは問題を視覚的に理解するための一つの方法です。開発者と顧客は、関係性が十分に定義されていなければ、まったく異なる理解のもとで行動している可能性があります。
図15:戦略をブローカーAで展開した場合、苦労して実現した口座残高の上昇トレンドを再現することはできない
図16でブローカーBにおけるパフォーマンスのより詳細な分析を確認し、これを図17のブローカーAでのモデルのパフォーマンスと対比することもできます。
図16:対象ブローカーに焦点を当てた場合の取引パフォーマンスの詳細な分析
複数のブローカーで有意に機能する戦略を構築することが、決して些細な問題ではないことは明らかです。機械学習モデルが複雑になるにつれて、入力のわずかな変化にも敏感になっていきます。銘柄の数値的特性のこれらの変動は、取引戦略を共有し、意味のある形で再現する能力に壊滅的な影響を与える可能性があります。
図17:訓練に使用していないブローカー上で戦略を適用しようとした場合の詳細な分析
結論
世界の金融市場が分散型であるという性質は、私たちのコミュニティが互いの成果を再現することを困難にする現実的な制約を課しています。ブローカーは価格が一致することを保証しておらず、つまり、あなたが自分のブローカーで利用している戦略で得られる非効率性が、同じ戦略を同じ銘柄で私のブローカーで実行した場合には存在しない可能性があるのです。
コミュニティ内での役割に応じて、これらの知見は実務的な意味を持ちます。
- MQL5 Webサイトのフリーランスセクションを利用するのが好きな方は、アプリケーションを依頼する際には自分のブローカーを明示し、開発者にデモ口座を作成させることで、自分に最適化されたソリューションを受け取るようにしてください。「EURUSD取引アプリケーション求む」のような漠然とした依頼は避けるべきです。これまで見てきたように、できるだけ詳細に依頼内容を指定する方が安全です。
- マーケットプレイスで頻繁にアプリケーションを購入するユーザーは、ブローカーごとに最適化された製品が、汎用を謳う製品よりも高い価値を提供する理由を理解できるでしょう。
- シグナル購読者は、同じブローカーを使用するシグナルプロバイダーを選ぶことで、報告されるリターンと実現リターンが常に一致するようにし、満足度を最大化できます。
- 最後に、MQL5開発者の皆さんは、一貫した製品と信頼性の高いサービスを提供し、クライアントを満足させるために何が必要かをより明確に理解できるようになります。
これらの課題を認識することで、コミュニティ全体に利益をもたらす、より再現性の高いブローカー特化型ソリューションに向けて取り組むことができます。本記事は、1つのONNXモデルを異なるブローカー間で共有しようとすることの危険性を示すための事例として作成されました。 MQL5開発者として、私たちはより高い実務標準に責任を持ち、顧客をそのような危険にさらさないよう努めるべきだと考えます。
ファイル名 | ファイルの説明 |
---|---|
Requesting Broker Data.ipynb | 2つのブローカーから日次EURUSD過去データを取得するために使用したJupyter Notebook |
Analyzing Broker Data.ipynb | 2つのブローカーの過去日次EURUSDデータの一貫性をテストするために使用したJupyter Notebook |
EURUSD.mq5 | 同じモデルに基づき2つの異なるブローカーでの収益性を評価するために作成したエキスパートアドバイザー(EA) |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/18133
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索