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:
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user