English Русский Deutsch 日本語
preview
利用CatBoost机器学习模型作为趋势跟踪策略的过滤器

利用CatBoost机器学习模型作为趋势跟踪策略的过滤器

MetaTrader 5积分 |
32 12
Zhuo Kai Chen
Zhuo Kai Chen

概述

CatBoost是一种强大的基于树的机器学习模型,擅长基于静态特征进行决策。其他基于树的模型,如XGBoost和随机森林(Random Forest),在稳健性、处理复杂模式的能力以及可解释性方面具有相似特性。这些模型应用广泛,可用于特征分析、风险管理等多个领域。 

在本文中,我们将逐步介绍如何将训练好的CatBoost模型用作经典移动平均线交叉趋势跟踪策略的过滤器。本文旨在深入剖析策略开发过程,并探讨在此过程中可能遭遇的种种挑战。我将介绍自己从MetaTrader 5获取数据、在Python中训练机器学习模型,并将其重新集成到MetaTrader 5中EA的工作流程。在本文结尾,我们将通过统计测试来验证该策略,并探讨基于当前方法的未来展望。


策略思路

在商品交易(CTA)策略开发领域,有一条经验法则:每个策略想法背后都最好有清晰、直观的解释。这不仅是人们最初构思策略想法的方式,更有助于避免过拟合问题。即便在使用机器学习模型时,这一建议也同样适用。我们将尝试解释这一想法背后的直觉。

为何这一方法可能奏效:

CatBoost模型会创建决策树,这些决策树接收特征输入并输出每种结果的概率。在本例中,我们仅针对二元结果(1代表盈利,0代表亏损)进行训练。模型会调整决策树中的规则,以最小化训练数据集中的损失函数。如果模型在样本外测试结果中展现出一定的预测能力,我们就可以考虑用它来过滤掉那些盈利概率较低的交易,从而提升整体盈利能力。

对于像我们这样的散户交易者而言,现实期望是,我们训练的模型不会像先知一样精准无误,而只会比随机游走略胜一筹。提高模型精度的途径有很多,我将在后文讨论,但即便只是微小的改进,也是值得尝试的。


优化核心策略

从上述内容中我们已经了解到,只能期望模型能略微提升性能,因此,核心策略本身必须已经具备一定的盈利能力。

此外,该策略还必须能够生成大量样本,因为:

  1. 模型会过滤掉一部分交易,我们希望确保剩余的样本足够多,以体现出大数定律的统计显著性。 
  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之间。每笔交易即为一个样本。)
  2. 策略已展现出一定的盈利能力,但不宜过高(我认为盈利因子在1至1.15之间就足够了。由于MetaTrader 5测试器已经考虑了点差因素,因此盈利因子为1即意味着该策略已具备统计优势。若盈利因子超过1.15,则该策略很可能已足够优秀,无需再添加更多过滤器来增加复杂性。)
  3. 核心策略的参数不宜过多。(核心策略最好保持简单,因为使用机器学习模型作为过滤器已经为您的策略增加了一些复杂性。过滤器越少,过拟合的可能性就越小。)

以下是我为优化该策略所采取的措施:

  1. 寻找合适的时间框架。在尝试了不同时间框架后,我发现该策略在较高时间框架上表现最优,但为了生成足够的样本,我最终选择了1小时时间框架。
  2. 优化参数我以步长为5对慢速移动平均线和快速移动平均线的周期进行了优化,并得出了上述代码中的设置。  
  3. 我曾尝试添加一条规则,即入场时价格必须已经高于某一周期的移动平均线,以表明价格已沿相应方向形成趋势。(需要注意的是,添加过滤器也必须有一个直观的理由,并且要在不窥探数据的情况下验证这一假设。)但最终我发现,这并没有显著提升策略表现,因此我放弃了这个想法,以避免策略过于复杂。

最后,以下是2004年1月1日至2024年11月1日黄金兑美元(XAUUSD)1小时时间框架下的测试结果:

设置

参数

曲线1

结果1


获取数据

为了训练模型,我们需要每笔交易时的特征值,以及每笔交易的结果。我所采用的最有效且可靠的方法是编写一个EA,将所有对应的特征存储到一个二维数组中,而对于交易结果数据,我们只需从回测中导出交易报告即可。

首先,要获取交易结果数据,我们可以直接进入回测界面,右键点击选择报告,然后打开类似这样的XML文件。

Excel报告

接下来,要将一个二维数组转换为CSV格式,我们将使用这篇文章中介绍的CFileCSV类。

我们在核心策略脚本的基础上进行以下扩展步骤:

1. 引入mqh文件并创建类对象。

#include <FileCSV.mqh>

CFileCSV csvFile;

2. 声明要保存的文件名以及包含“索引”和所有其他特征名称的表头。这里的“索引”仅用于在运行测试器时更新数组索引,后续在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()函数,该函数计算所有特征值并将它们存储到全局数组中。在此情况下,我们使用时间、振荡指标(oscillators)和价格平稳性(stationary price)作为特征。每当出现交易信号时,都会调用此函数,以确保其与您的交易同步。特征的选择将在之后提及。

//+------------------------------------------------------------------+
//| 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文件。


数据清理与调整

现在我们手头已经有了这两个数据文件,但仍有许多潜在问题需要解决。

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}")

结果文件大致如下:


bin
0 1
1 0
2 1
3 0
4 0
5 1

