수정본
This commit is contained in:
@@ -47,6 +47,11 @@ class OptimizationResult:
|
||||
applied: bool
|
||||
apply_reason: str
|
||||
total_combinations: int
|
||||
# IS/OOS 검증 결과 (v2 추가)
|
||||
oos_sharpe: float = 0.0
|
||||
oos_win_rate: float = 0.0
|
||||
oos_mdd: float = 0.0
|
||||
is_oos_sharpe_ratio: float = 0.0 # OOS Sharpe / IS Sharpe (1.0에 가까울수록 건전)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -282,7 +287,7 @@ async def run_optimization(
|
||||
Returns:
|
||||
OptimizationResult
|
||||
"""
|
||||
logger.info(f"🔧 전략 최적화 시작 (trigger={trigger}, period={period_days}일)")
|
||||
logger.info(f"전략 최적화 시작 (trigger={trigger}, period={period_days}일)")
|
||||
|
||||
# 1. 현재 파라미터 저장 (롤백용)
|
||||
prev_params = await _get_current_params()
|
||||
@@ -310,16 +315,26 @@ async def run_optimization(
|
||||
total_combinations=0,
|
||||
)
|
||||
|
||||
# ── IS/OOS 기간 분할 (70% IS / 30% OOS, 날짜 기준) ──
|
||||
end_date = date.today()
|
||||
start_date = end_date - timedelta(days=period_days)
|
||||
is_days = int(period_days * 0.7)
|
||||
is_end_date = start_date + timedelta(days=is_days)
|
||||
oos_start_date = is_end_date + timedelta(days=1)
|
||||
oos_end_date = end_date
|
||||
|
||||
# 4. 현재 파라미터 기준 Sharpe 계산
|
||||
cur_score, cur_sharpe, cur_wr, cur_mdd = await _evaluate_params(
|
||||
prev_params, stock_codes, start_date, end_date, initial_capital
|
||||
logger.info(
|
||||
f" IS 기간: {start_date} ~ {is_end_date} ({is_days}일), "
|
||||
f"OOS 기간: {oos_start_date} ~ {oos_end_date} ({period_days - is_days}일)"
|
||||
)
|
||||
logger.info(f" 현재 성과 → Sharpe: {cur_sharpe:.3f}, 승률: {cur_wr:.1f}%, MDD: {cur_mdd:.1f}%")
|
||||
|
||||
# 5. 그리드 서치 (배치 5개씩 병렬)
|
||||
# 4. 현재 파라미터 기준 Sharpe 계산 (IS 기간)
|
||||
cur_score, cur_sharpe, cur_wr, cur_mdd = await _evaluate_params(
|
||||
prev_params, stock_codes, start_date, is_end_date, initial_capital
|
||||
)
|
||||
logger.info(f" 현재 IS 성과 -> Sharpe: {cur_sharpe:.3f}, 승률: {cur_wr:.1f}%, MDD: {cur_mdd:.1f}%")
|
||||
|
||||
# 5. 그리드 서치 — IS 기간에서 최적 파라미터 탐색 (배치 5개씩 병렬)
|
||||
best_score = cur_score
|
||||
best_params = dict(prev_params)
|
||||
best_sharpe = cur_sharpe
|
||||
@@ -330,7 +345,7 @@ async def run_optimization(
|
||||
for i in range(0, len(combinations), batch_size):
|
||||
batch = combinations[i:i + batch_size]
|
||||
tasks = [
|
||||
_evaluate_params(p, stock_codes, start_date, end_date, initial_capital)
|
||||
_evaluate_params(p, stock_codes, start_date, is_end_date, initial_capital)
|
||||
for p in batch
|
||||
]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
@@ -345,39 +360,65 @@ async def run_optimization(
|
||||
best_mdd = mdd
|
||||
best_params = {**prev_params, **combo}
|
||||
|
||||
logger.info(f" 최적 성과 → Sharpe: {best_sharpe:.3f}, 승률: {best_win_rate:.1f}%, MDD: {best_mdd:.1f}%")
|
||||
logger.info(f" IS 최적 성과 -> Sharpe: {best_sharpe:.3f}, 승률: {best_win_rate:.1f}%, MDD: {best_mdd:.1f}%")
|
||||
|
||||
# 6. 자동 적용 여부 결정 (Sharpe 10% 이상 개선)
|
||||
# 6. OOS 기간에서 최적 파라미터 검증 (과적합 탐지)
|
||||
oos_score, oos_sharpe, oos_wr, oos_mdd = await _evaluate_params(
|
||||
best_params, stock_codes, oos_start_date, oos_end_date, initial_capital
|
||||
)
|
||||
is_oos_ratio = (oos_sharpe / best_sharpe) if best_sharpe > 0 else 0.0
|
||||
logger.info(
|
||||
f" OOS 검증 -> Sharpe: {oos_sharpe:.3f}, 승률: {oos_wr:.1f}%, MDD: {oos_mdd:.1f}%, "
|
||||
f"IS/OOS Sharpe 비율: {is_oos_ratio:.2f}"
|
||||
)
|
||||
|
||||
if is_oos_ratio < 0.5 and best_sharpe > 0:
|
||||
logger.warning(
|
||||
f" [과적합 경고] OOS Sharpe / IS Sharpe = {is_oos_ratio:.2f} < 0.5 "
|
||||
"-- 파라미터가 IS 기간에 과적합되었을 가능성 높음"
|
||||
)
|
||||
|
||||
# 7. 자동 적용 여부 결정
|
||||
# 조건: IS Sharpe 10% 이상 개선 AND OOS Sharpe > 0 (OOS에서도 양의 성과)
|
||||
improvement_threshold = 1.1
|
||||
should_apply = (
|
||||
_is_improved = (
|
||||
cur_sharpe > 0
|
||||
and best_sharpe >= cur_sharpe * improvement_threshold
|
||||
and best_params != prev_params
|
||||
)
|
||||
if cur_sharpe <= 0 and best_sharpe > 0:
|
||||
should_apply = True
|
||||
_is_improved = True
|
||||
|
||||
# OOS 검증 게이트: OOS Sharpe > 0 이어야 실전 적용 허가
|
||||
_oos_passed = oos_sharpe > 0
|
||||
_should_apply = _is_improved and _oos_passed
|
||||
|
||||
applied = False
|
||||
apply_reason = ""
|
||||
|
||||
if should_apply:
|
||||
if _should_apply:
|
||||
improvement_pct = ((best_sharpe - cur_sharpe) / max(abs(cur_sharpe), 0.001)) * 100
|
||||
apply_reason = f"Sharpe {improvement_pct:.1f}% 향상 ({cur_sharpe:.3f} → {best_sharpe:.3f})"
|
||||
apply_reason = (
|
||||
f"IS Sharpe {improvement_pct:.1f}% 향상 ({cur_sharpe:.3f} -> {best_sharpe:.3f}), "
|
||||
f"OOS Sharpe: {oos_sharpe:.3f}, IS/OOS 비율: {is_oos_ratio:.2f}"
|
||||
)
|
||||
# 최적화된 파라미터만 적용 (prev와 다른 키만)
|
||||
changed = {k: v for k, v in best_params.items() if str(v) != str(prev_params.get(k, ""))}
|
||||
if changed:
|
||||
applied = await apply_params(changed, reason="auto_optimizer")
|
||||
if applied:
|
||||
logger.info(f"✅ 자동 적용 완료: {apply_reason}")
|
||||
logger.info(f"자동 적용 완료: {apply_reason}")
|
||||
else:
|
||||
if cur_sharpe <= 0 and best_sharpe <= 0:
|
||||
apply_reason = "insufficient_data"
|
||||
elif not _oos_passed and _is_improved:
|
||||
apply_reason = f"OOS 검증 실패 (OOS Sharpe: {oos_sharpe:.3f} <= 0, 과적합 의심)"
|
||||
elif best_sharpe < cur_sharpe * improvement_threshold:
|
||||
pct = ((best_sharpe - cur_sharpe) / max(abs(cur_sharpe), 0.001)) * 100
|
||||
apply_reason = f"개선 미달 ({pct:+.1f}% < 10%)"
|
||||
apply_reason = f"IS 개선 미달 ({pct:+.1f}% < 10%)"
|
||||
else:
|
||||
apply_reason = "no_improvement"
|
||||
logger.info(f"⏭️ 자동 적용 건너뜀: {apply_reason}")
|
||||
logger.info(f"자동 적용 건너뜀: {apply_reason}")
|
||||
|
||||
return OptimizationResult(
|
||||
best_params=best_params,
|
||||
@@ -390,6 +431,10 @@ async def run_optimization(
|
||||
applied=applied,
|
||||
apply_reason=apply_reason,
|
||||
total_combinations=total_combos,
|
||||
oos_sharpe=oos_sharpe,
|
||||
oos_win_rate=oos_wr,
|
||||
oos_mdd=oos_mdd,
|
||||
is_oos_sharpe_ratio=round(is_oos_ratio, 4),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user