English Русский 中文 Español Deutsch 日本語
preview
Ciência de dados e aprendizado de máquina (Parte 31): Aplicação de modelos CatBoost no trading

Ciência de dados e aprendizado de máquina (Parte 31): Aplicação de modelos CatBoost no trading

MetaTrader 5Negociação |
34 5
Omega J Msigwa
Omega J Msigwa

"CatBoost é uma biblioteca de gradient boosting que se destaca pelo processamento eficiente e escalável de características categóricas, proporcionando um aumento significativo de performance para várias tarefas práticas."

— Anthony Goldbloom.


O que é CatBoost?

CatBoost é uma biblioteca de código aberto que implementa algoritmos de gradient boosting sobre árvores de decisão. Foi desenvolvida especialmente para resolver tarefas relacionadas ao processamento de características categóricas e dados em problemas de aprendizado de máquina.

Desenvolvida pela empresa Yandex, a biblioteca foi disponibilizada como código aberto em 2017 (saiba mais).

Embora o CatBoost tenha surgido relativamente recentemente em comparação com outros métodos de aprendizado de máquina, como regressão linear ou SVM, a biblioteca rapidamente ganhou popularidade e se tornou um dos modelos mais usados em competições do Kaggle.

O CatBoost atraiu muita atenção por sua capacidade de processar automaticamente características categóricas em conjuntos de dados, algo bastante desafiador para muitos algoritmos de aprendizado de máquina.

  • Os modelos CatBoost geralmente oferecem melhor desempenho em comparação com outros, exigindo um esforço mínimo. Mesmo com parâmetros e configurações padrão, esses modelos demonstram excelente precisão.
  • Diferentemente das redes neurais, que exigem conhecimento aprofundado da área, o CatBoost é mais simples de implementar.

Este material assume que você já tem conhecimento básico sobre aprendizado de máquina, árvores de decisão, XGBoost, LightGBM e ONNX.


Como o CatBoost funciona?

O CatBoost é baseado no algoritmo de gradient boosting, o que o torna semelhante aos métodos Light Gradient Machine (LightGBM) e Extreme Gradient Boosting (XGBoost). Seu princípio de funcionamento está na construção sequencial de vários modelos baseados em árvores de decisão, onde cada novo modelo tenta corrigir os erros do anterior.

A previsão final é uma soma ponderada das previsões de todos os modelos envolvidos no processo.

O objetivo das árvores de decisão com gradient boosting é minimizar a função de perda. Para isso, é adicionada uma nova árvore ao modelo, que compensa os erros da anterior.


Processamento de características categóricas

Como mencionado no início do artigo, o CatBoost lida com características categóricas sem a necessidade de codificação manual, como one-hot ou label encoding, geralmente exigidas por outros modelos de aprendizado de máquina. Isso é possível graças ao mecanismo incorporado de codificação baseada no alvo (target-based encoding), que codifica as características categóricas utilizando informações da variável-alvo.

A essência disso é que, para cada valor único da característica categórica, é calculada a distribuição condicional da variável alvo.

Outra característica importante do CatBoost é o uso do chamado boosting ordenado (ordered boosting) para o cálculo das estatísticas das características categóricas. Nesse caso, a codificação de cada objeto de dados se baseia apenas nas informações obtidas dos objetos anteriores.

Essa abordagem ajuda a evitar perda de dados e sobreajuste.


Uso de estruturas simétricas de árvores de decisão

Ao contrário do LightGBM e do XGBoost, que utilizam árvores assimétricas, o CatBoost emprega árvores de decisão simétricas na construção dos modelos. Em uma árvore simétrica, ambos os ramos em cada divisão são formados de forma simétrica, usando a mesma regra de divisão. Essa abordagem oferece várias vantagens:

  • Aprendizado mais rápido devido às divisões simétricas;
  • Uso eficiente da memória graças à estrutura simplificada da árvore;
  • As árvores simétricas são mais estáveis frente a pequenas variações nos dados.


