섹터 리포트와 대표종목 모니터 고도화
This commit is contained in:
@@ -9,8 +9,14 @@ from openpyxl.chart import BarChart, LineChart, Reference
|
||||
from openpyxl.styles import Font, PatternFill, Alignment
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.quant_engine.sector_universe_refresh import build_sector_universe_refresh_audit
|
||||
|
||||
INPUT_XLSX = ROOT / "GatherTradingData.xlsx"
|
||||
OUTPUT_DIR = ROOT / "outputs" / "sector_insights_enhanced"
|
||||
OUTPUT_XLSX = OUTPUT_DIR / "GatherTradingData_sector_insights.xlsx"
|
||||
@@ -593,10 +599,12 @@ def build_sector_summary(wb, data: dict) -> None:
|
||||
ws["A20"] = "Notes"
|
||||
ws["A20"].fill = SUBHEADER_FILL
|
||||
ws["A20"].font = BOLD_FONT
|
||||
ws["A21"] = "섹터별 ETF 프록시와 스마트머니 방향이 다르면 매수 근거를 보수적으로 해석해야 합니다."
|
||||
ws["A21"] = "섹터별 ETF 프록시를 기준으로 보고, 은행/증권/지주회사는 분리해서 구성비 상위 종목을 증빙해야 합니다. 대표주 모니터는 섹터 기본 3종, 로보틱스 5종 바스켓으로 함께 확인해야 합니다."
|
||||
ws["A21"].alignment = Alignment(wrap_text=True)
|
||||
ws["A22"] = "데이터 결측은 하네스 업데이트가 필요합니다."
|
||||
ws["A22"] = "Universe_Source가 DEFAULT_TEMPLATE인 행은 템플릿이며, 실제 시트 입력으로 전환되어야 provenance가 완성됩니다."
|
||||
ws["A22"].alignment = Alignment(wrap_text=True)
|
||||
ws["A23"] = "다음 세분화 후보는 바이오/제약과 방산/우주처럼 현재 섹터를 더 세밀하게 나누는 방향입니다. 로보틱스는 RISE 현대차고정피지컬AI를 섹터 프록시로 사용하고, 대표주는 해당 ETF의 실제 구성비 상위 5개 종목에서 뽑습니다."
|
||||
ws["A23"].alignment = Alignment(wrap_text=True)
|
||||
|
||||
chart = LineChart()
|
||||
chart.title = "Average Sector Score / Breadth Trend"
|
||||
@@ -622,11 +630,11 @@ def build_sector_analysis(wb, data: dict) -> None:
|
||||
style_title(
|
||||
ws,
|
||||
"섹터 동향 분석",
|
||||
"섹터별 ETF 프록시, 스마트머니 유입, 수익률, 유동성 방향을 함께 보는 상세 시트",
|
||||
"섹터별 ETF 프록시, 대표주 모니터, 스마트머니 유입, 수익률, 유동성 방향을 함께 보는 상세 시트",
|
||||
end_col=18,
|
||||
)
|
||||
headers = [
|
||||
"sector", "proxy_ticker", "proxy_name", "proxy_type", "etf_code",
|
||||
"sector", "proxy_ticker", "proxy_name", "proxy_type", "universe_source", "etf_code",
|
||||
"etf_execution_use", "etf_liquidity_score", "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",
|
||||
@@ -661,7 +669,7 @@ def build_sector_analysis(wb, data: dict) -> None:
|
||||
chart.x_axis.title = "20D Return"
|
||||
chart.height = 8
|
||||
chart.width = 14
|
||||
data_ref = Reference(ws, min_col=17, min_row=4, max_row=4 + len(rows))
|
||||
data_ref = Reference(ws, min_col=18, min_row=4, max_row=4 + len(rows))
|
||||
cats = Reference(ws, min_col=1, min_row=5, max_row=4 + len(rows))
|
||||
chart.add_data(data_ref, titles_from_data=True)
|
||||
chart.set_categories(cats)
|
||||
@@ -817,6 +825,67 @@ def build_sector_timeline(wb, data: dict, source_data: dict | None = None) -> No
|
||||
ws.add_chart(money_chart, "L36")
|
||||
|
||||
|
||||
def build_sector_universe_refresh_audit_sheet(wb, source_data: dict) -> None:
|
||||
ws = wb.create_sheet("sector_universe_refresh_audit")
|
||||
style_sheet(ws)
|
||||
style_title(
|
||||
ws,
|
||||
"섹터 월간 갱신 감사",
|
||||
"Naver ETF 페이지 기반 구성종목 갱신 상태와 provenance 분리 상태를 점검하는 감사 시트. AJAX/XHR 전제는 두지 않고 HTML 서버렌더링 테이블을 우선한다.",
|
||||
end_col=16,
|
||||
)
|
||||
payload = {"data": source_data}
|
||||
audit = build_sector_universe_refresh_audit(payload)
|
||||
summary = audit.get("summary") or {}
|
||||
items = [
|
||||
("formula_id", audit.get("formula_id", "")),
|
||||
("gate", audit.get("gate", "")),
|
||||
("sector_count", summary.get("sector_count", 0)),
|
||||
("current_count", summary.get("current_count", 0)),
|
||||
("due_count", summary.get("due_count", 0)),
|
||||
("overdue_count", summary.get("overdue_count", 0)),
|
||||
("layout_changed_count", summary.get("layout_changed_count", 0)),
|
||||
("missing_count", summary.get("missing_count", 0)),
|
||||
("template_count", summary.get("template_count", 0)),
|
||||
("sheet_input_count", summary.get("sheet_input_count", 0)),
|
||||
("naver_source_count", summary.get("naver_source_count", 0)),
|
||||
("missing_source_url_count", summary.get("missing_source_url_count", 0)),
|
||||
("stale_sector_count", summary.get("stale_sector_count", 0)),
|
||||
]
|
||||
add_kpi_block(ws, 4, items)
|
||||
ws["D4"] = "Refresh policy"
|
||||
ws["D4"].fill = SUBHEADER_FILL
|
||||
ws["D4"].font = BOLD_FONT
|
||||
ws["D5"] = "NAVER_ETF_PAGE rows are the monthly refreshed source."
|
||||
ws["D6"] = "SHEET_INPUT rows are manual/provisional and must stay separate."
|
||||
ws["D7"] = "DEFAULT_TEMPLATE rows are a fail in the monthly gate."
|
||||
ws["D8"] = "Source_URL and Source_AsOf are required for provenance."
|
||||
ws["D9"] = "This is HTML-server-rendered, not AJAX. JS is only a fallback probe for candidate URLs."
|
||||
ws["D10"] = "No guessed holdings are written when the page layout changes."
|
||||
ws["D11"] = "NAVER_ETF_PAGE_FAIL_LAYOUT_CHANGED is a separate layout-change failure state."
|
||||
ws["D12"] = "Financial sectors are split as 은행 / 증권 / 지주회사 in sector_universe; sector_flow reflects carryover until GAS runDataFeed is rerun."
|
||||
ws["D13"] = "This split is part of the monthly refresh harness; Source_URL and Source_AsOf must remain valid for provenance."
|
||||
rows = audit.get("rows") or []
|
||||
if rows:
|
||||
headers = [
|
||||
"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",
|
||||
]
|
||||
write_table(ws, 14, 1, headers, [[row.get(h, "") for h in headers] for row in rows])
|
||||
for col, width in {
|
||||
"A": 16, "B": 12, "C": 18, "D": 12, "E": 16, "F": 18, "G": 42, "H": 14,
|
||||
"I": 10, "J": 14, "K": 12, "L": 12, "M": 12, "N": 12, "O": 24,
|
||||
}.items():
|
||||
ws.column_dimensions[col].width = width
|
||||
ws.freeze_panes = "A5"
|
||||
ws["A11"] = "Notes"
|
||||
ws["A11"].fill = SUBHEADER_FILL
|
||||
ws["A11"].font = BOLD_FONT
|
||||
ws["A12"] = "홈페이지 리뉴얼로 표 구조가 바뀌면, 파서는 추정하지 않고 실패 상태를 남겨 월간 게이트에서 잡는다."
|
||||
ws["A12"].alignment = Alignment(wrap_text=True)
|
||||
|
||||
|
||||
def build_etf_summary(wb, data: dict) -> None:
|
||||
ws = wb.create_sheet("etf_representative_summary")
|
||||
style_sheet(ws)
|
||||
@@ -847,6 +916,7 @@ def build_etf_summary(wb, data: dict) -> None:
|
||||
ws["D6"] = "2) Missing slots filled with same-sector live candidates"
|
||||
ws["D7"] = "3) Missing data stays explicit as DATA_MISSING"
|
||||
ws["D8"] = "4) Minimum 3 names per sector basket"
|
||||
ws["D9"] = "5) Universe_Source=DEFAULT_TEMPLATE rows are provisional until sheet-backed data exists."
|
||||
ws["G4"] = "Top reps"
|
||||
ws["G4"].fill = SUBHEADER_FILL
|
||||
ws["G4"].font = BOLD_FONT
|
||||
@@ -865,7 +935,7 @@ def build_etf_monitor(wb, data: dict) -> None:
|
||||
end_col=18,
|
||||
)
|
||||
headers = [
|
||||
"sector", "etf_proxy_ticker", "etf_proxy_name", "etf_proxy_type", "sector_rank",
|
||||
"sector", "etf_proxy_ticker", "etf_proxy_name", "etf_proxy_type", "universe_source", "sector_rank",
|
||||
"sector_score", "sector_smart_money_5d_krw", "sector_ret20d", "representative_count",
|
||||
"representative_ticker", "representative_name", "representative_basis",
|
||||
"representative_basis_detail", "constituent_weight", "basket_quality_state",
|
||||
@@ -894,7 +964,7 @@ def build_etf_monitor(wb, data: dict) -> None:
|
||||
chart.x_axis.title = "Coverage %"
|
||||
chart.height = 8
|
||||
chart.width = 14
|
||||
data_ref = Reference(ws, min_col=16, min_row=4, max_row=4 + len(rows))
|
||||
data_ref = Reference(ws, min_col=17, min_row=4, max_row=4 + len(rows))
|
||||
cats = Reference(ws, min_col=1, min_row=5, max_row=4 + len(rows))
|
||||
chart.add_data(data_ref, titles_from_data=True)
|
||||
chart.set_categories(cats)
|
||||
@@ -922,6 +992,7 @@ def main() -> None:
|
||||
"performance_readiness_summary",
|
||||
"operational_eval_queue_summary",
|
||||
"portfolio_sector_exposure",
|
||||
"sector_universe_refresh_audit",
|
||||
"_portfolio_holdings_helper",
|
||||
"sector_trend_summary",
|
||||
"sector_trend_analysis",
|
||||
@@ -936,6 +1007,7 @@ def main() -> None:
|
||||
build_performance_readiness_summary(wb)
|
||||
build_operational_eval_queue_summary(wb)
|
||||
build_portfolio_sector_exposure(wb)
|
||||
build_sector_universe_refresh_audit_sheet(wb, raw_source)
|
||||
build_sector_timeline(wb, sector, raw_source)
|
||||
build_sector_analysis(wb, sector)
|
||||
build_sector_summary(wb, sector)
|
||||
@@ -949,6 +1021,7 @@ def main() -> None:
|
||||
"performance_readiness_summary",
|
||||
"operational_eval_queue_summary",
|
||||
"portfolio_sector_exposure",
|
||||
"sector_universe_refresh_audit",
|
||||
"sector_trend_summary",
|
||||
"sector_trend_analysis",
|
||||
"sector_trend_timeline",
|
||||
|
||||
Reference in New Issue
Block a user