Análisis de meseta: cómo distinguir un óptimo robusto del overfitting
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
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:
-
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.
-
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
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 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 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
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
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:
donde es la caída de PnL cuando el parámetro se desvía del óptimo en .
Interpretación:
- — el parámetro es robusto: un desplazamiento del 10% causa menos del 5% de caída en el PnL
- — sensibilidad moderada
- — 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 del óptimo:
Ancho relativo de meseta:
donde el denominador es el rango de búsqueda completo del parámetro.
Interpretación:
- — la meseta cubre más del 30% del rango en el umbral del 10%. Parámetro robusto.
- — 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:
donde es la importancia normalizada del parámetro según fANOVA ().
El producto de anchos ponderados es una métrica estricta: si incluso un solo parámetro importante tiene una meseta estrecha, será bajo. Los parámetros no importantes (con pequeño) apenas tienen efecto.
Interpretación:
- — la estrategia es robusta
- — se requiere validación adicional (walk-forward)
- — el overfitting es muy probable
Código Python para detección automatizada 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
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: — robusto.
Ancho de meseta en el umbral del 10%: de 0.013 a 0.027, .
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: — 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, .
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: — frá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, .
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
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
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 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
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 () y el walk-forward () — 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
-
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.
-
Establece rangos con sentido. No pongas
htf_entry_sellde 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. -
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
-
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.
-
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
-
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. -
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.
-
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.
-
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:
-
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. — luz verde.
-
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.
-
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
- Optuna Documentation — Visualization
- Hutter, F., Hoos, H., Leyton-Brown, K. — An Efficient Approach for Assessing Hyperparameter Importance (fANOVA, 2014)
- Pardo, R. — The Evaluation and Optimization of Trading Strategies
- Marcos Lopez de Prado — Advances in Financial Machine Learning, Chapter 11: Dangers of Backtesting
- Bailey, D.H. et al. — The Probability of Backtest Overfitting (2015)
- Optuna — optuna.visualization.plot_contour
- Optuna — optuna.importance.FanovaImportanceEvaluator
- 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.}
}
Authors
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.