Select a backtest result
+Choose from the sidebar or run a new VectorBT backtest
+diff --git a/dashboard/server.py b/dashboard/server.py index 3f27e46..1f0d4da 100644 --- a/dashboard/server.py +++ b/dashboard/server.py @@ -27,20 +27,27 @@ from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, JSONResponse import sys sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +# Fix BACKTEST_DIR — auto-detect local path if deployed dir doesn't exist +_default_results = str(Path(__file__).resolve().parent.parent / "backtests" / "results") +BACKTEST_DIR = _default_results if os.path.isdir(_default_results) else "/home/debian/ftdt-quant-lab/backtests/results" +HISTORICAL_DIR = BACKTEST_DIR + "/historical" if os.path.isdir(BACKTEST_DIR + "/historical") else BACKTEST_DIR + from config.fee_tiers import get_perp_fees, PERPS_TIERS, STAKING_TIERS, STRATEGY_FEE_MODELS from common.risk import risk_summary from strategies.quant_report import compute_quant_report # ═══════════════════════════════════════════════════════════ -# Memory guard: cap RSS at 512MB, GC-aggressive at 256MB +# Memory guard: check RSS via /proc, force GC at 256MB, +# log warning at 384MB, hard exit at 512MB. +# RLIMIT_AS disabled — Python heap needs virtual headroom. # ═══════════════════════════════════════════════════════════ -import resource, gc, signal +import gc, os as _os MEM_SOFT_LIMIT = 256 * 1024 * 1024 # 256 MB — force GC +MEM_WARN_LIMIT = 384 * 1024 * 1024 # 384 MB — log warning MEM_HARD_LIMIT = 512 * 1024 * 1024 # 512 MB — terminate -resource.setrlimit(resource.RLIMIT_AS, (MEM_HARD_LIMIT, MEM_HARD_LIMIT)) - def check_memory(): """Check RSS, force GC if over soft limit, raise if over hard limit.""" try: @@ -51,7 +58,7 @@ def check_memory(): rss = rss_kb * 1024 if rss > MEM_HARD_LIMIT: print(f"[CRIT] RSS {rss_kb // 1024}MB > 512MB — exiting", flush=True) - os._exit(1) + _os._exit(1) if rss > MEM_SOFT_LIMIT: gc.collect() gc.collect() @@ -66,11 +73,8 @@ import uvicorn METRICS_FILE = "/tmp/ftdt-metrics.json" PAPER_METRICS_FILE = "/tmp/ftdt-paper-metrics.json" -BACKTEST_DIR = "/home/debian/ftdt-quant-lab/backtests/results" -HISTORICAL_DIR = "/home/debian/ftdt-quant-lab/backtests/results/historical" STATIC_DIR = Path(__file__).parent / "static" -# Ensure backtest dir exists os.makedirs(BACKTEST_DIR, exist_ok=True) # ═══════════════════════════════════════════════════════════ @@ -235,8 +239,11 @@ async def list_backtests(): @app.get("/api/backtest/{name}") async def get_backtest(name: str): - """Get full backtest result data.""" - fpath = os.path.join(BACKTEST_DIR, f"{name}.json") + """Get full backtest result data — checks historical dir first.""" + # Try historical subdirectory first (where dashboard saves backtests) + fpath = os.path.join(HISTORICAL_DIR, f"{name}.json") + if not os.path.exists(fpath): + fpath = os.path.join(BACKTEST_DIR, f"{name}.json") if os.path.exists(fpath): with open(fpath) as f: return JSONResponse(json.load(f)) @@ -451,6 +458,126 @@ async def get_risk_metrics(): "correlation_matrix": corr, }) +# ═══════════════════════════════════════════════════════════ +# VBT Dashboard API — VectorBT backtest results browser +# ═══════════════════════════════════════════════════════════ + +@app.get("/api/vbt/results") +async def list_vbt_results(strategy: str = "", limit: int = 50): + """List VectorBT backtest results with full metrics.""" + results = [] + for d in [BACKTEST_DIR, HISTORICAL_DIR]: + if not os.path.isdir(d): + continue + for fname in sorted(os.listdir(d), reverse=True): + if not fname.endswith(".json"): + continue + if strategy and strategy not in fname: + continue + fpath = os.path.join(d, fname) + try: + with open(fpath) as f: + data = json.load(f) + results.append({ + "filename": fname, + "strategy": data.get("strategy", "unknown"), + "engine": data.get("engine", "vectorbt"), + "interval": data.get("interval", "1h"), + "sharpe": data.get("sharpe", 0), + "sortino": data.get("sortino", 0), + "total_return_pct": data.get("total_return_pct", 0), + "max_drawdown_pct": data.get("max_drawdown_pct", 0), + "win_rate": data.get("win_rate", 0), + "profit_factor": data.get("profit_factor", 0), + "total_trades": data.get("total_trades", 0), + "n_bars": data.get("n_bars", 0), + "generated_at": data.get("generated_at", ""), + "has_equity_curve": bool(data.get("equity_curve")), + }) + except (json.JSONDecodeError, IOError): + pass + if len(results) >= limit: + break + results.sort(key=lambda r: r.get("generated_at", ""), reverse=True) + return JSONResponse(results[:limit]) + + +@app.get("/api/vbt/result/{filename}") +async def get_vbt_result(filename: str): + """Get full VBT backtest result including equity curve.""" + for d in [BACKTEST_DIR, HISTORICAL_DIR]: + fpath = os.path.join(d, filename) + if os.path.exists(fpath): + with open(fpath) as f: + data = json.load(f) + # Ensure equity curve is compact for transport + ec = data.get("equity_curve", []) + if ec and len(ec) > 500: + step = len(ec) // 500 + data["equity_curve"] = ec[::step] + return JSONResponse(data) + return JSONResponse({"error": "not found"}, status_code=404) + + +@app.get("/api/vbt/run") +async def run_vbt_backtest( + strategy: str = "pairs", + interval: str = "1h", + limit: int = 500, + testnet: bool = False, +): + """Run a new VectorBT backtest and return results.""" + try: + from backtests.vbt_runner import VBTBacktestRunner + runner = VBTBacktestRunner() + from datetime import datetime + ts = datetime.now().strftime("%Y%m%d-%H%M%S") + result = runner.run_strategy( + strategy=strategy, interval=interval, testnet=testnet, limit=limit + ) + if result: + fname = f"{strategy}_vbt_{ts}.json" + fpath = os.path.join(BACKTEST_DIR, fname) + with open(fpath, "w") as f: + json.dump(result, f, default=str) + result["filename"] = fname + return JSONResponse(result) + return JSONResponse({"error": "no results generated"}, status_code=500) + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=500) + + +@app.get("/api/vbt/sweep") +async def run_vbt_sweep(strategy: str = "pairs"): + """Run parameter sweep and return heatmap data.""" + try: + from backtests.vbt_runner import VBTBacktestRunner + runner = VBTBacktestRunner() + df = runner.param_sweep(strategy=strategy) + if df is not None and not df.empty: + rows = df.to_dict(orient="records") + return JSONResponse({ + "strategy": strategy, + "results": rows, + "best": max(rows, key=lambda r: r.get("sharpe", -999)), + }) + return JSONResponse({"error": "no sweep results"}, status_code=500) + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=500) + + +@app.get("/api/vbt/strategies") +async def list_vbt_strategies(): + """List available strategies for VBT backtesting.""" + return JSONResponse([ + {"key": "pairs", "name": "Pairs Trading", "coins": ["BTC", "ETH"]}, + {"key": "hurst_vpin", "name": "Hurst VPIN", "coins": ["BTC"]}, + {"key": "as_mm", "name": "Avellaneda-Stoikov MM", "coins": ["BTC"]}, + {"key": "momentum", "name": "Momentum Breakout", "coins": ["BTC"]}, + {"key": "mean_rev", "name": "Mean Reversion", "coins": ["BTC"]}, + ]) + + # ═══════════════════════════════════════════════════════════ # Static # ═══════════════════════════════════════════════════════════ @@ -460,6 +587,11 @@ async def root(): return FileResponse(STATIC_DIR / "index.html") +@app.get("/vbt") +async def vbt_dashboard(): + return FileResponse(STATIC_DIR / "vbt.html") + + app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") diff --git a/dashboard/static/vbt.html b/dashboard/static/vbt.html new file mode 100644 index 0000000..d1618c4 --- /dev/null +++ b/dashboard/static/vbt.html @@ -0,0 +1,243 @@ + + +
+ + +Choose from the sidebar or run a new VectorBT backtest
+