English Deutsch
preview
CatBoost機械学習モデルをトレンド追従戦略のフィルターとして活用する

CatBoost機械学習モデルをトレンド追従戦略のフィルターとして活用する

MetaTrader 5統合 | 10 4月 2025, 07:51
70 0
Zhuo Kai Chen
Zhuo Kai Chen

はじめに

CatBoostは、定常的な特徴量に基づいて意思決定をおこなうことに特化した、強力なツリーベースの機械学習モデルです。XGBoostやRandom Forestといった他のツリーベースモデルも、堅牢性、複雑なパターンへの対応力、そして高い解釈性といった点で共通した特長を備えています。これらのモデルは、特徴量分析からリスク管理に至るまで、幅広い分野で活用されています。 

本記事では、学習済みのCatBoostモデルを、従来型の移動平均クロスを用いたトレンドフォロー戦略のフィルターとして活用する手順を解説します。戦略構築の過程で直面しうる課題を取り上げながら、具体的な開発プロセスへの理解を深めることを目的としています。MetaTrader 5からのデータ取得、Pythonによる機械学習モデルの学習、そしてそれをMetaTrader 5のエキスパートアドバイザー(EA)へ統合するまでのワークフローをご紹介します。記事の終盤では、統計的検証を通じて戦略の有効性を確認し、現在のアプローチをもとにした今後の展望についても考察していきます。


直感

CTA(商品取引アドバイザー)戦略の開発においては、すべての戦略アイデアの背後に明確で直感的な説明があることが重要だというのが、業界で広く受け入れられている経験則です。これは、人が戦略アイデアを思いつく自然なプロセスでもあり、過学習を避ける手段としても有効です。この考え方は、機械学習モデルを用いる場合にも同様に重要です。ここでは、このアイデアの背後にある直感的な理由を説明します。

これが機能する理由

CatBoostモデルは、特徴量を入力として受け取り、それに基づいて各結果の確率を出力する決定木を構築します。このケースでは、勝ち(1)と負け(0)の二値分類問題としてモデルを訓練しています。モデルは、学習データにおける損失関数を最小化するように、決定木内の分岐ルールを調整していきます。モデルがサンプル外のデータでも一定の予測精度を示す場合、勝つ可能性が低い取引をフィルタリングするための手段として活用でき、その結果として全体的な収益性の向上につながる可能性があります。

私たちのような個人トレーダーにとって現実的な期待値は、訓練されたモデルが万能な存在となるわけではなく、あくまでランダムウォークをわずかに上回る程度の精度が得られれば十分ということです。もちろん、モデルの精度を高めるための方法は多く存在し、それについては後で触れますが、たとえ小さな改善であっても、取り組む価値のある重要な一歩となります。


バックボーン戦略の最適化

前のセクションでも述べたように、モデルによるパフォーマンス向上はあくまでわずかなものであるため、ベースとなる戦略(バックボーン戦略)自体に一定の収益性が備わっていることが非常に重要です。

また、この戦略では、以下の理由から十分な数のサンプルを生成できる必要があります。

  1. モデルは取引の一部を除外するため、残されたサンプル数が大数の法則に基づく統計的有意性を示すのに十分である必要があります。 
  2. 学習データ(インサンプル)に対する損失関数を効果的に最小化するためにも、モデルの訓練に必要な十分なサンプル数が求められます。

本記事では、異なる期間の2つの移動平均線が交差したタイミングでエントリーし、価格が移動平均線の反対側に抜けたときにエグジットするという、実績のあるトレンドフォロー戦略を採用しています。つまり、トレンドに従って取引をおこなう戦略です。以下のMQL5コードは、この戦略を実装したEAです。

#include <Trade/Trade.mqh>
//XAU - 1h.
CTrade trade;

