Files
ftdt-quant-lab/dashboard/vbt_server.py
T
ramseshk 887a33f278 feat: trade log table, strategy params panel, B+W color scheme
Dashboard:
- Trade log table: all trades with time, side, size, entry/exit price, PnL, duration
  in scrollable panel below charts
- Strategy params panel: displays all coefficients (z_entry, gamma, obi_entry,
  grid_levels, etc.) for the selected strategy
- Color scheme: professional black/white
  • positive: #03A9F4 (light blue)
  • negative: #FF5252 (red)
  • neutral: #777 (gray)
  • backgrounds: #0a0a0a / #111 / #181818
  • borders: #222 / #333

VBT runner:
- _extract_metrics now captures trades from pf.trades.records_readable
  (Avg Entry Price, Avg Exit Price, PnL, Return, Duration, Direction)
- _strategy_params() returns key coefficients per strategy type
- _empty_result includes empty trades/params

New vbt_server.py: minimal standalone dashboard (no live trading machinery,
no memory guard, no broadcast loop) — avoids crashing issues
2026-08-07 12:53:52 +08:00

205 lines
8.4 KiB
Python

"""
Minimal VBT dashboard server — no live trading, no memory guard, no broadcast.
Just serves the VBT dashboard HTML and backtest API endpoints.
"""
import json, os, sys
from pathlib import Path
from datetime import datetime
project_root = str(Path(__file__).resolve().parent.parent)
sys.path.insert(0, project_root)
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, JSONResponse
RESULTS_DIR = Path(project_root) / "backtests" / "results"
HISTORICAL_DIR = RESULTS_DIR / "historical"
STATIC_DIR = Path(project_root) / "dashboard" / "static"
os.makedirs(RESULTS_DIR, exist_ok=True)
app = FastAPI(title="FTDT VBT Dashboard")
# ── Field normalization ──────────────────────────────────────────
def _normalize(data: dict) -> dict:
out = dict(data)
if "total_return_pct" not in out:
out["total_return_pct"] = out.get("pnl_pct", out.get("ann_return_pct", 0))
if out.get("total_return_pct") is None:
out["total_return_pct"] = 0
if "max_drawdown_pct" not in out:
dd = out.get("max_dd_pct", out.get("max_dd"))
if dd is not None and isinstance(dd, (int, float)) and abs(dd) < 1:
dd = dd * 100
out["max_drawdown_pct"] = dd or 0
if out.get("max_drawdown_pct") is None:
out["max_drawdown_pct"] = 0
if "n_bars" not in out:
out["n_bars"] = out.get("num_periods", 0)
if out.get("n_bars") is None:
out["n_bars"] = 0
if "profit_factor" not in out:
trades = out.get("trades", [])
if trades:
gross_win = sum(t.get("pnl", t.get("pnl_net", t.get("pnl_gross", 0))) or 0
for t in trades if (t.get("pnl", t.get("pnl_net", t.get("pnl_gross", 0))) or 0) > 0)
gross_loss = abs(sum(t.get("pnl", t.get("pnl_net", t.get("pnl_gross", 0))) or 0
for t in trades if (t.get("pnl", t.get("pnl_net", t.get("pnl_gross", 0))) or 0) < 0))
out["profit_factor"] = round(gross_win / gross_loss, 3) if gross_loss > 0 else 0
else:
out["profit_factor"] = 0
if "total_trades" not in out:
out["total_trades"] = len(out.get("trades", []))
if out.get("total_trades") is None:
out["total_trades"] = 0
if not out.get("win_rate") and "trades" in out:
trades = out.get("trades", [])
if trades:
wins = sum(1 for t in trades if (t.get("pnl", t.get("pnl_net", t.get("pnl_gross", 0))) or 0) > 0)
out["win_rate"] = round(wins / len(trades), 3)
return out
def _infer_asset(strategy_name: str, filename: str) -> str:
name = (strategy_name + " " + filename).lower()
for key, asset in {
"pairs": "BTC/ETH", "order book": "BTC", "obi": "BTC",
"iceberg": "BTC", "hurst": "BTC", "vpin": "BTC",
"avellaneda": "BTC", "as_mm": "BTC", "grid": "BTC",
"composite": "BTC", "funding": "BTC", "kalman": "BTC/ETH",
"cartea": "BTC", "gueant": "BTC", "hawkes": "BTC",
"deep lob": "BTC", "queue": "BTC",
}.items():
if key in name:
return asset
return "BTC" if "btc" in name or "eth" not in name else "ETH"
# ── REST API ────────────────────────────────────────────────────
@app.get("/api/vbt/results")
async def list_results(strategy: str = "", interval: str = "", sort: str = "date", limit: int = 200):
results = []
for d in [RESULTS_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
try:
with open(os.path.join(d, fname)) as f:
n = _normalize(json.load(f))
if interval and n.get("interval", "1h") != interval:
continue
results.append({
"filename": fname,
"strategy": n.get("strategy", "unknown"),
"asset": _infer_asset(n.get("strategy", ""), fname),
"engine": n.get("engine", "vectorbt"),
"interval": n.get("interval", "1h"),
"sharpe": n.get("sharpe", 0),
"sortino": n.get("sortino", 0),
"total_return_pct": n["total_return_pct"],
"max_drawdown_pct": n["max_drawdown_pct"],
"win_rate": n.get("win_rate", 0),
"profit_factor": n["profit_factor"],
"total_trades": n["total_trades"],
"n_bars": n["n_bars"],
"generated_at": n.get("generated_at", ""),
"has_equity_curve": bool(n.get("equity_curve")),
})
except (json.JSONDecodeError, IOError):
pass
if len(results) >= limit:
break
sort_keys = {
"sharpe": ("sharpe", True), "return": ("total_return_pct", True),
"dd": ("max_drawdown_pct", False), "trades": ("total_trades", True),
}
if sort in sort_keys:
key, rev = sort_keys[sort]
results.sort(key=lambda r: r.get(key, -999 if rev else 999), reverse=rev)
else:
results.sort(key=lambda r: r.get("generated_at", ""), reverse=True)
return JSONResponse(results[:limit])
@app.get("/api/vbt/result/{filename}")
async def get_result(filename: str):
for d in [RESULTS_DIR, HISTORICAL_DIR]:
fpath = os.path.join(d, filename)
if os.path.exists(fpath):
with open(fpath) as f:
data = _normalize(json.load(f))
ec = data.get("equity_curve", [])
if ec and len(ec) > 500:
data["equity_curve"] = ec[::len(ec)//500]
return JSONResponse(data)
return JSONResponse({"error": "not found"}, status_code=404)
@app.get("/api/vbt/run")
async def run_backtest(strategy: str = "pairs", interval: str = "1h", limit: int = 500, coin: str = ""):
try:
from backtests.vbt_runner import VBTBacktestRunner
runner = VBTBacktestRunner()
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
result = runner.run_strategy(strategy=strategy, interval=interval, limit=limit)
if result:
if coin:
result["asset"] = coin.upper()
fname = f"{strategy}_{'' if not coin else coin+'_'}vbt_{ts}.json"
fpath = RESULTS_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"}, status_code=500)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
@app.get("/api/vbt/strategies")
async def list_strategies():
return JSONResponse([
{"key": "pairs", "name": "Pairs Trading", "coins": ["BTC", "ETH"]},
{"key": "hurst_vpin", "name": "Hurst VPIN", "coins": ["BTC"]},
{"key": "as_mm", "name": "Avellaneda-Stoikov", "coins": ["BTC"]},
{"key": "obi", "name": "Order Book Imbalance", "coins": ["BTC"]},
{"key": "grid_mm", "name": "Grid Market Making", "coins": ["BTC"]},
{"key": "composite_mm", "name": "Composite MM", "coins": ["BTC"]},
{"key": "iceberg", "name": "Iceberg Detection", "coins": ["BTC"]},
])
# ── Static ──────────────────────────────────────────────────────
@app.get("/vbt")
async def vbt_page():
return FileResponse(STATIC_DIR / "vbt.html")
@app.get("/")
async def root():
return FileResponse(STATIC_DIR / "vbt.html")
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# ── Main ────────────────────────────────────────────────────────
if __name__ == "__main__":
import uvicorn, argparse
p = argparse.ArgumentParser()
p.add_argument("--port", type=int, default=9175)
p.add_argument("--host", default="0.0.0.0")
args = p.parse_args()
print(f"VBT Dashboard → http://{args.host}:{args.port}/vbt")
uvicorn.run(app, host=args.host, port=args.port, log_level="error")