플래토 분석: 견고한 최적해와 과적합을 구별하는 방법
「환상 없는 백테스트」 시리즈 제6편
study.optimize()를 실행하고, Optuna가 PnL +87%인 파라미터 세트를 찾았다. 흥분하며 전략을 프로덕션에 투입할 준비를 한다. 라이브 트레이딩 2주 후, PnL은 거의 제로. 무슨 일이 일어난 걸까?
옵티마이저는 파라미터 공간에서 바늘의 끝을 찾은 것이다. 파라미터는 과거 거래 시퀀스에 완벽하게 맞추어져 있지만, 시장 상황이 조금만 바뀌어도 전체 구조가 무너진다. 이것이 전형적인 과적합이며, 투입 전에 감지할 수 있었다.
이전 기사에서는 좌표 하강법과 베이지안 최적화를 비교하고 Optuna가 더 효율적으로 최적해를 찾는 이유를 보여주었다. 오늘은 다음 단계: 찾은 최적해가 노이즈에 대한 피팅이 아닌 견고한 것인지 확인하는 방법이다.
"최적" 파라미터를 찾는 것이 왜 절반의 작업에 불과한가
진정한 최적해를 찾아 광대한 다차원 파라미터 공간을 탐색하는 옵티마이저
전략 파라미터 최적화는 다차원 공간에서의 최대값 탐색이다. 문제는 최대값에 두 가지 유형이 있다는 것이다:
-
플래토 — 파라미터 변동에 대해 PnL이 일관되게 높은 넓고 평평한 영역. 시장 상황이 변하여 유효 파라미터가 10-20% 이동하더라도 전략은 계속 수익을 낸다.
-
뾰족한 피크 — 정확한 파라미터 값에서만 PnL이 높은 좁은 정상. 한 단계만 이동해도 수익성이 붕괴된다. 이것은 거의 확실히 과적합이다: 옵티마이저가 안정적인 패턴이 아닌 과거 데이터의 아티팩트를 찾은 것이다.
등산 비유: 플래토는 안전하게 걸어 다닐 수 있는 산정 고원이다. 뾰족한 피크는 균형을 잡는 것만 가능한 바늘 끝이다.
뾰족한 피크 vs 평평한 플래토 — 시각적 직관
왼쪽: 견고한 플래토(완만한 경사의 넓은 테이블 마운틴). 오른쪽: 취약한 뾰족한 피크(깊은 계곡에 둘러싸인 바늘 끝)
두 전략 파라미터를 축으로, PnL을 색상으로 나타낸 등고선 지도를 상상해 보자. 두 가지 패턴은 시각적으로 쉽게 구별된다:
플래토 (견고한 최적해):
- 같은 색상의 넓은 영역
- PnL 수준 간의 부드러운 전환
- 등고선 간격이 넓음
- 최적값에서 +/-20% 이동해도 PnL 변화는 10% 이내
히트맵을 상상해 보자: 중앙에 전체 맵의 약 1/3 크기의 밝은 노란색 직사각형이 있다. 색상은 가장자리로 갈수록 점차 주황색, 빨간색으로 변한다. 최적해는 점이 아닌 영역이다.
뾰족한 피크 (과적합):
- 차가운 색에 둘러싸인 좁고 밝은 점
- 급격한 전환: 최적해 바로 옆에서 붕괴
- 등고선이 빽빽한 동심원으로 압축
- +/-5% 이동 시 PnL이 50% 이상 하락
같은 히트맵을 상상하되, 중앙에는 작은 노란색 점이 있고 즉시 파란색과 보라색에 둘러싸여 있다. 단 하나의 "올바른" 파라미터 조합.
파라미터 민감도 분석
개별 파라미터 값에 대한 PnL 의존성을 보여주는 슬라이스 플롯 — 넓은 밴드는 견고성, 좁은 클러스터는 취약성을 나타냄
1차원 분석: PnL vs 단일 파라미터
가장 간단한 접근법 — 하나의 파라미터를 제외한 모든 파라미터를 고정하고 PnL이 그 값에 어떻게 의존하는지 본다. Optuna는 이를 위한 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()
슬라이스 플롯에서 확인할 것:
- 견고한 파라미터: 점 구름이 최적값 근처에서 넓은 수평 밴드를 형성한다. 최상위 트라이얼이 파라미터 값의 넓은 범위에 분포한다.
- 취약한 파라미터: 최상위 트라이얼이 좁은 범위에 집중된다. 파라미터를 한두 단계 이동하면 수익성이 붕괴된다.
2차원 분석: 등고선 플롯 (히트맵)
등고선 플롯은 두 파라미터의 상호작용을 동시에 보여준다. 파라미터가 독립적으로 작용하는 경우는 드물며 — 진입과 청산 임계값, 타임프레임, 포지션 크기는 상호 연결되어 있기 때문에 플래토 분석의 핵심 도구이다.
from optuna.visualization import plot_contour
fig = plot_contour(study, params=["htf_entry_sell", "htf_exit_buy"])
fig.show()
견고한 파라미터 쌍의 등고선 플롯은 구릉 평야의 지형도처럼 보인다: 부드럽고 넓은 등고선, 같은 색의 넓은 영역. 취약한 쌍의 등고선 플롯은 화산추의 지도처럼 보인다: 단일 점을 중심으로 한 빽빽한 동심원.
12개의 분리 파라미터를 가진 전략의 경우, 개의 쌍별 등고선 플롯이 나온다. 모두 조사할 필요는 없다 — Optuna가 가장 중요하다고 평가한 파라미터부터 시작하자.
다차원 분석: 파라미터 중요도 순위
Optuna는 각 파라미터의 목적 함수에 대한 기여도를 추정할 수 있다:
from optuna.visualization import plot_param_importances
fig = plot_param_importances(study)
fig.show()
파라미터 중요도 차트는 수평 히스토그램이다. 파라미터는 PnL 분산에 대한 기여도 내림차순으로 순위가 매겨진다. 보통 상위 3-4개의 파라미터가 분산의 70-80%를 설명한다.
규칙: 파라미터가 PnL 분산의 2% 미만을 설명한다면, 그 값은 결과에 실질적으로 무관하다 — 정의상 견고하다. 플래토 분석은 가장 중요한 상위 5개 파라미터에 집중하자.
Optuna 시각화 도구
파라미터 상호작용 랜드스케이프를 보여주는 등고선 히트맵과 중요도 순위
plot_slice — 1차원 슬라이스
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()
결과 — 산점도 그리드. 각 서브플롯은 단일 파라미터 값(X축)에 대한 목적 함수 값(PnL, Y축)을 보여준다. 점은 개별 트라이얼이다. 견고한 파라미터의 경우 최상위 점(최고 PnL)이 넓은 X 범위에 분포한다. 취약한 파라미터의 경우 — 좁은 열에 그룹화된다.
plot_contour — 2차원 등고선
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()
각 등고선 플롯은 두 파라미터를 축으로 하는 히트맵이다. 색상은 파라미터 공간의 특정 영역에서의 평균 PnL을 인코딩한다. 노란색/녹색 — 높은 PnL, 파란색/보라색 — 낮은 PnL. 등고선은 같은 PnL의 점을 연결한다.
plot_param_importances — 파라미터 기여도
from optuna.visualization import plot_param_importances
fig = plot_param_importances(
study,
evaluator=optuna.importance.FanovaImportanceEvaluator()
)
fig.show()
fANOVA(functional ANOVA)는 목적 함수의 분산을 파라미터와 그 상호작용으로 분해한다. 비선형 효과를 고려하기 때문에 단순 상관보다 강력하다.
정량적 플래토 메트릭
민감도 비율, 플래토 폭, 견고성 점수 — 플래토 품질을 정량화하는 세 가지 메트릭
시각적 평가는 주관적이다. 숫자가 필요하다. "플래토"의 개념을 정량화하는 세 가지 메트릭을 소개한다.
민감도 비율
PnL 변화와 파라미터 변화의 비율:
여기서 은 파라미터 가 최적값에서 만큼 벗어났을 때의 PnL 하락량.
해석:
- — 파라미터가 견고: 10% 이동 시 PnL 하락이 5% 미만
- — 중간 수준의 민감도
- — 파라미터가 취약: 10% 이동 시 PnL이 20% 이상 하락
플래토 폭
PnL이 최적값의 이내에 유지되는 파라미터 영역의 폭:
상대 플래토 폭:
분모는 파라미터의 전체 탐색 범위.
해석:
- — 플래토가 10% 임계값에서 범위의 30% 이상을 커버. 견고한 파라미터.
- — 플래토가 범위의 5% 미만. 레드 플래그.
견고성 점수
모든 파라미터에 걸친 통합 메트릭:
여기서 는 fANOVA에 의한 파라미터 의 정규화 중요도().
가중 폭의 곱은 엄격한 메트릭이다: 중요한 파라미터 하나라도 좁은 플래토를 가지면 이 낮아진다. 중요하지 않은 파라미터(가 작은)는 거의 영향을 미치지 않는다.
해석:
- — 전략이 견고
- — 추가 검증 필요 (워크포워드)
- — 과적합 가능성이 매우 높음
자동 플래토 감지를 위한 Python 코드
파라미터 랜드스케이프를 스캔하여 견고한 플래토와 취약한 피크를 식별하는 자동화 시스템
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"
),
}
사용 방법
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()
출력 예시:
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%
분리 전략의 실전 예시
전략 A(넓은 플래토, 견고), 전략 B(중간), 전략 C(뾰족한 피크, 과적합) 비교
12개의 분리 파라미터를 가진 3개의 전략을 검토한다. 각 전략은 500 트라이얼의 Optuna 최적화를 거쳤다.
전략 A (PnL 약 55%, 거래 약 500건, 시간 약 15%)
전략 A의 파라미터는 넓은 플래토를 형성한다. 핵심 파라미터 htf_entry_sell을 살펴보자:
- 최적값: 0.020
- 0.015에서의 PnL: +51% (7% 하락)
- 0.025에서의 PnL: +49% (11% 하락)
- 0.010에서의 PnL: +43% (22% 하락)
- 0.030에서의 PnL: +41% (25% 하락)
1차원 플롯(X축 — htf_entry_sell 값, Y축 — PnL)을 상상하면, 평평한 꼭대기를 가진 완만한 포물선이 보인다. 0.010-0.030 범위가 플래토로, PnL이 최적값의 +/-25% 이내에 유지된다.
민감도 비율: — 견고.
10% 임계값에서의 플래토 폭: 0.013에서 0.027, .
전략 B (PnL 약 25%, 거래 약 40건, 시간 약 5%)
전략 B는 적은 거래 수로 최적화되었다. 파라미터 htf_entry_sell:
- 최적값: 0.018
- 0.015에서의 PnL: +24% (4% 하락)
- 0.025에서의 PnL: +9% (64% 하락)
- 0.012에서의 PnL: +11% (56% 하락)
플롯에서 — 비대칭적이고 가파른 곡선. 플래토는 0.015-0.020의 좁은 범위에만 존재한다. 최적값 오른쪽은 절벽이다.
민감도 비율: — 중간 수준의 민감도이지만, 40건의 거래에서는 이것이 레드 플래그다. 작은 샘플 + 좁은 플래토 = 높은 과적합 확률.
10% 임계값에서의 플래토 폭: 0.016에서 0.020, .
전략 C (PnL 약 300%, 거래 약 400건, 시간 약 45%)
전략 C는 놀라운 PnL을 보여주지만, 플래토 분석에서 문제가 드러난다:
htf_entry_sell의 최적값: 0.022- 0.020에서의 PnL: +295% (2% 하락)
- 0.025에서의 PnL: +142% (53% 하락)
- 0.019에서의 PnL: +128% (57% 하락)
플롯에서 — 특징적인 "바늘": 0.022에서 매우 높은 피크, 모든 방향으로 급격한 하락. 등고선 플롯에서는 차가운 색에 바로 둘러싸인 밝은 점이 나타난다.
민감도 비율: — 취약. 400건의 거래에도 불구하고, 전략이 단일 파라미터의 정확한 값에 과도하게 의존한다.
10% 임계값에서의 플래토 폭: 0.021에서 0.023, .
요약 표
| 전략 | PnL | 거래 수 | 민감도 | 플래토 폭 | 견고성 점수 | 판정 |
|---|---|---|---|---|---|---|
| 전략 A | +55% | 약 500 | 0.44 | 35% | 0.148 | 견고 |
| 전략 B | +25% | 약 40 | 1.64 | 10% | 0.032 | 확인 필요 (소규모 샘플) |
| 전략 C | +300% | 약 400 | 3.79 | 5% | 0.008 | 과적합 |
역설: PnL +300%인 전략 C가 최악의 견고성 점수를 가진다. "소박한" +55%의 전략 A가 가장 견고하다. 이것이 플래토 분석의 전형적인 결과다: 인상적인 숫자는 종종 취약성을 감춘다.
각 전략의 신뢰 구간은 몬테카를로 부트스트랩으로 추가 검증할 수 있다 — 거래 리샘플링 시 PnL의 분산을 보여준다.
3D 시각화와 히트맵
두 파라미터에 대한 PnL의 3D 표면 플롯과 바닥면에 투영된 등고선
가장 중요한 파라미터 쌍에 대해 3D 표면과 히트맵을 구축하면 유용하다. 랜드스케이프의 형태를 직관적으로 이해할 수 있다.
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()
견고한 전략의 3D 표면 플롯은 테이블 마운틴과 유사하다 — 완만한 경사의 평평한 꼭대기. 취약한 전략의 경우 — 마터호른과 같은 뾰족한 피크. 히트맵은 3D 뷰를 보완하며, 같은 정보를 등고선이 있는 상면 투영으로 보여준다.
레드 플래그: 최적화 결과가 의심스러울 때
최적화 결과에서 잠재적 과적합을 나타내는 경고 지표
최적화가 실제 패턴이 아닌 과적합을 찾았음을 나타내는 8가지 신호:
1. 핵심 파라미터의 민감도 비율 > 2
10% 파라미터 이동으로 PnL이 20% 이상 하락하면 — 최적해는 취약하다.
2. 플래토 폭 < 탐색 범위의 10%
"좋은" 영역이 탐색 범위의 10% 미만을 차지하면 — 옵티마이저가 아티팩트를 찾았을 가능성이 높다.
3. 상위 3 트라이얼의 PnL이 중앙값의 2-3배
최상위 트라이얼이 "언덕 꼭대기"가 아닌 나머지에 대한 이상치라면 — 플래토가 아니다.
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. 적은 거래 수 (< 50)와 높은 PnL
작은 샘플 + 높은 PnL = 추정치의 높은 분산. 40건 거래로의 플래토 분석은 그 자체가 신뢰성이 부족하다. 이러한 전략에는 몬테카를로 부트스트랩이 필수적이다.
5. 하나의 "마법" 파라미터 조합
등고선 플롯이 회색 필드 안에 하나의 밝은 점을 보여준다면 — 이것은 전략이 아니라 데이터에 맞춰진 조합이다.
6. 파라미터가 너무 많음
12개의 파라미터에 각각 10개의 값이 있으면, 탐색 공간은 개의 조합을 포함한다. Optuna는 약 500개를 탐색한다. 이런 공간에서 "좋은" 아티팩트를 찾을 확률은 높다. 파라미터가 많을수록 플래토 분석은 더 엄격해야 한다.
7. 아웃오브샘플에서 PnL 급락
인샘플 PnL이 +87%이고 워크포워드가 +12%를 보여주면 — 최적화가 파라미터를 훈련 기간에 맞춰버린 것이다. 자세한 내용은 워크포워드 최적화 기사를 참조.
8. 파라미터가 범위 경계에 "고정"됨
최적값이 탐색 그리드의 경계와 일치하면 — 최적해가 범위 밖에 있을 수 있다. 범위를 확장하고 최적화를 다시 실행하자.
자동 플래토 분석 리포트
모든 것을 각 최적화 후 생성되는 단일 리포트로 통합한다:
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))
출력 예시:
{
"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
}
}
}
워크포워드 검증과의 관계
파라메트릭 견고성(플래토 분석)과 시간적 견고성(워크포워드) — 두 가지 보완적 검증 시스템
플래토 분석과 워크포워드 검증(WFO)은 보완적인 방법이다:
- 플래토 분석은 다음 질문에 답한다: "최적해가 작은 파라미터 이동에 대해 얼마나 안정적인가?" 이것은 파라메트릭 견고성 검증이다.
- 워크포워드는 다음 질문에 답한다: "파라미터가 옵티마이저가 보지 못한 데이터에서도 작동하는가?" 이것은 시간적 견고성 검증이다.
전략이 플래토 분석을 통과하더라도(넓은 플래토) 워크포워드에서 실패할 수 있다(시장 레짐이 변경됨). 반대로 — 고정 파라미터로 워크포워드를 통과하더라도 취약한 최적해를 가질 수 있다.
권장사항: 항상 두 방법을 모두 사용하라. 전략이 플래토 분석() 그리고 워크포워드()를 모두 통과하면 — 이것은 견고성의 강력한 신호다. 자세한 내용은 워크포워드 최적화 기사를 참조.
각 단계에서의 PnL 신뢰 구간을 평가하려면 몬테카를로 부트스트랩을 적용하라. 활성 시간이 다른 전략을 올바르게 비교하려면 활성 시간당 PnL 메트릭을 사용하라.
권장사항
최적화 전
-
파라미터 수를 제한하라. 파라미터가 적을수록 플래토의 신뢰성이 높다. 5-7개가 합리적인 최대치. 12개는 이미 경계를 높여야 한다.
-
의미 있는 범위를 설정하라. 현실적 범위가 0.005
0.05라면1.0으로 설정하지 마라. 불필요하게 넓은 범위는 플래토의 환상을 만든다.htf_entry_sell을 0.001 -
충분한 트라이얼 수를 사용하라. 12개 파라미터의 경우 최소 300-500 트라이얼. 신뢰할 수 있는 플래토 분석을 위해서는 1000 이상.
최적화 중
-
수렴을 주시하라. Optuna가 400 트라이얼 후에도 상당히 더 좋은 솔루션을 계속 찾는다면 — 프로세스가 수렴하지 않았으며 플래토 분석은 신뢰할 수 없다.
-
가지치기는 신중히 사용하라. 공격적인 가지치기(MedianPruner)는 초기 단계에서 나빠 보이지만 완전한 랜드스케이프 구축에 중요한 트라이얼을 제거할 수 있다.
최적화 후
-
플래토 리포트를 자동 생성하라.
generate_plateau_report()를 최적화 파이프라인에 통합하라. 시각적 평가에 의존하지 말고 숫자를 사용하라. -
상위 5개 파라미터를 확인하라. fANOVA가 3개 파라미터가 분산의 80%를 설명한다고 보여주면 — 나머지 9개는 덜 철저히 확인해도 된다.
-
기준 전략과 비교하라. 기본 파라미터(최적화 없음)의 전략이 +30%를 보이고 최적화 후 +55%라면 — 차이는 25pp에 불과하며 플래토는 아마 넓을 것이다. 기본이 0%이고 최적화 후 +300%라면 — 모든 수익성이 정확한 파라미터 피팅에 의존한다.
-
최종 확인 — 워크포워드. 플래토 분석은 견고성의 필요 조건이지만 충분 조건은 아니다. 항상 아웃오브샘플로 검증하라.
결론
파라미터 최적화는 강력한 도구이지만, 플래토 분석 없이는 룰렛이다. 안정적인 패턴을 찾았는지, 노이즈에 모델을 피팅했는지 알 수 없다.
플래토 분석의 3가지 규칙:
-
견고성 점수를 계산하라. 가중 플래토 폭의 곱이 모든 파라미터의 견고성을 요약하는 단일 숫자를 제공한다. — 진행 가능.
-
핵심 파라미터의 민감도 비율 < 1. 10% 파라미터 이동으로 PnL 하락이 10% 미만이면 — 파라미터는 견고하다. 그 이상이면 — 주의가 필요하다.
-
등고선 플롯을 시각화하라. 랜드스케이프 형태의 이해를 대체할 수 있는 메트릭은 없다. 평평한 테이블 마운틴 — 좋다. 뾰족한 바늘 — 나쁘다.
플래토 분석은 최적화 후 5분이 걸리며 수 주간의 비수익 라이브 트레이딩을 방지할 수 있다. study.optimize()와 봇 투입 사이의 필수 단계다.
참고 링크
- 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.}
}
MarketMaker.cc Team
퀀트 리서치 및 전략