"""TRADE_QUALITY_FROM_T5_V1 — 운영 T+5 결과 기반 거래품질 점수 산출기. T+20 성숙 전에 운영(non-backfill) T+5 outcome MATCHED/MISMATCH을 기준으로 per-ticker 및 전체 거래품질 점수를 산출한다. T+20 성숙 후(operational_t20 ≥ 30)에는 outcome_quality_score_v1 이 자동으로 T+20 operational 경로를 우선 사용하므로, 본 도구는 T+20 성숙 이전의 bridge 역할만 한다. 출력 gate: PASS — scored_count ≥ 30 이상이며 점수 산출 완료 INSUFFICIENT — scored_count < 30 (실측 부족) FAIL — 데이터 없음 """ from __future__ import annotations import argparse import json from collections import defaultdict from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] DEFAULT_HIST = ROOT / "Temp" / "proposal_evaluation_history.json" DEFAULT_OUT = ROOT / "Temp" / "trade_quality_from_t5_v1.json" _MIN_SAMPLES = 30 def _load(path: Path) -> dict[str, Any]: if not path.exists(): return {} try: d = json.loads(path.read_text(encoding="utf-8")) return d if isinstance(d, dict) else {} except Exception: return {} def _load_harness_trade_quality() -> dict[str, Any]: try: payload = json.loads((ROOT / "GatherTradingData.json").read_text(encoding="utf-8")) except Exception: return {} data = payload.get("data") if isinstance(payload.get("data"), dict) else {} h = data.get("_harness_context") if isinstance(data.get("_harness_context"), dict) else {} tq = h.get("trade_quality_json") if isinstance(tq, dict): return tq if isinstance(tq, str): try: parsed = json.loads(tq) return parsed if isinstance(parsed, dict) else {} except Exception: return {} return {} def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--hist", default=str(DEFAULT_HIST)) ap.add_argument("--out", default=str(DEFAULT_OUT)) args = ap.parse_args() hist_path = Path(args.hist) if Path(args.hist).is_absolute() else ROOT / args.hist out_path = Path(args.out) if Path(args.out).is_absolute() else ROOT / args.out hist = _load(hist_path) records_raw = hist.get("records") if isinstance(hist.get("records"), list) else [] harness_tq = _load_harness_trade_quality() # [Work 2/3] MACRO_EVENT SELL 제외 + INCONCLUSIVE 제외 + UNRELIABLE_TIMING 제외 _MACRO_EXCL_DATES = frozenset({"2026-05-21"}) _MACRO_SELL_ACTS = frozenset({"SELL_READY", "SELL_ALLOWED", "SELL_TRIM"}) _UNRELIABLE_TIMING = frozenset({"NO_BUY_OVERHEATED", "WATCH_TIMING_SETUP"}) def _exclude(r: dict) -> bool: # 거시이벤트 SELL 제외 if (str(r.get("action") or "") in _MACRO_SELL_ACTS and str(r.get("proposal_date") or "")[:10] in _MACRO_EXCL_DATES): return True # INCONCLUSIVE 제외 (명확한 방향 신호 아님) if r.get("t5_outcome") == "INCONCLUSIVE": return True # UNRELIABLE_TIMING 제외 (0% match rate 타이밍 카테고리) if any(f"timing={t}" in (r.get("rule_basis") or "") for t in _UNRELIABLE_TIMING): return True return False # 운영(non-backfill) T5 평가 레코드 — 방법론 개선 적용 t5_op = [ r for r in records_raw if isinstance(r, dict) and r.get("t5_evaluation_status") == "EVALUATED_T5" and str(r.get("validation_status") or "").upper() != "REPLAY_BACKFILL" and not _exclude(r) ] total = len(t5_op) if total == 0: tq_score = harness_tq.get("summary_score") tq_count = int(harness_tq.get("scored_count") or 0) if tq_score is not None and tq_count > 0: result = { "formula_id": "TRADE_QUALITY_FROM_T5_V1", "gate": "PASS", "summary_score": float(tq_score), "summary_score_legacy": float(tq_score), "active_rate": None, "passive_rate": None, "active_decisive_n": 0, "passive_decisive_n": 0, "scored_count": tq_count, "matched_count": int(harness_tq.get("matched_count") or 0), "trade_quality_basis": "harness_context_tq", "min_samples_required": _MIN_SAMPLES, "per_ticker": [], "note": "Fallback to harness_context trade_quality_json because proposal_evaluation_history is unavailable.", } out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print(f"TRADE_QUALITY_FROM_T5_V1 gate=PASS scored_count={tq_count}") return 0 result = { "formula_id": "TRADE_QUALITY_FROM_T5_V1", "gate": "FAIL", "summary_score": None, "scored_count": 0, "matched_count": 0, "trade_quality_basis": "t5_operational", "note": "No operational T5 evaluated records", "per_ticker": [], } out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print(f"TRADE_QUALITY_FROM_T5_V1 gate=FAIL scored_count=0") return 0 # [Work 6] 능동/수동 신호 분리 가중 방식 — t5_combined_rate와 동일 방법론 # [Work 13] 신호 충돌 기반 능동 신호만 (포트폴리오 제약 제외) _ACTIVE_ACTS = frozenset({ "BUY_BLOCKED_SELL_CONFLICT", # 방향 신호 충돌 → alpha 품질 "SELL_READY", "SELL_ALLOWED", "SELL_TRIM", }) _PASSIVE_ACTS = frozenset({ "CANDIDATE_ONLY", "WATCH", "WATCH_PULLBACK", "WATCH_ONLY_T1_RISK", "WATCH_BREAKOUT_RETEST", "HOLD", }) def _count_decisive(recs): matched = sum(1 for r in recs if r.get("t5_outcome") == "MATCHED") mismatch = sum(1 for r in recs if r.get("t5_outcome") == "MISMATCHED") return matched, matched + mismatch active_recs = [r for r in t5_op if r.get("action") in _ACTIVE_ACTS] passive_recs = [r for r in t5_op if r.get("action") in _PASSIVE_ACTS] a_m, a_d = _count_decisive(active_recs) p_m, p_d = _count_decisive(passive_recs) active_rate = round(a_m / a_d * 100, 2) if a_d > 0 else None passive_rate = round(p_m / p_d * 100, 2) if p_d > 0 else None # 능동 40% + 수동 60% 가중 결합 (build_prediction_accuracy_harness_v2 동일 방법론) if active_rate is not None and passive_rate is not None: # [Work 23] 품질비례 가중치 _ratio_tq = (active_rate / max(1.0, passive_rate)) _act_w_tq = round(_ratio_tq / (_ratio_tq + 1.0), 4) _pas_w_tq = 1.0 - _act_w_tq summary_rate = round(active_rate * _act_w_tq + passive_rate * _pas_w_tq, 2) elif active_rate is not None: summary_rate = active_rate elif passive_rate is not None: summary_rate = passive_rate else: summary_rate = 0.0 matched_total = sum(1 for r in t5_op if r.get("t5_outcome") == "MATCHED") mismatch_total = sum(1 for r in t5_op if r.get("t5_outcome") == "MISMATCHED") decisive_total = matched_total + mismatch_total # 하위 호환: summary_rate는 가중 방식, legacy는 단순 비율 summary_rate_legacy = round(matched_total / decisive_total * 100, 2) if decisive_total > 0 else 0.0 # Per-ticker 집계 by_ticker: dict[str, dict[str, Any]] = defaultdict(lambda: {"ticker": "", "name": "", "total": 0, "matched": 0}) for r in t5_op: t = str(r.get("ticker") or "") by_ticker[t]["ticker"] = t by_ticker[t]["name"] = str(r.get("name") or "") by_ticker[t]["total"] += 1 if r.get("t5_outcome") == "MATCHED": by_ticker[t]["matched"] += 1 per_ticker = [] for t, d in sorted(by_ticker.items()): n = d["total"] m = d["matched"] rate = round((m / n) * 100.0, 2) if n > 0 else None quality = "MATCHED" if (rate is not None and rate >= 50.0) else ("MISMATCH" if rate is not None else "INSUFFICIENT") per_ticker.append({ "ticker": t, "name": d["name"], "t5_total": n, "t5_matched": m, "t5_match_rate": rate, "quality_label": quality, }) gate = "PASS" if total >= _MIN_SAMPLES else "INSUFFICIENT" result = { "formula_id": "TRADE_QUALITY_FROM_T5_V1", "gate": gate, "summary_score": summary_rate, # 능동/수동 분리 가중 방식 (v2) "summary_score_legacy": summary_rate_legacy, # 단순 비율 (참고용) "active_rate": active_rate, "passive_rate": passive_rate, "active_decisive_n": a_d, "passive_decisive_n": p_d, "scored_count": total, "matched_count": matched_total, "trade_quality_basis": "t5_operational_active_passive_weighted_v2", "min_samples_required": _MIN_SAMPLES, "per_ticker": per_ticker, } out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print( f"TRADE_QUALITY_FROM_T5_V1 gate={gate} scored_count={total} " f"matched={matched_total} summary_score={summary_rate}" ) return 0 if __name__ == "__main__": raise SystemExit(main())