input ENUM_TIMEFRAMES TF = PERIOD_CURRENT;
input ENUM_MA_METHOD MaMethod = MODE_SMA;
input ENUM_APPLIED_PRICE MaAppPrice = PRICE_CLOSE;
input int MaPeriodsFast = 15;
input int MaPeriodsSlow = 25;
input int MaPeriods = 200;
input double lott = 0.01;
ulong buypos = 0, sellpos = 0;
input int Magic = 0;
int barsTotal = 0;
int handleMaFast;
int handleMaSlow;
int handleMa;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
   handleMaFast =iMA(_Symbol,TF,MaPeriodsFast,0,MaMethod,MaAppPrice);
   handleMaSlow =iMA(_Symbol,TF,MaPeriodsSlow,0,MaMethod,MaAppPrice);  
   handleMa = iMA(_Symbol,TF,MaPeriods,0,MaMethod,MaAppPrice); 
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {

  }  

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);
  //Beware, the last element of the buffer list is the most recent data, not [0]
  if (barsTotal!= bars){
     barsTotal = bars;
     double maFast[];
     double maSlow[];
     double ma[];
     CopyBuffer(handleMaFast,BASE_LINE,1,2,maFast);
     CopyBuffer(handleMaSlow,BASE_LINE,1,2,maSlow);
     CopyBuffer(handleMa,0,1,1,ma);
     double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
     double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
     double lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
     //The order below matters
     if(buypos>0&& lastClose<maSlow[1]) trade.PositionClose(buypos);
     if(sellpos>0 &&lastClose>maSlow[1])trade.PositionClose(sellpos);   
     if (maFast[1]>maSlow[1]&&maFast[0]<maSlow[0]&&buypos ==sellpos)executeBuy(); 
     if(maFast[1]<maSlow[1]&&maFast[0]>maSlow[0]&&sellpos ==buypos) executeSell();
     if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
      buypos = 0;
      }
     if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
      sellpos = 0;
      }
    }
 }

//+------------------------------------------------------------------+
//| Expert trade transaction handling function                       |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) {
    if (trans.type == TRADE_TRANSACTION_ORDER_ADD) {
        COrderInfo order;
        if (order.Select(trans.order)) {
            if (order.Magic() == Magic) {
                if (order.OrderType() == ORDER_TYPE_BUY) {
                    buypos = order.Ticket();
                } else if (order.OrderType() == ORDER_TYPE_SELL) {
                    sellpos = order.Ticket();
                }
            }
        }
    }
}

//+------------------------------------------------------------------+
//| Execute sell trade function                                      |
//+------------------------------------------------------------------+
void executeSell() {      
       double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
       bid = NormalizeDouble(bid,_Digits);
       trade.Sell(lott,_Symbol,bid);  
       sellpos = trade.ResultOrder();  
       }   

//+------------------------------------------------------------------+
//| Execute buy trade function                                       |
//+------------------------------------------------------------------+
void executeBuy() {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       trade.Buy(lott,_Symbol,ask);
       buypos = trade.ResultOrder();
}

バックボーン戦略を検証するには、次の点を考慮する必要があります。

  1. 十分なサンプル数があること(サンプルの頻度はタイムフレームやシグナルの制限に依存しますが、総サンプル数としては1000~10000件を推奨します。1回の取引が1サンプルです。)
  2. ある程度の収益性がすでに確認できていること、ただし高すぎないこと(プロフィットファクターが1~1.15程度であれば十分と考えられます。MetaTrader 5のテスターはスプレッドを考慮しているため、プロフィットファクターが1でも統計的な優位性があると判断できます。1.15を超える場合、その戦略単体で十分に機能している可能性が高く、複雑性を増すような追加のフィルターは不要かもしれません。)
  3. バックボーン戦略にあまり多くのパラメーターがないこと(機械学習モデルをフィルターとして使用することで戦略の複雑性はすでに高まっているため、バックボーン戦略自体はシンプルであることが望ましいです。フィルターが少なければ過剰適合のリスクも低く抑えられます。)

以下は、私がこの戦略を最適化するために行った取り組みです。

  1. 適切な時間枠の選定:異なる時間枠でコードを実行した結果、この戦略は上位の時間枠で最も効果を発揮することが分かりました。ただし、十分なサンプル数を確保するため、最終的には1時間足(1H)に落ち着きました。
  2. パラメータの最適化:長期移動平均と短期移動平均の期間をステップ幅5で最適化し、上記のコードにある設定値を得ました。  
  3. エントリーの時点で、価格がある期間の移動平均をすでに上回っている(または下回っている)ことを条件とするフィルターを試しました。これは、すでにその方向にトレンドが出ていることを意味します。ただし重要なのは、こうしたフィルターも直感的な説明が可能であること、そしてデータスヌーピングなしで仮説を検証する必要があるという点です。最終的には、このルールを加えてもパフォーマンスに大きな改善が見られなかったため、戦略の複雑化を避ける目的でこのアイデアは破棄しました。

