phase 7-8 完成 + warmup 修复 + 产物结构化重组
主要内容: - Phase 8 PROMOTE: finalist #1 (trial #324) registry 条目,自动生成 - Optuna objective warmup bug 修复 (shared/optimizer/objective.py) - studies/ 目录按用途重组为 optuna/ + finalists/ + features/ 三层 - reports/ 加入 Optuna 中文 dashboard (5 主图 + 18 slice + 15 contour) - 新增 PROJECT_GUIDE.md 项目说明文档 - 新增 build_registry_entry.py / build_optuna_dashboard.py / build_feature_datasets.py - .gitignore: 允许提交 studies/*.db (Optuna DB) 和 reports/*.html (MT5 + dashboard)
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
"""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_<study>.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 <section> 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.<fn>(study), localize, return HTML fragment.
|
||||
|
||||
plotly.js is loaded ONCE via CDN <script> in ``<head>`` (see
|
||||
build_index_html). Every chart fragment therefore passes
|
||||
include_plotlyjs=False — no per-chart JS bundle, no async race, no 5 MB
|
||||
of inline JS blocking the parser before any chart can render.
|
||||
"""
|
||||
fn = getattr(optuna.visualization, fn_name, None)
|
||||
if fn is None:
|
||||
return f'<div class="chart-error">⚠ 函数 <code>{escape(fn_name)}</code> 不存在</div>'
|
||||
try:
|
||||
fig = fn(study)
|
||||
except Exception as e: # some plots fail on trivial studies
|
||||
return (f'<div class="chart-error">⚠ <code>{escape(fn_name)}</code> 生成失败:'
|
||||
f'{escape(str(e))}</div>')
|
||||
title_zh = next(t for fn_, t, *_ in CHARTS if fn_ == fn_name)
|
||||
x_zh = next(x for fn_, _, x, *_ in CHARTS if fn_ == fn_name)
|
||||
y_zh = next(y for fn_, _, _, y in CHARTS if fn_ == fn_name)
|
||||
fig = localize(fig, title_zh, x_zh, y_zh)
|
||||
fig.update_layout(height=520, width=1100)
|
||||
return fig.to_html(
|
||||
full_html=False,
|
||||
include_plotlyjs=False,
|
||||
div_id=f"chart-{fn_name}",
|
||||
)
|
||||
|
||||
|
||||
def render_slice_grid(study: optuna.Study, params: list[str]) -> list[tuple[str, str]]:
|
||||
"""Render one slice chart PER parameter — readable single-column subplots.
|
||||
|
||||
plot_slice(study) defaults to cramming all params into one 5400px-wide
|
||||
figure where axis labels overlap. Splitting per-param gives each a
|
||||
1100×520 card where labels are readable.
|
||||
"""
|
||||
fragments: list[tuple[str, str]] = []
|
||||
for p in params:
|
||||
try:
|
||||
fig = optuna.visualization.plot_slice(study, params=[p])
|
||||
except Exception as e:
|
||||
frag = (f'<div class="chart-error">⚠ slice[{escape(p)}] 生成失败:'
|
||||
f'{escape(str(e))}</div>')
|
||||
fragments.append((f"切片 — {p}", frag))
|
||||
continue
|
||||
fig = localize(fig, f"参数切片 — {p}", p, "目标值 Objective (score)")
|
||||
fig.update_layout(height=420, width=900, margin=dict(l=60, r=40, t=60, b=60))
|
||||
frag = fig.to_html(full_html=False, include_plotlyjs=False,
|
||||
div_id=f"slice-{p}")
|
||||
fragments.append((f"切片 — {p}", frag))
|
||||
return fragments
|
||||
|
||||
|
||||
def render_contour_grid(study: optuna.Study, params: list[str]) -> list[tuple[str, str]]:
|
||||
"""Render contour charts for the most important param pairs.
|
||||
|
||||
Full N×N contour is unreadable (19² = 361 subplots). Instead, take the
|
||||
top-K most important params (by fANOVA) and render only those pairs —
|
||||
a K×K grid that's actually readable.
|
||||
"""
|
||||
try:
|
||||
importances = optuna.importance.get_param_importances(study)
|
||||
# Get top-K by importance; only params that exist in our list.
|
||||
top_params = [p for p, _ in sorted(importances.items(),
|
||||
key=lambda x: x[1], reverse=True)
|
||||
if p in params][:6]
|
||||
except Exception:
|
||||
top_params = params[:6] # fallback: first 6
|
||||
|
||||
fragments: list[tuple[str, str]] = []
|
||||
# Render each pair (i<j) as its own contour chart.
|
||||
for i, p1 in enumerate(top_params):
|
||||
for p2 in top_params[i + 1:]:
|
||||
try:
|
||||
fig = optuna.visualization.plot_contour(study, params=[p1, p2])
|
||||
except Exception as e:
|
||||
frag = (f'<div class="chart-error">⚠ contour[{escape(p1)}×{escape(p2)}] '
|
||||
f'生成失败:{escape(str(e))}</div>')
|
||||
fragments.append((f"等高线 — {p1} × {p2}", frag))
|
||||
continue
|
||||
fig = localize(fig, f"等高线 — {p1} × {p2}", p1, p2)
|
||||
fig.update_layout(height=520, width=700,
|
||||
margin=dict(l=70, r=70, t=60, b=70))
|
||||
frag = fig.to_html(full_html=False, include_plotlyjs=False,
|
||||
div_id=f"contour-{p1}-{p2}")
|
||||
fragments.append((f"等高线 — {p1} × {p2}", frag))
|
||||
return fragments
|
||||
|
||||
|
||||
def build_index_html(
|
||||
study: optuna.Study,
|
||||
main_fragments: list[tuple[str, str]],
|
||||
slice_fragments: list[tuple[str, str]],
|
||||
contour_fragments: list[tuple[str, str]],
|
||||
) -> str:
|
||||
"""Assemble chart fragments into a single styled dashboard HTML."""
|
||||
n_trials = len(study.trials)
|
||||
completed = len([t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE])
|
||||
pruned = len([t for t in study.trials if t.state == optuna.trial.TrialState.PRUNED])
|
||||
failed = len([t for t in study.trials if t.state == optuna.trial.TrialState.FAIL])
|
||||
best = study.best_trial if study.best_trial is not None else None
|
||||
best_str = (
|
||||
f"trial #{best.number}, score={best.value:.4f}"
|
||||
if best is not None else "—"
|
||||
)
|
||||
|
||||
def cards(frs):
|
||||
return "\n".join(
|
||||
f'<section class="chart"><h2>{escape(title)}</h2>{frag}</section>'
|
||||
for title, frag in frs
|
||||
)
|
||||
|
||||
main_cards = cards(main_fragments)
|
||||
slice_cards = cards(slice_fragments)
|
||||
contour_cards = cards(contour_fragments)
|
||||
|
||||
slice_section = (
|
||||
f'<h2 class="section-title">参数切片图(单参数影响)</h2>'
|
||||
f'<p class="section-desc">每参数独立小图,避免 19 参数挤在 5400px 宽的复合图里导致标签重叠。</p>'
|
||||
f'{slice_cards}'
|
||||
if slice_fragments else ""
|
||||
)
|
||||
contour_section = (
|
||||
f'<h2 class="section-title">等高线图(参数两两交互)</h2>'
|
||||
f'<p class="section-desc">按 fANOVA 重要性 Top-6 参数两两配对,避免 19²=361 子图密集到无法读。</p>'
|
||||
f'{contour_cards}'
|
||||
if contour_fragments else ""
|
||||
)
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Optuna Dashboard — {escape(STUDY_NAME)}</title>
|
||||
<!-- plotly.js loaded via CDN, SYNCHRONOUSLY in <head> (no async/defer).
|
||||
Browser blocks parsing until this <script> finishes, so by the time
|
||||
the body's chart <script>Plotly.newPlot(...)</script> tags execute,
|
||||
window.Plotly is defined. -->
|
||||
<script src="https://cdn.plot.ly/plotly-3.6.0.min.js"></script>
|
||||
<style>
|
||||
:root {{
|
||||
--bg:#f5f5f7; --fg:#222; --card:#fff; --border:#ddd;
|
||||
--accent:#2563eb; --muted:#666;
|
||||
}}
|
||||
* {{ box-sizing: border-box; }}
|
||||
body {{
|
||||
margin: 0; padding: 2rem; background: var(--bg); color: var(--fg);
|
||||
font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif; line-height: 1.6;
|
||||
}}
|
||||
header {{ margin-bottom: 2rem; border-bottom: 2px solid var(--accent); padding-bottom: 1rem; }}
|
||||
h1 {{ margin: 0 0 .25rem; font-size: 1.75rem; }}
|
||||
h2 {{ margin: 0 0 .75rem; font-size: 1.25rem; color: var(--accent); }}
|
||||
.meta {{ display: flex; gap: 1.5rem; flex-wrap: wrap; color: var(--muted); font-size: .9rem; }}
|
||||
.meta b {{ color: var(--fg); }}
|
||||
.grid {{ display: flex; flex-direction: column; gap: 2rem; }}
|
||||
.chart {{
|
||||
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,.04);
|
||||
overflow-x: auto;
|
||||
}}
|
||||
.chart-error {{ color: #b00; padding: 1rem; background: #fff0f0; border-radius: 6px; }}
|
||||
.section-title {{
|
||||
margin: 3rem 0 0.5rem; padding-top: 1.5rem; border-top: 2px dashed var(--accent);
|
||||
font-size: 1.4rem; color: var(--accent);
|
||||
}}
|
||||
.section-desc {{ margin: 0 0 1.5rem; color: var(--muted); font-size: .9rem; }}
|
||||
footer {{ margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--border);
|
||||
color: var(--muted); font-size: .85rem; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Optuna 优化仪表盘</h1>
|
||||
<div class="meta">
|
||||
<span>研究名称:<b>{escape(STUDY_NAME)}</b></span>
|
||||
<span>试验总数:<b>{n_trials}</b></span>
|
||||
<span>完成:<b>{completed}</b></span>
|
||||
<span>剪枝:<b>{pruned}</b></span>
|
||||
<span>失败:<b>{failed}</b></span>
|
||||
<span>最佳:<b>{best_str}</b></span>
|
||||
</div>
|
||||
</header>
|
||||
<main class="grid">
|
||||
{main_cards}
|
||||
{slice_section}
|
||||
{contour_section}
|
||||
</main>
|
||||
<footer>
|
||||
生成时间:2026-06-26 · 来源:<code>{escape(str(STUDY_DB.relative_to(PROJECT)))}</code>
|
||||
· 框架:<a href="https://optuna.org">Optuna</a> + <a href="https://plotly.com/python">Plotly</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
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())
|
||||
Reference in New Issue
Block a user