English Deutsch 日本語
preview
Машинное обучение и Data Science (Часть 31): Применение моделей CatBoost в трейдинге

Машинное обучение и Data Science (Часть 31): Применение моделей CatBoost в трейдинге

MetaTrader 5Трейдинг | 27 мая 2025, 11:44
295 4
Omega J Msigwa
Omega J Msigwa

 «CatBoost — это библиотека градиентного бустинга, которая отличается эффективной и масштабируемой обработкой категориальных признаков, обеспечивая значительный прирост производительности для множества прикладных задач».

— Энтони Голдблум.


Что такое CatBoost?

CatBoost — это библиотека с открытым исходным кодом, реализующая алгоритмы градиентного бустинга на деревьях решений. Разработана специально для решения задач, связанных с обработкой категориальных признаков и данных в задачах машинного обучения.

Разработанная компанией Яндекс, библиотека была выложена в открытый доступ в 2017 году (подробнее).

Несмотря на то, что CatBoost появился относительно недавно по сравнению с другими методами машинного обучения, например, линейной регрессией или SVM, библиотека быстро завоевал популярность и вошла в число самых используемых моделей машинного обучения на Kaggle.

CatBoost привлек столько внимания благодаря своей способности автоматически обрабатывать категориальные признаки в наборе данных — а это довольно сложная задача для многих алгоритмов машинного обучения.

  • Модели Catboost обычно дают лучшую производительность по сравнению с другими, требуя при этом минимальные усилиях, и даже с параметрами и настройками по умолчанию эти модели демонстрируют отличную точность.
  • В отличие от нейронных сетей, для работы с которыми требуются глубокие знания предметной области, CatBoost проще для реализации.

Данный материал предполагает, что у вас есть базовые знания о машинном обучении, деревьях решений, XGBoost, LightGBM и ONNX.


Как работает CatBoost?

CatBoost основан на алгоритме градиентного усиления, это делает его сходим с методами Light Gradient Machine (LightGBM) и Extreme Gradient Boosting (XGBoost). Принцип его работы заключается в последовательном построении нескольких моделей на основе решающих деревьев, где каждая последующая модель пытается скорректировать ошибки предыдущей.

Окончательный прогноз представляет собой взвешенную сумму предсказаний всех моделей, участвующих в процессе.

Цель деревьев решений с градиентным бустингом заключается в минимизации функции потерь. Для этого и добавляется новая модель, которая компенсирует ошибки предыдущей.


Обработка категориальных признаков

Как упоминалось в начале статьи, CatBoost способен обрабатывать категориальные признаки без необходимости кодирования вручную, например, с помощью one-hot или label encoding, которые обычно нужны для других моделей машинного обучения. Это становится возможным благодаря встроенному механизму target-based encoding —кодирования категориальных признаков с использованием информации о целевой переменной.

Суть его в том, что для каждого уникального значения категориального признака рассчитывается условное распределение целевой переменной.

Еще одна важная особенность CatBoost — использование так называемого упорядоченного бустинга (ordered boosting) для вычисления статистики категориальных признаков. При этом кодирование каждого объекта данных опирается только на информацию, полученную из предыдущих объектов.

Такой подход помогает избежать потери данных и переобучения.


Использование симметричных структур решающих деревьев

В отличие от LightGBM и XGBoost, которые используют асимметричные деревья, CatBoost использует симметричные деревья решений для построения моделей. В симметричном дереве обе ветви на каждом разбиении формируются симметрично, с использованием одинакового правила разбиения. Такой подход имеет несколько преимуществ:

  • Более быстрое обучение за счет симметричных разбиений;
  • Эффективное использование памяти благодаря упрощенной структуре дерева;
  • Симметричные деревья более устойчивы к небольшим изменениям в данных.


Сравнение CatBoost, XGBoost и LightGBM

Давайте разберемся, чем CatBoost отличается от других деревьев решений с градиентным бустингом. Ниже представлена сравнительная таблица, которая поможет лучше понять, какой вариант подходит для той или иной ситуации.


Характеристики

CatBoostLightGBMXGBoost

Обработка категориальных признаков

Автоматическое определение и упорядоченный бустинг для обработки категориальных переменных.Требует предварительного кодирования (например, one-hot encoding, label encoding и др.)Требует предварительного кодирования (например, one-hot encoding, label encoding и др.)

