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:
@@ -0,0 +1,140 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user