古典的な戦略を再構築する(第14回):複数戦略分析
自己最適化エキスパートアドバイザー(EA)に関する姉妹連載記事の前回の説明では、複数の戦略をアンサンブルとして組み合わせ、当初のものよりも強力な単一の戦略へと統合するという課題に取り組みました。
私たちは、それぞれの戦略に1票ずつ与えるという一種の公平な仕組みを採用し、戦略同士が協調して機能するようにしました。各投票の重みは調整パラメータとなり、前述のとおり、遺伝的最適化を使って取引戦略の収益性を最大化するよう最適化をおこないました。そして、遺伝的最適化によって最小の重みが割り当てられた戦略を排除し、残った2つの戦略を分析対象として統計モデルを構築しました。
今回の検討では、遺伝的最適化が導き出した最良の結果に基づき、MQL5スクリプトを使って市場データを抽出しました。私たちは、バックテストとフォワードテストの両方で安定した結果を示したものを選び、それを判断基準としました。
しかし、遺伝的最適化によって選ばれた2つの戦略のリターンを詳細に分析した結果、両者の相関が非常に高いことが分かりました。つまり、どちらの戦略もほぼ同じタイミングで利益と損失を出す傾向にあったのです。相関性の高い2つの取引戦略を持つことは、実質的には1つの戦略しか持たないのと同じであり、単一戦略しかないという状況は複数戦略分析の目的を根本的に損ないます。
取引戦略の構築に人工知能を導入すると、予期しない動作や問題が発生する可能性があります。今回、遺伝的最適化は私たちが与えたフレームワークを利用し、最も相関性の高い戦略を選択してしまったようです。純粋に数学的な観点から見れば、これは巧妙な動きとも言えます。主要な戦略同士が相関している場合、遺伝的最適化にとって口座全体のバランスを予測することが容易になるからです。
当初、私は遺伝的最適化が最も収益性の高い戦略に高い重みを、収益性の低い戦略には低い重みを割り当てると予想していました。しかし、選択肢が3つしかなく、この最適化手順を1回しか実行していなかったことを考えると、今回の結果が偶然によるものである可能性も否定できません。言い換えれば、より完全で時間をかけた最適化アルゴリズムを用いて投票重みの最適化を繰り返した場合、最適化によって相関の高い戦略が選ばれなかったかもしれません。
この洞察により、戦略の最適な設定を選択するためのアプローチを見直すことになりました。まず、各投票の重みをすべて1に固定することから始めるべきだと考えます。これにより、遺伝的最適化は、各インジケーターの最も収益性の高い設定の探索に集中できるようになります。私たちが今後の検討を進める中で明らかになるように、この修正版のアプローチが当初の計画を上回ることが分かります。相関の高い2つの戦略を複数戦略分析に使用しても、実質的な進歩は得られません。したがって、複数戦略分析の目的をより適切に定義する方法を学びました。それはすなわち次のような問いです。「相関のないリターンを持つ複数の戦略を選び、口座の収益性を最大化する最良の方法とは何か?」
MQL5の始め方
まず、これまでに使用してきた2つの戦略を選定した前回のテストで、最大の収益を得られた設定を用いて、過去の市場データを取得するスクリプトを作成します。私たちのシステムは、先に説明した遺伝的最適化テストから得られた固定パラメータに依存します。これらのパラメータは、理想的な戦略の一部として固定した状態でデータを取得します。
//+------------------------------------------------------------------+ //| 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 100 //--- Period for our moving average #define MA_TYPE MODE_EMA //--- Type of moving average we have #define RSI_PERIOD 24 //--- Period For Our RSI Indicator #define RSI_PRICE PRICE_CLOSE //--- Applied Price For our RSI Indicator #define HORIZON 38 //--- Holding period #define TF PERIOD_H3 //--- Time Frame
このシステムは、スクリプトの実行中に呼び出すテクニカル指標の値を保持するため、いくつかの重要なグローバル変数に依存します。これらはそれぞれ適切なハンドルとバッファに格納されます。さらに、出力ファイル名やリクエストするデータ量など、他の変数も定義します。
//--- Our handlers for our indicators int ma_handle,ma_o_handle,rsi_handle; //--- Data structures to store the readings from our indicators double ma_reading[],ma_o_reading[],rsi_reading[]; //--- File name string file_name = Symbol() + " Market Data As Series Multiple Strategy Analysis.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,TF,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(_Symbol,TF,MA_PERIOD,0,MA_TYPE,PRICE_OPEN); rsi_handle = iRSI(_Symbol,TF,RSI_PERIOD,RSI_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(rsi_handle,0,0,fetch,rsi_reading); ArraySetAsSeries(rsi_reading,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","True Open","True High","True Low","True Close","True MA C","True MA O","True RSI","Open","High","Low","Close","MA Close","MA Open","RSI"); } else { FileWrite(file_handle, iTime(_Symbol,TF,i), iOpen(_Symbol,TF,i), iHigh(_Symbol,TF,i), iLow(_Symbol,TF,i), iClose(_Symbol,TF,i), ma_reading[i], ma_o_reading[i], rsi_reading[i], iOpen(_Symbol,TF,i) - iOpen(_Symbol,TF,(i + HORIZON)), iHigh(_Symbol,TF,i) - iHigh(_Symbol,TF,(i + HORIZON)), iLow(_Symbol,TF,i) - iLow(_Symbol,TF,(i + HORIZON)), iClose(_Symbol,TF,i) - iClose(_Symbol,TF,(i + HORIZON)), ma_reading[i] - ma_reading[(i + HORIZON)], ma_o_reading[i] - ma_o_reading[(i + HORIZON)], rsi_reading[i] - rsi_reading[(i + HORIZON)] ); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+
Pythonでのデータ分析
ここからは、Pythonで利用できる数値計算ライブラリを使って市場データを分析します。まず、pandasを読み込み、市場データを読み込みます。
#Load our libraries import pandas as pd
次に、与えられた市場環境下で学習戦略がどのような行動を取ったかをラベル付けし、それぞれの行動が生み出した損益を計算します。
#Read in the data data = pd.read_csv("EURUSD Market Data As Series Multiple Strategy Analysis.csv") #The optimal holding period suggested by our MT5 Genetic optimizer HORIZON = 38 #Calculate the true market return data['Return'] = data['True Close'].shift(-HORIZON) - data['True Close'] #The action suggested by our first strategy, MA Cross data['Action 1'] = 0 #The action suggested by our second strategy, RSI Strategy data['Action 2'] = 0 #Buy conditions data.loc[data['True MA C'] > data['True MA O'],'Action 1'] = 1 data.loc[data['True RSI'] > 50,'Action 2'] = 1 #Sell conditions data.loc[data['True MA C'] < data['True MA O'],'Action 1'] = -1 data.loc[data['True RSI'] < 50,'Action 2'] = -1 #Perform a linear transformation of the true market return, using our trading stragies data['Return 1'] = data['Return'] * data['Action 1'] data['Return 2'] = data['Return'] * data['Action 2'] data = data.iloc[:-HORIZON,:]
これは、あらゆる統計モデリングや取引システム構築において重要なステップです。モデルがすべてのデータに過剰適合してしまうと、分析やテストは意味を失い、モデルの信頼性が損なわれてしまいます。
#Drop our back test data _ = data.iloc[-((365 * 2 * 6)):,:] data = data.iloc[:-((365 * 2 * 6)),:]
ターゲットのラベル付けは、教師あり学習における重要な工程です。視覚化のため、戦略1が戦略2より高いリターンを生み出したか、またはその逆かを示すラベルを付けます。ターゲットは、戦略2が戦略1より高い収益を上げたかどうかを示します。比較のため、モデルが将来の市場リターンを直接予測する能力をベンチマークとして使用します。
#Gether inputs X = data.iloc[:,1:15] #Both Strategies will earn equal reward data['Target 1'] = 0 data['Target 2'] = 0 #Strategy 1 is more profitable data.loc[data['Return 1'] > data['Return 2'],'Target 1'] = 1 #Strategy 2 is more profitable data.loc[data['Return 2'] > data['Return 1'],'Target 2'] = 1 #Classical Target data['Classical Target'] = 0 data.loc[data['Return'] > 0,'Classical Target'] = 1
次に、収集した市場データの数値的特性を分析するために、scikit-learnライブラリを読み込みます。
#Loading our scikit learn libraries from sklearn.model_selection import TimeSeriesSplit,cross_val_score from sklearn.linear_model import LinearRegression,LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.discriminant_analysis import LinearDiscriminantAnalysis from sklearn.neural_network import MLPRegressor from sklearn.model_selection import RandomizedSearchCV
まず、遺伝的最適化で求めた最適なホライゾンに合わせてギャップを設定した5分割の時系列検証オブジェクトを作成します。次に、列ごとの平均値と標準偏差を計算し、データセットを標準化して、平均0、標準偏差1のスケールに整えます。
#Prepare the data for time series modelling tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON) Z1 = X.mean() Z2 = X.std() X = ((X-X.mean()) / X.std())
次に、新たに設定したターゲットを予測する精度を測定し、将来の価格リターンを直接予測する従来のターゲットとの精度を比較します。scikit-learnの交差検証機能を用いて線形分類器による精度を評価し、結果を配列に格納して棒グラフを作成します。観察されたように、従来のターゲットでの精度は約50%であるのに対し、どちらの戦略がより高い利益を上げるかを予測する精度は約90%に達し、明らかに従来のターゲットを上回っています。
#Measuring our accuracy on our new target res = [] model = LinearDiscriminantAnalysis() res.append(np.mean(np.abs(cross_val_score(model,X,data['Classical Target'],cv=tscv,scoring='accuracy')))) model = LinearDiscriminantAnalysis() res.append(np.mean(np.abs(cross_val_score(model,X,data['Target 1'],cv=tscv,scoring='accuracy')))) model = LinearDiscriminantAnalysis() res.append(np.mean(np.abs(cross_val_score(model,X,data['Target 2'],cv=tscv,scoring='accuracy',n_jobs=-1)))) sns.barplot(res,color='black') plt.xticks([0,1,2],['Classical Target','MA Cross Over Target','RSI Target']) plt.axhline(res[0],linestyle=':',color='red') plt.ylabel('5-Fold Percentage Accuracy %') plt.title('Outperforming The Classical Target of Direct Price Prediction')

