English
preview
機械学習の限界を克服する(第7回):自動戦略選択

機械学習の限界を克服する(第7回):自動戦略選択

MetaTrader 5 |
19 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

多くのトレーダーは、学び始めたときに、リスク許容度に合った戦略を選ぶよう助言されます。これは妥当ですが、本記事ではまず戦略を理想的な好みではなく、期待されるパフォーマンスに基づいて発見すべきであることを提案します。収益性の高い戦略を特定することは、経験の有無にかかわらずアルゴリズムトレーダーにとって普遍的な課題です。新しい戦略やインジケーター、エキスパートアドバイザー(EA)が急速に増え続けるため、その難しさはさらに増しています。

私たちは、かつてないほど情報が相互接続された時代に生きています。しかし、新しいアイデアが、トレーダーが評価するよりも速いペースで広がった場合、どうすればよいでしょうか。無数の戦略の中から、テストに値する候補を自動的に特定するにはどうすれば良いでしょうか。すべての組み合わせを総当たりで試すことなく、潜在的に収益性のある戦略の組み合わせを発見することは可能でしょうか。

本記事では、これらの問いに答えるために、2つの補完的なアプローチを提案します。

  1. ホワイトボックスソリューション:期待リターンに対して行列分解(特に特異値分解SVD)を適用し、現在の市場条件によって正の影響を受ける戦略の組み合わせを特定します。
  2. ブラックボックスソリューション:ディープニューラルネットワークを用いて、観測された市場の挙動に基づき戦略を動的に選択します。

私たちのソリューションは、手元にある取引戦略を実行した場合に得られたであろうリターンを推定できる能力に依存しています。数値計算の手法を用いて、各戦略から得られる期待収益を学習します。任意の戦略から生成されるリターンを近似することで、貴重な洞察を得ることができます。


必要なデータの取得

まず、必要な市場データを取得する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.mq5SVD分解によって生成された4つのユニークな戦略によるホワイトボックスソリューション
Automatic Strategy Selection.mq5 EURUSD市場のディープニューラルネットワークによって生成されたブラックボックスソリューション
Fetch Data Indicators.mq5 必要な市場データを取得し、戦略リターンの分析を開始するために作成したMQL5スクリプト

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

取引戦略の開発:Flower Volatility Indexのトレンドフォローアプローチ 取引戦略の開発:Flower Volatility Indexのトレンドフォローアプローチ
市場のリズムを解読する絶え間ない探求により、トレーダーやクオンツアナリストは数多くの数学モデルを生み出してきました。本記事では、Flower Volatility Index (FVI)を紹介します。これは、バラ曲線の数学的優雅さを実用的な取引ツールに変換した新しいアプローチです。この研究を通じて、数学モデルを実際の市場環境で分析や意思決定を支援できる実用的な取引メカニズムに適応できることを示しました。
MQL5でのAI搭載取引システムの構築(第6回):チャットの削除と検索機能の導入 MQL5でのAI搭載取引システムの構築(第6回):チャットの削除と検索機能の導入
連載第6回では、ChatGPT統合型エキスパートアドバイザー(EA)をさらに進化させ、サイドバーのインタラクティブな削除ボタン、大・小の履歴ポップアップ、新しい検索ポップアップを導入することで、トレーダーが永続的な会話履歴を効率的に管理および整理できるようにしました。これにより、チャートデータからのAI駆動のシグナルを維持しつつ、暗号化されたストレージに会話を安全に保存できます。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
取引戦略の開発:擬似ピアソン相関アプローチ 取引戦略の開発:擬似ピアソン相関アプローチ
既存のインジケーターから新しいインジケーターを生成することは、取引分析を強化するための非常に強力な方法です。既存のインジケーターの出力を統合する数学的関数を定義することで、トレーダーは複数のシグナルを1つの効率的なツールにまとめたハイブリッドインジケーターを作成できます。本記事では、ピアソン相関関数を改良した「擬似ピアソン相関(PPC, Pseudo Pearson Correlation)」を用いて、3つのオシレーターから構築された新しいインジケーターを紹介します。PPCインジケーターは、オシレーター同士の動的な関係を数値化し、それを実践的な取引戦略に応用することを目的としています。