fix(report): 레포트 프로 수준 개선 — gate_trace 정형화, HTS표 재설계, 중복섹션 제거

- _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 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 23:05:47 +09:00
parent 20f0973f74
commit e2820065d1
5 changed files with 411 additions and 27 deletions
+1 -1
View File
@@ -15,5 +15,5 @@
"keep package scripts within release envelope"
]
},
"source_zip_sha256": "06621508ea3846a5c8aba634a3901c105d4d46ed84148d5886d063d624945b6e"
"source_zip_sha256": "49f64b3773ba3c19fa8323d0b08833928c637935483039579bb8ab22a391f70c"
}
+3
View File
@@ -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",
+252 -26
View File
@@ -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<!-- {name} -->\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(
+152
View File
@@ -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",
@@ -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",