機械学習の限界を克服する(第7回):自動戦略選択
多くのトレーダーは、学び始めたときに、リスク許容度に合った戦略を選ぶよう助言されます。これは妥当ですが、本記事ではまず戦略を理想的な好みではなく、期待されるパフォーマンスに基づいて発見すべきであることを提案します。収益性の高い戦略を特定することは、経験の有無にかかわらずアルゴリズムトレーダーにとって普遍的な課題です。新しい戦略やインジケーター、エキスパートアドバイザー(EA)が急速に増え続けるため、その難しさはさらに増しています。
私たちは、かつてないほど情報が相互接続された時代に生きています。しかし、新しいアイデアが、トレーダーが評価するよりも速いペースで広がった場合、どうすればよいでしょうか。無数の戦略の中から、テストに値する候補を自動的に特定するにはどうすれば良いでしょうか。すべての組み合わせを総当たりで試すことなく、潜在的に収益性のある戦略の組み合わせを発見することは可能でしょうか。
本記事では、これらの問いに答えるために、2つの補完的なアプローチを提案します。
- ホワイトボックスソリューション:期待リターンに対して行列分解(特に特異値分解SVD)を適用し、現在の市場条件によって正の影響を受ける戦略の組み合わせを特定します。
- ブラックボックスソリューション:ディープニューラルネットワークを用いて、観測された市場の挙動に基づき戦略を動的に選択します。
私たちのソリューションは、手元にある取引戦略を実行した場合に得られたであろうリターンを推定できる能力に依存しています。数値計算の手法を用いて、各戦略から得られる期待収益を学習します。任意の戦略から生成されるリターンを近似することで、貴重な洞察を得ることができます。
必要なデータの取得
まず、必要な市場データを取得するMQL5スクリプトを作成します。通常の市場データを取得するだけでなく、インジケーターの入力に関連するデータも取得します。これにより、ONNXモデルが、本番環境で使用されるインジケーター計算と同じデータで学習できるようになります。
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2020, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs //--- Define our moving average indicator #define MA_PERIOD 5 //--- Moving Average Period #define MA_TYPE MODE_SMA //--- Type of moving average we have #define RSI_PERIOD 15 //--- RSI Period #define STOCH_K 5 //--- Stochastich K Period #define STOCH_D 3 //--- Stochastich D Period #define STOCH_SLOWING 3 //--- Stochastic slowing #define STOCH_MODE MODE_EMA //--- Stochastic mode #define STOCH_PRICE STO_LOWHIGH //--- Stochastic price feeds #define HORIZON 5 //--- Forecast horizon //--- Our handlers for our indicators int ma_handle,ma_o_handle,ma_h_handle,ma_l_handle,rsi_handle,stoch_handle; //--- Data structures to store the readings from our indicators double ma_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[],rsi_reading[],sto_reading_main[],sto_reading_signal[]; //--- File name string file_name = Symbol() + " Market Data As Series Indicators.csv"; //--- Amount of data requested input int size = 3000; //+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { int fetch = size + (HORIZON * 2); //---Setup our technical indicators ma_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_OPEN); ma_h_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_HIGH); ma_l_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_LOW); rsi_handle = iRSI(_Symbol,PERIOD_CURRENT,RSI_PERIOD,PRICE_CLOSE); stoch_handle = iStochastic(_Symbol,PERIOD_CURRENT,STOCH_K,STOCH_D,STOCH_SLOWING,STOCH_MODE,STOCH_PRICE); //---Set the values as series CopyBuffer(ma_handle,0,0,fetch,ma_reading); ArraySetAsSeries(ma_reading,true); CopyBuffer(ma_o_handle,0,0,fetch,ma_o_reading); ArraySetAsSeries(ma_o_reading,true); CopyBuffer(ma_h_handle,0,0,fetch,ma_h_reading); ArraySetAsSeries(ma_h_reading,true); CopyBuffer(ma_l_handle,0,0,fetch,ma_l_reading); ArraySetAsSeries(ma_l_reading,true); CopyBuffer(rsi_handle,0,0,fetch,rsi_reading); ArraySetAsSeries(rsi_reading,true); CopyBuffer(stoch_handle,0,0,fetch,sto_reading_main); ArraySetAsSeries(sto_reading_main,true); CopyBuffer(stoch_handle,0,0,fetch,sto_reading_signal); ArraySetAsSeries(sto_reading_signal,true); //---Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(int i=size;i>=1;i--) { if(i == size) { FileWrite(file_handle, //--- Time "Time", //--- OHLC "Open", "High", "Low", "Close", //--- MA OHLC "MA O", "MA H", "MA L", "MA C", //--- RSI "RSI", //--- Stochastic Oscilator "Stoch Main", "Stoch Signal" ); } else { FileWrite(file_handle, iTime(_Symbol,PERIOD_CURRENT,i), //--- OHLC iOpen(_Symbol,PERIOD_CURRENT,i), iHigh(_Symbol,PERIOD_CURRENT,i), iLow(_Symbol,PERIOD_CURRENT,i), iClose(_Symbol,PERIOD_CURRENT,i), //--- MA OHLC ma_o_reading[i], ma_h_reading[i], ma_l_reading[i], ma_reading[i], //--- RSI rsi_reading[i], //--- Stochastic Oscilator sto_reading_main[i], sto_reading_signal[i] ); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef HORIZON #undef MA_PERIOD #undef MA_TYPE //+------------------------------------------------------------------+
必要なデータの分析
次に、必要な標準Pythonライブラリをインポートします。
#Import the standard libraries import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns
まず、MQL5スクリプトを使用して作成したデータセットを読み込みます。
data = pd.read_csv("../EURUSD Market Data As Series Indicators.csv")
次に、データセットを分割し、意図したバックテスト期間と重複する期間を除外します。重複したデータを含めると、結果の妥当性が損なわれる可能性があります。
#Drop the last 3 years of historical data data = data.iloc[:-(365*3),:] _ = data.iloc[-(365*3):,:]
予測期間がスクリプトで定義された予測期間と一致していることを確認します。
HORIZON = 5次に、実現した市場リターンを計算します。
data['Return'] = data['Close'].shift(-HORIZON) - data['Close'] data.dropna(inplace=True)
各戦略のリターンを推定するには、まずその戦略が市場のどの方向に動くと予測していたかを判断する必要があります。戦略が上昇を予測していた場合はリターンを1に、下降を予測していた場合は-1に設定します。次に、この予測リターンに実際の市場リターンを掛けることで、戦略のパフォーマンスを近似します。リターンが正になるのは、戦略が市場の方向を正しく予測した場合のみです。
data['MA OC Strategy'] = 0 data['MA HL Strategy'] = 0 data['RSI Strategy'] = 0 data['Stochastic Strategy'] = 0 #Moving Average Open and Close strategy data.loc[data['MA O']<data['MA C'],'MA OC Strategy'] = 1 data.loc[data['MA O']>data['MA C'],'MA OC Strategy'] = -1 #Moving average High Low Strategy data.loc[data['Close']>data['MA H'],'MA HL Strategy'] = 1 data.loc[data['Close']<data['MA L'],'MA HL Strategy'] = -1 #RSI Strategy data.loc[data['RSI']>50,'RSI Strategy'] = 1 data.loc[data['RSI']<50,'RSI Strategy'] = -1 #Stoch Main Strategy data.loc[data['Stoch Main']>80,'Stochastic Strategy'] = 1 data.loc[data['Stoch Main']<30,'Stochastic Strategy'] = -1 #Strategy Returns for i in np.arange(4): data.iloc[:,-1*(i+1)]= data.iloc[:,-1*(i+1)] * data['Return'] data.iloc[:,-1*(i+1)]= data.iloc[:,-1*(i+1)].cumsum() data['Return'] = data['Return'].cumsum()
また、市場リターンが複数の時間ステップにわたってどのように変化するかも確認したいと考えています。
data['MA OC 1'] = data['MA OC Strategy'].shift(-1) data['MA OC 2'] = data['MA OC Strategy'].shift(-HORIZON) data['MA HL 1'] = data['MA HL Strategy'].shift(-1) data['MA HL 2'] = data['MA HL Strategy'].shift(-HORIZON) data['RSI 1'] = data['RSI Strategy'].shift(-1) data['RSI 2'] = data['RSI Strategy'].shift(-HORIZON) data['Stochastic 1'] = data['Stochastic Strategy'].shift(-1) data['Stochastic 2'] = data['Stochastic Strategy'].shift(-HORIZON) data.dropna(inplace=True) data
次に、入力データとターゲットデータを分離します。
X = data.iloc[:,1:12] y = data.iloc[:,-8:]
次に、戦略の予想リターンを可視化してみます。図1に示すように、最初の時点では4つの戦略すべてが収益性がないように見えます。しかし、この情報も依然として価値があります。
plt.plot(data.iloc[:,-12:-8]) plt.legend(data.columns[-12:-8]) plt.grid() plt.title('Estimating The Effectiveness of Different Strategies') plt.ylabel('Estimated Profit Level') plt.xlabel('Historical Training Epochs')

