English Русский 中文 Español Deutsch 日本語
preview
Utilizando o modelo de Machine Learning CatBoost como Filtro para Estratégias de Seguimento de Tendência

Utilizando o modelo de Machine Learning CatBoost como Filtro para Estratégias de Seguimento de Tendência

MetaTrader 5Integração |
171 12
Zhuo Kai Chen
Zhuo Kai Chen

Introdução

CatBoost é um poderoso modelo de machine learning baseado em árvores que se especializa em tomada de decisão com base em features estacionárias. Outros modelos baseados em árvores como XGBoost e Random Forest compartilham características semelhantes em termos de robustez, capacidade de lidar com padrões complexos e interpretabilidade. Esses modelos têm uma ampla gama de usos, desde análise de features até gestão de risco. 

Neste artigo, vamos percorrer o procedimento de utilização de um modelo CatBoost treinado como filtro para uma estratégia clássica de seguimento de tendência com cruzamento de médias móveis. Este artigo tem como objetivo fornecer insights sobre o processo de desenvolvimento da estratégia, ao mesmo tempo em que aborda os desafios que podem surgir ao longo do caminho. Apresentarei meu fluxo de trabalho de obtenção de dados do MetaTrader 5, treinamento do modelo de machine learning em Python e integração de volta ao MetaTrader 5 expert advisors. Ao final deste artigo, validaremos a estratégia por meio de testes estatísticos e discutiremos aspirações futuras a partir da abordagem atual.


Intuição

A regra geral na indústria para desenvolver estratégia de CTA (Commodity Trading Advisor) é que é melhor ter uma explicação clara e intuitiva por trás de cada ideia de estratégia. É basicamente assim que as pessoas pensam em ideias de estratégia em primeiro lugar, sem mencionar que também evita overfitting. Essa sugestão é válida mesmo ao trabalhar com modelos de machine learning. Tentaremos explicar a intuição por trás desta ideia.

Por que isso pode funcionar:

O modelo CatBoost cria árvores de decisão que recebem as entradas de recursos e produzem a probabilidade de cada resultado. Neste caso, estamos apenas treinando em resultados binários (1 é ganho, 0 é perda). O modelo ajustará regras nas árvores de decisão para que minimize a função de perda no conjunto de dados de treinamento. Se o modelo exibir certo nível de previsibilidade no resultado do teste fora da amostra, podemos considerar usá-lo para filtrar trades que têm pouca probabilidade de ganhar, o que, por sua vez, pode aumentar a lucratividade geral.

Uma expectativa realista para traders de varejo como você e eu é que os modelos que treinamos não serão como oráculos, mas sim apenas um pouco melhores que um passeio aleatório. Existem muitas maneiras de melhorar a precisão do modelo, que discutirei mais adiante, mas ainda assim é um ótimo esforço para uma pequena melhora.


Otimizando a Estratégia Base

Já sabemos pela seção acima que só podemos esperar que o modelo aumente levemente o desempenho e, portanto, é crucial que a estratégia base já tenha algum tipo de lucratividade.

A estratégia também deve ser capaz de gerar amostras abundantes porque:

  1. O modelo filtrará uma parte dos trades, queremos garantir que restem amostras suficientes para exibir significância estatística das Leis dos Grandes Números. 
  2. Precisamos de amostras suficientes para o modelo treinar de forma que minimize a função de perda nos dados in-sample de forma eficaz.

Usamos uma estratégia de seguimento de tendência historicamente comprovada que realiza trades quando duas médias móveis de períodos diferentes se cruzam, e saímos das posições quando o preço vira para o lado oposto da média móvel. Ou seja, seguindo a tendência. O código MQL5 a seguir é o expert advisor para esta estratégia.

#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();
}

Para validar sua estratégia base, aqui estão algumas coisas a considerar:

  1. Tamanho de amostra suficiente (a frequência depende do seu timeframe e restrição de sinal, mas sugiro um total de 1000-10000 amostras. Cada trade é uma amostra.)
  2. Já exibe algum tipo de lucratividade, mas não demais (Fator de lucro de 1–1,15 eu diria que é bom o suficiente. Como o testador do MetaTrader 5 já leva em conta spreads, ter um fator de lucro de 1 significa que já há uma vantagem estatística. Se o fator de lucro ultrapassar 1,15, a estratégia provavelmente já é boa o bastante por si só, e você provavelmente não precisa de mais filtros para aumentar a complexidade.)
  3. A estratégia base não tem muitos parâmetros. (A estratégia base é melhor ser simples, já que usar um modelo de machine learning como filtro já aumenta bastante a complexidade da sua estratégia. Quanto menos filtros, menor a chance de overfitting.)

