From e2820065d1be133d3c97891f08d61c1b0972e4f2 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sun, 14 Jun 2026 23:05:47 +0900 Subject: [PATCH] =?UTF-8?q?fix(report):=20=EB=A0=88=ED=8F=AC=ED=8A=B8=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=20=EC=88=98=EC=A4=80=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=E2=80=94=20gate=5Ftrace=20=EC=A0=95=ED=98=95=ED=99=94,=20HTS?= =?UTF-8?q?=ED=91=9C=20=EC=9E=AC=EC=84=A4=EA=B3=84,=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EC=84=B9=EC=85=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _fmt_gate_trace(): 게이트 요약 compact 출력 (손절✓ 상대손절✓ 현금바닥⊘) - _concise_hts_input_sheet: gate_trace 제거, 지정가/매도수량/손절가/TP2가/실행스타일 추가 - _immediate_execution_playbook: 게이트요약 compact, sell_sequence 정형화된 표 - _reference_price_ledger: watch_breakout_gate 중복 fallback 제거, prices_json 기준가 원장 - _sparkline: 데이터 4개 미만 시 데이터부족 표시 - SECTION_TITLES: 내부 formula ID 한국어 명칭으로 통일 - report dict: generated_at/section_errors 추가 (PASS) Co-Authored-By: Claude Sonnet 4.6 --- runtime/refactor_baseline_v1.yaml | 2 +- tools/operational_report_contract.py | 3 + tools/render_operational_report.py | 278 ++++++++++++++++-- tools/update_workbook_sector_insights.py | 152 ++++++++++ ...validate_report_section_completeness_v1.py | 3 + 5 files changed, 411 insertions(+), 27 deletions(-) diff --git a/runtime/refactor_baseline_v1.yaml b/runtime/refactor_baseline_v1.yaml index 195dd39..d7fcb63 100644 --- a/runtime/refactor_baseline_v1.yaml +++ b/runtime/refactor_baseline_v1.yaml @@ -15,5 +15,5 @@ "keep package scripts within release envelope" ] }, - "source_zip_sha256": "06621508ea3846a5c8aba634a3901c105d4d46ed84148d5886d063d624945b6e" + "source_zip_sha256": "49f64b3773ba3c19fa8323d0b08833928c637935483039579bb8ab22a391f70c" } \ No newline at end of file diff --git a/tools/operational_report_contract.py b/tools/operational_report_contract.py index de67243..933af35 100644 --- a/tools/operational_report_contract.py +++ b/tools/operational_report_contract.py @@ -11,6 +11,7 @@ REPORT_SECTION_ORDER = [ "final_execution_decision", "concise_hts_input_sheet", "watch_breakout_gate", + "reference_price_ledger", # PHASE-1: single conclusion + playbook "single_conclusion", "immediate_execution_playbook", @@ -19,6 +20,8 @@ REPORT_SECTION_ORDER = [ "portfolio_sector_exposure_summary", "sector_trend_analysis_v1", "etf_representative_monitor_v1", + "performance_readiness_summary", + "operational_eval_queue_summary", # PHASE-2: quality + readiness scores "investment_quality_headline", "operational_truth_score", diff --git a/tools/render_operational_report.py b/tools/render_operational_report.py index de4fb1e..ed037bd 100644 --- a/tools/render_operational_report.py +++ b/tools/render_operational_report.py @@ -21,7 +21,7 @@ from src.quant_engine.sector_trend_analysis import build_sector_trend_analysis SECTION_ORDER = [ "exec_safety_declaration", "final_judgment_table", "final_execution_decision", - "concise_hts_input_sheet", "watch_breakout_gate", + "concise_hts_input_sheet", "watch_breakout_gate", "reference_price_ledger", "single_conclusion", "immediate_execution_playbook", "market_context_learning_note", "portfolio_performance_summary", "portfolio_sector_exposure_summary", @@ -40,7 +40,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", - "outcome_eval_window_monitor", + "performance_readiness_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", @@ -53,6 +53,7 @@ SECTION_TITLES = { "final_execution_decision": "최종 실행 결정", "concise_hts_input_sheet": "HTS 입력 요약표", "watch_breakout_gate": "투명한 감시 원장 / 돌파 감시 게이트", + "reference_price_ledger": "투명한 감시 원장", "single_conclusion": "단일 결론", "immediate_execution_playbook": "즉시 실행 플레이북", "market_context_learning_note": "시장 컨텍스트 학습 노트", @@ -68,21 +69,21 @@ SECTION_TITLES = { "routing_serving_trace": "라우팅 서빙 추적", "export_gate_diagnosis": "내보내기 게이트 진단", "QEH_AUDIT_BLOCK": "QEH 감사 블록", - "fundamental_quality_gate_v1": "FUNDAMENTAL_QUALITY_GATE_V1", - "horizon_allocation_lock_v1": "HORIZON_ALLOCATION_LOCK_V1", - "smart_money_liquidity_gate_v1": "SMART_MONEY_LIQUIDITY_GATE_V1", - "routing_serving_trace_v2": "ROUTING_SERVING_DECISION_TRACE_V2", - "fundamental_multifactor_v2": "FUNDAMENTAL_MULTI_FACTOR_SCORE_V2", - "earnings_growth_quality_v1": "EARNINGS_GROWTH_QUALITY_GATE_V1", - "market_share_proxy_v1": "MARKET_SHARE_MOMENTUM_PROXY_V1", - "cashflow_stability_v1": "CASHFLOW_STABILITY_GATE_V1", - "routing_decision_explain_v1": "ROUTING_DECISION_EXPLAIN_LOCK_V1", - "benchmark_relative_harness_table": "benchmark_relative_harness_table", - "index_relative_health_table": "index_relative_health_table", - "entry_freshness_gate_table": "entry_freshness_gate_table", - "sell_value_preservation_gate_table": "sell_value_preservation_gate_table", - "watch_release_checklist": "watch_release_checklist", - "alpha_feedback_loop_report": "alpha_feedback_loop_report", + "fundamental_quality_gate_v1": "펀더멘털 품질 게이트", + "horizon_allocation_lock_v1": "지평선 배분 잠금", + "smart_money_liquidity_gate_v1": "스마트머니 유동성 게이트", + "routing_serving_trace_v2": "라우팅 서빙 결정 추적", + "fundamental_multifactor_v2": "펀더멘털 다중팩터 점수", + "earnings_growth_quality_v1": "실적 성장 품질 게이트", + "market_share_proxy_v1": "시장점유율 모멘텀 프록시", + "cashflow_stability_v1": "현금흐름 안정성 게이트", + "routing_decision_explain_v1": "라우팅 결정 설명", + "benchmark_relative_harness_table": "벤치마크 상대 하네스", + "index_relative_health_table": "지수 상대 건강 테이블", + "entry_freshness_gate_table": "진입 신선도 게이트", + "sell_value_preservation_gate_table": "매도 가치 보존 게이트", + "watch_release_checklist": "관찰 해제 체크리스트", + "alpha_feedback_loop_report": "알파 피드백 루프", "backdata_feature_bank_table": "백데이터 특성 원장", "alpha_lead_table": "알파 선행 테이블", "anti_distribution_table": "분산 매도 위험 테이블", @@ -91,6 +92,8 @@ SECTION_TITLES = { "execution_quality_table": "체결 품질 테이블", "sell_priority_decision_table": "매도 우선순위 결정 테이블", "strategy_performance_scoreboard": "전략 성과 스코어보드", + "performance_readiness_summary": "성과 준비도 요약", + "operational_eval_queue_summary": "운영 T+20 대기열 요약", "outcome_eval_window_monitor": "성과 평가 윈도우 모니터", "decision_trace_table": "판단 추적 테이블", "anti_whipsaw_reentry_gate": "반등 재진입 감시 게이트", @@ -170,6 +173,10 @@ def _sparkline(values: list[Any]) -> str: continue if not points: return "n/a" + if len(points) < 4: + def _fp(p: float) -> str: + return f"{p:,.0f}" if abs(p) >= 1000 or p == int(p) else f"{p:.2f}" + return "데이터부족(" + ", ".join(_fp(p) for p in points) + ")" lo = min(points) hi = max(points) bars = "▁▂▃▄▅▆▇█" @@ -182,6 +189,50 @@ def _sparkline(values: list[Any]) -> str: return "".join(out) +def _fmt_gate_trace(trace_raw: Any) -> str: + """gate_trace list → compact readable 게이트 요약 (예: 손절✓ 상대손절✓ 열게이트✓ 현금바닥⊘).""" + if isinstance(trace_raw, str): + try: + trace_raw = json.loads(trace_raw) + except Exception: + return str(trace_raw)[:60] + if not isinstance(trace_raw, list): + return str(trace_raw)[:60] + _labels = { + "STOP_BREACH": "손절", + "RELATIVE_STOP": "상대손절", + "INTRADAY_LOCK": "장중잠금", + "HEAT_GATE": "열게이트", + "MEAN_REVERSION_GATE": "평균회귀", + "CASH_FLOOR": "현금바닥", + "EXIT_POLICY": "청산정책", + } + _icons = { + "PASS": "✓", "FAIL": "✗", "FORCE_EXIT": "✗", + "HARD_BLOCK": "⊘", "INACTIVE": "–", "SKIP": "?", + } + parts = [] + for g in trace_raw: + if not isinstance(g, dict): + continue + gate = g.get("gate", "") + result = g.get("result", "") + label = _labels.get(gate, gate[:4]) + icon = _icons.get(result, result[:2]) + if result == "INACTIVE": + continue + parts.append(f"{label}{icon}") + return " ".join(parts) if parts else "–" + + +def _build_ticker_lookup(hctx: dict, key: str) -> dict: + """hctx에서 key를 JSON 파싱 후 ticker → row dict 매핑 반환.""" + raw = _sj(hctx.get(key, [])) + if not isinstance(raw, list): + return {} + return {row.get("ticker", ""): row for row in raw if isinstance(row, dict)} + + # ── PHASE-0 렌더러 ──────────────────────────────────────────────────────────── def _exec_safety_declaration(hctx: dict, se: list) -> str: @@ -228,7 +279,30 @@ def _concise_hts_input_sheet(hctx: dict, se: list) -> str: items = _sj(hctx.get("decisions_json", [])) if not isinstance(items, list) or not items: return _err(se, "concise_hts_input_sheet", "decisions_json 없음") - return _tbl(items, ["ticker", "name", "final_action", "gate_trace", "rs_verdict"]) + lpp_map = _build_ticker_lookup(hctx, "limit_price_policy_json") + sqj_map = _build_ticker_lookup(hctx, "sell_quantities_json") + pj_map = _build_ticker_lookup(hctx, "prices_json") + rows = [] + for it in items: + ticker = it.get("ticker", "") + lpp = lpp_map.get(ticker, {}) + sqj = sqj_map.get(ticker, {}) + pj = pj_map.get(ticker, {}) + hts_p = lpp.get("hts_limit_price", "") + stop_p = pj.get("stop_price", "") + tp2_p = pj.get("tp2_price", "") + rows.append({ + "ticker": ticker, + "종목명": it.get("name", ""), + "매매구분": it.get("final_action", ""), + "지정가": f"{hts_p:,}" if isinstance(hts_p, (int, float)) and hts_p else hts_p or "DATA_MISSING", + "매도수량": sqj.get("sell_qty", "-"), + "손절가": f"{stop_p:,}" if isinstance(stop_p, (int, float)) and stop_p else stop_p or "DATA_MISSING", + "TP2가": f"{tp2_p:,}" if isinstance(tp2_p, (int, float)) and tp2_p else tp2_p or "-", + "RS판정": it.get("rs_verdict", ""), + "실행스타일": lpp.get("execution_style", "-"), + }) + return _tbl(rows, ["ticker", "종목명", "매매구분", "지정가", "매도수량", "손절가", "TP2가", "RS판정", "실행스타일"]) def _watch_breakout_gate(hctx: dict, se: list) -> str: @@ -247,6 +321,37 @@ def _watch_breakout_gate(hctx: dict, se: list) -> str: return "".join(parts) +def _reference_price_ledger(hctx: dict, se: list) -> str: + ledger = _sj(hctx.get("reference_price_ledger_json", [])) + if isinstance(ledger, list) and ledger: + return _tbl(ledger, _first_keys(ledger)) + # fallback: prices_json로 기준가 원장 표시 + pj = _sj(hctx.get("prices_json", [])) + if not isinstance(pj, list) or not pj: + return "기준가 원장 없음 — 하네스 업데이트 필요" + rows = [] + for row in pj: + if not isinstance(row, dict): + continue + avg = row.get("avg_cost", "") + stop = row.get("stop_price", "") + tp1 = row.get("tp1_price", "") + tp2 = row.get("tp2_price", "") + rows.append({ + "ticker": row.get("ticker", ""), + "종목명": row.get("name", ""), + "평균단가": f"{avg:,}" if isinstance(avg, (int, float)) and avg else avg or "-", + "손절가": f"{stop:,}" if isinstance(stop, (int, float)) and stop else stop or "-", + "TP1가": f"{tp1:,}" if isinstance(tp1, (int, float)) and tp1 else tp1 or "-", + "TP1상태": row.get("tp1_state", ""), + "TP2가": f"{tp2:,}" if isinstance(tp2, (int, float)) and tp2 else tp2 or "-", + "TP2상태": row.get("tp2_state", ""), + "수익률(%)": row.get("profit_pct", ""), + "이익잠금": row.get("profit_lock_stage", ""), + }) + return _tbl(rows, ["ticker", "종목명", "평균단가", "손절가", "TP1가", "TP1상태", "TP2가", "TP2상태", "수익률(%)", "이익잠금"]) + + # ── PHASE-1 렌더러 ──────────────────────────────────────────────────────────── def _single_conclusion(hctx: dict, se: list) -> str: @@ -270,17 +375,42 @@ def _immediate_execution_playbook(hctx: dict, se: list) -> str: plan = _sj(hctx.get("cash_recovery_plan_json", {})) parts = [] if isinstance(items, list) and items: - parts.append("**실행 결정**\n\n" + _tbl(items, ["ticker", "name", "final_action", "gate_trace"])) + exec_rows = [] + for it in items: + exec_rows.append({ + "ticker": it.get("ticker", ""), + "종목명": it.get("name", ""), + "매매구분": it.get("final_action", ""), + "게이트요약": _fmt_gate_trace(it.get("gate_trace", [])), + "RS판정": it.get("rs_verdict", ""), + }) + parts.append("**실행 결정**\n\n" + _tbl(exec_rows, ["ticker", "종목명", "매매구분", "게이트요약", "RS판정"])) else: parts.append(_err(se, "immediate_execution_playbook", "decisions_json 없음")) if isinstance(plan, dict): - sell_seq = plan.get("sell_sequence", "") + sell_seq = _sj(plan.get("sell_sequence", [])) + exp_total = plan.get("expected_total_krw", "") parts.append("\n\n**현금 회수 계획**\n\n" + _kv([ - ("매도 시퀀스", str(sell_seq)[:120]), - ("예상 즉시 회수 (KRW)", plan.get("expected_total_krw", "")), + ("예상 즉시 회수 (KRW)", f"{exp_total:,}" if isinstance(exp_total, (int, float)) else exp_total), ("부족액 충족", plan.get("shortfall_met", "")), ("필요 건수", plan.get("items_needed", "")), ])) + if isinstance(sell_seq, list) and sell_seq: + seq_rows = [] + for item in sell_seq: + if not isinstance(item, dict): + continue + lp = item.get("limit_price", "") + ek = item.get("expected_krw", "") + seq_rows.append({ + "ticker": item.get("ticker", ""), + "종목명": item.get("name", ""), + "수량": item.get("qty", ""), + "지정가": f"{lp:,}" if isinstance(lp, (int, float)) and lp else lp or "-", + "방식": item.get("preserve_style", ""), + "예상회수(KRW)": f"{ek:,}" if isinstance(ek, (int, float)) and ek else ek or "-", + }) + parts.append("\n\n**매도 시퀀스**\n\n" + _tbl(seq_rows, ["ticker", "종목명", "수량", "지정가", "방식", "예상회수(KRW)"])) return "".join(parts) @@ -921,6 +1051,90 @@ def _strategy_performance_scoreboard(hctx: dict, se: list) -> str: return _kv(rows) +def _performance_readiness_summary(hctx: dict, se: list) -> str: + oac_path = ROOT / "Temp" / "operational_alpha_calibration_v2.json" + prb_path = ROOT / "Temp" / "performance_readiness_replay_bridge_v1.json" + prb2_path = ROOT / "Temp" / "performance_readiness_replay_bridge_v2.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 {} + + oac = _load(oac_path) + if not oac: + return _err(se, "performance_readiness_summary", "operational_alpha_calibration_v2.json 없음") + + prb = _load(prb_path) + prb2 = _load(prb2_path) + live = prb.get("live") if isinstance(prb.get("live"), dict) else {} + replay = prb.get("replay_informational") if isinstance(prb.get("replay_informational"), dict) else {} + metrics = oac.get("metrics") if isinstance(oac.get("metrics"), dict) else {} + + rows = [ + ("CHECK_83 게이트", oac.get("gate", "")), + ("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", "")), + ("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", "")), + ("value_damage_pct_avg", metrics.get("value_damage_pct_avg", "")), + ] + if prb: + rows += [ + ("readiness_bridge_gate", prb.get("gate", "")), + ("performance_readiness_score", prb.get("performance_readiness_score", "")), + ("live_t20_count", live.get("t20_count", "")), + ("live_sample_gate", live.get("sample_gate", "")), + ("replay_t20_count", replay.get("t20_count", "")), + ] + if prb2: + rows += [ + ("promotion_rule", prb2.get("promotion_rule", "")), + ("promotion_allowed", prb2.get("promotion_allowed", "")), + ] + rows += [ + ("operational_gate_note", "live T+20가 30건 미만이면 PERFORMANCE_READY로 승격하지 않음"), + ("current_state", "DATA_GATED" if str(oac.get("gate", "")).upper() != "PERFORMANCE_READY" else "READY"), + ] + return _kv(rows) + + +def _operational_eval_queue_summary(hctx: dict, se: list) -> str: + q_path = ROOT / "Temp" / "operational_eval_queue_v1.json" + if not q_path.exists(): + return _err(se, "operational_eval_queue_summary", "operational_eval_queue_v1.json 없음") + try: + q = json.loads(q_path.read_text(encoding="utf-8")) + except Exception: + return _err(se, "operational_eval_queue_summary", "operational_eval_queue_v1.json 파싱 실패") + if not isinstance(q, dict): + return _err(se, "operational_eval_queue_summary", "operational_eval_queue_v1.json 구조 오류") + + metrics = q.get("metrics") if isinstance(q.get("metrics"), dict) else {} + queue = q.get("queue") if isinstance(q.get("queue"), list) else [] + todo = q.get("todo_protocol") if isinstance(q.get("todo_protocol"), list) else [] + rows = [ + ("formula_id", q.get("formula_id", "")), + ("as_of", q.get("as_of", "")), + ("t20_days_threshold", q.get("t20_days_threshold", "")), + ("records_total", metrics.get("records_total", "")), + ("t20_evaluated_count", metrics.get("t20_evaluated_count", "")), + ("t20_due_capture_count", metrics.get("t20_due_capture_count", "")), + ("missing_due_date_count", metrics.get("missing_due_date_count", "")), + ("all_proposals_have_due_dates", q.get("all_proposals_have_due_dates", "")), + ("queue_count", len(queue)), + ] + if todo: + rows.append(("todo_protocol", " / ".join(str(x) for x in todo))) + return _kv(rows) + + def _outcome_eval_window_monitor(hctx: dict, se: list) -> str: oqs = _sj(hctx.get("outcome_quality_score_v1_json", {})) shom = _sj(hctx.get("short_horizon_outcome_monitor_v1_json", {})) @@ -1318,6 +1532,7 @@ def main() -> int: "final_execution_decision": lambda: _final_execution_decision(hctx, se), "concise_hts_input_sheet": lambda: _concise_hts_input_sheet(hctx, se), "watch_breakout_gate": lambda: _watch_breakout_gate(hctx, se), + "reference_price_ledger": lambda: _reference_price_ledger(hctx, se), "single_conclusion": lambda: _single_conclusion(hctx, se), "immediate_execution_playbook": lambda: _immediate_execution_playbook(hctx, se), "market_context_learning_note": lambda: _market_context_learning_note(hctx, se), @@ -1356,6 +1571,8 @@ def main() -> int: "execution_quality_table": lambda: _execution_quality_table(hctx, se), "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_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), "anti_whipsaw_reentry_gate": lambda: _anti_whipsaw_reentry_gate(hctx, se), @@ -1378,6 +1595,11 @@ def main() -> int: md = render_fn() except Exception as exc: md = _err(se, name, f"렌더링 예외: {exc}") + if not str(md).lstrip().startswith(f"## {title}"): + body = str(md).lstrip() + if body.startswith("## "): + body = body.split("\n\n", 1)[1] if "\n\n" in body else "" + md = f"## {title}\n\n\n\n{body}" sections.append({"name": name, "title": title, "markdown": md}) # 섹션 처리 오류 요약을 마지막 섹션으로 추가 @@ -1391,6 +1613,8 @@ def main() -> int: }) _section_names = {s.get("name", "") for s in sections} + expected_order = [name for name in SECTION_ORDER] + actual_order = [s.get("name", "") for s in sections if s.get("name", "") != "section_processing_errors"] _eg = _sj(hctx.get("export_gate_json", {})) _json_vs = _eg.get("json_validation_status", "PENDING_EXPORT") if isinstance(_eg, dict) else "PENDING_EXPORT" report = { @@ -1398,11 +1622,15 @@ def main() -> int: "generated_at": datetime.now(timezone.utc).isoformat(), "source_json": data_path.name, "section_count": len(sections), - "section_error_count": len(se), "section_errors": se, "summary": { + "found_settlement": "final_execution_decision" in _section_names, + "found_heat": "watch_breakout_gate" in _section_names, "found_routing": "routing_serving_trace_v2" in _section_names, "found_qeh": "QEH_AUDIT_BLOCK" in _section_names, + "found_concise_hts_input_sheet": "concise_hts_input_sheet" in _section_names, + "found_reference_price_ledger": "reference_price_ledger" in _section_names, + "canonical_order_ok": actual_order[:len(expected_order)] == expected_order, "found_outcome_eval_window": "outcome_eval_window_monitor" in _section_names, "json_validation_status": _json_vs, }, @@ -1416,9 +1644,7 @@ def main() -> int: md_lines = ["# Operational Investment Report\n"] for s in sections: - section_name = s.get("name", "") - section_title = s.get("title", section_name) - md_lines.append(f"## {section_name} - {section_title}\n\n{s.get('markdown', '')}\n") + md_lines.append(str(s.get("markdown", "")).rstrip() + "\n") out_md.write_text("\n".join(md_lines), encoding="utf-8") Path(args.improvement_harness_json).write_text( diff --git a/tools/update_workbook_sector_insights.py b/tools/update_workbook_sector_insights.py index aa6e786..e793263 100644 --- a/tools/update_workbook_sector_insights.py +++ b/tools/update_workbook_sector_insights.py @@ -16,6 +16,10 @@ OUTPUT_DIR = ROOT / "outputs" / "sector_insights_enhanced" OUTPUT_XLSX = OUTPUT_DIR / "GatherTradingData_sector_insights.xlsx" SECTOR_JSON = ROOT / "Temp" / "sector_trend_analysis_v1.json" ETF_JSON = ROOT / "Temp" / "etf_representative_monitor_v1.json" +READINESS_JSON = ROOT / "Temp" / "operational_alpha_calibration_v2.json" +READINESS_BRIDGE_JSON = ROOT / "Temp" / "performance_readiness_replay_bridge_v1.json" +READINESS_BRIDGE_V2_JSON = ROOT / "Temp" / "performance_readiness_replay_bridge_v2.json" +EVAL_QUEUE_JSON = ROOT / "Temp" / "operational_eval_queue_v1.json" HEADER_FILL = PatternFill("solid", fgColor="1F4E78") @@ -236,6 +240,148 @@ def build_portfolio_summary(wb) -> None: set_col_widths(ws, {"A": 22, "B": 18, "C": 18, "D": 24, "E": 18, "F": 18, "G": 26, "H": 26, "I": 18, "J": 18}) +def build_performance_readiness_summary(wb) -> None: + def display(value): + return value if value not in (None, "") else "DATA_MISSING — 하네스 업데이트 필요" + + readiness = load_json(READINESS_JSON) if READINESS_JSON.exists() else {} + bridge = load_json(READINESS_BRIDGE_JSON) if READINESS_BRIDGE_JSON.exists() else {} + bridge_v2 = load_json(READINESS_BRIDGE_V2_JSON) if READINESS_BRIDGE_V2_JSON.exists() else {} + live = bridge.get("live", {}) if isinstance(bridge.get("live"), dict) else {} + replay = bridge.get("replay_informational", {}) if isinstance(bridge.get("replay_informational"), dict) else {} + metrics = readiness.get("metrics", {}) if isinstance(readiness.get("metrics"), dict) else {} + + ws = wb.create_sheet("performance_readiness_summary") + style_sheet(ws) + style_title( + ws, + "성과 준비도 요약", + "CHECK_83의 live T+20 데이터 게이트와 replay 브리지를 함께 보여주는 상태 시트", + end_col=10, + ) + + items = [ + ("check_83_gate", display(readiness.get("gate"))), + ("confidence_score", display(readiness.get("confidence_score"))), + ("performance_ready", display(readiness.get("performance_ready"))), + ("readiness_reasons", display(", ".join(readiness.get("readiness_reasons", [])) if isinstance(readiness.get("readiness_reasons"), list) else readiness.get("readiness_reasons"))), + ("outcome_quality_score", display(metrics.get("outcome_quality_score"))), + ("t20_operational_sample", display(metrics.get("t20_operational_sample"))), + ("t5_operational_pass_rate", display(metrics.get("t5_operational_pass_rate"))), + ("value_damage_pct_avg", display(metrics.get("value_damage_pct_avg"))), + ("live_t20_count", display(live.get("t20_count"))), + ("live_sample_gate", display(live.get("sample_gate"))), + ] + add_kpi_block(ws, 4, items) + + ws["D4"] = "Readiness rule" + ws["D4"].fill = SUBHEADER_FILL + ws["D4"].font = BOLD_FONT + ws["D5"] = "live T+20가 30건 미만이면 PERFORMANCE_READY로 승격하지 않습니다." + ws["D6"] = f"현재 상태: {readiness.get('gate', 'MISSING')}" + ws["D7"] = f"브리지 승격 규칙: {bridge_v2.get('promotion_rule', 'DATA_MISSING — 하네스 업데이트 필요')}" + ws["D8"] = f"브리지 승격 가능: {bridge_v2.get('promotion_allowed', 'DATA_MISSING — 하네스 업데이트 필요')}" + + ws["G4"] = "Replay reference" + ws["G4"].fill = SUBHEADER_FILL + ws["G4"].font = BOLD_FONT + ws["G5"] = f"replay_t20_count: {display(replay.get('t20_count'))}" + ws["G6"] = f"replay_t20_pass_rate_pct: {display(replay.get('t20_pass_rate_pct'))}" + ws["G7"] = f"replay_t20_avg_return_pct: {display(replay.get('t20_avg_return_pct'))}" + ws["G8"] = f"replay_note: {display(replay.get('note'))}" + + readiness_rows = [ + ["metric", "count"], + ["live_t20_count", live.get("t20_count") or 0], + ["required_live_t20_count", 30], + ["replay_t20_count", replay.get("t20_count") or 0], + ] + write_table(ws, 4, 10, readiness_rows[0], readiness_rows[1:]) + + readiness_chart = BarChart() + readiness_chart.type = "bar" + readiness_chart.style = 10 + readiness_chart.title = "T20 Readiness vs Threshold" + readiness_chart.y_axis.title = "Metric" + readiness_chart.x_axis.title = "Count" + readiness_chart.height = 6.5 + readiness_chart.width = 11 + readiness_data = Reference(ws, min_col=11, min_row=4, max_row=7) + readiness_cats = Reference(ws, min_col=10, min_row=5, max_row=7) + readiness_chart.add_data(readiness_data, titles_from_data=True) + readiness_chart.set_categories(readiness_cats) + readiness_chart.legend = None + ws.add_chart(readiness_chart, "J13") + + set_col_widths(ws, {"A": 24, "B": 18, "C": 18, "D": 28, "E": 18, "F": 18, "G": 26, "H": 26, "I": 18, "J": 20, "K": 16}) + + +def build_operational_eval_queue_summary(wb) -> None: + def display(value): + return value if value not in (None, "") else "DATA_MISSING — 하네스 업데이트 필요" + + queue_data = load_json(EVAL_QUEUE_JSON) if EVAL_QUEUE_JSON.exists() else {} + metrics = queue_data.get("metrics", {}) if isinstance(queue_data.get("metrics"), dict) else {} + queue_rows = queue_data.get("queue", []) if isinstance(queue_data.get("queue"), list) else [] + todo = queue_data.get("todo_protocol", []) if isinstance(queue_data.get("todo_protocol"), list) else [] + + ws = wb.create_sheet("operational_eval_queue_summary") + style_sheet(ws) + style_title( + ws, + "운영 T+20 대기열 요약", + "T+20 실제 결과 입력 대기 상태와 처리 프로토콜을 한 장에 정리", + end_col=10, + ) + + items = [ + ("formula_id", display(queue_data.get("formula_id"))), + ("as_of", display(queue_data.get("as_of"))), + ("t20_days_threshold", display(queue_data.get("t20_days_threshold"))), + ("records_total", display(metrics.get("records_total"))), + ("t20_evaluated_count", display(metrics.get("t20_evaluated_count"))), + ("t20_due_capture_count", display(metrics.get("t20_due_capture_count"))), + ("missing_due_date_count", display(metrics.get("missing_due_date_count"))), + ("all_proposals_have_due_dates", display(queue_data.get("all_proposals_have_due_dates"))), + ("queue_count", display(len(queue_rows))), + ("todo_count", display(len(todo))), + ] + add_kpi_block(ws, 4, items) + + ws["D4"] = "Queue protocol" + ws["D4"].fill = SUBHEADER_FILL + ws["D4"].font = BOLD_FONT + for idx, line in enumerate(todo[:5], start=5): + ws.cell(idx, 4).value = line + + ws["G4"] = "Queue status" + ws["G4"].fill = SUBHEADER_FILL + ws["G4"].font = BOLD_FONT + ws["G5"] = f"current_queue_rows: {display(len(queue_rows))}" + ws["G6"] = f"t20_due_capture_count: {display(metrics.get('t20_due_capture_count'))}" + ws["G7"] = f"missing_due_date_count: {display(metrics.get('missing_due_date_count'))}" + + queue_table_rows = [["metric", "value"], ["records_total", metrics.get("records_total") or 0], ["t20_evaluated_count", metrics.get("t20_evaluated_count") or 0], ["t20_due_capture_count", metrics.get("t20_due_capture_count") or 0]] + write_table(ws, 17, 1, queue_table_rows[0], queue_table_rows[1:]) + + chart = BarChart() + chart.type = "bar" + chart.style = 10 + chart.title = "T20 Queue Status" + chart.y_axis.title = "Metric" + chart.x_axis.title = "Count" + chart.height = 6 + chart.width = 11 + data_ref = Reference(ws, min_col=2, min_row=17, max_row=20) + cats = Reference(ws, min_col=1, min_row=18, max_row=20) + chart.add_data(data_ref, titles_from_data=True) + chart.set_categories(cats) + chart.legend = None + ws.add_chart(chart, "J17") + + set_col_widths(ws, {"A": 24, "B": 18, "C": 18, "D": 40, "E": 18, "F": 18, "G": 28, "H": 28, "I": 18, "J": 20, "K": 16}) + + def build_portfolio_sector_exposure(wb) -> None: daily_headers, daily_rows = extract_sheet_rows(wb, "daily_history") account_headers, account_rows = extract_sheet_rows(wb, "account_snapshot") @@ -773,6 +919,8 @@ def main() -> None: wb = load_workbook(INPUT_XLSX) for name in [ "portfolio_performance_summary", + "performance_readiness_summary", + "operational_eval_queue_summary", "portfolio_sector_exposure", "_portfolio_holdings_helper", "sector_trend_summary", @@ -785,6 +933,8 @@ def main() -> None: # Build data sheets first so summary sheets can reference the timeline sheet. build_portfolio_summary(wb) + build_performance_readiness_summary(wb) + build_operational_eval_queue_summary(wb) build_portfolio_sector_exposure(wb) build_sector_timeline(wb, sector, raw_source) build_sector_analysis(wb, sector) @@ -796,6 +946,8 @@ def main() -> None: order = [ "settings", "portfolio_performance_summary", + "performance_readiness_summary", + "operational_eval_queue_summary", "portfolio_sector_exposure", "sector_trend_summary", "sector_trend_analysis", diff --git a/tools/validate_report_section_completeness_v1.py b/tools/validate_report_section_completeness_v1.py index 0d9d8b4..9f2b94f 100644 --- a/tools/validate_report_section_completeness_v1.py +++ b/tools/validate_report_section_completeness_v1.py @@ -17,11 +17,14 @@ ROOT = Path(__file__).resolve().parents[1] REPORT_SECTION_ORDER = [ "exec_safety_declaration", "final_judgment_table", "final_execution_decision", "concise_hts_input_sheet", "watch_breakout_gate", + "reference_price_ledger", "single_conclusion", "immediate_execution_playbook", "market_context_learning_note", "portfolio_performance_summary", "portfolio_sector_exposure_summary", "sector_trend_analysis_v1", "etf_representative_monitor_v1", + "performance_readiness_summary", + "operational_eval_queue_summary", "investment_quality_headline", "operational_truth_score", "execution_readiness_matrix", "pass_100_criteria", "today_decision_summary_card", "routing_serving_trace",