feat: 리밸런싱 엔진 V1 + GAS 버그 수정 (2026-06-13)
주요 변경: - tools/build_rebalance_engine_v1.py: REBALANCE_ENGINE_V1 신규 * account_snapshot 직접 합산(_build_snap_position_map) → 소수주 분리 행 병합 * 레짐 소스 macro.REGIME_PRELIM 최우선 (GAS 와 동일) - src/gas_adapter_parts/gdf_06_rebalance.gs: runRebalanceSheet_() 신규 * Logger.log / getSpreadsheet_() 로 run_all 연동 수정 - src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs * _mergePositionRecord_(): 소수주 중복 행 합산 신규 * parseInt → parseFloat (qty, availQty) - src/gas_adapter_parts/gdf_01_price_metrics.gs * 미보유 종목 SELL_READY → WATCH_EXIT_SIGNAL - spec/41_release_dag.yaml: build_rebalance_sheet 노드 추가 (step_count 63) - spec/51_formula_lifecycle_registry.yaml: REBALANCE_ENGINE_V1 등록 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,654 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
render_operational_report.py — 30개 섹션 완전 렌더링.
|
||||
섹션 처리 오류는 section_errors 배열에 기록되어 하네스 검증에 노출된다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
SECTION_ORDER = [
|
||||
"exec_safety_declaration", "final_judgment_table", "final_execution_decision",
|
||||
"concise_hts_input_sheet", "watch_breakout_gate",
|
||||
"single_conclusion", "immediate_execution_playbook", "market_context_learning_note",
|
||||
"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",
|
||||
"backdata_feature_bank_table", "alpha_lead_table", "anti_distribution_table",
|
||||
"profit_preservation_table", "smart_cash_raise_table", "execution_quality_table",
|
||||
"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": "돌파 감시 게이트",
|
||||
"single_conclusion": "단일 결론",
|
||||
"immediate_execution_playbook": "즉시 실행 플레이북",
|
||||
"market_context_learning_note": "시장 컨텍스트 학습 노트",
|
||||
"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 감사 블록",
|
||||
"backdata_feature_bank_table": "백데이터 특성 원장",
|
||||
"alpha_lead_table": "알파 선행 테이블",
|
||||
"anti_distribution_table": "분산 매도 위험 테이블",
|
||||
"profit_preservation_table": "수익 보존 테이블",
|
||||
"smart_cash_raise_table": "현금 확보 테이블",
|
||||
"execution_quality_table": "체결 품질 테이블",
|
||||
"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 []
|
||||
|
||||
|
||||
# ── 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 _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 없음")
|
||||
return _tbl(items, ["ticker", "name", "final_action", "gate_trace", "rs_verdict"])
|
||||
|
||||
|
||||
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")),
|
||||
])]
|
||||
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)
|
||||
|
||||
|
||||
# ── 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:
|
||||
parts.append("**실행 결정**\n\n" + _tbl(items, ["ticker", "name", "final_action", "gate_trace"]))
|
||||
else:
|
||||
parts.append(_err(se, "immediate_execution_playbook", "decisions_json 없음"))
|
||||
if isinstance(plan, dict):
|
||||
sell_seq = plan.get("sell_sequence", "")
|
||||
parts.append("\n\n**현금 회수 계획**\n\n" + _kv([
|
||||
("매도 시퀀스", str(sell_seq)[:120]),
|
||||
("예상 즉시 회수 (KRW)", plan.get("expected_total_krw", "")),
|
||||
("부족액 충족", plan.get("shortfall_met", "")),
|
||||
("필요 건수", plan.get("items_needed", "")),
|
||||
]))
|
||||
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)
|
||||
|
||||
|
||||
# ── 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)
|
||||
|
||||
|
||||
# ── 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 없음")
|
||||
return _tbl(items, ["ticker", "name", "buy_permission_state", "max_tranche_pct",
|
||||
"composite_verdict", "blocked_reason_codes"])
|
||||
|
||||
|
||||
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 _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),
|
||||
"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),
|
||||
"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),
|
||||
"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),
|
||||
"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}")
|
||||
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),
|
||||
})
|
||||
|
||||
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_error_count": len(se),
|
||||
"section_errors": se,
|
||||
"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(f"## {s.get('title', s.get('name'))}\n\n{s.get('markdown', '')}\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())
|
||||
Reference in New Issue
Block a user