SELECT A BACKTEST
-Choose from the sidebar or configure and run a new test
+diff --git a/dashboard/server.py b/dashboard/server.py index 83c8a6c..1c1d7ce 100644 --- a/dashboard/server.py +++ b/dashboard/server.py @@ -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.""" diff --git a/dashboard/static/vbt.html b/dashboard/static/vbt.html index e25faa2..5964d1d 100644 --- a/dashboard/static/vbt.html +++ b/dashboard/static/vbt.html @@ -73,6 +73,20 @@ select:focus{outline:none;border-color:#555} .export-row{display:flex;gap:6px;margin-bottom:6px} .progress-bar{height:4px;background:#03A9F4;border-radius:2px;transition:width .3s;margin-top:4px} .run-status{font-size:10px;color:#f59e0b;padding:4px 0} +.heatmap-table{border-collapse:collapse;font-size:10px;font-family:monospace;width:100%} +.heatmap-table th{text-align:center;padding:3px 6px;color:#555;font-weight:500;font-size:8px;text-transform:uppercase;letter-spacing:1px} +.heatmap-table th:first-child{text-align:left;min-width:130px} +.heatmap-table td{text-align:center;padding:3px 6px;border:1px solid #1a1a1a;font-size:10px} +.heatmap-table td:first-child{text-align:left;color:#aaa;font-size:10px} +.heatmap-cell{cursor:pointer;transition:all .12s;border-radius:2px} +.heatmap-cell:hover{outline:1px solid #555;z-index:1;position:relative} +.strat-summary{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:6px;margin-bottom:10px} +.strat-card{background:#111;border:1px solid #222;border-radius:3px;padding:7px 9px;cursor:pointer;transition:all .12s} +.strat-card:hover{border-color:#444;background:#161616} +.strat-card .strat-name{font-size:10px;font-weight:600;color:#ccc;margin-bottom:4px} +.strat-card .strat-row{display:flex;justify-content:space-between;font-size:9px;padding:1px 0;color:#666} +.strat-card .strat-row .val{font-family:monospace;color:#888} +.strat-card .strat-row .val.pos{color:#03A9F4}.strat-card .strat-row .val.neg{color:#FF5252}
@@ -179,9 +193,9 @@ select:focus{outline:none;border-color:#555}Choose from the sidebar or configure and run a new test
+| '; + for(const iv of intervals)hmap+=' | '+iv+' | '; + hmap+='
|---|---|
| '+s+' | '; + for(const iv of intervals){ + const cell=(heatmap[s]||{})[iv]||{}; + const sh=cell.sharpe||0; + let bg='#111',clr='#555'; + if(sh>2){bg='#0a2a0a';clr='#00e676'} + else if(sh>0.5){bg='#0a1a0a';clr='#4caf50'} + else if(sh>0){bg='#1a1a0a';clr='#aaa'} + else if(sh>-0.5){bg='#1a0a0a';clr='#aa5555'} + else {bg='#2a0a0a';clr='#ff1744'} + hmap+=''+sh.toFixed(1)+' | '; + } + hmap+='