English
preview
ダーバスボックスブレイクアウト戦略における高度な機械学習技術の探究

ダーバスボックスブレイクアウト戦略における高度な機械学習技術の探究

MetaTrader 5トレーディング | 30 6月 2025, 09:22
24 2
Zhuo Kai Chen
Zhuo Kai Chen

はじめに

ニコラス・ダーバスによって考案された「ダーバスボックスブレイクアウト戦略」は、株価が一定の「ボックス」レンジを上抜けたときに強い上昇モメンタムが示唆されることから、買いシグナルを見極めるためのテクニカル取引手法です。本記事では、この戦略コンセプトを例として用い、機械学習の3つの高度な技術を探っていきます。それは、取引をフィルタリングするのではなくシグナルを生成するために機械学習モデルを使用すること、離散的ではなく連続的なシグナルを用いること、異なる時間枠で学習されたモデルを使って取引を確認すること、の3点です。これらの手法は、従来のアプローチを超えてアルゴリズム取引を強化するうえで、機械学習の新たな活用法を提示します。

本記事では、教育現場ではあまり取り上げられられない、革新的で高度な3つの技術の特徴と理論を深く掘り下げて解説します。  また、モデル学習過程における特徴量エンジニアリングやハイパーパラメータの調整といった上級トピックにも触れます。ただし、機械学習モデルの学習ワークフローのすべてのステップを詳細に扱うわけではありません。省略された手順に興味のある読者は、実装プロセスの完全版を以下の記事リンクよりご確認ください。


シグナル生成

機械学習には主に3つの種類があります。教師あり学習、教師なし学習、そして強化学習です。クオンツトレーディングにおいては、他の手法よりも教師あり学習が主に使われるのには2つの重要な理由があります。
  1. まず、教師なし学習は、取引の成果と市場の特徴との間にある複雑な関係性を捉えるにはシンプルすぎる傾向があります。ラベル(正解データ)が存在しないため、予測目標との整合性が取れず、取引戦略の直接的な成果よりも、間接的なデータの分析に向いています。
  2. 一方、強化学習は、長期的な利益の最大化を目的とした報酬関数を持つトレーニング環境の構築が必要となります。これは個々の予測の精度を重視するよりも、全体としてのパフォーマンスを重視するアプローチであり、単純な予測タスクに対しては設定が複雑すぎ、個人トレーダーにとってはコストに見合わない場合が多いです。

一方、教師あり学習はアルゴリズムトレーディングにおいて非常に多くの応用例があります。よくある活用法は、フィルターとしての使用です。まず、大量の取引候補を生成する元の戦略があり、その後、戦略が成功しやすいか失敗しやすいかを識別するモデルを訓練します。モデルの信頼度に応じて、失敗する可能性が高い取引を除外することができます。

本記事では、もう一つのアプローチであるシグナルの生成に教師あり学習を用いる方法について詳しく見ていきます。価格を予測するような一般的な回帰タスクであれば、「価格が上昇すると予測されれば買い、下落と予測されれば売り」というようにシンプルです。しかし、これをダーバスボックスブレイクアウトのような戦略とどう組み合わせるのか、という点がポイントです。

まずは、後でPythonでモデルを学習させるための特徴量データとラベルデータを収集するエキスパートアドバイザー(EA)を開発します。

ダーバスボックスブレイクアウト戦略では、高値や安値の後に現れる複数の反発ローソク足によりボックスが定義され、その価格帯を上抜けまたは下抜けしたときに取引が発動されます。いずれにせよ、特徴量の収集と将来の予測を開始するためのシグナル(トリガー)が必要です。そのため、価格がボックスの上限または下限をブレイクした瞬間を、トリガーとして設定します。この関数は、指定したルックバック期間と、判定に使うローソク足の本数に基づいて、ダーバスボックスが存在するかを検出し、高値・安値の範囲を変数に代入して、チャート上にボックスを描画します。

double high;
double low;
bool boxFormed = false;

bool DetectDarvasBox(int n = 100, int M = 3)
{
   // Clear previous Darvas box objects
   for (int k = ObjectsTotal(0, 0, -1) - 1; k >= 0; k--)
   {
      string name = ObjectName(0, k);
      if (StringFind(name, "DarvasBox_") == 0)
         ObjectDelete(0, name);
   }
   bool current_box_active = false;
   // Start checking from the oldest bar within the lookback period
   for (int i = M+1; i <= n; i++)
   {
      // Get high of current bar and previous bar
      double high_current = iHigh(_Symbol, PERIOD_CURRENT, i);
      double high_prev = iHigh(_Symbol, PERIOD_CURRENT, i + 1);
      // Check for a new high
      if (high_current > high_prev)
      {
         // Check if the next M bars do not exceed the high
         bool pullback = true;
         for (int k = 1; k <= M; k++)
         {
            if (i - k < 0) // Ensure we don't go beyond available bars
            {
               pullback = false;
               break;
            }
            double high_next = iHigh(_Symbol, PERIOD_CURRENT, i - k);
            if (high_next > high_current)
            {
               pullback = false;
               break;
            }
         }

         // If pullback condition is met, define the box
         if (pullback)
         {
            double top = high_current;
            double bottom = iLow(_Symbol, PERIOD_CURRENT, i);

            // Find the lowest low over the bar and the next M bars
            for (int k = 1; k <= M; k++)
            {
               double low_next = iLow(_Symbol, PERIOD_CURRENT, i - k);
               if (low_next < bottom)
                  bottom = low_next;
            }

            // Check for breakout from i - M - 1 to the current bar (index 0)
            int j = i - M - 1;
            while (j >= 0)
            {
               double close_j = iClose(_Symbol, PERIOD_CURRENT, j);
               if (close_j > top || close_j < bottom)
                  break; // Breakout found
               j--;
            }
            j++; // Adjust to the bar after breakout (or 0 if no breakout)

            // Create a unique object name
            string obj_name = "DarvasBox_" + IntegerToString(i);

            // Plot the box
            datetime time_start = iTime(_Symbol, PERIOD_CURRENT, i);
            datetime time_end;
            if (j > 0)
            {
               // Historical box: ends at breakout
               time_end = iTime(_Symbol, PERIOD_CURRENT, j);
            }
            else
            {
               // Current box: extends to the current bar
               time_end = iTime(_Symbol, PERIOD_CURRENT, 0);
               current_box_active = true;
            }
            high = top;
            low = bottom;
            ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, time_start, top, time_end, bottom);
            ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue);
            ObjectSetInteger(0, obj_name, OBJPROP_STYLE, STYLE_SOLID);
            ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 1);
            boxFormed = true;

            // Since we're only plotting the most recent box, break after finding it
            break;
         }
      }
   }

   return current_box_active;
}