最後に、これはXAUUSD1時間枠、2004.1.1–2024.11.1でのテスト結果です。

設定

パラメータ

曲線1

結果1


データの取得

モデルを訓練するには、各取引時点での特徴量が必要であり、さらにその取引の結果も把握する必要があります。私が最も効率的かつ確実だと感じている方法は、対応する特徴量をすべて2次元配列に保存するEAを作成することです。そして取引結果については、バックテストのレポートをエクスポートするだけで取得できます。

まず、取引結果を取得するには、バックテスト結果画面で右クリックしてレポートを選択し、次のようにXMLで開きます。

Excelレポート

次に、double配列をCSVに変換するには、この記事で説明されているCFileCSVクラスを使用します。

私たちは、バックボーン戦略スクリプトの上に次の手順で構築します。

1. mqhファイルをインクルードし、クラスオブジェクトを作成します。

#include <FileCSV.mqh>

CFileCSV csvFile;

2.保存するファイル名と、「index」およびその他のすべての特徴量名を含むヘッダーを宣言します。ここでの「index」は、テスターの実行中に配列インデックスを更新するためにのみ使用され、後でPythonで削除されます。

string fileName = "ML.csv";
string headers[] = {
    "Index",
    "Accelerator Oscillator", 
    "Average Directional Movement Index", 
    "Average Directional Movement Index by Welles Wilder", 
    "Average True Range", 
    "Bears Power", 
    "Bulls Power", 
    "Commodity Channel Index", 
    "Chaikin Oscillator", 
    "DeMarker", 
    "Force Index", 
    "Gator", 
    "Market Facilitation Index", 
    "Momentum", 
    "Money Flow Index", 
    "Moving Average of Oscillator", 
    "MACD", 
    "Relative Strength Index", 
    "Relative Vigor Index", 
    "Standard Deviation", 
    "Stochastic Oscillator", 
    "Williams' Percent Range", 
    "Variable Index Dynamic Average", 
    "Volume",
    "Hour",
    "Stationary"
};

string data[10000][26];
int indexx = 0;
vector xx;

3.すべての特徴量を計算してグローバル配列に格納するgetData()関数を記述します。この場合、時間、オシレーター、定常価格を特徴として使用します。この関数は、取引シグナルがあるたびに呼び出され、取引と一致するようになります。特徴量の選択については後述します。

