from __future__ import annotations import argparse import json from datetime import date from pathlib import Path from typing import Any from lib_trading_calendar import next_trading_day ROOT = Path(__file__).resolve().parents[1] DEFAULT_HISTORY = ROOT / "Temp" / "proposal_evaluation_history.json" DEFAULT_OUT = ROOT / "Temp" / "operational_eval_queue_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 _add_trading_days(start: date, n: int) -> date: cur = start for _ in range(max(0, n)): cur = next_trading_day(cur) return cur def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--history", default=str(DEFAULT_HISTORY)) ap.add_argument("--out", default=str(DEFAULT_OUT)) ap.add_argument("--t20-days", type=int, default=28) args = ap.parse_args() hp = Path(args.history) op = Path(args.out) if not hp.is_absolute(): hp = ROOT / hp if not op.is_absolute(): op = ROOT / op history = _load(hp) rows = history.get("records") if isinstance(history.get("records"), list) else [] today = date.today() queue: list[dict[str, Any]] = [] due_cnt = 0 done_cnt = 0 missing_due_dates = 0 for r in rows: if not isinstance(r, dict): continue pd = str(r.get("proposal_date") or "") if not pd: missing_due_dates += 1 continue try: d0 = date.fromisoformat(pd) except Exception: missing_due_dates += 1 continue age = (today - d0).days due_t1 = _add_trading_days(d0, 1).isoformat() due_t5 = _add_trading_days(d0, 5).isoformat() due_t20 = _add_trading_days(d0, 20).isoformat() t20_status = str(r.get("t20_evaluation_status") or "") if t20_status == "EVALUATED_T20": done_cnt += 1 continue if age >= args.t20_days: due_cnt += 1 queue.append( { "proposal_date": pd, "due_date_t1": due_t1, "due_date_t5": due_t5, "due_date_t20": due_t20, "ticker": r.get("ticker"), "name": r.get("name"), "strategy_action": r.get("strategy_action"), "validation_status": r.get("validation_status"), "age_days": age, "next_action": "CAPTURE_T20_OUTCOME_REQUIRED", } ) queue = sorted(queue, key=lambda x: int(x.get("age_days") or 0), reverse=True) out = { "formula_id": "OPERATIONAL_EVAL_QUEUE_V1", "as_of": today.isoformat(), "t20_days_threshold": int(args.t20_days), "metrics": { "records_total": len(rows), "t20_evaluated_count": done_cnt, "t20_due_capture_count": due_cnt, "missing_due_date_count": missing_due_dates, }, "all_proposals_have_due_dates": missing_due_dates == 0 and len(rows) > 0, "queue": queue[:500], "todo_protocol": [ "1) queue 상위 종목부터 T+20 실제 결과값 입력", "2) validation_status=REPLAY_BACKFILL는 운영성과로 집계 금지", "3) 입력 후 update-evaluation-history -> build-execution-quality-harness -> build-operational-outcome-lock-v1 재실행", ], } 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())