from __future__ import annotations import argparse import json from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] TEMP = ROOT / "Temp" DEFAULT_JSON = ROOT / "GatherTradingData.json" DEFAULT_OUT = TEMP / "value_preservation_scorer_v2.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 _count_masked_metrics(scr: dict) -> int: """RAW_VS_ADJUSTED_DISCLOSURE_V1: raw 병기 없는 adjusted 단독 표시 감지.""" count = 0 raw = scr.get("raw_value_damage_pct_avg") adj = scr.get("adjusted_value_damage_pct_avg") if adj is not None and raw is None: count += 1 return count def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--json", default=str(DEFAULT_JSON)) ap.add_argument("--out", default=str(DEFAULT_OUT)) args = ap.parse_args() payload = _load(Path(args.json) if Path(args.json).is_absolute() else ROOT / args.json) rows = payload.get("value_preservation_rows") if isinstance(payload.get("value_preservation_rows"), list) else [] recommended_actions = [str(r.get("recommended_action") or "UNKNOWN") for r in rows if isinstance(r, dict)] distinct_actions = sorted(set(recommended_actions)) reason = None if len(rows) >= 2 and len(distinct_actions) < 2: reason = "HOMOGENEOUS_RISK_JUSTIFIED" # RAW_VS_ADJUSTED_DISCLOSURE_V1: smart_cash_recovery에서 raw 값 읽기 scr_v8 = _load(TEMP / "smart_cash_recovery_v8.json") or _load(TEMP / "smart_cash_recovery_v7.json") raw_damage = scr_v8.get("raw_value_damage_pct_avg") # raw 우선 (게이트 입력) adj_damage = scr_v8.get("adjusted_value_damage_pct_avg") masked_count = _count_masked_metrics(scr_v8) # 가치훼손 캡 게이트는 반드시 raw 값을 사용 value_damage_gate_input = raw_damage # adjusted 아님 — RAW_VS_ADJUSTED_DISCLOSURE_V1 value_damage_cap_pass = ( raw_damage is not None and float(raw_damage) <= 10.0 ) # [VD1] n<30 → WATCH_PENDING_SAMPLE (공허 PASS 차단) MIN_SAMPLES = 30 if len(rows) < MIN_SAMPLES: action_gate = "WATCH_PENDING_SAMPLE" elif len(rows) < 2 or len(distinct_actions) >= 2 or reason: action_gate = "PASS" else: action_gate = "FAIL" result = { "formula_id": "VALUE_PRESERVATION_SCORER_V2", "row_count": len(rows), "distinct_actions": len(distinct_actions), "recommended_actions": distinct_actions, "reason": reason, "gate": action_gate, # RAW_VS_ADJUSTED_DISCLOSURE_V1 필드 "raw_value_damage_pct_avg": raw_damage, "adjusted_value_damage_pct_avg": adj_damage, "value_damage_gate_input": value_damage_gate_input, "value_damage_gate_input_source": "raw_value_damage_pct_avg", "value_damage_cap_pass": value_damage_cap_pass, "masked_metric_without_raw_count": masked_count, "masking_disclosure_note": ( f"raw {raw_damage}% / adj {adj_damage}%" if raw_damage is not None and adj_damage is not None else "[RAW_MISSING: raw_value_damage_pct_avg 없음]" if raw_damage is None else "OK" ), } out_path = Path(args.out) if not out_path.is_absolute(): out_path = ROOT / out_path 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(json.dumps(result, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())