Comparação entre CatBoost, XGBoost e LightGBM

Vamos entender como o CatBoost se diferencia de outras árvores de decisão com gradient boosting. Abaixo está uma tabela comparativa que ajudará a entender melhor qual opção é mais adequada para cada situação.


Características

CatBoostLightGBMXGBoost

Processamento de características categóricas

Detecção automática e boosting ordenado para tratar variáveis categóricas.Requer codificação prévia (por exemplo, one-hot encoding, label encoding etc.)Requer codificação prévia (por exemplo, one-hot encoding, label encoding etc.)

Estrutura da árvore de decisão

Árvores de decisão simétricas, que são balanceadas e crescem de forma uniforme. Oferecem previsões mais rápidas e menor risco de sobreajuste.
Crescimento por folhas (estrutura assimétrica), foca nas folhas com maior perda. Resulta em árvores profundas e desequilibradas, que podem ter mais precisão, mas aumentam o risco de sobreajuste.


Crescimento por níveis (estrutura assimétrica), onde a árvore cresce com base na melhor divisão para cada nó. Gera árvores flexíveis, porém com previsões mais lentas e risco potencial de sobreajuste.


Precisão do modelo


Boa precisão ao trabalhar com conjuntos de dados com muitas variáveis categóricas, graças ao boosting ordenado e menor risco de sobreajuste em dados pequenos.

Oferecem boa precisão, especialmente para grandes conjuntos de dados e de alta dimensionalidade, sendo aplicados para melhorar desempenho em áreas com alta margem de erro.Garantem boa precisão na maioria dos conjuntos de dados, mas geralmente perdem para o CatBoost em conjuntos categóricos e para o LightGBM em conjuntos muito grandes, devido a uma estratégia de crescimento de árvore menos agressiva.

Velocidade e precisão no treinamento


Normalmente treinam mais devagar que o LightGBM, mas são mais eficientes em conjuntos pequenos e médios, especialmente ao lidar com características categóricas.

Geralmente o mais rápido entre os três métodos, especialmente ao trabalhar com grandes volumes de dados, graças ao crescimento por folhas, mais eficiente em dados multidimensionais.O mais lento dos três. Eficiente para trabalhar com grandes volumes de dados. 


Implantação do modelo CatBoost

Antes de começarmos a trabalhar diretamente com o CatBoost, vamos definir o cenário da tarefa. Vamos gerar uma previsão de sinais de trading (compra/venda) com base nos dados Open, High, Low, Close, além de algumas características categóricas: data atual, dia da semana (de segunda a domingo), dia do ano (de 1 a 365) e mês (de janeiro a dezembro).

Os valores OHLC (abertura, máxima, mínima, fechamento) são características contínuas, enquanto os demais são categóricos. O script de coleta desses dados está anexado ao artigo.

Vamos começar importando o modelo CatBoost.

Instalação

Comando de terminal

pip install catboost

Importação.

Código Python

import numpy as np
import pandas as pd
import catboost
from catboost import CatBoostClassifier
from sklearn.pipeline import Pipeline 
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
import seaborn as sns
import matplotlib.pyplot as plt

sns.set_style("darkgrid")

Para entender os dados, vamos visualizá-los.

df = pd.read_csv("/kaggle/input/ohlc-eurusd/EURUSD.OHLC.PERIOD_D1.csv")

df.head()

Saída

OpenHighLowCloseDayDayofWeekDayofYearMonth
01.093811.095481.090031.0937321.03.0264.09.0
11.096781.098101.093611.0939922.04.0265.09.0
21.097011.099731.096061.0980523.05.0266.09.0
31.096391.098691.095421.0974226.01.0269.09.0
41.103021.103961.095131.0975727.02.0270.09.0

