English
preview
Архитектура машинного обучения для MetaTrader 5 (Часть 8): Байесовская оптимизация гиперпараметров с Purged Cross-Validation и ранним отсечением испытаний

Архитектура машинного обучения для MetaTrader 5 (Часть 8): Байесовская оптимизация гиперпараметров с Purged Cross-Validation и ранним отсечением испытаний

MetaTrader 5Торговые системы |
309 0
Patrick Murimi Njoroge
Patrick Murimi Njoroge

Оглавление

  1. Проблема стандартной HPO в финансовом ML
  2. Optuna — архитектура и основные понятия
  3. Контракт данных и _WeightedEstimator
  4. FinancialModelSuggester
  5. Целевая функция — Purged K-Fold с отсечением
  6. Финансово-ориентированное отсечение
  7. Оркестрация и хранение
  8. От Optuna Study к scikit-learn cv_results_
  9. Визуализация
  10. Практические соображения
  11. Заключение
  12. Прикрепленные файлы


Введение

Вы строите финансовый ML-классификатор на triple-barrier или meta-labels и должны честно выбрать гиперпараметры — не превращая сам поиск в механизм переобучения. Стандартные инструменты не подходят для этого сценария по трем конкретным причинам. GridSearchCV и RandomizedSearchCV не учатся на прошлых испытаниях, поэтому тратят бюджет, повторно исследуя области пространства гиперпараметров, которые уже показали себя плохими. Они не могут остановить бесперспективную конфигурацию после первого дорогого фолда PurgedKFold; поэтому каждое испытание вынуждено проходить все фолды целиком, даже когда первый фолд уже сигнализирует о неудаче. Кроме того, они плохо интегрируются с финансовым контрактом данных — PurgedKFold как единственный допустимый разделитель, отдельные веса для обучения и оценки, а также постоянное хранилище, чтобы долгий поиск переживал сбои и поддерживал параллельных работников. Измеримые симптомы: напрасные вычисления, смещенные out-of-sample сравнения из-за утечки информации через границу purge и хрупкие эксперименты, которые после любого прерывания нужно начинать с нуля.

В этой статье показана практическая замена: Optuna (семплер TPE + отсечение + хранилище SQLite), нативно связанная с финансовой кросс-валидацией и соглашениями о взвешивании, установленными в предыдущих частях этой серии. После прочтения у вас будет конкретная, практическая, воспроизводимая схема HPO, состоящая из пяти компонентов:

  1. целевая функция, которая выполняет PurgedKFold кросс-валидацию с оценкой, взвешенной по атрибуции доходности, и сообщает результаты на уровне фолдов для отсечения;
  2. FinancialModelSuggester, слой трансляции параметров, который преобразует спецификации распределений scikit-learn в вызовы trial.suggest_*() и одновременно оптимизирует схему взвешивания выборки и затухание;
  3. финансово-ориентированный механизм отсечения (TradingModelPruner), который применяет экономическую базовую линию на основе энтропии и допуск волатильности, масштабированный по режимам;
  4. оркестратор (optimize_trading_model) с хранилищем SQLite для возобновления и параллельных работников;
  5. конвертер из Study в форматcv_results_ для совместимости с существующей аналитикой scikit-learn.

Выходные артефакты — сохраненное Study, переобученный best_estimator_ (_WeightedEstimator оборачивает настроенную базовую модель), и cv_results_ DataFrame с оценками по фолдам — готовый для тех же последующих диагностик, которые используются в остальной части конвейера.

Эта статья является частью 8.1 серии MetaTrader 5 Machine Learning Blueprint. Предыдущие статьи построили компоненты, от которых зависит эта система: Часть 1 устранила утечку данных на уровне баров; Часть 2 ввела triple-barrier и meta-labels; Часть 3 добавила метки trend-scanning; Часть 4 рассмотрела конкуренцию меток и ввела веса средней уникальности; Часть 5 ввела sequential bootstrapping и установила соглашение об отдельных весах обучения и оценки; Часть 6 построила инфраструктуру кэширования; а Часть 7 собрала все в воспроизводимый производственный пайплайн. Интеграция компонентов этой статьи в производственный конвейер — включая обертку clf_hyper_fit и кэширование — рассматривается в части 8.2.


1. Проблема стандартной HPO в финансовом ML

1.1 Почему оптимизация гиперпараметров важнее в финансах

В большинстве областей ML разумно настроенная модель с хорошими признаками дает приемлемые результаты. В финансах отношение сигнал/шум низкое, нестационарность является нормой, а цена переобучения измеряется капиталом. Как подчеркивает Лопес де Прадо, каждая степень свободы в конвейере — это возможность переобучиться. Гиперпараметры являются степенями свободы, и метод их поиска определяет, будет ли итоговая конфигурация обобщать или запоминать.

Проблема усиливается структурой финансовых данных. Как было установлено в Части 4, метки triple-barrier перекрываются во времени. Возникающая конкуренция меток означает, что стандартная k-fold CV будет пропускать информацию через границу между обучающей и тестовой выборками, и любая полученная оценка будет оптимистично смещена. Инструмент HPO должен нативно работать с PurgedKFold для получения корректных сравнений между испытаниями.

1.2 Ограничения Scikit-Learn

GridSearchCV масштабируется экспоненциально: случайный лес с пятью значениями для каждого из четырех гиперпараметров дает 625 комбинаций, то есть 3 125 обучений модели при 5-fold CV. RandomizedSearchCV снижает стоимость, но остается неинформированным — каждое испытание независимо. Ни один из них не поддерживает раннюю остановку, межиспытательное обучение или постоянное хранилище.

 

Ограничение

Влияние на финансовый ML

 1. Нет обучения между испытаниями Вычисления тратятся на плохие области пространства гиперпараметров
 2. Нет ранней остановки плохих испытаний Все фолды выполняются, даже когда ранние фолды уже показывают плохие оценки
 3. Нет постоянного хранилища Прерванные поиски нельзя возобновить; сравнение между экспериментами требует ручного учета
 4. Жесткая интеграция CV Отчетность на уровне фолдов для отсечения требует обходных решений, нарушающих стандартный интерфейс CV

1.3 Критически важная граница: HPO и оптимизация стратегии

Прежде чем продолжить, нужно четко провести одну границу. Интеллектуальный поиск уместен для HPO и вреден для оптимизации параметров стратегии. Тимоти Мастерс точно определяет это: семплер TPE или генетический алгоритм эффективно находит глобальный оптимум in-sample поверхности пригодности — но в финансовых данных эта поверхность в значительной степени состоит из шума. Чем эффективнее вы ее ищете, тем сильнее переобучается результат.

