캘리브레이션 거버넌스 도구 + WBS-7.1/7.2 실증 격차 가시화
캘리브레이션 백로그 → 우선순위 → 검토리포트 → 승인목록 → 결정초안으로 이어지는 임계값 보정 거버넌스 파이프라인을 추가하고, 2026-06-21 비판적 리뷰에서 발견한 두 가지 stale-수치 문제를 도구 차원에서 해소한다. - registry_health(): 190여 개 임계값의 source별(SPEC_DERIVED/EXPERT_PRIOR/ PROVISIONAL/CALIBRATED) 분포를 매 실행마다 자동 집계 — 수동 grep 불필요 - live_t5_status(): T+5 적중률을 하드코딩(35.86 리터럴) 대신 Temp/prediction_accuracy_harness_v2.json에서 항상 최신값으로 읽음 - spec/calibration_registry.yaml: SEMI_CLUSTER_CAP_RISK_OFF 중복 id로 인한 조용한 무시 버그 수정(SEMI_CLUSTER_CAP_RISK_OFF_MWA로 분리) - spec/27_bch_calibration_runbook.yaml: current_status_2026_06_21 블록 신설(단일 진실원천), 기존 05-30 스냅샷은 "역사적, 현재로 인용 금지"로 명시
This commit is contained in:
@@ -29,6 +29,41 @@ ROOT = Path(__file__).resolve().parent.parent
|
||||
AFL = ROOT / "Temp" / "alpha_feedback_loop_v2.json"
|
||||
REG = ROOT / "spec" / "calibration_registry.yaml"
|
||||
OUTPUT = ROOT / "Temp" / "calibration_priority_v1.json"
|
||||
PREDICTION_ACCURACY = ROOT / "Temp" / "prediction_accuracy_harness_v2.json"
|
||||
|
||||
|
||||
def registry_source_breakdown(reg_index: dict[str, dict]) -> dict:
|
||||
"""WBS-7.1(2026-06-21) — calibration_registry.yaml 전체의 source별 분포를 매 실행마다
|
||||
집계해 'CALIBRATED 비율이 실제로 몇 %인가'를 사람이 grep으로 직접 세지 않아도
|
||||
항상 최신 상태로 노출한다(2026-06-21 비판적 리뷰 0c절에서 0/190 발견 당시 수동 집계 필요했던 문제 해소)."""
|
||||
counts: dict[str, int] = {"SPEC_DERIVED": 0, "EXPERT_PRIOR": 0, "PROVISIONAL": 0, "CALIBRATED": 0}
|
||||
for entry in reg_index.values():
|
||||
source = str(entry.get("source", "")).upper()
|
||||
if source in counts:
|
||||
counts[source] += 1
|
||||
total = sum(counts.values())
|
||||
return {
|
||||
"total_thresholds": total,
|
||||
"counts": counts,
|
||||
"calibrated_pct": round(100.0 * counts["CALIBRATED"] / total, 2) if total else 0.0,
|
||||
"unvalidated_pct": round(100.0 * (counts["SPEC_DERIVED"] + counts["EXPERT_PRIOR"]) / total, 2) if total else 0.0,
|
||||
}
|
||||
|
||||
|
||||
def live_t5_status() -> dict:
|
||||
"""WBS-7.2/7.1(2026-06-21) — T+5 수치를 하드코딩하지 않고 항상 최신 산출물에서 읽는다.
|
||||
Temp/prediction_accuracy_harness_v2.json이 없거나 sample=0이면 정직하게 DATA_GATED로 보고한다."""
|
||||
if not PREDICTION_ACCURACY.exists():
|
||||
return {"status": "ARTIFACT_MISSING", "t5_sample": 0, "t5_match_rate_pct": None}
|
||||
data = load_json(PREDICTION_ACCURACY)
|
||||
t5_sample = int(data.get("t5_sample") or 0)
|
||||
t5_rate = data.get("t5_op_rate")
|
||||
return {
|
||||
"status": "DATA_GATED" if t5_sample == 0 else "OK",
|
||||
"as_of_date": data.get("as_of_date"),
|
||||
"t5_sample": t5_sample,
|
||||
"t5_match_rate_pct": t5_rate,
|
||||
}
|
||||
|
||||
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
|
||||
sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1)
|
||||
@@ -90,6 +125,42 @@ def load_registry(p: Path) -> dict[str, dict]:
|
||||
return {t["id"]: t for t in data.get("thresholds", []) if "id" in t}
|
||||
|
||||
|
||||
def _priority_from_registry_entry(entry: dict, source_tag: str, urgency_bias: int) -> dict:
|
||||
sample_n = int(entry.get("sample_n", 0) or 0)
|
||||
source = str(entry.get("source", "EXPERT_PRIOR"))
|
||||
threshold_class = str(entry.get("threshold_class", "standard"))
|
||||
urgency = urgency_bias
|
||||
if source == "EXPERT_PRIOR":
|
||||
urgency += 10
|
||||
if source == "PROVISIONAL":
|
||||
urgency += 20
|
||||
if threshold_class == "live_critical":
|
||||
urgency += 15
|
||||
if sample_n == 0:
|
||||
urgency += 5
|
||||
if sample_n > 0:
|
||||
urgency += max(0, 30 - sample_n)
|
||||
return {
|
||||
"calibration_id": entry.get("id", ""),
|
||||
"current_value": entry.get("value"),
|
||||
"owner_formula": entry.get("owner_formula", ""),
|
||||
"source": source,
|
||||
"sample_n": sample_n,
|
||||
"linked_factor": source_tag,
|
||||
"alpha_action": "registry_review",
|
||||
"urgency_score": urgency,
|
||||
"calibration_path": (
|
||||
(
|
||||
"표본 30건 이상 확보 후 PROVISIONAL 승격 → "
|
||||
if sample_n >= 30
|
||||
else f"표본 {30 - sample_n}건 추가 수집 후 PROVISIONAL 승격 → "
|
||||
)
|
||||
+ "실측 T+5 승률 기반 최적값 backtest → CALIBRATED 확정"
|
||||
),
|
||||
"rationale": f"source={source}, class={threshold_class}, sample_n={sample_n}",
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
afl_data = load_json(AFL)
|
||||
reg_index = load_registry(REG)
|
||||
@@ -112,48 +183,32 @@ def main() -> int:
|
||||
priority_list: list[dict] = []
|
||||
|
||||
for adj in adjustments:
|
||||
factor = adj.get("factor", "")
|
||||
action = adj.get("action", "")
|
||||
rationale = adj.get("rationale", "")
|
||||
reg_ids = FACTOR_TO_REGISTRY.get(factor, [])
|
||||
factor = str(adj.get("factor", ""))
|
||||
action = str(adj.get("action", ""))
|
||||
rationale = str(adj.get("rationale", ""))
|
||||
reg_ids = FACTOR_TO_REGISTRY.get(factor, [])
|
||||
|
||||
for rid in reg_ids:
|
||||
reg_entry = reg_index.get(rid)
|
||||
if not reg_entry:
|
||||
continue
|
||||
source = reg_entry.get("source", "EXPERT_PRIOR")
|
||||
sample_n = int(reg_entry.get("sample_n", 0) or 0)
|
||||
value = reg_entry.get("value")
|
||||
formula = reg_entry.get("owner_formula", "")
|
||||
item = _priority_from_registry_entry(reg_entry, factor, miss5_count if factor == "passive_signal_quality" else 0)
|
||||
item["alpha_action"] = action or "feedback_review"
|
||||
if rationale:
|
||||
item["rationale"] = rationale[:200]
|
||||
priority_list.append(item)
|
||||
|
||||
# 보정 우선도 점수: miss5_count 기여 + 미보정 가중
|
||||
urgency = 0
|
||||
if factor == "passive_signal_quality":
|
||||
urgency += miss5_count # miss가 많을수록 높은 urgency
|
||||
if source == "EXPERT_PRIOR":
|
||||
urgency += 10
|
||||
if sample_n == 0:
|
||||
urgency += 5
|
||||
|
||||
priority_list.append({
|
||||
"calibration_id": rid,
|
||||
"current_value": value,
|
||||
"owner_formula": formula,
|
||||
"source": source,
|
||||
"sample_n": sample_n,
|
||||
"linked_factor": factor,
|
||||
"alpha_action": action,
|
||||
"urgency_score": urgency,
|
||||
"calibration_path": (
|
||||
(
|
||||
"표본 30건 이상 확보 후 PROVISIONAL 승격 → "
|
||||
if sample_n >= 30
|
||||
else f"표본 {30 - sample_n}건 추가 수집 후 PROVISIONAL 승격 → "
|
||||
)
|
||||
+ "실측 T+5 승률 기반 최적값 backtest → CALIBRATED 확정"
|
||||
),
|
||||
"rationale": rationale[:200] if rationale else "",
|
||||
})
|
||||
if not priority_list:
|
||||
# alpha_feedback_loop가 비어 있어도 registry 자체의 보정 debt를 추적할 수 있게 한다.
|
||||
for reg_id, reg_entry in reg_index.items():
|
||||
source = str(reg_entry.get("source", "EXPERT_PRIOR"))
|
||||
if source not in {"EXPERT_PRIOR", "PROVISIONAL"}:
|
||||
continue
|
||||
tag = f"registry:{source.lower()}"
|
||||
item = _priority_from_registry_entry(reg_entry, tag, 0)
|
||||
if source == "PROVISIONAL":
|
||||
item["urgency_score"] += 5
|
||||
priority_list.append(item)
|
||||
|
||||
# 중복 제거 (같은 rid, 높은 urgency 유지)
|
||||
seen: dict[str, dict] = {}
|
||||
@@ -177,7 +232,19 @@ def main() -> int:
|
||||
print(f" Step 2 (30건 후): ALEG_V2_GATE1_BLOCK_PCT 3.0% → 실측 최적값으로 PROVISIONAL 승격")
|
||||
print(f" Step 3 (50건 후): DSD_V1 가중치 logistic regression 최적화")
|
||||
print(f" Step 4 (100건 후): K2_SPLIT_RATIO backtest 비교 → CALIBRATED 확정")
|
||||
print(f" miss5_count={miss5_count}건 → passive_signal_quality 개선이 T+5 35.86%→50%+ 핵심")
|
||||
registry_health = registry_source_breakdown(reg_index)
|
||||
t5_status = live_t5_status()
|
||||
|
||||
print(f"\n [캘리브레이션 레지스트리 건강도] (WBS-7.1)")
|
||||
print(f" total={registry_health['total_thresholds']} {registry_health['counts']}")
|
||||
print(f" CALIBRATED={registry_health['calibrated_pct']}% 미검증(SPEC_DERIVED+EXPERT_PRIOR)={registry_health['unvalidated_pct']}%")
|
||||
|
||||
if t5_status["status"] == "DATA_GATED":
|
||||
print(f" miss5_count={miss5_count}건 → T+5 현재 DATA_GATED(sample=0) — passive_signal_quality 개선 영향은 표본 누적 후 측정 가능")
|
||||
elif t5_status["status"] == "ARTIFACT_MISSING":
|
||||
print(f" miss5_count={miss5_count}건 → T+5 산출물 없음(Temp/prediction_accuracy_harness_v2.json) — 먼저 생성 필요")
|
||||
else:
|
||||
print(f" miss5_count={miss5_count}건 → T+5={t5_status['t5_match_rate_pct']}% (as_of={t5_status.get('as_of_date')}) → passive_signal_quality 개선 핵심")
|
||||
|
||||
result = {
|
||||
"status": "CALIBRATION_PRIORITY_OK",
|
||||
@@ -191,10 +258,14 @@ def main() -> int:
|
||||
"step3": "50건 후: DSD_V1 가중치 logistic regression 최적화",
|
||||
"step4": "100건 후: K2_SPLIT_RATIO 30/70~60/40 backtest → CALIBRATED",
|
||||
},
|
||||
"priority_basis": "alpha_feedback_loop_v2" if adjustments else "registry_warning_fallback",
|
||||
"registry_health": registry_health,
|
||||
"target_improvement": {
|
||||
"current_t5_pct": 35.86,
|
||||
"t5_status": t5_status["status"],
|
||||
"current_t5_pct": t5_status["t5_match_rate_pct"],
|
||||
"t5_as_of_date": t5_status.get("as_of_date"),
|
||||
"target_t5_pct": 55.0,
|
||||
"key_lever": "passive_signal_quality (miss5_count=51건 개선)",
|
||||
"key_lever": f"passive_signal_quality (miss5_count={miss5_count}건 개선)",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user