feat: VBT dashboard overview — strategy cards + heatmap + /api/vbt/summary

dashboard/server.py — new /api/vbt/summary endpoint:
  - Aggregated per-strategy best stats (Sharpe, Return, Calmar, trades,
    positive-Sharpe %, top combo details)
  - Heatmap: strategy × interval matrix with best Sharpe per cell

dashboard/static/vbt.html — overview panel replaces empty state:
  - Strategy cards grid: best Sharpe, best return, top combo,
    positive-Sharpe %, run count. Click to filter sidebar.
  - Heatmap: color-coded strategy × interval table (green=good,
    red=bad). Hover for tooltip, click to load result.
  - Back-to-overview button when viewing a strategy detail
  - Overview re-shown on page load, auto-loads from /api/vbt/summary

Dashboard opens to overview → select strategy → drill into details
This commit is contained in:
ramseshk
2026-08-10 12:03:49 +08:00
parent 140b0cc360
commit b7c7fbb0af
2 changed files with 154 additions and 3 deletions
+65
View File
@@ -875,6 +875,71 @@ async def run_vbt_sweep(strategy: str = "pairs"):
return JSONResponse({"error": str(e)}, status_code=500)
@app.get("/api/vbt/summary")
async def vbt_summary(sort_by: str = "sharpe"):
"""Aggregated stats: per-strategy best, per-interval count, heatmap data."""
_refresh_vbt_cache()
all_results = list(_vbt_meta_cache.values())
if not all_results:
return JSONResponse({"error": "no results"}, status_code=404)
# Per-strategy best
strategies = {}
for r in all_results:
s = r.get("strategy", "?")
if s not in strategies:
strategies[s] = []
strategies[s].append(r)
best_by_strat = {}
for s, results in strategies.items():
by_sharpe = sorted(results, key=lambda x: x.get("sharpe", -999), reverse=True)[:3]
best_by_strat[s] = {
"count": len(results),
"best_sharpe": max(r.get("sharpe", -999) for r in results),
"best_return": max(r.get("total_return_pct", -999) for r in results),
"best_calmar": max(r.get("calmar", -999) for r in results),
"avg_trades": round(sum(r.get("total_trades", 0) for r in results) / len(results), 1),
"positive_sharpe_pct": round(sum(1 for r in results if r.get("sharpe", 0) > 0) / len(results) * 100, 1),
"top_combo": {
"interval": by_sharpe[0].get("interval", "?"),
"limit": by_sharpe[0].get("n_bars", 0),
"sharpe": by_sharpe[0].get("sharpe", 0),
"return": by_sharpe[0].get("total_return_pct", 0),
},
}
# Heatmap: strategy × interval
intervals_set = sorted(set(r.get("interval", "?") for r in all_results))
heatmap = {}
for r in all_results:
s = r.get("strategy", "?")
iv = r.get("interval", "?")
if s not in heatmap:
heatmap[s] = {}
existing = heatmap[s].get(iv)
sh = r.get("sharpe", -999)
if existing is None or sh > existing.get("sharpe", -999):
heatmap[s][iv] = {
"interval": iv,
"sharpe": r.get("sharpe", 0),
"return_pct": r.get("total_return_pct", 0),
"win_rate": r.get("win_rate", 0),
"trades": r.get("total_trades", 0),
"n_bars": r.get("n_bars", 0),
}
return JSONResponse({
"total_results": len(all_results),
"strategy_count": len(strategies),
"interval_count": len(intervals_set),
"intervals": intervals_set,
"best_by_strategy": best_by_strat,
"heatmap_best_sharpe": heatmap,
})
@app.get("/api/vbt/strategies")
async def list_vbt_strategies():
"""List available strategies for VBT backtesting."""