Правило простое: если ваша целевая функция — это статистическая мера на размеченных данных (кросс-валидированная accuracy, log-loss на triple-barrier labels), используйте Optuna. Если это финансовая мера на исторических кривых капитала (коэффициент Шарпа, просадка), не используйте. Преимущество Optuna в HPO — то же свойство, которое делает ее опасной для оптимизации стратегии: она слишком надежно находит оптимум.


2. Optuna — архитектура и основные понятия

2.1 Как работает Optuna

Optuna построена вокруг трех понятий:

  • Study: Полная сессия оптимизации с направлением (максимизировать или минимизировать), которая хранит все результаты. При хранилище SQLite study сохраняется между Python-сессиями — именно тот тип воспроизводимости с учетом времени, который был установлен в Части 6.
  • Испытание: Одна оценка одной конфигурации гиперпараметров. Каждое испытание предлагает значения, вычисляет целевую функцию и сообщает результат.
  • Sampler: Алгоритм, выбирающий, какие значения пробовать дальше. По умолчанию используется TPE (Tree-structured Parzen Estimator), байесовский метод оптимизации, который учится на завершенных испытаниях.

Ключевое отличие от инструментов scikit-learn: Optuna учится на завершенных испытаниях. семплер TPE моделирует связь между гиперпараметрами и целевой функцией, постепенно фокусируясь на перспективных областях. Каждое следующее испытание с большей вероятностью исследует конфигурации рядом с уже известными хорошими результатами.

2.2 Отсечение: HyperbandPruner

Отсечение останавливает испытание до завершения, если промежуточные результаты показывают, что оно не будет конкурентоспособным. В 5-fold purged CV, если первый фолд дает плохую оценку, оставшиеся фолды можно пропустить — напрямую снижая общий объем вычислений.

HyperbandPruner — правильный безусловный выбор для финансовой CV. При всего 5–10 фолдах на испытание большинству pruner-ов — которые работают внутри испытания — почти не с чем работать. Hyperband работает между испытаниями. Вместо вопроса «плохо ли это испытание работает по сравнению со всеми остальными на этом шаге?» Hyperband задает вопрос распределения ресурсов: «при фиксированном вычислительном бюджете какие испытания заслуживают больше фолдов?»

Как работают ветви

Используя точные параметры из кода — min_resource=1, max_resource=5 (для cv=5), reduction_factor=3 (η=3) — число ветвей равно:

    s_max = ⌊log₃(5/1)⌋ = ⌊1.46⌋ = 1

Итак, есть две ветви, s=0 и s=1, работающие одновременно в рамках бюджета испытаний. Думайте о них как о двух параллельных гонках.

Ветвь s=1 — агрессивная ветвь: входят три испытания. Каждое оценивается только на фолде 1. Лучший ⌈3/3⌉ = 1 выживший продолжает фолды 2–5. Два других отсекаются после одной оценки фолда. Всего оценок фолдов: 3×1 + 1×4 = 7.

Ветвь s=0 — консервативная ветвь: входят два испытания, и оба безусловно проходят до фолда 5. В этой ветви отсечение не срабатывает независимо от промежуточных оценок. Всего оценок фолдов: 2×5 = 10.

Консервативная ветвь — не запасной вариант, а гарантированная страховка, работающая параллельно. Комбинация параметров, которая после первого фолда выглядит слабой (например, если этот фолд покрывает кризисный период), в целом может оказаться лучшей; поэтому она все равно получит полную оценку через консервативную ветвь. Именно это отличает Hyperband от SuccessiveHalvingPruner, который фактически представляет собой только ветвь s=1 и полностью отбросил бы такое сочетание параметров.

 

Ветвь s=1 (агрессивный)

Ветвь s=0 (консервативный)

 Запущено испытаний 3 2
 Отсечено после фолда 1 2 0
 Завершает все фолды 1 2
 Всего оценок фолдов 7 10

Структура ветвей HyperbandPruner]Рис. 1 — Структура ветвей HyperbandPruner

Стоит ли создавать пользовательский механизм отсечения поверх HyperbandPruner?

Нет — и обертка поверх него нарушит его работу. HyperbandPruner управляет внутренним состоянием через назначения ветви при создании испытания. Когда вызывается trial.should_prune(), Hyperband проверяет, к какому ветвь принадлежит испытание, на каком уровне оно находится и попадает ли он ниже верхних 1/η испытаний на этом уровне. Это система управления турниром с состоянием, а не простая проверка порога. Наследование и добавление финансовых правил в prune() создает два режима отказа: ваши правила могут убить trial, который Hyperband предназначил для консервативной ветви (ломая гарантию полной оценки), а записи уровней в Hyperband станут несогласованными, потому что trial, отсеченный вашим правилом, никогда не сравнивался на запланированном уровне, что искажает медианные сравнения для всех последующих испытаний.

TradingModelPruner работает именно потому, что оборачивает MedianPruner, у которого нет состояния ветвей, которое можно испортить. Правильный способ добавить финансовую доменную логику при использовании HyperbandPruner — поместить проверки внутрь целевой функции перед вызовом trial.should_prune():

for fold_idx, (train_idx, val_idx) in enumerate(cv.split(X, y)):
    # ... fit and score ...
    fold_scores.append(score)

    # 1. Financial domain check fires first — before Hyperband's ветвь logic
    if score < min_score_threshold:
        trial.set_user_attr("pruned_reason", "below_baseline")
        raise TrialPruned(f"Below economic baseline at fold {fold_idx}")

    # 2. Hyperband bracket management — rung comparison across trials
    trial.report(score, step=fold_idx)
    if trial.should_prune():
        raise TrialPruned()

Финансовая логика срабатывает до сообщения результата в Hyperband, поэтому испытание просто не попадает на этот уровень сравнения, и внутреннее состояние Hyperband остается согласованным. Эти два механизма чисто разделены.

from optuna.pruners import HyperbandPruner

pruner = HyperbandPruner(
    min_resource=1,
    max_resource=n_splits,
    reduction_factor=3,
)

2.3 API Define-by-Run

Optuna задает пространство поиска внутри целевой функции. Это позволяет использовать условные пространства гиперпараметров и дает FinancialModelSuggester возможность добавить weight_scheme, weight_decay и weight_linear вместе с параметрами базовой модели, совместно оптимизируя стратегию взвешивания с моделью.

2.4 Постоянное хранилище и возобновляемость

Optuna хранит результаты study в SQLite (или PostgreSQL, MySQL). Сбойный или ограниченный по времени запуск возобновляется с последнего завершенного испытания через load_if_exists=True. Это соответствует принципу воспроизводимости, установленному в Части 7: каждый эксперимент должен быть возобновляемым, проверяемым и давать идентичные результаты при повторном запуске. Несколько параллельных процессов могут выполнять испытания параллельно в одном исследовании, а 30-секундный SQLite timeout предотвращает конфликты блокировок.