図1:市場リターンを直接予測する従来のタスクよりも、戦略と市場の関係性をモデル化することで精度が向上している
最後に、scikit-learnのランダムサーチ機能を用いて、市場データに対するニューラルネットワークを構築します。まず、シャッフルや早期終了といった固定したい設定を指定して、ニューラルネットワークを初期化します。
#Use random search to build a neural network for our market data #Initialize the model model = MLPRegressor(shuffle=False,early_stopping=False) distributions = {'solver':['lbfgs','adam','sgd'], 'hidden_layer_sizes':[(X.shape[1],2,10,20),(X.shape[1],30,50,10),(X.shape[1],14,14,14),(X.shape[1],5,20,2),(X.shape[1],1,2,3,4,5,6,10),(X.shape[1],1,14,14,1)], 'activation':['relu','identity','logistic','tanh'] } rscv = RandomizedSearchCV(model,distributions,n_jobs=-1,n_iter=50) rscv.fit(X,data.loc[:,['Target 1','Target 2']])次に、学習済みニューラルネットワークをONNX (Open Neural Network Exchange)形式にエクスポートします。まずONNXライブラリを読み込み、必要なコンバーターを読み込みます。ONNXは、機械学習モデルをフレームワークに依存しない形式で構築し、エクスポートできるオープンソースの規格です。
#Exporting our model to ONNX import onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType initial_types = [('float_input',FloatTensorType([1,X.shape[1]]))] final_types = [('float_output',FloatTensorType([2,1]))] model = rscv.best_estimator_ model.fit(X,data.loc[:,['Target 1','Target 2']]) onnx_proto = convert_sklearn(model=model,initial_types=initial_types,final_types=final_types,target_opset=12) onnx.save(onnx_proto,'EURUSD NN MSA.onnx')
ONNX形式のニューラルネットワークのグラフを表示するために、まずNetronライブラリをインポートし、次にnetron.start関数を使用してONNXニューラルネットワークのパスを渡すだけで表示を開始します。
#Viewing our ONNX graph in netron import netron netron.start('../EURUSD NN MSA.onnx')
以下の図2では、ONNXモデルのメタプロパティを示しています。ONNXモデルが14個の入力と2個の出力(いずれもfloat型)を持ち、producerやONNXのバージョンなどの重要なメタデータも含まれていることが確認できます。

