← Volver a los artículos
March 12, 2026
5 min de lectura

Análisis de meseta: cómo distinguir un óptimo robusto del overfitting

Análisis de meseta: cómo distinguir un óptimo robusto del overfitting
#algotrading
#backtest
#optimization
#overfitting
#plateau analysis
#parameter stability
🎯
Part 4 of 9 · Collection
Backtesting Without Fooling Yourself

Artículo 6 de la serie "Backtests sin ilusiones"

📄 Este artículo se convirtió en un paper de investigación. Las métricas están formalizadas y sometidas a pruebas de estrés con simulaciones reproducibles, comparadas con la Probability of Backtest Overfitting y el Deflated Sharpe Ratio. Lee el paper online (versión interactiva + PDF) en plateau.marketmaker.cc, código y datos en github.com/suenot/plateau-robustness.

Ejecutaste study.optimize(), Optuna encontró un conjunto de parámetros con PnL +87%. Estás entusiasmado y preparas la estrategia para producción. Dos semanas de trading en vivo después, el PnL está cerca de cero. ¿Qué pasó?

El optimizador encontró la punta de una aguja en el espacio de parámetros. Los parámetros están perfectamente ajustados a la secuencia histórica de operaciones — pero la más mínima desviación en las condiciones de mercado destruye todo el constructo. Esto es overfitting clásico, y podría haberse detectado antes del lanzamiento.

En el artículo anterior comparamos coordinate descent con optimización bayesiana y mostramos por qué Optuna encuentra el óptimo de forma más eficiente. Hoy — el siguiente paso: cómo asegurarse de que el óptimo encontrado es robusto, en lugar de ser el resultado de ajustarse al ruido.

Por qué encontrar los "mejores" parámetros es solo la mitad del trabajo

Búsqueda en el espacio de parámetros multidimensional Un optimizador navegando por un vasto paisaje de parámetros multidimensional en busca del verdadero óptimo

La optimización de parámetros de una estrategia es una búsqueda de un máximo en un espacio multidimensional. El problema es que los máximos vienen en dos tipos:

  1. Meseta — una región amplia y plana donde el PnL se mantiene consistentemente alto ante variaciones de parámetros. Incluso si las condiciones de mercado desplazan los parámetros efectivos en un 10-20%, la estrategia seguirá siendo rentable.

  2. Pico agudo — una cumbre estrecha donde el PnL es alto solo en el valor exacto del parámetro. Un desplazamiento de un paso colapsa la rentabilidad. Esto es casi con certeza overfitting: el optimizador encontró un artefacto de los datos históricos, no un patrón estable.

Una metáfora alpinista: una meseta es un altiplano de montaña donde puedes caminar con seguridad. Un pico agudo es la punta de una aguja donde solo puedes hacer equilibrio.

Pico agudo vs. meseta plana — intuición visual

Comparación de pico agudo versus meseta plana Izquierda: una meseta robusta (una amplia montaña de mesa con pendientes suaves). Derecha: un pico agudo frágil (punta de aguja rodeada de valles profundos)

Imagina un mapa de contorno donde los ejes son dos parámetros de la estrategia y el color representa el PnL. Dos patrones son fáciles de distinguir visualmente:

Meseta (óptimo robusto):

  • Áreas amplias del mismo color
  • Transiciones suaves entre niveles de PnL
  • Isolíneas muy separadas
  • Desplazarse del óptimo en +/-20% cambia el PnL en no más del 10%

Imagina un mapa de calor: en el centro — un rectángulo amarillo brillante que ocupa aproximadamente un tercio del mapa completo. El color transiciona gradualmente a naranja, luego a rojo hacia los bordes. El óptimo no es un punto, sino una región.

Pico agudo (overfitting):

  • Un punto brillante estrecho rodeado de colores fríos
  • Transiciones abruptas: un colapso justo al lado del óptimo
  • Isolíneas comprimidas en anillos estrechos
  • Un desplazamiento de +/-5% reduce el PnL en un 50% o más

Imagina el mismo mapa de calor, pero en el centro — un diminuto punto amarillo inmediatamente rodeado de azul y morado. Una única combinación de parámetros "correcta".

Análisis de sensibilidad de parámetros

Gráficos de corte de sensibilidad de parámetros Gráficos de corte que muestran cómo depende el PnL de los valores de parámetros individuales — las bandas anchas indican robustez, los clusters estrechos indican fragilidad

Análisis unidimensional: PnL frente a un parámetro

El enfoque más simple — fijar todos los parámetros excepto uno y ver cómo depende el PnL de su valor. Optuna proporciona plot_slice para esto:

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

