from __future__ import annotations import argparse import json from datetime import date, timedelta from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] DEFAULT_HISTORY = ROOT / "Temp" / "proposal_evaluation_history.json" DEFAULT_OUT = ROOT / "Temp" / "operational_t20_outcome_ledger_v1.json" # T+20 성숙 판정: 20 영업일 ≈ 28 캘린더일 (보수적 기준) T20_CALENDAR_DAYS = 28 def _load(path: Path) -> dict[str, Any]: if not path.exists(): return {} try: obj = json.loads(path.read_text(encoding="utf-8")) except Exception: return {} return obj if isinstance(obj, dict) else {} def _is_matured(r: dict) -> bool: """proposal_date + 28 캘린더일 <= today 이면 T+20 성숙 판정.""" pd = r.get("proposal_date") or r.get("entry_date") or "" if not pd: return False try: entry = date.fromisoformat(str(pd)[:10]) return (date.today() - entry).days >= T20_CALENDAR_DAYS except Exception: return False def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--history", default=str(DEFAULT_HISTORY)) ap.add_argument("--out", default=str(DEFAULT_OUT)) args = ap.parse_args() hist_path = Path(args.history) if Path(args.history).is_absolute() else ROOT / args.history hist = _load(hist_path) records = hist.get("records") if isinstance(hist.get("records"), list) else [] # [T3/SG1] 운영(비-REPLAY) 레코드만 추출, T+20 성숙 확인 operational = [ r for r in records if isinstance(r, dict) and str(r.get("validation_status") or "").upper() != "REPLAY_BACKFILL" ] # T+20 평가 완료 OR 20 캘린더일 이상 경과한 행 → matured t20 = [ r for r in operational if str(r.get("t20_evaluation_status") or "").startswith("EVALUATED_") or _is_matured(r) ] # INCONCLUSIVE는 통계에서 제외 (match_rate 분자/분모 모두) decisive = [r for r in t20 if r.get("t20_outcome") in ("MATCHED", "MISMATCHED")] matched = sum(1 for r in decisive if r.get("t20_outcome") == "MATCHED") mismatched = sum(1 for r in decisive if r.get("t20_outcome") == "MISMATCHED") rate = round((matched / len(decisive)) * 100.0, 2) if decisive else 0.0 result = { "formula_id": "OPERATIONAL_T20_OUTCOME_LEDGER_V1", "evaluated_count": len(t20), "decisive_count": len(decisive), "matched_count": matched, "mismatched_count": mismatched, "pass_rate_pct": rate, # [SG1] n<30 → WATCH_PENDING_SAMPLE (공허PASS 금지) "gate": ( "PASS" if len(decisive) >= 30 and rate >= 60.0 else "WATCH_PENDING_SAMPLE" ), "operational_total": len(operational), "maturity_threshold_days": T20_CALENDAR_DAYS, "rows": [ { "proposal_id": r.get("proposal_id"), "ticker": r.get("ticker"), "name": r.get("name"), "proposal_date": r.get("proposal_date"), "t20_evaluation_status": r.get("t20_evaluation_status"), "t20_outcome": r.get("t20_outcome"), "t20_return_pct": r.get("t20_return_pct"), "validation_status": r.get("validation_status"), "matured": _is_matured(r), } for r in t20[:500] ], } out = Path(args.out) if not out.is_absolute(): out = ROOT / out out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps(result, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())