図1:現在の形での各独立戦略のリターンを可視化する
次に、戦略リターンに対して特異値分解(SVD)を実行します。
#Analyze the returns U,S,VT = np.linalg.svd(data.iloc[:,-12:-8])
SVDはデータ内の基盤となる構造を明らかにします。本議論では、特に戦略が示す独自の変動モードの数に注目します。各変動モードは、市場が取り得る異なる挙動パターンを反映しています。
本質的に、SVDは戦略リターンの独立した組み合わせのセットを返し、それぞれが市場の特定の挙動下でポートフォリオのパフォーマンスを最大化する組み合わせを表します。私たちは一般的に、総変動の少なくとも80%を説明する最小限の支配的モードの数に注目します。
numpyのSVD関数から得られるS行列(Sigma)は特異値を含み、各主成分がどれだけの変動を説明しているかを示します。図2では、特異値の累積和をL1ノルムで正規化してプロットしています。このプロットから、最初の2つの特異値が総変動の80%以上を占めていることが分かります。つまり、最初の2つの主成分が支配的であることを示しています。
#Standardize and scale the singular values sigma_scaled = S / np.linalg.norm(S,1) sns.barplot(np.cumsum(sigma_scaled),color='black') plt.axhline(0.8,linestyle='--',color='red') plt.title('Number of Singular Values Needed To Capture 80% of Variance') plt.ylabel('Proportion of Variance Explained') plt.xticks([0,1,2,3],['First Total','Second Total','Third Total','Total']) plt.xlabel('Number of Singular Values Needed To Recreate The Original Dataset')