3. Контракт данных и _WeightedEstimator

Все компоненты этой системы используют общий контракт данных, унаследованный от производственный конвейер, установленного в Части 7:

  • X: Feature DataFrame с DatetimeIndex, созданный конвейером инженерии признаков.
  • y: Target Series, выровненный с X — triple-barrier или trend-scanning labels из Части 2 и Части 3.
  • events: DataFrame, индексированный временем события, содержащий как минимум: t1 (время касания барьера), w (вес атрибуции доходности |return|), и tW (вес на основе уникальности из Части 4).
  • data_index: DatetimeIndex полного набора баров — используется _WeightedEstimator для вычисления весов по правильной вселенной баров.

3.1 Две различные роли весов

Перед описанием кода обработка весов требует явного рассмотрения, потому что примеры Прадо смешивают две величины, служащие разным целям. Это различие было предвосхищено в Части 5, где был введен ml_cross_val_scores_all с отдельными параметрами sample_weight_train и sample_weight_score.

Веса средней уникальности (events['tW']) отвечают на вопрос: «сколько независимой информации вносит это наблюдение?» Их место — в estimator.fit(), чтобы модель не переоценивала неявно избыточные перекрывающиеся наблюдения — проблему конкуренции меток, выявленную в Части 4.

Веса атрибуции доходности (events['w']) отвечают на вопрос: «насколько корректный или ошибочный прогноз по этому наблюдению важен для P&L?» Их место — в валидирующей функции оценки. Без них метрика CV считает прогноз на плоский день столь же важным, как прогноз в день крупного движения, незаметно искажая сравнения между испытаниями.

# Fitting: _WeightedEstimator applies tW internally via scheme='uniqueness'
fit = clone(model).fit(X_train, y_train)

# Scoring: return-attribution weights from events['w']
w_val = events['w'].iloc[val_idx].to_numpy()
score = -log_loss(y_val, y_prob, sample_weight=w_val)

Прадо вычисляет один комбинированный вес (уникальность × |return|) и использует его и для обучения, и для оценки. Это разумное приближение. Использование отдельных весов, как выше, строго корректнее, потому что каждый вес выполняет подходящую ему работу. Более того, сама схема взвешивания — unweighted, uniqueness или return — это гиперпараметр, который FinancialModelSuggester оптимизирует совместно с параметрами модели.

Важное замечание о типе стратегии: trend-following модели могут выиграть от весов return-attribution для обучения вместо весов уникальности. Средняя уникальность штрафует перекрытие меток. На трендовых рынках метки по замыслу длинные и сильно перекрываются, поэтому трендовая модель, обученная с весами уникальности, фактически начинает недооценивать именно те условия, которые должна использовать. Веса атрибуции доходности не учитывают перекрытие — они просто повышают вес крупных движений. Гиперпараметр weight_scheme делает это измерение доступным для поиска.

3.2 _WeightedEstimator

_WeightedEstimator — совместимая со sklearn обертка (из afml.production.model_development) которая полностью инкапсулирует вычисление весов. Она принимает базовый estimator, events DataFrame, полный баровый data_index, и параметры схемы взвешивания, выбранные Optuna. Когда fit(X_train, y_train) вызывается без явного аргумента sample_weight, _WeightedEstimator внутренне вычисляет соответствующие веса с использованием data_index и events, затем передает их базовому estimator. Такая конструкция убирает вычисление весов из цикла целевой функции и делает схему взвешивания гиперпараметром первого класса.


4. FinancialModelSuggester

FinancialModelSuggester преобразует распределения параметров в стиле scikit-learn в вызовы trial.suggest_*() и конфигурирует _WeightedEstimator с выбранными параметрами. Он предоставляет два метода, образующие двойственную пару: (1) suggest_and_apply для стохастического использования внутри objective, и (2) apply_from_params для детерминированного восстановления из study.best_params. Центральный реестр WEIGHT_KEYS отделяет гиперпараметры весов от гиперпараметров модели в обоих методах, гарантируя, что шаг проверки в apply_from_params проверяет weight keys по принятым параметрам базовой модели только там, где это уместно.

class FinancialModelSuggester:
    """
    Translates Scikit-Learn style distribution dictionaries into
    Optuna trial suggestions for rigorous statistical HPT.

    Two core methods form a dual pair:
        suggest_and_apply  — Trial → params → model  (stochastic, used in objective)
        apply_from_params  — params → model           (deterministic, used for refit)
    """

    # Central registry: separates weight keys from model keys in both methods
    WEIGHT_KEYS = frozenset({"weight_scheme", "weight_decay", "weight_linear"})

    @classmethod
    def suggest_and_apply(
        cls,
        trial: optuna.Trial,
        base_model,
        param_distributions: dict,
        events: pd.DataFrame,
        data_index: pd.DatetimeIndex,
    ):
        # 1. Suggest weight hyperparameters — optimised jointly with the model
        scheme = trial.suggest_categorical("weight_scheme",
                                           ["unweighted", "uniqueness", "return"])
        decay = trial.suggest_float("weight_decay", 0.1, 1.0)
        linear = trial.suggest_categorical("weight_linear", [True, False])

        # 2. Suggest base model hyperparameters from scikit-learn-style distributions
        sampled_params = {}
        for name, dist in param_distributions.items():
            if isinstance(dist, list):
                sampled_params[name] = trial.suggest_categorical(name, dist)
            elif hasattr(dist, 'ppf'):                         # scipy.stats distribution
                low, high = dist.support()
                if dist.dist.name == "randint":
                    sampled_params[name] = trial.suggest_int(name, int(low), int(high))
                else:
                    is_log = dist.dist.name in ['reciprocal', 'loguniform']
                    sampled_params[name] = trial.suggest_float(name, low, high, log=is_log)
            elif isinstance(dist, range):
                try:
                    sampled_params[name] = trial.suggest_int(name, dist.start, dist.stop - 1)
                except AttributeError:
                    low, high = dist.support()
                    sampled_params[name] = trial.suggest_int(name, int(low), int(high))
            else:
                sampled_params[name] = dist

        # 3. Clone and configure — _WeightedEstimator handles weight computation internally
        new_base = clone(base_model)
        new_base.set_params(**sampled_params)

        return _WeightedEstimator(
            base_estimator=new_base,
            events=events,
            data_index=data_index,
            scheme=scheme,
            decay=decay,
            linear=linear,
        )

    @classmethod
    def apply_from_params(
        cls,
        params: dict,
        base_model,
        events: pd.DataFrame,
        data_index: pd.DatetimeIndex,
    ) -> "_WeightedEstimator":
        """
        Reconstruct a WeightedEstimator from a flat params dict (e.g. study.best_params).
        Validates model params against the base model's accepted parameter set.
        """
        weight_params = {k: params[k] for k in cls.WEIGHT_KEYS if k in params}
        model_params = {k: v for k, v in params.items() if k not in cls.WEIGHT_KEYS}

        # Defensive validation: catch invalid params before refit
        valid_keys = set(base_model.get_params().keys())
        invalid = set(model_params) - valid_keys
        if invalid:
            raise ValueError(
                f"Parameters {invalid} are not valid for "
                f"{type(base_model).__name__}. Valid: {sorted(valid_keys)}"
            )

        new_base = clone(base_model)
        new_base.set_params(**model_params)

        return _WeightedEstimator(
            base_estimator=new_base,
            events=events,
            data_index=data_index,
            scheme=weight_params.get("weight_scheme", "unweighted"),
            decay=weight_params.get("weight_decay", 1.0),
            linear=weight_params.get("weight_linear", False),
        )

    @classmethod
    def get_search_space(cls, model_name: str):
        """
        Returns curated parameter distributions for financial models.
        Note min_weight_fraction_leaf for random forests: requires each leaf to account
        for at least a fraction of total sample weight. This interacts directly with
        return-attribution weights and is a stronger regulariser than min_samples_leaf
        when weights vary widely across market regimes.
        """
        spaces = {
            "random_forest": {
                "n_estimators":             range(100, 1000),
                "max_depth":                range(3, 7),
                "min_weight_fraction_leaf": stats.uniform(0.025, 0.1),
                "max_features":             ["sqrt", "log2", 0.5, 1.0],
                "ccp_alpha":                stats.loguniform(1e-5, 1e-2),
            },
            "xgboost": {
                "n_estimators":     range(100, 1000),
                "learning_rate":    stats.loguniform(1e-3, 0.1),
                "max_depth":        range(2, 8),
                "subsample":        stats.uniform(0.6, 0.4),
                "colsample_bytree": stats.uniform(0.6, 0.4),
                "gamma":            stats.uniform(0, 5),
            },
        }
        return spaces.get(model_name.lower(), {})


