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