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