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,161 @@
|
||||
"""run_release_ci_gate_v2.py — RELEASE_CI_GATE_V2
|
||||
|
||||
P1-024: 100% 완료 정의를 코드로 강제하는 CI 게이트.
|
||||
schema → source_recheck → formula_coverage → golden → pass100 → execution_precedence
|
||||
→ outcome_readiness → LLM_freedom 순서로 순차 검증.
|
||||
하나라도 FAIL이면 release_ci_gate=BLOCK_DEPLOYMENT.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from v7_hardening_common import ROOT, TEMP, load_json, save_json
|
||||
|
||||
DEFAULT_OUT = TEMP / "release_ci_gate_v2.json"
|
||||
|
||||
|
||||
def _f(v: Any, default: float = 0.0) -> float:
|
||||
try:
|
||||
return float(v)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _check(step: str, condition: bool, actual: Any, target: str, source: str) -> dict[str, Any]:
|
||||
return {
|
||||
"step": step,
|
||||
"status": "PASS" if condition else "FAIL",
|
||||
"actual": actual,
|
||||
"target": target,
|
||||
"source": source,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
checks: list[dict[str, Any]] = []
|
||||
|
||||
# ── Step 1: Schema Validity ──────────────────────────────────────────────
|
||||
dq = load_json(TEMP / "data_quality_reconciliation_v1.json")
|
||||
schema_score = _f(dq.get("schema_presence_score"))
|
||||
checks.append(_check(
|
||||
"1_SCHEMA_VALIDITY",
|
||||
schema_score >= 99.0,
|
||||
schema_score,
|
||||
">= 99",
|
||||
"data_quality_reconciliation_v1.json",
|
||||
))
|
||||
|
||||
# ── Step 2: Source Recheck (Single Truth) ────────────────────────────────
|
||||
stl = load_json(TEMP / "single_truth_ledger_v2.json")
|
||||
conflict = int(stl.get("conflict_count") or 0)
|
||||
checks.append(_check(
|
||||
"2_SOURCE_RECHECK",
|
||||
conflict == 0,
|
||||
conflict,
|
||||
"== 0",
|
||||
"single_truth_ledger_v2.json",
|
||||
))
|
||||
|
||||
# ── Step 3: Formula Coverage ─────────────────────────────────────────────
|
||||
cov = load_json(TEMP / "harness_coverage_audit.json")
|
||||
true_missing = int(cov.get("true_missing_count") or 0)
|
||||
checks.append(_check(
|
||||
"3_FORMULA_COVERAGE",
|
||||
true_missing == 0,
|
||||
true_missing,
|
||||
"== 0",
|
||||
"harness_coverage_audit.json",
|
||||
))
|
||||
|
||||
# ── Step 4: Golden Test ──────────────────────────────────────────────────
|
||||
golden = load_json(TEMP / "formula_behavioral_coverage_v3.json")
|
||||
golden_fail = int(golden.get("failed_cases") or 0)
|
||||
checks.append(_check(
|
||||
"4_GOLDEN_TEST",
|
||||
golden_fail == 0,
|
||||
golden_fail,
|
||||
"failed_cases == 0",
|
||||
"formula_behavioral_coverage_v3.json",
|
||||
))
|
||||
|
||||
# ── Step 5: PASS_100 Active (v3) ─────────────────────────────────────────
|
||||
p100 = load_json(TEMP / "pass_100_criteria_v3.json")
|
||||
is_active = bool(p100.get("is_active"))
|
||||
checks.append(_check(
|
||||
"5_PASS_100_ACTIVE",
|
||||
is_active,
|
||||
is_active,
|
||||
"is_active == True",
|
||||
"pass_100_criteria_v3.json",
|
||||
))
|
||||
|
||||
# ── Step 6: Execution Precedence ─────────────────────────────────────────
|
||||
v4 = load_json(TEMP / "final_execution_decision_v4.json")
|
||||
audit_hts = str(v4.get("global_execution_gate") or "") == "AUDIT_ONLY" and int(v4.get("hts_order_count") or 0) == 0
|
||||
hts_ready = str(v4.get("global_execution_gate") or "") == "HTS_READY" and int(v4.get("hts_order_count") or 0) > 0
|
||||
precedence_ok = audit_hts or hts_ready
|
||||
checks.append(_check(
|
||||
"6_EXECUTION_PRECEDENCE",
|
||||
precedence_ok,
|
||||
f"gate={v4.get('global_execution_gate')},hts={v4.get('hts_order_count')}",
|
||||
"AUDIT_ONLY→hts=0 OR HTS_READY→hts>0",
|
||||
"final_execution_decision_v4.json",
|
||||
))
|
||||
|
||||
# ── Step 7: Outcome Readiness (honest) ───────────────────────────────────
|
||||
truth = load_json(TEMP / "operational_truth_score_v1.json")
|
||||
truth_gate = str(truth.get("gate") or "")
|
||||
# readiness is expected to be WATCH_PENDING_SAMPLE until T+20 accumulates — not a hard block
|
||||
readiness_ok = truth_gate != "BLOCK_EXECUTION"
|
||||
checks.append(_check(
|
||||
"7_OUTCOME_READINESS",
|
||||
readiness_ok,
|
||||
truth_gate,
|
||||
"!= BLOCK_EXECUTION (WATCH acceptable)",
|
||||
"operational_truth_score_v1.json",
|
||||
))
|
||||
|
||||
# ── Step 8: LLM Freedom ──────────────────────────────────────────────────
|
||||
honesty = load_json(TEMP / "truthfulness_guard_v1.json")
|
||||
violations = int(honesty.get("contradiction_count") or 0)
|
||||
checks.append(_check(
|
||||
"8_LLM_FREEDOM",
|
||||
violations == 0,
|
||||
violations,
|
||||
"contradiction_count == 0",
|
||||
"truthfulness_guard_v1.json",
|
||||
))
|
||||
|
||||
# ── 결과 집계 ────────────────────────────────────────────────────────────
|
||||
failed = [c for c in checks if c["status"] == "FAIL"]
|
||||
gate = "PASS" if not failed else "BLOCK_DEPLOYMENT"
|
||||
|
||||
result = {
|
||||
"formula_id": "RELEASE_CI_GATE_V2",
|
||||
"gate": gate,
|
||||
"checks_total": len(checks),
|
||||
"checks_passed": len(checks) - len(failed),
|
||||
"checks_failed": len(failed),
|
||||
"failed_steps": [c["step"] for c in failed],
|
||||
"deploy_allowed": gate == "PASS",
|
||||
"checks": checks,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"source_path": "Temp/release_ci_gate_v2.json",
|
||||
}
|
||||
save_json(str(DEFAULT_OUT), result)
|
||||
summary = {k: v for k, v in result.items() if k != "checks"}
|
||||
print(json.dumps(summary, indent=2, ensure_ascii=True))
|
||||
if gate == "PASS":
|
||||
print("RELEASE_CI_GATE_V2_PASS")
|
||||
else:
|
||||
print(f"RELEASE_CI_GATE_V2_BLOCK_DEPLOYMENT ({len(failed)} checks failed)")
|
||||
for c in failed:
|
||||
print(f" FAIL {c['step']}: actual={c['actual']} target={c['target']}")
|
||||
return 0 if gate == "PASS" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user