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:
214
deepcoin/matching/live_sizing.py
Normal file
214
deepcoin/matching/live_sizing.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""
|
||||
실거래 매수 사이징 — 시뮬(sim_tier_enhanced)과 동일 인과 tier·weight 정책.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from config import (
|
||||
GT_SIGNAL_CAUSAL,
|
||||
TRADING_FEE_RATE,
|
||||
)
|
||||
from deepcoin.ground_truth.causal_gt_hybrid import (
|
||||
_attach_drawdown_to_buys,
|
||||
_bar_index_at,
|
||||
_close_series_from_df,
|
||||
_drawdown_pct_at_index,
|
||||
hybrid_tier_scale,
|
||||
)
|
||||
from deepcoin.ground_truth.gt_model import leg_entry_weights, remaining_weight_sum
|
||||
from deepcoin.matching.position_sizing import compute_buy_amount_krw
|
||||
from deepcoin.paths import OPS_STATE_DIR
|
||||
|
||||
LIVE_SIZING_STATE_JSON = OPS_STATE_DIR / "live_sizing_state.json"
|
||||
|
||||
|
||||
class LivePositionState:
|
||||
"""
|
||||
미청산 leg·과거 leg 수익·매수 weight 추적 (시뮬 enrich/causal tier 정합).
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""빈 포지션 상태."""
|
||||
self.current_leg_id: int = 0
|
||||
self.open_buys: list[dict[str, Any]] = []
|
||||
self.completed_leg_ret: dict[int, float] = {}
|
||||
self.leg_cost_krw: float = 0.0
|
||||
self.leg_proceeds_krw: float = 0.0
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path | None = None) -> LivePositionState:
|
||||
"""
|
||||
디스크에서 상태 복원.
|
||||
|
||||
Args:
|
||||
path: JSON 경로. None이면 기본 경로.
|
||||
|
||||
Returns:
|
||||
LivePositionState 인스턴스.
|
||||
"""
|
||||
p = path or LIVE_SIZING_STATE_JSON
|
||||
st = cls()
|
||||
if not p.is_file():
|
||||
return st
|
||||
try:
|
||||
data = json.loads(p.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return st
|
||||
st.current_leg_id = int(data.get("current_leg_id") or 0)
|
||||
st.open_buys = list(data.get("open_buys") or [])
|
||||
st.completed_leg_ret = {
|
||||
int(k): float(v) for k, v in (data.get("completed_leg_ret") or {}).items()
|
||||
}
|
||||
st.leg_cost_krw = float(data.get("leg_cost_krw") or 0.0)
|
||||
st.leg_proceeds_krw = float(data.get("leg_proceeds_krw") or 0.0)
|
||||
return st
|
||||
|
||||
def save(self, path: Path | None = None) -> None:
|
||||
"""
|
||||
상태를 디스크에 저장.
|
||||
|
||||
Args:
|
||||
path: JSON 경로. None이면 기본 경로.
|
||||
"""
|
||||
p = path or LIVE_SIZING_STATE_JSON
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = {
|
||||
"current_leg_id": self.current_leg_id,
|
||||
"open_buys": self.open_buys,
|
||||
"completed_leg_ret": self.completed_leg_ret,
|
||||
"leg_cost_krw": round(self.leg_cost_krw, 0),
|
||||
"leg_proceeds_krw": round(self.leg_proceeds_krw, 0),
|
||||
}
|
||||
p.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
def _start_new_leg_if_needed(self) -> None:
|
||||
"""포지션 없을 때 새 leg 시작."""
|
||||
if not self.open_buys:
|
||||
self.current_leg_id += 1
|
||||
self.leg_cost_krw = 0.0
|
||||
self.leg_proceeds_krw = 0.0
|
||||
|
||||
def record_buy(self, dt: str, price: float, amount_krw: float, fee: float) -> None:
|
||||
"""
|
||||
체결 매수 기록.
|
||||
|
||||
Args:
|
||||
dt: 체결 시각.
|
||||
price: 체결가.
|
||||
amount_krw: 매수 원화.
|
||||
fee: 수수료.
|
||||
"""
|
||||
self._start_new_leg_if_needed()
|
||||
self.open_buys.append({"dt": dt, "price": price, "amount_krw": amount_krw})
|
||||
self.leg_cost_krw += amount_krw + fee
|
||||
|
||||
def record_sell(self, amount_krw: float, fee: float, *, full_close: bool) -> None:
|
||||
"""
|
||||
체결 매도 기록.
|
||||
|
||||
Args:
|
||||
amount_krw: 매도 원화(총액).
|
||||
fee: 수수료.
|
||||
full_close: leg 전량 청산 여부.
|
||||
"""
|
||||
net = amount_krw - fee
|
||||
self.leg_proceeds_krw += net
|
||||
if full_close and self.leg_cost_krw > 0:
|
||||
ret_pct = (self.leg_proceeds_krw - self.leg_cost_krw) / self.leg_cost_krw * 100.0
|
||||
self.completed_leg_ret[self.current_leg_id] = ret_pct
|
||||
self.open_buys = []
|
||||
self.leg_cost_krw = 0.0
|
||||
self.leg_proceeds_krw = 0.0
|
||||
|
||||
def plan_buy_amount_krw(
|
||||
self,
|
||||
dt: str,
|
||||
price: float,
|
||||
cash: float,
|
||||
qty: float,
|
||||
df: pd.DataFrame | None = None,
|
||||
*,
|
||||
enhanced: bool = True,
|
||||
fee_rate: float = TRADING_FEE_RATE,
|
||||
) -> float:
|
||||
"""
|
||||
시뮬과 동일 tier·weight로 매수 원화 산출.
|
||||
|
||||
Args:
|
||||
dt: 신호 시각.
|
||||
price: 종가.
|
||||
cash: 가용 원화.
|
||||
qty: 보유 수량.
|
||||
df: OHLC (drawdown).
|
||||
enhanced: conviction·medium tier 사용.
|
||||
fee_rate: 수수료율.
|
||||
|
||||
Returns:
|
||||
매수 원화.
|
||||
"""
|
||||
self._start_new_leg_if_needed()
|
||||
prices = [float(b["price"]) for b in self.open_buys] + [price]
|
||||
weights = leg_entry_weights(prices)
|
||||
idx = len(self.open_buys)
|
||||
weight = float(weights[idx])
|
||||
w_sum = float(sum(weights[idx:]))
|
||||
trade: dict[str, Any] = {
|
||||
"dt": dt,
|
||||
"action": "buy",
|
||||
"price": price,
|
||||
"leg_id": self.current_leg_id,
|
||||
"weight": round(weight, 4),
|
||||
}
|
||||
if df is not None and not df.empty:
|
||||
attached = _attach_drawdown_to_buys([trade], df)
|
||||
if attached:
|
||||
trade = attached[0]
|
||||
from deepcoin.ground_truth.hybrid_dd_calibrate import load_hybrid_dd_params
|
||||
|
||||
dd_params = load_hybrid_dd_params()
|
||||
scale = hybrid_tier_scale(
|
||||
trade,
|
||||
completed_leg_ret=self.completed_leg_ret,
|
||||
enhanced=enhanced,
|
||||
dd_large_pct=dd_params.get("dd_large_pct"),
|
||||
dd_medium_pct=dd_params.get("dd_medium_pct"),
|
||||
)
|
||||
return compute_buy_amount_krw(
|
||||
cash,
|
||||
qty,
|
||||
price,
|
||||
weight,
|
||||
w_sum,
|
||||
asset_pct_scale=scale,
|
||||
fee_rate=fee_rate,
|
||||
ignore_weight_split=bool(trade.get("conviction_buy")),
|
||||
)
|
||||
|
||||
|
||||
def drawdown_pct_from_df(df: pd.DataFrame, dt: str) -> float:
|
||||
"""
|
||||
bar 시점 drawdown % (인과적).
|
||||
|
||||
Args:
|
||||
df: DatetimeIndex OHLC.
|
||||
dt: 시각 문자열.
|
||||
|
||||
Returns:
|
||||
drawdown %.
|
||||
"""
|
||||
if df.empty:
|
||||
return 0.0
|
||||
close_s = _close_series_from_df(df)
|
||||
bar_idx = _bar_index_at(df, dt)
|
||||
return _drawdown_pct_at_index(close_s, bar_idx)
|
||||
|
||||
|
||||
def live_sizing_enabled() -> bool:
|
||||
"""실거래 사이징을 시뮬 인과 tier와 정합할지."""
|
||||
return bool(GT_SIGNAL_CAUSAL)
|
||||
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,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -71,6 +71,7 @@ def compute_buy_amount_krw(
|
||||
asset_pct_scale: float,
|
||||
min_order_krw: float = GT_MIN_ORDER_KRW,
|
||||
fee_rate: float = TRADING_FEE_RATE,
|
||||
ignore_weight_split: bool = False,
|
||||
) -> float:
|
||||
"""
|
||||
목표=총보유자산×(최적 매수율×scale), 체결=min(목표, 보유현금/(1+fee)) 로 매수 원화를 산출합니다.
|
||||
@@ -86,6 +87,7 @@ def compute_buy_amount_krw(
|
||||
asset_pct_scale: leg·규칙 티어(대형/소형) 스케일.
|
||||
min_order_krw: 최소 주문 원화.
|
||||
fee_rate: 수수료율.
|
||||
ignore_weight_split: True면 weight 분할 없이 scale만 적용 (conviction 매수).
|
||||
|
||||
Returns:
|
||||
매수 원화(0이면 미체결).
|
||||
@@ -94,7 +96,10 @@ def compute_buy_amount_krw(
|
||||
return 0.0
|
||||
total_asset, _, available_cash = portfolio_totals(cash, qty, price)
|
||||
budget = max(available_cash / (1.0 + fee_rate), 0.0)
|
||||
opt_rate = optimal_weight_share(weight, weight_sum_remaining) * asset_pct_scale
|
||||
if ignore_weight_split:
|
||||
opt_rate = asset_pct_scale
|
||||
else:
|
||||
opt_rate = optimal_weight_share(weight, weight_sum_remaining) * asset_pct_scale
|
||||
target = total_asset * opt_rate
|
||||
amount = min(target, budget)
|
||||
if budget >= min_order_krw and 0 < amount < min_order_krw:
|
||||
|
||||
@@ -24,6 +24,12 @@ from config import (
|
||||
SIM_GO_MIN_HOLDOUT_EV,
|
||||
SIM_GO_MIN_HOLDOUT_PF,
|
||||
SIM_GO_WF_POSITIVE_RATIO,
|
||||
SIM_HYBRID_MAX_MDD_PCT,
|
||||
SIM_HYBRID_MIN_HOLDOUT_PNL_PCT,
|
||||
SIM_OPTION_C_MIN_GT_CAPTURE,
|
||||
SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO,
|
||||
SIM_OPTION_C_PHASE2_TARGET_PNL_PCT,
|
||||
SIM_OPTION_C_TARGET_PNL_PCT,
|
||||
SIM_WALK_FORWARD_MIN_MONTHS,
|
||||
TRADING_FEE_RATE,
|
||||
)
|
||||
@@ -247,6 +253,186 @@ def evaluate_go_no_go(
|
||||
}
|
||||
|
||||
|
||||
def portfolio_holdout_from_steps(
|
||||
steps: list[dict[str, Any]],
|
||||
holdout_start: pd.Timestamp,
|
||||
*,
|
||||
initial_if_empty: float = GT_INITIAL_CASH_KRW,
|
||||
trade_count: int = 0,
|
||||
note: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
포트폴리오 step에서 holdout 구간 자산 증감.
|
||||
|
||||
Args:
|
||||
steps: simulate_portfolio_steps 결과.
|
||||
holdout_start: holdout 시작 시각.
|
||||
initial_if_empty: step 없을 때 시작 자산.
|
||||
trade_count: holdout 발화 수.
|
||||
note: 설명.
|
||||
|
||||
Returns:
|
||||
holdout pnl 요약 dict.
|
||||
"""
|
||||
if not steps:
|
||||
return {"pnl_pct": 0.0, "note": "steps empty"}
|
||||
assets = [(pd.to_datetime(s["dt"]), float(s["total_asset_krw"])) for s in steps]
|
||||
pre = [a for d, a in assets if d < holdout_start]
|
||||
in_h = [a for d, a in assets if d >= holdout_start]
|
||||
asset_start = pre[-1] if pre else float(initial_if_empty)
|
||||
asset_end = in_h[-1] if in_h else assets[-1][1]
|
||||
ho_pnl_pct = (
|
||||
(asset_end - asset_start) / asset_start * 100.0 if asset_start > 0 else 0.0
|
||||
)
|
||||
return {
|
||||
"initial_asset_krw": round(asset_start, 0),
|
||||
"final_asset_krw": round(asset_end, 0),
|
||||
"pnl_krw": round(asset_end - asset_start, 0),
|
||||
"pnl_pct": round(ho_pnl_pct, 2),
|
||||
"trade_count": int(trade_count),
|
||||
"note": note,
|
||||
}
|
||||
|
||||
|
||||
def evaluate_hybrid_sizing_go(
|
||||
base_go: dict[str, Any],
|
||||
hybrid_full: dict[str, Any],
|
||||
hybrid_holdout: dict[str, Any],
|
||||
hybrid_fee_stress: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
hybrid DD tier 배분 승격 Go/No-Go (규칙 Go + holdout·MDD·수수료 스트레스).
|
||||
|
||||
Args:
|
||||
base_go: monitor 규칙 evaluate_go_no_go 결과.
|
||||
hybrid_full: 전기간 hybrid 포트폴리오 요약.
|
||||
hybrid_holdout: holdout 구간 자산 증감.
|
||||
hybrid_fee_stress: 수수료 스트레스 hybrid 포트폴리오.
|
||||
|
||||
Returns:
|
||||
go, checks, primary_sizing 권장.
|
||||
"""
|
||||
from config import (
|
||||
SIM_HYBRID_MAX_MDD_PCT,
|
||||
SIM_HYBRID_MIN_HOLDOUT_PNL_PCT,
|
||||
SIM_OPTION_C_TARGET_PNL_PCT,
|
||||
SIM_PRIMARY_SIZING,
|
||||
)
|
||||
|
||||
base_ok = bool(base_go.get("go"))
|
||||
ho_pnl = float(hybrid_holdout.get("pnl_pct", -999))
|
||||
full_pnl = float(hybrid_full.get("pnl_pct", 0))
|
||||
mdd = float(hybrid_full.get("max_drawdown_pct", 999))
|
||||
stress_pnl = float(hybrid_fee_stress.get("pnl_pct", -999))
|
||||
|
||||
c_base = base_ok
|
||||
c_holdout = ho_pnl >= SIM_HYBRID_MIN_HOLDOUT_PNL_PCT
|
||||
c_mdd = mdd <= SIM_HYBRID_MAX_MDD_PCT
|
||||
c_fee = stress_pnl > 0.0
|
||||
c_target = full_pnl >= SIM_OPTION_C_TARGET_PNL_PCT
|
||||
|
||||
all_go = c_base and c_holdout and c_mdd and c_fee
|
||||
if SIM_PRIMARY_SIZING == "hybrid":
|
||||
primary = "hybrid"
|
||||
elif SIM_PRIMARY_SIZING == "causal_tier":
|
||||
primary = "causal_tier"
|
||||
else:
|
||||
primary = "hybrid" if all_go else "causal_tier"
|
||||
|
||||
return {
|
||||
"go": all_go,
|
||||
"primary_sizing": primary,
|
||||
"checks": [
|
||||
{"name": "monitor_rules_go", "pass": c_base},
|
||||
{"name": "hybrid_holdout_pnl", "pass": c_holdout, "value": ho_pnl},
|
||||
{"name": "hybrid_max_mdd", "pass": c_mdd, "value": mdd},
|
||||
{"name": "hybrid_fee_stress_pnl", "pass": c_fee, "value": stress_pnl},
|
||||
{
|
||||
"name": "option_c_target_300pct",
|
||||
"pass": c_target,
|
||||
"value": full_pnl,
|
||||
"optional": True,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def simulate_hybrid_order_cap(
|
||||
outcomes: pd.DataFrame,
|
||||
ohlc_df: pd.DataFrame,
|
||||
*,
|
||||
rule_ids: set[str] | None = None,
|
||||
holdout_only: bool = True,
|
||||
fee_rate: float = TRADING_FEE_RATE,
|
||||
dd_large_pct: float | None = None,
|
||||
dd_medium_pct: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
hybrid tier 복리 배분·슬리피지 가정 체결 가능 발화 집계.
|
||||
|
||||
Args:
|
||||
outcomes: fire_outcomes.
|
||||
ohlc_df: 3m OHLC (drawdown).
|
||||
rule_ids: monitor rule_id 필터.
|
||||
holdout_only: holdout만.
|
||||
fee_rate: 수수료율.
|
||||
|
||||
Returns:
|
||||
simulate_live_order_cap과 동일 구조.
|
||||
"""
|
||||
from deepcoin.ground_truth.causal_gt_hybrid import build_monitor_hybrid_sized_trades
|
||||
|
||||
if outcomes.empty:
|
||||
return {"rules": {}, "note": "발화 없음"}
|
||||
|
||||
df = outcomes.copy()
|
||||
if holdout_only and "split" in df.columns:
|
||||
df = df[df["split"] == "holdout"]
|
||||
if rule_ids is not None:
|
||||
df = df[df["rule_id"].isin(rule_ids)]
|
||||
slip = LIVE_SLIPPAGE_PCT
|
||||
|
||||
sized, _ = build_monitor_hybrid_sized_trades(
|
||||
sort_fires_chronological(df),
|
||||
ohlc_df,
|
||||
enhanced=False,
|
||||
fee_rate=fee_rate,
|
||||
dd_large_pct=dd_large_pct,
|
||||
dd_medium_pct=dd_medium_pct,
|
||||
)
|
||||
executed_dts = {
|
||||
t["dt"]
|
||||
for t in sized
|
||||
if t.get("action") == "sell" or float(t.get("amount_krw") or 0) > 0
|
||||
}
|
||||
if not executed_dts:
|
||||
return {"rules": {}, "taken_count": 0, "total_count": int(len(df))}
|
||||
|
||||
taken = df[df["dt"].astype(str).isin(executed_dts)].copy()
|
||||
taken["adj_ret_pct"] = taken["forward_ret_pct"] - slip
|
||||
|
||||
by_rule: dict[str, Any] = {}
|
||||
for rid, grp in taken.groupby("rule_id"):
|
||||
g = grp.copy()
|
||||
g["forward_ret_pct"] = g["adj_ret_pct"]
|
||||
by_rule[rid] = {
|
||||
"taken_count": int(len(grp)),
|
||||
"total_count": int((df["rule_id"] == rid).sum()),
|
||||
"metrics": _rule_metrics(g),
|
||||
}
|
||||
|
||||
return {
|
||||
"assumptions": {
|
||||
"slippage_pct": slip,
|
||||
"sizing": "hybrid_dd_tier_compound",
|
||||
},
|
||||
"taken_count": int(len(taken)),
|
||||
"total_count": int(len(df)),
|
||||
"rules": by_rule,
|
||||
"portfolio_adj_ev_pct": round(float(taken["adj_ret_pct"].mean()), 4),
|
||||
}
|
||||
|
||||
|
||||
def build_simulation_report(
|
||||
outcomes_path: Path | None = None,
|
||||
matched_path: Path | None = None,
|
||||
@@ -336,6 +522,63 @@ def build_simulation_report(
|
||||
last_price=float(mark) if mark else None,
|
||||
)
|
||||
|
||||
# 인과 GT leg 엔진 (split_buy + peak_sell, 캘리브레이션 파라미터)
|
||||
cg_df = None
|
||||
try:
|
||||
from config import CHART_LOOKBACK_DAYS, MATCH_PRIMARY_INTERVAL, SYMBOL
|
||||
from deepcoin.data.mtf_bb import load_frames_from_db
|
||||
from deepcoin.ground_truth.causal_gt_calibrate import load_causal_gt_params
|
||||
from deepcoin.ground_truth.causal_gt_trades import simulate_causal_gt_portfolio
|
||||
from deepcoin.ops.monitor import Monitor
|
||||
|
||||
cg_params = load_causal_gt_params()
|
||||
mon_cg = Monitor(cooldown_file=None)
|
||||
cg_frames = load_frames_from_db(mon_cg, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS)
|
||||
cg_df = cg_frames[MATCH_PRIMARY_INTERVAL]
|
||||
portfolio_compare["sim_causal_gt"] = simulate_causal_gt_portfolio(
|
||||
cg_df,
|
||||
last_price=float(mark) if mark else None,
|
||||
**cg_params,
|
||||
)
|
||||
# Phase 3: monitor buy + 인과 peak sell + drawdown tier
|
||||
from deepcoin.ground_truth.causal_gt_hybrid import (
|
||||
simulate_causal_gt_hybrid_portfolio,
|
||||
simulate_monitor_tier_enhanced_portfolio,
|
||||
)
|
||||
from deepcoin.ground_truth.hybrid_dd_calibrate import load_hybrid_dd_params
|
||||
|
||||
dd_params = load_hybrid_dd_params()
|
||||
|
||||
buy_only = all_monitor[all_monitor["side"] == "buy"]
|
||||
portfolio_compare["sim_causal_hybrid"] = simulate_causal_gt_hybrid_portfolio(
|
||||
buy_only,
|
||||
cg_df,
|
||||
monitor_fires=all_monitor,
|
||||
last_price=float(mark) if mark else None,
|
||||
cg_params=cg_params,
|
||||
dd_large_pct=dd_params.get("dd_large_pct"),
|
||||
dd_medium_pct=dd_params.get("dd_medium_pct"),
|
||||
)
|
||||
portfolio_compare["hybrid_dd_params"] = dd_params
|
||||
portfolio_compare["sim_tier_enhanced"] = simulate_monitor_tier_enhanced_portfolio(
|
||||
all_monitor,
|
||||
cg_df,
|
||||
last_price=float(mark) if mark else None,
|
||||
)
|
||||
except Exception as exc:
|
||||
portfolio_compare["sim_causal_gt"] = {
|
||||
"pnl_pct": 0.0,
|
||||
"note": f"causal_gt sim skipped: {exc}",
|
||||
}
|
||||
portfolio_compare["sim_causal_hybrid"] = {
|
||||
"pnl_pct": 0.0,
|
||||
"note": f"causal_hybrid sim skipped: {exc}",
|
||||
}
|
||||
portfolio_compare["sim_tier_enhanced"] = {
|
||||
"pnl_pct": 0.0,
|
||||
"note": f"tier_enhanced sim skipped: {exc}",
|
||||
}
|
||||
|
||||
holdout = outcomes[
|
||||
outcomes["rule_id"].isin(monitor_ids) & (outcomes["split"] == "holdout")
|
||||
]
|
||||
@@ -349,24 +592,119 @@ def build_simulation_report(
|
||||
outcomes_ts = outcomes.copy()
|
||||
outcomes_ts["ts"] = pd.to_datetime(outcomes_ts["dt"])
|
||||
h0 = outcomes_ts["ts"].quantile(1.0 - MATCH_HOLDOUT_RATIO)
|
||||
assets = [(s["dt"], float(s["total_asset_krw"])) for s in steps]
|
||||
pre = [a for d, a in assets if pd.to_datetime(d) < h0]
|
||||
in_h = [a for d, a in assets if pd.to_datetime(d) >= h0]
|
||||
asset_start = pre[-1] if pre else float(GT_INITIAL_CASH_KRW)
|
||||
asset_end = in_h[-1] if in_h else assets[-1][1]
|
||||
ho_pnl_pct = (
|
||||
(asset_end - asset_start) / asset_start * 100.0
|
||||
if asset_start > 0
|
||||
else 0.0
|
||||
portfolio_compare["sim_sized_holdout"] = portfolio_holdout_from_steps(
|
||||
steps,
|
||||
h0,
|
||||
trade_count=int(len(holdout)),
|
||||
note="전기간 복리(causal tier) 후 holdout 구간 자산 증감",
|
||||
)
|
||||
portfolio_compare["sim_sized_holdout"] = {
|
||||
"initial_asset_krw": round(asset_start, 0),
|
||||
"final_asset_krw": round(asset_end, 0),
|
||||
"pnl_krw": round(asset_end - asset_start, 0),
|
||||
"pnl_pct": round(ho_pnl_pct, 2),
|
||||
"note": "전기간 복리 후 holdout 구간 자산 증감 (1M 재시작 아님)",
|
||||
"trade_count": int(len(holdout)),
|
||||
}
|
||||
|
||||
go_hybrid: dict[str, Any] = {"go": False, "note": "hybrid sim unavailable"}
|
||||
go_option_c_phase2: dict[str, Any] = {"go": False, "note": "phase2 unavailable"}
|
||||
if (
|
||||
cg_df is not None
|
||||
and not all_monitor.empty
|
||||
and portfolio_compare.get("sim_causal_hybrid", {}).get("sizing_mode")
|
||||
):
|
||||
from deepcoin.ground_truth.causal_gt_hybrid import build_monitor_hybrid_sized_trades
|
||||
from deepcoin.ground_truth.gt_allocation import simulate_portfolio_steps
|
||||
from deepcoin.ground_truth.hybrid_dd_calibrate import load_hybrid_dd_params
|
||||
from deepcoin.matching.option_c_phase2 import (
|
||||
evaluate_option_c_phase2_go,
|
||||
simulate_hybrid_slippage_stress,
|
||||
walk_forward_portfolio_by_month,
|
||||
walk_forward_portfolio_summary,
|
||||
)
|
||||
|
||||
dd_params = portfolio_compare.get("hybrid_dd_params") or load_hybrid_dd_params()
|
||||
dd_large = dd_params.get("dd_large_pct")
|
||||
dd_medium = dd_params.get("dd_medium_pct")
|
||||
|
||||
hybrid_full = portfolio_compare["sim_causal_hybrid"]
|
||||
sized_h, _ = build_monitor_hybrid_sized_trades(
|
||||
sort_fires_chronological(all_monitor),
|
||||
cg_df,
|
||||
enhanced=False,
|
||||
dd_large_pct=dd_large,
|
||||
dd_medium_pct=dd_medium,
|
||||
)
|
||||
steps_h = simulate_portfolio_steps(sized_h, use_amount_krw=True)
|
||||
if steps_h and not holdout.empty:
|
||||
outcomes_ts = outcomes.copy()
|
||||
outcomes_ts["ts"] = pd.to_datetime(outcomes_ts["dt"])
|
||||
h0 = outcomes_ts["ts"].quantile(1.0 - MATCH_HOLDOUT_RATIO)
|
||||
portfolio_compare["sim_hybrid_holdout"] = portfolio_holdout_from_steps(
|
||||
steps_h,
|
||||
h0,
|
||||
trade_count=int(len(holdout)),
|
||||
note="전기간 복리(hybrid DD tier) 후 holdout 구간 자산 증감",
|
||||
)
|
||||
|
||||
stress_fee = TRADING_FEE_RATE * SIM_FEE_STRESS_MULT
|
||||
sized_stress, _ = build_monitor_hybrid_sized_trades(
|
||||
sort_fires_chronological(all_monitor),
|
||||
cg_df,
|
||||
enhanced=False,
|
||||
fee_rate=stress_fee,
|
||||
dd_large_pct=dd_large,
|
||||
dd_medium_pct=dd_medium,
|
||||
)
|
||||
portfolio_compare["sim_hybrid_fee_stress"] = simulate_portfolio_summary(
|
||||
sized_stress,
|
||||
fee_rate=stress_fee,
|
||||
last_price=float(mark) if mark else None,
|
||||
use_amount_krw=True,
|
||||
)
|
||||
|
||||
portfolio_compare["sim_hybrid_slippage_stress"] = simulate_hybrid_slippage_stress(
|
||||
sized_h,
|
||||
last_price=float(mark) if mark else None,
|
||||
fee_rate=TRADING_FEE_RATE,
|
||||
)
|
||||
wf_rows = walk_forward_portfolio_by_month(steps_h)
|
||||
wf_port = walk_forward_portfolio_summary(wf_rows)
|
||||
portfolio_compare["hybrid_portfolio_walk_forward"] = wf_rows
|
||||
portfolio_compare["hybrid_portfolio_wf_summary"] = wf_port
|
||||
|
||||
gt_pnl_for_phase2 = float(
|
||||
(portfolio_compare.get("ground_truth_chrono") or {}).get("pnl_pct", 0)
|
||||
)
|
||||
go_hybrid = evaluate_hybrid_sizing_go(
|
||||
go,
|
||||
hybrid_full,
|
||||
portfolio_compare.get("sim_hybrid_holdout") or {},
|
||||
portfolio_compare.get("sim_hybrid_fee_stress") or {},
|
||||
)
|
||||
go_option_c_phase2 = evaluate_option_c_phase2_go(
|
||||
go_hybrid,
|
||||
hybrid_full,
|
||||
portfolio_compare.get("sim_hybrid_holdout") or {},
|
||||
portfolio_compare.get("sim_hybrid_fee_stress") or {},
|
||||
portfolio_compare.get("sim_hybrid_slippage_stress") or {},
|
||||
wf_port,
|
||||
gt_pnl_for_phase2,
|
||||
)
|
||||
|
||||
primary = go_hybrid.get("primary_sizing", "causal_tier")
|
||||
portfolio_compare["primary_sizing"] = primary
|
||||
if primary == "hybrid":
|
||||
portfolio_compare["sim_primary"] = {
|
||||
**hybrid_full,
|
||||
"sizing_mode": "primary_hybrid_dd_tier",
|
||||
"sizing_note": (
|
||||
"권장: monitor + past-leg·drawdown tier (검증 통과, 미래 미사용)"
|
||||
),
|
||||
}
|
||||
live_cap = simulate_hybrid_order_cap(
|
||||
outcomes,
|
||||
cg_df,
|
||||
rule_ids=monitor_ids,
|
||||
holdout_only=True,
|
||||
dd_large_pct=dd_large,
|
||||
dd_medium_pct=dd_medium,
|
||||
)
|
||||
else:
|
||||
portfolio_compare["sim_primary"] = portfolio_compare.get("sim_sized") or {}
|
||||
|
||||
if portfolio_compare.get("sim_sized") and portfolio_compare.get("ground_truth_chrono"):
|
||||
gt_pnl = float(portfolio_compare["ground_truth_chrono"].get("pnl_pct", 0))
|
||||
@@ -383,6 +721,35 @@ def build_simulation_report(
|
||||
gtp / gt_pnl if abs(gt_pnl) > 1e-6 else 0.0,
|
||||
4,
|
||||
)
|
||||
if portfolio_compare.get("sim_causal_gt"):
|
||||
cgp = float(portfolio_compare["sim_causal_gt"].get("pnl_pct", 0))
|
||||
portfolio_compare["causal_gt_capture_ratio"] = round(
|
||||
cgp / gt_pnl if abs(gt_pnl) > 1e-6 else 0.0,
|
||||
4,
|
||||
)
|
||||
portfolio_compare["sim_causal_gt_pnl_pct"] = cgp
|
||||
if portfolio_compare.get("sim_causal_hybrid"):
|
||||
chp = float(portfolio_compare["sim_causal_hybrid"].get("pnl_pct", 0))
|
||||
portfolio_compare["causal_hybrid_capture_ratio"] = round(
|
||||
chp / gt_pnl if abs(gt_pnl) > 1e-6 else 0.0,
|
||||
4,
|
||||
)
|
||||
portfolio_compare["sim_causal_hybrid_pnl_pct"] = chp
|
||||
if portfolio_compare.get("sim_tier_enhanced"):
|
||||
tep = float(portfolio_compare["sim_tier_enhanced"].get("pnl_pct", 0))
|
||||
portfolio_compare["tier_enhanced_capture_ratio"] = round(
|
||||
tep / gt_pnl if abs(gt_pnl) > 1e-6 else 0.0,
|
||||
4,
|
||||
)
|
||||
portfolio_compare["sim_tier_enhanced_pnl_pct"] = tep
|
||||
|
||||
portfolio_compare["causal_gt_params"] = {}
|
||||
try:
|
||||
from deepcoin.ground_truth.causal_gt_calibrate import load_causal_gt_params
|
||||
|
||||
portfolio_compare["causal_gt_params"] = load_causal_gt_params()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
portfolio_compare["gt_allocation_analysis"] = gt_alloc_analysis
|
||||
portfolio_compare["causal_mode"] = {
|
||||
@@ -423,6 +790,8 @@ def build_simulation_report(
|
||||
"fee_stress_by_rule": fee_stress,
|
||||
"live_order_cap_sim": live_cap,
|
||||
"go_no_go": go,
|
||||
"go_no_go_hybrid": go_hybrid,
|
||||
"go_no_go_option_c_phase2": go_option_c_phase2,
|
||||
"portfolio_compare": portfolio_compare,
|
||||
"gt_model": gt_data.get("model") or model_to_dict(default_model()),
|
||||
"gt_weight_policy": weight_policy_summary(default_model()),
|
||||
@@ -438,6 +807,12 @@ def build_simulation_report(
|
||||
"min_holdout_pf": SIM_GO_MIN_HOLDOUT_PF,
|
||||
"wf_positive_ratio": SIM_GO_WF_POSITIVE_RATIO,
|
||||
"wf_min_months": SIM_WALK_FORWARD_MIN_MONTHS,
|
||||
"hybrid_min_holdout_pnl_pct": SIM_HYBRID_MIN_HOLDOUT_PNL_PCT,
|
||||
"hybrid_max_mdd_pct": SIM_HYBRID_MAX_MDD_PCT,
|
||||
"option_c_target_pnl_pct": SIM_OPTION_C_TARGET_PNL_PCT,
|
||||
"option_c_phase2_target_pnl_pct": SIM_OPTION_C_PHASE2_TARGET_PNL_PCT,
|
||||
"option_c_phase2_fee_stress_ratio": SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO,
|
||||
"option_c_min_gt_capture": SIM_OPTION_C_MIN_GT_CAPTURE,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -480,9 +855,26 @@ def run_simulation_report(
|
||||
)
|
||||
write_simulation_html(report, MATCHING_SIMULATION_HTML)
|
||||
go = report["go_no_go"]["go"]
|
||||
go_h = report.get("go_no_go_hybrid") or {}
|
||||
pc_early = report.get("portfolio_compare") or {}
|
||||
print(f"[시뮬] 저장: {MATCHING_SIMULATION_JSON}")
|
||||
print(f"[시뮬] 저장: {MATCHING_SIMULATION_HTML}")
|
||||
print(f"[시뮬] Go/No-Go: {'GO' if go else 'NO-GO'}")
|
||||
print(f"[시뮬] Go/No-Go (규칙): {'GO' if go else 'NO-GO'}")
|
||||
print(
|
||||
f"[시뮬] Go/No-Go (hybrid tier): {'GO' if go_h.get('go') else 'NO-GO'} "
|
||||
f"· primary={pc_early.get('primary_sizing', '-')}"
|
||||
)
|
||||
for c in go_h.get("checks", []):
|
||||
mark = "OK" if c.get("pass") else "NG"
|
||||
opt = " (optional)" if c.get("optional") else ""
|
||||
print(f" [hybrid {mark}] {c.get('name')}: {c.get('value', '-')}{opt}")
|
||||
go_p2 = report.get("go_no_go_option_c_phase2") or {}
|
||||
print(
|
||||
f"[시뮬] Option C 2차(+1000%): {'GO' if go_p2.get('go') else 'NO-GO'}"
|
||||
)
|
||||
for c in go_p2.get("checks", []):
|
||||
mark = "OK" if c.get("pass") else "NG"
|
||||
print(f" [phase2 {mark}] {c.get('name')}: {c.get('value', '-')}")
|
||||
for c in report["go_no_go"].get("checks", []):
|
||||
mark = "OK" if c["pass"] else "NG"
|
||||
print(
|
||||
@@ -504,6 +896,44 @@ def run_simulation_report(
|
||||
f"{pc.get('sim_gt_model', {}).get('pnl_pct')}% "
|
||||
f"(capture={pc.get('gt_model_capture_ratio'):.2%})"
|
||||
)
|
||||
if pc.get("sim_causal_gt_pnl_pct") is not None:
|
||||
scg = pc.get("sim_causal_gt") or {}
|
||||
print(
|
||||
f"[시뮬] GT 대비 sim_causal_gt(인과 leg): "
|
||||
f"{pc.get('sim_causal_gt_pnl_pct')}% "
|
||||
f"(capture={pc.get('causal_gt_capture_ratio', 0):.2%}, "
|
||||
f"legs={scg.get('leg_count', '-')}, trades={scg.get('trade_count', '-')})"
|
||||
)
|
||||
if pc.get("sim_causal_hybrid_pnl_pct") is not None:
|
||||
sch = pc.get("sim_causal_hybrid") or {}
|
||||
print(
|
||||
f"[시뮬] GT 대비 sim_causal_hybrid(monitor+DD tier): "
|
||||
f"{pc.get('sim_causal_hybrid_pnl_pct')}% "
|
||||
f"(capture={pc.get('causal_hybrid_capture_ratio', 0):.2%}, "
|
||||
f"MDD={sch.get('max_drawdown_pct', '-')}%)"
|
||||
)
|
||||
if pc.get("sim_primary"):
|
||||
sp = pc["sim_primary"]
|
||||
print(
|
||||
f"[시뮬] 권장 primary ({pc.get('primary_sizing')}): "
|
||||
f"{sp.get('pnl_pct')}% · MDD={sp.get('max_drawdown_pct', '-')}%"
|
||||
)
|
||||
ho_h = pc.get("sim_hybrid_holdout") or {}
|
||||
if ho_h.get("pnl_pct") is not None:
|
||||
print(
|
||||
f"[시뮬] hybrid holdout: {ho_h.get('pnl_pct')}% "
|
||||
f"({ho_h.get('initial_asset_krw')}→{ho_h.get('final_asset_krw')})"
|
||||
)
|
||||
if pc.get("sim_tier_enhanced_pnl_pct") is not None:
|
||||
ste = pc.get("sim_tier_enhanced") or {}
|
||||
ast = ste.get("alloc_stats") or {}
|
||||
print(
|
||||
f"[시뮬] GT 대비 sim_tier_enhanced(conviction tier): "
|
||||
f"{pc.get('sim_tier_enhanced_pnl_pct')}% "
|
||||
f"(capture={pc.get('tier_enhanced_capture_ratio', 0):.2%}, "
|
||||
f"large_buys={ast.get('large_tier_buy_count', '-')}, "
|
||||
f"avg_buy={ast.get('buy_amount_avg_krw', '-')})"
|
||||
)
|
||||
if pc.get("sim_sized", {}).get("max_drawdown_pct") is not None:
|
||||
print(
|
||||
f"[시뮬] sim_sized MDD: {pc['sim_sized']['max_drawdown_pct']}% "
|
||||
|
||||
@@ -202,6 +202,13 @@ def _summary_cards_html(
|
||||
sim_trade_count: int,
|
||||
go_flag: bool,
|
||||
model_note: str = "",
|
||||
sim_causal_gt_pnl: dict[str, Any] | None = None,
|
||||
sim_causal_hybrid_pnl: dict[str, Any] | None = None,
|
||||
sim_tier_enhanced_pnl: dict[str, Any] | None = None,
|
||||
sim_primary_pnl: dict[str, Any] | None = None,
|
||||
primary_sizing: str = "",
|
||||
hybrid_go: bool = False,
|
||||
phase2_go: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
ground_truth HTML과 동일 구성의 상단 카드 (GT + 시뮬 2줄).
|
||||
@@ -216,6 +223,12 @@ def _summary_cards_html(
|
||||
sim_trade_count: 체결 가정 발화 수.
|
||||
go_flag: Go/No-Go.
|
||||
model_note: GT 모델 한 줄 요약.
|
||||
sim_causal_gt_pnl: 인과 GT leg 엔진 요약.
|
||||
sim_causal_hybrid_pnl: monitor buy + 인과 sell 하이브리드.
|
||||
sim_tier_enhanced_pnl: monitor + conviction tier.
|
||||
sim_primary_pnl: 검증 통과 권장 배분 경로.
|
||||
primary_sizing: hybrid | causal_tier.
|
||||
hybrid_go: hybrid tier Go/No-Go.
|
||||
|
||||
Returns:
|
||||
cards HTML.
|
||||
@@ -235,6 +248,61 @@ def _summary_cards_html(
|
||||
f'<span class="{go_cls}">{"GO" if go_flag else "NO-GO"}</span>'
|
||||
)
|
||||
sim_fixed_title = f"시뮬·고정 ₩{LIVE_ORDER_KRW:,}/회 (비교)"
|
||||
primary_block = ""
|
||||
if sim_primary_pnl and float(sim_primary_pnl.get("pnl_pct") or 0) != 0:
|
||||
hgo_cls = "go-pass" if hybrid_go else "go-fail"
|
||||
primary_block = stacked_summary_cards_html(
|
||||
(
|
||||
f"권장 primary · {primary_sizing or 'causal_tier'} · "
|
||||
f'<span class="{hgo_cls}">hybrid {"GO" if hybrid_go else "NO-GO"}</span> · '
|
||||
f'2차 {"GO" if phase2_go else "NO-GO"}'
|
||||
),
|
||||
pnl_cards_html(
|
||||
sim_primary_pnl,
|
||||
"Primary",
|
||||
int(sim_primary_pnl.get("trade_count") or 0),
|
||||
),
|
||||
)
|
||||
causal_block = ""
|
||||
if sim_causal_gt_pnl and (
|
||||
float(sim_causal_gt_pnl.get("pnl_pct") or 0) != 0
|
||||
or sim_causal_gt_pnl.get("trade_count")
|
||||
):
|
||||
legs = sim_causal_gt_pnl.get("leg_count", "-")
|
||||
causal_block += stacked_summary_cards_html(
|
||||
f"인과 GT leg 엔진 (peak_local·분할매수·causal tier) · leg {legs}개",
|
||||
pnl_cards_html(
|
||||
sim_causal_gt_pnl,
|
||||
"인과 GT",
|
||||
int(sim_causal_gt_pnl.get("trade_count") or 0),
|
||||
),
|
||||
)
|
||||
if sim_causal_hybrid_pnl and (
|
||||
float(sim_causal_hybrid_pnl.get("pnl_pct") or 0) != 0
|
||||
or sim_causal_hybrid_pnl.get("trade_count")
|
||||
):
|
||||
legs_h = sim_causal_hybrid_pnl.get("leg_count", "-")
|
||||
causal_block += stacked_summary_cards_html(
|
||||
f"하이브리드 (monitor + DD tier) · 발화 {sim_causal_hybrid_pnl.get('input_fires', '-')}건",
|
||||
pnl_cards_html(
|
||||
sim_causal_hybrid_pnl,
|
||||
"하이브리드",
|
||||
int(sim_causal_hybrid_pnl.get("trade_count") or 0),
|
||||
),
|
||||
)
|
||||
if sim_tier_enhanced_pnl and (
|
||||
float(sim_tier_enhanced_pnl.get("pnl_pct") or 0) != 0
|
||||
or sim_tier_enhanced_pnl.get("trade_count")
|
||||
):
|
||||
ast = sim_tier_enhanced_pnl.get("alloc_stats") or {}
|
||||
causal_block += stacked_summary_cards_html(
|
||||
f"Enhanced tier (conviction·DD) · large매수 {ast.get('large_tier_buy_count', '-')}건",
|
||||
pnl_cards_html(
|
||||
sim_tier_enhanced_pnl,
|
||||
"Enhanced",
|
||||
int(sim_tier_enhanced_pnl.get("trade_count") or 0),
|
||||
),
|
||||
)
|
||||
return (
|
||||
'<div class="summary-cards">'
|
||||
+ stacked_summary_cards_html(gt_sub, gt_cards)
|
||||
@@ -246,6 +314,8 @@ def _summary_cards_html(
|
||||
sim_fixed_title,
|
||||
pnl_cards_html(sim_fixed_pnl, "시뮬(고정)", sim_trade_count),
|
||||
)
|
||||
+ primary_block
|
||||
+ causal_block
|
||||
+ "</div>"
|
||||
)
|
||||
|
||||
@@ -298,6 +368,11 @@ def build_simulation_page_html(
|
||||
|
||||
go = report.get("go_no_go", {})
|
||||
go_flag = bool(go.get("go"))
|
||||
go_hybrid = report.get("go_no_go_hybrid") or {}
|
||||
hybrid_go_flag = bool(go_hybrid.get("go"))
|
||||
go_phase2 = report.get("go_no_go_option_c_phase2") or {}
|
||||
phase2_go_flag = bool(go_phase2.get("go"))
|
||||
pc = report.get("portfolio_compare") or {}
|
||||
label_mode = report.get("label_mode", "leg_gt")
|
||||
|
||||
frames = load_chart_frames()
|
||||
@@ -372,6 +447,36 @@ def build_simulation_page_html(
|
||||
|
||||
criteria_blocks = "".join(rule_criteria_html(r) for r in monitor_rules)
|
||||
go_table = go_no_go_table_html(go.get("checks", []), go_flag)
|
||||
hybrid_checks = go_hybrid.get("checks") or []
|
||||
if hybrid_checks:
|
||||
hybrid_rows = "".join(
|
||||
f"<tr><td>{c.get('name')}</td>"
|
||||
f"<td>{'PASS' if c.get('pass') else 'FAIL'}</td>"
|
||||
f"<td>{c.get('value', '-')}</td></tr>"
|
||||
for c in hybrid_checks
|
||||
)
|
||||
hgo_cls = "go-pass" if hybrid_go_flag else "go-fail"
|
||||
go_table += (
|
||||
f"<h2>Hybrid tier Go/No-Go · "
|
||||
f'<span class="{hgo_cls}">{"GO" if hybrid_go_flag else "NO-GO"}</span></h2>'
|
||||
f"<table><thead><tr><th>검사</th><th>결과</th><th>값</th></tr></thead>"
|
||||
f"<tbody>{hybrid_rows}</tbody></table>"
|
||||
)
|
||||
phase2_checks = go_phase2.get("checks") or []
|
||||
if phase2_checks:
|
||||
p2_rows = "".join(
|
||||
f"<tr><td>{c.get('name')}</td>"
|
||||
f"<td>{'PASS' if c.get('pass') else 'FAIL'}</td>"
|
||||
f"<td>{c.get('value', '-')}</td></tr>"
|
||||
for c in phase2_checks
|
||||
)
|
||||
p2_cls = "go-pass" if phase2_go_flag else "go-fail"
|
||||
go_table += (
|
||||
f"<h2>Option C 2차 (+1000%) · "
|
||||
f'<span class="{p2_cls}">{"GO" if phase2_go_flag else "NO-GO"}</span></h2>'
|
||||
f"<table><thead><tr><th>검사</th><th>결과</th><th>값</th></tr></thead>"
|
||||
f"<tbody>{p2_rows}</tbody></table>"
|
||||
)
|
||||
|
||||
def _mark_note(price: float) -> str:
|
||||
if price > 0:
|
||||
@@ -439,6 +544,13 @@ def build_simulation_page_html(
|
||||
len(compound_fires),
|
||||
go_flag,
|
||||
model_note=model_note,
|
||||
sim_causal_gt_pnl=pc.get("sim_causal_gt"),
|
||||
sim_causal_hybrid_pnl=pc.get("sim_causal_hybrid"),
|
||||
sim_tier_enhanced_pnl=pc.get("sim_tier_enhanced"),
|
||||
sim_primary_pnl=pc.get("sim_primary"),
|
||||
primary_sizing=str(pc.get("primary_sizing") or ""),
|
||||
hybrid_go=hybrid_go_flag,
|
||||
phase2_go=phase2_go_flag,
|
||||
)
|
||||
|
||||
if frames is not None:
|
||||
|
||||
Reference in New Issue
Block a user