図2:データセットの変動の80%を捉えるには最初の2つの主成分のみで十分である
次に、戦略リターン間の相関関係を確認することもできます。特に、移動平均戦略とRSI戦略は強い正の相関を示しており、有用な洞察を提供する可能性があります。
data.iloc[:,-12:-8].corr()

図3:利用可能な市場データ入力の相関行列を可視化する
支配的な主成分を特定した後でも、各主成分に対してどの戦略が正の寄与をしているかを判断する必要があります。これらの寄与は主成分負荷量(principal component loadings)として知られています。支配的な主成分に対して正の負荷量を持つ戦略に注目します。これらの戦略は、市場が対応する挙動を示した場合に良好なパフォーマンスを発揮すると予想されます。
VT
array([[ 0.64587337, 0.37029478, 0.63092801, 0.21830991],
[ 0.10444288, -0.33948578, 0.38679319, -0.85101828],
[ 0.64575265, 0.19765237, -0.67157641, -0.30483139],
[ 0.39362773, -0.84176287, -0.03613857, 0.36767716]])
最後に、選択した組み合わせによって生成された戦略リターンを図4にプロットします。
plt.plot(data.iloc[:,13]+data.iloc[:,14]+data.iloc[:,15]+data.iloc[:,16],color='red') plt.plot(data.iloc[:,13]+data.iloc[:,15],color='Orange') plt.plot(data.iloc[:,13]+data.iloc[:,14],color='Green') plt.plot(data.iloc[:,13]+data.iloc[:,16],color='Blue') plt.legend(['High Risk','Medium Risk','Low Risk','Minimal Risk']) plt.grid() plt.title('Estimating The Returns Produced by Each of Our Risk Settings') plt.ylabel('Estimated Profit') plt.xlabel('Historical Epochs')

