""" 인과 GT leg 엔진 파라미터 그리드 탐색·최적 저장. """ from __future__ import annotations import json from itertools import product from pathlib import Path from typing import Any from config import ( CHART_LOOKBACK_DAYS, GT_BUY_BB_MAX, GT_BUY_MIN_SWING_PCT, GT_MIN_SWING_PCT, GT_PIVOT_ORDER, MATCH_PRIMARY_INTERVAL, SYMBOL, ) from deepcoin.data.mtf_bb import load_frames_from_db from deepcoin.ground_truth.causal_gt_trades import simulate_causal_gt_portfolio from deepcoin.ground_truth.ground_truth import load_ground_truth from deepcoin.ops.monitor import Monitor from deepcoin.paths import MATCHING_CAUSAL_GT_CALIBRATION_JSON, resolve_ground_truth_file def default_causal_gt_params() -> dict[str, Any]: """ 인과 GT leg 엔진 기본 파라미터. Returns: build_causal_split_buy_peak_sell_trades 키워드 인자. """ from config import ( CAUSAL_GT_MIN_BARS_BETWEEN_LEGS, CAUSAL_GT_MIN_LEG_PCT, CAUSAL_GT_PEAK_MODE, CAUSAL_GT_USE_LOCAL_TROUGH, ) return { "pivot_order": GT_PIVOT_ORDER, "buy_swing_pct": GT_BUY_MIN_SWING_PCT, "sell_swing_pct": GT_MIN_SWING_PCT, "bb_max": GT_BUY_BB_MAX, "min_leg_pct": CAUSAL_GT_MIN_LEG_PCT, "use_local_trough": CAUSAL_GT_USE_LOCAL_TROUGH, "peak_mode": CAUSAL_GT_PEAK_MODE, "min_bars_between_legs": CAUSAL_GT_MIN_BARS_BETWEEN_LEGS, } def load_causal_gt_params(path: Path | None = None) -> dict[str, Any]: """ 캘리브레이션 JSON 또는 config 기본값. Args: path: JSON 경로. None이면 MATCHING_CAUSAL_GT_CALIBRATION_JSON. Returns: best params dict. """ p = path or MATCHING_CAUSAL_GT_CALIBRATION_JSON if p.is_file(): data = json.loads(p.read_text(encoding="utf-8")) best = data.get("best_params") or data.get("params") if best: return dict(best) return default_causal_gt_params() def _grid_space() -> dict[str, list[Any]]: """탐색 그리드 (로컬 peak 최적화 반영, 조합 ~864).""" return { "peak_mode": ["local", "zigzag"], "pivot_order": [8, 10, 12, 15], "buy_swing_pct": [2.0, 2.5, 3.0], "sell_swing_pct": [3.0, 4.0], "bb_max": [0.55, 0.65, 0.75], "min_leg_pct": [3.0, 5.0, 8.0], "min_bars_between_legs": [60, 90], "use_local_trough": [True, False], } def run_causal_gt_calibration( *, min_trades: int = 30, top_n: int = 20, out_path: Path | None = None, ) -> dict[str, Any]: """ 그리드 탐색 후 최적 파라미터 JSON 저장. Args: min_trades: 최소 체결 수 미만 조합 제외. top_n: 상위 N개 기록. out_path: 저장 경로. Returns: calibration report dict. """ gt = load_ground_truth(resolve_ground_truth_file()) or {} mark = float((gt.get("summary") or {}).get("mark_price") or 0) gt_pnl = float( (gt.get("summary") or {}).get("pnl_pct") or 0 ) mon = Monitor(cooldown_file=None) frames = load_frames_from_db(mon, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS) df = frames[MATCH_PRIMARY_INTERVAL].copy() grid = _grid_space() keys = list(grid.keys()) results: list[dict[str, Any]] = [] total = 1 for k in keys: total *= len(grid[k]) print(f"[causal_gt] 그리드 {total} 조합 탐색...") done = 0 for combo in product(*(grid[k] for k in keys)): params = dict(zip(keys, combo)) r = simulate_causal_gt_portfolio(df, last_price=mark or None, **params) tc = int(r.get("trade_count") or 0) done += 1 if done % 200 == 0: print(f" ... {done}/{total}") if tc < min_trades: continue pnl = float(r.get("pnl_pct") or 0) results.append( { "pnl_pct": round(pnl, 2), "trade_count": tc, "leg_count": r.get("leg_count", 0), "max_drawdown_pct": r.get("max_drawdown_pct"), "capture_ratio": round(pnl / gt_pnl, 4) if gt_pnl else 0, "params": params, } ) results.sort(key=lambda x: x["pnl_pct"], reverse=True) best = results[0] if results else None report: dict[str, Any] = { "symbol": SYMBOL, "interval_min": MATCH_PRIMARY_INTERVAL, "gt_pnl_pct": gt_pnl, "grid_combinations": total, "valid_combinations": len(results), "min_trades": min_trades, "best": best, "best_params": best["params"] if best else default_causal_gt_params(), "top": results[:top_n], "target_pnl_pct": 300.0, "target_met": bool(best and best["pnl_pct"] >= 300.0), } out = out_path or MATCHING_CAUSAL_GT_CALIBRATION_JSON out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") print(f"[causal_gt] 저장: {out}") if best: print( f"[causal_gt] 최적 PnL={best['pnl_pct']}% " f"trades={best['trade_count']} legs={best['leg_count']} " f"capture={best.get('capture_ratio', 0):.2%}" ) else: print("[causal_gt] 유효 조합 없음") return report