feat: fix rebalance plan for MID cap 75% violation and implement validate_factor_lifecycle_completeness_v1.py
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user