hybrid DD tier와 Option C 2차(+1000%) 검증을 추가하고 실거래 사이징을 정합한다.
인과 GT leg 엔진·drawdown tier·train 캘리브레이션, Phase 2 Go/No-Go 및 시뮬 리포트를 반영한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
245
deepcoin/matching/option_c_phase2.py
Normal file
245
deepcoin/matching/option_c_phase2.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""
|
||||
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,
|
||||
},
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user