""" ground_truth·시뮬 등 차트 HTML 공통 레이아웃·스타일. """ from __future__ import annotations from typing import Any CHART_REPORT_CSS = """ body { font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; background: #f8fafc; } h1 { font-size: 1.35rem; } h2 { font-size: 1.1rem; margin-top: 28px; } .meta { color: #475569; font-size: 0.9rem; } .note { background: #f1f5f9; border: 1px solid #cbd5e1; padding: 10px; border-radius: 6px; color: #334155; font-size: 0.9rem; line-height: 1.5; } .go { font-size: 1.25rem; font-weight: 700; } .go-pass { color: #16a34a; } .go-fail { color: #dc2626; } .cards { display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0; } .card { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px 14px; } .card span { font-size: 0.75rem; color: #64748b; display: block; } .card b { font-size: 1.05rem; } .chart-wrap { background:#fff; border:1px solid #e2e8f0; border-radius:8px; padding:8px; } .legend-box { font-size:0.85rem; color:#475569; margin-bottom:10px; line-height: 1.6; } table { width:100%; border-collapse:collapse; background:#fff; font-size:0.85rem; } th, td { border:1px solid #e2e8f0; padding:8px; text-align:left; } th { background:#f1f5f9; } td.buy { color:#16a34a; font-weight:600; } td.sell { color:#dc2626; font-weight:600; } td.num { text-align: right; font-variant-numeric: tabular-nums; } .criteria { background:#fff; border:1px solid #e2e8f0; border-radius:8px; padding:12px 16px; margin:12px 0; } .criteria h3 { margin: 0 0 8px; font-size: 1rem; } .criteria ul { margin: 6px 0 0 18px; padding: 0; } .criteria li { margin: 4px 0; color: #334155; } .criteria .kind { color: #64748b; font-size: 0.85rem; } .table-scroll { max-height: 480px; overflow-y: auto; border: 1px solid #e2e8f0; border-radius: 8px; } .pass { color: #16a34a; font-weight: 600; } .fail { color: #dc2626; font-weight: 600; } .cards-group-title { font-size: 0.82rem; color: #475569; margin: 14px 0 6px; font-weight: 600; } """ def initial_change_pct(pnl: dict[str, Any]) -> float: """ 초기 금액 대비 총보유자산 증감율(%)을 계산합니다. Args: pnl: initial_cash_krw, final_asset_krw (또는 pnl_pct) 포함 dict. Returns: 증감율 %. """ if pnl.get("pnl_pct") is not None: return float(pnl["pnl_pct"]) initial = float(pnl.get("initial_cash_krw") or 0) final = float(pnl.get("final_asset_krw") or 0) if initial <= 0: return 0.0 return (final - initial) / initial * 100.0 def pnl_cards_html(pnl: dict[str, Any], trade_label: str, trade_count: int) -> str: """ GT·시뮬 HTML 공통 자산 요약 카드 (총보유자산·초기 대비 증감율). Args: pnl: simulate_truth_portfolio 또는 simulate_fixed_order_portfolio 결과. trade_label: 타점 라벨(예: 정답 타점, 시뮬 체결). trade_count: 타점 건수. Returns: card div HTML 연속 문자열. """ if pnl.get("initial_cash_krw") is None: return card_html(trade_label, f"{trade_count}건") change_pct = initial_change_pct(pnl) out = card_html(trade_label, f"{trade_count}건") out += card_html("초기 금액", f"₩{pnl['initial_cash_krw']:,.0f}") out += card_html("총보유자산", f"₩{pnl['final_asset_krw']:,.0f}") out += card_html("초기 대비 증감율", f"{change_pct:+.2f}%") out += card_html("수수료", f"₩{pnl['total_fees_krw']:,.0f}") if pnl.get("holding_qty", 0) > 0: out += card_html( "미청산", f"{pnl['holding_qty']}개 (₩{pnl['holding_value_krw']:,.0f})", ) return out def market_cards_html(close_last: float, bb_pos_txt: str) -> str: """ 종가·BB %B 카드. Args: close_last: 종가. bb_pos_txt: BB %B 표시 문자열. Returns: card HTML. """ return card_html("종가", f"₩{close_last:,.2f}") + card_html("BB %B", bb_pos_txt) def card_html(label: str, value: str) -> str: """ 요약 카드 HTML 한 칸. Args: label: 라벨. value: 값(HTML 허용). Returns: div.card 문자열. """ return f'
종합 판정: {label}
| 규칙 | side | 판정 | holdout EV% | holdout PF | WF 양수월 | 수수료 2x EV% |
|---|