Estas foram as coisas que fiz para otimizar a estratégia:

  1. Encontrar um bom timeframe. Depois de rodar o código em diferentes timeframes, descobri que essa estratégia funciona melhor em timeframes mais altos, mas para gerar amostras suficientes, acabei ficando no timeframe de 1h.
  2. Otimizando parâmetros. Otimizei o período da média móvel lenta e da média móvel rápida com passo 5 e obtive as configurações no código acima.  
  3. Tentei adicionar uma regra em que a entrada já deveria estar acima de uma média móvel de algum período, indicando que já estava em tendência na direção correspondente. (Importante notar que adicionar filtros também precisa ter uma explicação intuitiva, e validar essa hipótese para testar sem viés de dados.) Mas acabei descobrindo que isso não melhorou muito o desempenho, então descartei essa ideia para evitar complicações excessivas.

Finalmente, este é o resultado do teste em XAUUSD timeframe 1h, 01/01/2004 – 01/11/2024

Configurações

Parâmetros

curve1

result1


Buscando os Dados

Para treinar o modelo, precisamos dos valores de features em cada trade e precisamos saber o resultado de cada trade. Minha forma mais eficiente e confiável é escrever um expert advisor que armazene todas as features correspondentes em um array 2D, e para os dados de resultado simplesmente exportamos o relatório de trading do backtest.

Primeiramente, para obter os dados de resultado, podemos simplesmente ir ao backtest, clicar com o botão direito, selecionar relatório e abrir o XML assim.

relatório excel

Em seguida, para transformar um array duplo em CSV, usaremos a classe CFileCSV explicada neste artigo.

Construímos em cima do nosso script da estratégia base com os seguintes passos:

1. Incluir o arquivo mqh e criar o objeto da classe.

#include <FileCSV.mqh>

CFileCSV csvFile;

2. Declare o nome do arquivo a ser salvo e os cabeçalhos que contêm "index" e todos os outros nomes de features. O "index" aqui é usado apenas para atualizar o índice do array enquanto o tester está rodando e será descartado mais tarde no 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. Escrevemos uma função getData() que calcula todos os valores das features e os armazena no array global. Neste caso, usamos tempo, osciladores e preço estacionário como features. Essa função será chamada toda vez que houver um sinal de trade para que esteja alinhada com seus trades. A seleção de features será mencionada mais adiante.

//+------------------------------------------------------------------+
//| 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;
}

Observe que adicionamos uma verificação aqui.

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

Isso porque, quando ocorre o sinal de trade, ele pode não resultar em uma operação, já que o EA pode estar rodando durante o fechamento do mercado, mas o tester não abrirá nenhum trade.

4. Salvamos o arquivo quando OnDeInit() é chamado, que é quando o teste termina.

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

  }

Execute este consultor especialosta no testador estratégico, depois disso você deverá ver o arquivo csv formado no diretório /Tester/Agent-sth000.


Limpando e Ajustando os Dados

Agora temos os dois arquivos de dados em mãos, mas ainda restam muitos problemas subjacentes a serem resolvidos.

1. O relatório do backtest é confuso e está em formato .xlsx. Só queremos saber se ganhamos ou não em cada trade.

Primeiro, extraímos as linhas onde são exibidos apenas os resultados das operações. Pode ser necessário rolar o arquivo XLSX até ver algo como isto:

encontrar linha

Lembre-se do número da linha e aplique-o ao seguinte código 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}.")

Em seguida, aplicamos este conteúdo extraído ao código a seguir para obter o binário processado. Onde trades vencedores seriam 1 e trades perdedores seriam 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}")

O arquivo resultante deve se parecer com isto


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

Observe que, se todos os valores forem 0, isso pode ser porque suas linhas iniciais estão incorretas; verifique se a linha inicial agora é par ou ímpar e altere-a de acordo no código Python.

2. Os dados de features estão todos como string devido à classe CFileCSV, e estão presos em uma única coluna, separados apenas por vírgulas.

O código Python a seguir resolve o problema.

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

Por fim, usamos este código para mesclar os dois arquivos de forma que possamos acessá-los facilmente como um único data frame no futuro.

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