Qué buscar en un slice plot:

  • Parámetro robusto: la nube de puntos forma una banda horizontal amplia cerca del óptimo. Los mejores trials están distribuidos en un amplio rango de valores del parámetro.
  • Parámetro frágil: los mejores trials se concentran en un rango estrecho. Desplazar el parámetro uno o dos pasos — y la rentabilidad colapsa.

Análisis bidimensional: gráficos de contorno (heatmaps)

Un gráfico de contorno muestra la interacción de dos parámetros simultáneamente. Esta es la herramienta clave para el análisis de meseta, porque los parámetros rara vez actúan de forma independiente — los umbrales de entrada y salida, los timeframes y el tamaño de posición están interconectados.

from optuna.visualization import plot_contour

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

Un gráfico de contorno para un par de parámetros robusto se parece a un mapa topográfico de una llanura ondulada: isolíneas suaves y amplias, grandes áreas del mismo color. Un gráfico de contorno para un par frágil — como un mapa de un cono volcánico: anillos concéntricos apretados alrededor de un único punto.

Para una estrategia con 12 parámetros de separación, esto da (122)=66\binom{12}{2} = 66 gráficos de contorno por pares. No hace falta estudiarlos todos — empieza por los parámetros que Optuna calificó como más importantes.

Análisis multidimensional: ranking de importancia de parámetros

Optuna puede estimar la contribución de cada parámetro a la función objetivo:

from optuna.visualization import plot_param_importances

fig = plot_param_importances(study)
fig.show()

El gráfico de importancia de parámetros es un histograma horizontal. Los parámetros se clasifican por su contribución a la varianza del PnL en orden descendente. Los 3-4 parámetros principales suelen explicar el 70-80% de la varianza.

Regla: si un parámetro explica menos del 2% de la varianza del PnL, su valor es prácticamente irrelevante para el resultado — es robusto por definición. Concentra el análisis de meseta en los 5 parámetros más importantes.

Herramientas de visualización de Optuna

Gráficos de contorno de Optuna y visualización de importancia de parámetros Heatmaps de contorno que muestran el paisaje de interacción de parámetros junto con los rankings de importancia

plot_slice — Cortes unidimensionales

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

El resultado — una cuadrícula de gráficos de dispersión. Cada subgráfico muestra el valor de la función objetivo (PnL, eje Y) frente a un único valor de parámetro (eje X). Los puntos son trials individuales. Para un parámetro robusto, los mejores puntos (mayor PnL) se distribuyen en un amplio rango de X. Para uno frágil — agrupados en una columna estrecha.

plot_contour — Contornos bidimensionales

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

Cada gráfico de contorno es un heatmap con dos parámetros en los ejes. El color codifica el PnL promedio en una región dada del espacio de parámetros. Amarillo/verde — PnL alto, azul/morado — bajo. Las isolíneas conectan puntos con el mismo PnL.

plot_param_importances — Contribuciones de parámetros

from optuna.visualization import plot_param_importances

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

fANOVA (functional ANOVA) descompone la varianza de la función objetivo entre parámetros y sus interacciones. Esto es más potente que una simple correlación porque tiene en cuenta efectos no lineales.

Métricas cuantitativas de meseta

Visualización de métricas cuantitativas de robustez Sensitivity ratio, ancho de meseta y robustness score — tres métricas que formalizan la calidad de la meseta

La evaluación visual es subjetiva. Necesitamos números. Aquí hay tres métricas que formalizan el concepto de "meseta".

Sensitivity ratio

El ratio del cambio de PnL respecto al cambio del parámetro:

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

donde ΔPnL\Delta \text{PnL} es la caída de PnL cuando el parámetro pip_i se desvía del óptimo en Δpi\Delta p_i.

Interpretación:

  • Si<0.5S_i < 0.5 — el parámetro es robusto: un desplazamiento del 10% causa menos del 5% de caída en el PnL
  • 0.5Si<2.00.5 \leq S_i < 2.0 — sensibilidad moderada
  • Si2.0S_i \geq 2.0 — el parámetro es frágil: un desplazamiento del 10% derrumba el PnL en un 20%+

Ancho de meseta

El ancho de la región del parámetro dentro de la cual el PnL se mantiene dentro de X%X\% del óptimo:

Wi(X)=pi,maxpi,minsubject toPnL(pi)(1X/100)×PnLoptW_i(X) = p_{i,max} - p_{i,min} \quad \text{subject to} \quad \text{PnL}(p_i) \geq (1 - X/100) \times \text{PnL}_{opt}

Ancho relativo de meseta:

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

donde el denominador es el rango de búsqueda completo del parámetro.