Durante a coleta dos dados no script em MQL5, obtive os valores de DayofWeek (de segunda a domingo) e Month (de janeiro a dezembro) como inteiros, e não como strings. Isso ocorreu porque os dados eram armazenados em uma matriz, e não é possível adicionar strings nela. Embora essas características sejam naturalmente categóricas. No formato atual, elas não são reconhecidas como categóricas, portanto vamos convertê-las de volta para dados categóricos e observar como o CatBoost lida com elas.

Preparar a variável alvo.

Código Python

new_df = df.copy()  # Create a copy of the original DataFrame

# we Shift the 'Close' and 'open' columns by one row to ge the future close and open price values, then we add these new columns to the dataset

new_df["target_close"] = df["Close"].shift(-1)  
new_df["target_open"] = df["Open"].shift(-1)  

new_df = new_df.dropna()  # Drop the rows with NaN values resulting from the shift operation

open_values = new_df["target_open"]
close_values = new_df["target_close"]  

target = []
for i in range(len(open_values)):
    if close_values[i] > open_values[i]: 
        target.append(1) # buy signal 
    else:
        target.append(0) # sell signal

new_df["signal"] = target # we create the signal column and add the target variable we just prepared


print(new_df.shape)
new_df.head()

Saída

OpenHighLowCloseDayDayofWeekDayofYearMonthtarget_closetarget_opensignal
01.093811.095481.090031.0937321.03.0264.09.01.093991.096780
11.096781.098101.093611.0939922.04.0265.09.01.098051.097011
21.097011.099731.096061.0980523.05.0266.09.01.097421.096391
31.096391.098691.095421.0974226.01.0269.09.01.097571.103020
41.103021.103961.095131.0975727.02.0270.09.01.102971.104310

Então, entendemos os sinais que podemos prever. Agora vamos dividir os dados em conjuntos de treino e teste.

X = new_df.drop(columns = ["target_close", "target_open", "signal"]) # we drop future values
y = new_df["signal"] # trading signals are the target variables we wanna predict 

X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, random_state=42)

Definimos a lista de características categóricas do nosso conjunto de dados.

categorical_features = ["Day","DayofWeek", "DayofYear", "Month"]

Depois podemos usar essa lista para converter as características categóricas para o formato string, como geralmente são representadas as variáveis categóricas.

X_train[categorical_features] = X_train[categorical_features].astype(str)
X_test[categorical_features] = X_test[categorical_features].astype(str)

X_train.info() # we print the data types now

Saída

<class 'pandas.core.frame.DataFrame'>
Index: 6999 entries, 9068 to 7270
Data columns (total 8 columns):
 #   Column     Non-Null Count  Dtype  
---  ------     --------------  -----  
 0   Open       6999 non-null   float64
 1   High       6999 non-null   float64
 2   Low        6999 non-null   float64
 3   Close      6999 non-null   float64
 4   Day        6999 non-null   object 
 5   DayofWeek  6999 non-null   object 
 6   DayofYear  6999 non-null   object 
 7   Month      6999 non-null   object 
dtypes: float64(4), object(4)
memory usage: 492.1+ KB

Obtivemos variáveis categóricas com o tipo de dados object, que para nós isso é string. Se tentarmos aplicar esses dados em outro modelo de aprendizado de máquina (que não seja o CatBoost), vamos obter erros, pois o tipo object não é aceito em conjuntos de treinamento típicos para aprendizado de máquina.


Treinamento do modelo CatBoost

Antes de chamar o método fit para treinar o modelo CatBoost, vamos entender os parâmetros do modelo.


Parâmetro

Descrição

Iterations


Número de iterações para construir as árvores de decisão.

Um número maior geralmente melhora o desempenho, mas também aumenta o risco de sobreajuste.


learning_rate
  

Controla a contribuição de cada árvore na previsão final.

Valores menores exigem mais iterações para convergir, mas frequentemente resultam em modelos de maior qualidade.


depth


Profundidade máxima das árvores.

Árvores mais profundas conseguem capturar padrões mais complexos nos dados, mas tendem ao sobreajuste.


cat_features