Para confirmar que os dois dados foram mesclados corretamente, podemos verificar os três arquivos CSV que acabamos de produzir e ver se o índice final é o mesmo. Se sim, muito provavelmente está tudo certo.


Treinando o Modelo

Não entraremos muito em detalhes técnicos sobre cada aspecto do machine learning. No entanto, recomendo fortemente que você consulte Advances in Financial Machine Learning, de Marcos López de Prado, se estiver interessado em trading com ML como um todo.

Nosso objetivo para esta seção é bastante claro.

Primeiro, usamos a biblioteca pandas para ler os dados mesclados e separar a coluna bin como y e o restante como 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

Em seguida, dividimos os dados em 80% para treinamento e 20% para teste.

Depois treinamos. Os detalhes de cada parâmetro no classificador estão documentados no site do 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)

Salvamos o arquivo .cbm.

catboost_clf.save_model('catboost_test.cbm')

Infelizmente, ainda não terminamos. O MetaTrader 5 só suporta modelo em formato ONNX, então usamos o seguinte código deste artigo para transformá-lo em formato 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())


Testes Estatísticos

Após obter o arquivo .onnx, arrastamos ele para a pasta MQL5/Files. Agora construímos com base no consultor especialista que usamos para coletar dados anteriormente. Novamente, este artigo já explica em detalhes o procedimento de inicialização do modelo .onnx em consultores especialistas em detalhes,eu apenas enfatizaria como mudamos os critérios de entrada. 

     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();
      }

Aqui chamamos getData() para armazenar a informação do vetor na variável xx, depois retornamos a probabilidade de sucesso de acordo com o modelo. Adicionamos um comando print para que possamos ter uma noção da faixa em que isso ficará. Para estratégia de seguimento de tendência, devido à sua baixa taxa de acerto e alto índice de recompensa/risco por trade, normalmente vemos o modelo dar probabilidade menor que 0,5.

Adicionamos um limite para filtrar trades que exibem baixa probabilidade de sucesso, e finalizamos a parte de codificação. Agora vamos testar.

Lembra que dividimos em proporção 8-2? Agora vamos fazer um teste fora da amostra com os dados não treinados, que correspondem aproximadamente a 01/01/2021–01/11/2024.

Primeiro, rodamos o teste na amostra com limite de probabilidade 0,05 para confirmar se treinamos com os dados corretos. O resultado deve ser quase perfeito como este.

curva dentro da amostra

Em seguida, rodamos um teste fora da amostra sem limite como linha de base. Esperamos que, se aumentarmos o limite, possamos superar esse resultado de linha de base por uma boa margem.

curva de base

resultado da linha de base

Finalmente, conduzimos testes fora da amostra para analisar os padrões de lucratividade em relação a diferentes limites.

Resultados do threshold = 0,05:

0.05 curve

0.05 result

Resultados do threshold = 0.1:

0.1 curve

0.1 result

Resultados do threshold = 0.2:

0.2 curve

0.2 result

Para um threshold de 0,05, o modelo filtrou aproximadamente metade dos trades originais, mas isso levou a uma queda na lucratividade. Isso pode sugerir que o preditor está overfitted, tornando-se muito ajustado aos padrões treinados e falhando em capturar padrões semelhantes compartilhados entre os conjuntos de treinamento e teste. Em machine learning aplicado a finanças, isso é um problema comum. No entanto, quando o limite é aumentado para 0,1, o fator de lucro melhora gradualmente, superando a nossa linha de base.

Com um threshold de 0,2, o modelo filtra cerca de 70% dos trades originais, mas a qualidade geral dos trades restantes é significativamente mais lucrativa do que a dos originais. A análise estatística mostra que, dentro desta faixa de threshold, a lucratividade geral é positivamente correlacionada com o valor do limite. Isso sugere que, à medida que a confiança do modelo em um trade aumenta, também aumenta seu desempenho geral, o que é um resultado favorável.

Eu rodei uma validação cruzada com dez divisões no Python para confirmar que a precisão do modelo é consistente. 

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

A diferença entre cada pontuação de validação cruzada é leve, indicando que a precisão do modelo permanece consistente em diferentes períodos de treinamento e teste.

Além disso, com uma média de log-loss em torno de -1, o desempenho do modelo pode ser considerado moderadamente eficaz.

Para melhorar ainda mais a precisão do modelo, podemos recorrer às seguintes ideias:

1. Engenharia de Recursos

