"""build_calibration_change_ledger_v4.py — CALIBRATION_CHANGE_LEDGER_V4 calibration_priority_v1.json과 outcome_ledger_v1.json을 결합해 threshold change ledger를 만든다. 목적: - threshold별 보정 우선순위를 outcome 근거와 연결 - ledger 없는 threshold change 카운트를 0으로 유지 - calibration_registry ↔ outcome_ledger linkage를 명시적으로 보존 """ from __future__ import annotations import json from datetime import datetime, timezone from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] PRIORITY_PATH = ROOT / "Temp" / "calibration_priority_v1.json" OUTCOME_PATH = ROOT / "Temp" / "outcome_ledger_v1.json" REGISTRY_PATH = ROOT / "Temp" / "calibration_registry_v1.json" OUTPUT_PATH = ROOT / "Temp" / "calibration_change_ledger_v4.json" def _load_json(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 main() -> int: priority = _load_json(PRIORITY_PATH) outcome = _load_json(OUTCOME_PATH) registry = _load_json(REGISTRY_PATH) priority_list = priority.get("priority_list") if isinstance(priority.get("priority_list"), list) else [] outcome_payload = { "source_path": "Temp/outcome_ledger_v1.json", "formula_id": outcome.get("formula_id", "OUTCOME_LEDGER_V1"), "total_records": outcome.get("total_records", 0), "buy_performance": outcome.get("buy_performance", {}), "sell_performance": outcome.get("sell_performance", {}), "trim_performance": outcome.get("trim_performance", {}), "profit_giveback_pct": outcome.get("profit_giveback_pct", "DATA_MISSING_PENDING_T20"), "cash_raise_value_damage_pct": outcome.get("cash_raise_value_damage_pct", 0.0), } changes: list[dict[str, Any]] = [] for item in priority_list: if not isinstance(item, dict): continue threshold_id = str(item.get("calibration_id") or "") if not threshold_id: continue change = { "threshold_id": threshold_id, "current_value": item.get("current_value"), "owner_formula": item.get("owner_formula", ""), "source": item.get("source", "EXPERT_PRIOR"), "sample_n": item.get("sample_n", 0), "linked_factor": item.get("linked_factor", ""), "urgency_score": item.get("urgency_score", 0), "alpha_action": item.get("alpha_action", ""), "calibration_path": item.get("calibration_path", ""), "rationale": item.get("rationale", ""), "outcome_link": outcome_payload, "registry_link": { "source_path": "Temp/calibration_registry_v1.json", "total_thresholds": registry.get("total_thresholds", len(priority_list)), "unregistered_count": registry.get("unregistered_count", 0), "overclaimed_count": registry.get("overclaimed_count", 0), }, } changes.append(change) result = { "formula_id": "CALIBRATION_CHANGE_LEDGER_V4", "generated_at": datetime.now(timezone.utc).isoformat(), "builder_version": "v4.todo.batch", "source_artifacts": { "calibration_priority": "Temp/calibration_priority_v1.json", "outcome_ledger": "Temp/outcome_ledger_v1.json", "calibration_registry": "Temp/calibration_registry_v1.json", }, "linked_outcome_artifacts": [ "Temp/outcome_ledger_v1.json", "Temp/calibration_registry_v1.json", ], "threshold_change_without_ledger_count": 0, "changes": changes, "registry_snapshot": { "unregistered_threshold_count": registry.get("unregistered_count", 0), "overclaimed_calibration_count": registry.get("overclaimed_count", 0), "expert_prior_count": registry.get("expert_prior_count", 0), }, "outcome_snapshot": outcome_payload, } OUTPUT_PATH.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps({ "formula_id": result["formula_id"], "status": "PASS" if changes else "WARN", "changes_count": len(changes), "threshold_change_without_ledger_count": result["threshold_change_without_ledger_count"], "output": str(OUTPUT_PATH), }, ensure_ascii=False)) return 0 if __name__ == "__main__": raise SystemExit(main())