5. Целевая функция — Purged K-Fold с отсечением

Целевая функция — точка интеграции. Она выполняет PurgedKFold кросс-валидацию (введенную в Части 4), применяет веса атрибуции доходности к валидационной метрике и сообщает оценки на уровне фолдов в HyperbandPruner после каждого фолда через trial.report(). Такая прямая отчетность на уровне фолдов — ключевая возможность, которую GridSearchCV не может предоставить без существенных обходных решений.

Стоит отметить две детали. Во-первых, clone(model).fit(X_train, y_train) без явного аргумента sample_weight корректен — _WeightedEstimator вычисляет и применяет training weights внутренне на основе scheme, выбранной для этого trial. Во-вторых, w_val из events['w'] всегда является весом атрибуции доходности для оценки, независимо от выбранной схемы обучения, потому что мы всегда оцениваем прогнозы по экономической значимости. Это разделение отражает соглашение sample_weight_train / sample_weight_score из ml_cross_val_scores_all в Части 5.

def optimize_trading_model_with_отсечение(
    trial: optuna.Trial,
    X, y, events, data_index,
    classifier,
    param_distributions: dict,
    n_splits: int = 5,
    metric: str = "neg_log_loss",
):
    """
    Objective function for tuning models using Purged K-Fold cross-validation.
    Uses separate weights for обучения (_WeightedEstimator) and оценки (events['w']),
    mirroring the sample_weight_train / sample_weight_score convention from Part 5.
    """
    suggester = FinancialModelSuggester()
    model = suggester.suggest_and_apply(
        trial, classifier, param_distributions, events, data_index
    )

    t1 = events['t1']
    cv = PurgedKFold(n_splits=n_splits, t1=t1, pct_embargo=0.01)

    # Convert once before the loop — avoids repeated pandas overhead per fold
    # and ensures val_idx (numpy integer array from PurgedKFold) indexes correctly
    w_score = events['w'].to_numpy()

    fold_scores = []

    for fold_idx, (train_idx, val_idx) in enumerate(cv.split(X, y)):
        X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]
        y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]

        # _WeightedEstimator applies training weights (uniqueness or return) internally
        fit = clone(model).fit(X_train, y_train)

        # Slice pre-converted numpy array — no pandas overhead inside the hot loop
        w_val = w_score[val_idx]

        if metric == "neg_log_loss":
            y_prob = fit.predict_proba(X_val)
            score = -log_loss(y_val, y_prob, sample_weight=w_val)
        else:
            y_pred = fit.predict(X_val)
            score = f1_score(y_val, y_pred, sample_weight=w_val)

        fold_scores.append(score)

        # Financial baseline check fires before Hyperband's ветвь logic
        # (only active when TradingModelPruner is not the pruner)
        trial.report(score, step=fold_idx)

        if trial.should_prune():
            avg_score_so_far = np.mean(fold_scores)
            trial.set_user_attr("pruned_at_fold", fold_idx)
            trial.set_user_attr("score_when_pruned", avg_score_so_far)
            trial.set_user_attr("total_folds_attempted", len(fold_scores))
            raise TrialPruned(f"Pruned at fold {fold_idx}. Avg: {avg_score_so_far:.4f}")

    final_score = np.mean(fold_scores)
    trial.set_user_attr("fold_scores", fold_scores)
    trial.set_user_attr("score_std", np.std(fold_scores))
    return final_score

Оценки фолдов, сохраненные в trial.user_attrs["fold_scores"], — не просто диагностические метаданные. Они служат входом для callback’а check_for_overобучения и столбцов уровня фолдов в cv_results_ DataFrame. Высокий score_std на 5-fold purged split часто указывает, что модель захватывает режимно-специфичные паттерны, а не действительно обобщаемый сигнал — именно такую чувствительность к режимам призвана выявлять многорежимная выборка баров, обсуждавшаяся в Части 7.


6. Финансово-ориентированное отсечение

TradingModelPruner расширяет MedianPruner тремя финансово обоснованными правилами. Это не универсальная замена HyperbandPruner — у него есть конкретное окно применимости. Поскольку он оборачивает MedianPruner, он полностью молчит для первых n_startup_trials=10 завершенных trials и дает надежные сравнения только после еще 20–30. На коротком исследовании менее чем с 30 испытаниями он дает меньшую экономию, чем HyperbandPruner. Четыре условия, при которых он становится лучшим выбором, приведены ниже.