Структура дерева решений

Симметричные деревья решений, которые сбалансированы и растут равномерно. Они дают более быстрые прогнозы и меньший риск переобучения.
Рост по листьям (асимметричная структура), фокусируется на листьях с наибольшими потерями. В результате получаются глубокие и несбалансированные деревья, которые могут обеспечить более высокую точность, но при этом повышают риск переобучения.


Поуровневый рост (асимметричная структура), которая выращивает дерево на основе наилучшего разбиения для каждого узла. Дает гибкие, но более медленные прогнозы, есть потенциальный риск переобучения.


Точность модели


Хорошая точность при работе с наборами данных, содержащими множество категориальных признаков, благодаря упорядоченному усилению и снижению риска переобучения на меньших данных.

Дают хорошую точность, особенно для больших и многомерных наборов данных, применяются для улучшения производительности в областях с высокой погрешностью.Обеспечивают хорошую точность для большинства наборов данных, но, как правило, уступают CatBoost на категориальных наборах данных и LightGBM на очень больших наборах данных из-за менее агрессивной стратегии выращивания деревьев.

Скорость и точность обучения


Обычно обучаются медленнее, чем LightGBM, но более эффективны на небольших и средних наборах данных, особенно когда работаем с категориальными признаками.

Обычно это самый быстрый из этих трех методов, особенно при сравнении на больших наборах данных, благодаря росту дерева по листьям, что более эффективно для многомерных данных.Самый медленный из этих трех. Эффективен для работы с большими наборами данных. 


Развертывание модели CatBoost

Прежде чем мы приступим к непосредственной работе с CatBoost, давайте создадим сценарий задачи. Мы будем генерировать прогноз торговых сигналов (покупка/продажа) на основе данных Open, High, Low, Close, а также нескольких категориальных признаков — текущая дата, день недели (с понедельника до воскресенья), день года (от 1 до 365) и месяц (с января до декабря).

Значения OHLC (открытие, максимум, минимум, закрытие) являются непрерывными признаками, а остальные — категориальными. Скрипт для сбора этих данных приложен к статье.

Начнем с импорта модели CatBoost.

Установка

Командная строка

pip install catboost

Импорт.

Код 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")

Чтобы разобраться в данных, визуализируем их.

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

df.head()

Вывод

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

При сборе данных в скрипте на MQL5 я получил значения DayofWeek (с понедельника по воскресенья) и Month (с января по декабря) в виде целочисленных значений, а не строк. Это связано с тем, что данные сохранялись в матрице, и в нее нельзя добавить строки. Хотя эти признаки по своей природе являются категориальными. В их текущем виде они не распознаются как категориальные, поэтому преобразуем их обратно в категориальные данные и посмотрим, как CatBoost справится с ними.

Подготовим целевую переменную.

Код 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()

Вывод

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

Итак, мы разобрались с сигналами, которые можно спрогнозировать. Теперь давайте разделим данные на обучающую и тестовую выборки.

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)

Определим список категориальных признаков в нашем наборе данных.

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

Затем мы можем использовать этот список для преобразования категориальных признаков в формат string, которым обычно бывают представлены категориальные переменные.

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

Вывод

<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

Мы получили категориальные переменные с типом данных object — e нас это string. Если попытаться применить эти данные к другой модели машинного обучения (не CatBoost), мы получим ошибки, поскольку тип данных object не поддерживается в типичных обучающих выборках для машинного обучения.


Обучение модели CatBoost

Прежде чем вызывать метод обучения модели CatBoost fit, давайте разберемся с параметрами модели.


Параметр

Описание

Iterations


Количество итераций построения решающих деревьев.

Большее количество итераций обычно приводит к улучшению производительности, однако также увеличивает риск переобучения.


learning_rate               
  

Этот параметр контролирует вклад каждого дерева в итоговое предсказание.

Меньшее значение learning_rate требует большего числа итераций для сходимости деревьев, но зачастую приводит к более качественным моделям.


depth


Максимальная глубина деревьев.

Более глубокие деревья способны улавливать более сложные закономерности в данных, но часто могут приводить к переобучению.


cat_features


Список категориальных индексов.
Хотя модель CatBoost способна самостоятельно определять категориальные признаки, хорошей практикой считается явное указание модели, какие признаки являются категориальными.
Это помогает модели правильно интерпретировать категориальные данные с человеческой точки зрения, так как автоматические методы обнаружения категориальных переменных могут иногда ошибаться.

