ee3e799de1
주요 변경: - 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>
141 lines
4.5 KiB
Python
141 lines
4.5 KiB
Python
"""validate_golden_coverage_100.py — P1-016 Golden Case Coverage Acceptance Test
|
|
|
|
formula_golden_cases_v4.yaml(및 이전 버전)까지 포함해 golden_coverage_ratio가
|
|
목표치를 초과하는지 검증.
|
|
|
|
수락 기준:
|
|
- golden_coverage_ratio >= 0.95 (95%+): PASS
|
|
- decision_critical_min_cases >= 3: PASS for all 28 decision-critical formulas
|
|
|
|
Exit:
|
|
0 = PASS (목표 커버리지 달성)
|
|
1 = FAIL (커버리지 미달 또는 decision-critical 케이스 부족)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
COVERAGE_JSON = ROOT / "Temp" / "yaml_code_coverage_v1.json"
|
|
COVERAGE_TARGET = 0.95
|
|
|
|
DECISION_CRITICAL = {
|
|
"STOP_BREACH_V1",
|
|
"SMART_CASH_RECOVERY_V4",
|
|
"SMART_CASH_RECOVERY_V7",
|
|
"ANTI_LATE_ENTRY_PULLBACK_GATE_V4",
|
|
"REGIME_CONDITIONAL_MACRO_FACTOR_V1",
|
|
"MACRO_REGIME_ALIGNMENT_GATE_V2",
|
|
"DISTRIBUTION_EXIT_PRESIGNAL_V2",
|
|
"SELL_EXECUTION_TIMING_LOCK_V2",
|
|
"SELL_EXECUTION_QUALITY_GATE_V1",
|
|
"PORTFOLIO_HEALTH_V1",
|
|
"INDEX_RELATIVE_HEALTH_GATE_V1",
|
|
"CASH_RAISE_PARETO_EXECUTOR_V2",
|
|
"CASH_RAISE_VALUE_OPTIMIZER_V3",
|
|
"CASH_RECOVERY_OPTIMIZER_V4",
|
|
"CASH_RECOVERY_V1",
|
|
"DATA_MATURITY_TRUTH_GATE_V1",
|
|
"DATA_MATURITY_TRUTH_GATE_VALIDATOR_V1",
|
|
"DATA_QUALITY_GATE_V2_PY",
|
|
"DATA_QUALITY_GATE_V3",
|
|
"IMPUTED_DATA_EXPOSURE_GATE_V2",
|
|
"OPERATIONAL_ALPHA_CALIBRATION_V2",
|
|
"PREDICTIVE_ALPHA_DIALECTIC_ENGINE_V1_BRIDGE",
|
|
"REBOUND_SELL_EFFICIENCY_V1",
|
|
"SELL_ENGINE_AUDIT_V1",
|
|
"SELL_SLIPPAGE_BUDGET_FACTOR_V1",
|
|
"ENTRY_TIMING_DECILE_FACTOR_V1",
|
|
"DYNAMIC_VALUE_PRESERVATION_SELL_V3_BRIDGE",
|
|
"ARTIFACT_FRESHNESS_GATE_V1",
|
|
}
|
|
|
|
MIN_CASES_PER_CRITICAL = 3
|
|
|
|
|
|
def _rebuild_coverage() -> dict:
|
|
"""coverage JSON이 없으면 빌더를 실행해 갱신."""
|
|
if not COVERAGE_JSON.exists():
|
|
subprocess.run(
|
|
[sys.executable, str(ROOT / "tools" / "build_yaml_code_coverage_v1.py")],
|
|
check=True,
|
|
)
|
|
with COVERAGE_JSON.open(encoding="utf-8") as fh:
|
|
return json.load(fh)
|
|
|
|
|
|
def _count_cases_per_formula() -> dict[str, int]:
|
|
"""golden_cases_v2/v3/v4 에서 formula_id별 케이스 수 집계."""
|
|
import yaml # type: ignore
|
|
|
|
spec_dir = ROOT / "spec"
|
|
files = [
|
|
spec_dir / "formula_golden_cases_v2.yaml",
|
|
spec_dir / "formula_golden_cases_v3.yaml",
|
|
spec_dir / "formula_golden_cases_v4.yaml",
|
|
]
|
|
counts: dict[str, int] = {}
|
|
for f in files:
|
|
if not f.exists():
|
|
continue
|
|
try:
|
|
doc = yaml.safe_load(f.read_text(encoding="utf-8")) or {}
|
|
except Exception:
|
|
continue
|
|
for entry in doc.get("golden_cases", []):
|
|
fid = entry.get("formula_id")
|
|
if fid:
|
|
counts[fid] = counts.get(fid, 0) + 1
|
|
return counts
|
|
|
|
|
|
def main() -> int:
|
|
cov = _rebuild_coverage()
|
|
ratio = cov.get("golden_coverage_ratio", 0.0)
|
|
total = cov.get("yaml_formula_count", 0)
|
|
golden = cov.get("golden_test_count", 0)
|
|
uncovered = cov.get("golden_uncovered_rules", [])
|
|
|
|
case_counts = _count_cases_per_formula()
|
|
|
|
critical_missing: list[str] = []
|
|
for fid in DECISION_CRITICAL:
|
|
if case_counts.get(fid, 0) < MIN_CASES_PER_CRITICAL:
|
|
critical_missing.append(fid)
|
|
|
|
ok_ratio = ratio >= COVERAGE_TARGET
|
|
ok_critical = len(critical_missing) == 0
|
|
|
|
print(f"[GOLDEN_COVERAGE_100] total={total} golden={golden} ratio={ratio:.4f} "
|
|
f"({'≥' if ok_ratio else '<'}{COVERAGE_TARGET}) "
|
|
f"critical_missing={len(critical_missing)}")
|
|
|
|
if critical_missing:
|
|
print(f" [WARN] decision-critical with <{MIN_CASES_PER_CRITICAL} cases:")
|
|
for fid in critical_missing:
|
|
print(f" - {fid}: {case_counts.get(fid, 0)} case(s)")
|
|
|
|
if uncovered:
|
|
print(f" [WARN] still uncovered ({len(uncovered)}): "
|
|
+ ", ".join(uncovered[:20])
|
|
+ ("..." if len(uncovered) > 20 else ""))
|
|
|
|
if ok_ratio and ok_critical:
|
|
print(" [PASS] golden_coverage_ratio >= 95% and all decision-critical formulas covered")
|
|
return 0
|
|
else:
|
|
issues = []
|
|
if not ok_ratio:
|
|
issues.append(f"golden_coverage_ratio={ratio:.4f} < {COVERAGE_TARGET}")
|
|
if not ok_critical:
|
|
issues.append(f"{len(critical_missing)} critical formula(s) have <{MIN_CASES_PER_CRITICAL} cases")
|
|
print(f" [FAIL] {'; '.join(issues)}")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|