以下に、チャート上のダーバスボックスの例をいくつか示します。

ダーバスボックス

フィルターとして使う方法と比べると、この方法にはいくつかの欠点があります。たとえば、「次の10本のローソク足が上昇するか下降するか」や「先に10 pips上に達するか、下に達するか」といった、バランスの取れた結果(確率が五分五分のもの)を予測する必要があります。もう一つの欠点は、ベースとなる戦略(バックボーン戦略)が本来持っている優位性を失ってしまうことです。つまり、取引の優位性が完全にモデルの予測精度に依存することになります一方で、この手法にはメリットもあります。それは、バックボーン戦略のトリガー時にのみサンプルが得られるという制約がなくなるため、より多くの初期サンプルを使うことができ、リターンの可能性も広がるという点です。このロジックは、onTick関数内で次のように実装されます。

input int checkBar = 30;
input int lookBack = 100;
input int countMax = 10;

void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);

  if (barsTotal!= bars){
     barsTotal = bars;
     boxFormed = false;
     bool NotInPosition = true;
 
     lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
     lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2);
     
     for(int i = 0; i<PositionsTotal(); i++){
         ulong pos = PositionGetTicket(i);
         string symboll = PositionGetSymbol(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;}
            /*count++;
            if(count >=countMax ){
              trade.PositionClose(pos);  
              count = 0;}
            }}*/
     DetectDarvasBox(lookBack,checkBar);
     if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low)))executeBuy(); 
    }
 }

この戦略では、「次の10本のローソク足の結果を追跡する」よりも、テイクプロフィット(TP、利確)とストップロス(SL、損切り)を同じサイズで設定する方が一貫性があります。前者(TP/SLの設定)は予測結果を最終的な損益に直接結びつけられますが、後者(10本のローソク足の結果)は、期間ごとのリターンが変動するため不確実性が増します。 なお、テイクプロフィットとストップロスは、価格に対するパーセンテージで設定されているため、異なる銘柄にも適用しやすく、特に金や株価指数のようなトレンドが発生しやすい資産に適しています。なお、読者の方は、コメントアウトされているコードのコメントを外し、買い関数からテイクプロフィットとストップロスを削除することで、代替手法(10バー後の結果を用いた戦略)をテストできます。

結果を予測するための特徴量データとしては、過去3期間の正規化リターン、レンジ高値および安値からの正規化距離、そしていくつかの一般的な定常系インジケーターを選定しました。これらのデータはマルチ配列に格納され、その後、インクルードファイルに定義されたCFileCSVクラスを使ってCSVファイルとして保存されます。すべての時間枠と銘柄を、以下に示す通りに設定しておくことで、時間枠や資産を簡単に切り替えることができます。
string data[50000][12];
int indexx = 0;