Lista de índices categóricos.
Embora o CatBoost consiga identificar automaticamente os dados categóricos, é boa prática informar explicitamente quais são esses atributos.
Isso ajuda o modelo a interpretar corretamente os dados categóricos do ponto de vista humano, já que os métodos automáticos podem falhar ocasionalmente.

l2_leaf_reg


Coeficiente de regularização L2.

Ajuda a combater o sobreajuste aplicando uma penalidade sobre os pesos elevados dos nós folha.


border_count
 


Número de divisões para cada característica categórica. Quanto maior esse valor, melhor tende a ser a qualidade do modelo, mas o custo computacional também aumenta.



eval_metric


Métrica de avaliação usada durante o treinamento.

Permite monitorar o desempenho do modelo de forma eficiente.


early_stopping_rounds


Se houver dados de validação, o treinamento será interrompido se não houver melhoria na precisão após o número definido de iterações.

Esse parâmetro ajuda a reduzir o risco de sobreajuste e pode encurtar significativamente o tempo de treinamento.

Vamos definir um dicionário com esses parâmetros.

params = dict(
    iterations=100, 
    learning_rate=0.01,
    depth=10,
    l2_leaf_reg=5,  
    bagging_temperature=1,
    border_count=64,  # Number of splits for categorical features
    eval_metric='Logloss',
    random_seed=42,  # Seed for reproducibility
    verbose=1,  # Verbosity level
    # early_stopping_rounds=10  # Early stopping for validation
)

Finalmente, podemos criar o modelo CatBoost dentro de um pipeline do Sklearn, e, em seguida, chamar o método fit para realizar o treinamento. Nos parâmetros do método, passaremos os dados de validação e a lista de características categóricas.

pipe = Pipeline([
    ("catboost", CatBoostClassifier(**params))
])

# Fit the pipeline to the training data
pipe.fit(X_train, y_train, catboost__eval_set=(X_test, y_test), catboost__cat_features=categorical_features)

Resultados

90:     learn: 0.6880592        test: 0.6936112 best: 0.6931239 (3)     total: 523ms    remaining: 51.7ms
91:     learn: 0.6880397        test: 0.6936100 best: 0.6931239 (3)     total: 529ms    remaining: 46ms
92:     learn: 0.6880350        test: 0.6936051 best: 0.6931239 (3)     total: 532ms    remaining: 40ms
93:     learn: 0.6880280        test: 0.6936103 best: 0.6931239 (3)     total: 535ms    remaining: 34.1ms
94:     learn: 0.6879448        test: 0.6936110 best: 0.6931239 (3)     total: 541ms    remaining: 28.5ms
95:     learn: 0.6878328        test: 0.6936387 best: 0.6931239 (3)     total: 547ms    remaining: 22.8ms
96:     learn: 0.6877888        test: 0.6936473 best: 0.6931239 (3)     total: 553ms    remaining: 17.1ms
97:     learn: 0.6877408        test: 0.6936508 best: 0.6931239 (3)     total: 559ms    remaining: 11.4ms
98:     learn: 0.6876611        test: 0.6936708 best: 0.6931239 (3)     total: 565ms    remaining: 5.71ms
99:     learn: 0.6876230        test: 0.6936898 best: 0.6931239 (3)     total: 571ms    remaining: 0us

bestTest = 0.6931239281
bestIteration = 3

Shrink model to first 4 iterations.


Avaliação do modelo

Para avaliar a performance do modelo, utilizaremos as métricas do Sklearn.

# Make predicitons on training and testing sets
y_train_pred = pipe.predict(X_train)
y_test_pred = pipe.predict(X_test)

# Training set evaluation
print("Training Set Classification Report:")
print(classification_report(y_train, y_train_pred))

# Testing set evaluation
print("\nTesting Set Classification Report:")
print(classification_report(y_test, y_test_pred))

Resultados