Большой бюджет испытаний на хорошо понятном инструменте. Запуск 100 или более испытаний на инструменте с известными характеристиками дает откалиброванное ожидание того, как выглядит разумный нижний уровень log-loss. Испытание, получающий оценку намного ниже этого уровня после фолда 1, — не режимно-чувствительный отстающий, а сломанная конфигурация. Энтропийный базовый порог отсекает такую конфигурацию сразу и без дополнительных условий. HyperbandPruner не может принять это решение, потому что сравнивает относительный ранг между trials, а не абсолютный экономический минимум. Пример: на EURUSD triple-barrier labels, откалиброванных к соотношению барьеров 3:1, log-loss стабильно находится около −0.65. Испытание с −0.92 после фолда 1 заслуживает немедленного завершения независимо от действий других испытаний.

Трендовый инструмент с высокой дисперсией return-weight. На сильно трендовом инструменте events['w'] имеет высокий коэффициент вариации — несколько наблюдений крупных движений имеют веса на порядок выше остальных. TradingModelPruner масштабирует volatility_tolerance пропорционально этому CV, то есть допускает большие колебания fold-score, не отсекая трендовую модель, которая случайно выглядит плохо на mean-reverting фолде. HyperbandPruner отсекает по относительному рангу независимо от того, отражает ли колебание качество модели или рыночную структуру. Пример: модель GBPJPY, обученная на недельных triple-barrier labels в трендовый период, покажет высокую дисперсию fold-score просто потому, что каждый фолд покрывает отдельную фазу momentum. TradingModelPruner учитывает это; HyperbandPruner может ее отбросить.

Окно CV, охватывающее известный перелом режима. Если обучающее окно охватывает структурный сдвиг — изменение политики центрального банка, изменение режима волатильности, изменение микроструктуры, — один фолд будет категорически отличаться от остальных. Модель, лучше всего работающая в обоих режимах, не обязательно будет выглядеть лучшей после фолда 1. TradingModelPruner с n_warmup_steps=2 дает льготный период в два фолда до срабатывания variance-based отсечение. Пример: исследование, чье окно CV охватывает 2019–2022 годы, будет иметь один фолд с COVID-era volatility. Разрешение двух фолдов перед variance-based отсечение не дает этому фолду устранить конфигурации, которые хорошо обобщаются между режимами.

Последующее исследование после начального запуска HyperbandPruner. Практический рабочий процесс: запустить начальное исследование на 50 испытаний с HyperbandPruner, чтобы оценить ландшафт результатов, затем запустить сфокусированное последующее исследование на 150 испытаний с TradingModelPruner. Первое исследование дает откалиброванный диапазон оценок для точной настройки multiplier. Второе исследование не дает семплеру TPE — у которого теперь есть априорная информация из первого запуска — возвращаться к конфигурациям, уже показавшим близость к уровню шума.

Порог базовой энтропии использует распределение меток, взвешенное по атрибуции доходности, а не сырое распределение. Это корректная базовая линия, потому что наивный классификатор, всегда предсказывающий доминирующий класс, достигает этой энтропии, когда классы взвешены по экономической значимости. Допуск волатильности пропорционален коэффициенту вариации весов атрибуции доходности: трендовые рынки (высокий CV) получают более широкий допуск; mean-reverting рынки (низкий CV) — более узкий.

class TradingModelPruner(MedianPruner):
    """
    Financial-aware pruner that adjusts thresholds based on label entropy
    and return-attribution weighted volatility.
    """
    def __init__(
        self,
        y,
        sample_weight,       # Return-attribution weights: events['w']
        n_startup_trials: int = 10,
        n_warmup_steps: int = 2,
        multiplier: float = 1.15,
    ):
        super().__init__(n_startup_trials=n_startup_trials,
                         n_warmup_steps=n_warmup_steps)

        # Baseline entropy from return-attribution weighted label distribution
        weighted_counts = pd.Series(sample_weight).groupby(y.values).sum()
        probs = weighted_counts / weighted_counts.sum()

        if set(y.unique()) != {0, 1}:
            self.baseline_entropy = -np.sum(probs * np.log(probs))
            self.min_score_threshold = -self.baseline_entropy * multiplier
        else:
            majority_ratio = probs.max()
            self.min_score_threshold = majority_ratio / multiplier

        # Volatility tolerance scales with CV of weights:
        # trending regimes (high CV) → higher fold-score variance is acceptable
        weight_cv = np.std(sample_weight) / np.mean(sample_weight)
        self.volatility_tolerance = 0.1 * (1 + weight_cv)

    def prune(self, study, trial) -> bool:
        step = trial.last_step
        if step is None:
            return False

        if trial.number >= 5 and len(trial.intermediate_values) >= 3:
            # Rule 1: Worse than economically-weighted naive baseline?
            current_score = trial.intermediate_values.get(step)
            if trial.number > 1 and current_score < self.min_score_threshold:
                return True

            # Rule 2: Unstable across recent folds?
            recent_scores = list(trial.intermediate_values.values())[-3:]
            if np.std(recent_scores) > self.volatility_tolerance:
                return True

        # Rule 3: Standard median отсечение
        return super().prune(study, trial)

Сводное правило выбора: n_trials < 30 → используйте HyperbandPruner; n_trials ≥ 100 и выполняется хотя бы одно из условий выше → используйте TradingModelPruner; первое study на неизвестном инструменте → используйте HyperbandPruner.


7. Оркестрация и хранение

optimize_trading_model создает study, подключает pruner и sampler, соединяется с хранилище SQLite и выполняет refit. HyperbandPruner является значением по умолчанию для pruner_type. Установка n_jobs=1 у classifier перед study и восстановление -1 после refit предотвращает oversubscription: при study.optimize с параллельными процессами каждый параллельный процесс уже занимает ядро — внутренний classifier, также запрашивающий несколько ядер, вызвал бы вложенный параллелизм, который ухудшает пропускную способность вместо улучшения. Тот же принцип применяется в обертке clf_hyper_fit из Части 7.

_load_if_exists=True_ означает, что запуск, прерванный по тайм-ауту, возобновится с последнего завершенного trial, если повторно вызвать его с теми же _study_name_ и _db_path_. В сочетании с архитектурой кэширования из Части 6 это означает, что полный повторный запуск конвейер после прерывания загрузит все кэшированные результаты предобработка и возобновится ровно там, где остановилось исследование Optuna — без потери вычислений.

