from __future__ import annotations import argparse import json from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] DEFAULT_JSON = ROOT / "GatherTradingData.json" DEFAULT_OUT = ROOT / "Temp" / "value_preservation_scorer_v1.json" def _load(path: Path) -> dict[str, Any]: try: obj = json.loads(path.read_text(encoding="utf-8")) except Exception: return {} return obj if isinstance(obj, dict) else {} def _rows(v: Any) -> list[dict[str, Any]]: if isinstance(v, list): return [x for x in v if isinstance(x, dict)] if isinstance(v, str): try: return _rows(json.loads(v)) except Exception: return [] return [] def _obj(v: Any) -> dict[str, Any]: if isinstance(v, dict): return v if isinstance(v, str): try: x = json.loads(v) return x if isinstance(x, dict) else {} except Exception: return {} return {} def _f(v: Any) -> float: try: return float(v) except Exception: return 0.0 def _bound(v: float, lo: float = 0.0, hi: float = 100.0) -> float: return max(lo, min(hi, v)) 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() jp = Path(args.json) op = Path(args.out) if not jp.is_absolute(): jp = ROOT / jp if not op.is_absolute(): op = ROOT / op payload = _load(jp) data = payload.get("data") if isinstance(payload.get("data"), dict) else {} h = data.get("_harness_context") if isinstance(data.get("_harness_context"), dict) else {} if isinstance(payload.get("hApex"), dict): h = dict(h) | payload["hApex"] scrs = _obj(h.get("scrs_v2_json")) rows = _rows(scrs.get("selected_combo")) prices = {str(r.get("ticker") or ""): r for r in _rows(h.get("prices_json"))} scored = [] raw_damages: list[float] = [] null_limit_price_count = 0 MIN_SAMPLES = 30 for r in rows: t = str(r.get("ticker") or "") p = prices.get(t, {}) atr = _f(p.get("atr20") or p.get("ATR20")) prev_close = _f(p.get("prev_close") or p.get("prevClose")) current = _f(p.get("current_price") or p.get("current_price_krw")) adv20 = _f(p.get("adv20") or p.get("avg_trade_value_20d") or 0.0) # [VD1] raw_value_damage_pct — adjusted 마스킹 전 원본값 damage_pct = _f(r.get("value_damage_pct")) raw_damages.append(damage_pct) price_stress = 0.0 if atr <= 0 else abs(current - prev_close) / atr * 10.0 value_damage_score = round(_bound(damage_pct * 4.0 + price_stress), 2) rebound_potential = round(_bound(100.0 - value_damage_score + (_f(r.get("rebound_wait_qty")) > 0) * 10.0), 2) if rebound_potential >= 70: action = "WAIT_REBOUND" elif rebound_potential >= 45: action = "SPLIT_REBOUND" else: action = "EXECUTE_NOW" # [VD1] hts_limit_price null 감지 (비상 외 매도에서 null이면 설거지 위험) hts_lp = r.get("hts_limit_price") emergency = bool(r.get("emergency_full_sell") or r.get("emergency")) if hts_lp is None and not emergency: null_limit_price_count += 1 # [VD1] participation_rate = qty / adv20 (5% 초과면 TWAP 권고) qty = _f(r.get("immediate_sell_qty") or r.get("qty") or 0) participation_rate = round(qty / adv20, 4) if adv20 > 0 and current > 0 else None scored.append( { "ticker": t, "name": r.get("name"), "value_damage_pct_raw": round(damage_pct, 2), "value_damage_score": value_damage_score, "rebound_potential": rebound_potential, "recommended_action": action, "hts_limit_price": hts_lp, "hts_limit_price_null": hts_lp is None, "emergency_full_sell": emergency, "participation_rate": participation_rate, "twap_recommended": participation_rate is not None and participation_rate > 0.05, } ) # [VD1] raw_value_damage_pct_avg 기준 게이트 raw_avg = round(sum(raw_damages) / len(raw_damages), 2) if raw_damages else 0.0 n = len(scored) if n == 0: gate = "CAUTION" gate_reason = "NO_ROWS" elif n < MIN_SAMPLES: # [SG1] n<30 → 공허PASS 금지 gate = "WATCH_PENDING_SAMPLE" gate_reason = f"INSUFFICIENT_SAMPLES(n={n}<{MIN_SAMPLES})" elif raw_avg > 10.0: # [VD1] raw 손상 10% 초과 → BLOCK gate = "BLOCK" gate_reason = f"VALUE_DAMAGE_GT_10(raw_avg={raw_avg}%)" else: gate = "PASS" gate_reason = f"OK(raw_avg={raw_avg}%,n={n})" distinct_actions = len({str(r.get("recommended_action") or "") for r in scored}) out = { "formula_id": "VALUE_PRESERVATION_SCORER_V1", "gate": gate, "gate_reason": gate_reason, # [VD1] raw 기준 집계 — adjusted 마스킹 금지 "raw_value_damage_pct_avg": raw_avg, "value_damage_gt_10": raw_avg > 10.0, "hts_limit_price_null_count_non_emergency": null_limit_price_count, "min_samples": MIN_SAMPLES, "rows": scored, "row_count": n, "distinct_actions": distinct_actions, } op.parent.mkdir(parents=True, exist_ok=True) op.write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps(out, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())