인과 GT leg 엔진·drawdown tier·train 캘리브레이션, Phase 2 Go/No-Go 및 시뮬 리포트를 반영한다. Co-authored-by: Cursor <cursoragent@cursor.com>
246 lines
7.4 KiB
Python
246 lines
7.4 KiB
Python
"""
|
|
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,
|
|
},
|
|
],
|
|
}
|