3분~일봉 GT 타점 분석(03c), leg 체결 순서 수정, 총자산 90% 검증 루프, walk-forward Go/No-Go 시뮬, monitor·live_trader 및 reference 문서를 포함한다. Co-authored-by: Cursor <cursoragent@cursor.com>
178 lines
5.3 KiB
Python
178 lines
5.3 KiB
Python
"""
|
|
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),
|
|
}
|