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