Análise de plateau: como distinguir um ótimo robusto de overfitting
Artigo 6 da série "Backtests sem ilusões"
📄 Este artigo se transformou em um paper de pesquisa. As métricas estão formalizadas e testadas sob estresse com simulações reprodutíveis, comparadas com a Probability of Backtest Overfitting e o Deflated Sharpe Ratio. Leia o paper online (versão interativa + PDF) em plateau.marketmaker.cc, código e dados em github.com/suenot/plateau-robustness.
Você executou study.optimize(), o Optuna encontrou um conjunto de parâmetros com PnL +87%. Você está animado e preparando a estratégia para produção. Duas semanas de trading ao vivo depois, o PnL está próximo de zero. O que aconteceu?
O otimizador encontrou a ponta de uma agulha no espaço de parâmetros. Os parâmetros estão perfeitamente ajustados à sequência histórica de trades — mas o menor desvio nas condições de mercado destrói toda a construção. Isso é overfitting clássico, e poderia ter sido detectado antes do lançamento.
No artigo anterior comparamos coordinate descent com otimização bayesiana e mostramos por que o Optuna encontra o ótimo de forma mais eficiente. Hoje — o próximo passo: como garantir que o ótimo encontrado seja robusto, em vez de ser o resultado de um ajuste ao ruído.
Por que encontrar os "melhores" parâmetros é apenas metade do trabalho
Um otimizador navegando por uma vasta paisagem de parâmetros multidimensional em busca do verdadeiro ótimo
A otimização de parâmetros de estratégia é uma busca por um máximo em um espaço multidimensional. O problema é que os máximos vêm em dois tipos:
-
Plateau — uma região ampla e plana onde o PnL permanece consistentemente alto com variações de parâmetros. Mesmo que as condições de mercado desloquem os parâmetros efetivos em 10-20%, a estratégia continuará lucrativa.
-
Pico agudo — um cume estreito onde o PnL é alto apenas no valor exato do parâmetro. Um deslocamento de um passo colapsa a lucratividade. Isso é quase certamente overfitting: o otimizador encontrou um artefato dos dados históricos, não um padrão estável.
Uma metáfora alpinista: um plateau é um planalto montanhoso onde você pode caminhar com segurança. Um pico agudo é a ponta de uma agulha onde você só consegue se equilibrar.
Pico agudo vs plateau plano — intuição visual
Esquerda: um plateau robusto (ampla montanha de mesa com encostas suaves). Direita: um pico agudo frágil (ponta de agulha cercada por vales profundos)
Imagine um mapa de contorno onde os eixos são dois parâmetros da estratégia e a cor representa o PnL. Dois padrões são fáceis de distinguir visualmente:
Plateau (ótimo robusto):
- Áreas amplas da mesma cor
- Transições suaves entre níveis de PnL
- Isolinhas bem espaçadas
- Deslocar-se do ótimo em +/-20% muda o PnL em no máximo 10%
Imagine um heatmap: no centro — um retângulo amarelo brilhante ocupando cerca de um terço de todo o mapa. A cor transita gradualmente para laranja, depois para vermelho em direção às bordas. O ótimo não é um ponto, mas uma região.
Pico agudo (overfitting):
- Um ponto brilhante estreito cercado por cores frias
- Transições abruptas: um colapso bem ao lado do ótimo
- Isolinhas comprimidas em anéis apertados
- Um deslocamento de +/-5% derruba o PnL em 50% ou mais
Imagine o mesmo heatmap, mas no centro — um pequenino ponto amarelo imediatamente cercado de azul e roxo. Uma única combinação de parâmetros "correta".
Análise de sensibilidade dos parâmetros
Gráficos de corte mostrando como o PnL depende dos valores individuais dos parâmetros — bandas largas indicam robustez, clusters estreitos indicam fragilidade
Análise unidimensional: PnL versus um parâmetro
A abordagem mais simples — fixar todos os parâmetros exceto um e ver como o PnL depende de seu valor. O Optuna fornece plot_slice para isso:
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()
O que observar em um slice plot:
- Parâmetro robusto: a nuvem de pontos forma uma faixa horizontal larga perto do ótimo. Os melhores trials estão distribuídos por uma ampla faixa de valores de parâmetro.
- Parâmetro frágil: os melhores trials estão concentrados em uma faixa estreita. Deslocar o parâmetro em um ou dois passos — e a lucratividade colapsa.
Análise bidimensional: gráficos de contorno (heatmaps)
Um gráfico de contorno mostra a interação de dois parâmetros simultaneamente. Esta é a ferramenta-chave para a análise de plateau, porque os parâmetros raramente atuam de forma independente — limiares de entrada e saída, timeframes e tamanhos de posição estão interconectados.
from optuna.visualization import plot_contour
fig = plot_contour(study, params=["htf_entry_sell", "htf_exit_buy"])
fig.show()
Um gráfico de contorno para um par de parâmetros robusto se parece com um mapa topográfico de uma planície ondulada: isolinhas suaves e largas, grandes áreas da mesma cor. Um gráfico de contorno para um par frágil — como um mapa de um cone vulcânico: anéis concêntricos apertados ao redor de um único ponto.
Para uma estratégia com 12 parâmetros de separação, isso resulta em gráficos de contorno em pares. Você não precisa estudar todos — comece pelos parâmetros que o Optuna classificou como mais importantes.
Análise multidimensional: ranking de importância dos parâmetros
O Optuna pode estimar a contribuição de cada parâmetro para a função objetivo:
from optuna.visualization import plot_param_importances
fig = plot_param_importances(study)
fig.show()
O gráfico de importância de parâmetros é um histograma horizontal. Os parâmetros são classificados por sua contribuição para a variância do PnL em ordem decrescente. Os 3-4 principais parâmetros geralmente explicam 70-80% da variância.
Regra: se um parâmetro explica menos de 2% da variância do PnL, seu valor é praticamente irrelevante para o resultado — ele é robusto por definição. Concentre a análise de plateau nos 5 parâmetros mais importantes.
Ferramentas de visualização do Optuna
Heatmaps de contorno mostrando a paisagem de interação de parâmetros junto com os rankings de importância
plot_slice — Cortes unidimensionais
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()
O resultado — uma grade de gráficos de dispersão. Cada subgráfico mostra o valor da função objetivo (PnL, eixo Y) em relação a um único valor de parâmetro (eixo X). Os pontos são trials individuais. Para um parâmetro robusto, os melhores pontos (maior PnL) estão distribuídos por uma ampla faixa de X. Para um frágil — agrupados em uma coluna estreita.
plot_contour — Contornos bidimensionais
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 é um heatmap com dois parâmetros nos eixos. A cor codifica o PnL médio em uma dada região do espaço de parâmetros. Amarelo/verde — PnL alto, azul/roxo — baixo. As isolinhas conectam pontos com o mesmo PnL.
plot_param_importances — Contribuições dos parâmetros
from optuna.visualization import plot_param_importances
fig = plot_param_importances(
study,
evaluator=optuna.importance.FanovaImportanceEvaluator()
)
fig.show()
fANOVA (functional ANOVA) decompõe a variância da função objetivo entre parâmetros e suas interações. Isso é mais poderoso do que uma simples correlação porque considera efeitos não lineares.
Métricas quantitativas de plateau
Sensitivity ratio, largura do plateau e robustness score — três métricas que formalizam a qualidade do plateau
A avaliação visual é subjetiva. Precisamos de números. Aqui estão três métricas que formalizam o conceito de "plateau".
Sensitivity ratio
A razão entre a mudança de PnL e a mudança do parâmetro:
onde é a queda de PnL quando o parâmetro se desvia do ótimo em .
Interpretação:
- — o parâmetro é robusto: um deslocamento de 10% causa uma queda de PnL inferior a 5%
- — sensibilidade moderada
- — o parâmetro é frágil: um deslocamento de 10% derruba o PnL em 20%+
Largura do plateau
A largura da região de parâmetro dentro da qual o PnL permanece dentro de do ótimo:
Largura relativa do plateau:
onde o denominador é a faixa de busca completa do parâmetro.
Interpretação:
- — o plateau cobre mais de 30% da faixa no limiar de 10%. Parâmetro robusto.
- — o plateau é mais estreito que 5% da faixa. Sinal de alerta.
Robustness score
Uma métrica combinada em todos os parâmetros:
onde é a importância normalizada do parâmetro obtida via fANOVA ().
O produto das larguras ponderadas é uma métrica rigorosa: se mesmo um único parâmetro importante tiver um plateau estreito, será baixo. Parâmetros não importantes (com pequeno) têm efeito quase nulo.
Interpretação:
- — a estratégia é robusta
- — validação adicional necessária (walk-forward)
- — overfitting é muito provável
Código Python para detecção automatizada de plateau
Sistema automatizado que varre a paisagem de parâmetros para identificar plateaus robustos e picos frágeis
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()
Exemplo de saída:
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%
Exemplos práticos com estratégias de separação
Comparação da Estratégia A (plateau amplo, robusto), Estratégia B (moderada) e Estratégia C (pico agudo, overfitted)
Vamos examinar três estratégias com 12 parâmetros de separação. Cada estratégia passou por otimização Optuna com 500 trials.
Estratégia A (~55% PnL, ~500 trades, ~15% do tempo)
Os parâmetros da Estratégia A formam um plateau amplo. Vamos considerar o parâmetro-chave htf_entry_sell:
- Valor ótimo: 0,020
- PnL em 0,015: +51% (queda de 7%)
- PnL em 0,025: +49% (queda de 11%)
- PnL em 0,010: +43% (queda de 22%)
- PnL em 0,030: +41% (queda de 25%)
Se você imaginar isso como um gráfico unidimensional (eixo X — valor de htf_entry_sell, eixo Y — PnL), verá uma parábola suave com um topo plano. A faixa 0,010-0,030 é o plateau, onde o PnL permanece dentro de +/-25% do ótimo.
Sensitivity ratio: — robusto.
Largura do plateau no limiar de 10%: de 0,013 a 0,027, .
Estratégia B (~25% PnL, ~40 trades, ~5% do tempo)
A Estratégia B é otimizada com um pequeno número de trades. Parâmetro htf_entry_sell:
- Valor ótimo: 0,018
- PnL em 0,015: +24% (queda de 4%)
- PnL em 0,025: +9% (queda de 64%)
- PnL em 0,012: +11% (queda de 56%)
No gráfico — uma curva assimétrica e íngreme. O plateau existe apenas na faixa estreita 0,015-0,020. À direita do ótimo — um penhasco.
Sensitivity ratio: — sensibilidade moderada, mas com 40 trades isso é um sinal de alerta. Amostra pequena + plateau estreito = alta probabilidade de overfitting.
Largura do plateau no limiar de 10%: de 0,016 a 0,020, .
Estratégia C (~300% PnL, ~400 trades, ~45% do tempo)
A Estratégia C mostra um PnL impressionante, mas a análise de plateau revela problemas:
- Valor ótimo de
htf_entry_sell: 0,022 - PnL em 0,020: +295% (queda de 2%)
- PnL em 0,025: +142% (queda de 53%)
- PnL em 0,019: +128% (queda de 57%)
No gráfico — uma "agulha" característica: um pico muito alto em 0,022, queda acentuada em todas as direções. O gráfico de contorno mostraria um ponto brilhante imediatamente cercado de cores frias.
Sensitivity ratio: — frágil. Apesar de 400 trades, a estratégia depende excessivamente do valor exato de um único parâmetro.
Largura do plateau no limiar de 10%: de 0,021 a 0,023, .
Tabela resumo
| Estratégia | PnL | Trades | Sensitivity | Largura do plateau | Robustness score | Veredito |
|---|---|---|---|---|---|---|
| Estratégia A | +55% | ~500 | 0.44 | 35% | 0.148 | Robusta |
| Estratégia B | +25% | ~40 | 1.64 | 10% | 0.032 | Verificar (amostra pequena) |
| Estratégia C | +300% | ~400 | 3.79 | 5% | 0.008 | Overfitting |
Paradoxo: a Estratégia C com PnL +300% tem o pior robustness score. A Estratégia A com um "modesto" +55% é a mais robusta. Este é um resultado típico da análise de plateau: números impressionantes frequentemente mascaram fragilidade.
Os intervalos de confiança de cada estratégia podem ser verificados adicionalmente por meio de Monte Carlo bootstrap — isso mostrará a dispersão do PnL ao reamostrar trades.
Visualização 3D e heatmaps
Gráfico de superfície 3D do PnL sobre dois parâmetros com linhas de contorno projetadas no plano do chão
Para os pares de parâmetros mais importantes, é útil construir uma superfície 3D e um heatmap. Isso proporciona uma compreensão intuitiva da forma da paisagem.
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()
Um gráfico de superfície 3D para uma estratégia robusta se assemelha a uma montanha de mesa — um topo plano com encostas suaves. Para uma estratégia frágil — um pico agudo, como o Cervino. O heatmap complementa a visualização 3D, mostrando a mesma informação em uma projeção de cima com isolinhas.
Sinais de alerta: quando os resultados de otimização são suspeitos
Indicadores de aviso que sinalizam um possível overfitting nos resultados de otimização
Oito sinais de que a otimização encontrou overfitting em vez de um padrão real:
1. Sensitivity ratio > 2 para um parâmetro-chave
Se o PnL cai mais de 20% com um deslocamento de 10% do parâmetro — o ótimo é frágil.
2. Largura do plateau < 10% da faixa de busca
Se a região "boa" ocupa menos de 10% da faixa explorada — o otimizador provavelmente encontrou um artefato.
3. Os top-3 trials geram PnL 2-3x acima da mediana
Se os melhores trials são outliers em relação ao resto, em vez do "topo da colina" — não é um plateau.
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. Baixo número de trades (< 50) com PnL alto
Amostra pequena + PnL alto = alta variância na estimativa. A análise de plateau com 40 trades é, por si só, pouco confiável. Para tais estratégias, Monte Carlo bootstrap é fundamental.
5. Uma combinação de parâmetros "mágica"
Se o gráfico de contorno mostra um único ponto brilhante em meio a um campo cinza — isso não é uma estratégia, é uma combinação ajustada aos dados.
6. Parâmetros demais
Para 12 parâmetros com 10 valores cada, o espaço de busca contém combinações. O Optuna explora ~500. A probabilidade de encontrar um "bom" artefato em tal espaço é alta. Quanto mais parâmetros, mais rigorosa deve ser a análise de plateau.
7. O PnL cai drasticamente fora da amostra
Se o PnL in-sample é +87% e o walk-forward mostra +12% — a otimização ajustou os parâmetros ao período de treinamento. Mais sobre isso no artigo sobre otimização Walk-Forward.
8. Os parâmetros estão "presos" aos limites da faixa
Se o valor ótimo coincide com o limite da grade de busca — o ótimo pode estar além da faixa. Amplie a faixa e execute a otimização novamente.
Relatório automatizado de análise de plateau
Reunindo tudo em um único relatório gerado após cada otimização:
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))
Exemplo de saída:
{
"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
}
}
}
Relação com a validação walk-forward
Robustez paramétrica (análise de plateau) e robustez temporal (walk-forward) como dois sistemas de validação complementares
A análise de plateau e a validação walk-forward (WFO) são métodos complementares:
- A análise de plateau responde à pergunta: "Quão estável é o ótimo diante de pequenos deslocamentos de parâmetros?" Esta é uma verificação da robustez paramétrica.
- O walk-forward responde à pergunta: "Os parâmetros funcionam em dados que o otimizador não viu?" Esta é uma verificação da robustez temporal.
Uma estratégia pode passar na análise de plateau (plateau amplo) mas falhar no walk-forward (o regime de mercado mudou). E vice-versa — pode passar no walk-forward com parâmetros fixos, mas ter um ótimo frágil.
Recomendação: use sempre ambos os métodos. Se uma estratégia passa na análise de plateau () e no walk-forward () — este é um forte sinal de robustez. Mais detalhes no artigo sobre otimização Walk-Forward.
Para avaliar os intervalos de confiança do PnL em cada etapa, aplique Monte Carlo bootstrap. E para comparar corretamente estratégias com tempo ativo diferente, use a métrica PnL por tempo ativo.
Recomendações
Antes da otimização
-
Limite o número de parâmetros. Quanto menos parâmetros — mais confiável o plateau. 5-7 parâmetros é um máximo razoável. Com 12, já é necessária cautela redobrada.
-
Defina faixas significativas. Não defina
htf_entry_sellde 0,001 a 1,0 se a faixa realista é de 0,005 a 0,05. Faixas desnecessariamente amplas criam a ilusão de um plateau. -
Use trials suficientes. Para 12 parâmetros, um mínimo de 300-500 trials. Para uma análise de plateau confiável — 1000+.
Durante a otimização
-
Observe a convergência. Se o Optuna continua encontrando soluções significativamente melhores após 400 trials — o processo não convergiu, e a análise de plateau será pouco confiável.
-
Use pruning com cautela. Pruning agressivo (MedianPruner) pode cortar trials que parecem ruins nos passos iniciais, mas que são importantes para construir uma imagem completa da paisagem.
Após a otimização
-
Gere o relatório de plateau automaticamente. Integre
generate_plateau_report()ao pipeline de otimização. Não confie na avaliação visual — use números. -
Verifique os 5 principais parâmetros. Se o fANOVA mostra que 3 parâmetros explicam 80% da variância — os 9 restantes podem ser verificados com menos rigor.
-
Compare com a estratégia baseline. Se a estratégia com parâmetros padrão (sem otimização) mostra +30%, e a otimizada +55% — a diferença é de apenas 25 pontos percentuais, e o plateau provavelmente é amplo. Se o padrão mostra 0%, e o otimizado +300% — toda a lucratividade depende do ajuste preciso dos parâmetros.
-
Verificação final — walk-forward. A análise de plateau é uma condição necessária mas não suficiente para a robustez. Sempre valide out-of-sample.
Conclusão
A otimização de parâmetros é uma ferramenta poderosa, mas sem análise de plateau é um jogo de roleta. Você não sabe se encontrou um padrão estável ou ajustou o modelo ao ruído.
Três regras da análise de plateau:
-
Calcule o robustness score. O produto das larguras de plateau ponderadas fornece um único número que resume a robustez de todos os parâmetros. — luz verde.
-
Sensitivity ratio < 1 para parâmetros-chave. Se um deslocamento de 10% do parâmetro causa uma queda de PnL inferior a 10% — o parâmetro é robusto. Se maior — tenha cautela.
-
Visualize gráficos de contorno. Nenhuma métrica pode substituir a compreensão da forma da paisagem. Uma montanha de mesa plana — bom. Uma agulha afiada — ruim.
A análise de plateau leva 5 minutos após a otimização e pode economizar semanas de trading ao vivo não lucrativo. É uma etapa obrigatória entre study.optimize() e o lançamento do bot.
Links úteis
- 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ção
@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.