Training Set Classification Report:
              precision    recall  f1-score   support

           0       0.55      0.44      0.49      3483
           1       0.54      0.64      0.58      3516

    accuracy                           0.54      6999
   macro avg       0.54      0.54      0.54      6999
weighted avg       0.54      0.54      0.54      6999


Testing Set Classification Report:
              precision    recall  f1-score   support

           0       0.53      0.41      0.46      1547
           1       0.49      0.61      0.54      1453

    accuracy                           0.51      3000
   macro avg       0.51      0.51      0.50      3000
weighted avg       0.51      0.51      0.50      3000

Como resultado, obtivemos um modelo com desempenho mediano. Notei que, ao remover a lista de características categóricas, a precisão do modelo no conjunto de treino aumentou para 60%, mas no conjunto de teste permaneceu no mesmo nível.

pipe.fit(X_train, y_train, catboost__eval_set=(X_test, y_test))

Saída

91:     learn: 0.6844878        test: 0.6933503 best: 0.6930500 (30)    total: 395ms    remaining: 34.3ms
92:     learn: 0.6844035        test: 0.6933539 best: 0.6930500 (30)    total: 399ms    remaining: 30ms
93:     learn: 0.6843241        test: 0.6933791 best: 0.6930500 (30)    total: 404ms    remaining: 25.8ms
94:     learn: 0.6842277        test: 0.6933732 best: 0.6930500 (30)    total: 408ms    remaining: 21.5ms
95:     learn: 0.6841427        test: 0.6933758 best: 0.6930500 (30)    total: 412ms    remaining: 17.2ms
96:     learn: 0.6840422        test: 0.6933796 best: 0.6930500 (30)    total: 416ms    remaining: 12.9ms
97:     learn: 0.6839896        test: 0.6933825 best: 0.6930500 (30)    total: 420ms    remaining: 8.58ms
98:     learn: 0.6839040        test: 0.6934062 best: 0.6930500 (30)    total: 425ms    remaining: 4.29ms
99:     learn: 0.6838397        test: 0.6934259 best: 0.6930500 (30)    total: 429ms    remaining: 0us

bestTest = 0.6930499562
bestIteration = 30

Shrink model to first 31 iterations.
Training Set Classification Report:
              precision    recall  f1-score   support

           0       0.61      0.53      0.57      3483
           1       0.59      0.67      0.63      3516

    accuracy                           0.60      6999
   macro avg       0.60      0.60      0.60      6999
weighted avg       0.60      0.60      0.60      6999

Para entender melhor o funcionamento do modelo, vamos construir o gráfico de importância das características.

# Extract the trained CatBoostClassifier from the pipeline
catboost_model = pipe.named_steps['catboost']

# Get feature importances
feature_importances = catboost_model.get_feature_importance()

feature_im_df = pd.DataFrame({
    "feature": X.columns,
    "importance": feature_importances
})

feature_im_df = feature_im_df.sort_values(by="importance", ascending=False)

plt.figure(figsize=(10, 6))
sns.barplot(data = feature_im_df, x='importance', y='feature', palette="viridis")

plt.title("CatBoost feature importance")
plt.xlabel("Importance")
plt.ylabel("feature")
plt.show()

Saída

O gráfico de importância dos atributos acima ilustra claramente como o modelo tomou suas decisões. Parece que o CatBoost considerou as variáveis categóricas mais relevantes para a previsão final do que as variáveis contínuas.


Salvando o modelo CatBoost em ONNX

Para utilizar o modelo na plataforma MetaTrader 5, é necessário salvá-lo no formato ONNX. No entanto, salvar um modelo CatBoost pode ser um pouco mais trabalhoso do que com Sklearn ou Keras, que oferecem métodos de conversão mais diretos.

Mas tudo é perfeitamente possível se você seguir as instruções da documentação oficial. Não foi necessário mergulhar nos detalhes do código.

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

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

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

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

    if no_zipmap:
        return probability_tensor

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


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


