Utilizando o modelo de Machine Learning CatBoost como Filtro para Estratégias de Seguimento de Tendência
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:
- 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.
- 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:
- 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.)
- 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.)
- 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:
- 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.
- 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.
- 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




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.

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:

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.

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.


Finalmente, conduzimos testes fora da amostra para analisar os padrões de lucratividade em relação a diferentes limites.
Resultados do threshold = 0,05:


Resultados do threshold = 0.1:


Resultados do threshold = 0.2:


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.

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
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Negociando com o Calendário Econômico MQL5 (Parte 4): Implementando Atualizações de Notícias em Tempo Real no Painel
Simulação de mercado: Position View (XII)
Algoritmo do Restaurateur de Sucesso — Successful Restaurateur Algorithm (SRA)
Do básico ao intermediário: Filas, Listas e Árvores (IV)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
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.
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.
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!
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!