""" Option C 2차 목표(+1000%) 검증 — hybrid tier 포트폴리오 WF·슬리피지·Go/No-Go. """ from __future__ import annotations from typing import Any import pandas as pd from config import ( GT_INITIAL_CASH_KRW, LIVE_SLIPPAGE_PCT, SIM_HYBRID_MAX_MDD_PCT, SIM_HYBRID_PORTFOLIO_WF_MIN_RATIO, SIM_OPTION_C_MIN_GT_CAPTURE, SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO, SIM_OPTION_C_PHASE2_TARGET_PNL_PCT, ) from deepcoin.ground_truth.gt_allocation import simulate_portfolio_summary def apply_slippage_to_trades( trades: list[dict[str, Any]], slippage_pct: float = LIVE_SLIPPAGE_PCT, ) -> list[dict[str, Any]]: """ 체결가에 슬리피지 반영 (매수 불리·매도 불리). Args: trades: amount_krw·price·action trade dict. slippage_pct: 체결가 대비 % (0.05 = 0.05%). Returns: price 조정된 trade dict 복사본. """ slip = float(slippage_pct) / 100.0 out: list[dict[str, Any]] = [] for t in trades: row = dict(t) price = float(row.get("price") or 0) if price <= 0: out.append(row) continue action = row.get("action", "") if action == "buy": row["price"] = round(price * (1.0 + slip), 4) elif action == "sell": row["price"] = round(price * (1.0 - slip), 4) out.append(row) return out def walk_forward_portfolio_by_month( steps: list[dict[str, Any]], *, initial_cash: float = GT_INITIAL_CASH_KRW, ) -> list[dict[str, Any]]: """ 포트폴리오 step에서 월별 자산 증감률 (인과적). Args: steps: simulate_portfolio_steps 결과. initial_cash: 첫 달 시작 자산(이전 달 종료 없을 때). Returns: {month, pnl_pct, start_asset, end_asset} 리스트. """ if not steps: return [] df = pd.DataFrame(steps) df["ts"] = pd.to_datetime(df["dt"]) df = df.sort_values("ts") df["month"] = df["ts"].dt.to_period("M").astype(str) rows: list[dict[str, Any]] = [] prev_end = float(initial_cash) for month in sorted(df["month"].unique()): grp = df[df["month"] == month] end_asset = float(grp["total_asset_krw"].iloc[-1]) pnl_pct = (end_asset - prev_end) / prev_end * 100.0 if prev_end > 0 else 0.0 rows.append( { "month": month, "pnl_pct": round(pnl_pct, 2), "start_asset_krw": round(prev_end, 0), "end_asset_krw": round(end_asset, 0), } ) prev_end = end_asset return rows def walk_forward_portfolio_summary(wf_rows: list[dict[str, Any]]) -> dict[str, Any]: """ 월별 포트폴리오 WF 요약. Args: wf_rows: walk_forward_portfolio_by_month 결과. Returns: months, positive_months, positive_ratio, mean_pnl_pct. """ if not wf_rows: return {"months": 0, "positive_ratio": 0.0, "mean_pnl_pct": 0.0} n = len(wf_rows) pos = sum(1 for r in wf_rows if float(r.get("pnl_pct") or 0) > 0) mean_pnl = sum(float(r.get("pnl_pct") or 0) for r in wf_rows) / n return { "months": n, "positive_months": pos, "positive_ratio": round(pos / n, 4), "mean_pnl_pct": round(mean_pnl, 2), } def simulate_hybrid_slippage_stress( sized_trades: list[dict[str, Any]], *, last_price: float | None, slippage_pct: float = LIVE_SLIPPAGE_PCT, initial_cash: float = GT_INITIAL_CASH_KRW, fee_rate: float, ) -> dict[str, Any]: """ hybrid sized trades + 슬리피지 포트폴리오 요약. Args: sized_trades: build_monitor_hybrid_sized_trades 출력. last_price: 미청산 평가가. slippage_pct: 체결 슬리피지 %. initial_cash: 시작 현금. fee_rate: 수수료율. Returns: simulate_portfolio_summary 결과. """ slipped = apply_slippage_to_trades(sized_trades, slippage_pct) result = simulate_portfolio_summary( slipped, initial_cash=initial_cash, fee_rate=fee_rate, last_price=last_price, use_amount_krw=True, ) result["slippage_pct"] = slippage_pct result["sizing_mode"] = "hybrid_slippage_stress" return result def evaluate_option_c_phase2_go( hybrid_go: dict[str, Any], hybrid_full: dict[str, Any], hybrid_holdout: dict[str, Any], hybrid_fee_stress: dict[str, Any], hybrid_slippage: dict[str, Any], portfolio_wf_summary: dict[str, Any], gt_pnl_pct: float, ) -> dict[str, Any]: """ Option C 2차(+1000%) Go/No-Go. Args: hybrid_go: 1차 hybrid tier Go 결과. hybrid_full: 전기간 hybrid 포트폴리오. hybrid_holdout: holdout 구간 증감. hybrid_fee_stress: 수수료 2x hybrid. hybrid_slippage: 슬리피지 반영 hybrid. portfolio_wf_summary: 월별 포트폴리오 WF. gt_pnl_pct: GT 전기간 PnL %. Returns: go, checks, targets. """ full_pnl = float(hybrid_full.get("pnl_pct", 0)) capture = full_pnl / gt_pnl_pct if abs(gt_pnl_pct) > 1e-6 else 0.0 ho_pnl = float(hybrid_holdout.get("pnl_pct", -999)) mdd = float(hybrid_full.get("max_drawdown_pct", 999)) fee_pnl = float(hybrid_fee_stress.get("pnl_pct", -999)) slip_pnl = float(hybrid_slippage.get("pnl_pct", -999)) wf_ratio = float(portfolio_wf_summary.get("positive_ratio", 0)) c_phase1 = bool(hybrid_go.get("go")) c_pnl = full_pnl >= SIM_OPTION_C_PHASE2_TARGET_PNL_PCT c_capture = capture >= SIM_OPTION_C_MIN_GT_CAPTURE c_holdout = ho_pnl > 0.0 c_mdd = mdd <= SIM_HYBRID_MAX_MDD_PCT c_fee = fee_pnl >= SIM_OPTION_C_PHASE2_TARGET_PNL_PCT * SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO c_slip = slip_pnl > 0.0 c_wf = wf_ratio >= SIM_HYBRID_PORTFOLIO_WF_MIN_RATIO all_go = ( c_phase1 and c_pnl and c_capture and c_holdout and c_mdd and c_fee and c_slip and c_wf ) return { "go": all_go, "gt_capture_ratio": round(capture, 4), "targets": { "phase2_pnl_pct": SIM_OPTION_C_PHASE2_TARGET_PNL_PCT, "min_gt_capture": SIM_OPTION_C_MIN_GT_CAPTURE, "portfolio_wf_min_ratio": SIM_HYBRID_PORTFOLIO_WF_MIN_RATIO, }, "checks": [ {"name": "phase1_hybrid_go", "pass": c_phase1}, { "name": "full_pnl_1000pct", "pass": c_pnl, "value": full_pnl, }, { "name": "gt_capture_23pct", "pass": c_capture, "value": round(capture, 4), }, {"name": "holdout_pnl_positive", "pass": c_holdout, "value": ho_pnl}, {"name": "max_mdd", "pass": c_mdd, "value": mdd}, { "name": "fee_stress_ratio", "pass": c_fee, "value": fee_pnl, "threshold": round( SIM_OPTION_C_PHASE2_TARGET_PNL_PCT * SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO, 2, ), }, { "name": "slippage_stress_positive", "pass": c_slip, "value": slip_pnl, "note": "체결가 슬리피지 반영 후에도 흑자", }, { "name": "portfolio_wf_positive_ratio", "pass": c_wf, "value": wf_ratio, }, ], }