feat: .NET 운영 리포트 렌더러와 CI 경로 전환

- operational_report.json/md와 final_decision_packet_v4 생성 경로를 .NET으로 전환했습니다.
- CI, 운영 게이트, 릴리스 DAG, 대시보드의 운영 진입점을 새 경로로 정렬했습니다.
- legacy Python 렌더러는 비운영으로 명시했습니다.
This commit is contained in:
2026-06-26 14:18:03 +09:00
parent 8f13bb4a48
commit 9e6e2ded2f
15 changed files with 649 additions and 120 deletions
+88 -3
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
"""
render_operational_report.py — 30개 섹션 완전 렌더링.
render_operational_report.py — legacy renderer.
운영/CI 기준 구현은 src/dotnet/QuantEngine.Tools/Program.cs 이다.
이 파일은 유지보수 및 과거 호환성 참조용으로만 남긴다.
섹션 처리 오류는 section_errors 배열에 기록되어 하네스 검증에 노출된다.
"""
from __future__ import annotations
@@ -42,7 +44,7 @@ SECTION_ORDER = [
"backdata_feature_bank_table", "alpha_lead_table", "anti_distribution_table",
"profit_preservation_table", "smart_cash_raise_table", "execution_quality_table",
"sell_priority_decision_table", "strategy_performance_scoreboard",
"performance_readiness_summary", "operational_eval_queue_summary", "outcome_eval_window_monitor",
"performance_readiness_summary", "operational_t20_activation_summary", "operational_eval_queue_summary", "outcome_eval_window_monitor",
"decision_trace_table", "anti_whipsaw_reentry_gate", "proposal_reference_sheet",
"satellite_buy_proposal_sheet", "core_satellite_timing_gate_table",
"engine_feedback_loop_report", "prediction_evaluation_improvement_report",
@@ -96,6 +98,7 @@ SECTION_TITLES = {
"sell_priority_decision_table": "매도 우선순위 결정 테이블",
"strategy_performance_scoreboard": "전략 성과 스코어보드",
"performance_readiness_summary": "성과 준비도 요약",
"operational_t20_activation_summary": "운영 T+20 활성화 요약",
"operational_eval_queue_summary": "운영 T+20 대기열 요약",
"outcome_eval_window_monitor": "성과 평가 윈도우 모니터",
"decision_trace_table": "판단 추적 테이블",
@@ -1121,7 +1124,11 @@ def _performance_readiness_summary(hctx: dict, se: list) -> str:
oac = _load(oac_path)
if not oac:
return _err(se, "performance_readiness_summary", "operational_alpha_calibration_v2.json 없음")
return _kv([
("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
("대상 파일", "operational_alpha_calibration_v2.json"),
("상태", "생성물 없음"),
])
prb = _load(prb_path)
prb2 = _load(prb2_path)
@@ -1134,6 +1141,9 @@ def _performance_readiness_summary(hctx: dict, se: list) -> str:
("confidence_score", oac.get("confidence_score", "")),
("performance_ready", oac.get("performance_ready", "")),
("readiness_reasons", ", ".join(oac.get("readiness_reasons", [])) if isinstance(oac.get("readiness_reasons"), list) else oac.get("readiness_reasons", "")),
("bridge_gate", prb.get("gate", "")),
("bridge_live_t20_count", live.get("t20_count", "")),
("bridge_required_live_t20_count", prb.get("required_live_t20_count", "")),
("outcome_quality_score", metrics.get("outcome_quality_score", "")),
("t20_operational_sample", metrics.get("t20_operational_sample", "")),
("t5_operational_pass_rate", metrics.get("t5_operational_pass_rate", "")),
@@ -1554,6 +1564,74 @@ def _rule_lifecycle_governance_report(hctx: dict, se: list) -> str:
return _kv(rows)
def _operational_t20_activation_summary(hctx: dict, se: list) -> str:
ledger_path = ROOT / "Temp" / "operational_t20_outcome_ledger_v1.json"
gate_path = ROOT / "Temp" / "live_data_activation_gate_v1.json"
replay_path = ROOT / "Temp" / "replay_live_separation_v1.json"
def _load(path: Path) -> dict:
if not path.exists():
return {}
try:
data = json.loads(path.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}
except Exception:
return {}
ledger = _load(ledger_path)
gate = _load(gate_path)
replay = _load(replay_path)
rows = [
("ledger_total_cases", ledger.get("total_cases", "")),
("ledger_win_rate_pct", ledger.get("win_rate_pct", "")),
("activation_gate", gate.get("gate", "")),
("live_t20_count", gate.get("live_t20_count", "")),
("live_t20_threshold", gate.get("live_t20_threshold", "")),
("activation_progress_pct", gate.get("progress_pct", "")),
("activation_message", gate.get("message", "")),
("replay_live_mix_count", replay.get("replay_live_mix_count", "")),
("live_metrics_null_when_insufficient", replay.get("live_metrics_null_when_insufficient", "")),
]
if not ledger:
rows.append(("ledger_state", "DATA_MISSING — 하네스 업데이트 필요"))
if not gate:
rows.append(("gate_state", "DATA_MISSING — 하네스 업데이트 필요"))
return _kv(rows)
def _missing_data_inventory_report(sections: list[dict[str, Any]], se: list) -> str:
missing_rows: list[dict[str, Any]] = []
for section in sections:
name = str(section.get("name") or "")
markdown = str(section.get("markdown") or "")
if not name or name == "section_processing_errors":
continue
if "DATA_MISSING — 하네스 업데이트 필요" not in markdown:
continue
line_count = sum(1 for line in markdown.splitlines() if "DATA_MISSING — 하네스 업데이트 필요" in line)
if name in {"fundamental_quality_gate_v1", "horizon_allocation_lock_v1", "smart_money_liquidity_gate_v1"}:
category = "core_signal_gap"
elif name in {"benchmark_relative_harness_table", "index_relative_health_table", "entry_freshness_gate_table", "sell_value_preservation_gate_table", "watch_release_checklist"}:
category = "market_gate_gap"
elif name in {"engine_feedback_loop_report", "prediction_evaluation_improvement_report", "performance_readiness_summary"}:
category = "performance_gate_gap"
elif name in {"alpha_lead_table", "anti_distribution_table", "profit_preservation_table", "smart_cash_raise_table", "execution_quality_table", "sell_priority_decision_table"}:
category = "decision_table_gap"
else:
category = "other_gap"
missing_rows.append({
"section": name,
"category": category,
"missing_line_count": line_count,
})
if not missing_rows:
return _kv([
("상태", "DATA_MISSING 섹션 없음"),
("건수", 0),
])
return _tbl(missing_rows, ["category", "section", "missing_line_count"])
# ── 메인 ─────────────────────────────────────────────────────────────────────
def main() -> int:
@@ -1627,6 +1705,7 @@ def main() -> int:
"sell_priority_decision_table": lambda: _sell_priority_decision_table(hctx, se),
"strategy_performance_scoreboard": lambda: _strategy_performance_scoreboard(hctx, se),
"performance_readiness_summary": lambda: _performance_readiness_summary(hctx, se),
"operational_t20_activation_summary": lambda: _operational_t20_activation_summary(hctx, se),
"operational_eval_queue_summary": lambda: _operational_eval_queue_summary(hctx, se),
"outcome_eval_window_monitor": lambda: _outcome_eval_window_monitor(hctx, se),
"decision_trace_table": lambda: _decision_trace_table(hctx, se),
@@ -1657,6 +1736,12 @@ def main() -> int:
md = f"## {title}\n\n<!-- {name} -->\n\n{body}"
sections.append({"name": name, "title": title, "markdown": md})
sections.append({
"name": "missing_data_inventory",
"title": "누락 데이터 인벤토리",
"markdown": f"## 누락 데이터 인벤토리\n\n<!-- missing_data_inventory -->\n\n{_missing_data_inventory_report(sections, se)}",
})
# 섹션 처리 오류 요약을 마지막 섹션으로 추가
if se:
err_rows = ["| 섹션 | 오류 |", "| --- | --- |"]