Files
QuantEngineByItz/tools/build_truth_reconciliation_gate_v1.py
T
kjh2064 ee3e799de1 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>
2026-06-13 13:20:14 +09:00

150 lines
5.6 KiB
Python

"""build_truth_reconciliation_gate_v1.py — TRUTH_RECONCILIATION_GATE_V1
P0-T3: 동일 지표가 파일마다 다른 값을 가지면 자동 FAIL.
감시 지표: prediction_match_rate_pct, t20_pass_rate, value_damage_pct_avg,
gs_coverage_pct, portfolio_alpha_confidence, performance_readiness_score
허용 오차: 비율 지표 ±0.5%p, 금액 지표 ±1원
"""
from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
TEMP = ROOT / "Temp"
DEFAULT_OUT = TEMP / "truth_reconciliation_gate_v1.json"
TOLERANCE_RATE = 0.5 # %p
TOLERANCE_KRW = 1.0 # 원
# 감시 지표: (정규화된 metric_id, json_pointer_list, 단위, 제외 파일 패턴)
# 위양성 방지: 같은 key명이 다른 개념에 쓰이는 파일은 명시 제외
MONITORED_METRICS: list[tuple[str, list[str], str, set[str]]] = [
("prediction_match_rate_pct",
["prediction_match_rate_pct", "t5_ap_combined"],
"rate",
# v5 = legacy v5.todo.batch 파일 (builder 없음), v7 = 다른 블렌드 점수
{"prediction_accuracy_harness_v5", "smart_cash_recovery_v7"}),
("t20_pass_rate",
["t20_pass_rate"], # pass_rate_pct는 제외 (completion_gap과 혼동)
"rate",
{"completion_gap", "phase_checks"}), # 완료기준 통과율 파일 제외
("value_damage_pct_avg",
["value_damage_pct_avg"],
"rate",
# 다른 목적함수 + 구버전 아카이브 파일 제외 (현재 파이프라인 외 레거시)
{"dynamic_value_preservation", "cash_raise_value_optimizer",
"cash_raise_value_preservation", "value_preserving_cash_raise_v1",
"hts_sell_blueprint",
"smart_cash_recovery_v7.json"}), # v7 non-authoritative (2026-05-31 legacy)
("gs_strict_coverage_pct", # gs_coverage_pct 대신 strict 전용 포인터
["gs_coverage_pct"],
"rate",
{"gs_native_coverage_lock"}), # native coverage는 다른 개념
("portfolio_alpha_confidence",
["portfolio_alpha_confidence", "alpha_confidence"],
"rate",
set()),
("performance_readiness_score",
["performance_readiness_score", "blended_performance_readiness_score"],
"rate",
set()),
]
def _load(p: Path) -> dict[str, Any]:
if not p.exists():
return {}
try:
obj = json.loads(p.read_text(encoding="utf-8"))
return obj if isinstance(obj, dict) else {}
except Exception:
return {}
def _extract(d: dict[str, Any], pointers: list[str]) -> float | None:
for ptr in pointers:
v = d.get(ptr)
if v is not None:
try:
f = float(v)
if f != 0.0 or ptr in d:
return f
except (TypeError, ValueError):
pass
return None
def main() -> int:
# 모든 Temp JSON 로드
json_files = list(TEMP.glob("*.json"))
# 제외: 보고서/golden/binary
exclude_patterns = {"formula_golden", "formula_behavioral", "formula_gas_parity", "engine_audit_2026"}
candidates = [f for f in json_files if not any(ex in f.name for ex in exclude_patterns)]
observations: dict[str, list[dict[str, Any]]] = {m[0]: [] for m in MONITORED_METRICS}
for f in candidates:
d = _load(f)
if not d:
continue
rel = str(f.relative_to(ROOT))
for metric_id, pointers, unit, exclude_patterns in MONITORED_METRICS:
# 제외 패턴 파일 스킵
if any(ep in f.name for ep in exclude_patterns):
continue
val = _extract(d, pointers)
if val is not None:
observations[metric_id].append({"file": rel, "value": val})
conflicts: list[dict[str, Any]] = []
for metric_id, pointers, unit, _ in MONITORED_METRICS:
obs = observations[metric_id]
if len(obs) < 2:
continue
values = [o["value"] for o in obs]
min_v, max_v = min(values), max(values)
tol = TOLERANCE_RATE if unit == "rate" else TOLERANCE_KRW
if (max_v - min_v) > tol:
conflicts.append({
"metric_id": metric_id,
"min": min_v,
"max": max_v,
"spread": round(max_v - min_v, 4),
"tolerance": tol,
"unit": unit,
"observations": sorted(obs, key=lambda x: x["value"]),
})
gate = "PASS" if not conflicts else "FAIL"
result = {
"formula_id": "TRUTH_RECONCILIATION_GATE_V1",
"gate": gate,
"conflict_count": len(conflicts),
"conflicts": conflicts,
"monitored_metrics": [m[0] for m in MONITORED_METRICS],
"excluded_per_metric": {m[0]: list(m[3]) for m in MONITORED_METRICS if m[3]},
"files_scanned": len(candidates),
"generated_at": datetime.now(timezone.utc).isoformat(),
}
DEFAULT_OUT.parent.mkdir(parents=True, exist_ok=True)
DEFAULT_OUT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
summary = {k: v for k, v in result.items() if k != "conflicts"}
print(json.dumps(summary, indent=2, ensure_ascii=False))
if gate == "PASS":
print("TRUTH_RECONCILIATION_GATE_V1_PASS")
else:
print(f"TRUTH_RECONCILIATION_GATE_V1_FAIL ({len(conflicts)} conflicts)")
for c in conflicts:
print(f" {c['metric_id']}: spread={c['spread']} (tol={c['tolerance']})")
for o in c["observations"]:
print(f" {o['file']}: {o['value']}")
return 0 if gate == "PASS" else 1
if __name__ == "__main__":
raise SystemExit(main())