수정본
Some checks failed
CI/CD Pipeline / test-backend (push) Has been cancelled
CI/CD Pipeline / test-frontend (push) Has been cancelled
CI/CD Pipeline / build-and-push (push) Has been cancelled

This commit is contained in:
2026-03-27 14:47:49 +09:00
parent 844ee8944f
commit c818810667
25 changed files with 2708 additions and 266 deletions

View File

@@ -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),
)