update_registered_converter(
    CatBoostClassifier,
    "CatBoostCatBoostClassifier",
    calculate_linear_classifier_output_shapes,
    skl2onnx_convert_catboost,
    parser=skl2onnx_parser_castboost_classifier,
    options={"nocl": [True, False], "zipmap": [True, False, "columns"]},
)

Abaixo está o código final para conversão do modelo e salvamento no arquivo .onnx.

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

# And save.
with open("CatBoost.EURUSD.OHLC.D1.onnx", "wb") as f:
    f.write(model_onnx.SerializeToString())

Ao visualizar no Netron, a estrutura do modelo aparece da mesma forma que nos modelos do XGBoost e LightGBM.

Modelo catboost no netron

Isso faz sentido, já que o CatBoost também é baseado em árvores de decisão com gradient boosting. Em essência, eles compartilham estruturas semelhantes.

Quando tentei converter um modelo CatBoost, integrado ao pipeline, para o formato ONNX com características categóricas, encontrei um erro:

CatBoostError: catboost/libs/model/model_export/model_exporter.cpp:96: ONNX-ML format export does yet not support categorical features

As características categóricas precisam ser representadas no formato float64 (double), como nos dados originalmente coletados no MetaTrader 5. Isso resolveu o problema. Assim, podemos usar o modelo no MQL5 sem preocupação de que valores double ou float sejam confundidos com inteiros.

categorical_features = ["Day","DayofWeek", "DayofYear", "Month"]

# Remove these two lines of code operations

# X_train[categorical_features] = X_train[categorical_features].astype(str)
# X_test[categorical_features] = X_test[categorical_features].astype(str)

X_train.info() # we print the data types now

Apesar dessa alteração, a precisão do modelo CatBoost permaneceu no mesmo nível, pois ele é capaz de trabalhar com conjuntos de dados de diferentes naturezas.


Criação de um robô de trading com CatBoost

Vamos começar incorporando o modelo ONNX no nosso EA na forma de recurso.

Código MQL5

#resource "\\Files\\CatBoost.EURUSD.OHLC.D1.onnx" as uchar catboost_onnx[]

We import the library for loading the CatBoost model.

#include <MALE5\Gradient Boosted Decision Trees(GBDTs)\CatBoost\CatBoost.mqh>
CCatBoost cat_boost;

É necessário coletar os dados da mesma maneira como fizemos durante o treinamento. Isso será feito dentro da função OnTick.

void OnTick()
 {
...
...
...     

   if (CopyRates(Symbol(), timeframe, 1, 1, rates) < 0) //Copy information from the previous bar
    {
      printf("Failed to obtain OHLC price values error = ",GetLastError());
      return;
    }
   
   MqlDateTime time_struct;
   string time = (string)datetime(rates[0].time); //converting the date from seconds to datetime then to string
   TimeToStruct((datetime)StringToTime(time), time_struct); //converting the time in string format to date then assigning it to a structure
      
   vector x = {rates[0].open,
               rates[0].high, 
               rates[0].low, 
               rates[0].close, 
               time_struct.day, 
               time_struct.day_of_week, 
               time_struct.day_of_year, 
               time_struct.mon}; //input features from the previously closed bar
...
...
...     
  }

Depois disso, podemos obter o sinal e o vetor de probabilidade. Vamos definir dois sinais: um sinal de baixa de classe 0 e um sinal de alta de classe 1.

   vector proba = cat_boost.predict_proba(x); //predict the probability between the classes
   long signal = cat_boost.predict_bin(x); //predict the trading signal class
   
   Comment("Predicted Probability = ", proba,"\nSignal = ",signal);

Resta agora implementar no EA a estratégia de trading com base nas previsões obtidas pelo modelo. 

Vamos usar a estratégia mais simples: quando o modelo prevê um sinal de alta, abrimos uma ordem de compra e, se houver uma ordem de venda aberta, fechamos essa venda. Quando houver sinal de baixa, faremos o inverso, isto é, vendemos e fechamos a compra.

