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

210 lines
10 KiB
Python

#!/usr/bin/env python3
"""
validate_behavioral_coverage_v1.py
───────────────────────────────────────────────────────────────────────────────
행위기반 커버리지 하네스 — 3-way 동등성 게이트 (BCH-V1 B05 단계)
Python 미러(B03)와 GAS 패리티(B04) 결과를 통합해 최종 판정을 내린다.
판정 기준:
BEHAVIORAL_COVERAGE_V1_OK ← 아래 조건 모두 충족
- behavioral_coverage_pct >= 100.0 (decision-critical 공식 전부 통과)
- implementation_divergence_count == 0 (Python ≠ GAS 불일치 0건)
- python_fail_count == 0 (Python 미러 실패 0건)
- gas_fail_count == 0 (GAS 패리티 실패 0건)
IMPLEMENTATION_DIVERGENCE ← Python ≠ GAS 불일치 발견 시
→ B06에서 spec/13_formula_registry.yaml 기준으로 근본 정정 필요.
BEHAVIORAL_COVERAGE_GAP ← golden case 없는 decision-critical 공식 발견 시.
출력: Temp/formula_behavioral_coverage_summary_v1.json
사용법:
python tools/validate_behavioral_coverage_v1.py
python tools/validate_behavioral_coverage_v1.py --strict
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
PY_RESULT = ROOT / "Temp" / "formula_behavioral_coverage_v1.json"
GAS_RESULT = ROOT / "Temp" / "formula_gas_parity_v1.json"
CONTRACT = ROOT / "spec" / "26_behavioral_coverage_contract.yaml"
SUMMARY_OUT = ROOT / "Temp" / "formula_behavioral_coverage_summary_v1.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)
def load_json(path: Path) -> dict:
if not path.exists():
return {}
return json.loads(path.read_text(encoding="utf-8"))
def main() -> int:
strict = "--strict" in sys.argv
py_data = load_json(PY_RESULT)
gas_data = load_json(GAS_RESULT)
missing = []
if not PY_RESULT.exists():
missing.append(str(PY_RESULT))
if not GAS_RESULT.exists():
missing.append(str(GAS_RESULT))
if missing:
print("BEHAVIORAL_COVERAGE_V1_FAIL")
for m in missing:
print(f" - MISSING: {m}")
print(" 먼저 B03(run_formula_golden_cases_v2.py)과 B04(run_gas_golden_parity.js)를 실행하세요.")
return 1
sep = "=" * 70
print(sep)
print(" 행위기반 커버리지 — 3-way 동등성 게이트 (BCH-V1 B05)")
print(sep)
# ── Python 미러 결과 분석 ────────────────────────────────────────────
py_coverage = py_data.get("behavioral_coverage_pct", 0.0)
py_divergences = py_data.get("python_divergences", [])
py_missing_mirrors = py_data.get("missing_python_mirrors", [])
py_per_formula = py_data.get("per_formula", [])
py_fail_formulas = [f for f in py_per_formula if f.get("status") == "FAIL"]
py_fail_count = sum(f.get("case_count", 0) - f.get("pass_count", 0) for f in py_per_formula)
# ── GAS 패리티 결과 분석 ─────────────────────────────────────────────
gas_pass = gas_data.get("gas_pass", 0)
gas_fail = gas_data.get("gas_fail", 0)
gas_divergences = gas_data.get("divergences", [])
gas_load_errors = gas_data.get("load_errors", [])
gas_coverage = gas_data.get("gas_coverage_pct", 0.0)
# ── IMPLEMENTATION_DIVERGENCE 계산 ───────────────────────────────────
# Python ≠ GAS 불일치 = py_divergences (Python ≠ spec_correct)
# + gas_divergences with type=GAS_FAIL
impl_divergences: list[dict] = []
# Python ≠ spec_correct (= GAS가 정답인 경우)
for d in py_divergences:
impl_divergences.append({
"type": "PYTHON_DIVERGES_FROM_SPEC",
"formula_id": d.get("formula_id"),
"case_id": d.get("case_id"),
"spec_correct": d.get("spec_correct"),
"python_output": d.get("python_output"),
"note": d.get("note", ""),
"resolution": "B06: Python 미러 함수를 spec/13 expression(floor 방식)으로 정정",
})
# GAS가 spec_correct를 못 내는 경우
for d in gas_divergences:
if d.get("type") == "GAS_FAIL":
impl_divergences.append({
"type": "GAS_DIVERGES_FROM_GOLDEN",
"formula_id": d.get("formula_id"),
"case_id": d.get("case_id"),
"errors": d.get("errors", []),
"actual": d.get("actual"),
"expected": d.get("expected"),
"resolution": "B06: GAS 함수를 spec/13 expression에 맞게 정정",
})
impl_divergence_count = len(impl_divergences)
# ── BEHAVIORAL_COVERAGE_GAP 계산 ─────────────────────────────────────
# Python 미러 없는 공식 (golden case 있지만 Python 실행 불가)
gap_formulas = py_missing_mirrors + gas_load_errors
# ── 종합 판정 ────────────────────────────────────────────────────────
overall_ok = (
py_coverage >= 100.0
and impl_divergence_count == 0
and gas_fail == 0
and len(gas_load_errors) == 0
)
# ── 콘솔 출력 ────────────────────────────────────────────────────────
print(f"\n [Python 미러] behavioral_coverage_pct: {py_coverage}%")
print(f" 미러 없는 공식: {len(py_missing_mirrors)}개")
print(f" [GAS 패리티] coverage: {gas_coverage}% pass={gas_pass} fail={gas_fail}")
print(f" [분기 건수] implementation_divergence_count: {impl_divergence_count}")
if impl_divergences:
print(f"\n [IMPLEMENTATION_DIVERGENCE] {impl_divergence_count}건:")
for d in impl_divergences:
print(f" [{d['type']}] {d['formula_id']}:{d['case_id']}")
if d.get("spec_correct") is not None:
print(f" spec_correct={d['spec_correct']}, python_output={d['python_output']}")
if d.get("errors"):
for e in d["errors"]:
print(f" - {e}")
print(f" → {d['resolution']}")
if py_fail_formulas:
print(f"\n [Python FAIL 공식] {len(py_fail_formulas)}개:")
for f in py_fail_formulas:
print(f" {f['formula_id']}: {f['pass_count']}/{f['case_count']} 통과")
if gas_load_errors:
print(f"\n [GAS 로드 실패] {len(gas_load_errors)}건:")
for e in gas_load_errors:
print(f" {e}")
if py_missing_mirrors:
print(f"\n [Python 미러 미구현] {len(py_missing_mirrors)}개:")
for m in py_missing_mirrors:
print(f" {m}")
print()
# ── 요약 표 ──────────────────────────────────────────────────────────
print(" ┌─────────────────────────────────────────────────────────────┐")
print(" │ 행위기반 커버리지 최종 판정 (BCH-V1) │")
print(" ├──────────────────────────────────┬──────────────────────────┤")
print(f" │ behavioral_coverage_pct │ {py_coverage:>6.2f}% {'✓' if py_coverage >= 100 else '✗'} │")
print(f" │ implementation_divergence_count │ {impl_divergence_count:>6d} {'✓' if impl_divergence_count == 0 else '✗ B06 정정 필요'} │")
print(f" │ gas_pass / gas_fail │ {gas_pass:>4d} / {gas_fail:<4d} {'✓' if gas_fail == 0 else '✗'} │")
print(f" │ python_mirrors_missing │ {len(py_missing_mirrors):>6d} {'(허용)' if py_missing_mirrors else ''} │")
print(" ├──────────────────────────────────┴──────────────────────────┤")
status_token = "BEHAVIORAL_COVERAGE_V1_OK" if overall_ok else "BEHAVIORAL_COVERAGE_V1_FAIL"
print(f" │ STATUS: {status_token:<51}│")
print(" └─────────────────────────────────────────────────────────────┘")
# ── JSON 저장 ────────────────────────────────────────────────────────
summary = {
"status": status_token,
"behavioral_coverage_pct": py_coverage,
"implementation_divergence_count": impl_divergence_count,
"gas_pass": gas_pass,
"gas_fail": gas_fail,
"gas_coverage_pct": gas_coverage,
"python_mirrors_missing": py_missing_mirrors,
"gas_load_errors": gas_load_errors,
"implementation_divergences": impl_divergences,
"behavioral_coverage_gaps": gap_formulas,
"completion_gate": {
"behavioral_coverage_pct_min": 100.0,
"implementation_divergence_count_max": 0,
"met": overall_ok,
},
}
SUMMARY_OUT.parent.mkdir(parents=True, exist_ok=True)
SUMMARY_OUT.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"\n → 결과 저장: {SUMMARY_OUT}")
print(f" {status_token}\n")
if strict and not overall_ok:
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())