from __future__ import annotations import argparse import json from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] DEFAULT_HISTORY = ROOT / "Temp" / "proposal_evaluation_history.json" DEFAULT_OUTCOME = ROOT / "Temp" / "outcome_quality_score_v1.json" DEFAULT_EXEC = ROOT / "Temp" / "execution_quality_harness_v1.json" DEFAULT_PERF = ROOT / "Temp" / "perf_recovery_harness_v1.json" DEFAULT_OUT = ROOT / "Temp" / "operational_outcome_lock_v1.json" 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 _f(v: Any, default: float = 0.0) -> float: try: return float(v) except Exception: return default def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--history", default=str(DEFAULT_HISTORY)) ap.add_argument("--outcome", default=str(DEFAULT_OUTCOME)) ap.add_argument("--execution", default=str(DEFAULT_EXEC)) ap.add_argument("--perf", default=str(DEFAULT_PERF)) ap.add_argument("--out", default=str(DEFAULT_OUT)) args = ap.parse_args() hp = Path(args.history) op = Path(args.outcome) ep = Path(args.execution) pp = Path(args.perf) out = Path(args.out) if not hp.is_absolute(): hp = ROOT / hp if not op.is_absolute(): op = ROOT / op if not ep.is_absolute(): ep = ROOT / ep if not pp.is_absolute(): pp = ROOT / pp if not out.is_absolute(): out = ROOT / out history = _load(hp) outcome = _load(op) execution = _load(ep) perf = _load(pp) records = history.get("records") if isinstance(history.get("records"), list) else [] t5_oper = [ r for r in records if isinstance(r, dict) and r.get("t5_evaluation_status") == "EVALUATED_T5" and str(r.get("validation_status") or "").upper() != "REPLAY_BACKFILL" ] t20_oper = [ r for r in records if isinstance(r, dict) and r.get("t20_evaluation_status") == "EVALUATED_T20" and str(r.get("validation_status") or "").upper() != "REPLAY_BACKFILL" ] t20_match = len([r for r in t20_oper if str(r.get("t20_outcome") or "") == "MATCHED"]) t20_rate = round((t20_match / len(t20_oper)) * 100.0, 2) if t20_oper else 0.0 oq_score = _f(outcome.get("score")) eq_metrics = (execution.get("metrics") or {}).get("operational_t20") if isinstance((execution.get("metrics") or {}).get("operational_t20"), dict) else {} expectancy = _f(eq_metrics.get("expectancy_pct")) win_rate = _f(eq_metrics.get("win_rate_pct")) late_precision = _f((perf.get("metrics") or {}).get("late_chase_block_precision"), 0.0) value_damage = _f((perf.get("metrics") or {}).get("rebound_sell_value_damage"), 0.0) # [Work 29] threshold 현실화 # execution_quality_harness_v1.json 미존재 시 EXPECTANCY/WIN_RATE는 "데이터 없음"으로 처리 # (실거래 기록 없음 = 알 수 없음, 아닌 0%) _exec_data_available = ep.exists() and bool(eq_metrics) reasons: list[str] = [] if len(t20_oper) < 30: reasons.append("OPERATIONAL_T20_SAMPLE_LT_30") if t20_rate < 60.0: reasons.append("OPERATIONAL_T20_PASS_LT_60") if oq_score < 60.0: reasons.append("OUTCOME_QUALITY_LT_60") # EXPECTANCY/WIN_RATE: 실거래 T+20 표본이 충분할 때만 체크 (samples>0) _exec_samples = int(_f(eq_metrics.get("samples"), 0.0)) if _exec_data_available and _exec_samples >= 10: if expectancy <= 0.1: reasons.append("EXPECTANCY_LE_0_1") if win_rate < 45.0: reasons.append("WIN_RATE_LT_45") # VALUE_DAMAGE: 10% 초과는 실행 차단 if value_damage > 10.0: reasons.append("VALUE_DAMAGE_GT_10") if late_precision < 80.0 and late_precision > 0.0: reasons.append("LATE_CHASE_PRECISION_LOW") unlock_state = "PERFORMANCE_READY" if not reasons else "WATCH_PENDING_SAMPLE" if any(x in reasons for x in ("OUTCOME_QUALITY_LT_60", "EXPECTANCY_LE_0_1", "WIN_RATE_LT_45", "VALUE_DAMAGE_GT_10")): unlock_state = "NOT_PERFORMANCE_READY" result = { "formula_id": "OPERATIONAL_OUTCOME_LOCK_V1", "unlock_state": unlock_state, "reasons": reasons, "metrics": { "operational_t5_count": len(t5_oper), "operational_t20_count": len(t20_oper), "operational_t20_pass_rate": t20_rate, "outcome_quality_score": oq_score, "execution_expectancy_pct": expectancy, "execution_win_rate_pct": win_rate, "late_chase_block_precision": late_precision, "sell_after_rebound_damage_pct": value_damage, }, "targets": { "operational_t20_count_min": 30, "operational_t20_pass_rate_min": 60.0, "outcome_quality_score_min": 60.0, "execution_expectancy_pct_min": 0.1, "execution_win_rate_pct_min": 45.0, "sell_after_rebound_damage_pct_max": 10.0, }, } 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())