void getData(){
double close = iClose(_Symbol,PERIOD_CURRENT,1);
double close2 = iClose(_Symbol,PERIOD_CURRENT,2);
double close3 = iClose(_Symbol,PERIOD_CURRENT,3);
double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close;
double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2;
double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3;
double highDistance = 1000*(close-high)/close;
double lowDistance = 1000*(close-low)/close;
double boxSize = 1000*(high-low)/close;
double adx[];       // Average Directional Movement Index
double wilder[];    // Average Directional Movement Index by Welles Wilder
double dm[];        // DeMarker
double rsi[];       // Relative Strength Index
double rvi[];       // Relative Vigor Index
double sto[];       // Stochastic Oscillator


CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

//2 means 2 decimal places
data[indexx][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
data[indexx][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
data[indexx][2] = DoubleToString(dm[0], 2);       // DeMarker
data[indexx][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
data[indexx][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
data[indexx][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
data[indexx][6] = DoubleToString(stationary,2);
data[indexx][7] = DoubleToString(boxSize,2);
data[indexx][8] = DoubleToString(stationary2,2);
data[indexx][9] = DoubleToString(stationary3,2);
data[indexx][10] = DoubleToString(highDistance,2);
data[indexx][11] = DoubleToString(lowDistance,2);
indexx++;
}

データ取得用EAの最終コードは次のようになります。

#include <Trade/Trade.mqh>
CTrade trade;
#include <FileCSV.mqh>
CFileCSV csvFile;
string fileName = "box.csv";
string headers[] = {
    "Average Directional Movement Index", 
    "Average Directional Movement Index by Welles Wilder",  
    "DeMarker", 
    "Relative Strength Index", 
    "Relative Vigor Index", 
    "Stochastic Oscillator",
    "Stationary",
    "Box Size",
    "Stationary2",
    "Stationary3",
    "Distance High",
    "Distance Low"
};

input double lott = 0.01;
input int Magic = 0;
input int checkBar = 30;
input int lookBack = 100;
input int countMax = 10;
input double slp = 0.003;
input double tpp = 0.003;
input bool saveData = true;

string data[50000][12];
int indexx = 0;
int barsTotal = 0;
int count = 0;
double high;
double low;
bool boxFormed = false;
double lastClose;
double lastlastClose;

int handleAdx;     // Average Directional Movement Index - 3
int handleWilder;  // Average Directional Movement Index by Welles Wilder - 3
int handleDm;      // DeMarker - 1
int handleRsi;     // Relative Strength Index - 1
int handleRvi;     // Relative Vigor Index - 2
int handleSto;     // Stochastic Oscillator - 2

int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
handleAdx=iADX(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index - 3
handleWilder=iADXWilder(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index by Welles Wilder - 3
handleDm=iDeMarker(_Symbol,PERIOD_CURRENT,14);//DeMarker - 1
handleRsi=iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_CLOSE);//Relative Strength Index - 1
handleRvi=iRVI(_Symbol,PERIOD_CURRENT,10);//Relative Vigor Index - 2
handleSto=iStochastic(_Symbol,PERIOD_CURRENT,5,3,3,MODE_SMA,STO_LOWHIGH);//Stochastic Oscillator - 2
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {
   if (!saveData) return;
   if(csvFile.Open(fileName, FILE_WRITE|FILE_ANSI))
     {
      //Write the header
      csvFile.WriteHeader(headers);
      //Write data rows
      csvFile.WriteLine(data);
      //Close the file
      csvFile.Close();
     }
   else
     {
      Print("File opening error!");
     }
  }

void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);

  if (barsTotal!= bars){
     barsTotal = bars;
     boxFormed = false;
     bool NotInPosition = true;
 
     lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
     lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2);
     
     for(int i = 0; i<PositionsTotal(); i++){
         ulong pos = PositionGetTicket(i);
         string symboll = PositionGetSymbol(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;}
            /*count++;
            if(count >=countMax ){
              trade.PositionClose(pos);  
              count = 0;}
            }}*/
     DetectDarvasBox(lookBack,checkBar);
     if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low)))executeBuy(); 
    }
 }

void executeBuy() {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       double sl = lastClose*(1-slp);
       double tp = lastClose*(1+tpp);
       trade.Buy(lott,_Symbol,ask,sl,tp);
       if(PositionsTotal()>0)getData();
}

bool DetectDarvasBox(int n = 100, int M = 3)
{
   // Clear previous Darvas box objects
   for (int k = ObjectsTotal(0, 0, -1) - 1; k >= 0; k--)
   {
      string name = ObjectName(0, k);
      if (StringFind(name, "DarvasBox_") == 0)
         ObjectDelete(0, name);
   }
   bool current_box_active = false;
   // Start checking from the oldest bar within the lookback period
   for (int i = M+1; i <= n; i++)
   {
      // Get high of current bar and previous bar
      double high_current = iHigh(_Symbol, PERIOD_CURRENT, i);
      double high_prev = iHigh(_Symbol, PERIOD_CURRENT, i + 1);
      // Check for a new high
      if (high_current > high_prev)
      {
         // Check if the next M bars do not exceed the high
         bool pullback = true;
         for (int k = 1; k <= M; k++)
         {
            if (i - k < 0) // Ensure we don't go beyond available bars
            {
               pullback = false;
               break;
            }
            double high_next = iHigh(_Symbol, PERIOD_CURRENT, i - k);
            if (high_next > high_current)
            {
               pullback = false;
               break;
            }
         }

         // If pullback condition is met, define the box
         if (pullback)
         {
            double top = high_current;
            double bottom = iLow(_Symbol, PERIOD_CURRENT, i);

            // Find the lowest low over the bar and the next M bars
            for (int k = 1; k <= M; k++)
            {
               double low_next = iLow(_Symbol, PERIOD_CURRENT, i - k);
               if (low_next < bottom)
                  bottom = low_next;
            }

            // Check for breakout from i - M - 1 to the current bar (index 0)
            int j = i - M - 1;
            while (j >= 0)
            {
               double close_j = iClose(_Symbol, PERIOD_CURRENT, j);
               if (close_j > top || close_j < bottom)
                  break; // Breakout found
               j--;
            }
            j++; // Adjust to the bar after breakout (or 0 if no breakout)

            // Create a unique object name
            string obj_name = "DarvasBox_" + IntegerToString(i);

            // Plot the box
            datetime time_start = iTime(_Symbol, PERIOD_CURRENT, i);
            datetime time_end;
            if (j > 0)
            {
               // Historical box: ends at breakout
               time_end = iTime(_Symbol, PERIOD_CURRENT, j);
            }
            else
            {
               // Current box: extends to the current bar
               time_end = iTime(_Symbol, PERIOD_CURRENT, 0);
               current_box_active = true;
            }
            high = top;
            low = bottom;
            ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, time_start, top, time_end, bottom);
            ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue);
            ObjectSetInteger(0, obj_name, OBJPROP_STYLE, STYLE_SOLID);
            ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 1);
            boxFormed = true;

            // Since we're only plotting the most recent box, break after finding it
            break;
         }
      }
   }

   return current_box_active;
}

