Analyse de plateau : comment distinguer un optimum robuste de l'overfitting
Article 6 de la série « Backtests sans illusions »
📄 Cet article est devenu un papier de recherche. Les métriques sont formalisées et stress-testées avec des simulations reproductibles, comparées à la Probability of Backtest Overfitting et au Deflated Sharpe Ratio. Lisez le papier en ligne (version interactive + PDF) sur plateau.marketmaker.cc, code et données sur github.com/suenot/plateau-robustness.
Vous avez lancé study.optimize(), Optuna a trouvé un jeu de paramètres avec un PnL de +87%. Vous êtes enthousiaste et préparez la stratégie pour la production. Deux semaines de trading en direct plus tard, le PnL est proche de zéro. Que s'est-il passé ?
L'optimiseur a trouvé la pointe d'une aiguille dans l'espace des paramètres. Les paramètres sont parfaitement ajustés à la séquence historique de trades — mais la moindre déviation des conditions de marché détruit toute la construction. C'est de l'overfitting classique, et cela aurait pu être détecté avant le lancement.
Dans l'article précédent, nous avons comparé la descente de coordonnées à l'optimisation bayésienne et montré pourquoi Optuna trouve l'optimum plus efficacement. Aujourd'hui — l'étape suivante : comment s'assurer que l'optimum trouvé est robuste, plutôt que le résultat d'un ajustement au bruit.
Pourquoi trouver les « meilleurs » paramètres n'est que la moitié du travail
Un optimiseur navigant dans un vaste paysage de paramètres multidimensionnel à la recherche du véritable optimum
L'optimisation des paramètres d'une stratégie est une recherche de maximum dans un espace multidimensionnel. Le problème est que les maximums se présentent sous deux formes :
-
Plateau — une région large et plate où le PnL reste constamment élevé malgré les variations de paramètres. Même si les conditions de marché déplacent les paramètres effectifs de 10-20%, la stratégie continuera d'être rentable.
-
Pic aigu — un sommet étroit où le PnL n'est élevé qu'à la valeur exacte du paramètre. Un décalage d'un pas fait s'effondrer la rentabilité. C'est presque certainement de l'overfitting : l'optimiseur a trouvé un artefact des données historiques, pas un motif stable.
Une métaphore alpine : un plateau est un haut plateau montagneux où l'on peut marcher en sécurité. Un pic aigu est la pointe d'une aiguille où l'on ne peut que se tenir en équilibre.
Pic aigu vs plateau plat — intuition visuelle
À gauche : un plateau robuste (large montagne tabulaire aux pentes douces). À droite : un pic aigu fragile (pointe d'aiguille entourée de vallées profondes)
Imaginez une carte de contour où les axes représentent deux paramètres de la stratégie et la couleur représente le PnL. Deux motifs sont faciles à distinguer visuellement :
Plateau (optimum robuste) :
- Larges zones de même couleur
- Transitions douces entre les niveaux de PnL
- Isolignes très espacées
- Un déplacement de +/-20% par rapport à l'optimum change le PnL de pas plus de 10%
Imaginez une heatmap : au centre — un rectangle jaune vif occupant environ un tiers de la carte entière. La couleur passe progressivement à l'orange, puis au rouge vers les bords. L'optimum n'est pas un point, mais une région.
Pic aigu (overfitting) :
- Un point lumineux étroit entouré de couleurs froides
- Transitions abruptes : un effondrement juste à côté de l'optimum
- Isolignes comprimées en anneaux serrés
- Un déplacement de +/-5% fait chuter le PnL de 50% ou plus
Imaginez la même heatmap, mais au centre — un minuscule point jaune immédiatement entouré de bleu et de violet. Une seule combinaison de paramètres « correcte ».
Analyse de sensibilité des paramètres
Graphiques de coupe montrant comment le PnL dépend des valeurs individuelles des paramètres — les bandes larges indiquent la robustesse, les clusters étroits indiquent la fragilité
Analyse unidimensionnelle : PnL en fonction d'un paramètre
L'approche la plus simple — fixer tous les paramètres sauf un et observer comment le PnL dépend de sa valeur. Optuna fournit plot_slice pour cela :
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()
Ce qu'il faut observer sur un slice plot :
- Paramètre robuste : le nuage de points forme une bande horizontale large près de l'optimum. Les meilleurs trials sont répartis sur une large plage de valeurs de paramètre.
- Paramètre fragile : les meilleurs trials se concentrent dans une plage étroite. Un décalage du paramètre d'un ou deux pas — et la rentabilité s'effondre.
Analyse bidimensionnelle : graphiques de contour (heatmaps)
Un graphique de contour montre l'interaction de deux paramètres simultanément. C'est l'outil clé pour l'analyse de plateau, car les paramètres agissent rarement de manière indépendante — les seuils d'entrée et de sortie, les timeframes et les tailles de position sont interconnectés.
from optuna.visualization import plot_contour
fig = plot_contour(study, params=["htf_entry_sell", "htf_exit_buy"])
fig.show()
Un graphique de contour pour une paire de paramètres robuste ressemble à une carte topographique d'une plaine vallonnée : isolignes larges et douces, grandes zones de même couleur. Un graphique de contour pour une paire fragile — comme une carte d'un cône volcanique : anneaux concentriques serrés autour d'un seul point.
Pour une stratégie avec 12 paramètres de séparation, cela donne graphiques de contour par paires. Vous n'avez pas besoin de tous les étudier — commencez par les paramètres qu'Optuna a jugés les plus importants.
Analyse multidimensionnelle : classement d'importance des paramètres
Optuna peut estimer la contribution de chaque paramètre à la fonction objectif :
from optuna.visualization import plot_param_importances
fig = plot_param_importances(study)
fig.show()
Le graphique d'importance des paramètres est un histogramme horizontal. Les paramètres sont classés selon leur contribution à la variance du PnL par ordre décroissant. Les 3-4 principaux paramètres expliquent généralement 70-80% de la variance.
Règle : si un paramètre explique moins de 2% de la variance du PnL, sa valeur est pratiquement sans importance pour le résultat — il est robuste par définition. Concentrez l'analyse de plateau sur les 5 paramètres les plus importants.
Outils de visualisation Optuna
Heatmaps de contour montrant le paysage d'interaction des paramètres ainsi que les classements d'importance
plot_slice — Coupes unidimensionnelles
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()
Le résultat — une grille de nuages de points. Chaque sous-graphique montre la valeur de la fonction objectif (PnL, axe Y) en fonction d'une seule valeur de paramètre (axe X). Les points sont des trials individuels. Pour un paramètre robuste, les meilleurs points (PnL le plus élevé) sont répartis sur une large plage de X. Pour un paramètre fragile — regroupés dans une colonne étroite.
plot_contour — Contours bidimensionnels
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()
Chaque graphique de contour est une heatmap avec deux paramètres sur les axes. La couleur encode le PnL moyen dans une région donnée de l'espace des paramètres. Jaune/vert — PnL élevé, bleu/violet — faible. Les isolignes relient les points ayant le même PnL.
plot_param_importances — Contributions des paramètres
from optuna.visualization import plot_param_importances
fig = plot_param_importances(
study,
evaluator=optuna.importance.FanovaImportanceEvaluator()
)
fig.show()
fANOVA (functional ANOVA) décompose la variance de la fonction objectif entre les paramètres et leurs interactions. C'est plus puissant qu'une simple corrélation car cela prend en compte les effets non linéaires.
Métriques quantitatives de plateau
Sensitivity ratio, largeur de plateau et robustness score — trois métriques qui formalisent la qualité du plateau
L'évaluation visuelle est subjective. Nous avons besoin de chiffres. Voici trois métriques qui formalisent le concept de « plateau ».
Sensitivity ratio
Le ratio entre le changement de PnL et le changement de paramètre :
où est la baisse du PnL lorsque le paramètre s'écarte de l'optimum de .
Interprétation :
- — le paramètre est robuste : un décalage de 10% cause une baisse de PnL inférieure à 5%
- — sensibilité modérée
- — le paramètre est fragile : un décalage de 10% fait chuter le PnL de 20%+
Largeur de plateau
La largeur de la région de paramètre dans laquelle le PnL reste dans de l'optimum :
Largeur relative de plateau :
où le dénominateur est la plage de recherche complète du paramètre.
Interprétation :
- — le plateau couvre plus de 30% de la plage au seuil de 10%. Paramètre robuste.
- — le plateau est plus étroit que 5% de la plage. Signal d'alarme.
Robustness score
Une métrique combinée sur tous les paramètres :
où est l'importance normalisée du paramètre issue de fANOVA ().
Le produit des largeurs pondérées est une métrique stricte : si ne serait-ce qu'un seul paramètre important a un plateau étroit, sera faible. Les paramètres peu importants (avec un petit) ont peu d'effet.
Interprétation :
- — la stratégie est robuste
- — validation supplémentaire requise (walk-forward)
- — l'overfitting est très probable
Code Python pour la détection automatisée de plateau
Système automatisé scannant le paysage des paramètres pour identifier des plateaux robustes et des pics fragiles
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"
),
}
Utilisation
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()
Exemple de sortie :
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%
Exemples pratiques avec des stratégies de séparation
Comparaison de la Stratégie A (plateau large, robuste), de la Stratégie B (modérée) et de la Stratégie C (pic aigu, overfittée)
Examinons trois stratégies avec 12 paramètres de séparation. Chaque stratégie a subi une optimisation Optuna avec 500 trials.
Stratégie A (~55% PnL, ~500 trades, ~15% du temps)
Les paramètres de la Stratégie A forment un plateau large. Prenons le paramètre clé htf_entry_sell :
- Valeur optimale : 0,020
- PnL à 0,015 : +51% (baisse de 7%)
- PnL à 0,025 : +49% (baisse de 11%)
- PnL à 0,010 : +43% (baisse de 22%)
- PnL à 0,030 : +41% (baisse de 25%)
Si vous imaginez cela comme un graphique unidimensionnel (axe X — valeur de htf_entry_sell, axe Y — PnL), vous verrez une parabole douce avec un sommet plat. La plage 0,010-0,030 est le plateau, où le PnL reste dans +/-25% de l'optimum.
Sensitivity ratio : — robuste.
Largeur de plateau au seuil de 10% : de 0,013 à 0,027, .
Stratégie B (~25% PnL, ~40 trades, ~5% du temps)
La Stratégie B est optimisée sur un petit nombre de trades. Paramètre htf_entry_sell :
- Valeur optimale : 0,018
- PnL à 0,015 : +24% (baisse de 4%)
- PnL à 0,025 : +9% (baisse de 64%)
- PnL à 0,012 : +11% (baisse de 56%)
Sur le graphique — une courbe asymétrique et abrupte. Le plateau n'existe que dans la plage étroite 0,015-0,020. À droite de l'optimum — une falaise.
Sensitivity ratio : — sensibilité modérée, mais avec 40 trades c'est un signal d'alarme. Petit échantillon + plateau étroit = probabilité élevée d'overfitting.
Largeur de plateau au seuil de 10% : de 0,016 à 0,020, .
Stratégie C (~300% PnL, ~400 trades, ~45% du temps)
La Stratégie C affiche un PnL stupéfiant, mais l'analyse de plateau révèle des problèmes :
- Valeur optimale de
htf_entry_sell: 0,022 - PnL à 0,020 : +295% (baisse de 2%)
- PnL à 0,025 : +142% (baisse de 53%)
- PnL à 0,019 : +128% (baisse de 57%)
Sur le graphique — une « aiguille » caractéristique : un pic très élevé à 0,022, chute brutale dans toutes les directions. Le graphique de contour montrerait un point lumineux immédiatement entouré de couleurs froides.
Sensitivity ratio : — fragile. Malgré 400 trades, la stratégie dépend excessivement de la valeur exacte d'un seul paramètre.
Largeur de plateau au seuil de 10% : de 0,021 à 0,023, .
Tableau récapitulatif
| Stratégie | PnL | Trades | Sensitivity | Largeur de plateau | Robustness score | Verdict |
|---|---|---|---|---|---|---|
| Stratégie A | +55% | ~500 | 0.44 | 35% | 0.148 | Robuste |
| Stratégie B | +25% | ~40 | 1.64 | 10% | 0.032 | À vérifier (petit échantillon) |
| Stratégie C | +300% | ~400 | 3.79 | 5% | 0.008 | Overfitting |
Paradoxe : la Stratégie C avec un PnL de +300% a le pire robustness score. La Stratégie A avec un « modeste » +55% est la plus robuste. C'est un résultat typique de l'analyse de plateau : des chiffres impressionnants masquent souvent la fragilité.
Les intervalles de confiance de chaque stratégie peuvent en outre être vérifiés via le bootstrap Monte Carlo — il montrera la dispersion du PnL lors du rééchantillonnage des trades.
Visualisation 3D et heatmaps
Graphique de surface 3D du PnL sur deux paramètres avec des lignes de contour projetées sur le plan du sol
Pour les paires de paramètres les plus importantes, il est utile de construire une surface 3D et une heatmap. Cela apporte une compréhension intuitive de la forme du paysage.
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 graphique de surface 3D pour une stratégie robuste ressemble à une montagne tabulaire — un sommet plat avec des pentes douces. Pour une stratégie fragile — un pic aigu, comme le Cervin. La heatmap complète la vue 3D, montrant la même information dans une projection de dessus avec des isolignes.
Signaux d'alarme : quand les résultats d'optimisation sont suspects
Indicateurs d'avertissement signalant un overfitting potentiel dans les résultats d'optimisation
Huit signes que l'optimisation a trouvé de l'overfitting plutôt qu'un motif réel :
1. Sensitivity ratio > 2 pour un paramètre clé
Si le PnL chute de plus de 20% avec un décalage de 10% du paramètre — l'optimum est fragile.
2. Largeur de plateau < 10% de la plage de recherche
Si la « bonne » région occupe moins de 10% de la plage explorée — l'optimiseur a très probablement trouvé un artefact.
3. Les top-3 trials produisent un PnL 2-3x supérieur à la médiane
Si les meilleurs trials sont des outliers par rapport au reste plutôt que le « sommet de la colline » — ce n'est pas un 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. Faible nombre de trades (< 50) avec un PnL élevé
Petit échantillon + PnL élevé = forte variance de l'estimation. L'analyse de plateau sur 40 trades est en soi peu fiable. Pour de telles stratégies, le bootstrap Monte Carlo est critique.
5. Une combinaison de paramètres « magique »
Si le graphique de contour montre un unique point lumineux au milieu d'un champ gris — ce n'est pas une stratégie, c'est une combinaison ajustée aux données.
6. Trop de paramètres
Pour 12 paramètres avec 10 valeurs chacun, l'espace de recherche contient combinaisons. Optuna en explore ~500. La probabilité de trouver un « bon » artefact dans un tel espace est élevée. Plus il y a de paramètres, plus l'analyse de plateau doit être stricte.
7. Le PnL chute fortement hors échantillon
Si le PnL in-sample est de +87% et le walk-forward montre +12% — l'optimisation a ajusté les paramètres à la période d'entraînement. Plus d'informations dans l'article sur l'optimisation Walk-Forward.
8. Les paramètres sont « épinglés » aux limites de la plage
Si la valeur optimale coïncide avec la limite de la grille de recherche — l'optimum peut se situer au-delà de la plage. Élargissez la plage et relancez l'optimisation.
Rapport automatisé d'analyse de plateau
Tout rassembler en un seul rapport généré après chaque optimisation :
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))
Exemple de sortie :
{
"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
}
}
}
Relation avec la validation walk-forward
Robustesse paramétrique (analyse de plateau) et robustesse temporelle (walk-forward) comme deux systèmes de validation complémentaires
L'analyse de plateau et la validation walk-forward (WFO) sont des méthodes complémentaires :
- L'analyse de plateau répond à la question : « Quelle est la stabilité de l'optimum face à de petits décalages de paramètres ? » C'est une vérification de la robustesse paramétrique.
- Le walk-forward répond à la question : « Les paramètres fonctionnent-ils sur des données que l'optimiseur n'a pas vues ? » C'est une vérification de la robustesse temporelle.
Une stratégie peut réussir l'analyse de plateau (plateau large) mais échouer au walk-forward (le régime de marché a changé). Et inversement — elle peut réussir le walk-forward avec des paramètres fixes mais avoir un optimum fragile.
Recommandation : utilisez toujours les deux méthodes. Si une stratégie réussit l'analyse de plateau () et le walk-forward () — c'est un signal fort de robustesse. Plus de détails dans l'article sur l'optimisation Walk-Forward.
Pour évaluer les intervalles de confiance du PnL à chaque étape, appliquez le bootstrap Monte Carlo. Et pour comparer correctement des stratégies avec un temps actif différent, utilisez la métrique PnL par temps actif.
Recommandations
Avant l'optimisation
-
Limitez le nombre de paramètres. Moins il y a de paramètres — plus le plateau est fiable. 5-7 paramètres est un maximum raisonnable. Avec 12, une prudence accrue est déjà nécessaire.
-
Définissez des plages significatives. Ne définissez pas
htf_entry_sellde 0,001 à 1,0 si la plage réaliste est de 0,005 à 0,05. Des plages inutilement larges créent l'illusion d'un plateau. -
Utilisez suffisamment de trials. Pour 12 paramètres, un minimum de 300-500 trials. Pour une analyse de plateau fiable — 1000+.
Pendant l'optimisation
-
Surveillez la convergence. Si Optuna continue de trouver des solutions significativement meilleures après 400 trials — le processus n'a pas convergé, et l'analyse de plateau sera peu fiable.
-
Utilisez le pruning avec précaution. Un pruning agressif (MedianPruner) peut couper des trials qui semblent mauvais dans les premières étapes mais qui sont importants pour construire une image complète du paysage.
Après l'optimisation
-
Générez le rapport de plateau automatiquement. Intégrez
generate_plateau_report()dans le pipeline d'optimisation. Ne vous fiez pas à l'évaluation visuelle — utilisez des chiffres. -
Vérifiez les 5 principaux paramètres. Si fANOVA montre que 3 paramètres expliquent 80% de la variance — les 9 restants peuvent être vérifiés moins minutieusement.
-
Comparez avec la stratégie de référence. Si la stratégie avec des paramètres par défaut (sans optimisation) affiche +30%, et l'optimisée +55% — la différence n'est que de 25 points de pourcentage, et le plateau est probablement large. Si le défaut affiche 0%, et l'optimisée +300% — toute la rentabilité dépend de l'ajustement précis des paramètres.
-
Vérification finale — walk-forward. L'analyse de plateau est une condition nécessaire mais non suffisante pour la robustesse. Validez toujours en out-of-sample.
Conclusion
L'optimisation des paramètres est un outil puissant, mais sans analyse de plateau, c'est un jeu de roulette. Vous ne savez pas si vous avez trouvé un motif stable ou ajusté le modèle au bruit.
Trois règles de l'analyse de plateau :
-
Calculez le robustness score. Le produit des largeurs de plateau pondérées donne un seul chiffre qui résume la robustesse de tous les paramètres. — feu vert.
-
Sensitivity ratio < 1 pour les paramètres clés. Si un décalage de 10% du paramètre cause une baisse de PnL inférieure à 10% — le paramètre est robuste. Si c'est plus — soyez prudent.
-
Visualisez les graphiques de contour. Aucune métrique ne peut remplacer la compréhension de la forme du paysage. Une montagne tabulaire plate — bien. Une aiguille aiguë — mal.
L'analyse de plateau prend 5 minutes après l'optimisation et peut épargner des semaines de trading en direct non rentable. C'est une étape obligatoire entre study.optimize() et le lancement du bot.
Liens utiles
- 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)
Citation
@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.