需要注意的是,如果所有值都是0,可能是因为您的起始行设置不正确。请务必检查起始行现在是偶数还是奇数,并相应地修改Python代码。

2. 由于使用了CFileCSV类,特征数据全部变成了字符串类型,并且所有数据都挤在一列里,仅用逗号分隔。

下面的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}")

最后,我们使用这段代码将这两个文件合并在一起,以便将来能够轻松地将它们作为一个统一的数据结构来访问和使用。

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}")

为确保两个文件中的数据已正确合并,我们可以检查刚刚生成的三个CSV文件,查看它们的最终索引是否一致。如果一致,那基本就没问题了。


训练模型

我们不会深入探讨机器学习各个方面的技术原理。不过,如果您对机器学习交易整体感兴趣,我强烈推荐您阅读马科斯·洛佩兹·德·普拉多(Marcos López de Prado)所著的《金融机器学习的进展》(Advances in Financial Machine Learning)

本部分的目标非常明确。

首先,我们使用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官网中查阅。

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中,然后根据模型返回交易成功的概率。我们添加了打印语句,以便了解该概率值的范围。对于趋势跟踪策略,由于其准确率较低但每笔交易的盈亏比较高,模型给出的成功概率通常低于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中进行了十折交叉验证,以确认模型精度的一致性。 

{'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 将特征和结果合并为一个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)
最近评论 | 前往讨论 (12)
johnboy85
johnboy85 | 7 6月 2025 在 10:01

我在玩CatBoost,发现在 2024 年(全部)数据上训练的策略在 2024 年回溯测试(在 MetaTrader 中)时能产生大于 300% 的收益,但在其他年份的表现却很差。有人有这方面的经验吗?直观感觉是过度拟合,但即使我用更低的迭代次数(如 1k)进行训练,也会得到相同的结果。

我使用 ~40 - 50 个特征对分钟数据进行训练,因此每年大约有 250,000 行数据。.cbm 文件的大小往往是迭代次数的 1000 倍(例如,1000 次迭代 = 1MB,10000 次迭代 = 10MB,以此类推)。在 Metatrader 上进行的回溯测试限制了我的回溯测试速度,大约 100,000MB 的回溯测试才会停止。我可以用 C++ 进行任意大容量的回溯测试,但 Metatrader 和 C++ 的收益却大相径庭。

Zhuo Kai Chen
Zhuo Kai Chen | 8 6月 2025 在 10:23
johnboy85 CatBoost,发现在 2024 年(全部)数据上训练的策略在 2024 年回溯测试(在 MetaTrader 中)时能产生大于 300% 的收益,但在其他年份的表现却很差。有人有这方面的经验吗?直观感觉是过度拟合,但即使我用更低的迭代次数(如 1k)进行训练,也会得到相同的结果。

我使用 ~40 - 50 个特征对分钟数据进行训练,因此每年大约有 250,000 行数据。.cbm 文件的大小往往是迭代次数的 1000 倍(例如,1000 次迭代 = 1MB,10000 次迭代 = 10MB,以此类推)。在 Metatrader 上进行的回溯测试限制了我的回溯测试速度,大约 100,000MB 的回溯测试才会停止。我可以用 C++ 进行任意大容量的回溯测试,但 Metatrader 和 C++ 的收益却大相径庭。

您好。首先,MetaTrader 反向测试仪会考虑点差和佣金,这可能是它与您在 C++ 中的结果不同的原因。其次,在我看来,机器学习本质上是一个过度拟合的过程。有很多方法可以减少过度拟合,比如集合、剔除和特征工程。但归根结底,样本内总是比样本外要好得多。使用机器学习预测金融时间序列是一个老问题。如果你想预测收益率(我假设是因为你说的是 25 万行),那么噪音是意料之中的,因为你和其他玩家的预测目标是一样的。而我在这篇文章中介绍的是一种金属标签法,由于预测目标仅限于自己的策略,因此噪音较小,但可学习的样本较少,使得复杂性约束更加严格。我建议降低对 ML 方法的期望值,并探索减少过度拟合的方法。

johnboy85
johnboy85 | 8 6月 2025 在 11:29

谢谢你这么快就回复了一个超过 6 个月的主题。这里有很多值得思考的问题。我正在适应巨大的参数空间,并试图找到减少过度拟合的方法。

再次感谢!

Zhuo Kai Chen
Zhuo Kai Chen | 8 6月 2025 在 11:40
johnboy85 #:

谢谢你这么快就回复了一个超过 6 个月的主题。这里有很多值得思考的问题。我正在适应巨大的参数空间,并试图找到减少过度拟合的方法。

再次感谢!

祝你研究顺利

Maxim Dmitrievsky
Maxim Dmitrievsky | 4 7月 2025 在 11:19
MO 的炒作和材料的质量实在令人沮丧。
交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
交易中的神经网络:双曲型潜在扩散模型(HypDiff) 交易中的神经网络:双曲型潜在扩散模型(HypDiff)
本文研究经由各向异性扩散过程在双曲型潜在空间中编码初始数据的方法。这有助于更准确地保留当前市场状况的拓扑特征,并提升其分析品质。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
从基础到中级:数组(一) 从基础到中级:数组(一)
本文是迄今为止所讨论的内容与新的研究阶段之间的过渡。要理解这篇文章,您需要阅读前面的文章。此处提供的内容仅用于教育目的。在任何情况下,除了学习和掌握所提出的概念外,都不应出于任何目的使用此应用程序。