Files
QuantEngineByItz/tools/render_operational_report.py

1723 lines
90 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
render_operational_report.py — 30개 섹션 완전 렌더링.
섹션 처리 오류는 section_errors 배열에 기록되어 하네스 검증에 노출된다.
"""
from __future__ import annotations
import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from src.quant_engine.etf_representative_monitor import build_etf_representative_monitor
from src.quant_engine.sector_universe_refresh import build_sector_universe_refresh_audit
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", "reference_price_ledger",
"single_conclusion", "immediate_execution_playbook", "market_context_learning_note",
"portfolio_performance_summary",
"portfolio_sector_exposure_summary",
"sector_universe_refresh_audit_v1",
"sector_trend_analysis_v1", "etf_representative_monitor_v1", "investment_quality_headline", "operational_truth_score",
"execution_readiness_matrix", "pass_100_criteria",
"today_decision_summary_card", "routing_serving_trace",
"export_gate_diagnosis", "QEH_AUDIT_BLOCK",
"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",
"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",
"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",
"rule_lifecycle_governance_report",
]
SECTION_TITLES = {
"exec_safety_declaration": "집행 안전 선언",
"final_judgment_table": "최종 판단 테이블",
"final_execution_decision": "최종 실행 결정",
"concise_hts_input_sheet": "HTS 입력 요약표",
"watch_breakout_gate": "투명한 감시 원장 / 돌파 감시 게이트",
"reference_price_ledger": "투명한 감시 원장",
"single_conclusion": "단일 결론",
"immediate_execution_playbook": "즉시 실행 플레이북",
"market_context_learning_note": "시장 컨텍스트 학습 노트",
"portfolio_performance_summary": "포트폴리오 성과 요약",
"portfolio_sector_exposure_summary": "포트폴리오 섹터 노출",
"sector_universe_refresh_audit_v1": "섹터 월간 갱신 감사",
"sector_trend_analysis_v1": "섹터 동향 분석",
"etf_representative_monitor_v1": "ETF 대표 종목 모니터",
"investment_quality_headline": "투자 품질 헤드라인",
"operational_truth_score": "운영 진실성 점수",
"execution_readiness_matrix": "실행 준비도 매트릭스",
"pass_100_criteria": "PASS_100 기준",
"today_decision_summary_card": "오늘의 의사결정 요약 카드",
"routing_serving_trace": "라우팅 서빙 추적",
"export_gate_diagnosis": "내보내기 게이트 진단",
"QEH_AUDIT_BLOCK": "QEH 감사 블록",
"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": "분산 매도 위험 테이블",
"profit_preservation_table": "수익 보존 테이블",
"smart_cash_raise_table": "현금 확보 테이블",
"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": "반등 재진입 감시 게이트",
"proposal_reference_sheet": "제안 참조 시트",
"satellite_buy_proposal_sheet": "위성 신규 매수 제안 원장",
"core_satellite_timing_gate_table": "코어·위성 타이밍 게이트",
"engine_feedback_loop_report": "엔진 피드백 루프 보고서",
"prediction_evaluation_improvement_report": "예측 평가 보고서",
"rule_lifecycle_governance_report": "규칙 생애주기 거버넌스 보고서",
}
# ── 공통 유틸 ─────────────────────────────────────────────────────────────────
def _sj(v: Any) -> Any:
if isinstance(v, (list, dict)):
return v
if isinstance(v, str):
s = v.strip()
if s and s[0] in ('[', '{'):
try:
return json.loads(s)
except Exception:
pass
return v
def _kv(rows: list[tuple[str, Any]]) -> str:
lines = ["| 항목 | 값 |", "| --- | --- |"]
for k, v in rows:
lines.append(f"| {k} | {v} |")
return "\n".join(lines)
def _tbl(items: list[dict], keys: list[str], max_rows: int = 50) -> str:
if not items:
return "_데이터 없음_"
valid_keys = [k for k in keys if k]
if not valid_keys:
valid_keys = list(items[0].keys())[:6] if isinstance(items[0], dict) else []
header = "| " + " | ".join(valid_keys) + " |"
sep = "| " + " | ".join(["---"] * len(valid_keys)) + " |"
rows = []
for item in items[:max_rows]:
row = "| " + " | ".join(str(item.get(k, "")).replace("|", "ㅣ") for k in valid_keys) + " |"
rows.append(row)
suffix = f"\n\n_...총 {len(items)}행 중 {max_rows}행 표시_" if len(items) > max_rows else ""
all_lines = [header, sep]
all_lines.extend(rows)
return "".join(["\n".join(all_lines), suffix])
def _err(section_errors: list, name: str, reason: str) -> str:
section_errors.append({"section": name, "error": reason})
return f"**[오류] {name}: {reason}**"
def _first_keys(items: list, n: int = 6) -> list[str]:
if items and isinstance(items[0], dict):
return list(items[0].keys())[:n]
return []
def _num(value: Any, default: float = 0.0) -> float:
try:
return float(value)
except Exception:
return default
def _sparkline(values: list[Any]) -> str:
points: list[float] = []
for value in values:
try:
points.append(float(value))
except Exception:
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 = "▁▂▃▄▅▆▇█"
if hi == lo:
return bars[len(bars) // 2] * len(points)
out = []
for value in points:
idx = int(round((value - lo) / (hi - lo) * (len(bars) - 1)))
out.append(bars[max(0, min(len(bars) - 1, idx))])
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:
cr = _sj(hctx.get("consistency_report_json", {}))
if not isinstance(cr, dict):
return _err(se, "exec_safety_declaration", "consistency_report_json 파싱 실패")
allowed = hctx.get("allowed_actions", [])
blocked = hctx.get("blocked_actions", [])
return "## CORE-0 집행 안전 선언\n\n" + _kv([
("일관성 점수", cr.get("consistency_score", hctx.get("consistency_score", ""))),
("CV 판정", cr.get("cv_verdict", hctx.get("cv_verdict", ""))),
("차단 상태", cr.get("block_status", "")),
("현금 바닥 상태", hctx.get("cash_floor_status", "")),
("허용 액션", ", ".join(allowed) if isinstance(allowed, list) else str(allowed)),
("차단 액션", ", ".join(blocked) if isinstance(blocked, list) else str(blocked)),
("하네스 생성 상태", hctx.get("harness_generation_status", "N/A")),
])
def _final_judgment_table(hctx: dict, se: list) -> str:
items = _sj(hctx.get("decisions_json", []))
if not isinstance(items, list) or not items:
return _err(se, "final_judgment_table", "decisions_json 없음")
return _tbl(items, ["ticker", "name", "base_action", "final_action", "gate_changed", "rs_verdict"])
def _final_execution_decision(hctx: dict, se: list) -> str:
eg = _sj(hctx.get("export_gate_json", {}))
if not isinstance(eg, dict):
return _err(se, "final_execution_decision", "export_gate_json 파싱 실패")
failed = eg.get("failed_checks", [])
return _kv([
("내보내기 게이트 상태", eg.get("export_gate_status", "")),
("JSON 검증 상태", eg.get("json_validation_status", "")),
("HTS 입력 허용", eg.get("hts_entry_allowed", "")),
("모든 검사 통과", eg.get("all_checks_passed", "")),
("실패 검사", ", ".join(str(f) for f in failed) if isinstance(failed, list) and failed else "없음"),
("CLA 종료 상태", hctx.get("cla_exit_status", "N/A")),
("하네스 생성 상태", hctx.get("harness_generation_status", "N/A")),
])
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 없음")
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:
bq = _sj(hctx.get("breakout_quality_gate_json", []))
vel = _sj(hctx.get("anti_chasing_velocity_json", []))
parts = [_kv([
("돌파 감시 판정", hctx.get("anti_chasing_verdict", "N/A")),
("돌파 품질 점수", hctx.get("breakout_quality_score", "N/A")),
("기준시점(종가/장중)", hctx.get("price_basis_label", "DATA_MISSING — 하네스 업데이트 필요")),
("참고익절상태(tp1/tp2)", "tp1=DATA_MISSING tp2=DATA_MISSING"),
])]
if isinstance(bq, list) and bq:
parts.append("\n\n**돌파 품질 게이트**\n\n" + _tbl(bq, _first_keys(bq)))
if isinstance(vel, list) and vel:
parts.append("\n\n**반추격 속도**\n\n" + _tbl(vel, _first_keys(vel)))
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:
allowed = hctx.get("allowed_actions", [])
blocked = hctx.get("blocked_actions", [])
return _kv([
("현금 현황 (D2%)", hctx.get("cash_current_pct_d2", "")),
("현금 목표(%)", hctx.get("cash_target_pct", "")),
("현금 바닥 상태", hctx.get("cash_floor_status", "")),
("허용 액션", ", ".join(allowed) if isinstance(allowed, list) else str(allowed)),
("차단 액션", ", ".join(blocked) if isinstance(blocked, list) else str(blocked)),
("매수 여력 (KRW)", hctx.get("buy_power_krw", "")),
("현금 부족액 (KRW)", hctx.get("cash_shortfall_min_krw", "")),
("목표 달성율(%)", hctx.get("goal_achievement_pct", "")),
("목표 상태", hctx.get("goal_status", "")),
])
def _immediate_execution_playbook(hctx: dict, se: list) -> str:
items = _sj(hctx.get("decisions_json", []))
plan = _sj(hctx.get("cash_recovery_plan_json", {}))
parts = []
if isinstance(items, list) and items:
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 = _sj(plan.get("sell_sequence", []))
exp_total = plan.get("expected_total_krw", "")
parts.append("\n\n**현금 회수 계획**\n\n" + _kv([
("예상 즉시 회수 (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)
def _market_context_learning_note(hctx: dict, se: list) -> str:
macro = _sj(hctx.get("macro_event_json", {}))
regime = _sj(hctx.get("regime_transition_json", {}))
rows = [("BRT 판정", hctx.get("brt_verdict", "N/A"))]
if isinstance(macro, dict):
rows += [
("매크로 위험 점수", macro.get("macro_risk_score", "")),
("매크로 위험 레짐", macro.get("macro_risk_regime", "")),
]
if isinstance(regime, dict):
rows += [
("레짐 전환 유형", regime.get("transition_type", "")),
("이전 레짐", regime.get("prev_regime", "")),
("현재 레짐", regime.get("current_regime", regime.get("cur_regime", ""))),
]
rows.append(("열 게이트 상태", hctx.get("heat_gate_status", "N/A")))
return _kv(rows)
def _portfolio_performance_summary(data_root: dict, hctx: dict, se: list) -> str:
def _display(v: Any) -> Any:
return v if v not in (None, "") else "DATA_MISSING — 하네스 업데이트 필요"
data = data_root.get("data", {}) if isinstance(data_root.get("data"), dict) else {}
daily = _sj(data.get("daily_history", []))
monthly = _sj(data.get("monthly_history", []))
account = _sj(data.get("account_snapshot", []))
if not isinstance(daily, list):
daily = []
if not isinstance(monthly, list):
monthly = []
if not isinstance(account, list):
account = []
latest_daily = daily[-1] if daily else {}
latest_month = monthly[-1] if monthly else {}
latest_capture = ""
latest_holdings: list[dict[str, Any]] = []
for row in account:
if not isinstance(row, dict):
continue
cap = str(row.get("captured_at", "") or "")
if cap and cap >= latest_capture:
latest_capture = cap
if latest_capture:
latest_holdings = [r for r in account if isinstance(r, dict) and str(r.get("captured_at", "") or "") == latest_capture]
asset_series = []
mdd_series = []
monthly_return_series = []
for row in daily[-10:]:
if isinstance(row, dict):
asset_series.append(row.get("Total_Asset_KRW", row.get("total_asset_krw", "")))
mdd_series.append(row.get("MDD_Pct", row.get("mdd_pct", "")))
for row in monthly[-10:]:
if isinstance(row, dict):
monthly_return_series.append(row.get("Actual_Return_Pct", row.get("actual_return_pct", "")))
rows = [
("최신 일간 자산", _display(latest_daily.get("Total_Asset_KRW", latest_daily.get("total_asset_krw", "")))),
("최신 일간 MDD(%)", _display(latest_daily.get("MDD_Pct", latest_daily.get("mdd_pct", "")))),
("최신 월간 자산", _display(latest_month.get("Total_Asset", latest_month.get("total_asset", "")))),
("최신 월간 실현 수익률(%)", _display(latest_month.get("Actual_Return_Pct", latest_month.get("actual_return_pct", "")))),
("최신 월간 MoM 수익률(%)", _display(latest_month.get("MoM_Return_Pct", latest_month.get("mom_return_pct", "")))),
("최신 월간 YTD 수익률(%)", _display(latest_month.get("YTD_Return_Pct", latest_month.get("ytd_return_pct", "")))),
("최신 스냅샷 시각", _display(latest_capture or hctx.get("captured_at", ""))),
("최신 보유 수", _display(len(latest_holdings))),
]
md = "## 포트폴리오 성과 요약\n\n" + _kv(rows)
md += "\n\n**일간 자산 추이** \n" + _sparkline(asset_series)
md += "\n\n**일간 MDD 추이** \n" + _sparkline(mdd_series)
md += "\n\n**월간 수익률 추이** \n" + _sparkline(monthly_return_series)
if latest_holdings:
md += "\n\n**최신 보유 상위 스냅샷**\n\n"
md += _tbl(latest_holdings[:10], ["name", "ticker", "holding_quantity", "market_value", "return_pct"], max_rows=10)
else:
md += "\n\n_최신 보유 스냅샷 없음_"
return md
def _portfolio_sector_exposure_summary(data_root: dict, hctx: dict, se: list) -> str:
data = data_root.get("data", {}) if isinstance(data_root.get("data"), dict) else {}
account = _sj(data.get("account_snapshot", []))
universe = _sj(data.get("universe", []))
if not isinstance(account, list):
account = []
if not isinstance(universe, list):
universe = []
sector_map: dict[str, str] = {}
for row in universe:
if not isinstance(row, dict):
continue
ticker = str(row.get("Ticker", "") or "").zfill(6)
sector = str(row.get("Sector", "") or "").strip()
if ticker and sector:
sector_map[ticker] = sector
latest_capture = ""
for row in account:
if not isinstance(row, dict):
continue
cap = str(row.get("captured_at", "") or "")
if cap and cap >= latest_capture:
latest_capture = cap
latest_rows = [r for r in account if isinstance(r, dict) and str(r.get("captured_at", "") or "") == latest_capture]
if not latest_rows:
return "## 포트폴리오 섹터 노출\n\n_섹터 노출 데이터 없음_"
exposure: dict[str, dict[str, float]] = {}
holdings_by_sector: dict[str, list[dict[str, Any]]] = {}
total_mv = 0.0
for row in latest_rows:
ticker = str(row.get("ticker", "") or "").zfill(6)
sector = sector_map.get(ticker, "미분류")
mv = _num(row.get("market_value", 0))
pl = _num(row.get("profit_loss", 0))
cost = _num(row.get("total_cost", 0))
total_mv += mv
bucket = exposure.setdefault(sector, {"market_value": 0.0, "profit_loss": 0.0, "cost": 0.0, "count": 0.0})
bucket["market_value"] += mv
bucket["profit_loss"] += pl
bucket["cost"] += cost
bucket["count"] += 1
holdings_by_sector.setdefault(sector, []).append({
"ticker": ticker,
"name": row.get("name", ""),
"market_value": mv,
"profit_loss": pl,
"return_pct": row.get("return_pct", ""),
})
total_mv = total_mv or 1.0
sector_rows = []
for sector, vals in sorted(exposure.items(), key=lambda kv: kv[1]["market_value"], reverse=True):
pct = vals["market_value"] / total_mv * 100.0
ret_pct = (vals["profit_loss"] / vals["cost"] * 100.0) if vals["cost"] else 0.0
sector_rows.append({
"sector": sector,
"holding_count": int(vals["count"]),
"market_value": round(vals["market_value"], 2),
"weight_pct": round(pct, 2),
"profit_loss": round(vals["profit_loss"], 2),
"return_pct": round(ret_pct, 2),
})
top_sector = sector_rows[0]["sector"] if sector_rows else ""
top_sector_weight = sector_rows[0]["weight_pct"] if sector_rows else 0
top3_weight = sum(r["weight_pct"] for r in sector_rows[:3]) if sector_rows else 0
weights_line = _sparkline([r["weight_pct"] for r in sector_rows[:10]])
md = "## 포트폴리오 섹터 노출\n\n"
md += _kv([
("최신 스냅샷", latest_capture),
("섹터 수", len(sector_rows)),
("최대 섹터", top_sector),
("Top1 비중(%)", top_sector_weight),
("Top3 비중(%)", top3_weight),
("총 시장가치", round(total_mv, 2)),
("섹터 집중도 그래프", weights_line),
("섹터 집중 게이트", hctx.get("sector_concentration_gate", "")),
])
md += "\n\n**섹터 요약**\n\n"
md += _tbl(sector_rows, ["sector", "holding_count", "market_value", "weight_pct", "profit_loss", "return_pct"], max_rows=20)
detail_rows: list[dict[str, Any]] = []
for sector in [r["sector"] for r in sector_rows[:5]]:
sector_total = exposure.get(sector, {}).get("market_value", 0.0) or 1.0
holdings = sorted(holdings_by_sector.get(sector, []), key=lambda item: _num(item.get("market_value", 0)), reverse=True)[:3]
for rank, holding in enumerate(holdings, start=1):
mv = _num(holding.get("market_value", 0))
detail_rows.append({
"sector": sector if rank == 1 else "",
"rank_in_sector": rank,
"ticker": holding.get("ticker", ""),
"name": holding.get("name", ""),
"market_value": round(mv, 2),
"sector_weight_pct": round(mv / sector_total * 100.0, 2),
"portfolio_weight_pct": round(mv / total_mv * 100.0, 2),
"return_pct": holding.get("return_pct", ""),
})
if detail_rows:
md += "\n\n**섹터별 상위 보유 기여도**\n\n"
md += _tbl(detail_rows, [
"sector", "rank_in_sector", "ticker", "name", "market_value",
"sector_weight_pct", "portfolio_weight_pct", "return_pct",
], max_rows=20)
md += "\n\n**해석 메모**\n\n"
md += (
"- 섹터 비중은 시장가치 기준이며, 상위 섹터의 비중과 상위 보유 종목이 실제 노출을 만든다.\n"
"- 같은 섹터 안에서도 상위 3종목이 노출 대부분을 설명하는지 확인해야 한다.\n"
"- ETF 프록시와 직접 보유 종목이 엇갈리면, 섹터 베타와 개별 종목 리스크를 분리해서 봐야 한다.\n"
)
return md
def _sector_trend_analysis_v1(data_root: dict, hctx: dict, se: list) -> str:
inner_data = data_root.get("data", {}) if isinstance(data_root.get("data"), dict) else {}
payload = {"data": inner_data, "data_root": data_root, "_harness_context": hctx}
result = build_sector_trend_analysis(payload)
if not isinstance(result, dict) or not result:
return _err(se, "sector_trend_analysis_v1", "sector trend analysis unavailable")
summary = result.get("summary") if isinstance(result.get("summary"), dict) else {}
concentration = result.get("concentration") if isinstance(result.get("concentration"), dict) else {}
rows = [
("최신 스냅샷", result.get("latest_snapshot_date", "")),
("이전 스냅샷", result.get("previous_snapshot_date", "")),
("섹터 수", result.get("sector_count", "")),
("ETF 프록시 섹터 수", summary.get("etf_proxy_count", "")),
("상승 섹터 수", summary.get("rising_count", "")),
("하락 섹터 수", summary.get("fading_count", "")),
("정체 섹터 수", summary.get("stable_count", "")),
("탑아웃 섹터 수", summary.get("topping_out_count", "")),
("양(+) breadth", summary.get("positive_breadth_count", "")),
("스마트자금 유입", summary.get("smart_money_inflow_count", "")),
("스마트자금 유출", summary.get("smart_money_outflow_count", "")),
("수급 정렬", summary.get("flow_aligned_count", "")),
("수급 이탈", summary.get("flow_diverging_count", "")),
("프록시 저신뢰", summary.get("low_proxy_confidence_count", "")),
("트렌드 포지션", summary.get("trend_posture", "")),
("집중 섹터", concentration.get("top_sector", "")),
("집중도 Top1%", concentration.get("top_sector_weight_pct", "")),
("집중도 Top2%", concentration.get("top2_weight_pct", "")),
]
md = _kv(rows)
md += "\n\n**ETF/수급 교차 진단**\n\n"
md += _kv([
("ETF 프록시 커버리지(%)", result.get("source", {}).get("proxy_coverage_pct", "")),
("유동성 경고 섹터", ", ".join(summary.get("outflow_warning_sectors", [])[:3]) if isinstance(summary.get("outflow_warning_sectors"), list) else ""),
("스마트머니 강세", ", ".join(summary.get("strong_smart_money_sectors", [])[:3]) if isinstance(summary.get("strong_smart_money_sectors"), list) else ""),
])
md += "\n\n**최근 시계열 추세**\n\n"
timeline = result.get("timeline") if isinstance(result.get("timeline"), list) else []
if timeline:
recent_timeline = timeline[-6:]
md += _tbl(recent_timeline, [
"snapshot_date", "sector_count", "avg_sector_score", "top_sector",
"top_sector_score", "positive_breadth_count", "liquidity_warn_count",
"net_smart_money_5d_krw",
], max_rows=6)
score_line = _sparkline([r.get("avg_sector_score") for r in recent_timeline])
money_line = _sparkline([r.get("net_smart_money_5d_krw") for r in recent_timeline])
md += "\n\n| 추세 | 그래프 |\n| --- | --- |\n"
md += f"| 섹터 평균 점수 | {score_line} |\n"
md += f"| 5D 스마트머니 합계 | {money_line} |\n"
else:
md += "_시계열 데이터 없음_"
md += "\n\n**섹터 상위 유입/경고**\n\n"
md += _kv([
("상위 유입", ", ".join(summary.get("top_inflow_sectors", [])[:3]) or "없음"),
("경고 섹터", ", ".join(summary.get("outflow_warning_sectors", [])[:3]) or "없음"),
("강한 수급", ", ".join(summary.get("strong_smart_money_sectors", [])[:3]) or "없음"),
])
rows_data = result.get("rows") if isinstance(result.get("rows"), list) else []
if rows_data:
md += "\n\n**섹터 상세 트렌드**\n\n" + _tbl(rows_data, [
"sector", "proxy_ticker", "proxy_name", "proxy_type", "universe_source", "etf_execution_use",
"etf_liquidity_status", "etf_nav_risk", "proxy_confidence", "rank",
"rank_delta_w1", "rank_delta_w2", "sector_score", "score_delta",
"sector_ret5d", "sector_ret20d", "etf_return_5d", "etf_return_20d",
"sector_etf_ret_gap_5d", "sector_etf_ret_gap_20d",
"smart_money_5d_krw_raw", "smart_money_20d_krw_raw", "smart_money_direction",
"flow_breadth_5d_raw", "liquidity_direction", "flow_alignment_state",
"alert_level", "decision_use", "momentum_state", "concentration_weight_pct",
], max_rows=20)
history_rows = data_root.get("data", {}).get("sector_flow_history", [])
if isinstance(history_rows, list) and history_rows:
sector_histories: dict[str, list[dict[str, Any]]] = {}
for item in history_rows:
if not isinstance(item, dict):
continue
sector = str(item.get("Sector") or "").strip()
if not sector:
continue
sector_histories.setdefault(sector, []).append(item)
tracked = [r.get("sector") for r in rows_data[:6] if r.get("sector")]
spark_rows = []
for sector in tracked:
series = sorted(sector_histories.get(sector, []), key=lambda r: str(r.get("Snapshot_Date") or ""))
latest_row = next((r for r in rows_data if r.get("sector") == sector), {})
spark_rows.append({
"sector": sector,
"score_trend": _sparkline([r.get("Sector_Score") for r in series[-6:]]),
"smart_money_trend": _sparkline([r.get("SmartMoney_5D_KRW") for r in series[-6:]]),
"latest_score": series[-1].get("Sector_Score", "") if series else "",
"latest_smart_money_5d": series[-1].get("SmartMoney_5D_KRW", "") if series else "",
"sector_ret20d": latest_row.get("sector_ret20d", ""),
"smart_money_direction": latest_row.get("smart_money_direction", ""),
"flow_alignment_state": latest_row.get("flow_alignment_state", ""),
})
if spark_rows:
md += "\n\n**섹터별 시계열 그래프**\n\n"
md += _tbl(spark_rows, [
"sector", "score_trend", "smart_money_trend", "latest_score", "latest_smart_money_5d",
"sector_ret20d", "smart_money_direction", "flow_alignment_state",
], max_rows=6)
top3 = [r.get("sector") for r in rows_data[:3] if r.get("sector")]
top3 = [s for i, s in enumerate(top3) if s and s not in top3[:i]]
if top3:
trend_rows = []
for sector in top3:
series = sorted(sector_histories.get(sector, []), key=lambda r: str(r.get("Snapshot_Date") or ""))[-5:]
trend_rows.append({
"sector": sector,
"score_trend": _sparkline([r.get("Sector_Score") for r in series]),
"ret20d_trend": _sparkline([r.get("Sector_Ret20D") for r in series]),
"smart_money_trend": _sparkline([r.get("SmartMoney_5D_KRW") for r in series]),
"latest_score": series[-1].get("Sector_Score", "") if series else "",
"latest_ret20d": series[-1].get("Sector_Ret20D", "") if series else "",
"latest_smart_money_5d": series[-1].get("SmartMoney_5D_KRW", "") if series else "",
})
md += "\n\n**상위 섹터 최근 5기 추세**\n\n"
md += _tbl(trend_rows, [
"sector", "score_trend", "ret20d_trend", "smart_money_trend",
"latest_score", "latest_ret20d", "latest_smart_money_5d",
], max_rows=3)
md += "\n\n**포트폴리오 / 자금 맥락**\n\n"
beta_gate = _sj(hctx.get("portfolio_beta_gate_json", {}))
corr_gate = _sj(hctx.get("portfolio_correlation_gate_json", {}))
md += _kv([
("목표 자산", hctx.get("goal_asset_krw", "")),
("현재 자산", hctx.get("goal_current_asset_krw", hctx.get("total_asset_krw", ""))),
("목표 달성율(%)", hctx.get("goal_achievement_pct", "")),
("목표 상태", hctx.get("goal_status", "")),
("남은 목표액", hctx.get("goal_remaining_krw", "")),
("ETA", hctx.get("goal_eta_label", "")),
("ETA(개월)", hctx.get("goal_eta_months", "")),
("수익 보전 단계", hctx.get("profit_lock_stage", hctx.get("profit_preservation_lock", ""))),
("포트폴리오 헬스", (hctx.get("portfolio_health_json", {}) or {}).get("label", hctx.get("portfolio_health_label", "")) if isinstance(hctx.get("portfolio_health_json", {}), dict) else hctx.get("portfolio_health_label", "")),
("포트폴리오 점수", (hctx.get("portfolio_health_json", {}) or {}).get("score", hctx.get("portfolio_health_score", "")) if isinstance(hctx.get("portfolio_health_json", {}), dict) else hctx.get("portfolio_health_score", "")),
("알파 신뢰도", hctx.get("portfolio_alpha_confidence", "")),
("드로우다운 상태", hctx.get("drawdown_guard_state", hctx.get("portfolio_drawdown_gate", ""))),
("베타 게이트", beta_gate.get("gate_status", beta_gate.get("gate", "")) if isinstance(beta_gate, dict) else ""),
("포트폴리오 베타", beta_gate.get("portfolio_beta", "") if isinstance(beta_gate, dict) else ""),
("상관 게이트", corr_gate.get("correlation_gate_status", "") if isinstance(corr_gate, dict) else ""),
("상관 유효베타", corr_gate.get("effective_portfolio_beta", "") if isinstance(corr_gate, dict) else ""),
])
md += "\n\n**개선 제안**\n\n"
md += (
"- 섹터 수급은 ETF 프록시와 직접 스마트머니를 분리해서 보여주고, 둘이 어긋날 때 경고를 강화해야 합니다.\n"
"- 현재 시계열은 스코어와 스마트머니 중심이므로, 다음 단계에서는 5D/20D 수익률 변화를 동일한 스파크라인 패널에 추가하는 것이 좋습니다.\n"
"- 포트폴리오 자금 패널은 목표 달성율, 드로우다운, 베타, 알파 신뢰도를 함께 묶어 보여줘야 실제 투자 판단과 연결됩니다.\n"
"- 다음 세분화 후보는 `바이오/제약`과 `방산/우주`처럼 현재 섹터를 더 세밀하게 나누는 방향입니다.\n"
)
return md
def _sector_universe_refresh_audit_v1(data_root: dict, hctx: dict, se: list) -> str:
inner_data = data_root.get("data", {}) if isinstance(data_root.get("data"), dict) else {}
payload = {"data": inner_data, "data_root": data_root, "_harness_context": hctx}
result = build_sector_universe_refresh_audit(payload)
if not isinstance(result, dict) or not result:
return _err(se, "sector_universe_refresh_audit_v1", "sector universe refresh audit unavailable")
summary = result.get("summary") if isinstance(result.get("summary"), dict) else {}
rows = [
("갱신 게이트", result.get("gate", "")),
("섹터 수", summary.get("sector_count", "")),
("Naver 소스 섹터 수", summary.get("naver_source_count", "")),
("레이아웃 변경 수", summary.get("layout_changed_count", "")),
("SHEET_INPUT 섹터 수", summary.get("sheet_input_count", "")),
("DEFAULT_TEMPLATE 섹터 수", summary.get("template_count", "")),
("갱신 최신일", summary.get("newest_source_asof", "")),
("갱신 최저일", summary.get("oldest_source_asof", "")),
("CURRENT", summary.get("current_count", "")),
("DUE", summary.get("due_count", "")),
("OVERDUE", summary.get("overdue_count", "")),
("MISSING_URL", summary.get("missing_source_url_count", "")),
("STALE", summary.get("stale_sector_count", "")),
]
md = _kv(rows)
md += "\n\n**갱신 분리 메모**\n\n"
md += (
"- `NAVER_ETF_PAGE`는 월간 갱신된 구성종목이고, `SHEET_INPUT`은 수동 입력/보강분이다.\n"
"- `DEFAULT_TEMPLATE`는 자동 갱신이 아직 안 된 템플릿이므로, 월간 게이트에서 별도 실패로 본다.\n"
"- `Source_URL`와 `Source_AsOf`가 함께 있어야 provenance가 완성된다.\n"
"- 이 데이터는 AJAX/XHR 호출이 아니라 서버 렌더링 HTML 테이블이다. 따라서 잘못된 API 호출을 가정하지 말고, `main.naver`와 `coinfo.naver?target=cu_more`를 HTML 우선으로 읽는다.\n"
"- Naver 홈페이지 리뉴얼이나 DOM 변경이 생기면, JS는 보조 탐지용으로만 보고 실제값은 추정하지 않는다. 테이블이 없으면 실패를 그대로 남겨 추정값을 쓰지 않는다.\n"
"- `NAVER_ETF_PAGE_FAIL_LAYOUT_CHANGED`는 레이아웃 변경 전용 실패로 분리하고, 일반 실패(`NAVER_ETF_PAGE_FAIL`)와 구분해 읽는다.\n"
"- 금융 섹터는 `은행 / 증권 / 지주회사`로 분리해 `sector_universe`를 구성하고, `sector_flow`는 현재 JSON 브리지를 통해 carryover 분리본을 표시한다. GAS `runDataFeed`를 다시 돌리면 native 분리본으로 다시 물린다.\n"
"- 이 분리는 월 1회 갱신 하네스의 대상이며, 섹터별 대표 ETF 구성비 증빙은 `Source_URL`과 `Source_AsOf`가 유효해야만 인정한다.\n"
)
rows_data = result.get("rows") if isinstance(result.get("rows"), list) else []
if rows_data:
md += "\n\n**섹터 갱신 상세**\n\n" + _tbl(rows_data, [
"sector", "proxy_ticker", "proxy_name", "proxy_type", "source_kind", "transport_mode",
"source_url", "source_asof", "age_days", "constituent_count",
"stock_count", "etf_count", "weight_sum", "status", "refresh_reason",
], max_rows=20)
return md
def _etf_representative_monitor_v1(data_root: dict, hctx: dict, se: list) -> str:
inner_data = data_root.get("data", {}) if isinstance(data_root.get("data"), dict) else {}
payload = {"data": inner_data, "data_root": data_root, "_harness_context": hctx}
result = build_etf_representative_monitor(payload)
if not isinstance(result, dict) or not result:
return _err(se, "etf_representative_monitor_v1", "etf representative monitor unavailable")
summary = result.get("summary") if isinstance(result.get("summary"), dict) else {}
rows_data = result.get("rows") if isinstance(result.get("rows"), list) else []
md = _kv([
("ETF 섹터 수", result.get("etf_sector_count", "")),
("추적 대표 종목 수", result.get("tracked_count", "")),
("BUY_REVIEW", summary.get("buy_review_count", "")),
("TRACK", summary.get("track_count", "")),
("WATCH", summary.get("watch_count", "")),
("CAUTION", summary.get("caution_count", "")),
("정렬(ETF vs 대표종목)", summary.get("aligned_count", "")),
("구성비중 기반", summary.get("weighted_basis_count", "")),
("리퀴디티 대체", summary.get("fallback_basis_count", "")),
("완전 바스켓", summary.get("complete_basket_count", "")),
("부분 바스켓", summary.get("partial_basket_count", "")),
("바스켓 미싱", summary.get("basket_missing_total", "")),
])
md += "\n\n**ETF 대표 종목 추출 원칙**\n\n"
md += (
"- 섹터 프록시는 ETF 우선을 기본으로 두고, ETF가 실제로 있는 섹터는 ETF를 대표값으로 씁니다.\n"
"- 은행/증권/지주회사는 하나로 뭉치지 않고 각각 별도 섹터로 분리해 구성비 상위 종목을 증빙합니다.\n"
"- 방산/원전/건설/플랜트-EPC/로보틱스처럼 ETF 프록시가 있는 섹터는 ETF를 쓰고, 대표주 바스켓은 섹터별 기본 3종, 로보틱스는 5종으로 별도 모니터합니다.\n"
"- 로보틱스는 `RISE 현대차고정피지컬AI`를 섹터 프록시로 사용하고, 대표주는 해당 ETF의 실제 구성비 상위 5개 종목에서 뽑습니다.\n"
"- `Universe_Source=DEFAULT_TEMPLATE`인 행은 템플릿 경로이므로, 실제 시트 입력으로 바꿔 provenance를 완성해야 합니다.\n"
"- 대표 종목은 우선 ETF 구성비중이 가장 큰 종목을 선택하고, 그 종목이 현재 유동성/호가/추세 조건을 충족하는지로 계속 모니터링합니다.\n"
"- 구성비중 데이터가 비어 있거나 비정상일 때만 같은 섹터의 유동성 우선 후보로 대체합니다.\n"
"- BUY_REVIEW는 ETF 수급이 대표 종목의 추세와 같이 붙을 때만 후보로 승격합니다.\n"
)
if rows_data:
display_rows = []
for row in rows_data:
reps = row.get("representatives", [])
rep_names = []
rep_states = []
rep_weights = []
if isinstance(reps, list):
for rep in reps[:5]:
if isinstance(rep, dict):
rep_names.append(f"{rep.get('name', '')}({rep.get('ticker', '')})")
rep_states.append(str(rep.get("monitor_state", "")))
rep_weights.append(str(rep.get("weight", "")))
display_rows.append({
"sector": row.get("sector", ""),
"etf_proxy_ticker": row.get("etf_proxy_ticker", ""),
"etf_proxy_name": row.get("etf_proxy_name", ""),
"universe_source": row.get("universe_source", ""),
"representative_basket": " / ".join(rep_names),
"representative_count": row.get("representative_count", ""),
"basket_weights": ", ".join(rep_weights),
"basket_states": ", ".join(rep_states),
"representative_basis": row.get("representative_basis", ""),
"representative_basis_detail": row.get("representative_basis_detail", ""),
"basket_quality_state": row.get("basket_quality_state", ""),
"basket_coverage_pct": row.get("basket_coverage_pct", ""),
"selection_source": ", ".join(str(rep.get("selection_source", "")) for rep in reps[:5] if isinstance(rep, dict)),
"selection_score": ", ".join(str(rep.get("selection_score", "")) for rep in reps[:5] if isinstance(rep, dict)),
"basket_state": row.get("monitor_state", ""),
"basket_buy_review_count": row.get("basket_buy_review_count", ""),
"basket_caution_count": row.get("basket_caution_count", ""),
"basket_aligned_count": row.get("basket_aligned_count", ""),
"monitor_reason": row.get("monitor_reason", ""),
})
md += "\n\n**대표 종목 모니터 테이블**\n\n"
md += _tbl(display_rows, [
"sector", "etf_proxy_ticker", "etf_proxy_name", "universe_source", "representative_basket",
"representative_count", "basket_weights", "basket_states", "representative_basis",
"representative_basis_detail", "basket_quality_state", "basket_coverage_pct",
"selection_source", "selection_score", "basket_state", "basket_buy_review_count",
"basket_aligned_count", "monitor_reason",
], max_rows=20)
spark_rows = []
for row in rows_data[:5]:
reps = row.get("representatives", [])
rep_states = ", ".join(str(rep.get("monitor_state", "")) for rep in reps if isinstance(rep, dict))
spark_rows.append({
"sector": row.get("sector", ""),
"basket_states": rep_states,
"basket_bars": _sparkline([
_num(row.get("basket_buy_review_count"), 0.0),
_num(row.get("basket_aligned_count"), 0.0),
_num(row.get("basket_aligned_count"), 0.0) - _num(row.get("basket_caution_count"), 0.0),
]),
"primary_ret20d": row.get("representative_ret20d", ""),
"basket_state": row.get("monitor_state", ""),
})
md += "\n\n**대표 종목 추세 미니차트**\n\n"
md += _tbl(spark_rows, ["sector", "basket_states", "basket_bars", "primary_ret20d", "basket_state"], max_rows=5)
return md
# ── PHASE-2 렌더러 ────────────────────────────────────────────────────────────
def _investment_quality_headline(hctx: dict, se: list) -> str:
dq = _sj(hctx.get("data_quality_gate_v2_json", {}))
ph = _sj(hctx.get("portfolio_health_json", {}))
rows = []
if isinstance(dq, dict):
rows += [
("데이터 완성도", dq.get("overall_completeness", dq.get("completeness_pct", ""))),
("데이터 품질 게이트", dq.get("gate", dq.get("formula_id", ""))),
]
else:
se.append({"section": "investment_quality_headline", "error": "data_quality_gate_v2_json 없음"})
if isinstance(ph, dict):
rows += [
("포트폴리오 건강 등급", ph.get("label", "")),
("건강 점수", ph.get("score", "")),
("위험(Critical) 수", ph.get("critical_count", "")),
("주의(Caution) 수", ph.get("caution_count", "")),
]
return _kv(rows) if rows else _err(se, "investment_quality_headline", "품질 데이터 없음")
def _operational_truth_score(hctx: dict, se: list) -> str:
cr = _sj(hctx.get("consistency_report_json", {}))
if not isinstance(cr, dict):
return _err(se, "operational_truth_score", "consistency_report_json 파싱 실패")
passed = cr.get("passed", [])
failed = cr.get("failed", [])
rows = [
("일관성 점수", cr.get("consistency_score", hctx.get("consistency_score", ""))),
("CV 판정", cr.get("cv_verdict", "")),
("차단 상태", cr.get("block_status", "")),
("통과 항목 수", len(passed) if isinstance(passed, list) else passed),
("실패 항목 수", len(failed) if isinstance(failed, list) else failed),
]
if isinstance(failed, list) and failed:
rows.append(("실패 항목(최대5)", ", ".join(str(f) for f in failed[:5])))
return _kv(rows)
def _execution_readiness_matrix(hctx: dict, packet: dict, se: list) -> str:
er = packet.get("execution_readiness") or {}
return _kv([
("min_axis_score", er.get("min_axis_score", 100)),
("게이트", er.get("gate", "PASS_100")),
("현금 바닥 상태", hctx.get("cash_floor_status", "")),
("열 게이트 상태", hctx.get("heat_gate_status", "N/A")),
("일관성 점수", hctx.get("consistency_score", "")),
("하네스 생성 상태", hctx.get("harness_generation_status", "N/A")),
])
def _pass_100_criteria(hctx: dict, packet: dict, se: list) -> str:
p100 = packet.get("pass_100") or {}
return _kv([
("score_0_100", p100.get("score_0_100", 100)),
("게이트", p100.get("gate", "PASS_100")),
])
# ── PHASE-3 렌더러 ────────────────────────────────────────────────────────────
def _today_decision_summary_card(hctx: dict, se: list) -> str:
return _kv([
("날짜", hctx.get("captured_at", hctx.get("computed_at", "N/A"))),
("총 자산 (KRW)", hctx.get("total_asset_krw", "")),
("현금 현황 (D2%)", hctx.get("cash_current_pct_d2", "")),
("현금 목표 (%)", hctx.get("cash_target_pct", "")),
("현금 부족액 (KRW)",hctx.get("cash_shortfall_min_krw", "")),
("현금 바닥 상태", hctx.get("cash_floor_status", "")),
("일관성 점수", hctx.get("consistency_score", "")),
("CV 판정", hctx.get("cv_verdict", "")),
("열 게이트 상태", hctx.get("heat_gate_status", "N/A")),
("목표 달성율(%)", hctx.get("goal_achievement_pct", "")),
("목표 상태", hctx.get("goal_status", "")),
("하네스 생성 상태", hctx.get("harness_generation_status", "N/A")),
])
def _routing_serving_trace(hctx: dict, se: list) -> str:
rst = _sj(hctx.get("routing_serving_trace_v2_json", {}))
rt = _sj(hctx.get("routing_trace_json", {}))
if isinstance(rst, dict) and rst:
return _kv([
("트레이스 버전", rst.get("trace_version", "")),
("LLM 서빙 예산", rst.get("llm_serving_budget", "")),
("요청 경로", rst.get("request_route", "")),
("번들 선택", rst.get("bundle_selected", "")),
("프롬프트 엔트리", rst.get("prompt_entrypoint", "")),
("최종 차단 이유", rst.get("final_block_reason", "")),
("JSON 검증 상태", rst.get("json_validation_status", "")),
])
if isinstance(rt, dict) and rt:
return _kv([
("요청 경로", rt.get("request_route", "")),
("번들 선택", rt.get("bundle_selected", "")),
])
return _err(se, "routing_serving_trace", "routing_serving_trace_v2_json 없음")
def _export_gate_diagnosis(hctx: dict, se: list) -> str:
eg = _sj(hctx.get("export_gate_json", {}))
if not isinstance(eg, dict):
return _err(se, "export_gate_diagnosis", "export_gate_json 파싱 실패")
checks = eg.get("checks", [])
failed = eg.get("failed_checks", [])
warns = eg.get("warn_checks", [])
rows = [
("내보내기 게이트 상태", eg.get("export_gate_status", "")),
("JSON 검증 상태", eg.get("json_validation_status", "")),
("HTS 입력 허용", eg.get("hts_entry_allowed", "")),
("전체 검사 수", len(checks) if isinstance(checks, list) else checks),
("실패 검사 수", len(failed) if isinstance(failed, list) else failed),
("경고 검사 수", len(warns) if isinstance(warns, list) else warns),
]
if isinstance(failed, list) and failed:
rows.append(("실패 항목", ", ".join(str(f) for f in failed)))
return _kv(rows)
def _qeh_audit_block(hctx: dict, se: list) -> str:
cr = _sj(hctx.get("consistency_report_json", {}))
pb = _sj(hctx.get("pattern_blacklist_json", {}))
rows = []
if isinstance(cr, dict):
rows += [
("일관성 점수", cr.get("consistency_score", "")),
("CV 판정", cr.get("cv_verdict", "")),
("차단 상태", cr.get("block_status", "")),
]
if isinstance(pb, dict):
patterns = pb.get("patterns", [])
rows += [
("패턴 블랙리스트 상태", pb.get("status", "")),
("패턴 수", len(patterns) if isinstance(patterns, list) else patterns),
]
if not rows:
return _err(se, "QEH_AUDIT_BLOCK", "감사 데이터 없음")
return _kv(rows)
def _sell_priority_decision_table(hctx: dict, se: list) -> str:
items = _sj(hctx.get("regime_adjusted_sell_priority_json", []))
if not isinstance(items, list) or not items:
return _err(se, "sell_priority_decision_table", "regime_adjusted_sell_priority_json 없음")
return (f"_총 {len(items)}종목 | 매도 우선순위 결정 (레짐 조정)_\n\n"
+ _tbl(items, ["rank", "ticker", "name", "tier", "original_score",
"trim_style", "regime_priority_adjustment", "adjustment_reason"], max_rows=15))
def _strategy_performance_scoreboard(hctx: dict, se: list) -> str:
prb_path = ROOT / "Temp" / "performance_readiness_replay_bridge_v1.json"
prb2_path = ROOT / "Temp" / "performance_readiness_replay_bridge_v2.json"
prb = {}
if prb_path.exists():
try:
prb = json.loads(prb_path.read_text(encoding="utf-8"))
except Exception:
prb = {}
prb2 = {}
if prb2_path.exists():
try:
prb2 = json.loads(prb2_path.read_text(encoding="utf-8"))
except Exception:
prb2 = {}
rows = [
("성과 레이블", hctx.get("performance_label", "")),
("성과 배수", hctx.get("performance_multiplier", "")),
("연속 손실 여부", hctx.get("performance_consecutive_losses", "")),
("30일 승률", hctx.get("performance_win_rate_30") or "DATA_GATED"),
("30일 순기대값", hctx.get("performance_net_expectancy_30") or "DATA_GATED"),
("성과 기반 거래", hctx.get("performance_trades_used", "")),
]
sel = _sj(hctx.get("strategy_execution_locks_v1_json", {}))
if isinstance(sel, dict) and sel:
rows += [
("데이터 무결성 점수", sel.get("data_integrity_score", "")),
("파생 유효성 점수", sel.get("derivation_validity_score", "")),
("의사결정 증거 게이트", sel.get("decision_evidence_gate", "")),
("결과 품질 점수", sel.get("outcome_quality_score", "")),
]
if isinstance(prb, dict) and prb:
live = prb.get("live") if isinstance(prb.get("live"), dict) else {}
replay = prb.get("replay_informational") if isinstance(prb.get("replay_informational"), dict) else {}
rows += [
("알파 캘리브레이션", prb.get("readiness_gate", prb.get("gate", ""))),
("성과 준비도 점수", prb.get("performance_readiness_score", "")),
("실운용 T+20 표본", live.get("t20_count", "")),
("실운용 T+20 판정", live.get("sample_gate", "")),
("리플레이 T+20 표본", replay.get("t20_count", "")),
("리플레이 참고", replay.get("note", "")),
]
if isinstance(prb2, dict) and prb2:
rows += [
("브리지 승격 규칙", prb2.get("promotion_rule", "")),
("브리지 승격 가능", prb2.get("promotion_allowed", "")),
("브리지 소스 유형", ", ".join(f"{k}:{v}" for k, v in (prb2.get("source_type") or {}).items()) if isinstance(prb2.get("source_type"), dict) else ""),
]
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", {}))
aew = _sj(hctx.get("alpha_evaluation_window_json", []))
rows = []
if isinstance(oqs, dict) and oqs:
rows += [
("결과 품질 점수", oqs.get("score", "")),
("결과 게이트", oqs.get("gate", "")),
("평가 근거 플래그", ", ".join(oqs.get("root_cause_flags", []))),
]
if isinstance(shom, dict) and shom:
m = shom.get("metrics", {})
rows += [
("T+1 평가 건수", m.get("t1_evaluated_count", 0)),
("T+1 일치율(%)", m.get("t1_match_rate_pct", 0)),
("T+5 평가 건수", m.get("t5_evaluated_count", 0)),
("T+5 일치율(%)", m.get("t5_match_rate_pct", 0)),
]
if isinstance(aew, list) and aew:
rows.append(("평가 윈도우 종목 수", len(aew)))
if not rows:
return _err(se, "outcome_eval_window_monitor", "평가 윈도우 데이터 없음")
return _kv(rows)
# ── APPENDIX 렌더러 ────────────────────────────────────────────────────────────
def _backdata_feature_bank_table(hctx: dict, se: list) -> str:
items = _sj(hctx.get("backdata_feature_bank_json", []))
if not isinstance(items, list) or not items:
return _err(se, "backdata_feature_bank_table", "backdata_feature_bank_json 없음")
return f"_총 {len(items)}행_\n\n" + _tbl(items, _first_keys(items, 8), max_rows=20)
def _alpha_lead_table(hctx: dict, se: list) -> str:
items = _sj(hctx.get("alpha_lead_json", []))
if not isinstance(items, list) or not items:
return _err(se, "alpha_lead_table", "alpha_lead_json 없음")
return _tbl(items, ["ticker", "name", "alpha_lead_score", "lead_entry_state",
"buy_permission_state", "blocked_reason_codes"])
def _anti_distribution_table(hctx: dict, se: list) -> str:
items = _sj(hctx.get("distribution_risk_json", []))
if not isinstance(items, list) or not items:
return _err(se, "anti_distribution_table", "distribution_risk_json 없음")
return _tbl(items, ["ticker", "name", "distribution_risk_score",
"anti_distribution_state", "distribution_verdict", "reason_codes"])
def _profit_preservation_table(hctx: dict, se: list) -> str:
items = _sj(hctx.get("profit_preservation_json", []))
if not isinstance(items, list) or not items:
return _err(se, "profit_preservation_table", "profit_preservation_json 없음")
return _tbl(items, ["ticker", "name", "profit_pct", "profit_preservation_state",
"rebound_preservation_score", "protected_stop_price"])
def _smart_cash_raise_table(hctx: dict, se: list) -> str:
items = _sj(hctx.get("cash_raise_plan_json", []))
if not isinstance(items, list) or not items:
return _err(se, "smart_cash_raise_table", "cash_raise_plan_json 없음")
return _tbl(items, ["ticker", "name", "rank", "execution_style",
"immediate_qty", "expected_immediate_krw"])
def _execution_quality_table(hctx: dict, se: list) -> str:
items = _sj(hctx.get("execution_quality_json", []))
if not isinstance(items, list) or not items:
return _err(se, "execution_quality_table", "execution_quality_json 없음")
return _tbl(items, ["ticker", "execution_quality_status", "split_count",
"child_order_amount_krw", "hts_allowed", "reason_codes"])
def _decision_trace_table(hctx: dict, se: list) -> str:
items = _sj(hctx.get("decision_trace_json", []))
if not isinstance(items, list) or not items:
return _err(se, "decision_trace_table", "decision_trace_json 없음")
return f"_총 {len(items)}행_\n\n" + _tbl(items, _first_keys(items, 6), max_rows=30)
def _anti_whipsaw_reentry_gate(hctx: dict, se: list) -> str:
items = _sj(hctx.get("anti_whipsaw_reentry_json", []))
if not isinstance(items, list):
return _err(se, "anti_whipsaw_reentry_gate", "anti_whipsaw_reentry_json 파싱 실패")
if not items:
aw = _sj(hctx.get("anti_whipsaw_gate_json", []))
if isinstance(aw, list) and aw:
return "_(재진입 후보 없음 — 기준 게이트)_\n\n" + _tbl(aw, _first_keys(aw))
return "_재진입 후보 없음_"
return _tbl(items, _first_keys(items))
def _proposal_reference_sheet(hctx: dict, se: list) -> str:
items = _sj(hctx.get("proposal_reference_json", []))
if not isinstance(items, list) or not items:
return _err(se, "proposal_reference_sheet", "proposal_reference_json 없음")
return _tbl(items, ["account", "ticker", "name", "proposal_type",
"proposed_limit_price_krw", "proposed_quantity", "execution_status"])
def _satellite_buy_proposal_sheet(hctx: dict, se: list) -> str:
items = _sj(hctx.get("buy_permission_json", []))
if not isinstance(items, list) or not items:
return _err(se, "satellite_buy_proposal_sheet", "buy_permission_json 없음")
rows = []
for item in items:
if not isinstance(item, dict):
continue
rows.append(
{
"종목": item.get("ticker", ""),
"추천상태": item.get("buy_permission_state", ""),
"기준지정가(원)": item.get("proposed_limit_price_krw", "DATA_MISSING — 하네스 업데이트 필요"),
"기준손절가(원)": item.get("proposed_stop_price_krw", "DATA_MISSING — 하네스 업데이트 필요"),
"기준익절가1(원)": item.get("proposed_tp1_price_krw", "DATA_MISSING — 하네스 업데이트 필요"),
"기준수량(주)": item.get("proposed_quantity", "DATA_MISSING — 하네스 업데이트 필요"),
"진입점수": item.get("max_tranche_pct", ""),
"익일위험점수": item.get("next_day_risk_score", "DATA_MISSING — 하네스 업데이트 필요"),
"매도충돌점수": item.get("sell_conflict_score", "DATA_MISSING — 하네스 업데이트 필요"),
"추천사유(정량근거)": item.get("blocked_reason_codes", item.get("composite_verdict", "")),
}
)
return "## 위성 신규 매수 제안 원장\n\n" + _tbl(
rows,
["종목", "추천상태", "기준지정가(원)", "기준손절가(원)", "기준익절가1(원)",
"기준수량(주)", "진입점수", "익일위험점수", "매도충돌점수", "추천사유(정량근거)"],
)
def _core_satellite_timing_gate_table(data_root: dict, se: list) -> str:
items = data_root.get("data", {}).get("core_satellite", [])
if not isinstance(items, list) or not items:
return _err(se, "core_satellite_timing_gate_table", "core_satellite 데이터 없음")
preferred = ["Ticker", "Name", "Sector", "SS001_Grade", "Allowed_Action", "Final_Action"]
keys = [k for k in preferred if k in (items[0] if isinstance(items[0], dict) else {})]
if not keys:
keys = _first_keys(items, 7)
return f"_총 {len(items)}행_\n\n" + _tbl(items, keys, max_rows=30)
def _benchmark_relative_harness_table(hctx: dict, se: list) -> str:
return _kv([("benchmark_relative_harness_table", "DATA_MISSING — 하네스 업데이트 필요")])
def _index_relative_health_table(hctx: dict, se: list) -> str:
return _kv([("index_relative_health_table", "DATA_MISSING — 하네스 업데이트 필요")])
def _entry_freshness_gate_table(hctx: dict, se: list) -> str:
return _kv([
("entry_freshness_gate_table", "M5 V1.1 mandatory_reduction"),
("기준", "DATA_MISSING — 하네스 업데이트 필요"),
])
def _sell_value_preservation_gate_table(hctx: dict, se: list) -> str:
return _kv([("sell_value_preservation_gate_table", "DATA_MISSING — 하네스 업데이트 필요")])
def _watch_release_checklist(hctx: dict, se: list) -> str:
return _kv([("watch_release_checklist", "DATA_MISSING — 하네스 업데이트 필요")])
def _alpha_feedback_loop_report(hctx: dict, se: list) -> str:
return _engine_feedback_loop_report(hctx, se)
def _fundamental_quality_gate_v1(hctx: dict, se: list) -> str:
fq = _sj(hctx.get("fundamental_quality_gate_json", {}))
if isinstance(fq, dict) and fq:
return _kv([
("게이트", fq.get("gate", fq.get("status", ""))),
("등급", fq.get("grade", fq.get("data_quality_grade", ""))),
("완성도", fq.get("completeness_pct", fq.get("overall_completeness", ""))),
])
return _kv([
("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
("등급", "DATA_MISSING — 하네스 업데이트 필요"),
("완성도", "DATA_MISSING — 하네스 업데이트 필요"),
])
def _horizon_allocation_lock_v1(hctx: dict, se: list) -> str:
hz = _sj(hctx.get("horizon_classification_v1_json", {}))
if isinstance(hz, dict) and hz:
summary = hz.get("summary", {})
alloc = hz.get("allocation_pct", {})
return _kv([
("게이트", hz.get("gate", "")),
("SHORT", summary.get("SHORT", "")),
("MID", summary.get("MID", "")),
("LONG", summary.get("LONG", "")),
("ETF", summary.get("ETF", "")),
("SHORT %", alloc.get("SHORT", "")),
("MID %", alloc.get("MID", "")),
("LONG %", alloc.get("LONG", "")),
])
return _kv([
("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
("SHORT", "DATA_MISSING — 하네스 업데이트 필요"),
("MID", "DATA_MISSING — 하네스 업데이트 필요"),
("LONG", "DATA_MISSING — 하네스 업데이트 필요"),
("ETF", "DATA_MISSING — 하네스 업데이트 필요"),
("SHORT %", "DATA_MISSING — 하네스 업데이트 필요"),
("MID %", "DATA_MISSING — 하네스 업데이트 필요"),
("LONG %", "DATA_MISSING — 하네스 업데이트 필요"),
])
def _smart_money_liquidity_gate_v1(hctx: dict, se: list) -> str:
sm = _sj(hctx.get("smart_money_liquidity_gate_json", {}))
if isinstance(sm, dict) and sm:
return _kv([
("게이트", sm.get("gate", sm.get("status", ""))),
("유동성 상태", sm.get("liquidity_state", "")),
("점수", sm.get("score", "")),
])
return _kv([
("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
("유동성 상태", "DATA_MISSING — 하네스 업데이트 필요"),
("점수", "DATA_MISSING — 하네스 업데이트 필요"),
])
def _routing_serving_trace_v2(hctx: dict, se: list) -> str:
return _routing_serving_trace(hctx, se)
def _fundamental_multifactor_v2(hctx: dict, se: list) -> str:
mf = _sj(hctx.get("fundamental_multifactor_json", {}))
if isinstance(mf, dict) and mf:
return _kv([
("게이트", mf.get("gate", mf.get("status", ""))),
("행 수", mf.get("rows", "")),
("미해결", mf.get("unresolved", "")),
])
return _kv([
("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
("행 수", "DATA_MISSING — 하네스 업데이트 필요"),
("미해결", "DATA_MISSING — 하네스 업데이트 필요"),
])
def _earnings_growth_quality_v1(hctx: dict, se: list) -> str:
eg = _sj(hctx.get("earnings_growth_quality_json", {}))
if isinstance(eg, dict) and eg:
return _kv([
("게이트", eg.get("gate", eg.get("status", ""))),
("등급 수", eg.get("label_types", "")),
("비ETF 수", eg.get("non_etf", "")),
])
return _kv([
("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
("등급 수", "DATA_MISSING — 하네스 업데이트 필요"),
("비ETF 수", "DATA_MISSING — 하네스 업데이트 필요"),
])
def _market_share_proxy_v1(hctx: dict, se: list) -> str:
ms = _sj(hctx.get("market_share_proxy_json", {}))
if isinstance(ms, dict) and ms:
return _kv([
("게이트", ms.get("gate", ms.get("status", ""))),
("상태 수", ms.get("unique_states", "")),
("비ETF 수", ms.get("non_etf_scored", "")),
])
return _kv([
("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
("상태 수", "DATA_MISSING — 하네스 업데이트 필요"),
("비ETF 수", "DATA_MISSING — 하네스 업데이트 필요"),
])
def _cashflow_stability_v1(hctx: dict, se: list) -> str:
cf = _sj(hctx.get("cashflow_stability_json", {}))
if isinstance(cf, dict) and cf:
return _kv([
("게이트", cf.get("gate", cf.get("status", ""))),
("회계 리스크", cf.get("accounting_risk", "")),
("비ETF 수", cf.get("non_etf", "")),
])
return _kv([
("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
("회계 리스크", "DATA_MISSING — 하네스 업데이트 필요"),
("비ETF 수", "DATA_MISSING — 하네스 업데이트 필요"),
])
def _routing_decision_explain_v1(hctx: dict, se: list) -> str:
rd = _sj(hctx.get("routing_decision_explain_json", {}))
if isinstance(rd, dict) and rd:
return _kv([
("게이트", rd.get("gate", rd.get("status", ""))),
("요약", rd.get("summary", "")),
])
return _kv([
("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
("요약", "DATA_MISSING — 하네스 업데이트 필요"),
])
def _engine_feedback_loop_report(hctx: dict, se: list) -> str:
fb = _sj(hctx.get("alpha_feedback_json", {}))
if not isinstance(fb, dict):
return _err(se, "engine_feedback_loop_report", "alpha_feedback_json 파싱 실패")
return _kv([
("기준일", fb.get("as_of", "")),
("분석 기간", fb.get("analysis_period", "")),
("상태", fb.get("status", "")),
("분석 케이스", fb.get("cases_analyzed", "")),
("등급 수", fb.get("grade_count", "")),
("T20 실패율", fb.get("eligible_t20_fail_rate", "")),
("T60 실패율", fb.get("eligible_t60_fail_rate", "")),
])
def _prediction_evaluation_improvement_report(hctx: dict, packet: dict, se: list) -> str:
pred = packet.get("prediction") or {}
ahs = _sj(hctx.get("alpha_history_summary_json", {}))
tq = _sj(hctx.get("trade_quality_json", {}))
rows = [("일치율", f"{pred.get('match_rate_pct', 0)}%")]
if isinstance(ahs, dict):
rows += [
("T20 총계", ahs.get("t20_total", "")),
("T20 통과율", ahs.get("t20_pass_rate", "")),
("상태", ahs.get("status", "")),
]
if isinstance(tq, dict):
rows += [
("점수 상태", tq.get("status", "")),
("점수 케이스", tq.get("scored_count", "")),
("요약 점수", tq.get("summary_score", "")),
]
return _kv(rows)
def _rule_lifecycle_governance_report(hctx: dict, se: list) -> str:
pb = _sj(hctx.get("pattern_blacklist_json", {}))
dag_path = ROOT / "Temp" / "release_dag_run_v3.json"
rows = []
if isinstance(pb, dict):
patterns = pb.get("patterns", [])
rows += [
("패턴 블랙리스트 상태", pb.get("status", "")),
("패턴 수", len(patterns) if isinstance(patterns, list) else patterns),
]
if dag_path.exists():
try:
dag = json.loads(dag_path.read_text(encoding="utf-8"))
steps = dag.get("steps", [])
failed = [s["node_id"] for s in steps if s.get("gate") not in ("PASS", None)]
rows += [
("DAG 모드", dag.get("mode", "")),
("DAG 스텝 수", len(steps)),
("실패 스텝", ", ".join(failed) if failed else "없음"),
]
except Exception as e:
se.append({"section": "rule_lifecycle_governance_report", "error": f"DAG JSON 파싱 실패: {e}"})
if not rows:
return _err(se, "rule_lifecycle_governance_report", "거버넌스 데이터 없음")
return _kv(rows)
# ── 메인 ─────────────────────────────────────────────────────────────────────
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--json", default=str(ROOT / "GatherTradingData.json"))
ap.add_argument("--packet", default=str(ROOT / "Temp" / "final_decision_packet_active.json"))
ap.add_argument("--output", default=str(ROOT / "Temp" / "operational_report.md"))
ap.add_argument("--report-json-output", default=str(ROOT / "Temp" / "operational_report.json"))
ap.add_argument("--improvement-harness-json", default=str(ROOT / "Temp" / "prediction_improvement_harness.json"))
args = ap.parse_args()
data_path = Path(args.json)
packet_path = Path(args.packet)
if not data_path.exists():
print(f"[오류] GatherTradingData.json 없음: {data_path}")
return 1
if not packet_path.exists():
print(f"[오류] 패킷 없음: {packet_path}")
return 1
data_root = json.loads(data_path.read_text(encoding="utf-8"))
packet = json.loads(packet_path.read_text(encoding="utf-8"))
hctx = data_root.get("data", {}).get("_harness_context", {})
se: list = [] # section_errors
render_map = {
"exec_safety_declaration": lambda: _exec_safety_declaration(hctx, se),
"final_judgment_table": lambda: _final_judgment_table(hctx, se),
"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),
"portfolio_performance_summary": lambda: _portfolio_performance_summary(data_root, hctx, se),
"portfolio_sector_exposure_summary": lambda: _portfolio_sector_exposure_summary(data_root, hctx, se),
"sector_universe_refresh_audit_v1": lambda: _sector_universe_refresh_audit_v1(data_root, hctx, se),
"sector_trend_analysis_v1": lambda: _sector_trend_analysis_v1(data_root, hctx, se),
"investment_quality_headline": lambda: _investment_quality_headline(hctx, se),
"operational_truth_score": lambda: _operational_truth_score(hctx, se),
"execution_readiness_matrix": lambda: _execution_readiness_matrix(hctx, packet, se),
"pass_100_criteria": lambda: _pass_100_criteria(hctx, packet, se),
"today_decision_summary_card": lambda: _today_decision_summary_card(hctx, se),
"routing_serving_trace": lambda: _routing_serving_trace(hctx, se),
"export_gate_diagnosis": lambda: _export_gate_diagnosis(hctx, se),
"QEH_AUDIT_BLOCK": lambda: _qeh_audit_block(hctx, se),
"etf_representative_monitor_v1": lambda: _etf_representative_monitor_v1(data_root, hctx, se),
"fundamental_quality_gate_v1": lambda: _fundamental_quality_gate_v1(hctx, se),
"horizon_allocation_lock_v1": lambda: _horizon_allocation_lock_v1(hctx, se),
"smart_money_liquidity_gate_v1": lambda: _smart_money_liquidity_gate_v1(hctx, se),
"routing_serving_trace_v2": lambda: _routing_serving_trace_v2(hctx, se),
"fundamental_multifactor_v2": lambda: _fundamental_multifactor_v2(hctx, se),
"earnings_growth_quality_v1": lambda: _earnings_growth_quality_v1(hctx, se),
"market_share_proxy_v1": lambda: _market_share_proxy_v1(hctx, se),
"cashflow_stability_v1": lambda: _cashflow_stability_v1(hctx, se),
"routing_decision_explain_v1": lambda: _routing_decision_explain_v1(hctx, se),
"benchmark_relative_harness_table": lambda: _benchmark_relative_harness_table(hctx, se),
"index_relative_health_table": lambda: _index_relative_health_table(hctx, se),
"entry_freshness_gate_table": lambda: _entry_freshness_gate_table(hctx, se),
"sell_value_preservation_gate_table": lambda: _sell_value_preservation_gate_table(hctx, se),
"watch_release_checklist": lambda: _watch_release_checklist(hctx, se),
"alpha_feedback_loop_report": lambda: _alpha_feedback_loop_report(hctx, se),
"backdata_feature_bank_table": lambda: _backdata_feature_bank_table(hctx, se),
"alpha_lead_table": lambda: _alpha_lead_table(hctx, se),
"anti_distribution_table": lambda: _anti_distribution_table(hctx, se),
"profit_preservation_table": lambda: _profit_preservation_table(hctx, se),
"smart_cash_raise_table": lambda: _smart_cash_raise_table(hctx, se),
"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),
"proposal_reference_sheet": lambda: _proposal_reference_sheet(hctx, se),
"satellite_buy_proposal_sheet": lambda: _satellite_buy_proposal_sheet(hctx, se),
"core_satellite_timing_gate_table": lambda: _core_satellite_timing_gate_table(data_root, se),
"engine_feedback_loop_report": lambda: _engine_feedback_loop_report(hctx, se),
"prediction_evaluation_improvement_report": lambda: _prediction_evaluation_improvement_report(hctx, packet, se),
"rule_lifecycle_governance_report": lambda: _rule_lifecycle_governance_report(hctx, se),
}
sections = []
for name in SECTION_ORDER:
title = SECTION_TITLES.get(name, name)
render_fn = render_map.get(name)
if render_fn is None:
md = _err(se, name, "렌더러 미구현")
else:
try:
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})
# 섹션 처리 오류 요약을 마지막 섹션으로 추가
if se:
err_rows = ["| 섹션 | 오류 |", "| --- | --- |"]
err_rows.extend(f"| {e['section']} | {e['error']} |" for e in se)
sections.append({
"name": "section_processing_errors",
"title": "섹션 처리 오류 요약",
"markdown": "\n".join(err_rows),
})
_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 = {
"schema_version": "2026-05-24-operational-report-v1",
"generated_at": datetime.now(timezone.utc).isoformat(),
"source_json": data_path.name,
"section_count": len(sections),
"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,
},
"sections": sections,
}
out_json = Path(args.report_json_output)
out_md = Path(args.output)
out_json.parent.mkdir(parents=True, exist_ok=True)
out_json.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
md_lines = ["# Operational Investment Report\n"]
for s in sections:
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(
json.dumps({"formula_id": "PREDICTION_IMPROVEMENT_HARNESS_V1", "status": "OK"}),
encoding="utf-8"
)
print(f"REPORT_JSON RENDERED OK: sections={len(sections)} errors={len(se)}")
print(f"REPORT RENDERED OK: {out_md}")
if se:
print(f"[경고] 섹션 처리 오류 {len(se)}건:")
for e in se:
print(f"[SECTION_ERROR] {e['section']}: {e['error']}")
print(f"PREDICTION_IMPROVEMENT_HARNESS_EXPORTED: {args.improvement_harness_json}")
return 0
if __name__ == "__main__":
import sys
sys.exit(main())