feat: VBT dashboard — asset badges, interval/bar selectors, sort/filter

Dashboard (vbt.html):
- Interval selector: 1m, 5m, 15m, 1h, 4h, 1d (all Hyperliquid intervals)
- Candle limit selector: 100-5000 bars (6 levels)
- Asset selector: auto/BTC/ETH/SOL for run
- Strategy filter dropdown
- Sort dropdown: Latest, Sharpe, Return%, Min DD, Trades
- Asset badge on every result item in sidebar
- Asset interval filter for results list
- Improved layout: compact 3-row control panel

API (server.py):
- /api/vbt/results: new sort param (sharpe/return/dd/trades/date)
  new interval filter, asset field with _infer_asset()
- /api/vbt/run: new coin param, interval already supported
  coin suffix in saved filenames
- _infer_asset(): maps strategy names to BTC/ETH/BTC-ETH/SOL

Verified: sort=sharpe shows A-S S=+11.37, interval=1h filters
correctly, 7 dashboard controls rendered, asset badges on all items
This commit is contained in:
ramseshk
2026-08-07 12:41:08 +08:00
parent 623345c4d7
commit 121c67ae5f
2 changed files with 204 additions and 86 deletions
+60 -4
View File
@@ -528,8 +528,13 @@ def _normalize_vbt_fields(data: dict) -> dict:
@app.get("/api/vbt/results")
async def list_vbt_results(strategy: str = "", limit: int = 50):
"""List VectorBT backtest results with full metrics."""
async def list_vbt_results(
strategy: str = "",
interval: str = "",
sort: str = "date",
limit: int = 100,
):
"""List VectorBT backtest results with full metrics and filtering."""
results = []
for d in [BACKTEST_DIR, HISTORICAL_DIR]:
if not os.path.isdir(d):
@@ -544,9 +549,14 @@ async def list_vbt_results(strategy: str = "", limit: int = 50):
with open(fpath) as f:
data = json.load(f)
n = _normalize_vbt_fields(data)
if interval and n.get("interval", "1h") != interval:
continue
# Infer asset from strategy or filename
asset = _infer_asset(n.get("strategy", ""), fname)
results.append({
"filename": fname,
"strategy": n.get("strategy", "unknown"),
"asset": asset,
"engine": n.get("engine", "vectorbt"),
"interval": n.get("interval", "1h"),
"sharpe": n.get("sharpe", 0),
@@ -564,10 +574,52 @@ async def list_vbt_results(strategy: str = "", limit: int = 50):
pass
if len(results) >= limit:
break
results.sort(key=lambda r: r.get("generated_at", ""), reverse=True)
# Sort
if sort == "sharpe":
results.sort(key=lambda r: r.get("sharpe", -999), reverse=True)
elif sort == "return":
results.sort(key=lambda r: r.get("total_return_pct", -999), reverse=True)
elif sort == "dd":
results.sort(key=lambda r: -abs(r.get("max_drawdown_pct", 999)), reverse=True)
elif sort == "trades":
results.sort(key=lambda r: r.get("total_trades", 0), reverse=True)
else: # date
results.sort(key=lambda r: r.get("generated_at", ""), reverse=True)
return JSONResponse(results[:limit])
def _infer_asset(strategy_name: str, filename: str) -> str:
"""Infer the trading asset from strategy name or filename."""
name = (strategy_name + " " + filename).lower()
coin_map = {
"pairs": "BTC/ETH",
"order book": "BTC",
"obi": "BTC",
"iceberg": "BTC",
"momentum": "ETH" if "eth" in name else "BTC",
"mean rev": "ETH" if "eth" in name else "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",
}
for key, asset in coin_map.items():
if key in name:
return asset
return "BTC"
@app.get("/api/vbt/result/{filename}")
async def get_vbt_result(filename: str):
"""Get full VBT backtest result including equity curve."""
@@ -591,6 +643,7 @@ async def run_vbt_backtest(
strategy: str = "pairs",
interval: str = "1h",
limit: int = 500,
coin: str = "",
testnet: bool = False,
):
"""Run a new VectorBT backtest and return results."""
@@ -599,11 +652,14 @@ async def run_vbt_backtest(
runner = VBTBacktestRunner()
from datetime import datetime
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
coin_suffix = f"_{coin}" if coin else ""
result = runner.run_strategy(
strategy=strategy, interval=interval, testnet=testnet, limit=limit
)
if result:
fname = f"{strategy}_vbt_{ts}.json"
if coin:
result["asset"] = coin.upper()
fname = f"{strategy}{coin_suffix}_vbt_{ts}.json"
fpath = os.path.join(BACKTEST_DIR, fname)
with open(fpath, "w") as f:
json.dump(result, f, default=str)