Plateau-analyse: hoe onderscheid je een robuust optimum van overfitting
Artikel 6 in de serie "Backtests zonder illusies"
📄 Dit artikel is uitgegroeid tot een research paper. De metrics zijn geformaliseerd en stress-getest met reproduceerbare simulaties, benchmarkt tegen de Probability of Backtest Overfitting en de Deflated Sharpe Ratio. Lees het paper online (interactieve versie + PDF) op plateau.marketmaker.cc, code en data op github.com/suenot/plateau-robustness.
Je hebt study.optimize() uitgevoerd, Optuna vond een parameterset met PnL +87%. Je bent enthousiast en maakt de strategie klaar voor productie. Twee weken live trading later staat de PnL rond nul. Wat is er gebeurd?
De optimizer vond de punt van een naald in de parameterruimte. De parameters zijn perfect afgestemd op de historische reeks trades — maar de geringste afwijking in marktomstandigheden vernietigt de hele constructie. Dit is klassieke overfitting, en het had vóór de lancering ontdekt kunnen worden.
In het vorige artikel hebben we coordinate descent vergeleken met Bayesiaanse optimalisatie en laten zien waarom Optuna het optimum efficiënter vindt. Vandaag — de volgende stap: hoe zorg je ervoor dat het gevonden optimum robuust is, in plaats van het resultaat van aanpassen aan ruis.
Waarom het vinden van de "beste" parameters slechts het halve werk is
Een optimizer navigeert door een uitgestrekt multidimensionaal parameterlandschap op zoek naar het werkelijke optimum
Optimalisatie van strategieparameters is een zoektocht naar een maximum in een multidimensionale ruimte. Het probleem is dat maxima in twee vormen voorkomen:
-
Plateau — een breed vlak gebied waar de PnL consistent hoog blijft bij parametervariaties. Zelfs als marktomstandigheden de effectieve parameters met 10-20% verschuiven, blijft de strategie winstgevend.
-
Scherpe piek — een smalle top waar de PnL alleen hoog is bij de exacte parameterwaarde. Een verschuiving van één stap laat de winstgevendheid instorten. Dit is vrijwel zeker overfitting: de optimizer vond een artefact van historische data, geen stabiel patroon.
Een alpinisme-metafoor: een plateau is een hoogvlakte waar je veilig kunt lopen. Een scherpe piek is de punt van een naald waarop je alleen kunt balanceren.
Scherpe piek vs vlak plateau — visuele intuïtie
Links: een robuust plateau (breed tafelgebergte met zachte hellingen). Rechts: een fragiele scherpe piek (naaldpunt omgeven door diepe valleien)
Stel je een contourkaart voor waarbij de assen twee strategieparameters zijn en de kleur de PnL weergeeft. Twee patronen zijn visueel gemakkelijk te onderscheiden:
Plateau (robuust optimum):
- Brede gebieden van dezelfde kleur
- Vloeiende overgangen tussen PnL-niveaus
- Ver uit elkaar liggende isolijnen
- Een verschuiving van +/-20% ten opzichte van het optimum verandert de PnL met niet meer dan 10%
Stel je een heatmap voor: in het midden — een felgeel rechthoek dat ongeveer een derde van de hele kaart beslaat. De kleur gaat geleidelijk over in oranje, dan in rood naar de randen toe. Het optimum is geen punt, maar een gebied.
Scherpe piek (overfitting):
- Een smalle heldere vlek omringd door koude kleuren
- Abrupte overgangen: een instorting vlak naast het optimum
- Isolijnen samengeperst tot strakke ringen
- Een verschuiving van +/-5% laat de PnL met 50% of meer dalen
Stel je dezelfde heatmap voor, maar in het midden — een piepklein geel puntje onmiddellijk omgeven door blauw en paars. Een enkele "juiste" parametercombinatie.
Parametersensitiviteitsanalyse
Slice plots die laten zien hoe de PnL afhangt van individuele parameterwaarden — brede banden duiden op robuustheid, smalle clusters op fragiliteit
Eendimensionale analyse: PnL versus één parameter
De eenvoudigste aanpak — fixeer alle parameters behalve één en kijk hoe de PnL van diens waarde afhangt. Optuna biedt hiervoor plot_slice:
import optuna
from optuna.visualization import plot_slice
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=500)
fig = plot_slice(study, params=["htf_entry_sell", "ltf_momentum", "stop_loss_pct"])
fig.show()
Waar te letten op in een slice plot:
- Robuuste parameter: de puntenwolk vormt een brede horizontale band nabij het optimum. De beste trials zijn verspreid over een breed bereik van parameterwaarden.
- Fragiele parameter: de beste trials zijn geconcentreerd in een smal bereik. Een verschuiving van de parameter met een of twee stappen — en de winstgevendheid stort in.
Tweedimensionale analyse: contourplots (heatmaps)
Een contourplot toont de interactie van twee parameters tegelijk. Dit is het sleutelinstrument voor plateau-analyse, omdat parameters zelden onafhankelijk werken — entry- en exitdrempels, timeframes en positiegroottes zijn onderling verbonden.
from optuna.visualization import plot_contour
fig = plot_contour(study, params=["htf_entry_sell", "htf_exit_buy"])
fig.show()
Een contourplot voor een robuust parameterpaar lijkt op een topografische kaart van een glooiend vlak: vloeiende brede isolijnen, grote gebieden van dezelfde kleur. Een contourplot voor een fragiel paar — als een kaart van een vulkaankegel: strakke concentrische ringen rond één punt.
Voor een strategie met 12 separatieparameters levert dit paarsgewijze contourplots op. Je hoeft ze niet allemaal te bestuderen — begin met de parameters die Optuna als belangrijkst heeft beoordeeld.
Multidimensionale analyse: parameter-belangrijkheidsranking
Optuna kan de bijdrage van elke parameter aan de objectieve functie schatten:
from optuna.visualization import plot_param_importances
fig = plot_param_importances(study)
fig.show()
De parameter-belangrijkheidsgrafiek is een horizontaal histogram. Parameters worden gerangschikt naar hun bijdrage aan de PnL-variantie in aflopende volgorde. De top 3-4 parameters verklaren gewoonlijk 70-80% van de variantie.
Regel: als een parameter minder dan 2% van de PnL-variantie verklaart, is diens waarde vrijwel irrelevant voor het resultaat — hij is per definitie robuust. Richt de plateau-analyse op de top 5 belangrijkste parameters.
Optuna-visualisatietools
Contour-heatmaps die het interactielandschap van parameters tonen samen met belangrijkheidsranglijsten
plot_slice — Eendimensionale slices
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()
Het resultaat — een raster van spreidingsplots. Elke subplot toont de waarde van de objectieve functie (PnL, Y-as) tegen een enkele parameterwaarde (X-as). Punten zijn individuele trials. Voor een robuuste parameter zijn de beste punten (hoogste PnL) verspreid over een breed bereik van X. Voor een fragiele — gegroepeerd in een smalle kolom.
plot_contour — Tweedimensionale contouren
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()
Elke contourplot is een heatmap met twee parameters op de assen. Kleur codeert de gemiddelde PnL in een gegeven regio van de parameterruimte. Geel/groen — hoge PnL, blauw/paars — laag. Isolijnen verbinden punten met dezelfde PnL.
plot_param_importances — Bijdragen van parameters
from optuna.visualization import plot_param_importances
fig = plot_param_importances(
study,
evaluator=optuna.importance.FanovaImportanceEvaluator()
)
fig.show()
fANOVA (functional ANOVA) ontleedt de variantie van de objectieve functie over parameters en hun interacties. Dit is krachtiger dan eenvoudige correlatie omdat het rekening houdt met niet-lineaire effecten.
Kwantitatieve plateau-metrics
Sensitivity ratio, plateaubreedte en robustness score — drie metrics die plateaukwaliteit formaliseren
Visuele beoordeling is subjectief. We hebben getallen nodig. Hier zijn drie metrics die het concept van een "plateau" formaliseren.
Sensitivity ratio
De verhouding tussen PnL-verandering en parameterverandering:
waarbij de PnL-daling is wanneer parameter met afwijkt van het optimum.
Interpretatie:
- — parameter is robuust: een verschuiving van 10% veroorzaakt minder dan 5% PnL-daling
- — matige gevoeligheid
- — parameter is fragiel: een verschuiving van 10% laat de PnL met 20%+ instorten
Plateaubreedte
De breedte van het parametergebied waarbinnen de PnL binnen van het optimum blijft:
Relatieve plateaubreedte:
waarbij de noemer het volledige zoekbereik van de parameter is.
Interpretatie:
- — het plateau beslaat meer dan 30% van het bereik bij de 10%-drempel. Robuuste parameter.
- — het plateau is smaller dan 5% van het bereik. Waarschuwingssignaal.
Robustness score
Een gecombineerde metric over alle parameters:
waarbij het genormaliseerde belang van parameter uit fANOVA is ().
Het product van gewogen breedtes is een strikte metric: als zelfs maar één belangrijke parameter een smal plateau heeft, zal laag zijn. Onbelangrijke parameters (met kleine ) hebben nauwelijks effect.
Interpretatie:
- — de strategie is robuust
- — extra validatie vereist (walk-forward)
- — overfitting is zeer waarschijnlijk
Python-code voor geautomatiseerde plateaudetectie
Geautomatiseerd systeem dat het parameterlandschap scant om robuuste plateaus en fragiele pieken te identificeren
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"
),
}
Gebruik
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()
Voorbeelduitvoer:
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%
Praktische voorbeelden met separatiestrategieën
Vergelijking van Strategie A (breed plateau, robuust), Strategie B (matig) en Strategie C (scherpe piek, overfit)
Laten we drie strategieën met 12 separatieparameters onderzoeken. Elke strategie onderging Optuna-optimalisatie met 500 trials.
Strategie A (~55% PnL, ~500 trades, ~15% tijd)
De parameters van Strategie A vormen een breed plateau. Neem de sleutelparameter htf_entry_sell:
- Optimale waarde: 0,020
- PnL bij 0,015: +51% (7% daling)
- PnL bij 0,025: +49% (11% daling)
- PnL bij 0,010: +43% (22% daling)
- PnL bij 0,030: +41% (25% daling)
Als je dit als een eendimensionale plot voorstelt (X-as — waarde van htf_entry_sell, Y-as — PnL), zie je een zachte parabool met een vlakke top. Het bereik 0,010-0,030 is het plateau, waar de PnL binnen +/-25% van het optimum blijft.
Sensitivity ratio: — robuust.
Plateaubreedte bij 10%-drempel: van 0,013 tot 0,027, .
Strategie B (~25% PnL, ~40 trades, ~5% tijd)
Strategie B is geoptimaliseerd op een klein aantal trades. Parameter htf_entry_sell:
- Optimale waarde: 0,018
- PnL bij 0,015: +24% (4% daling)
- PnL bij 0,025: +9% (64% daling)
- PnL bij 0,012: +11% (56% daling)
Op de plot — een asymmetrische en steile curve. Het plateau bestaat alleen in het smalle bereik 0,015-0,020. Rechts van het optimum — een klif.
Sensitivity ratio: — matige gevoeligheid, maar met 40 trades is dit een waarschuwingssignaal. Kleine steekproef + smal plateau = hoge kans op overfitting.
Plateaubreedte bij 10%-drempel: van 0,016 tot 0,020, .
Strategie C (~300% PnL, ~400 trades, ~45% tijd)
Strategie C toont een verbluffende PnL, maar plateau-analyse onthult problemen:
- Optimale waarde van
htf_entry_sell: 0,022 - PnL bij 0,020: +295% (2% daling)
- PnL bij 0,025: +142% (53% daling)
- PnL bij 0,019: +128% (57% daling)
Op de plot — een karakteristieke "naald": een zeer hoge piek bij 0,022, scherpe daling in alle richtingen. De contourplot zou een heldere vlek tonen die onmiddellijk omringd is door koude kleuren.
Sensitivity ratio: — fragiel. Ondanks 400 trades is de strategie buitensporig afhankelijk van de exacte waarde van één parameter.
Plateaubreedte bij 10%-drempel: van 0,021 tot 0,023, .
Samenvattende tabel
| Strategie | PnL | Trades | Sensitivity | Plateaubreedte | Robustness score | Verdict |
|---|---|---|---|---|---|---|
| Strategie A | +55% | ~500 | 0.44 | 35% | 0.148 | Robuust |
| Strategie B | +25% | ~40 | 1.64 | 10% | 0.032 | Controleren (kleine steekproef) |
| Strategie C | +300% | ~400 | 3.79 | 5% | 0.008 | Overfitting |
Paradox: Strategie C met PnL +300% heeft de slechtste robustness score. Strategie A met een "bescheiden" +55% is het meest robuust. Dit is een typisch resultaat van plateau-analyse: indrukwekkende cijfers maskeren vaak fragiliteit.
Betrouwbaarheidsintervallen voor elke strategie kunnen extra worden geverifieerd via Monte Carlo bootstrap — dit toont de PnL-spreiding bij het opnieuw samplen van trades.
3D-visualisatie en heatmaps
3D-oppervlakteplot van PnL over twee parameters met contourlijnen geprojecteerd op het grondvlak
Voor de belangrijkste parameterparen is het nuttig om een 3D-oppervlak en heatmap te bouwen. Dit geeft intuïtief inzicht in de vorm van het landschap.
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()
Een 3D-oppervlakteplot voor een robuuste strategie lijkt op een tafelberg — een vlakke top met zachte hellingen. Voor een fragiele strategie — een scherpe piek, zoals de Matterhorn. De heatmap vult de 3D-weergave aan door dezelfde informatie te tonen in een bovenaanzicht met isolijnen.
Waarschuwingssignalen: wanneer optimalisatieresultaten verdacht zijn
Waarschuwingsindicatoren die mogelijke overfitting in optimalisatieresultaten signaleren
Acht tekenen dat de optimalisatie overfitting heeft gevonden in plaats van een echt patroon:
1. Sensitivity ratio > 2 voor een sleutelparameter
Als de PnL met meer dan 20% daalt bij een verschuiving van 10% van de parameter — is het optimum fragiel.
2. Plateaubreedte < 10% van het zoekbereik
Als het "goede" gebied minder dan 10% van het onderzochte bereik beslaat — heeft de optimizer hoogstwaarschijnlijk een artefact gevonden.
3. Top-3 trials leveren PnL 2-3x boven de mediaan
Als de beste trials uitschieters zijn ten opzichte van de rest in plaats van de "heuveltop" — is het geen 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. Laag aantal trades (< 50) met hoge PnL
Kleine steekproef + hoge PnL = hoge variantie in de schatting. Plateau-analyse op 40 trades is op zichzelf onbetrouwbaar. Voor zulke strategieën is Monte Carlo bootstrap cruciaal.
5. Eén "magische" parametercombinatie
Als de contourplot een enkele heldere stip te midden van een grijs veld toont — is dit geen strategie, maar een op data afgestemde combinatie.
6. Te veel parameters
Voor 12 parameters met elk 10 waarden bevat de zoekruimte combinaties. Optuna verkent er ~500. De kans om een "goed" artefact te vinden in zo'n ruimte is hoog. Hoe meer parameters, hoe strenger de plateau-analyse moet zijn.
7. PnL daalt sterk buiten de steekproef
Als de in-sample PnL +87% is en walk-forward +12% toont — heeft de optimalisatie parameters afgestemd op de trainingsperiode. Meer hierover in het artikel over Walk-Forward-optimalisatie.
8. Parameters zijn "vastgepind" aan bereikgrenzen
Als de optimale waarde samenvalt met de grens van het zoekraster — kan het optimum buiten het bereik liggen. Verbreed het bereik en voer de optimalisatie opnieuw uit.
Geautomatiseerd plateau-analyserapport
Alles samenbrengen in één rapport dat na elke optimalisatie wordt gegenereerd:
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))
Voorbeelduitvoer:
{
"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
}
}
}
Relatie met walk-forward-validatie
Parametrische robuustheid (plateau-analyse) en temporele robuustheid (walk-forward) als twee elkaar aanvullende validatiesystemen
Plateau-analyse en walk-forward-validatie (WFO) zijn complementaire methoden:
- Plateau-analyse beantwoordt de vraag: "Hoe stabiel is het optimum bij kleine parameterverschuivingen?" Dit is een controle van parametrische robuustheid.
- Walk-forward beantwoordt de vraag: "Werken de parameters op data die de optimizer niet heeft gezien?" Dit is een controle van temporele robuustheid.
Een strategie kan slagen voor plateau-analyse (breed plateau) maar falen bij walk-forward (marktregime veranderd). En andersom — hij kan slagen voor walk-forward met vaste parameters maar een fragiel optimum hebben.
Aanbeveling: gebruik altijd beide methoden. Als een strategie slaagt voor plateau-analyse () en walk-forward () — is dit een sterk signaal van robuustheid. Meer details in het artikel over Walk-Forward-optimalisatie.
Om PnL-betrouwbaarheidsintervallen in elke fase te beoordelen, pas je Monte Carlo bootstrap toe. En om strategieën met verschillende actieve tijd correct te vergelijken, gebruik je de metric PnL per actieve tijd.
Aanbevelingen
Vóór optimalisatie
-
Beperk het aantal parameters. Hoe minder parameters — hoe betrouwbaarder het plateau. 5-7 parameters is een redelijk maximum. Bij 12 is al verhoogde voorzichtigheid geboden.
-
Stel zinvolle bereiken in. Stel
htf_entry_sellniet in van 0,001 tot 1,0 als het realistische bereik 0,005 tot 0,05 is. Onnodig brede bereiken creëren de illusie van een plateau. -
Gebruik voldoende trials. Voor 12 parameters minimaal 300-500 trials. Voor betrouwbare plateau-analyse — 1000+.
Tijdens optimalisatie
-
Let op convergentie. Als Optuna na 400 trials nog steeds aanzienlijk betere oplossingen vindt — is het proces niet geconvergeerd, en zal de plateau-analyse onbetrouwbaar zijn.
-
Gebruik pruning met voorzichtigheid. Agressieve pruning (MedianPruner) kan trials wegsnijden die er in vroege stappen slecht uitzien maar belangrijk zijn voor het opbouwen van een compleet beeld van het landschap.
Na optimalisatie
-
Genereer het plateaurapport automatisch. Integreer
generate_plateau_report()in de optimalisatiepijplijn. Vertrouw niet op visuele beoordeling — gebruik getallen. -
Controleer de top-5 parameters. Als fANOVA aantoont dat 3 parameters 80% van de variantie verklaren — kunnen de overige 9 minder grondig worden gecontroleerd.
-
Vergelijk met de baseline-strategie. Als de strategie met standaardparameters (zonder optimalisatie) +30% toont, en de geoptimaliseerde +55% — is het verschil slechts 25 procentpunten, en is het plateau waarschijnlijk breed. Als de standaard 0% toont, en de geoptimaliseerde +300% — hangt alle winstgevendheid af van precieze parameterafstemming.
-
Laatste controle — walk-forward. Plateau-analyse is een noodzakelijke maar geen voldoende voorwaarde voor robuustheid. Valideer altijd out-of-sample.
Conclusie
Parameteroptimalisatie is een krachtig hulpmiddel, maar zonder plateau-analyse is het een roulettespel. Je weet niet of je een stabiel patroon hebt gevonden of het model op ruis hebt afgestemd.
Drie regels van plateau-analyse:
-
Bereken de robustness score. Het product van gewogen plateaubreedtes geeft één getal dat de robuustheid van alle parameters samenvat. — groen licht.
-
Sensitivity ratio < 1 voor sleutelparameters. Als een verschuiving van 10% van de parameter minder dan 10% PnL-daling veroorzaakt — is de parameter robuust. Als meer — wees voorzichtig.
-
Visualiseer contourplots. Geen enkele metric kan het begrip van de vorm van het landschap vervangen. Een vlakke tafelberg — goed. Een scherpe naald — slecht.
Plateau-analyse duurt 5 minuten na optimalisatie en kan weken van onwinstgevende live trading besparen. Het is een verplichte stap tussen study.optimize() en het lanceren van de bot.
Nuttige links
- 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)
Citatie
@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.}
}
Auteurs
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.