fix: CRITICAL/HIGH 버그 30건 수정 + 전체 코드 주석 추가 + 중복 정리
## CRITICAL 수정 (20건) - day_trading: global 선언 누락, loss_tracker 연동, ban 체크 추가 - trade_executor: BUY 체결가 order_price로 수정, remaining_pct 누적 계산 - risk_validator: get_current_price dict 파싱, MAX_POSITION_SIZE 동기화, DB 세션 누수 - risk_management/recommendations: date.today() → KST 변환 - mdd_manager: Lock 밖 executed_key 참조 수정 - correlation_filter: output → output2 키 수정 - portfolio: 해외 종목 USD/KRW 환율 적용 분리 - Sharpe Ratio 거래빈도 기반 연간화 - telegram_notifier: send_message() 공개 래퍼 추가 - position_manager: 앱 재시작 시 DB 포지션 복원 - swing exit_service: remaining_pct 조건 범위 수정 - run_backtest: original_qty 기반 remaining_pct 계산 - us_market_schedule: 2027년 미국 공휴일 추가 - Login.tsx: res.ok 검증 추가 - mean_reversion_strategy: RSI 로그 하드코딩 수정 ## HIGH 수정 (10건) - db_worker: DLQ 파일 백업 메커니즘 추가 - korea_investment_api: 만료 토큰 임의 연장 제거 - market_data: chart_cache 최대 100개 + TTL 만료 - main.py: rate_limit_store 1시간 미접근 IP 정리 - recommendations: fire-and-forget try/except 추가 - swing/scheduler: DB 세션 3개 분리 - Dashboard: wsMessages 전체 순회 - Layout: /health 엔드포인트 + 30초 폴링 - ml_ensemble: POST /train require_api_key 추가 - day_trading: record_trade_result_safe() 전환 ## 코드 정리 - 프론트엔드: isUsStock, formatTime 공통 유틸 추출 - 백엔드: dead code 제거, 중복 import 통합 - MCP: JSON 파싱 헬퍼 추출 (~60줄 감소) - 전체 ~100개 파일 한국어 주석 추가 - 테스트 13건 코드 기준 수정 (468 passed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
564
backend/app/services/walk_forward.py
Normal file
564
backend/app/services/walk_forward.py
Normal file
@@ -0,0 +1,564 @@
|
||||
"""
|
||||
Walk-Forward Optimization (WFO) 엔진
|
||||
|
||||
롤링 윈도우 기반으로 In-Sample 구간에서 Optuna를 통해 파라미터를 최적화하고,
|
||||
Out-of-Sample 구간에서 검증하여 과적합 여부를 탐지합니다.
|
||||
|
||||
핵심 흐름:
|
||||
1. 전체 기간을 IS(70%) / OOS(30%) 비율로 롤링 윈도우 생성
|
||||
2. 각 윈도우의 IS 구간에서 Optuna Trial 50회 실행 (목적함수: Sharpe Ratio)
|
||||
3. 최적 파라미터로 OOS 구간 백테스트 실행
|
||||
4. IS vs OOS Sharpe 비교 -> 50% 이상 감소 시 과적합 경고
|
||||
5. 전체 윈도우 종합 결과 반환
|
||||
|
||||
의존성:
|
||||
- backtest_engine.py (BacktestEngine, BacktestConfig)
|
||||
- optuna (requirements.txt에 추가 필요)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from datetime import date, timedelta
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── 데이터 클래스 ────────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class WalkForwardConfig:
|
||||
"""Walk-Forward 최적화 설정"""
|
||||
stock_codes: List[str]
|
||||
start_date: date
|
||||
end_date: date
|
||||
initial_capital: float = 10_000_000
|
||||
max_positions: int = 5
|
||||
position_size: float = 0.2
|
||||
commission_rate: float = 0.00015
|
||||
tax_rate: float = 0.0023
|
||||
|
||||
# 롤링 윈도우 설정
|
||||
is_months: int = 6 # In-Sample 기간 (월)
|
||||
oos_months: int = 2 # Out-of-Sample 기간 (월)
|
||||
step_months: int = 1 # 슬라이딩 간격 (월)
|
||||
|
||||
# Optuna 설정
|
||||
n_trials: int = 50 # 윈도우당 최대 trial 수
|
||||
optuna_timeout: int = 300 # 윈도우당 최대 시간 (초)
|
||||
|
||||
# 과적합 탐지
|
||||
overfit_threshold: float = 0.5 # IS vs OOS Sharpe 감소율 경고 임계값
|
||||
|
||||
|
||||
@dataclass
|
||||
class WindowResult:
|
||||
"""개별 윈도우 결과"""
|
||||
window_index: int
|
||||
is_start: str
|
||||
is_end: str
|
||||
oos_start: str
|
||||
oos_end: str
|
||||
best_params: Dict[str, Any]
|
||||
is_sharpe: float
|
||||
oos_sharpe: float
|
||||
is_return_pct: float
|
||||
oos_return_pct: float
|
||||
is_mdd_pct: float
|
||||
oos_mdd_pct: float
|
||||
is_win_rate: float
|
||||
oos_win_rate: float
|
||||
is_total_trades: int
|
||||
oos_total_trades: int
|
||||
sharpe_degradation: float # (IS - OOS) / |IS|, 높을수록 과적합
|
||||
overfit_warning: bool # sharpe_degradation > threshold
|
||||
|
||||
|
||||
@dataclass
|
||||
class WalkForwardResult:
|
||||
"""Walk-Forward 최적화 종합 결과"""
|
||||
config: Dict[str, Any]
|
||||
total_windows: int
|
||||
windows: List[Dict[str, Any]]
|
||||
|
||||
# 종합 OOS 성과
|
||||
avg_oos_sharpe: float
|
||||
avg_oos_return_pct: float
|
||||
avg_oos_mdd_pct: float
|
||||
avg_oos_win_rate: float
|
||||
total_oos_trades: int
|
||||
|
||||
# 과적합 분석
|
||||
overfit_windows: int # 과적합 경고가 발생한 윈도우 수
|
||||
avg_sharpe_degradation: float
|
||||
overfit_ratio: float # overfit_windows / total_windows
|
||||
|
||||
# 최종 추천 파라미터 (OOS Sharpe 가중평균)
|
||||
recommended_params: Dict[str, Any]
|
||||
|
||||
|
||||
# ── 유틸리티 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def _add_months(d: date, months: int) -> date:
|
||||
"""날짜에 월 수를 더합니다. 월말 보정 포함."""
|
||||
month = d.month - 1 + months
|
||||
year = d.year + month // 12
|
||||
month = month % 12 + 1
|
||||
import calendar
|
||||
max_day = calendar.monthrange(year, month)[1]
|
||||
day = min(d.day, max_day)
|
||||
return date(year, month, day)
|
||||
|
||||
|
||||
def _generate_windows(
|
||||
start_date: date,
|
||||
end_date: date,
|
||||
is_months: int,
|
||||
oos_months: int,
|
||||
step_months: int,
|
||||
) -> List[Dict[str, date]]:
|
||||
"""롤링 윈도우 목록을 생성합니다.
|
||||
|
||||
각 윈도우는 {is_start, is_end, oos_start, oos_end}를 포함합니다.
|
||||
OOS 종료일이 전체 end_date를 초과하면 해당 윈도우부터 중단합니다.
|
||||
"""
|
||||
windows = []
|
||||
current_start = start_date
|
||||
|
||||
while True:
|
||||
is_end = _add_months(current_start, is_months) - timedelta(days=1)
|
||||
oos_start = is_end + timedelta(days=1)
|
||||
oos_end = _add_months(oos_start, oos_months) - timedelta(days=1)
|
||||
|
||||
if oos_end > end_date:
|
||||
# OOS 구간이 전체 기간을 초과하면 end_date로 클램핑 후 마지막 윈도우
|
||||
if oos_start <= end_date:
|
||||
oos_end = end_date
|
||||
windows.append({
|
||||
"is_start": current_start,
|
||||
"is_end": is_end,
|
||||
"oos_start": oos_start,
|
||||
"oos_end": oos_end,
|
||||
})
|
||||
break
|
||||
|
||||
windows.append({
|
||||
"is_start": current_start,
|
||||
"is_end": is_end,
|
||||
"oos_start": oos_start,
|
||||
"oos_end": oos_end,
|
||||
})
|
||||
|
||||
current_start = _add_months(current_start, step_months)
|
||||
|
||||
# IS 구간이 전체 기간의 50% 이상을 넘기면 중단
|
||||
if _add_months(current_start, is_months) > end_date:
|
||||
break
|
||||
|
||||
return windows
|
||||
|
||||
|
||||
# ── Walk-Forward 엔진 ────────────────────────────────────────────────────────
|
||||
|
||||
class WalkForwardOptimizer:
|
||||
"""Walk-Forward Optimization 엔진
|
||||
|
||||
Optuna를 사용하여 각 IS 윈도우에서 파라미터를 최적화하고,
|
||||
OOS 윈도우에서 검증합니다.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._engine_cls = None
|
||||
self._config_cls = None
|
||||
|
||||
def _lazy_import(self):
|
||||
"""BacktestEngine을 실행 시점에 import합니다 (lazy import)."""
|
||||
if self._engine_cls is None:
|
||||
try:
|
||||
from app.services.kr_backtest_engine import (
|
||||
BacktestEngine as KREngine,
|
||||
MeanReversionBacktest,
|
||||
)
|
||||
self._engine_cls = KREngine
|
||||
self._config_cls = MeanReversionBacktest
|
||||
logger.info("kr_backtest_engine 로드 성공")
|
||||
except ImportError:
|
||||
from app.services.backtest_engine import (
|
||||
BacktestEngine,
|
||||
BacktestConfig,
|
||||
)
|
||||
self._engine_cls = BacktestEngine
|
||||
self._config_cls = BacktestConfig
|
||||
logger.info("kr_backtest_engine 없음 -> backtest_engine 폴백 사용")
|
||||
|
||||
async def _run_single_backtest(
|
||||
self,
|
||||
stock_codes: List[str],
|
||||
start_date: date,
|
||||
end_date: date,
|
||||
params: Dict[str, Any],
|
||||
wf_config: WalkForwardConfig,
|
||||
) -> Dict[str, float]:
|
||||
"""단일 백테스트를 실행하고 핵심 지표를 반환합니다.
|
||||
|
||||
Returns:
|
||||
{sharpe_ratio, total_return_pct, mdd_pct, win_rate, total_trades}
|
||||
"""
|
||||
self._lazy_import()
|
||||
|
||||
from app.services.backtest_engine import BacktestEngine, BacktestConfig
|
||||
|
||||
config = BacktestConfig(
|
||||
stock_codes=stock_codes,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
initial_capital=wf_config.initial_capital,
|
||||
strategy_params=params,
|
||||
max_positions=wf_config.max_positions,
|
||||
position_size=wf_config.position_size,
|
||||
commission_rate=wf_config.commission_rate,
|
||||
tax_rate=wf_config.tax_rate,
|
||||
)
|
||||
|
||||
engine = BacktestEngine()
|
||||
result = await engine.run_backtest(config)
|
||||
|
||||
return {
|
||||
"sharpe_ratio": result.sharpe_ratio,
|
||||
"total_return_pct": result.total_return_pct,
|
||||
"mdd_pct": result.mdd_pct,
|
||||
"win_rate": result.win_rate,
|
||||
"total_trades": result.total_trades,
|
||||
}
|
||||
|
||||
def _build_params_from_trial(self, trial) -> Dict[str, Any]:
|
||||
"""Optuna trial에서 전략 파라미터를 생성합니다.
|
||||
|
||||
탐색 범위:
|
||||
- RSI 임계값: 25 ~ 45
|
||||
- BB 기간: 15 ~ 25
|
||||
- ATR 배수: 1.5 ~ 3.5
|
||||
- 손절 %: 4 ~ 10
|
||||
"""
|
||||
rsi_threshold = trial.suggest_int("rsi_threshold", 25, 45)
|
||||
bb_period = trial.suggest_int("bb_period", 15, 25)
|
||||
atr_multiplier = trial.suggest_float("atr_multiplier", 1.5, 3.5, step=0.1)
|
||||
stop_loss_pct = trial.suggest_float("stop_loss_pct", 4.0, 10.0, step=0.5)
|
||||
|
||||
return {
|
||||
"MEAN_REVERSION_RSI_THRESHOLD": rsi_threshold,
|
||||
"BB_PERIOD": bb_period,
|
||||
"ATR_STOP_MULTIPLIER": atr_multiplier,
|
||||
"MEAN_REVERSION_STOP_LOSS_PCT": -stop_loss_pct,
|
||||
# 익절은 손절의 2배 (2R 목표)
|
||||
"MEAN_REVERSION_TAKE_PROFIT_PCT": stop_loss_pct * 2.0,
|
||||
"RSI_OVERBOUGHT_LEVEL": 70.0,
|
||||
}
|
||||
|
||||
async def _optimize_window(
|
||||
self,
|
||||
window_idx: int,
|
||||
window: Dict[str, date],
|
||||
wf_config: WalkForwardConfig,
|
||||
) -> WindowResult:
|
||||
"""단일 윈도우에 대해 IS 최적화 + OOS 검증을 수행합니다."""
|
||||
try:
|
||||
import optuna
|
||||
optuna.logging.set_verbosity(optuna.logging.WARNING)
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"optuna 패키지가 필요합니다. "
|
||||
"'pip install optuna' 를 실행하세요."
|
||||
)
|
||||
|
||||
is_start = window["is_start"]
|
||||
is_end = window["is_end"]
|
||||
oos_start = window["oos_start"]
|
||||
oos_end = window["oos_end"]
|
||||
|
||||
logger.info(
|
||||
f"[윈도우 {window_idx}] IS: {is_start}~{is_end}, "
|
||||
f"OOS: {oos_start}~{oos_end}"
|
||||
)
|
||||
|
||||
# ── Optuna 목적함수 (IS 구간 Sharpe Ratio 최대화) ──
|
||||
best_is_metrics = {"sharpe_ratio": -999.0}
|
||||
best_params_holder = [None]
|
||||
|
||||
async def objective_async(trial) -> float:
|
||||
params = self._build_params_from_trial(trial)
|
||||
|
||||
try:
|
||||
metrics = await self._run_single_backtest(
|
||||
stock_codes=wf_config.stock_codes,
|
||||
start_date=is_start,
|
||||
end_date=is_end,
|
||||
params=params,
|
||||
wf_config=wf_config,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f" Trial {trial.number} 실패: {e}")
|
||||
return -999.0
|
||||
|
||||
sharpe = metrics["sharpe_ratio"]
|
||||
|
||||
# 거래가 너무 적으면 페널티 (최소 3회)
|
||||
if metrics["total_trades"] < 3:
|
||||
sharpe = sharpe - 1.0
|
||||
|
||||
# MDD가 -20% 이하이면 페널티
|
||||
if metrics["mdd_pct"] < -0.20:
|
||||
sharpe = sharpe - 0.5
|
||||
|
||||
if sharpe > best_is_metrics["sharpe_ratio"]:
|
||||
best_is_metrics.update(metrics)
|
||||
best_params_holder[0] = params
|
||||
|
||||
return sharpe
|
||||
|
||||
# Optuna study 생성 (동기 wrapper로 async objective 실행)
|
||||
study = optuna.create_study(
|
||||
direction="maximize",
|
||||
study_name=f"wfo_window_{window_idx}",
|
||||
sampler=optuna.samplers.TPESampler(seed=42 + window_idx),
|
||||
)
|
||||
|
||||
def objective_sync(trial):
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
import concurrent.futures
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
future = pool.submit(
|
||||
asyncio.run, objective_async(trial)
|
||||
)
|
||||
return future.result()
|
||||
else:
|
||||
return loop.run_until_complete(objective_async(trial))
|
||||
|
||||
# async 환경에서 Optuna 실행
|
||||
# Optuna는 동기 API이므로 thread에서 실행
|
||||
def run_study():
|
||||
study.optimize(
|
||||
objective_sync,
|
||||
n_trials=wf_config.n_trials,
|
||||
timeout=wf_config.optuna_timeout,
|
||||
show_progress_bar=False,
|
||||
)
|
||||
|
||||
await asyncio.to_thread(run_study)
|
||||
|
||||
best_params = best_params_holder[0]
|
||||
if best_params is None:
|
||||
# 모든 trial이 실패한 경우 기본 파라미터 사용
|
||||
best_params = {
|
||||
"MEAN_REVERSION_RSI_THRESHOLD": 30,
|
||||
"BB_PERIOD": 20,
|
||||
"ATR_STOP_MULTIPLIER": 2.5,
|
||||
"MEAN_REVERSION_STOP_LOSS_PCT": -6.0,
|
||||
"MEAN_REVERSION_TAKE_PROFIT_PCT": 12.0,
|
||||
"RSI_OVERBOUGHT_LEVEL": 70.0,
|
||||
}
|
||||
logger.warning(f"[윈도우 {window_idx}] 모든 trial 실패 -> 기본 파라미터 사용")
|
||||
|
||||
# ── IS 최종 성과 (best_params) ──
|
||||
is_metrics = await self._run_single_backtest(
|
||||
stock_codes=wf_config.stock_codes,
|
||||
start_date=is_start,
|
||||
end_date=is_end,
|
||||
params=best_params,
|
||||
wf_config=wf_config,
|
||||
)
|
||||
|
||||
# ── OOS 검증 ──
|
||||
oos_metrics = await self._run_single_backtest(
|
||||
stock_codes=wf_config.stock_codes,
|
||||
start_date=oos_start,
|
||||
end_date=oos_end,
|
||||
params=best_params,
|
||||
wf_config=wf_config,
|
||||
)
|
||||
|
||||
# ── 과적합 탐지 ──
|
||||
is_sharpe = is_metrics["sharpe_ratio"]
|
||||
oos_sharpe = oos_metrics["sharpe_ratio"]
|
||||
|
||||
if abs(is_sharpe) > 1e-10:
|
||||
sharpe_degradation = (is_sharpe - oos_sharpe) / abs(is_sharpe)
|
||||
else:
|
||||
sharpe_degradation = 0.0
|
||||
|
||||
overfit_warning = sharpe_degradation > wf_config.overfit_threshold
|
||||
|
||||
if overfit_warning:
|
||||
logger.warning(
|
||||
f"[윈도우 {window_idx}] 과적합 경고: "
|
||||
f"IS Sharpe={is_sharpe:.2f}, OOS Sharpe={oos_sharpe:.2f}, "
|
||||
f"감소율={sharpe_degradation:.1%}"
|
||||
)
|
||||
|
||||
return WindowResult(
|
||||
window_index=window_idx,
|
||||
is_start=str(is_start),
|
||||
is_end=str(is_end),
|
||||
oos_start=str(oos_start),
|
||||
oos_end=str(oos_end),
|
||||
best_params=best_params,
|
||||
is_sharpe=round(is_sharpe, 4),
|
||||
oos_sharpe=round(oos_sharpe, 4),
|
||||
is_return_pct=round(is_metrics["total_return_pct"], 4),
|
||||
oos_return_pct=round(oos_metrics["total_return_pct"], 4),
|
||||
is_mdd_pct=round(is_metrics["mdd_pct"], 4),
|
||||
oos_mdd_pct=round(oos_metrics["mdd_pct"], 4),
|
||||
is_win_rate=round(is_metrics["win_rate"], 4),
|
||||
oos_win_rate=round(oos_metrics["win_rate"], 4),
|
||||
is_total_trades=is_metrics["total_trades"],
|
||||
oos_total_trades=oos_metrics["total_trades"],
|
||||
sharpe_degradation=round(sharpe_degradation, 4),
|
||||
overfit_warning=overfit_warning,
|
||||
)
|
||||
|
||||
def _compute_recommended_params(
|
||||
self, window_results: List[WindowResult]
|
||||
) -> Dict[str, Any]:
|
||||
"""OOS Sharpe 가중평균으로 최종 추천 파라미터를 산출합니다.
|
||||
|
||||
OOS Sharpe가 양수인 윈도우만 가중치에 포함합니다.
|
||||
모든 OOS Sharpe가 음수이면 동일 가중치(산술 평균)를 사용합니다.
|
||||
"""
|
||||
param_keys = [
|
||||
"MEAN_REVERSION_RSI_THRESHOLD",
|
||||
"BB_PERIOD",
|
||||
"ATR_STOP_MULTIPLIER",
|
||||
"MEAN_REVERSION_STOP_LOSS_PCT",
|
||||
"MEAN_REVERSION_TAKE_PROFIT_PCT",
|
||||
"RSI_OVERBOUGHT_LEVEL",
|
||||
]
|
||||
|
||||
# 가중치 계산 (OOS Sharpe, 최소 0)
|
||||
weights = []
|
||||
for wr in window_results:
|
||||
w = max(wr.oos_sharpe, 0.0)
|
||||
weights.append(w)
|
||||
|
||||
total_weight = sum(weights)
|
||||
|
||||
# 모든 가중치가 0이면 동일 가중치
|
||||
if total_weight < 1e-10:
|
||||
weights = [1.0] * len(window_results)
|
||||
total_weight = float(len(window_results))
|
||||
|
||||
recommended = {}
|
||||
for key in param_keys:
|
||||
weighted_sum = 0.0
|
||||
for i, wr in enumerate(window_results):
|
||||
val = wr.best_params.get(key, 0.0)
|
||||
weighted_sum += val * weights[i]
|
||||
raw_val = weighted_sum / total_weight
|
||||
|
||||
# 정수 파라미터 반올림
|
||||
if key in ("MEAN_REVERSION_RSI_THRESHOLD", "BB_PERIOD"):
|
||||
recommended[key] = int(round(raw_val))
|
||||
else:
|
||||
recommended[key] = round(raw_val, 2)
|
||||
|
||||
return recommended
|
||||
|
||||
async def run(self, config: WalkForwardConfig) -> WalkForwardResult:
|
||||
"""Walk-Forward 최적화를 실행합니다.
|
||||
|
||||
Args:
|
||||
config: WalkForwardConfig 설정
|
||||
|
||||
Returns:
|
||||
WalkForwardResult 종합 결과
|
||||
"""
|
||||
logger.info(
|
||||
f"Walk-Forward 최적화 시작: {config.start_date} ~ {config.end_date}, "
|
||||
f"IS={config.is_months}개월, OOS={config.oos_months}개월, "
|
||||
f"Step={config.step_months}개월, Trials={config.n_trials}"
|
||||
)
|
||||
|
||||
# 1. 롤링 윈도우 생성
|
||||
windows = _generate_windows(
|
||||
start_date=config.start_date,
|
||||
end_date=config.end_date,
|
||||
is_months=config.is_months,
|
||||
oos_months=config.oos_months,
|
||||
step_months=config.step_months,
|
||||
)
|
||||
|
||||
if not windows:
|
||||
raise ValueError(
|
||||
f"유효한 윈도우가 없습니다. 기간이 최소 "
|
||||
f"{config.is_months + config.oos_months}개월 이상이어야 합니다. "
|
||||
f"입력 기간: {config.start_date} ~ {config.end_date}"
|
||||
)
|
||||
|
||||
logger.info(f"총 {len(windows)}개 윈도우 생성")
|
||||
|
||||
# 2. 각 윈도우 순차 실행 (DB 부하 제한)
|
||||
window_results: List[WindowResult] = []
|
||||
for idx, window in enumerate(windows):
|
||||
try:
|
||||
wr = await self._optimize_window(idx, window, config)
|
||||
window_results.append(wr)
|
||||
logger.info(
|
||||
f"[윈도우 {idx}] 완료 - "
|
||||
f"IS Sharpe={wr.is_sharpe:.2f}, OOS Sharpe={wr.oos_sharpe:.2f}"
|
||||
f"{' [과적합 경고]' if wr.overfit_warning else ''}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[윈도우 {idx}] 실패: {e}")
|
||||
continue
|
||||
|
||||
if not window_results:
|
||||
raise RuntimeError("모든 윈도우 최적화가 실패했습니다.")
|
||||
|
||||
# 3. 종합 OOS 성과 계산
|
||||
oos_sharpes = [wr.oos_sharpe for wr in window_results]
|
||||
oos_returns = [wr.oos_return_pct for wr in window_results]
|
||||
oos_mdds = [wr.oos_mdd_pct for wr in window_results]
|
||||
oos_winrates = [wr.oos_win_rate for wr in window_results]
|
||||
oos_trades_total = sum(wr.oos_total_trades for wr in window_results)
|
||||
|
||||
overfit_count = sum(1 for wr in window_results if wr.overfit_warning)
|
||||
degradations = [wr.sharpe_degradation for wr in window_results]
|
||||
|
||||
# 4. 추천 파라미터 계산
|
||||
recommended = self._compute_recommended_params(window_results)
|
||||
|
||||
result = WalkForwardResult(
|
||||
config={
|
||||
"stock_codes": config.stock_codes,
|
||||
"start_date": str(config.start_date),
|
||||
"end_date": str(config.end_date),
|
||||
"is_months": config.is_months,
|
||||
"oos_months": config.oos_months,
|
||||
"step_months": config.step_months,
|
||||
"n_trials": config.n_trials,
|
||||
"initial_capital": config.initial_capital,
|
||||
},
|
||||
total_windows=len(window_results),
|
||||
windows=[asdict(wr) for wr in window_results],
|
||||
avg_oos_sharpe=round(float(np.mean(oos_sharpes)), 4),
|
||||
avg_oos_return_pct=round(float(np.mean(oos_returns)), 4),
|
||||
avg_oos_mdd_pct=round(float(np.mean(oos_mdds)), 4),
|
||||
avg_oos_win_rate=round(float(np.mean(oos_winrates)), 4),
|
||||
total_oos_trades=oos_trades_total,
|
||||
overfit_windows=overfit_count,
|
||||
avg_sharpe_degradation=round(float(np.mean(degradations)), 4),
|
||||
overfit_ratio=round(overfit_count / len(window_results), 4),
|
||||
recommended_params=recommended,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Walk-Forward 최적화 완료: "
|
||||
f"{len(window_results)}개 윈도우, "
|
||||
f"평균 OOS Sharpe={result.avg_oos_sharpe:.2f}, "
|
||||
f"과적합 비율={result.overfit_ratio:.0%}"
|
||||
)
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user