l2_leaf_reg


Коэффициент L2-регуляризации.

Позволяет бороться с переобучением, накладывая штраф на большие веса листьев.


border_count
 


Количество разделений для каждого категориального признака. Чем выше это число, тем выше качество модели, но при этом увеличивается время вычислений.



eval_metric


Метрика оценки, которая будет использоваться в процессе обучения.

Позволяет эффективно отслеживать производительность модели.


early_stopping_rounds


При наличии валидационных данных обучение остановится, если в течение указанного числа итераций не будет улучшения точности модели.

Параметр помогает снизить риск переобучения и может существенно сократить время обучения.

Определим словарь для этих параметров.

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
)

Наконец, мы можем создать модель CatBoost внутри пайплайна Sklearn, а затем вызвать метод fit для обучения. В параметрах методу передадим данные для валидации и список категориальных признаков.

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)

Результаты

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.


Оценка модели

Для оценки эффективности модели будем использовать метрики 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))

Результаты

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

В результате мы получили модель со средней производительностью. Я заметил, что после удаления списка категориальных признаков точность модели на обучающем наборе данных увеличилась до 60%, но на тестовом наборе осталась на прежнем уровне.

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

Вывод

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

Чтобы более подробно разобраться в этой модели, построим график значимости признаков.

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

Вывод

Приведенный выше график feature importance наглядно демонстрирует, как модель принимала решения. Похоже, что модель CatBoost посчитала категориальные признаки более значимыми для финального прогноза, чем непрерывные признаки.


Сохранение модели CatBoost в ONNX

Чтобы использовать модель в платформе MetaTrader 5, нужно сохранить ее в формате ONNX. Однако сохранение модели CatBoost может быть немного сложнее, чем в случае с Sklearn или Keras, которые предлагают более простые методы конвертации.

Но все вполне реализуемо, если следовать их инструкциям из официальной документации. Не пришлось особо вникать в детали кода.

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

Ниже показан финальный код для конвертации модели и сохранения ее в файле .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())

При визуализации в Netron структура модели выглядит так же, как у XGBoost и LightGBM.

Модель catboost в netron

Это логично, так как CatBoost тоже основан на деревьях решений с градиентным бустингом. В своей основе они имеют схожие структуры.

Когда я попытался конвертировать модель CatBoost, встроенную в пайплайн, в формат ONNX с категориальными признаками, я получил ошибку:

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

Категориальные признаки должны быть представлены в формате float64 (double), как и в изначально собранных данных в MetaTrader 5. Это решило проблему. Можно использовать модель в MQL5 и не переживать, что double или float перепутаются с целыми числами.

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

Несмотря на это изменение, точность модели CatBoost осталась на том же уровне, так как она способна работать с наборами данных различной природы.


Создание торгового робота с CatBoost

Начнем с внедрения модели ONNX в нашего советника в виде ресурса.

Код 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;

Нужно собрать данные таким же образом, как мы собирали обучающие данные. Делается это внутри функции 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
...
...
...     
  }

После этого мы можем получить сигнал и вектор вероятности. Определять будем два сигнала: медвежий класса 0 и бычий класса 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);

Осталось реализовать в советнике торговую стратегию на основе прогнозов, полученных с помощью модели. 

Будем использовать простейшую стратегию: когда модель предсказывает бычий сигнал, открываем сделку на покупку и, если при этом есть открытая сделка на продажу, закрываем ее. При медвежьем сигнале совершаем противоположные сделки — продаем, закрываем покупку.

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

Тестировали стратегию в тестере на периоде с 01.01.2021 по 08.10.2024 на 4-часовом таймфрейме. Тестирование проводилось только по ценам открытия. Ниже представлен результат.

'

Советник показал достойные результаты — 55% прибыльных сделок, что принесло общий чистый доход в размере $96 USD. Неплохой результат для простой выборки данных, простой модели и минимального объема сделок.


Заключительные мысли

CatBoost и другие модели градиентного бустинга на решающих деревьях хорошо подходят для работы в условиях ограниченных вычислительных ресурсов. Также это отличный вариант, если вам нужна модель, которая "просто работает". Или если вы не хотите заниматься не очень интересными аспектами вроде подбора признаков и настройки моделей, с которыми часто приходится сталкиваться при работе с другими методами машинного обучения. 

