""" GT 총자산 대비 시뮬/규칙 정확도 측정 (동일 체결·평가 모델). """ from __future__ import annotations from typing import Any import pandas as pd from config import GT_INITIAL_CASH_KRW, MATCH_GT_TOLERANCE_MIN, TRADING_FEE_RATE from deepcoin.ground_truth.ground_truth import simulate_truth_portfolio from deepcoin.matching.rule_eval import eval_rule_mask def gt_trades_for_legs( trades: list[dict[str, Any]], leg_ids: set[int], ) -> list[dict[str, Any]]: """ leg_id 집합에 속한 GT 체결만 반환. Args: trades: ground_truth trades. leg_ids: 포함할 leg_id. Returns: 필터된 trade dict 리스트. """ return [t for t in trades if int(t.get("leg_id", 0)) in leg_ids] def covered_legs_from_fires( trades: list[dict[str, Any]], fires: pd.DataFrame, buy_rule_ids: list[str], sell_rule_ids: list[str], tolerance_min: int = MATCH_GT_TOLERANCE_MIN, ) -> set[int]: """ 매수·매도 규칙 발화가 GT 타점 ±허용 내인 leg_id 집합. Args: trades: GT trades. fires: rule_fires. buy_rule_ids: 매수 규칙 ID. sell_rule_ids: 매도 규칙 ID. tolerance_min: 허용 분. Returns: 양쪽 모두 커버된 leg_id. """ if fires.empty: return set() tol = pd.Timedelta(minutes=tolerance_min) gt_df = pd.DataFrame(trades) gt_df["ts"] = pd.to_datetime(gt_df["dt"]) fires = fires.copy() fires["ts"] = pd.to_datetime(fires["dt"]) bf = fires[fires["rule_id"].isin(buy_rule_ids) & (fires["side"] == "buy")] sf = fires[fires["rule_id"].isin(sell_rule_ids) & (fires["side"] == "sell")] covered: set[int] = set() for lid in gt_df["leg_id"].unique(): leg = gt_df[gt_df["leg_id"] == lid] buys = leg[leg["action"] == "buy"] sells = leg[leg["action"] == "sell"] buy_ok = True for ts in buys["ts"]: if bf.empty or (bf["ts"] - ts).abs().min() > tol: buy_ok = False break sell_ok = True for ts in sells["ts"]: if sf.empty or (sf["ts"] - ts).abs().min() > tol: sell_ok = False break if buy_ok and sell_ok: covered.add(int(lid)) return covered def portfolio_asset_ratio( trades: list[dict[str, Any]], leg_ids: set[int], last_price: float | None, ) -> dict[str, Any]: """ GT 체결 모델로 전체 vs 부분 leg 포트폴리오 비율. Args: trades: 전체 GT trades. leg_ids: 포함 leg. last_price: 종가 평가. Returns: full/subset final_asset, asset_ratio, leg counts. """ full = simulate_truth_portfolio( trades, initial_cash=GT_INITIAL_CASH_KRW, fee_rate=TRADING_FEE_RATE, last_price=last_price, ) subset_trades = gt_trades_for_legs(trades, leg_ids) part = simulate_truth_portfolio( subset_trades, initial_cash=GT_INITIAL_CASH_KRW, fee_rate=TRADING_FEE_RATE, last_price=last_price, ) gt_final = float(full["final_asset_krw"]) sub_final = float(part["final_asset_krw"]) ratio = sub_final / gt_final if gt_final > 0 else 0.0 return { "gt_final_asset_krw": gt_final, "subset_final_asset_krw": sub_final, "asset_ratio": round(ratio, 4), "asset_accuracy_pct": round(ratio * 100.0, 2), "target_met_90": ratio >= 0.9, "legs_total": len(set(int(t.get("leg_id", 0)) for t in trades)), "legs_covered": len(leg_ids), "leg_coverage_ratio": round( len(leg_ids) / max(len(set(int(t.get("leg_id", 0)) for t in trades)), 1), 4, ), "full_pnl_pct": full.get("pnl_pct"), "subset_pnl_pct": part.get("pnl_pct"), } def evaluate_gt_snapshot_recall( trades_df: pd.DataFrame, rules: list[dict[str, Any]], ) -> dict[str, Any]: """ 03b 각 GT 행에서 규칙 스냅샷 충족 여부(OR across rules per side). Args: trades_df: general_analysis_trades.csv. rules: rule dict 리스트. Returns: buy/sell recall, per-rule counts. """ buy_gt = trades_df[trades_df["action"] == "buy"] sell_gt = trades_df[trades_df["action"] == "sell"] buy_rules = [r for r in rules if r.get("side") == "buy"] sell_rules = [r for r in rules if r.get("side") == "sell"] def _side_recall(gt: pd.DataFrame, side_rules: list[dict]) -> dict[str, Any]: if gt.empty or not side_rules: return {"gt_count": int(len(gt)), "matched": 0, "recall": 0.0} hit = 0 per_rule: dict[str, int] = {} for _, row in gt.iterrows(): fr = pd.DataFrame([row]) ok = False for rule in side_rules: if bool(eval_rule_mask(fr, rule).iloc[0]): ok = True rid = rule["rule_id"] per_rule[rid] = per_rule.get(rid, 0) + 1 if ok: hit += 1 n = len(gt) return { "gt_count": n, "matched": hit, "recall": round(hit / n, 4) if n else 0.0, "per_rule_hits": per_rule, } return { "buy": _side_recall(buy_gt, buy_rules), "sell": _side_recall(sell_gt, sell_rules), }