← К списку статей
March 12, 2026
5 мин. чтения

Plateau analysis: как отличить робастный оптимум от overfitting

Plateau analysis: как отличить робастный оптимум от overfitting
#алготрейдинг
#бэктест
#оптимизация
#overfitting
#plateau analysis
#parameter stability

Статья 6 из серии «Бэктесты без иллюзий»

Вы запустили study.optimize(), Optuna нашла набор параметров с PnL +87%. Вы радуетесь и готовите стратегию к продакшену. Через две недели живой торговли PnL около нуля. Что случилось?

Оптимизатор нашёл остриё иглы в пространстве параметров. Параметры идеально подогнаны под историческую последовательность сделок — но малейшее отклонение рыночных условий разрушает всю конструкцию. Это классический overfitting, и его можно было обнаружить до запуска.

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

Почему найти «лучшие» параметры — только половина работы

Поиск в многомерном пространстве параметров Оптимизатор исследует обширный многомерный ландшафт параметров в поисках истинного оптимума

Оптимизация параметров стратегии — это поиск точки максимума в многомерном пространстве. Проблема в том, что максимум бывает двух типов:

  1. Плато (plateau) — широкая плоская область, где PnL стабильно высок при вариациях параметров. Даже если рыночные условия сдвинут эффективные параметры на 10-20%, стратегия продолжит зарабатывать.

  2. Острый пик (sharp peak) — узкая вершина, где PnL высок только при точном значении параметра. Сдвиг на один шаг обрушивает доходность. Это почти наверняка overfitting: оптимизатор нашёл артефакт исторических данных, а не устойчивую закономерность.

Метафора из альпинизма: плато — это горное плоскогорье, на котором можно безопасно ходить. Острый пик — это вершина иглы, на которой можно только балансировать.

Sharp peak vs flat plateau — визуальная интуиция

Сравнение острого пика и плоского плато Слева: робастное плато (широкая столовая гора с пологими склонами). Справа: хрупкий острый пик (кончик иглы, окружённый глубокими впадинами)

Представьте контурную карту, где оси — два параметра стратегии, а цвет — PnL. Два паттерна легко различить визуально:

Плато (робастный оптимум):

  • Широкие области одного цвета
  • Плавные переходы между уровнями PnL
  • Изолинии далеко друг от друга
  • При сдвиге от оптимума на ±20% PnL меняется не более чем на 10%

Представьте тепловую карту: в центре — яркий жёлтый прямоугольник размером примерно треть всей карты. Цвет плавно переходит в оранжевый, затем красный к краям. Оптимум — не точка, а регион.

Острый пик (overfitting):

  • Узкое яркое пятно, окружённое холодными цветами
  • Резкие переходы: рядом с оптимумом — провал
  • Изолинии сжаты в тесные кольца
  • При сдвиге на ±5% PnL падает на 50% и более

Представьте ту же тепловую карту, но в центре — крошечная жёлтая точка, немедленно окружённая синим и фиолетовым. Единственная «правильная» комбинация параметров.

Parameter sensitivity analysis

Срезы чувствительности параметров Slice-графики зависимости PnL от отдельных параметров — широкие полосы указывают на робастность, узкие кластеры — на хрупкость

Одномерный анализ: PnL vs один параметр

Простейший способ — зафиксировать все параметры, кроме одного, и посмотреть, как PnL зависит от его значения. Optuna предоставляет для этого plot_slice:

import optuna
from optuna.visualization import plot_slice

study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=500)

fig = plot_slice(study, params=["htf_entry_sell", "ltf_momentum", "stop_loss_pct"])
fig.show()

Что смотреть на slice plot:

  • Робастный параметр: облако точек образует широкую горизонтальную полосу вблизи оптимума. Лучшие trial-ы разбросаны по широкому диапазону значений параметра.
  • Хрупкий параметр: лучшие trial-ы сконцентрированы в узком диапазоне. Сдвиг параметра на один-два шага — и доходность обрушивается.

Двумерный анализ: contour plots (heatmaps)