void getData(){
double close = iClose(_Symbol,PERIOD_CURRENT,1);
double close2 = iClose(_Symbol,PERIOD_CURRENT,2);
double close3 = iClose(_Symbol,PERIOD_CURRENT,3);
double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close;
double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2;
double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3;
double highDistance = 1000*(close-high)/close;
double lowDistance = 1000*(close-low)/close;
double boxSize = 1000*(high-low)/close;
double adx[];       // Average Directional Movement Index
double wilder[];    // Average Directional Movement Index by Welles Wilder
double dm[];        // DeMarker
double rsi[];       // Relative Strength Index
double rvi[];       // Relative Vigor Index
double sto[];       // Stochastic Oscillator

CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

//2 means 2 decimal places
data[indexx][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
data[indexx][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
data[indexx][2] = DoubleToString(dm[0], 2);       // DeMarker
data[indexx][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
data[indexx][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
data[indexx][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
data[indexx][6] = DoubleToString(stationary,2);
data[indexx][7] = DoubleToString(boxSize,2);
data[indexx][8] = DoubleToString(stationary2,2);
data[indexx][9] = DoubleToString(stationary3,2);
data[indexx][10] = DoubleToString(highDistance,2);
data[indexx][11] = DoubleToString(lowDistance,2);
indexx++;
}

この戦略は、XAUUSDの15分足で運用することを想定しています。これは、この資産が持つ十分なボラティリティに加えて、15分という時間足がノイズの軽減とサンプル数の確保のバランスを取るうえでちょうどよいためです。典型的な取引は次のようになります。

取引例

2020年から2024年までのデータを学習用および検証用データとして使用し、2024年から2025年のデータでMetaTrader 5ターミナル上で結果をテストします。このEAをストラテジーテスターで実行した後、EAの終了時にCSVファイルが/Tester/Agent-sth000ディレクトリに保存されます。

また、右クリックして以下のようなバックテストのExcelレポートを取得できます。

Excelレポート

[Deals]行の行番号をメモしてください。これは後で入力として使用されます。

行を検索

その後、Pythonでモデルをトレーニングします。

この記事で選択したモデルは、こちらの記事で使用したモデルと同様に、分類問題に最適な決定木ベースのモデルです。

import pandas as pd

# Replace 'your_file.xlsx' with the path to your file
input_file = 'box.xlsx'

# Load the Excel file and skip the first {skiprows} rows
data1 = pd.read_excel(input_file, skiprows=4417)

# Select the 'profit' column (assumed to be 'Unnamed: 10') and filter rows as per your instructions
profit_data = data1["Profit"][1:-1] 
profit_data = profit_data[profit_data.index % 2 == 0]  # Filter for rows with odd indices
profit_data = profit_data.reset_index(drop=True)  # Reset index
# Convert to float, then apply the condition to set values to 1 if > 0, otherwise to 0
profit_data = pd.to_numeric(profit_data, errors='coerce').fillna(0)  # Convert to float, replacing NaN with 0
profit_data = profit_data.apply(lambda x: 1 if x > 0 else 0)  # Apply condition

# Load the CSV file with semicolon separator
file_path = 'box.csv'
data2 = pd.read_csv(file_path, sep=';')

# Drop rows with any missing or incomplete values
data2.dropna(inplace=True)

# Drop any duplicate rows if present
data2.drop_duplicates(inplace=True)

# Convert non-numeric columns to numerical format
for col in data2.columns:
    if data2[col].dtype == 'object':
        # Convert categorical to numerical using label encoding
        data2[col] = data2[col].astype('category').cat.codes

# Ensure all remaining columns are numeric and cleanly formatted for CatBoost
data2 = data2.apply(pd.to_numeric, errors='coerce')
data2.dropna(inplace=True)  # Drop any rows that might still contain NaNs after conversion

# Merge the two DataFrames on the index
merged_data = pd.merge(profit_data, data2, left_index=True, right_index=True, how='inner')

# Save the merged data to a new CSV file
output_csv_path = 'merged_data.csv'
merged_data.to_csv(output_csv_path)

print(f"Merged data saved to {output_csv_path}")
このコードを使ってExcelレポートにラベル付けをおこないます。利益がプラスの取引には「1」を、そうでない取引には「0」を割り当てます。そして、そのラベルデータを、データ取得用EAが出力したCSVファイルから集めた特徴量データと結合します。なお、skiprowの値は[Deals]の行番号と一致させる必要があることに注意してください。
import numpy as np
import pandas as pd
import warnings
warnings.filterwarnings("ignore")
data = pd.read_csv("merged_data.csv",index_col=0)

XX = data.drop(columns=['Profit'])
yy = data['Profit']
y = yy.values
X = XX.values
pd.DataFrame(X,y)

次に、ラベル配列をy変数に割り当て、特徴データフレームをX変数に割り当てます。

import numpy as np
import pandas as pd
import warnings
import seaborn as sns
warnings.filterwarnings("ignore")
from sklearn.model_selection import train_test_split
import catboost as cb
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

# Split data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, shuffle=False)

# Identify categorical features
cat_feature_indices = [i for i, col in enumerate(XX.columns) if XX[col].dtype == 'object']

# Train CatBoost classifier
model = cb.CatBoostClassifier(   
    iterations=5000,             # Number of trees (similar to n_estimators)
    learning_rate=0.02,          # Learning rate
    depth=5,                    # Depth of each tree
    l2_leaf_reg=5,
    bagging_temperature=1,
    early_stopping_rounds=50,
    loss_function='Logloss',    # Use 'MultiClass' if it's a multi-class problem
    verbose=1000)
model.fit(X_train, y_train, cat_features=cat_feature_indices)

次に、データを9対1の割合で訓練用と検証用に分割し、モデルの学習を開始します。sklearnのtrain_test_split関数のデフォルト設定ではシャッフルが有効になっていますが、これは時系列データには適さないため、パラメータで必ずshuffle=Falseを指定してください。サンプル数に応じて、過学習や過少学習を避けるためにハイパーパラメータを調整することが重要です。個人的には、対数損失が約0.1あたりで学習を止めるのがうまくいくと感じています。

import numpy as np
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

# Assuming you already have y_test, X_test, and model defined
# Predict probabilities
y_prob = model.predict_proba(X_test)[:, 1]  # Probability for positive class

# Compute ROC curve and AUC (for reference)
fpr, tpr, thresholds = roc_curve(y_test, y_prob)
auc_score = roc_auc_score(y_test, y_prob)
print(f"AUC Score: {auc_score:.2f}")

# Define confidence thresholds to test (e.g., 50%, 60%, 70%, etc.)
confidence_thresholds = np.arange(0.5, 1.0, 0.05)  # From 50% to 95% in steps of 5%
accuracies = []
coverage = []  # Fraction of samples classified at each threshold

for thresh in confidence_thresholds:
    # Classify only when probability is >= thresh (positive) or <= (1 - thresh) (negative)
    y_pred_confident = np.where(y_prob >= thresh, 1, np.where(y_prob <= (1 - thresh), 0, -1))
    
    # Filter out unclassified samples (where y_pred_confident == -1)
    mask = y_pred_confident != -1
    y_test_confident = y_test[mask]
    y_pred_confident = y_pred_confident[mask]
    
    # Calculate accuracy and coverage
    if len(y_test_confident) > 0:  # Avoid division by zero
        acc = np.mean(y_pred_confident == y_test_confident)
        cov = len(y_test_confident) / len(y_test)
    else:
        acc = 0
        cov = 0
    
    accuracies.append(acc)
    coverage.append(cov)

# Plot Accuracy vs Confidence Threshold
plt.figure(figsize=(10, 6))
plt.plot(confidence_thresholds, accuracies, marker='o', label='Accuracy', color='blue')
plt.plot(confidence_thresholds, coverage, marker='s', label='Coverage', color='green')
plt.xlabel('Confidence Threshold')
plt.ylabel('Metric Value')
plt.title('Accuracy and Coverage vs Confidence Threshold')
plt.legend(loc='best')
plt.grid(True)
plt.show()

# Also show the original ROC curve for reference
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, label=f'ROC Curve (AUC = {auc_score:.2f})', color='blue')
plt.plot([0, 1], [0, 1], 'k--', label='Random Guess')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (ROC) Curve')
plt.legend(loc='lower right')
plt.grid(True)
plt.show()

次に、検証テストの結果を確認するために、結果の可視化を行います。一般的な「訓練・検証・テスト」のアプローチでは、検証ステップが最適なハイパーパラメータの選定を助け、また、学習済みモデルに予測力があるかどうかを初期段階で評価します。検証は、最終テストに進む前のバッファの役割を果たします。

精度信頼度閾値

AUCスコア

ここでは、AUCスコアが0.5を上回っており、信頼度の閾値を上げるにつれて精度も向上していることがわかります。これは一般的に良い兆候です。もしこれら二つの指標が一致しない場合でも慌てずに、モデルを完全に破棄する前にまずハイパーパラメータの調整を試みてください。
# Feature importance
feature_importance = model.get_feature_importance()
importance_df = pd.DataFrame({
    'feature': XX.columns,
    'importance': feature_importance
}).sort_values('importance', ascending=False)
print("Feature Importances:")
print(importance_df)
plt.figure(figsize=(10, 6))
sns.barplot(x='importance', y='feature', data=importance_df)
plt.title(' Feature Importance')
plt.xlabel('Importance')
plt.ylabel('Feature')
x = 100/len(XX.columns)
plt.axvline(x,color = 'red', linestyle = '--')
plt.show()

機能の重要性

このコードブロックは、特徴量の重要度と中央値のラインをプロットします。機械学習の分野では、特徴量の重要度を定義する方法は多様にあります。たとえば、

  1. ツリー基盤の重要度:決定木やランダムフォレスト、XGBoostのようなアンサンブルで、不純度(例:ジニ不純度)がどれだけ減少したかを測定します。
  2. 順列の重要度:特徴量の値をシャッフルしたときの性能低下を評価します。
  3. SHAP値:シャープレイ値に基づいて、予測に対する特徴量の寄与度を計算します。
  4. 係数の大きさ:線形モデルにおける係数の絶対値を用います。

この例では、決定木ベースのモデルであるCatBoostを使用しています。特徴量の重要度は、インサンプルデータ内で決定木を分割する際に、各特徴量がどれだけ不純度(混乱)を減らしたかを示します。ただし、最も重要な特徴量を最終セットとして選ぶことが、モデルの効率を高めることは多いものの、必ずしも予測性能の向上につながらない理由もあります。

  • 重要度はインサンプルデータから計算されており、アウトオブサンプルデータは考慮されていません。
  • 特徴量の重要度は、他の特徴量との関係に依存します。選んだ特徴量のほとんどが予測力に乏しい場合、弱いものを除いても効果は薄いです。
  • 重要度はツリーをどれだけうまく分割できるかを示すものであり、必ずしも最終的な意思決定にどれほど重要かを示すわけではありません。

私が予想外に気づいたのは、最も重要度の低い特徴量を選んだほうがアウトオブサンプルの精度が向上したということでした。 しかし一般的には、最も重要な特徴量を選び、重要度の低いものを除くことは、モデルの軽量化につながり、全体的に精度の向上が期待できます。

from onnx.helper import get_attribute_value
import onnxruntime as rt
from skl2onnx import convert_sklearn, update_registered_converter
from skl2onnx.common.shape_calculator import (
    calculate_linear_classifier_output_shapes,
)  # noqa
from skl2onnx.common.data_types import (
    FloatTensorType,
    Int64TensorType,
    guess_tensor_type,
)
from skl2onnx._parse import _apply_zipmap, _get_sklearn_operator_name
from catboost import CatBoostClassifier
from catboost.utils import convert_to_onnx_object

def skl2onnx_parser_castboost_classifier(scope, model, inputs, custom_parsers=None):
    
    options = scope.get_options(model, dict(zipmap=True))
    no_zipmap = isinstance(options["zipmap"], bool) and not options["zipmap"]

    alias = _get_sklearn_operator_name(type(model))
    this_operator = scope.declare_local_operator(alias, model)
    this_operator.inputs = inputs

    label_variable = scope.declare_local_variable("label", Int64TensorType())
    prob_dtype = guess_tensor_type(inputs[0].type)
    probability_tensor_variable = scope.declare_local_variable(
        "probabilities", prob_dtype
    )
    this_operator.outputs.append(label_variable)
    this_operator.outputs.append(probability_tensor_variable)
    probability_tensor = this_operator.outputs

    if no_zipmap:
        return probability_tensor

    return _apply_zipmap(
        options["zipmap"], scope, model, inputs[0].type, probability_tensor
    )

def skl2onnx_convert_catboost(scope, operator, container):
    """
    CatBoost returns an ONNX graph with a single node.
    This function adds it to the main graph.
    """
    onx = convert_to_onnx_object(operator.raw_operator)
    opsets = {d.domain: d.version for d in onx.opset_import}
    if "" in opsets and opsets[""] >= container.target_opset:
        raise RuntimeError("CatBoost uses an opset more recent than the target one.")
    if len(onx.graph.initializer) > 0 or len(onx.graph.sparse_initializer) > 0:
        raise NotImplementedError(
            "CatBoost returns a model initializers. This option is not implemented yet."
        )
    if (
        len(onx.graph.node) not in (1, 2)
        or not onx.graph.node[0].op_type.startswith("TreeEnsemble")
        or (len(onx.graph.node) == 2 and onx.graph.node[1].op_type != "ZipMap")
    ):
        types = ", ".join(map(lambda n: n.op_type, onx.graph.node))
        raise NotImplementedError(
            f"CatBoost returns {len(onx.graph.node)} != 1 (types={types}). "
            f"This option is not implemented yet."
        )
    node = onx.graph.node[0]
    atts = {}
    for att in node.attribute:
        atts[att.name] = get_attribute_value(att)
    container.add_node(
        node.op_type,
        [operator.inputs[0].full_name],
        [operator.outputs[0].full_name, operator.outputs[1].full_name],
        op_domain=node.domain,
        op_version=opsets.get(node.domain, None),
        **atts,
    )

update_registered_converter(
    CatBoostClassifier,
    "CatBoostCatBoostClassifier",
    calculate_linear_classifier_output_shapes,
    skl2onnx_convert_catboost,
    parser=skl2onnx_parser_castboost_classifier,
    options={"nocl": [True, False], "zipmap": [True, False, "columns"]},
)
model_onnx = convert_sklearn(
    model,
    "catboost",
    [("input", FloatTensorType([None, X.shape[1]]))],
    target_opset={"": 12, "ai.onnx.ml": 2},
)

# And save.
with open("box2024.onnx", "wb") as f:
    f.write(model_onnx.SerializeToString())

最後に、ONNXファイルをエクスポートし、MQL5/Filesディレクトリに保存します。

それでは、MetaTrader5コードエディターに戻って、取引用EAを作成しましょう。

ここでは、もともとのデータ取得用EAを少し修正するだけで十分です。CatBoostモデルを扱うためのインクルードファイルをいくつか読み込むことで対応します。

#resource "\\Files\\box2024.onnx" as uchar catboost_onnx[]
#include <CatOnnx.mqh>
CCatBoost cat_boost;
string data[1][12];
vector xx;
vector prob;

次に、getData関数を調整してベクトルを返します。

vector getData(){
double close = iClose(_Symbol,PERIOD_CURRENT,1);
double close2 = iClose(_Symbol,PERIOD_CURRENT,2);
double close3 = iClose(_Symbol,PERIOD_CURRENT,3);
double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close;
double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2;
double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3;
double highDistance = 1000*(close-high)/close;
double lowDistance = 1000*(close-low)/close;
double boxSize = 1000*(high-low)/close;
double adx[];       // Average Directional Movement Index
double wilder[];    // Average Directional Movement Index by Welles Wilder
double dm[];        // DeMarker
double rsi[];       // Relative Strength Index
double rvi[];       // Relative Vigor Index
double sto[];       // Stochastic Oscillator

CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

data[0][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
data[0][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
data[0][2] = DoubleToString(dm[0], 2);       // DeMarker
data[0][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
data[0][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
data[0][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
data[0][6] = DoubleToString(stationary,2);
data[0][7] = DoubleToString(boxSize,2);
data[0][8] = DoubleToString(stationary2,2);
data[0][9] = DoubleToString(stationary3,2);
data[0][10] = DoubleToString(highDistance,2);
data[0][11] = DoubleToString(lowDistance,2);

vector features(12);    
   for(int i = 0; i < 12; i++)
    {
      features[i] = StringToDouble(data[0][i]);
    }
    return features;
}

OnTick関数の最終的な取引ロジックは次のようになります。

void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);

  if (barsTotal!= bars){
     barsTotal = bars;
     boxFormed = false;
     bool NotInPosition = true;
 
     lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
     lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2);
     
     for(int i = 0; i<PositionsTotal(); i++){
         ulong pos = PositionGetTicket(i);
         string symboll = PositionGetSymbol(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;}
            /*count++;
            if(count >=countMax){
              trade.PositionClose(pos);  
              count = 0;}
            }}*/
     DetectDarvasBox(lookBack,checkBar);
     if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low))){
        xx = getData();
        prob = cat_boost.predict_proba(xx);
        if(prob[1]>threshold)executeBuy(); 
        if(prob[0]>threshold)executeSell();
        }
    }
 }

シグナルロジックでは、まず現在ポジションが開かれていないことを確認して、同時に1つの取引しかおこなわないようにします。その後、レンジの上下いずれかにブレイクアウトが発生したかを検出します。次に、getData()関数を呼び出して特徴量ベクトルを取得します。このベクトルはCatBoostモデルに入力され、各結果に対する予測信頼度がprob配列に出力されます。各結果の信頼度に基づいて、モデルが予測した方向に取引を実行します。つまり、このモデルを用いて買い・売りシグナルを生成しているということです。

MetaTrader 5のストラテジーテスターで、2020年から2024年までのインサンプルデータを使ってバックテストを実施し、学習データに誤りがないこと、特徴量とラベルの結合が正しくおこなわれていることを確認しました。すべてが正確であれば、資産曲線は次のようにほぼ完璧な形になるはずです。

インサンプル

次に、2024年から2025年までのアウトオブサンプル(検証用)期間でバックテストをおこない、この戦略が直近の相場でも収益性を持っているかどうかを確認します。 今回は閾値を0.7に設定しており、モデルはある方向のテイクプロフィットに到達する確率が70%以上と予測された場合にのみ、その方向への取引をおこなうようになっています。これは、学習データに基づいた信頼度をもとに判定しています。

バックテスト設定(離散)

パラメータ

資産曲線(離散)

結果(離散)

モデルは年の前半には非常に良いパフォーマンスを示しましたが、時間の経過とともにパフォーマンスが低下し始めたことが確認できます。これは機械学習モデルにおいてよくあることで、過去データから得られた優位性は一時的であることが多く、時間の経過とともにその効果は薄れていく傾向があります。この結果は、将来的に実際の取引環境でモデルを運用する際には、より直近の限られた期間のデータだけを使ってモデルを学習させる方が、パフォーマンスを維持しやすい可能性があることを示唆しています。総じて、取引コストを考慮したうえでも利益を維持できていたことから、モデルには一定の予測力があると言えます。


連続シグナル

アルゴリズム取引では、通常、離散的なシグナル(買いまたは売り)を用い、取引ごとに一定のリスクを設定するというシンプルな方法が一般的です。このやり方は、管理がしやすく、戦略のパフォーマンス分析にも適しています。一部のトレーダーは、この離散的なシグナルに加重シグナル(additive signal)の考え方を取り入れ、戦略条件の満たされ方に応じてリスク量を調整することで、手法を洗練させようと試みています。連続シグナルは、この加重アプローチをさらに発展させたもので、より抽象的な戦略条件に対して適用し、リスクを0から1の範囲で連続的に調整する考え方です。

この考え方の基本は、エントリー条件を満たすすべての取引が同じ価値を持つわけではないという前提にあります。戦略の非線形なルールに基づくと、シグナルの強さに差があり、成功する可能性が高い取引もあれば、やや不確実なものもあるという判断が可能です。これはリスク管理の一手法とも捉えられ、自信のあるシグナルではロットを大きく、確信が薄い場合はロットを抑えるといった運用が可能になります。たとえ期待値がプラスでも、リスクを抑えることで全体のリスクを調整できます。ただし、こうした連続シグナルの導入は戦略のパフォーマンスに新たな変数を加えることになるため、実装時に先読みバイアスや過学習のリスクに注意する必要があります。

この考え方を実際に取引用EAに組み込むためには、まずbuy/sell関数を修正し、ストップロスに到達した場合に損失が所定のリスク額に収まるようにロットサイズを計算する必要があります。そのロットサイズを算出する関数は以下のようになります。  

double calclots(double slpoints, string symbol, double risk)
{  
   double balance = AccountInfoDouble(ACCOUNT_BALANCE);
   double riskAmount = balance* risk / 100;

   double ticksize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
   double tickvalue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
   double lotstep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);

   double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep;
   double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep;
   lots = MathMin(lots, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX));
   lots = MathMax(lots, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN));
   return lots;
}
次に、buy/sell関数を更新して、このcalclots関数を呼び出して、リスク乗数を入力として受け取るようにします。  
void executeSell(double riskMultiplier) {      
       double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
       bid = NormalizeDouble(bid,_Digits);
       double sl = lastClose*(1+slp);
       double tp = lastClose*(1-tpp);
       double lots = 0.1;
       lots = calclots(slp*lastClose,_Symbol,risks*riskMultiplier);
       trade.Sell(lots,_Symbol,bid,sl,tp);  
       }

void executeBuy(double riskMultiplier) {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       double sl = lastClose*(1-slp);
       double tp = lastClose*(1+tpp);
       double lots = 0.1;
       lots = calclots(slp*lastClose,_Symbol,risks*riskMultiplier);
       trade.Buy(lots,_Symbol,ask,sl,tp);
}
私たちの機械学習モデルはすでに信頼度を出力しているため、それをそのままリスク乗数として使用することができます各取引において、信頼度がリスクにどの程度影響を与えるかを調整したい場合は、この信頼度を任意にスケーリングすれば簡単に対応できます。
if(prob[1]>threshold)executeBuy(prob[1]); 
if(prob[0]>threshold)executeSell(prob[0]);

たとえば、信頼度の差異の重要性を強調したい場合は、確率値を3回掛け合わせる(つまり確率の三乗をとる)方法があります。これにより、確率間の比率差が大きくなり、信頼度の影響がより顕著になります。

if(prob[1]>threshold)executeBuy(prob[1]*prob[1]*prob[1]); 
if(prob[0]>threshold)executeSell(prob[0]*prob[0]*prob[0]);

次に、バックテストで結果を確認してみます。 

バックテスト設定(連続)

資産曲線(連続)

結果(連続)

おこなわれた取引自体は離散シグナル版と同じですが、プロフィットファクターとシャープレシオがわずかに改善しました。これは、この特定のケースにおいて、連続シグナルがアウトオブサンプルテストの全体的なパフォーマンスを向上させたことを示唆しています。このテストは一度だけ行ったため、先読みバイアスは含まれていません。ただし、この手法が固定リスク方式を上回るのは、モデルの予測精度が信頼度の高いシグナルほど優れている場合に限られるという点に注意が必要です。そうでなければ、元の固定リスク方式のほうが優れている可能性があります。また、リスク乗数を0から1の範囲で適用したことで平均ロットサイズは減少しているため、以前と同じ総利益を狙う場合はリスク変数の値を上げる必要があります。


マルチタイムフレーム検証

異なる時間枠の特徴量を使って同じ結果を予測する複数の機械学習モデルを別々に訓練することは、取引のフィルタリングやシグナル生成を改善する強力な手法となり得ます。短期データに特化したモデル、そこから中期、さらに長期のトレンドに注目したモデルを用意することで、それぞれが専門的な知見を持ち寄り、単一モデルよりも信頼性の高い予測を得られます。このマルチモデルアプローチは、シグナルのクロスチェックを通じて取引判断への自信を高め、特定の時間軸のノイズに惑わされるリスクを軽減します。また、各モデルの出力を加重してトレードサイズやストップの調整に活かすことでリスク管理にも役立ちます。

一方で、この戦略はシステムを複雑にしがちです。特に複数モデルの予測に異なる重みを割り当てる場合、慎重に調整しないと新たなバイアスや誤りを生む恐れがあります。また、各モデルが特定の時間軸に過学習してしまい、市場の広範な動きを捉えきれない可能性もあります。モデル間の予測が食い違うと、判断が遅れたり自信を失ったりするリスクもあります。 

この手法は、次の2つの重要な前提に依存しています。まず、上位時間枠モデルで先読みバイアスが入らないこと(現在のバーではなく最後のバーの値を使うこと)、そして上位時間枠モデル自体に予測力があること(アウトオブサンプルテストでランダム以上の成績を出すこと)です。  

実装にあたっては、まずデータ取得用EAのコードを修正し、特徴量抽出に関わるすべての時間枠を1時間足などの上位時間枠に変更します。これにはインジケーターや価格計算、その他使用している特徴量すべてが含まれます。

int OnInit()
{
   trade.SetExpertMagicNumber(Magic);
   handleAdx = iADX(_Symbol, PERIOD_H1, 14); // Average Directional Movement Index - 3
   handleWilder = iADXWilder(_Symbol, PERIOD_H1, 14); // Average Directional Movement Index by Welles Wilder - 3
   handleDm = iDeMarker(_Symbol, PERIOD_H1, 14); // DeMarker - 1
   handleRsi = iRSI(_Symbol, PERIOD_H1, 14, PRICE_CLOSE); // Relative Strength Index - 1
   handleRvi = iRVI(_Symbol, PERIOD_H1, 10); // Relative Vigor Index - 2
   handleSto = iStochastic(_Symbol, PERIOD_H1, 5, 3, 3, MODE_SMA, STO_LOWHIGH); // Stochastic Oscillator - 2
   return(INIT_SUCCEEDED);
}

void getData()
{
   double close = iClose(_Symbol, PERIOD_H1, 1);
   double close2 = iClose(_Symbol, PERIOD_H1, 2);
   double close3 = iClose(_Symbol, PERIOD_H1, 3);
   double stationary = 1000 * (close - iOpen(_Symbol, PERIOD_H1, 1)) / close;
   double stationary2 = 1000 * (close2 - iOpen(_Symbol, PERIOD_H1, 2)) / close2;
   double stationary3 = 1000 * (close3 - iOpen(_Symbol, PERIOD_H1, 3)) / close3;
}

その後は、これまでと同様にデータの取得、モデルの学習、エクスポートを行います。先に説明した流れと同じです。

次に、取引用EA内で特徴量を取得するための2つ目の関数を作成します。この関数により、インポートした2つ目の機械学習モデルに特徴量を入力し、信頼度の出力を得られるようにします。

vector getData2()
{
   double close = iClose(_Symbol, PERIOD_H1, 1);
   double close2 = iClose(_Symbol, PERIOD_H1, 2);
   double close3 = iClose(_Symbol, PERIOD_H1, 3);
   double stationary = 1000 * (close - iOpen(_Symbol, PERIOD_H1, 1)) / close;
   double stationary2 = 1000 * (close2 - iOpen(_Symbol, PERIOD_H1, 2)) / close2;
   double stationary3 = 1000 * (close3 - iOpen(_Symbol, PERIOD_H1, 3)) / close3;
   double highDistance = 1000 * (close - high) / close;
   double lowDistance = 1000 * (close - low) / close;
   double boxSize = 1000 * (high - low) / close;
   double adx[];       // Average Directional Movement Index
   double wilder[];    // Average Directional Movement Index by Welles Wilder
   double dm[];        // DeMarker
   double rsi[];       // Relative Strength Index
   double rvi[];       // Relative Vigor Index
   double sto[];       // Stochastic Oscillator

   CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
   CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
   CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
   CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
   CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
   CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

   data[0][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
   data[0][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
   data[0][2] = DoubleToString(dm[0], 2);       // DeMarker
   data[0][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
   data[0][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
   data[0][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
   data[0][6] = DoubleToString(stationary, 2);
   data[0][7] = DoubleToString(boxSize, 2);
   data[0][8] = DoubleToString(stationary2, 2);
   data[0][9] = DoubleToString(stationary3, 2);
   data[0][10] = DoubleToString(highDistance, 2);
   data[0][11] = DoubleToString(lowDistance, 2);

   vector features(12);    
   for(int i = 0; i < 12; i++)
   {
      features[i] = StringToDouble(data[0][i]);
   }
   return features;
}

2つのモデルの出力に同じ重みを割り当てたい場合は、それぞれの出力の平均を取り、これまで使っていた単一の出力として扱えばよいです。

if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low))){
        xx = getData();
        xx2 = getData2();
        prob = cat_boost.predict_proba(xx);
        prob2 = cat_boost.predict_proba(xx2);
        double probability_buy = (prob[1]+prob2[1])/2;
        double probability_sell = (prob[0]+prob2[0])/2;

        if(probability_buy>threshold)executeBuy(probability_buy); 
        if(probability_sell>threshold)executeSell(probability_sell);
        }
    }

上記のように計算した2つの変数を組み合わせて、ひとつの信頼度としてまとめることができます。そして、それを先ほどと同様の方法で検証に活用します。


結論

この記事ではまず、機械学習モデルをフィルターではなくシグナル生成器として使うアイデアを、ダーバスボックスブレイクアウト戦略を通じて紹介しました。モデルの学習プロセスに軽く触れ、信頼度の閾値や特徴量の重要性についても議論しました。次に、連続シグナルの概念を紹介し、離散シグナルと比較しました。その結果、この例では連続シグナルの方がバックテストのパフォーマンスを改善し、信頼度が高まるほどモデルの予測精度も向上する傾向があることがわかりました。最後に、異なる時間枠で学習した複数の機械学習モデルを組み合わせてシグナルを検証する手法の概要にも触れました。  

総じて、本記事の目的はCTAトレードにおける教師あり学習での機械学習モデルの応用について、一般的ではない斬新な考え方を提示することにありました。どの手法が最良かを断言するものではなく、シナリオによって異なるため、読者が創造的に考え、シンプルな初歩的なアイデアから発展させていくためのヒントを提供することを目指しています。 結局のところ、まったく新しいものはなく、イノベーションは既存のアイデアの組み合わせから生まれることが多いのです。

ファイルの表

ファイル名 説明
Darvas_Box.ipynb MLモデルをトレーニングするためのJupyter Notebookファイル
Darvas Box Data.mq5 モデルトレーニング用のデータを取得するためのEA
ダーバスボックスEA.mq5 この記事の取引EA
CatOnnx.mqh CatBoostモデルを処理するためのインクルードファイル
FileCSV.mqh データをCSVに保存するためのインクルードファイル

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

添付されたファイル |
Darvas_ML.zip (136.61 KB)
最後のコメント | ディスカッションに移動 (2)
linfo2
linfo2 | 24 3月 2025 において 00:42
Zhouさん、興味深い記事とコードサンプルをありがとうございました。私の場合、動作させるためにいくつかのPythonコンポーネントを手動でインストールする必要がありました。これがどこから来ているのか、重要なことなのか、私にはわかりません。

5802

プロパティまたは値がMQL5でサポートされていません。
Zhuo Kai Chen
Zhuo Kai Chen | 24 3月 2025 において 01:09
linfo2 #:
Zhouさん、興味深い記事とコードサンプルをありがとうございました。私の場合、動作させるためにいくつかのPythonコンポーネントを手動でインストールする必要がありました。これがどこから来ているのか、重要なことなのか、私にはわかりません。

5802

プロパティまたは値がMQL5でサポートされていません。

ご指摘ありがとうございます。pip installの部分は無視されましたが、関連ライブラリをまだインストールしていない場合はインストールする必要があります。

エラーの原因は、モデルのトレーニングで使用されたディメンションが、EAで使用されたディメンションと異なっているためかもしれません。例えば、5つのフィーチャーでモデルをトレーニングした場合、EAでも4や6ではなく、5つのフィーチャーを入力する必要があります。より詳細なウォークスルーは、この記事のリンクに あります。お役に立てれば幸いです。そうでない場合は、より多くの文脈を提供してください。

プライスアクション分析ツールキットの開発(第18回):クォーターズ理論の紹介(III) - Quarters Board プライスアクション分析ツールキットの開発(第18回):クォーターズ理論の紹介(III) - Quarters Board
この記事では、元のQuarters Scriptを改良し、「Quarters Board」というツールを導入しています。これにより、コードを編集し直すことなく、チャート上でクォーターレベルを直接オン・オフできるようになります。特定のレベルを簡単に有効化・無効化できるほか、エキスパートアドバイザー(EA)はトレンド方向に関するコメントも提供し、市場の動きをより理解しやすくします。
知っておくべきMQL5ウィザードのテクニック(第57回):移動平均とストキャスティクスを用いた教師あり学習 知っておくべきMQL5ウィザードのテクニック(第57回):移動平均とストキャスティクスを用いた教師あり学習
移動平均線やストキャスティクスは非常に一般的なテクニカル指標ですが、その「遅行性」のために一部のトレーダーから敬遠されがちです。この3部構成のミニシリーズでは、機械学習の3つの主要なアプローチを軸に、この偏見が本当に正当なものなのか、それとも実はこれらの指標に優位性が隠れているのかを検証していきます。検証には、ウィザードで組み立てられたエキスパートアドバイザー(EA)を用います。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
MQL5における予測および分類評価のためのリサンプリング手法 MQL5における予測および分類評価のためのリサンプリング手法
本記事では、1つのデータセットを訓練(学習)用と検証用の両方として使用するモデル評価手法について、理論と実装の両面から検討します。