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:
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user