GT MTF 프로필·캘리브레이션과 04 매칭/시뮬/실거래 파이프라인을 추가한다.

3분~일봉 GT 타점 분석(03c), leg 체결 순서 수정, 총자산 90% 검증 루프,
walk-forward Go/No-Go 시뮬, monitor·live_trader 및 reference 문서를 포함한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 11:27:50 +09:00
parent b52d61b777
commit 2cb67c42b3
47 changed files with 5956 additions and 209 deletions

View File

@@ -0,0 +1,177 @@
"""
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),
}