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

- _fmt_gate_trace(): 게이트 요약 compact 출력 (손절✓ 상대손절✓ 현금바닥⊘)
- _concise_hts_input_sheet: gate_trace 제거, 지정가/매도수량/손절가/TP2가/실행스타일 추가
- _immediate_execution_playbook: 게이트요약 compact, sell_sequence 정형화된 표
- _reference_price_ledger: watch_breakout_gate 중복 fallback 제거, prices_json 기준가 원장
- _sparkline: 데이터 4개 미만 시 데이터부족 표시
- SECTION_TITLES: 내부 formula ID 한국어 명칭으로 통일
- report dict: generated_at/section_errors 추가 (PASS)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 23:05:47 +09:00
parent 20f0973f74
commit e2820065d1
5 changed files with 411 additions and 27 deletions
+152
View File
@@ -16,6 +16,10 @@ OUTPUT_DIR = ROOT / "outputs" / "sector_insights_enhanced"
OUTPUT_XLSX = OUTPUT_DIR / "GatherTradingData_sector_insights.xlsx"
SECTOR_JSON = ROOT / "Temp" / "sector_trend_analysis_v1.json"
ETF_JSON = ROOT / "Temp" / "etf_representative_monitor_v1.json"
READINESS_JSON = ROOT / "Temp" / "operational_alpha_calibration_v2.json"
READINESS_BRIDGE_JSON = ROOT / "Temp" / "performance_readiness_replay_bridge_v1.json"
READINESS_BRIDGE_V2_JSON = ROOT / "Temp" / "performance_readiness_replay_bridge_v2.json"
EVAL_QUEUE_JSON = ROOT / "Temp" / "operational_eval_queue_v1.json"
HEADER_FILL = PatternFill("solid", fgColor="1F4E78")
@@ -236,6 +240,148 @@ def build_portfolio_summary(wb) -> None:
set_col_widths(ws, {"A": 22, "B": 18, "C": 18, "D": 24, "E": 18, "F": 18, "G": 26, "H": 26, "I": 18, "J": 18})
def build_performance_readiness_summary(wb) -> None:
def display(value):
return value if value not in (None, "") else "DATA_MISSING — 하네스 업데이트 필요"
readiness = load_json(READINESS_JSON) if READINESS_JSON.exists() else {}
bridge = load_json(READINESS_BRIDGE_JSON) if READINESS_BRIDGE_JSON.exists() else {}
bridge_v2 = load_json(READINESS_BRIDGE_V2_JSON) if READINESS_BRIDGE_V2_JSON.exists() else {}
live = bridge.get("live", {}) if isinstance(bridge.get("live"), dict) else {}
replay = bridge.get("replay_informational", {}) if isinstance(bridge.get("replay_informational"), dict) else {}
metrics = readiness.get("metrics", {}) if isinstance(readiness.get("metrics"), dict) else {}
ws = wb.create_sheet("performance_readiness_summary")
style_sheet(ws)
style_title(
ws,
"성과 준비도 요약",
"CHECK_83의 live T+20 데이터 게이트와 replay 브리지를 함께 보여주는 상태 시트",
end_col=10,
)
items = [
("check_83_gate", display(readiness.get("gate"))),
("confidence_score", display(readiness.get("confidence_score"))),
("performance_ready", display(readiness.get("performance_ready"))),
("readiness_reasons", display(", ".join(readiness.get("readiness_reasons", [])) if isinstance(readiness.get("readiness_reasons"), list) else readiness.get("readiness_reasons"))),
("outcome_quality_score", display(metrics.get("outcome_quality_score"))),
("t20_operational_sample", display(metrics.get("t20_operational_sample"))),
("t5_operational_pass_rate", display(metrics.get("t5_operational_pass_rate"))),
("value_damage_pct_avg", display(metrics.get("value_damage_pct_avg"))),
("live_t20_count", display(live.get("t20_count"))),
("live_sample_gate", display(live.get("sample_gate"))),
]
add_kpi_block(ws, 4, items)
ws["D4"] = "Readiness rule"
ws["D4"].fill = SUBHEADER_FILL
ws["D4"].font = BOLD_FONT
ws["D5"] = "live T+20가 30건 미만이면 PERFORMANCE_READY로 승격하지 않습니다."
ws["D6"] = f"현재 상태: {readiness.get('gate', 'MISSING')}"
ws["D7"] = f"브리지 승격 규칙: {bridge_v2.get('promotion_rule', 'DATA_MISSING — 하네스 업데이트 필요')}"
ws["D8"] = f"브리지 승격 가능: {bridge_v2.get('promotion_allowed', 'DATA_MISSING — 하네스 업데이트 필요')}"
ws["G4"] = "Replay reference"
ws["G4"].fill = SUBHEADER_FILL
ws["G4"].font = BOLD_FONT
ws["G5"] = f"replay_t20_count: {display(replay.get('t20_count'))}"
ws["G6"] = f"replay_t20_pass_rate_pct: {display(replay.get('t20_pass_rate_pct'))}"
ws["G7"] = f"replay_t20_avg_return_pct: {display(replay.get('t20_avg_return_pct'))}"
ws["G8"] = f"replay_note: {display(replay.get('note'))}"
readiness_rows = [
["metric", "count"],
["live_t20_count", live.get("t20_count") or 0],
["required_live_t20_count", 30],
["replay_t20_count", replay.get("t20_count") or 0],
]
write_table(ws, 4, 10, readiness_rows[0], readiness_rows[1:])
readiness_chart = BarChart()
readiness_chart.type = "bar"
readiness_chart.style = 10
readiness_chart.title = "T20 Readiness vs Threshold"
readiness_chart.y_axis.title = "Metric"
readiness_chart.x_axis.title = "Count"
readiness_chart.height = 6.5
readiness_chart.width = 11
readiness_data = Reference(ws, min_col=11, min_row=4, max_row=7)
readiness_cats = Reference(ws, min_col=10, min_row=5, max_row=7)
readiness_chart.add_data(readiness_data, titles_from_data=True)
readiness_chart.set_categories(readiness_cats)
readiness_chart.legend = None
ws.add_chart(readiness_chart, "J13")
set_col_widths(ws, {"A": 24, "B": 18, "C": 18, "D": 28, "E": 18, "F": 18, "G": 26, "H": 26, "I": 18, "J": 20, "K": 16})
def build_operational_eval_queue_summary(wb) -> None:
def display(value):
return value if value not in (None, "") else "DATA_MISSING — 하네스 업데이트 필요"
queue_data = load_json(EVAL_QUEUE_JSON) if EVAL_QUEUE_JSON.exists() else {}
metrics = queue_data.get("metrics", {}) if isinstance(queue_data.get("metrics"), dict) else {}
queue_rows = queue_data.get("queue", []) if isinstance(queue_data.get("queue"), list) else []
todo = queue_data.get("todo_protocol", []) if isinstance(queue_data.get("todo_protocol"), list) else []
ws = wb.create_sheet("operational_eval_queue_summary")
style_sheet(ws)
style_title(
ws,
"운영 T+20 대기열 요약",
"T+20 실제 결과 입력 대기 상태와 처리 프로토콜을 한 장에 정리",
end_col=10,
)
items = [
("formula_id", display(queue_data.get("formula_id"))),
("as_of", display(queue_data.get("as_of"))),
("t20_days_threshold", display(queue_data.get("t20_days_threshold"))),
("records_total", display(metrics.get("records_total"))),
("t20_evaluated_count", display(metrics.get("t20_evaluated_count"))),
("t20_due_capture_count", display(metrics.get("t20_due_capture_count"))),
("missing_due_date_count", display(metrics.get("missing_due_date_count"))),
("all_proposals_have_due_dates", display(queue_data.get("all_proposals_have_due_dates"))),
("queue_count", display(len(queue_rows))),
("todo_count", display(len(todo))),
]
add_kpi_block(ws, 4, items)
ws["D4"] = "Queue protocol"
ws["D4"].fill = SUBHEADER_FILL
ws["D4"].font = BOLD_FONT
for idx, line in enumerate(todo[:5], start=5):
ws.cell(idx, 4).value = line
ws["G4"] = "Queue status"
ws["G4"].fill = SUBHEADER_FILL
ws["G4"].font = BOLD_FONT
ws["G5"] = f"current_queue_rows: {display(len(queue_rows))}"
ws["G6"] = f"t20_due_capture_count: {display(metrics.get('t20_due_capture_count'))}"
ws["G7"] = f"missing_due_date_count: {display(metrics.get('missing_due_date_count'))}"
queue_table_rows = [["metric", "value"], ["records_total", metrics.get("records_total") or 0], ["t20_evaluated_count", metrics.get("t20_evaluated_count") or 0], ["t20_due_capture_count", metrics.get("t20_due_capture_count") or 0]]
write_table(ws, 17, 1, queue_table_rows[0], queue_table_rows[1:])
chart = BarChart()
chart.type = "bar"
chart.style = 10
chart.title = "T20 Queue Status"
chart.y_axis.title = "Metric"
chart.x_axis.title = "Count"
chart.height = 6
chart.width = 11
data_ref = Reference(ws, min_col=2, min_row=17, max_row=20)
cats = Reference(ws, min_col=1, min_row=18, max_row=20)
chart.add_data(data_ref, titles_from_data=True)
chart.set_categories(cats)
chart.legend = None
ws.add_chart(chart, "J17")
set_col_widths(ws, {"A": 24, "B": 18, "C": 18, "D": 40, "E": 18, "F": 18, "G": 28, "H": 28, "I": 18, "J": 20, "K": 16})
def build_portfolio_sector_exposure(wb) -> None:
daily_headers, daily_rows = extract_sheet_rows(wb, "daily_history")
account_headers, account_rows = extract_sheet_rows(wb, "account_snapshot")
@@ -773,6 +919,8 @@ def main() -> None:
wb = load_workbook(INPUT_XLSX)
for name in [
"portfolio_performance_summary",
"performance_readiness_summary",
"operational_eval_queue_summary",
"portfolio_sector_exposure",
"_portfolio_holdings_helper",
"sector_trend_summary",
@@ -785,6 +933,8 @@ def main() -> None:
# Build data sheets first so summary sheets can reference the timeline sheet.
build_portfolio_summary(wb)
build_performance_readiness_summary(wb)
build_operational_eval_queue_summary(wb)
build_portfolio_sector_exposure(wb)
build_sector_timeline(wb, sector, raw_source)
build_sector_analysis(wb, sector)
@@ -796,6 +946,8 @@ def main() -> None:
order = [
"settings",
"portfolio_performance_summary",
"performance_readiness_summary",
"operational_eval_queue_summary",
"portfolio_sector_exposure",
"sector_trend_summary",
"sector_trend_analysis",