Interpretación:

  • Wirel(10%)>0.3W_i^{rel}(10\%) > 0.3 — la meseta cubre más del 30% del rango en el umbral del 10%. Parámetro robusto.
  • Wirel(10%)<0.05W_i^{rel}(10\%) < 0.05 — la meseta es más estrecha que el 5% del rango. Señal de alarma.

Robustness score

Una métrica combinada sobre todos los parámetros:

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

donde wiw_i es la importancia normalizada del parámetro ii según fANOVA (wi=1\sum w_i = 1).

El producto de anchos ponderados es una métrica estricta: si incluso un solo parámetro importante tiene una meseta estrecha, RR será bajo. Los parámetros no importantes (con wiw_i pequeño) apenas tienen efecto.

Interpretación:

  • R>0.1R > 0.1 — la estrategia es robusta
  • 0.01<R0.10.01 < R \leq 0.1 — se requiere validación adicional (walk-forward)
  • R0.01R \leq 0.01 — el overfitting es muy probable

Código Python para detección automatizada de mesetas

Pipeline automatizado de detección de mesetas Sistema automatizado que escanea el paisaje de parámetros para identificar mesetas robustas y picos frágiles

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:
    """
    Compute sensitivity ratio for a single parameter.

    Fixes all parameters at their best values, varies param_name,
    estimates PnL drop through trial interpolation.
    """
    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]:
    """
    Compute absolute and relative plateau width.

    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:
    """
    Compute combined robustness score.

    Returns:
        dict with per-parameter metrics and the final 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"
        ),
    }

Uso

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

Ejemplo de salida:

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%

Ejemplos prácticos con estrategias de separación

Tres estrategias comparadas por perfil de robustez Comparación de la Estrategia A (meseta amplia, robusta), la Estrategia B (moderada) y la Estrategia C (pico agudo, sobreajustada)

Examinemos tres estrategias con 12 parámetros de separación. Cada estrategia pasó por una optimización de Optuna con 500 trials.

Estrategia A (~55% PnL, ~500 trades, ~15% de tiempo)

Los parámetros de la Estrategia A forman una meseta amplia. Toma el parámetro clave htf_entry_sell:

  • Valor óptimo: 0.020
  • PnL en 0.015: +51% (caída del 7%)
  • PnL en 0.025: +49% (caída del 11%)
  • PnL en 0.010: +43% (caída del 22%)
  • PnL en 0.030: +41% (caída del 25%)

Si imaginas esto como un gráfico unidimensional (eje X — valor de htf_entry_sell, eje Y — PnL), verás una parábola suave con una parte superior plana. El rango 0.010-0.030 es la meseta, donde el PnL se mantiene dentro de +/-25% del óptimo.

Sensitivity ratio: S=0.110.25=0.44S = \frac{0.11}{0.25} = 0.44 — robusto.

Ancho de meseta en el umbral del 10%: de 0.013 a 0.027, Wrel=0.0140.04=35%W^{rel} = \frac{0.014}{0.04} = 35\%.

Estrategia B (~25% PnL, ~40 trades, ~5% de tiempo)

La Estrategia B está optimizada sobre un número pequeño de trades. Parámetro htf_entry_sell:

  • Valor óptimo: 0.018
  • PnL en 0.015: +24% (caída del 4%)
  • PnL en 0.025: +9% (caída del 64%)
  • PnL en 0.012: +11% (caída del 56%)

En el gráfico — una curva asimétrica y pronunciada. La meseta existe solo en el rango estrecho 0.015-0.020. A la derecha del óptimo — un acantilado.

Sensitivity ratio: S=0.640.39=1.64S = \frac{0.64}{0.39} = 1.64 — sensibilidad moderada, pero con 40 trades esto es una señal de alarma. Muestra pequeña + meseta estrecha = alta probabilidad de overfitting.

Ancho de meseta en el umbral del 10%: de 0.016 a 0.020, Wrel=0.0040.04=10%W^{rel} = \frac{0.004}{0.04} = 10\%.

Estrategia C (~300% PnL, ~400 trades, ~45% de tiempo)

La Estrategia C muestra un PnL asombroso, pero el análisis de meseta revela problemas:

  • Valor óptimo de htf_entry_sell: 0.022
  • PnL en 0.020: +295% (caída del 2%)
  • PnL en 0.025: +142% (caída del 53%)
  • PnL en 0.019: +128% (caída del 57%)

En el gráfico — una "aguja" característica: un pico muy alto en 0.022, caída abrupta en todas direcciones. El gráfico de contorno mostraría un punto brillante inmediatamente rodeado de colores fríos.