void OnTick()
  {
//---
   
   if (!NewBar())
    return;
   
//--- Trade at the opening of each bar

   if (CopyRates(Symbol(), timeframe, 1, 1, rates) < 0) //Copy information from the previous bar
    {
      printf("Failed to obtain OHLC price values error = ",GetLastError());
      return;
    }
   
   MqlDateTime time_struct;
   string time = (string)datetime(rates[0].time); //converting the date from seconds to datetime then to string
   TimeToStruct((datetime)StringToTime(time), time_struct); //converting the time in string format to date then assigning it to a structure
      
   vector x = {rates[0].open,
               rates[0].high, 
               rates[0].low, 
               rates[0].close, 
               time_struct.day, 
               time_struct.day_of_week, 
               time_struct.day_of_year, 
               time_struct.mon}; //input features from the previously closed bar
               
   vector proba = cat_boost.predict_proba(x); //predict the probability between the classes
   long signal = cat_boost.predict_bin(x); //predict the trading signal class
   
   Comment("Predicted Probability = ", proba,"\nSignal = ",signal);
   
//---
      
      MqlTick ticks;
      SymbolInfoTick(Symbol(), ticks);
      
      if (signal==1) //if the signal is bullish
       {
          if (!PosExists(POSITION_TYPE_BUY)) //There are no buy positions
            m_trade.Buy(min_lot, Symbol(), ticks.ask, 0, 0); //Open a buy trade
          
          ClosePosition(POSITION_TYPE_SELL); //close the opposite trade  
       }
      else //Bearish signal
        {
          if (!PosExists(POSITION_TYPE_SELL)) //There are no Sell positions
            m_trade.Sell(min_lot, Symbol(), ticks.bid, 0, 0); //open a sell trade
            
            
          ClosePosition(POSITION_TYPE_BUY); //close the opposite trade
        }
  }

Testamos a estratégia no testador entre 01.01.2021 e 08.10.2024 no timeframe de 4 horas. O teste foi realizado apenas com preços de abertura. Abaixo está o resultado.

O EA obteve resultados respeitáveis, pois 55% das operações foram lucrativas, resultando em um lucro líquido total de 96 dólares americanos. Um bom resultado, considerando o conjunto de dados simples, o modelo básico e o volume mínimo de operações.


Considerações finais

CatBoost e outros modelos de gradient boosting com árvores de decisão são bastante adequados para uso em ambientes com recursos computacionais limitados. Também são uma ótima escolha quando se precisa de um modelo que "simplesmente funcione". Ou ainda, quando se quer evitar tarefas pouco atrativas como seleção de atributos e ajuste fino de modelos, frequentemente necessárias em outros métodos de aprendizado de máquina. 

Apesar da simplicidade e da baixa barreira de entrada, esses modelos continuam entre as ferramentas mais eficazes aplicáveis a uma variedade de problemas reais.

Com os melhores cumprimentos.


Você pode acompanhar a evolução deste modelo de aprendizado de máquina e muito mais desta série de artigos no meu repositório no GitHub.

Tabela de anexos


Nome do arquivo

Tipo de arquivoDescrição e uso

Experts\CatBoost EA.mq5

 ExpertRobô de trading para carregar o modelo CatBoost em ONNX e testar a estratégia de trading resultante no MetaTrader 5.

Include\CatBoost.mqh

 Arquivo Include


Contém o código para carregar e executar o modelo CatBoost.


Files\CatBoost.EURUSD.OHLC.D1.onnx


 Modelo ONNX


Modelo CatBoost treinado no formato ONNX.

 
Scripts\CollectData.mq5

 Script MQL5Script para coleta de dados de treinamento. 

Jupyter Notebook\CatBoost-4-trading.ipynb 

 Python/Jupyter notebook O notebook contém todo o código em Python discutido neste artigo.


Fontes e links

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

Arquivos anexados |
Attachments.zip (40.07 KB)
Últimos Comentários | Ir para discussão (5)
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 15 out. 2024 em 20:28
Seu texto é instigante.