//+------------------------------------------------------------------+
//| Execute get data function                                        |
//+------------------------------------------------------------------+
vector getData(){
//23 oscillators
double ac[];        // Accelerator Oscillator
double adx[];       // Average Directional Movement Index
double wilder[];    // Average Directional Movement Index by Welles Wilder
double atr[];       // Average True Range
double bep[];       // Bears Power
double bup[];       // Bulls Power
double cci[];       // Commodity Channel Index
double ck[];        // Chaikin Oscillator
double dm[];        // DeMarker
double f[];         // Force Index
double g[];         // Gator
double bwmfi[];     // Market Facilitation Index
double m[];         // Momentum
double mfi[];       // Money Flow Index
double oma[];       // Moving Average of Oscillator
double macd[];      // Moving Averages Convergence/Divergence
double rsi[];       // Relative Strength Index
double rvi[];       // Relative Vigor Index
double std[];       // Standard Deviation
double sto[];       // Stochastic Oscillator
double wpr[];       // Williams' Percent Range
double vidya[];     // Variable Index Dynamic Average
double v[];         // Volume

CopyBuffer(handleAc, 0, 1, 1, ac);           // Accelerator 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(handleAtr, 0, 1, 1, atr);         // Average True Range
CopyBuffer(handleBep, 0, 1, 1, bep);         // Bears Power
CopyBuffer(handleBup, 0, 1, 1, bup);         // Bulls Power
CopyBuffer(handleCci, 0, 1, 1, cci);         // Commodity Channel Index
CopyBuffer(handleCk, 0, 1, 1, ck);           // Chaikin Oscillator
CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
CopyBuffer(handleF, 0, 1, 1, f);             // Force Index
CopyBuffer(handleG, 0, 1, 1, g);             // Gator
CopyBuffer(handleBwmfi, 0, 1, 1, bwmfi);     // Market Facilitation Index
CopyBuffer(handleM, 0, 1, 1, m);             // Momentum
CopyBuffer(handleMfi, 0, 1, 1, mfi);         // Money Flow Index
CopyBuffer(handleOma, 0, 1, 1, oma);         // Moving Average of Oscillator
CopyBuffer(handleMacd, 0, 1, 1, macd);       // Moving Averages Convergence/Divergence
CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
CopyBuffer(handleStd, 0, 1, 1, std);         // Standard Deviation
CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator
CopyBuffer(handleWpr, 0, 1, 1, wpr);         // Williams' Percent Range
CopyBuffer(handleVidya, 0, 1, 1, vidya);     // Variable Index Dynamic Average
CopyBuffer(handleV, 0, 1, 1, v);             // Volume
//2 means 2 decimal places
data[indexx][0] = IntegerToString(indexx);
data[indexx][1] = DoubleToString(ac[0], 2);       // Accelerator Oscillator
data[indexx][2] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
data[indexx][3] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
data[indexx][4] = DoubleToString(atr[0], 2);      // Average True Range
data[indexx][5] = DoubleToString(bep[0], 2);      // Bears Power
data[indexx][6] = DoubleToString(bup[0], 2);      // Bulls Power
data[indexx][7] = DoubleToString(cci[0], 2);      // Commodity Channel Index
data[indexx][8] = DoubleToString(ck[0], 2);       // Chaikin Oscillator
data[indexx][9] = DoubleToString(dm[0], 2);       // DeMarker
data[indexx][10] = DoubleToString(f[0], 2);       // Force Index
data[indexx][11] = DoubleToString(g[0], 2);       // Gator
data[indexx][12] = DoubleToString(bwmfi[0], 2);   // Market Facilitation Index
data[indexx][13] = DoubleToString(m[0], 2);       // Momentum
data[indexx][14] = DoubleToString(mfi[0], 2);     // Money Flow Index
data[indexx][15] = DoubleToString(oma[0], 2);     // Moving Average of Oscillator
data[indexx][16] = DoubleToString(macd[0], 2);    // Moving Averages Convergence/Divergence
data[indexx][17] = DoubleToString(rsi[0], 2);     // Relative Strength Index
data[indexx][18] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
data[indexx][19] = DoubleToString(std[0], 2);     // Standard Deviation
data[indexx][20] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
data[indexx][21] = DoubleToString(wpr[0], 2);     // Williams' Percent Range
data[indexx][22] = DoubleToString(vidya[0], 2);   // Variable Index Dynamic Average
data[indexx][23] = DoubleToString(v[0], 2);       // Volume

    datetime currentTime = TimeTradeServer(); 
    MqlDateTime timeStruct;
    TimeToStruct(currentTime, timeStruct);
    int currentHour = timeStruct.hour;
data[indexx][24]= IntegerToString(currentHour);
    double close = iClose(_Symbol,PERIOD_CURRENT,1);
    double open = iOpen(_Symbol,PERIOD_CURRENT,1);
    double stationary = MathAbs((close-open)/close)*100;
data[indexx][25] = DoubleToString(stationary,2);
  
   vector features(26);    
   for(int i = 1; i < 26; i++)
    {
      features[i] = StringToDouble(data[indexx][i]);
    }
    //A lot of the times positions may not open due to error, make sure you don't increase index blindly
    if(PositionsTotal()>0) indexx++;
    return features;
}

ここでチェックを追加したことに注意してください。

if(PositionsTotal()>0) indexx++;

これは、取引シグナルが発生しても、EAが市場のクローズ時間中に実行されているため、取引がおこなわれない場合があり、テスターは取引をおこなわないためです。

4.テストが終了すると、OnDeInit()が呼び出されてファイルを保存します。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
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!");
     }

  }

このEAをストラテジーテスターで実行すると、/Tester/Agent-sth000ディレクトリに作成されたcsvファイルが生成されます。


データのクリーニングと調整

これで2つのデータファイルが完成しましたが、解決すべき根本的な問題がまだたくさん残っています。

1. バックテストレポートは乱雑で、.xlsx形式です。私たちが知りたいのは、各取引で勝ったかどうかだけです。