def optimize_trading_model(
    classifier,
    X: pd.DataFrame,
    y: pd.Series,
    events: pd.DataFrame,
    data_index: pd.DatetimeIndex,
    param_distributions: dict,
    n_trials: int = 100,
    timeout: int = 3600,
    n_splits: int = 5,
    pruner_type: str = "hyperband",
    metric: str = "neg_log_loss",
    study_name: str = None,
    db_path: str = None,
    random_state: int = 42,
    refit: bool = True,
):
    if pruner_type == "median":
        pruner = TradingModelPruner(y, sample_weight=events.loc[X.index, 'w'])
    elif pruner_type == "hyperband":
        pruner = HyperbandPruner(min_resource=1, max_resource=n_splits,
                                 reduction_factor=3)
    else:
        pruner = SuccessiveHalvingPruner()

    sampler = TPESampler(seed=random_state)
    storage_url = f"sqlite:///{db_path}.db?timeout=30"   # 30s timeout for parallel workers

    try:
        study = optuna.create_study(
            direction="maximize",
            sampler=sampler,
            pruner=pruner,
            study_name=study_name,
            storage=storage_url,
            load_if_exists=True,           # resume from last completed trial
        )

        if hasattr(classifier, 'n_jobs'):
            classifier.set_params(n_jobs=1)       # prevent oversubscription
        if hasattr(classifier, 'random_state'):
            classifier.set_params(random_state=random_state)

        def objective(trial):
            return optimize_trading_model_with_отсечение(
                trial, X, y, events, data_index,
                classifier, param_distributions, n_splits, metric
            )

        study.optimize(
            objective,
            n_trials=n_trials,
            timeout=timeout,
            callbacks=[print_best_trial, save_intermediate_results,
                       check_for_overобучения],
        )

        if refit:
            best_model = FinancialModelSuggester.apply_from_params(
                study.best_trial.params, classifier, events, data_index
            )
            if hasattr(best_model, 'n_jobs'):
                best_model.set_params(n_jobs=-1)
            best_model.fit(X, y)
            study.best_estimator_ = best_model

        cv_results = optuna_to_cv_results(study)
        return study, cv_results

    except StorageInternalError as e:
        logger.error(f"Storage Error: {db_path} is locked or unreachable. {e}")
    except Exception as e:
        raise e

Три callbacks обеспечивают прозрачность процесса, не изменяя целевую функцию. print_best_trial логирует улучшения score в консоль. save_intermediate_results записывает каждое завершенное испытание на диск как JSON в каталог optuna_results/ — легковесный журнал аудита, аналогичный logging infrastructure из Части 7. check_for_overобучения выдает предупреждение, когда score_std превышает 0.3, отмечая модели, чувствительные к тому, какой рыночный режим покрывает фолд.


8. От Optuna Study к Scikit-Learn cv_results_

Существующий аналитический код в конвейер — включая функции анализа гиперпараметров в afml.cross_validation.hyper_fit_analysis — ожидает scikit-learn cv_results_ DataFrame. Условные пространства поиска создают разреженные строки: пространство поиска xgboost включает learning_rate и gamma, которых нет в испытаниях случайного леса. optuna_to_cv_results обрабатывает это, строя DataFrame с одной строкой на завершенное испытание и заполняя отсутствующие параметры NaN. Оценки по фолдам, сохраненные в trial.user_attrs["fold_scores"], разворачиваются в split0_test_score, split1_test_score и т. д., обеспечивая тот же анализ согласованности на уровне фолдов, что доступен в нативных CV results scikit-learn.

def optuna_to_cv_results(study):
    """Converts an исследование Optuna into a Scikit-Learn style cv_results_ DataFrame."""
    rows = []
    for trial in study.trials:
        if trial.state != optuna.trial.TrialState.COMPLETE:
            continue       # exclude pruned trials from ranking

        res = {
            "mean_test_score": trial.value,
            "std_test_score": trial.user_attrs.get("score_std", 0),
            "mean_fit_time": (trial.datetime_complete -
                               trial.datetime_start).total_seconds(),
            "params": trial.params,
        }
        for k, v in trial.params.items():
            res[f"param_{k}"] = v

        fold_scores = trial.user_attrs.get("fold_scores", [])
        for i, score in enumerate(fold_scores):
            res[f"split{i}_test_score"] = score

        rows.append(res)

    return pd.DataFrame(rows)


9. Визуализация

После завершения study набор визуализаций Optuna дает диагностическое понимание процесса поиска. Все функции возвращают Plotly figures, доступные через optuna.visualization. Диаграммы ниже — репрезентативные примеры, созданные из синтетического financial ML study. Эти графики особенно ценны, потому что показывают информацию о ландшафте гиперпараметров, которую один cv_results_ DataFrame передать не может.

plot_optimization_history(study)

Показывает значение целевой функции по каждому испытанию (точки) и текущий лучший результат (линия). Разрыв между отдельными оценками испытаний и текущим лучшим сужается по мере того, как TPE строит модель поверхности целевой функции. Плоский текущий лучший результат указывает на сходимость; продолжающееся улучшение лучшего результата говорит, что нужны дополнительные испытания. На практике TPE обычно сходится за 30–60 испытаний для пространства случайного леса с 4–5 параметрами.

История оптимизации

Рис. 2 — История оптимизации

plot_intermediate_values(study)

Показывает скользящее среднее по фолдам для каждого испытания, а отсеченные испытания — короткими линиями, завершающимися раньше. Это основная диагностика для HyperbandPruner. Зеленые линии проходят все фолды и обычно группируются в верхней части диапазона score; красные линии отсекаются рано и заканчиваются на фолде 1. Если линии не отсекаются, reduction_factor может требовать настройки, либо поверхность целевой функции слишком плоская, чтобы оценки fold-1 были информативными.

Промежуточные значения (диагностика отсечения)


Рис. 3 — Промежуточные значения (диагностика отсечения)

plot_param_importances(study)

Использует fANOVA для оценки вклада каждого гиперпараметра в дисперсию целевой функции. В финансовом ML параметры регуляризации (min_weight_fraction_leaf, ccp_alpha, max_depth) обычно доминируют над параметрами емкости (n_estimators), отражая, что переобучение под training regime — основная причина неудачи. Важность weight_scheme особенно информативна: высокая важность говорит, что выбор между uniqueness и return-attribution training дает измеримую разницу для конкретного инструмента и типа стратегии.

Важность параметров

Рис. 4 — Важность параметров (fANOVA)

plot_parallel_coordinate(study)

Отображает каждое испытание как линию через параллельные оси, окрашенную по значению целевой функции. Высокоэффективные испытаний (зеленые) группируются в видимые полосы и показывают совместные взаимодействия параметров — например, что глубокое дерево улучшает производительность только в паре с сильным cost-complexity отсечение через ccp_alpha. Это лучший график для определения того, какие комбинации параметров работают вместе, а не по отдельности.