Sensitivity ratio: S=0.530.14=3.79S = \frac{0.53}{0.14} = 3.79frágil. A pesar de 400 trades, la estrategia depende excesivamente del valor exacto de un único parámetro.

Ancho de meseta en el umbral del 10%: de 0.021 a 0.023, Wrel=0.0020.04=5%W^{rel} = \frac{0.002}{0.04} = 5\%.

Tabla resumen

Estrategia PnL Trades Sensitivity Ancho de meseta Robustness score Veredicto
Estrategia A +55% ~500 0.44 35% 0.148 Robusta
Estrategia B +25% ~40 1.64 10% 0.032 Revisar (muestra pequeña)
Estrategia C +300% ~400 3.79 5% 0.008 Overfitting

Paradoja: la Estrategia C con PnL +300% tiene el peor robustness score. La Estrategia A con un "modesto" +55% es la más robusta. Este es un resultado típico del análisis de meseta: los números impresionantes a menudo enmascaran fragilidad.

Los intervalos de confianza de cada estrategia pueden verificarse adicionalmente mediante Monte Carlo bootstrap — mostrará la dispersión del PnL al remuestrear trades.

Visualización 3D y heatmaps

Superficie de paisaje de parámetros 3D con proyección de contorno Gráfico de superficie 3D del PnL sobre dos parámetros con líneas de contorno proyectadas en el plano del suelo

Para los pares de parámetros más importantes, es útil construir una superficie 3D y un heatmap. Esto proporciona una comprensión intuitiva de la forma del paisaje.

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,
):
    """
    Build a 3D surface plot and heatmap for a pair of parameters.
    """
    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()

Un gráfico de superficie 3D para una estrategia robusta se asemeja a una montaña de mesa — una cumbre plana con pendientes suaves. Para una estrategia frágil — un pico agudo, como el Cervino. El heatmap complementa la vista 3D, mostrando la misma información en una proyección cenital con isolíneas.

Señales de alarma: cuándo los resultados de optimización son sospechosos

Panel de señales de alarma para resultados de optimización Indicadores de advertencia que señalan un posible overfitting en los resultados de optimización

Ocho señales de que la optimización encontró overfitting en lugar de un patrón real:

1. Sensitivity ratio > 2 para un parámetro clave

Si el PnL cae más del 20% con un desplazamiento del 10% del parámetro — el óptimo es frágil.

2. Ancho de meseta < 10% del rango de búsqueda

Si la región "buena" ocupa menos del 10% del rango explorado — lo más probable es que el optimizador haya encontrado un artefacto.

3. Los top-3 trials producen PnL 2-3x por encima de la mediana

Si los mejores trials son outliers respecto al resto en lugar de la "cima de la colina" — no es una meseta.

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. Bajo número de trades (< 50) con PnL alto

Muestra pequeña + PnL alto = alta varianza en la estimación. El análisis de meseta con 40 trades es poco fiable en sí mismo. Para tales estrategias, Monte Carlo bootstrap es crítico.

5. Una combinación de parámetros "mágica"

Si el gráfico de contorno muestra un único punto brillante en medio de un campo gris — esto no es una estrategia, es una combinación ajustada a los datos.

6. Demasiados parámetros

Para 12 parámetros con 10 valores cada uno, el espacio de búsqueda contiene 101210^{12} combinaciones. Optuna explora ~500. La probabilidad de encontrar un artefacto "bueno" en tal espacio es alta. Cuantos más parámetros, más estricto debe ser el análisis de meseta.

7. El PnL cae bruscamente fuera de muestra

Si el PnL in-sample es +87% y el walk-forward muestra +12% — la optimización ajustó los parámetros al período de entrenamiento. Más sobre esto en el artículo de optimización Walk-Forward.

8. Los parámetros quedan "clavados" en los límites del rango

Si el valor óptimo coincide con el límite de la cuadrícula de búsqueda — el óptimo puede estar más allá del rango. Amplía el rango y vuelve a ejecutar la optimización.

Informe automatizado de análisis de meseta

Uniendo todo en un único informe generado después de cada optimización:

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:
    """
    Generate a complete plateau analysis report.
    """
    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))

Ejemplo de salida:

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

Relación con la validación walk-forward

Validación walk-forward complementando el análisis de meseta Robustez paramétrica (análisis de meseta) y robustez temporal (walk-forward) como dos sistemas de validación complementarios

El análisis de meseta y la validación walk-forward (WFO) son métodos complementarios:

  • El análisis de meseta responde a la pregunta: "¿Qué tan estable es el óptimo ante pequeños desplazamientos de parámetros?" Esto es una comprobación de la robustez paramétrica.
  • Walk-forward responde a la pregunta: "¿Funcionan los parámetros con datos que el optimizador no ha visto?" Esto es una comprobación de la robustez temporal.