図4:SVDによって得られた新しいリターンの推移を可視化する
MQL5での戦略の実装
これで、MQL5で取引アプリケーションを実装する準備が整いました。記事での標準的な手順として、まずシステム定数を定義し、アプリケーションがモデリング段階で設定した期待通りに動作することを保証します。//+------------------------------------------------------------------+ //| Automatic Strategy Selection.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 definiyions | //+------------------------------------------------------------------+ #define MA_PERIOD 5 //--- Moving Average Period #define MA_TYPE MODE_SMA //--- Type of moving average #define RSI_PERIOD 15 //--- RSI Period #define STOCH_K 5 //--- Stochastich K Period #define STOCH_D 3 //--- Stochastich D Period #define STOCH_SLOWING 3 //--- Stochastic slowing #define STOCH_MODE MODE_EMA //--- Stochastic mode #define STOCH_PRICE STO_LOWHIGH //--- Stochastic price feeds #define TOTAL_STRATEGIES 4 //--- Total strategies we have to choose from
次に、市場ポジションを管理するための取引ライブラリを読み込み、アプリケーションのライフサイクル全体で使用するグローバル変数を定義します。
//+------------------------------------------------------------------+ //| System libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade;
まず、テクニカル指標およびその出力用の変数を設定します。次に、時間データやティックデータを格納するMqlオブジェクトを定義します。最後に、主成分の重みを保持する配列を宣言します。先に述べた通り、識別されたモードに対して正の反応を示す戦略には重み1を、その他の戦略には重み0を割り当てています。
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_c_handle,ma_o_handle,ma_h_handle,ma_l_handle,rsi_handle,stoch_handle,atr_handle; double ma_c_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[],rsi_reading[],sto_reading_main[],sto_reading_signal[],atr_reading[]; double long_vote,short_vote; MqlDateTime ts,tc; MqlTick current_tick; double const weights_1 [] = {1,1,1,1}; double const weights_2 [] = {1,0,1,0}; double const weights_3 [] = {1,1,0,0}; double const weights_4 [] = {1,0,0,1}; double selected_weights[] = {0,0,0,0};
次に、アプリケーションがどのモードで動作するかをユーザーが選択できるように、カスタム列挙型を定義します。新しい戦略を検証することが目的であるため、SVDによって提案された4つの戦略をテストする方が、すべての組み合わせを手動で評価するよりも簡単です。
//+------------------------------------------------------------------+ //| Custom enumrations | //+------------------------------------------------------------------+ enum operation_modes { HIGH=0, //High Risk MID=1, //Medium Risk LOW=2, //Low Risk MINIMUM=3 //Minimum Risk };
さらに、これら4つの戦略構成を順番に試すことができる入力パラメータも定義します。
//+------------------------------------------------------------------+ //| User inputs | //+------------------------------------------------------------------+ input group "User Risk Settings" input operation_modes user_mode = 1;//Define Your Risk Settings
アプリケーション起動時には、switch文を使用して、ユーザーが選択した重みをゼロで初期化されたweights配列に読み込みます。その後、時間データおよびテクニカル指標をそれに応じて設定します。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Setup our risk settings switch(user_mode) { case(0): Print("High risk mode selected"); ArrayCopy(selected_weights,weights_1,0,0,WHOLE_ARRAY); break; case(1): Print("Medium risk mode selected"); ArrayCopy(selected_weights,weights_2,0,0,WHOLE_ARRAY); break; case(2): Print("Low risk mode selected"); ArrayCopy(selected_weights,weights_3,0,0,WHOLE_ARRAY); break; case(3): Print("Minimum risk mode selected"); ArrayCopy(selected_weights,weights_4,0,0,WHOLE_ARRAY); break; default: Print("No risk mode selected! No Trades will be placed"); break; } //--- Setup the time TimeLocal(tc); TimeLocal(ts); //---Setup our technical indicators ma_c_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_OPEN); ma_h_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_HIGH); ma_l_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_LOW); atr_handle = iATR(_Symbol,PERIOD_CURRENT,14); rsi_handle = iRSI(_Symbol,PERIOD_CURRENT,RSI_PERIOD,PRICE_CLOSE); stoch_handle = iStochastic(_Symbol,PERIOD_CURRENT,STOCH_K,STOCH_D,STOCH_SLOWING,STOCH_MODE,STOCH_PRICE); //--- return(INIT_SUCCEEDED); }
アプリケーションの使用が終了した際には、確保したテクニカルインジケーターをすべて解放します。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(ma_c_handle); IndicatorRelease(ma_o_handle); IndicatorRelease(ma_h_handle); IndicatorRelease(ma_l_handle); IndicatorRelease(rsi_handle); IndicatorRelease(stoch_handle); IndicatorRelease(atr_handle); }
新しい価格データが到着し、新しい日が始まったら、時間データとインジケーターの値を更新します。ポジションが開いていない場合、システムは重みが1のすべての戦略の間で投票をおこないます。多数決によって、ロングエントリーかショートエントリーかが決定されます。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- TimeLocal(ts); if(ts.day != tc.day) { //--- Update the time TimeLocal(tc); //--- Update Our indicator readings CopyBuffer(ma_c_handle,0,0,1,ma_c_reading); CopyBuffer(ma_o_handle,0,0,1,ma_o_reading); CopyBuffer(ma_h_handle,0,0,1,ma_h_reading); CopyBuffer(ma_l_handle,0,0,1,ma_l_reading); CopyBuffer(rsi_handle,0,0,1,rsi_reading); CopyBuffer(stoch_handle,0,0,1,sto_reading_main); CopyBuffer(stoch_handle,0,0,1,sto_reading_signal); CopyBuffer(atr_handle,0,0,1,atr_reading); //--- Copy Market Data double close = iClose(Symbol(),PERIOD_CURRENT,0); SymbolInfoTick(Symbol(),current_tick); //--- Place a position if(PositionsTotal() ==0) { //--- Our strategies will vote on what should be done long_vote = 0; short_vote = 0; for(int i =0; i<TOTAL_STRATEGIES;i++) { //--- Is the strategy's vote valid? if(selected_weights[i] > 0) { //--- Moving average open close strategy if(i == 0) { if(ma_o_reading[0] > ma_c_reading[0]) long_vote += selected_weights[0]; else if(ma_o_reading[0] < ma_c_reading[0]) short_vote += selected_weights[0]; } //--- Moving average high low strategy if(i == 1) { if(close > ma_h_reading[0]) long_vote += selected_weights[1]; else if(close < ma_l_reading[0]) short_vote += selected_weights[1]; } //--- RSI Strategy if(i == 2) { if(rsi_reading[0] > 50) long_vote += selected_weights[2]; else if(rsi_reading[0] < 50) short_vote += selected_weights[2]; //--- Stochastic Strategy if(i == 3) { if(sto_reading_main[0] > 50) long_vote += selected_weights[3]; else if(sto_reading_main[0] < 50) short_vote += selected_weights[3]; } } } } if(long_vote > short_vote) Trade.Buy(0.01,Symbol(),current_tick.ask,current_tick.ask-(1.5*atr_reading[0]),current_tick.ask+(1.5*atr_reading[0])); if(long_vote < short_vote) Trade.Sell(0.01,Symbol(),current_tick.bid,current_tick.bid+(1.5*atr_reading[0]),current_tick.bid-(1.5*atr_reading[0])); } } } //+------------------------------------------------------------------+
アプリケーションの最後に、不要になったすべてのシステム定数を未定義にします。
//+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef MA_PERIOD #undef MA_TYPE #undef RSI_PERIOD #undef STOCH_K #undef STOCH_D #undef STOCH_SLOWING #undef STOCH_MODE #undef STOCH_PRICE #undef TOTAL_STRATEGIES //+------------------------------------------------------------------+
結果の分析
これで、アプリケーションの結果を分析できます。まず、モデル開発に使用した履歴データの範囲外の日付を選択します。

図5:取引アプリケーションのベースライン版のバックテスト日を選択する
次に、実際の市場の条件を模擬するため、実際のティックデータにランダム遅延を加えてモデリングを構成します。

図6:「ランダム遅延」設定を選択することで、バックテストが実際の市場条件を模擬することを保証する
その後、探索する入力パラメータを指定します。候補戦略が4つしかないため、遺伝的オプティマイザにこれら4つの入力を対象にラインサーチをおこなわせます。

図7:遺伝的オプティマイザが探索する最小値と最大値を選択する
図8では、SVDによって提案された最初の2つの戦略は収益性があり、残りの2つは信頼性が低いことが分かります。図2で確認した通り、これら2つの主成分は学習データの変動の80%以上を説明していました。これは、市場が2つの安定した挙動モードによって主導されており、その他のモードは弱く不安定であることを示唆しています。

図8:市場データの履歴バックテスト結果の分析
結果の改善
次に、ブラックボックスソリューションに移ります。このアプローチでは、現在の市場状況に基づいて最適な戦略を特定することを目指します。
import onnx from sklearn.linear_model import Ridge from sklearn.neural_network import MLPRegressor from skl2onnx.convert import convert_sklearn from skl2onnx.common.data_types import FloatTensorType from sklearn.model_selection import RandomizedSearchCV,TimeSeriesSplit
まず、必要なライブラリを読み込み、ディープニューラルネットワークの学習時に慎重にクロスバリデーションを適用できるよう、カスタムの時系列検証オブジェクトを定義します。
tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)次に、探索するニューラルネットワークのパラメータを指定します。パラメータ空間が大きいため、潜在的な解を含むと予想されるサブセットのみを探索します。このために、ホワイトボックスアプローチよりも設定が難しくなります。
dist = {
'max_iter':[10,50,100,500,1000,5000,10000,50000,100000],
'activation':['tanh','relu','identity','logistic'],
'alpha':[10e0,10e-1,10e-2,10e-3,10e-4,10-5,10e-6],
'solver':['lbfgs','adam','sgd'],
'learning_rate':['constant','invscaling','adaptive'],
'hidden_layer_sizes':[(11,1),(11,11),(11,11,11),(11,11,11,11),(11,22,33,44),(11,22,55,22,11),(11,100,11),(11,5,2,5,11),(11,3,9,18,9,3)]
}その後、実験間で固定される基本的なニューラルネットワークパラメータを定義します。
model = MLPRegressor(shuffle=False,early_stopping=False,random_state=0,verbose=True)
設定が完了したら、最適なパラメータの探索をおこないます。ランダムサーチにおけるn_iterパラメータの重要性に注意してください。反復回数を増やすことで探索の品質は一般的に向上します。
rscv = RandomizedSearchCV(model,dist,random_state=0,n_iter=20,scoring='neg_mean_squared_error',cv=tscv,n_jobs=-1,refit=True)
検索を開始します。
res = rscv.fit(X,y)
Iteration 1, loss = 0.21844802
Iteration 2, loss = 0.13287107
Iteration 3, loss = 0.08159530
Iteration 4, loss = 0.07053761
Iteration 5, loss = 0.07051259
探索が完了すると、最適なモデルはbest_estimator_属性に格納されます。
res.best_estimator_

