"""Build a single-file interactive Optuna dashboard (中文 HTML). Loads the persisted Optuna study from ``studies/optuna/gold_scalper_pro_is2025.db`` and writes ``reports/optuna_dashboard_.html`` containing 7 plotly charts bundled into one page (each ``fig.to_html(full_html=False, include_plotlyjs='cdn')`` fragments + minimal CSS). Every chart's title and axis labels are localized to 简体中文 so the report reads natively. Charts: 1. 优化历史 (plot_optimization_history) 2. 参数重要性 (plot_param_importances) 3. 平行坐标图 (plot_parallel_coordinate) 4. 参数切片图 (plot_slice) 5. 等高线图 (plot_contour) 6. 经验分布函数 (plot_edf) 7. 时间线 (plot_timeline) Usage: python scripts/build_optuna_dashboard.py """ from __future__ import annotations import sys from pathlib import Path from html import escape PROJECT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(PROJECT)) import optuna STUDY_NAME = "gold_scalper_pro_is2025" STUDY_DB = PROJECT / "studies" / "optuna" / "gold_scalper_pro_is2025.db" OUT_HTML = PROJECT / "reports" / f"optuna_dashboard_{STUDY_NAME}.html" # Chart-level metadata: (call name, 中文标题, 中文 X 轴, 中文 Y 轴) # Y axis label None means "leave Optuna default" (some plots set their own). # Note: plot_slice and plot_contour are rendered separately as per-param # grids below the main dashboard — those two are too dense (19 params) to # be readable as a single chart. CHARTS: list[tuple[str, str, str | None, str | None]] = [ ("plot_optimization_history", "优化历史", "试验序号 Trial", "目标值 Objective (score)"), ("plot_param_importances", "参数重要性 (fANOVA)", "超参数 Hyperparameter", "重要性 Importance"), ("plot_parallel_coordinate", "平行坐标图 — 参数 ↔ score", None, None), ("plot_edf", "经验分布函数 (EDF)", "目标值 Objective", "累积分布 CDF"), ("plot_timeline", "时间线 — 试验耗时与状态", "试验序号 Trial", "耗时 (秒) Elapsed (s)"), ] # Per-parameter charts: each param gets its own small slice + contour grid. # Rendered as separate
blocks below the main dashboard. PER_PARAM_CHARTS = [ "InpFastEmaPeriod", "InpSlowEmaPeriod", "InpRsiPeriod", "InpRsiBuyLevel", "InpRsiSellLevel", "InpPullbackAtrMult", "InpAtrPeriod", "InpMaxSpreadAtrPct", "InpRiskPercent", "InpAtrSLMult", "InpAtrTPMult", "InpBreakEvenPoints", "InpBreakEvenLock", "InpTrailStartPoints", "InpTrailStepPoints", "InpMaxTradesPerDay", "InpDailyLossLimit", "InpMinSecondsBetween", ] def localize(fig, title_zh: str, x_zh: str | None, y_zh: str | None): """Localize a plotly Figure's title + axis labels to 简体中文.""" fig.update_layout(title=title_zh) if x_zh is not None: fig.update_xaxes(title_text=x_zh) if y_zh is not None: fig.update_yaxes(title_text=y_zh) # Translate the legend "Objective" → "目标值" where it shows up. if fig.layout.legend and fig.layout.legend.title: leg = fig.layout.legend.title.text if leg and "Objective" in leg: fig.update_layout(legend_title_text="图例") # Apply a Chinese-readable base font + light theme. fig.update_layout( font=dict(family="Microsoft YaHei, Arial, sans-serif", size=12, color="#222"), template="plotly_white", ) return fig def render_chart(fn_name: str, study: optuna.Study, include_plotly: bool) -> str: """Call optuna.visualization.(study), localize, return HTML fragment. plotly.js is loaded ONCE via CDN

Optuna 优化仪表盘

研究名称:{escape(STUDY_NAME)} 试验总数:{n_trials} 完成:{completed} 剪枝:{pruned} 失败:{failed} 最佳:{best_str}
{main_cards} {slice_section} {contour_section}
""" def main() -> int: if not STUDY_DB.exists(): print(f"study DB not found: {STUDY_DB}") return 1 print(f"loading study: {STUDY_NAME} ← {STUDY_DB.relative_to(PROJECT)}") study = optuna.load_study( study_name=STUDY_NAME, storage=f"sqlite:///{STUDY_DB}", ) best_val = study.best_trial.value if study.best_trial is not None else None print(f" trials: {len(study.trials)} best: " f"{best_val:.4f}" if best_val is not None else " trials: (no best yet)") # Main dashboard charts (single-figure plots that render fine at 1100×520). main_fragments: list[tuple[str, str]] = [] for i, (fn_name, title_zh, *_) in enumerate(CHARTS): print(f" · {fn_name} ({title_zh}) …", end=" ", flush=True) frag = render_chart(fn_name, study, include_plotly=(i == 0)) main_fragments.append((title_zh, frag)) print("OK" if "chart-error" not in frag else "FAILED") # Per-parameter slice charts — readable single-column subplots instead # of plot_slice's 5400px-wide composite that crammed all 19 params. print(f"\n building per-param slice charts ({len(PER_PARAM_CHARTS)} params)…") slice_fragments = render_slice_grid(study, PER_PARAM_CHARTS) n_ok = sum(1 for _, f in slice_fragments if "chart-error" not in f) print(f" slice: {n_ok}/{len(slice_fragments)} OK") # Per-pair contour charts — only top-6 important params (15 pairs) # instead of plot_contour's 19² = 361 unreadable subplots. print(f" building per-pair contour charts (top-6 important params)…") contour_fragments = render_contour_grid(study, PER_PARAM_CHARTS) n_ok = sum(1 for _, f in contour_fragments if "chart-error" not in f) print(f" contour: {n_ok}/{len(contour_fragments)} OK") OUT_HTML.parent.mkdir(parents=True, exist_ok=True) html = build_index_html(study, main_fragments, slice_fragments, contour_fragments) OUT_HTML.write_text(html, encoding="utf-8") print(f"\nwritten: {OUT_HTML.relative_to(PROJECT)} ({len(html):,} bytes)") print(f"open: {OUT_HTML.as_uri()}") return 0 if __name__ == "__main__": raise SystemExit(main())