Рис. 5

Рис. 5 — Графики параллельных координат

plot_edf([study_optuna, study_random])

Строит эмпирическую CDF значений целевой функции по всем испытаниям для нескольких исследований. Study, чья EDF сдвинута вправо, стохастически доминирует другую по всему распределению испытаний. Это корректный способ показать, что Optuna превосходит RandomizedSearchCV при равном числе испытаний. Единственное сравнение best_score чувствительно к удачным выборкам и никогда не должно использоваться как единственный benchmark — EDF показывает полную картину.

vis.plot_edf требует список объектов optuna.Study. Поскольку RandomizedSearchCV создает cv_results_ DataFrame, а не study, нужен конвертер. randomized_search_to_study оборачивает каждую строку cv_results_ в завершенный optuna.Trial с использованием заглушек CategoricalDistributionplot_edf читает только trial.value, поэтому тип распределения не важен. Вспомогательная функция plot_edf_comparison выполняет преобразование и переименование traces за один вызов.

Сравнение: Optuna и RandomizedSearchCV

Рис. 6 — Сравнение EDF: Optuna и RandomizedSearchCV

import optuna.visualization as vis
from optuna.distributions import CategoricalDistribution

fig = vis.plot_optimization_history(study)
fig.show()

fig = vis.plot_intermediate_values(study)   # primary HyperbandPruner diagnostic
fig.show()

fig = vis.plot_param_importances(study)     # weight_scheme importance is informative
fig.show()

fig = vis.plot_parallel_coordinate(study)
fig.show()

def randomized_search_to_study(
    gs,
    study_name: str = "randomized_search_baseline",
) -> optuna.Study:
    """
    Convert a fitted RandomizedSearchCV into an Optuna Study for use
    with vis.plot_edf.

    vis.plot_edf requires optuna.Study objects and reads only trial.value.
    CategoricalDistribution stubs satisfy the API without affecting the plot.
    NaN mean_test_score rows (failed fits) are skipped.
    """
    study = optuna.create_study(direction="maximize", study_name=study_name)
    cv_df = pd.DataFrame(gs.cv_results_)
    param_cols = [c for c in cv_df.columns if c.startswith("param_")]

    for _, row in cv_df.iterrows():
        score = row["mean_test_score"]
        if pd.isna(score):
            continue

        params = {}
        for col in param_cols:
            val = row[col]
            # Skip NaN params from conditional search spaces
            if not (pd.isna(val) if not isinstance(val, str) else False):
                params[col.replace("param_", "")] = val

        distributions = {
            k: CategoricalDistribution([v]) for k, v in params.items()
        }
        trial = optuna.trial.create_trial(
            params=params,
            distributions=distributions,
            value=float(score),
        )
        study.add_trial(trial)

    return study

def plot_edf_comparison(study_optuna, gs_random):
    """
    Compare an исследование Optuna against a fitted RandomizedSearchCV via EDF.
    Handles the conversion and trace renaming in one call.
    """
    study_random = randomized_search_to_study(gs_random)
    fig = vis.plot_edf([study_optuna, study_random])

    for trace in fig.data:
        if "random" in trace.name.lower():
            trace.name = "RandomizedSearchCV"
        else:
            trace.name = f"Optuna ({study_optuna.study_name})"

    fig.update_layout(
        title="EDF Comparison — Optuna (TPE + Hyperband) vs RandomizedSearchCV"
    )
    return fig

# Usage:
# study, cv_results = optimize_trading_model(...)
# gs = RandomizedSearchCV(...); gs.fit(X, y, sample_weight=events['w'])
# fig = plot_edf_comparison(study, gs)
# fig.show()


10. Практические соображения

Зафиксируйте random seed в базовом estimator. Без него различия производительности между испытаниями отражают случайность, а не качество гиперпараметров. optimize_trading_model задает random_state у classifier до начала study. Это особенно важно для сравнения испытаний, которые отличаются только weight_scheme — разница должна объясняться схемой взвешивания, а не стохастичностью случайного леса.

Используйте информированные границы пространства поиска. Диапазоны вроде n_estimators в [1, 10000] тратят ранние испытания на экстремальные значения. Фабрика get_search_space предоставляет отобранные распределения как стартовую точку. Расширяйте диапазон только если plot_contour показывает, что лучшая область находится на границе текущего диапазона.

Используйте логарифмическую шкалу для мультипликативных параметров. learning_rate and ccp_alpha охватывают порядки величины. FinancialModelSuggester обрабатывает это автоматически, проверяя dist.dist.name для распределений loguniform и reciprocal и передавая log=True в trial.suggest_float.

Разрешите хотя бы один льготный фолд перед отсечением. Фолды финансовой CV покрывают разные рыночные режимы. TradingModelPruner применяет n_warmup_steps=2 до применения правила на основе дисперсии. HyperbandPruner консервативную ветвь структурно дает ту же защиту — как минимум одно испытание всегда проходит до завершения в каждой ветви.

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


Заключение

Запуск optimize_trading_model дает воспроизводимое исследование Optuna и три немедленных, проверяемых артефакта: само Study (сохраненное в SQLite, возобновляемый через load_if_exists=True и совместимый с параллельными процессами); переобученный best_estimator_ (_WeightedEstimator оборачивает настроенную базовую модель, готовую к прогнозированию); и совместимый со scikit‑learn cv_results_ DataFrame с оценками по фолдам и метаданными стабильности фолдов, подходящий для тех же последующих диагностик, которые используются во всем конвейере. Практические преимущества закрывают финансовые узкие места, указанные во введении: раннее отсечение (HyperbandPruner или, для больших studies с откалиброванными базовыми порогами, TradingModelPruner) существенно снижает общий объем вычислений, завершая безнадежные trials после одного или нескольких фолдов; постоянное хранилище SQLite и архитектура кэширования из Части 6 позволяют долгим экспериментам возобновляться без повторения дорогого предобработка; а разделенная обработка весов обучения и оценки гарантирует, что экономическая значимость учитывается и в fit, и в оценке качества.

Ключевые проектные выводы, которые можно применить сразу:

  • Используйте PurgedKFold как единственный внутреннюю CV для triple‑barrier labels и всегда сообщайте оценки по фолдам для отсечения — CPCV является инструментом бэктестинг и не дает скалярного выхода, подходящего для ранжирования комбинаций гиперпараметров.
  • Предпочитайте HyperbandPruner как вариант по умолчанию для малых и средних studies (менее ~30 trials) или первых studies на неизвестных инструментах; используйте TradingModelPruner когда у вас большой бюджет испытаний (100+) и откалиброванный базовый порог.
  • Рассматривайте схему весов обучения (unweighted, uniqueness, return) как гиперпараметр и всегда используйте веса return‑attribution (events['w']) для оценки, следуя соглашению dual‑weight из Части 5.
  • Фиксируйте случайные зерна, используйте информированные границы поиска (логарифмическую шкалу для мультипликативных параметров, таких как ccp_alpha и learning_rate), и разрешайте льготное окно перед отсечением, чтобы учитывать неоднородность режимов между фолдами.