図9:ランダムサーチ手順で探索された最適なニューラルネットワーク(議論のために許可した反復内)
ONNXにエクスポートする前に、ニューラルネットワークの入力と出力の形状を定義します。
initial_types = [('float_input',FloatTensorType([1,X.shape[1]]))] final_types = [('float_output',FloatTensorType([y.shape[1],1]))]
次に、モデルをONNXプロトタイプとして保存します。
onnx_proto = convert_sklearn(model=res.best_estimator_,initial_types=initial_types,final_types=final_types,target_opset=12)最後に、.onnx拡張子でディスクにモデルを書き込みます。
onnx.save(onnx_proto,'Unsupervised Strategy Selection MLP.onnx')
改善内容の実装
まず、ONNXモデルを読み込みます。
//+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\USS\\Unsupervised Strategy Selection MLP.onnx" as const uchar onnx_buffer[];
ONNXモデルの入力と出力の形状を定義します。
#define ONNX_INPUTS 11 //--- Total inputs needed by our ONNX model #define ONNX_OUTPUTS 8 //--- Total outputs needed by our ONNX model
モデルの入力および出力を扱うために、いくつかの追加グローバル変数を定義します。
long onnx_model; vectorf onnx_features,onnx_targets;
アプリケーション初期化時には、バッファからONNXモデルをロードし、Pythonで定義した入力と出力の形状を設定します。これらを確認し、モデルが有効であることを確認した後に処理を進めます。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Prepare the model's inputs and outputs onnx_features = vectorf::Zeros(ONNX_INPUTS); onnx_targets = vectorf::Zeros(ONNX_OUTPUTS); //--- Create the ONNX model onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DATA_TYPE_FLOAT); //--- Define the I/O shape ulong input_shape[] = {1,ONNX_INPUTS}; ulong output_shape[] = {ONNX_OUTPUTS,1}; if(!OnnxSetInputShape(onnx_model,0,input_shape)) { Print("Failed to define ONNX input shape"); return(INIT_FAILED); } if(!OnnxSetOutputShape(onnx_model,0,output_shape)) { Print("Failed to define ONNX output shape"); return(INIT_FAILED); } //--- Check if the model is valid if(onnx_model == INVALID_HANDLE) { Print("Failed to create our ONNX model from buffer"); return(INIT_FAILED); } //--- Setup the time TimeLocal(tc); TimeLocal(ts); //---Setup our technical indicators ma_c_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_OPEN); ma_h_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_HIGH); ma_l_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_LOW); atr_handle = iATR(_Symbol,PERIOD_CURRENT,14); rsi_handle = iRSI(_Symbol,PERIOD_CURRENT,RSI_PERIOD,PRICE_CLOSE); stoch_handle = iStochastic(_Symbol,PERIOD_CURRENT,STOCH_K,STOCH_D,STOCH_SLOWING,STOCH_MODE,STOCH_PRICE); //--- return(INIT_SUCCEEDED); }
ONNXモデルが不要になった際には、リソースを解放します。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- OnnxRelease(onnx_model); }
新しい価格データが到着すると、インジケーターバッファを更新し、ONNXが要求する形式にすべての入力をfloat型に変換します。モデルは2ステップ先の累積リターンを予測するため、累積残高の傾きが正である場合、最も期待傾きの大きい戦略に沿って取引をおこないます。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- TimeLocal(ts); if(ts.day != tc.day) { //--- Update the time TimeLocal(tc); //--- Update Our indicator readings CopyBuffer(ma_c_handle,0,0,1,ma_c_reading); CopyBuffer(ma_o_handle,0,0,1,ma_o_reading); CopyBuffer(ma_h_handle,0,0,1,ma_h_reading); CopyBuffer(ma_l_handle,0,0,1,ma_l_reading); CopyBuffer(rsi_handle,0,0,1,rsi_reading); CopyBuffer(stoch_handle,0,0,1,sto_reading_main); CopyBuffer(stoch_handle,0,0,1,sto_reading_signal); CopyBuffer(atr_handle,0,0,1,atr_reading); //--- Set our model inputs onnx_features[0] = (float) iOpen(Symbol(),PERIOD_CURRENT,0); onnx_features[1] = (float) iHigh(Symbol(),PERIOD_CURRENT,0); onnx_features[2] = (float) iLow(Symbol(),PERIOD_CURRENT,0); onnx_features[3] = (float) iClose(Symbol(),PERIOD_CURRENT,0); onnx_features[4] = (float) ma_o_reading[0]; onnx_features[5] = (float) ma_h_reading[0]; onnx_features[6] = (float) ma_l_reading[0]; onnx_features[7] = (float) ma_c_reading[0]; onnx_features[8] = (float) rsi_reading[0]; onnx_features[9] = (float) sto_reading_main[0]; onnx_features[10] = (float) sto_reading_signal[0]; //--- Copy Market Data double close = iClose(Symbol(),PERIOD_CURRENT,0); SymbolInfoTick(Symbol(),current_tick); //--- Place a position if(PositionsTotal() ==0) { if(OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_features,onnx_targets)) { Comment("Onnx Model Prediction: \n",onnx_targets); //--- Store our result vectorf res = {onnx_targets[1]-onnx_targets[0],onnx_targets[3]-onnx_targets[2],onnx_targets[5]-onnx_targets[4],onnx_targets[7]-onnx_targets[6]}; if(res.Max() > 0) { Print("Trading oppurtunity found"); Print(res); if(res.ArgMax()==0) { if(ma_o_reading[0]<ma_c_reading[0]) Buy(); if(ma_o_reading[0]>ma_c_reading[0]) Sell(); } if(res.ArgMax()==1) { if(close>ma_h_reading[0]) Buy(); if(close<ma_l_reading[0]) Sell(); } if(res.ArgMax()==2) { if(rsi_reading[0]>50) Buy(); if(rsi_reading[0]<50) Sell(); } if(res.ArgMax()==3) { if(sto_reading_main[0]>50) Buy(); if(sto_reading_main[0]<50) Sell(); } } else { Print("No trading oppurtunities expected."); } } } } } //+------------------------------------------------------------------+
メンテナンス性を高めるため、ロングおよびショートポジションに入るメソッドを別々に定義し、コードの重複を避けます。
//+------------------------------------------------------------------+ //| Enter a long position | //+------------------------------------------------------------------+ void Buy(void) { Trade.Buy(0.01,Symbol(),current_tick.ask,current_tick.ask-(1.5*atr_reading[0]),current_tick.ask+(1.5*atr_reading[0])); } //+------------------------------------------------------------------+ //| Enter a short position | //+------------------------------------------------------------------+ void Sell(void) { Trade.Sell(0.01,Symbol(),current_tick.bid,current_tick.bid+(1.5*atr_reading[0]),current_tick.bid-(1.5*atr_reading[0])); } //+------------------------------------------------------------------+
これで、ブラックボックス版の取引アプリケーションをバックテストする準備が整いました。

図10:2回目のテスト用にアプリケーションの正しいバージョンを選択する
まず、正しいバージョンを選択し、適切なテスト日付を読み込みます。これらの日付は、議論の残りの部分と一貫性を保つ必要があります。

図11:テストに使用するバックテスト日付を慎重に確認する
エクイティカーブは、期待通りの正の口座残高傾向を示しています。ただし、戦略は自動的に選択されており、ディープニューラルネットワークが最適と判断した単一戦略を選びました。

図12:ブラックボックスソリューションによる戦略で得られたエクイティカーブは、タスクを適切に捉えたことを示唆する
パフォーマンス統計を確認すると、ブラックボックスアプローチはホワイトボックスソリューションに比べてやや劣っています。この結果は予想通りで、ブラックボックスの設定は時間がかかり、探索回数を増やすことで改善する可能性があります。

図14:ブラックボックスソリューションによる詳細な結果
結論
本記事では、MetaTrader 5ツールキットを使用して取引戦略を自動的に特定する方法を示しました。コンピュータは、人間の目では見逃してしまう可能性のある戦略を迅速に発見できます。データは、私たちがそれを認識しているかどうかにかかわらず、パターンを明らかにします。今回の議論では、教師なし行列分解に基づくホワイトボックスソリューションの利点を強調しました。これらは設定時間が短く、解釈が明確で、保持すべき戦略について明示的な指針を提供します。結果として、時間を節約しつつ、診断価値を高めることができます。一方で、ブラックボックスソリューションは、市場が複雑な場合に、ホワイトボックスアプローチでは対応しきれない状況でより有用となります。
| ファイル名 | ファイルの説明 |
|---|---|
| Automatic Strategy Selection Baseline.mq5 | SVD分解によって生成された4つのユニークな戦略によるホワイトボックスソリューション |
| Automatic Strategy Selection.mq5 | EURUSD市場のディープニューラルネットワークによって生成されたブラックボックスソリューション |
| Fetch Data Indicators.mq5 | 必要な市場データを取得し、戦略リターンの分析を開始するために作成したMQL5スクリプト |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/20256
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
取引戦略の開発:Flower Volatility Indexのトレンドフォローアプローチ
MQL5でのAI搭載取引システムの構築(第6回):チャットの削除と検索機能の導入
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
取引戦略の開発:擬似ピアソン相関アプローチ
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索