Eu me pergunto o que aconteceria se também rastreássemos em qual sessão de negociação estamos.
Omega J Msigwa
Omega J Msigwa | 16 out. 2024 em 08:23
Sim, a sessão de negociação é uma variável valiosa a ser incluída em seus dados de treinamento
Maxim Dmitrievsky
Maxim Dmitrievsky | 27 mai. 2025 em 13:01
Todos os classificadores (inclusive o catboost) funcionam corretamente somente com atributos normalizados. Os preços como atributos não são adequados.
Aliaksandr Kazunka
Aliaksandr Kazunka | 27 mai. 2025 em 18:52

E há também o problema de exportar o modelo do classificador para o ONNX


Observação

O rótulo é inferido incorretamente para a classificação binária. Esse é um bug conhecido na implementação do onnxruntime. Ignore o valor desse parâmetro no caso de classificação binária.

zhainan
zhainan | 11 jul. 2025 em 10:05
O preço não pode ser usado como dados de treinamento. No início do ano passado, usei o preço do ouro para treinar o modelo. Quando o preço do ouro continua a atingir novos máximos, a entrada para o modelo desses novos dados de preços altos, o modelo não reconhece esses dados, não importa como alterar e exceder o preço mais alto do preço dos dados de treinamento, ele apresenta uma probabilidade constante de valor!
Adicionando um LLM personalizado a um robô investidor (Parte 5): Desenvolvimento e teste de uma estratégia de trading com LLM (II) - Configuração do LoRA Adicionando um LLM personalizado a um robô investidor (Parte 5): Desenvolvimento e teste de uma estratégia de trading com LLM (II) - Configuração do LoRA
Os modelos de linguagem (LLMs) são uma parte importante da inteligência artificial que evolui rapidamente. E para aproveitar isso devemos pensar em como integrar LLMs avançados em nossa negociação algorítmica. Muitos acham desafiador ajustar esses modelos de acordo com suas necessidades, implantá-los localmente e, logo, aplicá-los à negociação algorítmica. Esta série de artigos explorará uma abordagem passo a passo para alcançar esse objetivo.
Corpo em Connexus (Parte 4): Adicionando suporte ao corpo de requisições HTTP Corpo em Connexus (Parte 4): Adicionando suporte ao corpo de requisições HTTP
Neste artigo, abordamos o conceito de corpo nas requisições HTTP, que é necessário para o envio de dados como JSON e texto simples. Discutimos e explicamos como usá-lo corretamente junto com os cabeçalhos apropriados. Também introduzimos a classe ChttpBody, que faz parte da biblioteca Connexus e que irá simplificar o trabalho com o corpo das requisições.
Ganhe Vantagem em Qualquer Mercado (Parte V): Dados Alternativos FRED EURUSD Ganhe Vantagem em Qualquer Mercado (Parte V): Dados Alternativos FRED EURUSD
Na discussão de hoje, utilizamos dados alternativos diários do Federal Reserve de St. Louis sobre o Índice Amplo do Dólar dos EUA e um conjunto de outros indicadores macroeconômicos para prever a taxa de câmbio futura do EURUSD. Infelizmente, embora os dados aparentem ter uma correlação quase perfeita, não conseguimos obter ganhos materiais em nossa acurácia de modelo, o que pode nos indicar que os investidores talvez estejam melhores usando apenas as cotações normais do mercado.
Recursos do Assistente MQL5 que você precisa conhecer (Parte 42): Oscilador ADX Recursos do Assistente MQL5 que você precisa conhecer (Parte 42): Oscilador ADX
ADX é outro indicador técnico relativamente popular, usado por alguns traders para avaliar a força da tendência predominante. Atuando como uma combinação de dois outros indicadores, ele é um oscilador cujos padrões vamos explorar neste artigo com a ajuda do Assistente MQL5 e suas classes auxiliares.