При этом, несмотря на простоту и низкий порог входа, эти модели остаются одними из наиболее эффективных инструментов, применимых для множества реальных задач.

С наилучшими пожеланиями.


За развитием этой модели машинного обучения и многого другого из этой серии статей можно следить в моем репозитории на GitHub.

Таблица вложений


Наименование файла

Тип файлаОписание и использование

Experts\CatBoost EA.mq5

 ExpertТорговый робот для загрузки модели CatBoost в ONNX и тестирования полученной торговой стратегии в MetaTrader 5.

Include\CatBoost.mqh

 Include-файл


Содержит код для загрузки и развертывания модели CatBoost.


Files\ CatBoost.EURUSD.OHLC.D1.onnx


 Модель ONNX


Обученная модель CatBoost в формате ONNX.

 
Scripts\CollectData.mq5

 MQL5-скриптСкрипт для сбора обучающих данных. 

Jupyter Notebook\CatBoost-4-trading.ipynb 

 Python/Jupyter notebook Блокнот содержит все коды Python, обсуждаемые в этой статье.


Источники и ссылки

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/16017

Прикрепленные файлы |
Attachments.zip (40.07 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 15 окт. 2024 в 20:28
Ваша статья наводит на размышления.

Интересно, что было бы, если бы мы также отслеживали, в какой торговой сессии находимся.
Omega J Msigwa
Omega J Msigwa | 16 окт. 2024 в 08:23
Да, торговая сессия - это ценная переменная, которую необходимо иметь в обучающих данных
Maxim Dmitrievsky
Maxim Dmitrievsky | 27 мая 2025 в 13:01
Все классификаторы (включая catboost) корректно работают только с нормализованным признаками. Цены как признаки не подходят.
Aliaksandr Kazunka
Aliaksandr Kazunka | 27 мая 2025 в 18:52

а еще есть проблема экспорта модели классификатора в ONNX


Note

The label is inferred incorrectly for binary classification. This is a known bug in the onnxruntime implementation. Ignore the value of this parameter in case of binary classification.

Нейросети в трейдинге: Эффективное извлечение признаков для точной классификации (Mantis) Нейросети в трейдинге: Эффективное извлечение признаков для точной классификации (Mantis)
Познакомьтесь с Mantis — лёгкой фундаментальной моделью для классификации временных рядов на базе Transformer с контрастным предварительным обучением и гибридным вниманием, обеспечивающими рекордную точность и масштабируемость.
Создаем интерактивную MQL5-панель с использованием класса Controls (Часть 2): Добавление отзывчивости кнопок Создаем интерактивную MQL5-панель с использованием класса Controls (Часть 2): Добавление отзывчивости кнопок
В этой статье мы преобразуем нашу статическую панель мониторинга MQL5 в интерактивный инструмент, добавив отзывчивость кнопок. Мы рассмотрим, как автоматизировать функционал компонентов графического интерфейса, гарантируя, что они будут правильно реагировать на нажатия пользователя. К концу статьи мы создадим динамический интерфейс, который повышает вовлеченность пользователей и удобство торговли.
Создание торговой панели администратора на MQL5 (Часть V): Двухфакторная аутентификация (2FA) Создание торговой панели администратора на MQL5 (Часть V): Двухфакторная аутентификация (2FA)
В статье рассмотрено повышение безопасности панели торгового администратора, которая в настоящее время находится в разработке. Мы рассмотрим, как внедрить MQL5 в новую стратегию безопасности, интегрировав API Telegram для двухфакторной аутентификации (2FA). Статья предоставит ценную информацию о применении MQL5 для усиления мер безопасности. Кроме того, мы рассмотрим функцию MathRand, сосредоточившись на ее функциональности и на том, как ее можно эффективно использовать в нашей системе безопасности.
Детерминированный осциллирующий поиск — Deterministic Oscillatory Search (DOS) Детерминированный осциллирующий поиск — Deterministic Oscillatory Search (DOS)
Алгоритм Deterministic Oscillatory Search (DOS) — инновационный метод глобальной оптимизации, сочетающий преимущества градиентных и роевых алгоритмов без использования случайных чисел. Механизм осцилляций и наклонов фитнеса позволяет DOS исследовать сложные пространства поиска детерминированным методом.