Contour plot показывает взаимодействие двух параметров одновременно. Это ключевой инструмент plateau analysis, потому что параметры редко действуют независимо — пороги входа и выхода, таймфреймы и размеры позиции взаимосвязаны.

from optuna.visualization import plot_contour

fig = plot_contour(study, params=["htf_entry_sell", "htf_exit_buy"])
fig.show()

Contour plot для робастной пары параметров выглядит как топографическая карта холмистой равнины: плавные широкие изолинии, большие области одного цвета. Contour plot для хрупкой пары — как карта вулканического конуса: тесные концентрические кольца вокруг одной точки.

Для стратегии с 12 параметрами разделения это даёт (122)=66\binom{12}{2} = 66 попарных contour plots. Не обязательно изучать все — начните с параметров, которые Optuna оценила как наиболее важные.

Многомерный анализ: parameter importance ranking

Optuna умеет оценивать вклад каждого параметра в целевую функцию:

from optuna.visualization import plot_param_importances

fig = plot_param_importances(study)
fig.show()

График важности параметров — это горизонтальная гистограмма. Параметры ранжированы по убыванию их вклада в вариацию PnL. Первые 3-4 параметра обычно объясняют 70-80% вариации.

Правило: если параметр объясняет менее 2% вариации PnL, его значение практически не важно для результата — он робастный по определению. Фокусируйте plateau analysis на топ-5 наиболее важных параметров.

Optuna visualization tools

Контурные графики Optuna и визуализация важности параметров Контурные тепловые карты ландшафта взаимодействия параметров и рейтинг их важности

plot_slice — одномерные срезы

import optuna
from optuna.visualization import plot_slice

fig = plot_slice(study, params=[
    "htf_entry_sell", "htf_entry_buy",
    "ltf_momentum_threshold", "stop_loss_pct",
    "take_profit_pct", "trailing_stop_pct"
])
fig.update_layout(height=800, title="Parameter Slice Plots")
fig.show()

Результат — сетка точечных графиков. Каждый подграфик показывает значение целевой функции (PnL, ось Y) против значения одного параметра (ось X). Точки — отдельные trial-ы. Для робастного параметра лучшие точки (верхние по PnL) распределены по широкому диапазону X. Для хрупкого — сгруппированы в узком столбце.

plot_contour — двумерные контуры

from optuna.visualization import plot_contour

important_pairs = [
    ["htf_entry_sell", "htf_entry_buy"],
    ["htf_entry_sell", "stop_loss_pct"],
    ["ltf_momentum_threshold", "take_profit_pct"],
]

for params in important_pairs:
    fig = plot_contour(study, params=params)
    fig.update_layout(title=f"Contour: {params[0]} vs {params[1]}")
    fig.show()

Каждый contour plot — это тепловая карта с двумя параметрами по осям. Цвет кодирует средний PnL в данной области пространства параметров. Жёлтый/зелёный — высокий PnL, синий/фиолетовый — низкий. Изолинии соединяют точки с одинаковым PnL.

plot_param_importances — вклад параметров

from optuna.visualization import plot_param_importances

fig = plot_param_importances(
    study,
    evaluator=optuna.importance.FanovaImportanceEvaluator()
)
fig.show()

fANOVA (functional ANOVA) декомпозирует вариацию целевой функции по параметрам и их взаимодействиям. Это мощнее, чем простая корреляция, потому что учитывает нелинейные эффекты.

Количественные метрики плато

Визуализация количественных метрик робастности Коэффициент чувствительности, ширина плато и robustness score — три метрики для формализации качества плато

Визуальная оценка субъективна. Нужны числа. Вот три метрики, которые формализуют понятие «плато».

Sensitivity ratio

Отношение изменения PnL к изменению параметра:

Si=ΔPnL/PnLoptΔpi/pi,optS_i = \frac{\Delta \text{PnL} / \text{PnL}_{opt}}{\Delta p_i / p_{i,opt}}

где ΔPnL\Delta \text{PnL} — падение PnL при отклонении параметра pip_i от оптимума на Δpi\Delta p_i.