図2:ONNXモデルに関連するメタデータを可視化し、入力および出力サイズが正しく指定されていることを検証する
ONNXモデルは、計算ノードとエッジのグラフとして機械学習モデルを表現します。エッジは、ある計算ノードから次の計算ノードへ情報が渡される経路を示します。このようにして、すべての機械学習モデルは普遍的な形式であるONNXグラフに変換できます。下の図3は、scikit-learnのランダムサーチ手続きで構築した私たちのニューラルネットワークを表す計算グラフです。

図3:Netronライブラリを用いて可視化した、私たちが構築したディープニューラルネットワークの計算グラフ
MQL5でEAを構築する
EAを構築する最初のステップは、前節で作成したONNXニューラルネットワークを読み込むことです。
//+------------------------------------------------------------------+ //| MSA Test 1.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" //+------------------------------------------------------------------+ //| ONNX Model | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD NN MSA.onnx" as uchar onnx_buffer[];
Pythonで各列について測定した列の平均値と標準偏差は、それぞれZ1とZ2という配列に格納します。これらの値は、ONNXニューラルネットワークからの予測を得る前に各入力をスケーリングし、標準化するために使用します。
//+------------------------------------------------------------------+ //| ONNX Parameters | //+------------------------------------------------------------------+ double Z1[] = { 1.18932220e+00, 1.19077958e+00, 1.18786462e+00, 1.18931542e+00, 1.18994040e+00, 1.18994674e+00, 4.94395259e+01, -4.99204879e-04, -5.00701302e-04, -4.97575935e-04, -4.98995739e-04, -4.70848300e-04, -4.70289373e-04, -1.84697724e-02 }; double Z2[] = {1.09599015e-01, 1.09698934e-01, 1.09479324e-01, 1.09593123e-01, 1.09413744e-01, 1.09419007e-01, 1.00452009e+01, 1.31269558e-02, 1.31336302e-02, 1.31513465e-02, 1.31174740e-02, 6.88794916e-03, 6.89036979e-03, 1.28550006e+01 };
重要なシステム定数はプログラムの実行中ずっと定義・維持されます。これらの定数は以前に遺伝的最適化を用いて選定されたものです。
//+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define MA_SHIFT 0 #define MA_TYPE MODE_EMA #define RSI_PRICE PRICE_CLOSE #define ONNX_INPUTS 14 #define ONNX_OUTPUTS 2 #define HORIZON 38
移動平均期間やRSI期間などの重要な戦略パラメータは遺伝的最適化によって選ばれており、プログラム全体で定数として保持します。
//+------------------------------------------------------------------+ //| Strategy Parameters | //+------------------------------------------------------------------+ int MA_PERIOD = 100; //Moving Average Period int RSI_PERIOD = 24; //RSI Period ENUM_TIMEFRAMES STRATEGY_TIME_FRAME = PERIOD_H3; //Strategy Timeframe int HOLDING_PERIOD = 38; //Position Maturity Period
アプリケーションを完全に動作させるためにいくつかの依存関係が必要です。取引ライブラリのように明白なものもあれば、姉妹連載記事で一緒に作成した戦略のように読者に既に馴染みのあるものもあります。読み込む戦略は取引アプリケーションを動かすために必要です。
//+------------------------------------------------------------------+ //| Dependencies | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> #include <VolatilityDoctor\Time\Time.mqh> #include <VolatilityDoctor\Trade\TradeInfo.mqh> #include <VolatilityDoctor\Strategies\OpenCloseMACrossover.mqh> #include <VolatilityDoctor\Strategies\RSIMidPoint.mqh>
プログラム全体で使用する重要なグローバル変数を定義しますが、幸いにも必要な数は少数です。たとえば、私たちが作成したカスタムクラス(取引や時間クラス、RSI戦略、クロスオーバー戦略)のインスタンス用のグローバル変数が必要です。他に、ONNXニューラルネットワークからの読み取りやその予測を格納するためのグローバル変数も必要です。
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ //--- Custom Types CTrade Trade; Time *TradeTime; TradeInfo *TradeInformation; RSIMidPoint *RSIMid; OpenCloseMACrossover *MACross; long onnx_model; vectorf onnx_output; //--- Our handlers for our indicators int ma_handle,ma_o_handle,rsi_handle; //--- Data structures to store the readings from our indicators double ma_reading[],ma_o_reading[],rsi_reading[]; //--- System Types int position_timer;
アプリケーションを初期化するとき、必要な動的オブジェクトの新しいインスタンスを作成します。たとえば、時間と取引情報を管理するクラスのインスタンスや、必要なインジケーターハンドルのインスタンスを作成します。その後、読み込んだバッファからONNXニューラルネットワークを生成し、正しく読み込まれたかを検証します。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Create dynamic instances of our custom types TradeTime = new Time(Symbol(),STRATEGY_TIME_FRAME); TradeInformation = new TradeInfo(Symbol(),STRATEGY_TIME_FRAME); MACross = new OpenCloseMACrossover(Symbol(),STRATEGY_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE); RSIMid = new RSIMidPoint(Symbol(),STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE); onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT); onnx_output = vectorf::Zeros(ONNX_OUTPUTS); //---Setup our technical indicators ma_handle = iMA(_Symbol,STRATEGY_TIME_FRAME,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(_Symbol,STRATEGY_TIME_FRAME,MA_PERIOD,0,MA_TYPE,PRICE_OPEN); rsi_handle = iRSI(_Symbol,STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE); if(onnx_model != INVALID_HANDLE) { Print("Preparing ONNX model"); ulong input_shape[] = {1,ONNX_INPUTS}; if(!OnnxSetInputShape(onnx_model,0,input_shape)) { Print("Failed To Specify ONNX model input shape"); return(INIT_FAILED); } ulong output_shape[] = {ONNX_OUTPUTS,1}; if(!OnnxSetOutputShape(onnx_model,0,output_shape)) { Print("Failed To Specify ONNX model output shape"); return(INIT_FAILED); } } //--- Everything was fine Print("Successfully loaded all components for our Expert Advisor"); return(INIT_SUCCEEDED); } //--- End of OnInit Scope
アプリケーションの使用が終わったら、不要になったメモリを削除し、他のアプリケーションのためにリソースを解放して、安全にアプリケーションを終了します。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Delete the dynamic objects delete TradeTime; delete TradeInformation; delete MACross; delete RSIMid; OnnxRelease(onnx_model); IndicatorRelease(ma_handle); IndicatorRelease(ma_o_handle); IndicatorRelease(rsi_handle); } //--- End of Deinit Scope
新しい価格レベルがOnTickおよびOnExpertStart関数で受信されるたびに、まずChangeTimeクラス内のnew_candle関数を呼び出して、新しい日足が完全に形成されたかをチェックします。ローソク足が形成されている場合は、戦略のパラメータを更新してから取引機会を確認します。取引機会がある場合は取引をおこない、ない場合は、ポジションの保有期間が終了するまで待ってからクローズします。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Check if a new daily candle has formed if(TradeTime.NewCandle()) { //--- Update strategy Update(); //--- If we have no open positions if(PositionsTotal() == 0) { //--- Reset the position timer position_timer = 0; //--- Check for a trading signal CheckSignal(); } //--- Otherwise else { //--- The position has reached maturity if(position_timer == HOLDING_PERIOD) Trade.PositionClose(Symbol()); //--- Otherwise keep holding else position_timer++; } } } //--- End of OnTick Scope
Updateメソッドでは、遺伝的最適化で選択した予測ホライゾンなどの重要なパラメータを受け取り、使用する戦略とバッファに格納されたテクニカル指標の読み値を更新します。
//+------------------------------------------------------------------+ //| Update our technical indicators | //+------------------------------------------------------------------+ void Update(void) { int fetch = (HORIZON * 2); //--- Update the strategy RSIMid.Update(); MACross.Update(); //---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(rsi_handle,0,0,fetch,rsi_reading); ArraySetAsSeries(rsi_reading,true); } //--- End of Update Scope
ONNXモデルから予測を取得します。ONNXニューラルネットワークから予測を得るには、ONNXの実行関数を使用します。ただし呼び出す前に、モデルへ渡す入力変数を更新し、平均を引いて標準偏差で割ることでこれらの値をスケーリングし、標準化しなければなりません。
//+------------------------------------------------------------------+ //| Get A Prediction from our ONNX model | //+------------------------------------------------------------------+ void OnnxPredict(void) { vectorf input_variables = { iOpen(_Symbol,STRATEGY_TIME_FRAME,0), iHigh(_Symbol,STRATEGY_TIME_FRAME,0), iLow(_Symbol,STRATEGY_TIME_FRAME,0), iClose(_Symbol,STRATEGY_TIME_FRAME,0), ma_reading[0], ma_o_reading[0], rsi_reading[0], iOpen(_Symbol,STRATEGY_TIME_FRAME,0) - iOpen(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)), iHigh(_Symbol,STRATEGY_TIME_FRAME,0) - iHigh(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)), iLow(_Symbol,STRATEGY_TIME_FRAME,0) - iLow(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)), iClose(_Symbol,STRATEGY_TIME_FRAME,0) - iClose(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)), ma_reading[0] - ma_reading[(0 + HORIZON)], ma_o_reading[0] - ma_o_reading[(0 + HORIZON)], rsi_reading[0] - rsi_reading[(0 + HORIZON)] }; for(int i = 0; i < ONNX_INPUTS;i++) { input_variables[i] = ((input_variables[i] - Z1[i])/ Z2[i]); } OnnxRun(onnx_model,ONNX_DEFAULT,input_variables,onnx_output); }
クロスオーバー戦略を用いて取引シグナルを確認する際は、まずONNXニューラルネットワークから予測を取得します。ニューラルネットワークはどの戦略が最も収益性が高いかを予測します。次に、その戦略が対応するエントリーシグナルを出しているかを確認します。モデルが戦略の収益性を予測し、かつ戦略が有効な取引機会を提供している場合にのみエントリーします。
//+------------------------------------------------------------------+ //| Check for a trading signal using our cross-over strategy | //+------------------------------------------------------------------+ void CheckSignal(void) { OnnxPredict(); //--- MA Strategy is profitable if((onnx_output[0] > 0.5) && (onnx_output[1] < 0.5)) { //--- Long positions when the close moving average is above the open if(MACross.BuySignal()) { Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,""); return; } //--- Otherwise short else if(MACross.SellSignal()) { Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,""); return; } } //--- RSI strategy is profitable else if((onnx_output[0] < 0.5) && (onnx_output[1] > 0.5)) { if(RSIMid.BuySignal()) { Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,""); return; } //--- Otherwise short else if(MACross.SellSignal()) { Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,""); return; } } } //--- End of CheckSignal Scope
システム終盤では、アプリケーション開始時に定義したシステム定数をすべて#undefで未定義にします。
//+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef MA_SHIFT #undef RSI_PRICE #undef MA_TYPE #undef ONNX_INPUTS #undef ONNX_OUTPUTS #undef HORIZON //+------------------------------------------------------------------+
バックテストの日付選択は単純です。前回のテストで用いたフォワードテストと整合するように、新しいアプリケーションのテスト期間を調整しています。

図4:バックテスト日を前回実施したフォワードテストと一致するように選択する
常に最も現実的な設定を使いたいため、ランダム遅延設定を選択します。

図5:[ランダム遅延]を選択してモデリング条件を最も現実的な設定にする
ところが驚くべきことに、統計モデルを組み込んだことで新しい設定を適用した結果、パフォーマンスが大幅に悪化しました。これは通常、何かが計画通りに進んでいない強い兆候です。

図6:統計モデリングを用いない場合と比較して、新しい資産曲線の状態が悪化している
アプリケーションの詳細なパフォーマンス分析を見ると、総純利益が減少し、シャープ比も低下していることが分かります。これは望ましい兆候ではありません。すなわち、私たちが導入した統計モデリングツールによる期待される恩恵が実際には得られていないことを示しています。

図7:統計モデルに依存する新しい取引戦略のパフォーマンス詳細分析
Pythonで市場データを修正する
Pythonで市場データをさらに精査した結果、誤差の原因となっている可能性を詳しく調べたいと思いました。そこで、まず市場データを可視化するために使用している標準ライブラリをインポートしました。その後、2つの戦略によって生成されたリターンの累積和をプロットしてみると、すぐに問題が明らかになりました。import numpy as np import seaborn as sns import matplotlib.pyplot as plt
以下のプロットからわかるように、2つの戦略は非常によく似た特徴と傾きを持っています。2つの戦略はほとんど同じように上下し、まるで1つの戦略に従っているかのようです。
fig , axs = plt.subplots(2,1,sharex=True) fig.suptitle('Visualizing The Individual Cumulative Return of our 2 Strategies') sns.lineplot(data['Return 1'].cumsum(),ax=axs[0],color='black') sns.lineplot(data['Return 2'].cumsum(),ax=axs[1],color='black')

図8:遺伝的最適化が高度に相関した戦略を選択し、それらに最大の投票重みを割り当てたように見える
さらに、戦略によって生成されたリターンのローリングリスク量をプロットすると、もう1つ懸念すべき点が見えてきます。2つの戦略のリスクプロファイルは市場そのもののリスクプロファイルとほとんど見分けがつきません。ここでも、実際には同じ戦略を使っているように見えます。プロットの列名をラベル付けしていなかったら、戦略1と戦略2の区別はほとんど不可能でしょう。
fig , axs = plt.subplots(3,1,sharex=True) fig.suptitle('Visualizing The Risk In Our 2 Strategies') sns.lineplot(data['Return'].rolling(window=HORIZON).var(),ax=axs[0],color='black') axs[0].axhline(data['Return'].var(),color='red',linestyle=':') sns.lineplot(data['Return 1'].rolling(window=HORIZON).var(),ax=axs[1],color='black') axs[1].axhline(data['Return 1'].var(),color='red',linestyle=':') sns.lineplot(data['Return 2'].rolling(window=HORIZON).var(),ax=axs[2],color='black') axs[2].axhline(data['Return 2'].var(),color='red',linestyle=':')

図9:2つの戦略はリスクとリターンのレベルがほぼ同一であり、複数戦略分析の意義を損なっている
最後の決定的証拠となったのは、市場リターン、移動平均クロスオーバー戦略のリターン、そしてRSI戦略のリターンによって生成された相関行列を計算したときです。移動平均クロスオーバーとRSI戦略の相関が約0.75であることが明確に確認できました。これは非常に強い相関です。この結果から、遺伝的最適化は必ずしも収益性を最大化するために投票重みを調整していたわけではないと確信しました。むしろ、最適化は強く相関した戦略を分離する方向に重みを調整していたようです。なぜなら、その方が最適化にとって計算が容易だからです。
plt.title('Correlation Between Market Return And Strategy Returns')
sns.heatmap(data.loc[:,['Return','Return 1','Return 2']].corr(),annot=True)
図10:取引戦略およびEURUSD市場によって生成された相関行列
改善をおこなう
これまでに得た知見をもとに、私たちのアプリケーションをさらに改善することを試みます。まず、使用したい3つすべての戦略を含む以前のバージョンの取引戦略に戻します。

図11:最適化手続きのためのバックテスト期間を選択する
これまでと同様に、バックテストにはランダム遅延設定を使用します。

図12:実際に手順を追う場合は、必ず[ランダム遅延]を使用すること
ただし、これまでのテストとは異なり、今回はすべての投票重みを1に固定します。これにより、遺伝的最適化が戦略全体の収益性を高める方向にしか変更を加えられないようにします。さらに、最適化が相関した戦略だけを選び出すことを防ぎ、3つすべての戦略を必ず使用するよう強制します。これは、私たちの意図する結果に反しないための措置です。

図13:遺伝的最適化が私たちを「出し抜いている」可能性があるため、今回はすべての投票重みを1に固定することを忘れないこと
最適化結果を確認すると、すでに最初の試行よりも改善が見られます。初期のテストでは、3つすべての戦略を使用した場合の収益性は約40ドルから50ドルの範囲でした。しかし、今回は収益性が明確に向上していることが確認できます。

図14:最適化の結果は前回の試行に比べて大幅に改善されている
さらに、フォワードテストの結果を考慮すると、収益水準が再び上昇していることがわかります。今回のテストでは、バックテストとフォワードテストの両方で収益が得られており、これは設定が安定している強い兆候です。特に重要なのは、遺伝的最適化が両方のテストで利益を上げた戦略構成のバッチを生成できるようになったことです。以前、最適化に自由に重みを変更させていたときには、このような結果を得ることができませんでした。したがって、この構成におけるアプリケーションの安定性が改めて確認できました。

図15:フォワードテストの結果が、安定性と収益性の両方において強い兆候を示している
最後に、フォワードテストの結果から得られた最も収益性の高い設定を用いてバックテストを実行すると、バックテストとフォワードテストの両方で収益性が確認され、上昇傾向を描くエクイティカーブが得られました。これはまさに期待していた結果です。

図16:固定投票を採用した新しい取引戦略によって得られた資産曲線は、以前よりも明らかに高い収益性を示している
結論
今回の議論から、私たちはいくつかの重要な教訓を学びました。まず、「報酬ハッキング(reward hacking)」の問題は非常に広範であり、自覚の有無にかかわらず発生しうるという点です。遺伝的最適化のようなAIツールは高速で賢く、ときには私たち自身よりも賢く振る舞うことがあります。そのため、これらのツールが私たちの設定した成功条件を満たすだけの無効な解を生成して、私たちを出し抜くことがないよう、常に注意を払う必要があります。
最適化によって選択された戦略のリターンを慎重に確認した結果、私たちは「相関した戦略を選択すべきではない」という教訓も得ました。これは、複数戦略分析の目的そのものを損なう行為だからです。
また、遺伝的最適化の性能は、私たちがどれだけ時間をかけるかによってのみ制限されるという点にも注意が必要です。したがって、今回の議論はMetaTrader 5の遺伝的最適化が常に報酬をハッキングするという証拠を示すものではありません。私たちは少数の戦略のみを対象とし、最適化を1回だけおこなったにすぎないため、このプロセスを繰り返せば異なる結果が得られる可能性もあります。
むしろ、この結果は筆者である私自身が、遺伝的最適化に与える問題設定の仕方に十分注意を払っていなかったことを示しています。より適切なアプローチは、まず最適化にすべての利用可能な戦略を強制的に使用させ、その上で投票重みをより慎重に調整することだったでしょう。
最も重要なこととして、今回の演習を通じて明らかになったのは、次回の記事では、すべての投票重みを1に固定したときの収益性を上回る方法を慎重に検討しなければならないということです。この一様な重み設定によって得られたパフォーマンスは、今後の議論における堅固なベンチマークとなります。次回は、本稿で当初適用を意図していた統計モデルを用い、このベンチマークを上回る成果を目指します。
| ファイル名 | ファイルの説明 |
|---|---|
| Fetch Data MSA.mq5 | 遺伝的最適化が選択した2つの戦略のデータを取得するために使用したMQL5スクリプト |
| MSA Test 2.1.mq5 | 2つの戦略とONNXモデルを組み合わせて構築したEA |
| Analyzing Multiple Strategies I.ipynb | MQL5スクリプトで取得した市場データを分析するために作成したJupyter Notebook |
| EURUSD NN MSA.onnx | sklearnのランダムサーチライブラリを使用して構築したディープニューラルネットワーク |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/18847
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
データサイエンスとML(第46回):PythonでN-BEATSを使った株式市場予測
取引所価格のバイナリコードの分析(第2回):BIP39への変換とGPTモデルの記述
知っておくべきMQL5ウィザードのテクニック(第76回): Awesome Oscillatorのパターンとエンベロープチャネルを教師あり学習で利用する
時間進化移動アルゴリズム(TETA)
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索