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:
xavis
2026-06-01 16:09:18 +09:00
parent 9b00ef34c6
commit d385456867
21 changed files with 3315 additions and 1178 deletions

View File

@@ -0,0 +1,200 @@
"""
Hybrid DD tier 임계값 train 그리드 → holdout 검증 (Option C 2차).
"""
from __future__ import annotations
import json
from itertools import product
from pathlib import Path
from typing import Any
import pandas as pd
from config import GT_INITIAL_CASH_KRW, MATCH_HOLDOUT_RATIO, TRADING_FEE_RATE
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.matching.option_c_phase2 import walk_forward_portfolio_by_month
from deepcoin.matching.portfolio_sim import sort_fires_chronological
from deepcoin.matching.simulation import portfolio_holdout_from_steps
from deepcoin.paths import MATCHING_HYBRID_DD_CALIBRATION_JSON
def default_dd_grid() -> dict[str, list[float]]:
"""DD large/medium 탐색 그리드."""
return {
"dd_large_pct": [5.0, 6.0, 8.0, 10.0, 12.0],
"dd_medium_pct": [2.0, 3.0, 4.0, 6.0],
}
def load_hybrid_dd_params(path: Path | None = None) -> dict[str, float]:
"""
캘리브레이션 JSON 또는 config 기본값.
Args:
path: JSON 경로.
Returns:
{dd_large_pct, dd_medium_pct}.
"""
from config import CAUSAL_GT_DD_LARGE_PCT, CAUSAL_GT_DD_MEDIUM_PCT
p = path or MATCHING_HYBRID_DD_CALIBRATION_JSON
if p.is_file():
data = json.loads(p.read_text(encoding="utf-8"))
best = data.get("best_params") or {}
if best.get("dd_large_pct") is not None:
return {
"dd_large_pct": float(best["dd_large_pct"]),
"dd_medium_pct": float(
best.get("dd_medium_pct", CAUSAL_GT_DD_MEDIUM_PCT)
),
}
return {
"dd_large_pct": float(CAUSAL_GT_DD_LARGE_PCT),
"dd_medium_pct": float(CAUSAL_GT_DD_MEDIUM_PCT),
}
def calibrate_hybrid_dd_thresholds(
fires: pd.DataFrame,
ohlc_df: pd.DataFrame,
*,
holdout_start: pd.Timestamp,
grid: dict[str, list[float]] | None = None,
last_price: float | None = None,
) -> dict[str, Any]:
"""
train 구간 PnL 최대 → holdout PnL로 검증, 최적 DD 임계 저장.
Args:
fires: monitor 전체 발화.
ohlc_df: 3m OHLC.
holdout_start: holdout 시작 시각.
grid: dd_large/medium 후보.
last_price: 미청산 평가가.
Returns:
best_params, train/holdout metrics, grid top-N.
"""
from deepcoin.ground_truth.gt_allocation import simulate_portfolio_summary
grid = grid or default_dd_grid()
chron = sort_fires_chronological(fires)
results: list[dict[str, Any]] = []
for dd_large, dd_medium in product(
grid["dd_large_pct"],
grid["dd_medium_pct"],
):
if dd_medium >= dd_large:
continue
sized, stats = build_monitor_hybrid_sized_trades(
chron,
ohlc_df,
enhanced=False,
dd_large_pct=dd_large,
dd_medium_pct=dd_medium,
)
steps = simulate_portfolio_steps(sized, use_amount_krw=True)
train = portfolio_holdout_from_steps(
[s for s in steps if pd.to_datetime(s["dt"]) < holdout_start],
holdout_start,
initial_if_empty=GT_INITIAL_CASH_KRW,
note="train",
)
# train-only: start 1M → last asset before holdout
if steps:
pre = [
float(s["total_asset_krw"])
for s in steps
if pd.to_datetime(s["dt"]) < holdout_start
]
train_asset_end = pre[-1] if pre else GT_INITIAL_CASH_KRW
train_pnl = (train_asset_end - GT_INITIAL_CASH_KRW) / GT_INITIAL_CASH_KRW * 100
else:
train_pnl = 0.0
holdout = portfolio_holdout_from_steps(
steps,
holdout_start,
note="holdout",
)
full = simulate_portfolio_summary(
sized,
last_price=last_price,
use_amount_krw=True,
)
wf = walk_forward_portfolio_by_month(steps)
pos_months = sum(1 for w in wf if float(w.get("pnl_pct") or 0) > 0)
results.append(
{
"dd_large_pct": dd_large,
"dd_medium_pct": dd_medium,
"train_pnl_pct": round(train_pnl, 2),
"holdout_pnl_pct": float(holdout.get("pnl_pct", 0)),
"full_pnl_pct": float(full.get("pnl_pct", 0)),
"max_drawdown_pct": float(full.get("max_drawdown_pct", 0)),
"wf_positive_months": pos_months,
"wf_months": len(wf),
"large_tier_buys": stats.get("large_tier_buy_count", 0),
}
)
if not results:
return {"best_params": load_hybrid_dd_params(), "note": "empty grid"}
# train PnL 1순위, holdout PnL 2순위
ranked = sorted(
results,
key=lambda x: (x["train_pnl_pct"], x["holdout_pnl_pct"]),
reverse=True,
)
best = ranked[0]
return {
"best_params": {
"dd_large_pct": best["dd_large_pct"],
"dd_medium_pct": best["dd_medium_pct"],
},
"best_metrics": best,
"grid_size": len(results),
"top5": ranked[:5],
"holdout_start": str(holdout_start),
}
def run_and_save_calibration(
fires: pd.DataFrame,
ohlc_df: pd.DataFrame,
*,
outcomes: pd.DataFrame,
last_price: float | None = None,
out_path: Path | None = None,
) -> dict[str, Any]:
"""
캘리브레이션 실행 후 JSON 저장.
Args:
fires: monitor 발화.
ohlc_df: OHLC.
outcomes: fire_outcomes (holdout split).
last_price: 평가 종가.
out_path: 저장 경로.
Returns:
calibrate_hybrid_dd_thresholds 결과.
"""
outcomes_ts = outcomes.copy()
outcomes_ts["ts"] = pd.to_datetime(outcomes_ts["dt"])
holdout_start = outcomes_ts["ts"].quantile(1.0 - MATCH_HOLDOUT_RATIO)
result = calibrate_hybrid_dd_thresholds(
fires,
ohlc_df,
holdout_start=holdout_start,
last_price=last_price,
)
p = out_path or MATCHING_HYBRID_DD_CALIBRATION_JSON
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
return result