Интерпретация:

  • Si<0.5S_i < 0.5 — параметр робастный: 10% сдвиг даёт менее 5% падения PnL
  • 0.5Si<2.00.5 \leq S_i < 2.0 — умеренная чувствительность
  • Si2.0S_i \geq 2.0 — параметр хрупкий: 10% сдвиг обрушивает PnL на 20%+

Plateau width

Ширина области параметра, внутри которой PnL остаётся в пределах X%X\% от оптимума:

Wi(X)=pi,maxpi,minпри условииPnL(pi)(1X/100)×PnLoptW_i(X) = p_{i,max} - p_{i,min} \quad \text{при условии} \quad \text{PnL}(p_i) \geq (1 - X/100) \times \text{PnL}_{opt}

Относительная ширина плато:

Wirel(X)=Wi(X)pi,maxrangepi,minrangeW_i^{rel}(X) = \frac{W_i(X)}{p_{i,max}^{range} - p_{i,min}^{range}}

где знаменатель — полный диапазон поиска параметра.

Интерпретация:

  • Wirel(10%)>0.3W_i^{rel}(10\%) > 0.3 — плато покрывает более 30% диапазона при пороге 10%. Робастный параметр.
  • Wirel(10%)<0.05W_i^{rel}(10\%) < 0.05 — плато уже 5% диапазона. Красный флаг.

Robustness score

Комбинированная метрика по всем параметрам:

R=i=1k(Wirel(10%))wiR = \prod_{i=1}^{k} \left( W_i^{rel}(10\%) \right)^{w_i}

где wiw_i — нормализованная важность параметра ii из fANOVA (wi=1\sum w_i = 1).

Произведение взвешенных ширин — строгая метрика: если хотя бы один важный параметр имеет узкое плато, RR будет низким. Неважные параметры (с малым wiw_i) почти не влияют.

Интерпретация:

  • R>0.1R > 0.1 — стратегия робастна
  • 0.01<R0.10.01 < R \leq 0.1 — требуется дополнительная проверка (walk-forward)
  • R0.01R \leq 0.01 — overfitting весьма вероятен

Python-код для автоматизированного plateau detection

Конвейер автоматического обнаружения плато Автоматизированная система сканирования ландшафта параметров для выявления робастных плато и хрупких пиков

import numpy as np
import optuna
from optuna.importance import FanovaImportanceEvaluator
from typing import Dict, List, Tuple

