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
+132
View File
@@ -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())