from __future__ import annotations import argparse import json import re from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] DEFAULT_JSON = ROOT / "GatherTradingData.json" DEFAULT_REPORT = ROOT / "Temp" / "operational_report.md" def load_json(path: Path) -> dict[str, Any]: payload = json.loads(path.read_text(encoding="utf-8")) if not isinstance(payload, dict): raise ValueError("json payload must be object") return payload def extract_section(text: str, heading: str) -> str: marker = f"## {heading}" idx = text.find(marker) if idx < 0: return "" tail = text[idx:] m = re.search(r"\n##\s+", tail[1:]) return tail if not m else tail[: m.start() + 1] def parse_md_rows(section: str) -> list[list[str]]: rows: list[list[str]] = [] for line in section.splitlines(): if not line.strip().startswith("|"): continue rows.append([c.strip() for c in line.strip().strip("|").split("|")]) if len(rows) <= 2: return [] return rows[2:] def text(v: Any) -> str: return str(v or "").strip() def main() -> int: parser = argparse.ArgumentParser(description="Validate satellite buy proposal sheet consistency.") parser.add_argument("--json", default=str(DEFAULT_JSON)) parser.add_argument("--report", default=str(DEFAULT_REPORT)) args = parser.parse_args() json_path = Path(args.json) report_path = Path(args.report) if not json_path.is_absolute(): json_path = ROOT / json_path if not report_path.is_absolute(): report_path = ROOT / report_path payload = load_json(json_path) report = report_path.read_text(encoding="utf-8") section = extract_section(report, "위성 신규 매수 제안 원장") if not section: print("SATELLITE_PROPOSAL_SHEET_FAIL: section missing") return 1 required_headers = [ "종목", "추천상태", "기준지정가(원)", "기준손절가(원)", "기준익절가1(원)", "기준수량(주)", "진입점수", "익일위험점수", "매도충돌점수", "추천사유(정량근거)", ] for h in required_headers: if h not in section: print(f"SATELLITE_PROPOSAL_SHEET_FAIL: missing header {h}") return 1 data = payload.get("data") if isinstance(payload.get("data"), dict) else {} core = [r for r in (data.get("core_satellite") or []) if isinstance(r, dict)] core_with_state = [r for r in core if text(r.get("Execution_Recommendation_State"))] md_rows = parse_md_rows(section) if not md_rows: print("SATELLITE_PROPOSAL_SHEET_FAIL: no data rows") return 1 if len(md_rows) < min(1, len(core_with_state)): print(f"SATELLITE_PROPOSAL_SHEET_FAIL: insufficient rows md={len(md_rows)} core={len(core_with_state)}") return 1 # 최소 검증: 보고서에 core_satellite 종목코드가 최소 1개 이상 반영되어야 함 tickers = {text(r.get("Ticker")) for r in core_with_state if text(r.get("Ticker"))} section_tickers = {row[0] for row in md_rows if row and row[0] and row[0] != "-"} if not (tickers & section_tickers): print("SATELLITE_PROPOSAL_SHEET_FAIL: no overlapping tickers with core_satellite") return 1 print(f"SATELLITE_PROPOSAL_SHEET_OK: rows_md={len(md_rows)} overlap={len(tickers & section_tickers)}") return 0 if __name__ == "__main__": raise SystemExit(main())