Plotamos a importância dos recuros desta forma e removemos as que têm pouca relevância.

Para selecionar os recursos, qualquer coisa relacionada ao mercado é plausível, mas certifique-se de tornar os dados estacionários porque modelos baseados em árvores usam regras de valores fixos para processar entradas.

Importância do Recurso

2. Ajuste de hiperparâmetros

Lembra dos parâmetros na função classificador de que falei antes? Podemos escrever uma função para percorrer uma grade de valores e testar qual parâmetro de treinamento produziria as melhores pontuações de validação cruzada. 

3. Seleção de Modelo

Podemos tentar diferentes modelos de machine learning ou diferentes tipos de valores a prever. As pessoas descobriram que, embora modelos de machine learning sejam ruins em prever preços, eles são bastante competentes em prever volatilidade. Além disso, o modelo de Markov oculto é amplamente usado para prever tendências ocultas. Ambos podem ser filtros potentes para estratégias de seguimento de tendência.

Incentivo os leitores a testarem esses métodos com meu código anexado e me avisarem se encontrarem algum sucesso em melhorar o desempenho.


Conclusão

Neste artigo, percorremos todo o fluxo de trabalho de desenvolvimento de um filtro de machine learning CatBoost para uma estratégia de seguimento de tendência. No caminho, destacamos diferentes aspectos a serem observados ao pesquisar estratégias de machine learning. No caminho, destacamos diferentes aspectos a serem observados ao pesquisar estratégias de machine learning.


Tabela de Arquivos Anexos

Nome do Arquivo Uso
 ML-Momentum Data.mq5  O EA para coletar dados de features
 ML-Momentum.mq5  EA de execução final
 CB2.ipynb O fluxo de trabalho para treinar e testar o modelo CatBoost 
handleMql5DealReport.py O fluxo de trabalho para treinar e testar o modelo CatBoost
getBinFromMql5.py Obter resultado binário a partir do conteúdo extraído
clean_mql5_csv.py Limpar o CSV de recursos extraído do MT5
merge_data2.py Mesclar recursos e resultado em um único CSV
OnnxConvert.ipynb Converter modelo .cbm para formato .onnx
Seguimento de Tendência Clássica.mq5
O consultor especialista em estratégia backbone

Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/16487

Arquivos anexados |
ML-TF-Project.zip (186.72 KB)
Últimos Comentários | Ir para discussão (12)
johnboy85
johnboy85 | 7 jun. 2025 em 10:01

Olá. Estou brincando com o CatBoost e cheguei a um ponto em que uma estratégia treinada em (todos os) dados de 2024 produzirá retornos superiores a 300% quando for testada (no MetaTrader) em 2024, mas terá um desempenho ruim em outros anos. Alguém tem experiência com isso? Intuitivamente, parece um ajuste excessivo, mas mesmo que eu treine com iterações muito menores (como 1k), obtenho o mesmo resultado.

Estou treinando com cerca de 40 a 50 recursos, em dados de minuto, portanto, algo como 250.000 linhas por ano. O tamanho do arquivo .cbm tende a ser 1000 vezes maior que o número de iterações (por exemplo, 1000 iterações = 1 MB, 10.000 iterações = 10 MB e assim por diante). O backtesting no Metatrader me limita a cerca de 100.000 MB antes que o backtester pare de funcionar. Posso fazer backtest com o C++ em um tamanho arbitrariamente alto, mas meus retornos no Metatrader e no C++ são extremamente diferentes.

Zhuo Kai Chen
Zhuo Kai Chen | 8 jun. 2025 em 10:23
johnboy85 CatBoost e cheguei a um ponto em que uma estratégia treinada em (todos os) dados de 2024 produzirá retornos superiores a 300% quando for testada (no MetaTrader) em 2024, mas terá um desempenho ruim em outros anos. Alguém tem experiência com isso? Intuitivamente, parece um ajuste excessivo, mas mesmo que eu treine com iterações muito menores (como 1k), obtenho o mesmo resultado.

Estou treinando com cerca de 40 a 50 recursos, em dados de minuto, portanto, algo como 250.000 linhas por ano. O tamanho do arquivo .cbm tende a ser 1000 vezes maior que o número de iterações (por exemplo, 1000 iterações = 1 MB, 10.000 iterações = 10 MB e assim por diante). O backtesting no Metatrader me limita a cerca de 100.000 MB antes que o backtester pare de funcionar. Posso fazer backtest com o C++ em um tamanho arbitrariamente alto, mas meus retornos no Metatrader e no C++ são extremamente diferentes.