Una estrategia puede pasar el análisis de meseta (meseta amplia) pero fallar en walk-forward (el régimen de mercado cambió). Y viceversa — puede pasar walk-forward con parámetros fijos pero tener un óptimo frágil.

Recomendación: usa siempre ambos métodos. Si una estrategia pasa el análisis de meseta (R>0.1R > 0.1) y el walk-forward (PnLOOS>50%×PnLIS\text{PnL}_{OOS} > 50\% \times \text{PnL}_{IS}) — es una señal fuerte de robustez. Más detalles en el artículo de optimización Walk-Forward.

Para evaluar los intervalos de confianza del PnL en cada etapa, aplica Monte Carlo bootstrap. Y para comparar correctamente estrategias con diferente tiempo activo, usa la métrica PnL por tiempo activo.

Recomendaciones

Antes de la optimización

  1. Limita el número de parámetros. Cuantos menos parámetros — más fiable la meseta. 5-7 parámetros es un máximo razonable. Con 12 ya se requiere mayor precaución.

  2. Establece rangos con sentido. No pongas htf_entry_sell de 0.001 a 1.0 si el rango realista es de 0.005 a 0.05. Los rangos innecesariamente amplios crean la ilusión de una meseta.

  3. Usa suficientes trials. Para 12 parámetros, un mínimo de 300-500 trials. Para un análisis de meseta fiable — 1000+.

Durante la optimización

  1. Vigila la convergencia. Si Optuna sigue encontrando soluciones significativamente mejores después de 400 trials — el proceso no ha convergido, y el análisis de meseta no será fiable.

  2. Usa el pruning con precaución. El pruning agresivo (MedianPruner) puede eliminar trials que parecen malos en los primeros pasos pero que son importantes para construir una imagen completa del paisaje.

Después de la optimización

  1. Genera el informe de meseta automáticamente. Integra generate_plateau_report() en el pipeline de optimización. No confíes en la evaluación visual — usa números.

  2. Revisa los 5 parámetros principales. Si fANOVA muestra que 3 parámetros explican el 80% de la varianza — los 9 restantes se pueden revisar de forma menos exhaustiva.

  3. Compara con la estrategia baseline. Si la estrategia con parámetros por defecto (sin optimización) muestra +30%, y la optimizada +55% — la diferencia es de solo 25 puntos porcentuales, y la meseta probablemente sea amplia. Si el default muestra 0%, y el optimizado +300% — toda la rentabilidad depende del ajuste preciso de parámetros.

  4. Comprobación final — walk-forward. El análisis de meseta es una condición necesaria pero no suficiente para la robustez. Valida siempre out-of-sample.

Conclusión

La optimización de parámetros es una herramienta poderosa, pero sin análisis de meseta es un juego de ruleta. No sabes si has encontrado un patrón estable o has ajustado el modelo al ruido.

Tres reglas del análisis de meseta:

  1. Calcula el robustness score. El producto de anchos de meseta ponderados da un único número que resume la robustez de todos los parámetros. R>0.1R > 0.1 — luz verde.

  2. Sensitivity ratio < 1 para los parámetros clave. Si un desplazamiento del 10% del parámetro causa menos del 10% de caída del PnL — el parámetro es robusto. Si es más — ten cuidado.

  3. Visualiza los gráficos de contorno. Ninguna métrica puede reemplazar la comprensión de la forma del paisaje. Una montaña de mesa plana — bien. Una aguja afilada — mal.

El análisis de meseta toma 5 minutos después de la optimización y puede ahorrar semanas de trading en vivo no rentable. Es un paso obligatorio entre study.optimize() y el lanzamiento del bot.


Enlaces útiles

  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)

Cita

@article{soloviov2026plateauanalysis,
  author = {Soloviov, Eugen},
  title = {Plateau Analysis: How to Distinguish a Robust Optimum from Overfitting},
  year = {2026},
  url = {https://marketmaker.cc/en/blog/post/plateau-analysis-overfitting},
  version = {0.1.0},
  description = {Why finding the best strategy parameters is only half the work. How to visually and quantitatively distinguish a stable plateau from a fragile peak, and why Optuna contour plots are a mandatory step before launching an optimized strategy into production.}
}
blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Mantente a la vanguardia

Suscríbete a nuestro boletín para recibir información exclusiva sobre trading con IA, análisis de mercado y actualizaciones de la plataforma.

Respetamos tu privacidad. Puedes darte de baja en cualquier momento.