Files
QuantEngineByItz/tools/validate_factor_lifecycle_completeness_v1.py

145 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""validate_factor_lifecycle_completeness_v1.py — FACTOR_LIFECYCLE_COMPLETENESS_V1
실측 기반 팩터 생애주기 레지스트리 정합성을 검증한다.
- spec/factor_lifecycle_registry.yaml과 Temp/factor_shadow_eligibility_v1.json를 대조.
- 실측상 BLOCKED인데 명세상 shadow/active로 지정된 하드 정합성 위반 탐지 (FAIL)
- 실측상 ELIGIBLE인데 명세상 draft로 묶여서 shadow 승격 자격이 충분한 팩터들 탐지 및 요약 보고
- spec/13_formula_registry.yaml와 factor_lifecycle_registry.yaml 간의 팩터 개수 정합성 검증 (누락 시 WARN)
출력: Temp/factor_lifecycle_completeness_v1.json
"""
from __future__ import annotations
import argparse
import json
import yaml
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
TEMP = ROOT / "Temp"
FORMULA_ID = "FACTOR_LIFECYCLE_COMPLETENESS_V1"
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--registry", default="spec/factor_lifecycle_registry.yaml")
ap.add_argument("--eligibility", default="Temp/factor_shadow_eligibility_v1.json")
ap.add_argument("--formula-reg", default="spec/13_formula_registry.yaml")
ap.add_argument("--out", default="Temp/factor_lifecycle_completeness_v1.json")
args = ap.parse_args()
reg_path = ROOT / args.registry if not Path(args.registry).is_absolute() else Path(args.registry)
elig_path = ROOT / args.eligibility if not Path(args.eligibility).is_absolute() else Path(args.eligibility)
freg_path = ROOT / args.formula_reg if not Path(args.formula_reg).is_absolute() else Path(args.formula_reg)
out_path = ROOT / args.out if not Path(args.out).is_absolute() else Path(args.out)
if not reg_path.exists():
print(f"[ERROR] Registry not found: {reg_path}")
return 1
if not elig_path.exists():
print(f"[ERROR] Eligibility file not found: {elig_path}")
return 1
# Load inputs
registry_data = yaml.safe_load(reg_path.read_text(encoding="utf-8")) or {}
eligibility_data = json.loads(elig_path.read_text(encoding="utf-8")) or {}
formula_reg_data = {}
if freg_path.exists():
formula_reg_data = yaml.safe_load(freg_path.read_text(encoding="utf-8")) or {}
factors = registry_data.get("factors") or []
elig_rows = eligibility_data.get("rows") or []
elig_map = {r["factor_id"]: r for r in elig_rows if "factor_id" in r}
# 1. 팩터 개수 정합성 검증
# formula_registry의 formulas 목록
freg_formulas = formula_reg_data.get("formula_registry", {}).get("formulas", {})
freg_keys = set(freg_formulas.keys())
registry_keys = {f.get("factor_id") for f in factors if isinstance(f, dict)}
missing_in_lifecycle = list(freg_keys - registry_keys)
extra_in_lifecycle = list(registry_keys - freg_keys)
violations = []
shadow_ready_candidates = []
# 2. 실측-명세 정합성 검사
for f in factors:
if not isinstance(f, dict):
continue
fid = f.get("factor_id", "UNKNOWN")
promotion_gate = f.get("promotion_gate", "draft").lower()
elig_info = elig_map.get(fid)
if not elig_info:
violations.append({
"factor_id": fid,
"type": "MISSING_ELIGIBILITY_DATA",
"message": f"실측 섀도우 적격성 데이터에 팩터 {fid}가 누락되었습니다."
})
continue
eligibility = elig_info.get("eligibility", "UNKNOWN")
# 하드 위반: 데이터 결측(BLOCKED 또는 NO_REQUIRED_DATA)인데 shadow나 active로 지정된 경우
if eligibility in ("BLOCKED", "NO_REQUIRED_DATA") and promotion_gate in ("shadow", "active", "candidate"):
violations.append({
"factor_id": fid,
"type": "PROMOTION_VIOLATION",
"message": f"팩터 {fid}는 실측 데이터 결측 상태({eligibility})이나 명세 상 {promotion_gate}로 승급되어 있습니다."
})
# shadow 승격 자격이 충분한 draft 팩터 탐지
if eligibility == "ELIGIBLE" and promotion_gate == "draft":
shadow_ready_candidates.append({
"factor_id": fid,
"coverage_pct": elig_info.get("coverage_pct"),
"present_count": elig_info.get("present_count"),
"required_field_count": elig_info.get("required_field_count")
})
# 전체 게이트 상태 결정
# 하드 위반(PROMOTION_VIOLATION 등)이 있으면 FAIL
gate_status = "FAIL" if violations else "PASS"
# 3. 결과 JSON 조립
result = {
"formula_id": FORMULA_ID,
"gate": gate_status,
"summary": {
"total_factors_in_lifecycle": len(factors),
"total_formulas_in_registry": len(freg_keys),
"missing_factors_count": len(missing_in_lifecycle),
"extra_factors_count": len(extra_in_lifecycle),
"shadow_ready_count": len(shadow_ready_candidates),
"violation_count": len(violations),
},
"shadow_ready_candidates": shadow_ready_candidates,
"missing_in_lifecycle": missing_in_lifecycle,
"extra_in_lifecycle": extra_in_lifecycle,
"violations": violations,
"note": (
"FACTOR_LIFECYCLE_COMPLETENESS_V1: 팩터 생애주기 명세와 실측 데이터 일치성 검증. "
"BLOCKED/NO_REQUIRED_DATA 팩터가 shadow/active로 승급되어 있으면 FAIL 판정. "
"draft 상태 중 GatherTradingData.json에 모든 입력 필드가 실측된 팩터는 shadow_ready_candidates로 분류."
)
}
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f"[{FORMULA_ID}] gate={gate_status} total={len(factors)} shadow_ready={len(shadow_ready_candidates)} violations={len(violations)}")
if shadow_ready_candidates:
print(f" Shadow-ready candidates (first 5): {[c['factor_id'] for c in shadow_ready_candidates[:5]]}")
if violations:
print(f" [VIOLATION] First violation: {violations[0]['message']}")
return 0 if gate_status == "PASS" else 1
if __name__ == "__main__":
raise SystemExit(main())