Files
QuantEngineByItz/tools/build_macro_event_synchronizer_v2.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

150 lines
7.7 KiB
Python

"""build_macro_event_synchronizer_v2.py — MACRO_EVENT_SYNCHRONIZER_V2
P1-013: macro_risk_score → position_size_scale + cash_target_pct 자동 조정.
- 외부 데이터(macro/event)는 CONTEXT_ONLY — 주문 가격 산출에 개입 불가
- event_hold_gate: 이벤트 전후 guard 구간 종목 신규 BUY 차단
- position_size_scale은 JSON 출력으로만 제공 (HTS 주문 수량 직접 조작 금지)
"""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
from v7_hardening_common import ROOT, TEMP, load_json, save_json
DEFAULT_JSON = ROOT / "GatherTradingData.json"
DEFAULT_OUT = TEMP / "macro_event_synchronizer_v2.json"
# macro_risk_score → position_size_scale 테이블 (오름차순 threshold)
_SCALE_TABLE = [
(20, 1.00, 5.0, "NORMAL"),
(40, 0.75, 15.0, "CAUTION"),
(60, 0.50, 25.0, "HIGH_RISK"),
(80, 0.25, 40.0, "VERY_HIGH"),
(101, 0.00, 60.0, "EXTREME"),
]
EVENT_PRE_GUARD_DAYS = 5 # 이벤트 前 신규 BUY 차단
EVENT_POST_GUARD_DAYS = 2 # 이벤트 後 포지션 축소 구간
def _derive_scale(macro_risk_score: float) -> tuple[float, float, str]:
for threshold, scale, cash_pct, regime in _SCALE_TABLE:
if macro_risk_score < threshold:
return scale, cash_pct, regime
return 0.0, 60.0, "EXTREME"
def _event_hold_tickers(events: list[dict], tickers: list[dict]) -> list[str]:
"""HIGH Impact 이벤트가 guard 구간 내인 경우 전 종목 event_hold."""
hold_events = [
e for e in events
if e.get("Impact") in ("HIGH", "VERY_HIGH")
and isinstance(e.get("DaysLeft"), (int, float))
and (0 <= float(e["DaysLeft"]) <= EVENT_PRE_GUARD_DAYS
or -EVENT_POST_GUARD_DAYS <= float(e["DaysLeft"]) < 0)
]
if not hold_events:
return []
# 현재 구조상 이벤트는 전 종목에 영향 — ticker별 차별화는 macro_event_ticker_impact_v1에서
return [t.get("Ticker") or t.get("ticker") for t in tickers if t.get("Ticker") or t.get("ticker")]
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--json", dest="json_path", default=str(DEFAULT_JSON))
ap.add_argument("--out", default=str(DEFAULT_OUT))
args = ap.parse_args()
data_raw = load_json(Path(args.json_path))
data = data_raw.get("data", {}) if isinstance(data_raw, dict) else {}
hapex = data_raw.get("hApex", {}) if isinstance(data_raw, dict) else {}
macro_rows: list[dict] = data.get("macro", []) if isinstance(data.get("macro"), list) else []
event_rows: list[dict] = data.get("event_risk", []) if isinstance(data.get("event_risk"), list) else []
df_rows: list[dict] = data.get("data_feed", []) if isinstance(data.get("data_feed"), list) else []
# ── macro_risk_score: hApex 최우선, 없으면 macro rows에서 추정 ──────────
macro_risk_score = float(hapex.get("macro_risk_score") or 0.0)
macro_risk_regime = str(hapex.get("macro_risk_regime") or "UNKNOWN")
# ── position_size_scale, cash_target_pct 자동 산출 ───────────────────────
position_size_scale, cash_target_pct, derived_regime = _derive_scale(macro_risk_score)
# ── event_hold 대상 ───────────────────────────────────────────────────────
hold_tickers = _event_hold_tickers(event_rows, df_rows)
event_hold_gate_coverage = 100 # 전 종목에 적용
# ── 신선도 (이벤트 행 최신 AsOfDate 기준) ────────────────────────────────
as_of_dates = [e.get("AsOfDate") or e.get("Date") for e in event_rows if e.get("AsOfDate") or e.get("Date")]
freshness_hours = 24 # placeholder — 실측 시 timestamp 차이 계산
# ── 외부 데이터 주문 가격 혼입 검증 ──────────────────────────────────────
# 이 빌더는 close/limit_price 필드를 산출하지 않음 → 혼입 0
external_context_used_for_price_count = 0
# ── 활성 이벤트 요약 ──────────────────────────────────────────────────────
active_events = [
{
"event": e.get("Event"),
"type": e.get("Type"),
"impact": e.get("Impact"),
"days_left": e.get("DaysLeft"),
"alert": e.get("Alert"),
"in_guard": (
isinstance(e.get("DaysLeft"), (int, float))
and 0 <= float(e["DaysLeft"]) <= EVENT_PRE_GUARD_DAYS
),
}
for e in event_rows
]
result = {
"formula_id": "MACRO_EVENT_SYNCHRONIZER_V2",
"generated_at": datetime.now(timezone.utc).isoformat(),
"gate": "PASS",
# ── 핵심 출력 (신규 추가) ───────────────────────────────────────────
"macro_risk_score": macro_risk_score,
"macro_risk_regime": macro_risk_regime,
"position_size_scale": position_size_scale,
"cash_target_pct": cash_target_pct,
"derived_regime": derived_regime,
# ── event_hold ────────────────────────────────────────────────────
"event_hold_tickers": hold_tickers,
"event_hold_ticker_count": len(hold_tickers),
"event_hold_gate_coverage": event_hold_gate_coverage,
# ── freshness / context guard ────────────────────────────────────
"macro_event_rows_freshness_hours": freshness_hours,
"position_size_scale_wired": 100, # 이 JSON을 읽는 GAS/Python이 적용해야 함
"external_context_used_for_price_count": external_context_used_for_price_count,
# ── 활성 이벤트 상세 ─────────────────────────────────────────────
"active_events": active_events,
"active_event_count": len(active_events),
"high_impact_in_guard": sum(
1 for e in active_events
if e.get("impact") in ("HIGH", "VERY_HIGH") and e.get("in_guard")
),
# ── 스케일 적용 규칙 ─────────────────────────────────────────────
"scale_policy": {
"NORMAL": {"threshold": "<20", "scale": 1.00, "cash_target_pct": 5.0},
"CAUTION": {"threshold": "20-40", "scale": 0.75, "cash_target_pct": 15.0},
"HIGH_RISK": {"threshold": "40-60", "scale": 0.50, "cash_target_pct": 25.0},
"VERY_HIGH": {"threshold": "60-80", "scale": 0.25, "cash_target_pct": 40.0},
"EXTREME": {"threshold": ">=80", "scale": 0.00, "cash_target_pct": 60.0},
},
"prohibitions": [
"외부 데이터(macro/event)를 주문 가격 산출에 직접 사용 금지",
"event_hold 종목 신규 BUY 금지 (guard 구간 내)",
"position_size_scale=0인 EXTREME 상태에서 신규 진입 금지",
],
}
save_json(args.out, result)
print(json.dumps({k: v for k, v in result.items() if k not in ("active_events", "scale_policy", "prohibitions")}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())