Olá. Em primeiro lugar, o backtester do Metatrader leva em conta os spreads e a comissão, o que pode explicar por que os resultados são diferentes dos seus em C++. Em segundo lugar, na minha opinião, o aprendizado de máquina é essencialmente um processo de ajuste excessivo. Há muitas maneiras de reduzir o excesso de ajuste, como conjunto, abandono e engenharia de recursos. Mas, no final das contas, dentro da amostra é sempre muito melhor do que fora da amostra. O uso do aprendizado de máquina na previsão de séries temporais financeiras é um problema antigo. Se você está tentando prever o retorno (presumo que esteja dizendo 250 mil linhas), é de se esperar que haja ruído, pois você e outros jogadores têm o mesmo objetivo de previsão. Por outro lado, o que apresentei neste artigo é um método de rotulagem de metal em que há menos ruído, pois seu objetivo de previsão está restrito à sua própria estratégia, mas haveria menos amostras para aprender, tornando a restrição de complexidade ainda mais rigorosa. Eu diria para diminuir sua expectativa com o método ML e explorar maneiras de reduzir o ajuste excessivo.

johnboy85
johnboy85 | 8 jun. 2025 em 11:29

Obrigado por responder tão rapidamente em um tópico que tem mais de 6 meses. Há muito o que pensar aqui. Estou me acostumando com o enorme espaço de parâmetros e tentando encontrar maneiras de reduzir o ajuste excessivo.

Mais uma vez, obrigado!

Zhuo Kai Chen
Zhuo Kai Chen | 8 jun. 2025 em 11:40
johnboy85 #:

Obrigado por responder tão rapidamente em um tópico que tem mais de 6 meses. Há muito o que pensar aqui. Estou me acostumando com o enorme espaço de parâmetros e tentando encontrar maneiras de reduzir o excesso de ajuste.

Mais uma vez, obrigado!

Boa sorte com sua pesquisa!

[Excluído] | 4 jul. 2025 em 11:19
O hype no MO e a qualidade do material são simplesmente deprimentes.
Negociando com o Calendário Econômico MQL5 (Parte 4): Implementando Atualizações de Notícias em Tempo Real no Painel Negociando com o Calendário Econômico MQL5 (Parte 4): Implementando Atualizações de Notícias em Tempo Real no Painel
Este artigo aprimora nosso painel do Calendário Econômico implementando atualizações de notícias em tempo real para manter as informações de mercado atuais e acionáveis. Integramos técnicas de busca de dados ao vivo no MQL5 para atualizar os eventos no painel continuamente, melhorando a capacidade de resposta da interface. Essa atualização garante que possamos acessar as últimas notícias econômicas diretamente do painel, otimizando as decisões de negociação com base nos dados mais recentes.
Simulação de mercado: Position View (XII) Simulação de mercado: Position View (XII)
No artigo, você aprenderá como criar uma indicação visual na sua plataforma de trading para saber se você está em uma posição comprada ou vendida no gráfico, sem precisar acessar o terminal. Além disso, o texto aborda a implementação de uma funcionalidade que melhora a visualização ao mover linhas de take profit e stop loss, ocultando a linha de preço do mouse durante a movimentação para evitar confusões. A leitura oferece insights práticos para customizar sistemas de simulação de mercado.
Algoritmo do Restaurateur de Sucesso — Successful Restaurateur Algorithm (SRA) Algoritmo do Restaurateur de Sucesso — Successful Restaurateur Algorithm (SRA)
O Algoritmo do Restaurateur de Sucesso (SRA) é um método inovador de otimização inspirado nos princípios de gestão de um restaurante. Ao contrário das abordagens tradicionais, o SRA não descarta as soluções mais fracas, mas as melhora, combinando-as com elementos das soluções de maior sucesso. O algoritmo apresenta resultados competitivos e traz uma nova perspectiva sobre como equilibrar a diversificação e a intensificação em problemas de otimização.
Do básico ao intermediário: Filas, Listas e Árvores (IV) Do básico ao intermediário: Filas, Listas e Árvores (IV)
Neste artigo iremos finalizar a parte referente a implementação e explicação sobre o que seria uma lista encadeada. Porém a implementação mostrada aqui, não irá mostrar um certo detalhe que podemos fazer dentro de uma lista encadeada. Isto será visto futuramente em um outro artigo.