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,132 @@
|
||||
"""validate_completion_criteria_v1.py — COMPLETION_CRITERIA_VALIDATOR_V1
|
||||
|
||||
spec/30_completion_criteria_contract.yaml의 16개 기준을 build_completion_gap_v1.py
|
||||
산출물(Temp/completion_gap_v1.json)로부터 프로그램적으로 검증한다.
|
||||
|
||||
기본 모드: 기준 파일 존재·schema·계산 정확성 검증 (FAIL 기준도 허용).
|
||||
--require-pass N: N개 이상 PASS 필요.
|
||||
--strict: 모든 기준 PASS 필요 (이상적 목표 — 현재 달성 불가).
|
||||
|
||||
종료코드: 0=검증 통과, 1=검증 실패.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_GAP = ROOT / "Temp" / "completion_gap_v1.json"
|
||||
SPEC30 = ROOT / "spec" / "30_completion_criteria_contract.yaml"
|
||||
|
||||
|
||||
def _load(path: Path) -> dict:
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception as e:
|
||||
print(f"FAIL: cannot load {path}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--gap", default=str(DEFAULT_GAP))
|
||||
ap.add_argument("--require-pass", type=int, default=0,
|
||||
help="Required minimum PASS count (0=no requirement)")
|
||||
ap.add_argument("--strict", action="store_true",
|
||||
help="All 16 criteria must PASS (ideal goal)")
|
||||
args = ap.parse_args()
|
||||
|
||||
gap_path = Path(args.gap) if Path(args.gap).is_absolute() else ROOT / args.gap
|
||||
if not gap_path.exists():
|
||||
print(f"FAIL: {gap_path} not found — run build-completion-gap-v1 first")
|
||||
return 1
|
||||
|
||||
d = _load(gap_path)
|
||||
failures: list[str] = []
|
||||
|
||||
# 1) schema 검증
|
||||
required = ["formula_id", "total_criteria", "passed_count", "failed_count",
|
||||
"pass_rate_pct", "criteria", "priority_roadmap"]
|
||||
for f in required:
|
||||
if f not in d:
|
||||
failures.append(f"missing field: {f}")
|
||||
|
||||
if failures:
|
||||
for f in failures:
|
||||
print("FAIL:", f)
|
||||
return 1
|
||||
|
||||
total = d["total_criteria"]
|
||||
passed = d["passed_count"]
|
||||
pass_rate = d["pass_rate_pct"]
|
||||
criteria = d["criteria"]
|
||||
|
||||
# 2) 16개 기준 완전성 확인
|
||||
expected_criteria_count = 16
|
||||
if total != expected_criteria_count:
|
||||
failures.append(
|
||||
f"total_criteria={total} != {expected_criteria_count} "
|
||||
f"— spec/30에 {expected_criteria_count}개 기준 정의됨"
|
||||
)
|
||||
|
||||
# 3) 계산 정확성 검증
|
||||
actual_passed = sum(1 for c in criteria if c.get("status") == "PASS")
|
||||
actual_failed = sum(1 for c in criteria if c.get("status") == "FAIL")
|
||||
if actual_passed != passed:
|
||||
failures.append(f"passed_count mismatch: declared={passed} actual={actual_passed}")
|
||||
expected_rate = round(actual_passed / total * 100, 1) if total else 0
|
||||
if abs(expected_rate - pass_rate) > 0.2:
|
||||
failures.append(f"pass_rate_pct mismatch: declared={pass_rate} expected={expected_rate}")
|
||||
|
||||
# 4) 각 기준에 필수 필드 존재 확인
|
||||
req_crit_fields = ["id", "target", "current", "status", "fix", "effort"]
|
||||
for c in criteria:
|
||||
for f in req_crit_fields:
|
||||
if f not in c:
|
||||
failures.append(f"criterion {c.get('id','?')} missing field: {f}")
|
||||
|
||||
# 5) decision_source 필드 확인 (엔진 결정론 기준)
|
||||
reproducibility = next(
|
||||
(c for c in criteria if c.get("id") == "decision_reproducibility_score"), {}
|
||||
)
|
||||
if reproducibility.get("status") != "PASS":
|
||||
failures.append("CRITICAL: decision_reproducibility_score != PASS — 결정론 미달")
|
||||
|
||||
llm_field = next(
|
||||
(c for c in criteria if c.get("id") == "llm_generated_decision_field_count"), {}
|
||||
)
|
||||
if str(llm_field.get("current")) != "0" and llm_field.get("current") != 0:
|
||||
failures.append("CRITICAL: llm_generated_decision_field_count != 0 — LLM 판단 개입")
|
||||
|
||||
if failures:
|
||||
for f in failures:
|
||||
print("FAIL:", f)
|
||||
print(f"COMPLETION_CRITERIA_VALIDATOR_V1: FAIL ({len(failures)} issue(s))")
|
||||
return 1
|
||||
|
||||
# 6) --require-pass 검사
|
||||
if args.require_pass > 0 and passed < args.require_pass:
|
||||
print(
|
||||
f"REQUIRE_PASS_FAIL: {passed}/{total} < required {args.require_pass} "
|
||||
f"({pass_rate}%)"
|
||||
)
|
||||
return 1
|
||||
|
||||
# 7) --strict 검사
|
||||
if args.strict and passed < total:
|
||||
fail_ids = [c["id"] for c in criteria if c.get("status") == "FAIL"]
|
||||
print(f"STRICT_FAIL: {total-passed}/{total} criteria still FAIL: {fail_ids}")
|
||||
return 1
|
||||
|
||||
print(
|
||||
f"COMPLETION_CRITERIA_VALIDATOR_V1: OK | "
|
||||
f"{passed}/{total} PASS ({pass_rate}%) | "
|
||||
f"deterministic=PASS | llm_fields=0"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user