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>
This commit is contained in:
2026-06-13 13:20:14 +09:00
commit ee3e799de1
1474 changed files with 176087 additions and 0 deletions
@@ -0,0 +1,160 @@
"""validate_predictive_alpha_dialectic_v2.py — P1-014: Dialectical Predictive Alpha V2 Calibration
Checks:
1. bearish_buy_violation_count: BEARISH 판정 종목에 BUY 제안 0건 검증
2. synthesis_decile_monotonicity: T+20 표본 부족 시 INSUFFICIENT_DATA
3. alpha_calibration_gate: violations=0 → BEARISH_SAFE / T+20>=30 + monoton → CALIBRATED
T+20 표본이 30건 미만이면 monotonicity 검증 불가 → gate는 BEARISH_SAFE_PENDING_CALIBRATION.
"""
from __future__ import annotations
import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
from v7_hardening_common import ROOT, TEMP, load_json, save_json
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1)
DEFAULT_ALPHA = TEMP / "predictive_alpha_engine_v2.json"
DEFAULT_HIST = ROOT / "Temp" / "proposal_evaluation_history.json"
DEFAULT_OUT = TEMP / "predictive_alpha_calibration_v2.json"
DECILE_MIN_SAMPLES = 30 # decile monotonicity 검증에 필요한 최소 T+20 표본 수
DECILE_COUNT = 5 # synthesis_score 5분위
_BEARISH_VERDICTS = {"BEARISH", "STRONG_BEARISH", "AVOID"}
_BUY_ACTIONS = {"BUY", "STRONG_BUY", "LIMIT_BUY"}
def _check_bearish_violations(alpha_rows: list[dict]) -> list[dict]:
"""현재 alpha 행에서 BEARISH 판정이지만 allow_execution=True인 경우."""
violations = []
for row in alpha_rows:
ticker = row.get("ticker", "")
if ticker == "DATA_MISSING":
continue
verdict = str(row.get("synthesis_verdict") or "").upper()
dc = float(row.get("direction_confidence") or 0)
if verdict in _BEARISH_VERDICTS and row.get("allow_execution"):
violations.append({
"ticker": ticker,
"synthesis_verdict": verdict,
"direction_confidence": dc,
"violation_type": "BEARISH_EXECUTION_ALLOWED",
})
return violations
def _check_history_violations(records: list[dict]) -> list[dict]:
"""과거 이력에서 BEARISH 판정+BUY 제안 케이스."""
violations = []
for r in records:
if r.get("data_origin") == "REPLAY_FROM_KRX_EOD":
continue # REPLAY 제외
action = str(r.get("action") or "").upper()
pa_json = r.get("predictive_alpha_json") or {}
if isinstance(pa_json, str):
try:
pa_json = json.loads(pa_json)
except Exception:
continue
verdict = str(pa_json.get("synthesis_verdict") or "").upper() if pa_json else ""
if action in _BUY_ACTIONS and verdict in _BEARISH_VERDICTS:
violations.append({
"proposal_id": r.get("proposal_id"),
"ticker": r.get("ticker"),
"action": action,
"synthesis_verdict": verdict,
"violation_type": "HISTORY_BEARISH_BUY",
})
return violations
def _check_decile_monotonicity(records: list[dict]) -> dict:
"""T+20 표본이 있는 경우 synthesis_score decile별 수익률 단조 증가 검증."""
t20_with_score = [
r for r in records
if r.get("t20_evaluation_status") == "EVALUATED_T20"
and r.get("t20_return_pct") is not None
and r.get("data_origin") != "REPLAY_FROM_KRX_EOD"
]
if len(t20_with_score) < DECILE_MIN_SAMPLES:
return {
"status": "INSUFFICIENT_DATA",
"note": f"T+20 LIVE 표본 {len(t20_with_score)}/{DECILE_MIN_SAMPLES} — 단조성 검증 불가",
"pass": False,
}
# (실측 데이터가 있을 때의 로직: 향후 자동 실행)
return {
"status": "INSUFFICIENT_DATA",
"note": f"T+20 LIVE 표본 {len(t20_with_score)}/{DECILE_MIN_SAMPLES} — 검증 준비 중",
"pass": False,
}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--alpha", default=str(DEFAULT_ALPHA))
ap.add_argument("--hist", default=str(DEFAULT_HIST))
ap.add_argument("--out", default=str(DEFAULT_OUT))
args = ap.parse_args()
alpha = load_json(Path(args.alpha))
hist_raw = load_json(Path(args.hist))
records: list[dict] = hist_raw.get("records", []) if isinstance(hist_raw, dict) else (hist_raw if isinstance(hist_raw, list) else [])
alpha_rows: list[dict] = alpha.get("rows", []) if isinstance(alpha, dict) else []
# ── 검증 1: bearish buy violations ──────────────────────────────────────
live_violations = _check_bearish_violations(alpha_rows)
history_violations = _check_history_violations(records)
all_violations = live_violations + history_violations
bearish_buy_violation_count = len(all_violations)
# ── 검증 2: synthesis decile monotonicity ──────────────────────────────
monotonicity = _check_decile_monotonicity(records)
synthesis_decile_monotonicity = "PASS" if monotonicity.get("pass") else monotonicity["status"]
# ── gate 판정 ──────────────────────────────────────────────────────────
if bearish_buy_violation_count > 0:
gate = "BLOCKED_BEARISH_VIOLATION"
alpha_calibration_gate = "BLOCKED_BEARISH_VIOLATION"
elif synthesis_decile_monotonicity == "PASS":
gate = "CALIBRATED"
alpha_calibration_gate = "CALIBRATED"
else:
gate = "BEARISH_SAFE_PENDING_CALIBRATION"
alpha_calibration_gate = "BEARISH_SAFE_PENDING_CALIBRATION"
result = {
"formula_id": "PREDICTIVE_ALPHA_CALIBRATION_V2",
"generated_at": datetime.now(timezone.utc).isoformat(),
"gate": gate,
"alpha_calibration_gate": alpha_calibration_gate,
"bearish_buy_violation_count": bearish_buy_violation_count,
"synthesis_decile_monotonicity": synthesis_decile_monotonicity,
"violations": all_violations,
"monotonicity_detail": monotonicity,
"targets": {
"alpha_calibration_gate": "CALIBRATED",
"synthesis_decile_monotonicity": "PASS",
"bearish_buy_violation_count": "==0",
},
"gate_path": {
"CALIBRATED": "bearish_violations==0 AND decile_monotonicity==PASS",
"BEARISH_SAFE_PENDING_CALIBRATION": "bearish_violations==0 AND T+20<30 (현재 상태)",
"BLOCKED_BEARISH_VIOLATION": "bearish BUY 감지됨 → 즉시 차단",
},
}
save_json(args.out, result)
print(json.dumps({k: v for k, v in result.items() if k != "violations"}, ensure_ascii=False, indent=2))
return 1 if gate == "BLOCKED_BEARISH_VIOLATION" else 0
if __name__ == "__main__":
raise SystemExit(main())