def compute_sensitivity_ratio(
    study: optuna.Study,
    param_name: str,
    n_steps: int = 20,
) -> float:
    """
    Вычисляет sensitivity ratio для одного параметра.

    Фиксирует все параметры на лучших значениях, варьирует param_name,
    оценивает падение PnL через интерполяцию trial-ов.
    """
    best_trial = study.best_trial
    best_value = best_trial.values[0]
    best_param = best_trial.params[param_name]

    all_trials = [t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE]
    all_trials.sort(key=lambda t: t.values[0], reverse=True)
    top_trials = all_trials[:max(10, len(all_trials) // 5)]

    param_values = np.array([t.params[param_name] for t in top_trials])
    pnl_values = np.array([t.values[0] for t in top_trials])

    if best_param == 0 or best_value == 0:
        return float('inf')

    from numpy.polynomial import polynomial as P
    coeffs = np.polyfit(param_values, pnl_values, deg=2)
    dpnl_dparam = 2 * coeffs[0] * best_param + coeffs[1]

    sensitivity = abs(dpnl_dparam * best_param / best_value)
    return sensitivity


def compute_plateau_width(
    study: optuna.Study,
    param_name: str,
    threshold_pct: float = 10.0,
) -> Tuple[float, float]:
    """
    Вычисляет абсолютную и относительную ширину плато.

    Returns:
        (absolute_width, relative_width)
    """
    best_value = study.best_value
    threshold = best_value * (1 - threshold_pct / 100)

    trials = [t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE]
    good_trials = [t for t in trials if t.values[0] >= threshold]

    if not good_trials:
        return 0.0, 0.0

    good_params = [t.params[param_name] for t in good_trials]
    all_params = [t.params[param_name] for t in trials]

    plateau_min = min(good_params)
    plateau_max = max(good_params)
    absolute_width = plateau_max - plateau_min

    search_range = max(all_params) - min(all_params)
    relative_width = absolute_width / search_range if search_range > 0 else 0

    return absolute_width, relative_width


def compute_robustness_score(
    study: optuna.Study,
    threshold_pct: float = 10.0,
) -> Dict:
    """
    Вычисляет комбинированный robustness score.

    Returns:
        dict с per-parameter метриками и итоговым score
    """
    evaluator = FanovaImportanceEvaluator()
    importances = optuna.importance.get_param_importances(
        study, evaluator=evaluator
    )

    results = {}
    total_importance = sum(importances.values())

    for param_name, importance in importances.items():
        sensitivity = compute_sensitivity_ratio(study, param_name)
        abs_width, rel_width = compute_plateau_width(
            study, param_name, threshold_pct
        )

        weight = importance / total_importance
        results[param_name] = {
            "importance": importance,
            "weight": weight,
            "sensitivity_ratio": sensitivity,
            "plateau_width_abs": abs_width,
            "plateau_width_rel": rel_width,
        }

    log_score = sum(
        r["weight"] * np.log(max(r["plateau_width_rel"], 1e-10))
        for r in results.values()
    )
    robustness_score = np.exp(log_score)

    return {
        "robustness_score": robustness_score,
        "parameters": results,
        "verdict": (
            "robust" if robustness_score > 0.1
            else "check" if robustness_score > 0.01
            else "overfitting"
        ),
    }

Использование

report = compute_robustness_score(study, threshold_pct=10.0)

print(f"Robustness score: {report['robustness_score']:.4f}")
print(f"Verdict: {report['verdict']}")
print()

for name, metrics in report["parameters"].items():
    print(f"  {name}:")
    print(f"    Importance:       {metrics['importance']:.3f}")
    print(f"    Sensitivity:      {metrics['sensitivity_ratio']:.2f}")
    print(f"    Plateau width:    {metrics['plateau_width_rel']:.1%}")
    print()

Пример вывода:

Robustness score: 0.1482
Verdict: robust

  htf_entry_sell:
    Importance:       0.312
    Sensitivity:      0.38
    Plateau width:    42.5%

  htf_entry_buy:
    Importance:       0.251
    Sensitivity:      0.45
    Plateau width:    38.1%

  ltf_momentum_threshold:
    Importance:       0.187
    Sensitivity:      1.21
    Plateau width:    22.3%

  stop_loss_pct:
    Importance:       0.098
    Sensitivity:      0.67
    Plateau width:    31.0%

  take_profit_pct:
    Importance:       0.072
    Sensitivity:      0.89
    Plateau width:    28.4%

  trailing_delta:
    Importance:       0.031
    Sensitivity:      0.22
    Plateau width:    55.2%

Практические примеры со стратегиями разделения

Сравнение трёх стратегий по профилю робастности Сравнение Стратегии A (широкое плато, робастная), Стратегии B (умеренная) и Стратегии C (острый пик, overfitting)

Рассмотрим три стратегии с 12 параметрами разделения (separation parameters). Каждая стратегия прошла оптимизацию Optuna с 500 trial-ами.

Strategy A (~55% PnL, ~500 trades, ~15% time)

Параметры Strategy A формируют широкое плато. Возьмём ключевой параметр htf_entry_sell:

  • Оптимальное значение: 0.020
  • PnL при 0.015: +51% (падение 7%)
  • PnL при 0.025: +49% (падение 11%)
  • PnL при 0.010: +43% (падение 22%)
  • PnL при 0.030: +41% (падение 25%)

Если представить это как одномерный график (ось X — значение htf_entry_sell, ось Y — PnL), вы увидите пологую параболу с плоской вершиной. Диапазон 0.010–0.030 — это плато, где PnL остаётся в пределах ±25% от оптимума.

Sensitivity ratio: S=0.110.25=0.44S = \frac{0.11}{0.25} = 0.44 — робастный.

Plateau width при пороге 10%: от 0.013 до 0.027, Wrel=0.0140.04=35%W^{rel} = \frac{0.014}{0.04} = 35\%.

Strategy B (~25% PnL, ~40 trades, ~5% time)

Strategy B оптимизирована на малом количестве сделок. Параметр htf_entry_sell:

  • Оптимальное значение: 0.018
  • PnL при 0.015: +24% (падение 4%)
  • PnL при 0.025: +9% (падение 64%)
  • PnL при 0.012: +11% (падение 56%)

На графике — асимметричная и крутая кривая. Плато существует только в узком диапазоне 0.015–0.020. Справа от оптимума — обрыв.

Sensitivity ratio: S=0.640.39=1.64S = \frac{0.64}{0.39} = 1.64 — умеренная чувствительность, но при 40 сделках это красный флаг. Малая выборка + узкое плато = высокая вероятность overfitting.

Plateau width при пороге 10%: от 0.016 до 0.020, Wrel=0.0040.04=10%W^{rel} = \frac{0.004}{0.04} = 10\%.

Strategy C (~300% PnL, ~400 trades, ~45% time)

Strategy C показывает потрясающий PnL, но анализ плато выявляет проблемы:

  • Оптимальное значение htf_entry_sell: 0.022
  • PnL при 0.020: +295% (падение 2%)
  • PnL при 0.025: +142% (падение 53%)
  • PnL при 0.019: +128% (падение 57%)

На графике — характерная «игла»: очень высокий пик при 0.022, резкое падение во все стороны. Контурный plot покажет яркое пятно, немедленно окружённое холодными цветами.

Sensitivity ratio: S=0.530.14=3.79S = \frac{0.53}{0.14} = 3.79хрупкий. Несмотря на 400 сделок, стратегия чрезмерно зависит от точного значения одного параметра.

Plateau width при пороге 10%: от 0.021 до 0.023, Wrel=0.0020.04=5%W^{rel} = \frac{0.002}{0.04} = 5\%.

Сводная таблица

Стратегия PnL Trades Sensitivity Plateau width Robustness score Verdict
Strategy A +55% ~500 0.44 35% 0.148 Robust
Strategy B +25% ~40 1.64 10% 0.032 Check (малая выборка)
Strategy C +300% ~400 3.79 5% 0.008 Overfitting

Парадокс: Strategy C с PnL +300% имеет наихудший robustness score. Strategy A с «скромными» +55% — наиболее робастна. Это типичный результат plateau analysis: яркие цифры часто маскируют хрупкость.

Доверительные интервалы для каждой стратегии можно дополнительно проверить через Monte Carlo bootstrap — он покажет разброс PnL при ресемплинге сделок.

3D-визуализация и тепловые карты

3D-поверхность ландшафта параметров с контурной проекцией 3D-график поверхности PnL по двум параметрам с контурными линиями, спроецированными на нижнюю плоскость

Для наиболее важных пар параметров полезно построить 3D-поверхность и тепловую карту. Это даёт интуитивное понимание формы ландшафта.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D

def plot_parameter_landscape(
    study: "optuna.Study",
    param_x: str,
    param_y: str,
    grid_size: int = 50,
):
    """
    Строит 3D surface plot и heatmap для пары параметров.
    """
    trials = [t for t in study.trials
              if t.state == optuna.trial.TrialState.COMPLETE]

    x_vals = np.array([t.params[param_x] for t in trials])
    y_vals = np.array([t.params[param_y] for t in trials])
    z_vals = np.array([t.values[0] for t in trials])

    from scipy.interpolate import griddata

    xi = np.linspace(x_vals.min(), x_vals.max(), grid_size)
    yi = np.linspace(y_vals.min(), y_vals.max(), grid_size)
    Xi, Yi = np.meshgrid(xi, yi)
    Zi = griddata((x_vals, y_vals), z_vals, (Xi, Yi), method='cubic')

    fig = plt.figure(figsize=(18, 7))

    ax1 = fig.add_subplot(121, projection='3d')
    surf = ax1.plot_surface(Xi, Yi, Zi, cmap=cm.viridis, alpha=0.85,
                            edgecolor='none')
    ax1.set_xlabel(param_x)
    ax1.set_ylabel(param_y)
    ax1.set_zlabel('PnL, %')
    ax1.set_title('3D Parameter Landscape')
    fig.colorbar(surf, ax=ax1, shrink=0.5)

    ax2 = fig.add_subplot(122)
    hm = ax2.pcolormesh(Xi, Yi, Zi, cmap=cm.viridis, shading='auto')
    contours = ax2.contour(Xi, Yi, Zi, levels=10, colors='white',
                           linewidths=0.8, alpha=0.7)
    ax2.clabel(contours, inline=True, fontsize=8, fmt='%.0f%%')

    best = study.best_trial
    ax2.scatter(best.params[param_x], best.params[param_y],
                color='red', s=100, marker='*', zorder=5, label='Optimum')

    ax2.set_xlabel(param_x)
    ax2.set_ylabel(param_y)
    ax2.set_title('Contour Heatmap')
    ax2.legend()
    fig.colorbar(hm, ax=ax2)

    plt.tight_layout()
    plt.savefig(f'landscape_{param_x}_vs_{param_y}.png', dpi=150)
    plt.show()

3D surface plot для робастной стратегии напоминает столовую гору — плоская вершина с пологими склонами. Для хрупкой стратегии — остроконечный пик, как Маттерхорн. Heatmap дополняет 3D-вид, показывая ту же информацию в проекции сверху с изолиниями.

Red flags: когда результаты оптимизации подозрительны

Панель предупреждений для результатов оптимизации Индикаторы предупреждений, сигнализирующие о возможном overfitting в результатах оптимизации

Восемь признаков того, что оптимизация нашла overfitting, а не реальную закономерность:

1. Sensitivity ratio > 2 для ключевого параметра

Если PnL падает более чем на 20% при 10%-ном сдвиге параметра — оптимум хрупкий.

2. Plateau width < 10% диапазона поиска

Если «хорошая» область занимает менее 10% от исследованного диапазона — оптимизатор, вероятнее всего, нашёл артефакт.

3. Топ-3 trial-а дают PnL в 2-3 раза выше медианы

Если лучшие trial-ы — выбросы на фоне остальных, а не «верхушка холма» — это не плато.

top_3_mean = np.mean(sorted([t.values[0] for t in study.trials
                              if t.state == optuna.trial.TrialState.COMPLETE],
                             reverse=True)[:3])
median_pnl = np.median([t.values[0] for t in study.trials
                         if t.state == optuna.trial.TrialState.COMPLETE])

outlier_ratio = top_3_mean / median_pnl
if outlier_ratio > 2.5:
    print(f"WARNING: Top trials are {outlier_ratio:.1f}x above median — possible overfitting")

4. Малое количество сделок (< 50) при высоком PnL

Малая выборка + высокий PnL = высокая дисперсия оценки. Plateau analysis на 40 сделках ненадёжен сам по себе. Для таких стратегий критически важен Monte Carlo bootstrap.

5. Одна «магическая» комбинация параметров

Если contour plot показывает единственную яркую точку среди серого поля — это не стратегия, а подогнанная под данные комбинация.

6. Слишком много параметров

Для 12 параметров с 10 значениями каждый пространство поиска содержит 101210^{12} комбинаций. Optuna исследует ~500. Вероятность найти «хороший» артефакт в таком пространстве — высока. Чем больше параметров, тем строже должен быть plateau analysis.

7. PnL резко падает на out-of-sample

Если in-sample PnL +87%, а walk-forward показывает +12% — оптимизация подогнала параметры под тренировочный период. Подробнее об этом — в статье о Walk-Forward оптимизации.

8. Параметры «прижаты» к границам диапазона

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

Автоматический отчёт plateau analysis

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

import json
from datetime import datetime

def generate_plateau_report(
    study: "optuna.Study",
    strategy_name: str,
    n_trades: int,
    threshold_pct: float = 10.0,
) -> dict:
    """
    Генерирует полный отчёт plateau analysis.
    """
    robustness = compute_robustness_score(study, threshold_pct)

    red_flags = []

    sorted_params = sorted(
        robustness["parameters"].items(),
        key=lambda x: x[1]["importance"],
        reverse=True
    )
    for name, metrics in sorted_params[:3]:
        if metrics["sensitivity_ratio"] > 2.0:
            red_flags.append(
                f"High sensitivity for {name}: "
                f"S={metrics['sensitivity_ratio']:.2f}"
            )

    for name, metrics in robustness["parameters"].items():
        if metrics["plateau_width_rel"] < 0.05:
            red_flags.append(
                f"Narrow plateau for {name}: "
                f"W={metrics['plateau_width_rel']:.1%}"
            )

    all_values = sorted(
        [t.values[0] for t in study.trials
         if t.state == optuna.trial.TrialState.COMPLETE],
        reverse=True
    )
    if len(all_values) > 10:
        top3 = np.mean(all_values[:3])
        med = np.median(all_values)
        if med > 0 and top3 / med > 2.5:
            red_flags.append(
                f"Top trials are outliers: "
                f"{top3:.1f} vs median {med:.1f} "
                f"({top3/med:.1f}x)"
            )

    if n_trades < 50:
        red_flags.append(f"Low trade count: {n_trades}")

    report = {
        "strategy": strategy_name,
        "timestamp": datetime.now().isoformat(),
        "best_pnl": study.best_value,
        "n_trials": len(study.trials),
        "n_trades": n_trades,
        "robustness_score": robustness["robustness_score"],
        "verdict": robustness["verdict"],
        "red_flags": red_flags,
        "parameters": robustness["parameters"],
    }

    return report


report = generate_plateau_report(
    study, strategy_name="Strategy A", n_trades=491
)

print(json.dumps(report, indent=2, default=str))

Пример вывода:

{
  "strategy": "Strategy A",
  "best_pnl": 55.2,
  "n_trials": 500,
  "n_trades": 491,
  "robustness_score": 0.1482,
  "verdict": "robust",
  "red_flags": [],
  "parameters": {
    "htf_entry_sell": {
      "importance": 0.312,
      "sensitivity_ratio": 0.44,
      "plateau_width_rel": 0.35
    }
  }
}

Связь с Walk-Forward Validation

Walk-forward валидация как дополнение к plateau analysis Параметрическая робастность (plateau analysis) и временная робастность (walk-forward) как две комплементарные системы валидации

Plateau analysis и walk-forward validation (WFO) — комплементарные методы:

  • Plateau analysis отвечает на вопрос: «Насколько устойчив оптимум к небольшим сдвигам параметров?» Это проверка параметрической робастности.
  • Walk-forward отвечает на вопрос: «Работают ли параметры на данных, которых оптимизатор не видел?» Это проверка временной робастности.

Стратегия может пройти plateau analysis (широкое плато), но провалить walk-forward (рыночный режим изменился). И наоборот — может пройти walk-forward на фиксированных параметрах, но иметь хрупкий оптимум.

Рекомендация: всегда используйте оба метода. Если стратегия прошла plateau analysis (R>0.1R > 0.1) и walk-forward (PnLOOS>50%×PnLIS\text{PnL}_{OOS} > 50\% \times \text{PnL}_{IS}) — это сильный сигнал робастности. Подробнее — в статье о Walk-Forward оптимизации.

Для оценки доверительных интервалов PnL на каждом этапе применяйте Monte Carlo bootstrap. А для корректного сравнения стратегий с разным активным временем — метрику PnL по активному времени.

Рекомендации

Перед оптимизацией

  1. Ограничьте число параметров. Чем меньше параметров — тем надёжнее плато. 5-7 параметров — разумный максимум. 12 — уже требует повышенной осторожности.

  2. Задавайте осмысленные диапазоны. Не ставьте htf_entry_sell от 0.001 до 1.0, если реалистичный диапазон — 0.005 до 0.05. Широкие бессмысленные диапазоны создают иллюзию плато.

  3. Используйте достаточно trial-ов. Для 12 параметров минимум 300-500 trial-ов. Для надёжного plateau analysis — 1000+.

Во время оптимизации

  1. Следите за convergence. Если Optuna продолжает находить значительно лучшие решения после 400 trial-ов — процесс не сошёлся, и plateau analysis будет ненадёжным.

  2. Используйте pruning с осторожностью. Aggressive pruning (MedianPruner) может отсекать trial-ы, которые выглядят плохо на начальных шагах, но важны для построения полной картины ландшафта.

После оптимизации

  1. Генерируйте plateau report автоматически. Встройте generate_plateau_report() в пайплайн оптимизации. Не полагайтесь на визуальную оценку — используйте числа.

  2. Проверяйте top-5 параметров. Если fANOVA показывает, что 3 параметра объясняют 80% вариации — остальные 9 можно проверять менее тщательно.

  3. Сравнивайте с базовой стратегией. Если стратегия с дефолтными параметрами (без оптимизации) показывает +30%, а оптимизированная +55% — разница всего 25 п.п., и плато, скорее всего, широкое. Если дефолтная — 0%, а оптимизированная +300% — вся доходность зависит от точной подгонки параметров.

  4. Финальная проверка — walk-forward. Plateau analysis — необходимое, но недостаточное условие робастности. Обязательно валидируйте out-of-sample.

Заключение

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

Три правила plateau analysis:

  1. Считайте robustness score. Произведение взвешенных ширин плато даёт одно число, которое суммирует робастность всех параметров. R>0.1R > 0.1 — зелёный свет.

  2. Sensitivity ratio < 1 для ключевых параметров. Если 10% сдвиг параметра вызывает менее 10% падение PnL — параметр робастный. Если больше — будьте осторожны.

  3. Визуализируйте contour plots. Никакая метрика не заменит понимания формы ландшафта. Плоская столовая гора — хорошо. Остроконечная игла — плохо.

Plateau analysis занимает 5 минут после оптимизации и может сэкономить недели убыточной торговли в продакшене. Это обязательный шаг между study.optimize() и запуском бота.


Полезные ссылки

  1. Optuna Documentation — Visualization
  2. Hutter, F., Hoos, H., Leyton-Brown, K. — An Efficient Approach for Assessing Hyperparameter Importance (fANOVA, 2014)
  3. Pardo, R. — The Evaluation and Optimization of Trading Strategies
  4. Marcos Lopez de Prado — Advances in Financial Machine Learning, Chapter 11: Dangers of Backtesting
  5. Bailey, D.H. et al. — The Probability of Backtest Overfitting (2015)
  6. Optuna — optuna.visualization.plot_contour
  7. Optuna — optuna.importance.FanovaImportanceEvaluator
  8. Bergstra, J. & Bengio, Y. — Random Search for Hyper-Parameter Optimization (2012)

Цитирование

@article{soloviov2026plateauanalysis,
  author = {Soloviov, Eugen},
  title = {Plateau analysis: как отличить робастный оптимум от overfitting},
  year = {2026},
  url = {https://marketmaker.cc/ru/blog/post/plateau-analysis-overfitting},
  version = {0.1.0},
  description = {Почему найти лучшие параметры стратегии — только половина работы. Как визуально и количественно отличить устойчивое плато от хрупкого пика, и почему Optuna contour plots — обязательный шаг перед запуском оптимизированной стратегии в продакшен.}
}
Дисклеймер: Информация в этой статье предоставлена исключительно в образовательных и ознакомительных целях и не является финансовым, инвестиционным или торговым советом. Торговля криптовалютами сопряжена с высоким риском убытков.

MarketMaker.cc Team

Количественные исследования и стратегии

Обсудить в Telegram
Newsletter

Будьте в курсе событий

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

Мы уважаем вашу конфиденциальность. Отписаться можно в любой момент.