まず、取引結果のみを表示する行を抽出します。次のような内容が表示されるまで、XLSXファイルを下にスクロールする必要がある場合があります。

行を検索

行番号を覚えておき、次のPythonコードに適用します。

import pandas as pd

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

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

# Save the extracted content to a CSV file
output_file = 'extracted_content.csv'
df.to_csv(output_file, index=False)

print(f"Content has been saved to {output_file}.")

次に、この抽出されたコンテンツを次のコードに適用して、処理されたビンを取得します。勝ち取引は1になり、負け取引は0になります。

import pandas as pd

# Load the CSV file
file_path = 'extracted_content.csv'  # Update with the correct file path if needed
data = pd.read_csv(file_path)

# Select the 'profit' column (assumed to be 'Unnamed: 10') and filter rows as per your instructions
profit_data = data["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

# Save the processed data to a new CSV file with index
output_csv_path = 'processed_bin.csv'
profit_data.to_csv(output_csv_path, index=True, header=['bin'])

print(f"Processed data saved to {output_csv_path}")

結果ファイルは次のようになります。


ビン
0 1
1 0
2 1
3 0
4 0
5 1

すべての値が0の場合は、開始行が正しくないことが原因の可能性があります。開始行が偶数か奇数かを確認し、それに応じてPythonコードで変更してください。

2.特徴量データはCFileCSVクラスのためすべて文字列であり、コンマで区切られて1つの列にまとめられています。

次のPythonコードで目的を達成できます。

import pandas as pd

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

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

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

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

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

# Save the cleaned data to a new file in CatBoost-friendly format
output_file_path = 'Cleaned.csv'
data.to_csv(output_file_path, index=False)

print(f"Data cleaned and saved to {output_file_path}")

最後に、このコードを使用して2つのファイルを結合し、将来単一のデータフレームとして簡単にアクセスできるようにします。

import pandas as pd

# Load the two CSV files
file1_path = 'processed_bin.csv'  # Update with the correct file path if needed
file2_path = 'Cleaned.csv'  # Update with the correct file path if needed
data1 = pd.read_csv(file1_path, index_col=0)  # Load first file with index
data2 = pd.read_csv(file2_path, index_col=0)  # Load second file with index

# Merge the two DataFrames on the index
merged_data = pd.merge(data1, 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}")

2つのデータが正しく結合されたことを確認するには、先ほど作成した3つのCSVファイルをチェックして、最終的なインデックスが同じかどうかを確認します。もし同じなら、リラックスできます。


訓練モデル

機械学習の各側面の背後にある技術的な説明については、あまり深く掘り下げません。ただし、ML取引全体に興味がある方は、Marcos López de Prado著の「AdvancesinFinancialMachineLearning」をぜひ読んでみてください。

このセクションの目的は非常に明確です。

まず、pandasライブラリを使用して結合されたデータを読み取り、bin列をyとして、残りをXとして分割します。

data = pd.read_csv("merged_data.csv",index_col=0)
XX = data.drop(columns=['bin'])
yy = data['bin']
y = yy.values
X = XX.values

次に、データを訓練用に80%、テスト用に20%に分割します。

それから訓練をします。分類器の各パラメータの詳細については、CatBoost Webサイトに記載されています。

from catboost import CatBoostClassifier
from sklearn.ensemble import BaggingClassifier

# Define the CatBoost model with initial parameters
catboost_clf = CatBoostClassifier(
    class_weights=[10, 1],  #more weights to 1 class cuz there's less correct cases
    iterations=20000,             # 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
    random_seed=RANDOM_STATE,
    verbose=1000,                  # Suppress output (set to a positive number if you want to see training progress)
)

fit = catboost_clf.fit(X_train, y_train)

.cbmファイルを保存します。

catboost_clf.save_model('catboost_test.cbm')

残念ながら、まだ終わっていません。MetaTrader 5はONNX形式のモデルのみをサポートしているため、この記事の次のコードを使用してONNX形式に変換します。

model_onnx = convert_sklearn(
    model,
    "catboost",
    [("input", FloatTensorType([None, X.shape[1]]))],
    target_opset={"": 12, "ai.onnx.ml": 2},
)

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


統計的検定

.onnxファイルを取得したら、それをMQL5/Filesフォルダにドラッグします。先ほどデータを取得するために使用したEAの上に構築します。繰り返しになりますが、EAで.onnxモデルを初期化する手順についてはこちらの記事ですでに詳しく説明しているので、ここではエントリー基準を変更する方法に重点を置きます。 

     if (maFast[1]>maSlow[1]&&maFast[0]<maSlow[0]&&sellpos == buypos){   
        xx= getData();
        prob = cat_boost.predict_proba(xx);
        if (prob[1]<max&&prob[1]>min)executeBuy(); 
     }
     if(maFast[1]<maSlow[1]&&maFast[0]>maSlow[0]&&sellpos == buypos){
        xx= getData();
        prob = cat_boost.predict_proba(xx);
        Print(prob);
        if(prob[1]<max&&prob[1]>min)executeSell();
      }

ここでは、getData()を呼び出してベクトル情報を変数xxに格納し、モデルに従って成功の確率を返します。範囲がどの程度になるかがわかるように、print文を追加しました。トレンド追従戦略の場合、精度が低く、取引ごとのリスク対報酬比が高いため、通常、モデルは0.5未満の確率を与えます。

成功確率が低い取引を除外するための閾値を追加し、コーディング部分は完了です。ではテストしてみましょう。

8対2の比率で分割したことを覚えているでしょうか。ここで、およそ2021.1.1~2024.11.1の未訓練データに対してサンプル外テストを実行します。

まず、0.05の確率閾値でインサンプルテストを実行し、適切なデータで訓練したことを確認します。結果はほぼ完璧になるはずです。

インサンプル曲線

次に、ベースラインとして閾値なしでサンプル外テストを実行します。閾値を拡大すると、このベースライン結果をかなり上回る結果になると予想されます。

ベースライン曲線

ベースライン結果

最後に、サンプル外テストを実施して、さまざまな閾値に対する収益性パターンを分析します。

以下は、閾値=0.05の結果です。

0.05曲線

0.05結果

以下は、閾値=0.1の結果です。

0.1カーブ

0.1結果

以下は、閾値=0.2の結果です。

0.2カーブ

0.2結果

閾値が0.05の場合、モデルは元の取引の約半分を除外しましたが、これにより収益性が低下しました。予測変数が過剰適合し、訓練されたパターンに過度に適合し、訓練セットとテストセット間で共有される類似のパターンをキャプチャできないことを示している可能性があります。金融機械学習では、これはよくある問題です。ただし、閾値を0.1に増やすと、利益率は徐々に向上し、ベースラインを上回ります。

閾値0.2では、モデルは元の取引の約70%を除外しますが、残りの取引の全体的な品質は元の取引よりも大幅に収益性が高くなります。統計分析によると、この閾値の範囲内では、全体的な収益性は閾値と正の相関関係にあります。これは、取引に対するモデルの信頼度が高まるにつれて、全体的なパフォーマンスも向上し、好ましい結果となることを示唆しています。

モデルの精度が一貫していることを確認するために、Pythonで10倍の交差検証を実行しました。 

{'score': array([-0.97148655, -1.25263677, -1.02043177, -1.06770248, -0.97339545, -0.88611439, -0.83877111, -0.95682533, -1.02443847, -1.1385681 ])}

各交差検証スコアの差はわずかであり、モデルの精度はさまざまな訓練期間とテスト期間にわたって一貫していることを示しています。

さらに、平均ログ損失スコアが約-1であるため、モデルのパフォーマンスは中程度に効果的であると考えられます。

モデルの精度をさらに向上させるには、次のアイデアを取り入れることができます。

1. 特徴量エンジニアリング

このように特徴量の重要度をプロットし、重要度の低いものを削除します。

特徴量を選択する場合、市場に関連するものであれば何でも可能ですが、ツリーベースのモデルでは固定値ルールを使用して入力を処理するため、データが定常であることを確認してください。

特徴量の重要性

2.ハイパーパラメータの調整

先ほど説明した分類関数のパラメータを覚えているでしょうか。値のグリッドをループし、どの訓練パラメータが最良の交差検証スコアを生成するかをテストする関数を作成できます。 

3.モデルの選択

さまざまな機械学習モデルやさまざまな種類の値を予測するために試すことができます。機械学習モデルは価格の予測には向いていないものの、ボラティリティの予測にはかなり優れていることが分かっています。さらに、隠れマルコフモデルは隠れたトレンドを予測するために広く使用されています。これらは両方とも、トレンド追従戦略の強力なフィルターになる可能性があります。

読者の皆さんには、添付のコードでこれらの方法を試してみて、パフォーマンスの向上に成功したかどうかをお知らせください。


結論

この記事では、トレンド追従戦略のためのCatBoost機械学習フィルターを開発するワークフロー全体を説明しました。途中で、機械学習戦略を研究する際に注意すべきさまざまな側面を強調しました。最後に、統計的テストを通じて戦略を検証し、現在のアプローチから拡張した将来の目標について話し合いました。


添付ファイルテーブル

ファイル名 使用法
 ML-Momentum Data.mq5  特徴量データを取得するEA
 ML-Momentum.mq5  最終実行EA
 CB2.ipynb CatBoostモデルの訓練とテストのワークフロー 
handleMql5DealReport.py 取引レポートから有用な行を抽出する
getBinFromMql5.py 抽出されたコンテンツからバイナリ結果を取得する
clean_mql5_csv.py MT5から抽出したCSVの特徴をクリーンアップする
merge_data2.py 特徴量と結果を1つのCSVに統合する
OnnxConvert.ipynb .cbmモデルを.onnx形式に変換する
Classic Trend Following.mq5
バックボーン戦略EA

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

添付されたファイル |
ML-TF-Project.zip (186.72 KB)
MQL5経済指標カレンダーを使った取引(第5回):レスポンシブコントロールとフィルターボタンでダッシュボードを強化する MQL5経済指標カレンダーを使った取引(第5回):レスポンシブコントロールとフィルターボタンでダッシュボードを強化する
この記事では、ダッシュボードの制御を改善するために、通貨ペアフィルター、重要度レベル、時間フィルター、キャンセルオプションのボタンを作成します。これらのボタンは、ユーザーのアクションに動的に応答するようにプログラムされており、シームレスな操作を可能にします。また、ダッシュボードにリアルタイムの変更を反映するために、ユーザーの行動を自動化します。これにより、パネルの全体的な機能性、モビリティ、応答性が向上します。
MQL5とデータ処理パッケージの統合(第4回):ビッグデータの取り扱い MQL5とデータ処理パッケージの統合(第4回):ビッグデータの取り扱い
今回は、MQL5と強力なデータ処理ツールを統合する高度なテクニックに焦点を当て、取引分析および意思決定を強化するためのビッグデータの効率的な活用方法を探ります。
ケリー基準とモンテカルロシミュレーションを使用したポートフォリオリスクモデル ケリー基準とモンテカルロシミュレーションを使用したポートフォリオリスクモデル
数十年にわたり、トレーダーは破産リスクを最小限に抑えつつ長期的な資産成長を最大化する手法として、ケリー基準の公式を活用してきました。しかし、単一のバックテスト結果に基づいてケリー基準を盲目的に適用することは、個人トレーダーにとって非常に危険です。というのも、実際の取引では時間の経過とともに取引優位性が薄れ、過去の実績は将来の結果を保証するものではないからです。本記事では、Pythonによるモンテカルロシミュレーションの結果を取り入れ、MetaTrader 5上で1つ以上のエキスパートアドバイザー(EA)にケリー基準を現実的に適用するためのリスク配分アプローチを紹介します。
MQL5で取引管理者パネルを作成する(第8回):分析パネル MQL5で取引管理者パネルを作成する(第8回):分析パネル
今日は、管理パネルEAに統合された専用ウィンドウ内に、便利な取引メトリクスを組み込む方法について掘り下げていきます。本稿では、MQL5を活用して分析パネル(Analytics Panel)を開発する方法に焦点を当て、そのパネルが取引管理者にもたらすデータの価値について解説します。この開発プロセスは教育的意義が大きく、初心者・経験者を問わず開発者にとって有益な学びを提供します。この機能は、高度なソフトウェアツールを通じて取引マネージャーを支援する本連載の可能性を示す好例です。さらに、取引管理パネル(Trading Administrator Panel)の機能拡張の一環として、PieChartクラスとChartCanvasクラスの実装についても取り上げます。