Files
QuantEngineByItz/tools/validate_satellite_buy_proposal_sheet.py
T
kjh2064 ee3e799de1 feat: 리밸런싱 엔진 V1 + GAS 버그 수정 (2026-06-13)
주요 변경:
- tools/build_rebalance_engine_v1.py: REBALANCE_ENGINE_V1 신규
  * account_snapshot 직접 합산(_build_snap_position_map) → 소수주 분리 행 병합
  * 레짐 소스 macro.REGIME_PRELIM 최우선 (GAS 와 동일)
- src/gas_adapter_parts/gdf_06_rebalance.gs: runRebalanceSheet_() 신규
  * Logger.log / getSpreadsheet_() 로 run_all 연동 수정
- src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs
  * _mergePositionRecord_(): 소수주 중복 행 합산 신규
  * parseInt → parseFloat (qty, availQty)
- src/gas_adapter_parts/gdf_01_price_metrics.gs
  * 미보유 종목 SELL_READY → WATCH_EXIT_SIGNAL
- spec/41_release_dag.yaml: build_rebalance_sheet 노드 추가 (step_count 63)
- spec/51_formula_lifecycle_registry.yaml: REBALANCE_ENGINE_V1 등록

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 13:20:14 +09:00

109 lines
3.5 KiB
Python

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())