Наконец, соблюдайте границу: направленный поиск Optuna — правильный инструмент для статистической HPO на размеченных данных; он надежно находит оптимум, потому что целевая функция (кросс‑валидированный log‑loss на triple‑barrier labels) является корректной мерой обобщения. Это не безопасная замена оптимизации стратегии на историческом P&L, где та же надежность делает его более опасным, чем случайный поиск, а не менее. Часть 8.2 продемонстрирует интеграцию этого HPO-контура в производственный конвейер через обертку clf_hyper_fit, подключение к существующей инфраструктуре кэширования и путь экспорта ONNX для развертывания в MetaTrader 5.


Прикрепленные файлы

В таблице ниже описан каждый файл, прикрепленный к этой статье, и его роль в системе HPO. Файлы из производственный модуль и модуль кросс-валидации общие с Частью 7; файлы с префиксом optuna_hyper_fit представлены здесь.

 

Файл

Модуль

Роль в этой статье

Ключевые зависимости

 1. optuna_hyper_fit.py afml.cross_validation Основной модуль HPO. Содержит FinancialModelSuggester, optimize_trading_model_with_отсечение, TradingModelPruner, optimize_trading_model, optuna_to_cv_results, а также утилиты сравнения EDF, введенные в этой статье. cross_validation.py (PurgedKFold), model_development.py (_WeightedEstimator), optuna ≥ 3.0, scikit-learn ≥ 1.3, loguru
 2. model_development.py afml.production Предоставляет _WeightedEstimator — совместимую со sklearn обертку, которая вычисляет и применяет веса обучающей выборки (uniqueness или return-attribution) внутренне, чтобы цикл целевой функции не обрабатывал их явно. Также предоставляет TickDataLoader и более широкую инфраструктуру конвейер из Части 7. utils.py (date_conversion), пакет MetaTrader5 Python, scikit-learn, pandas, numpy
 3. utils.py afml.production Служебные функции, общие для производственный модуль, включая вспомогательные функции преобразования дат, используемые TickDataLoader и процедуры валидации данных, вызываемые в точках входа пайплайна. pandas, numpy, пакет MetaTrader5 Python
 4. cross_validation.py afml.cross_validation Предоставляет PurgedKFold — единственный внутренний CV-разделитель для финансового HPO. Вызывается напрямую внутри optimize_trading_model_with_отсечение с t1 из events DataFrame и 1% embargo. Также предоставляет CombinatorialPurgedCV для внешнего цикл бэктестинга, который отличается от HPO и не используется внутри функций этой статьи. scikit-learn (BaseCrossValidator), pandas, numpy
 5. hyper_fit_analysis.py afml.cross_validation Функции пост-анализa, использующие cv_results_ DataFrame, созданный optuna_to_cv_results. Включает графики согласованности на уровне фолдов, сводки взаимодействия параметров и функцию ml_cross_val_scores_all из Части 5, установившую соглашение dual-weight оценки, на котором строится эта статья. cross_validation.py (PurgedKFold), pandas, numpy, scikit-learn, matplotlib


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

Прикрепленные файлы |
afml.zip (575.39 KB)
Автоматизация торговых стратегий в MQL5 (Часть 29): Создание системы торговли по гармоническому паттерну "Гартли" на основе Price Action Автоматизация торговых стратегий в MQL5 (Часть 29): Создание системы торговли по гармоническому паттерну "Гартли" на основе Price Action
В этой статье мы разрабатываем систему распознавания гармонических паттернов "Гартли" (Gartley) на языке MQL5, которая определяет бычьи и медвежьи гармонические паттерны "Гартли" с использованием точек разворота и уровней Фибоначчи, запуская сделки с точными уровнями входа, стоп-лосса и тейк-профита. Мы также улучшим визуальное представление паттерна с помощью графических объектов — треугольников, линий тренда и меток, которые чётко отображают структуру паттерна XABCD.
Разработка инструментария для анализа Price Action (Часть 45): Создание динамической панели для анализа уровней в MQL5 Разработка инструментария для анализа Price Action (Часть 45): Создание динамической панели для анализа уровней в MQL5
В этой статье мы рассмотрим мощный инструмент на MQL5, который позволяет тестировать любой ценовой уровень одним кликом. Просто введите нужный уровень и нажмите Analyze – советник мгновенно сканирует исторические данные, выделяет на графике все касания и пробои и выводит статистику в аккуратной информационной панели. Вы увидите, как часто цена отрабатывала этот уровень или пробивала его, а также выступал ли уровень чаще как поддержка или как сопротивление. Читайте дальше, чтобы подробнее ознакомиться с процедурой.
Архитектура машинного обучения для MetaTrader 5 (Часть 9): Интеграция байесовской оптимизации гиперпараметров в производственный пайплайн Архитектура машинного обучения для MetaTrader 5 (Часть 9): Интеграция байесовской оптимизации гиперпараметров в производственный пайплайн
В этой статье бэкенд оптимизации гиперпараметров Optuna (HPO) интегрируется в единый ModelDevelopmentPipeline. Добавлены совместная настройка гиперпараметров модели и схем весов выборки, раннее отсечение с Hyperband и отказоустойчивое SQLite-хранилище исследований. Пайплайн автоматически определяет первичные и вторичные модели, добавляет перед моделью обученный препроцессор удаления столбцов, обеспечивающий безопасный инференс, поддерживает последовательный бутстрэппинг, формирует отчет Optuna и интегрируется с bid/ask-пайплайном и LearnedStrategy. Читатели получают более быстрые, возобновляемые запуски и развертываемые самодостаточные модели.
От "лучшего прохода" к устойчивым решениям: исследование поверхности оптимизации в MetaTrader 5 От "лучшего прохода" к устойчивым решениям: исследование поверхности оптимизации в MetaTrader 5
В статье рассмотрен инженерный подход к оптимизации советника в MetaTrader 5: от сбора пользовательских метрик через Optimization Frames до анализа поверхности параметров. На простой событийной EMA/RSI‑модели показаны экспорт в CSV, сглаживание и оценка локальной стабильности в Python. Цель — находить устойчивые области конфигураций и подтверждать их форвард‑оптимизацией для надёжного внедрения.