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
+59 -3
View File
@@ -528,8 +528,13 @@ def _normalize_vbt_fields(data: dict) -> dict:
@app.get("/api/vbt/results") @app.get("/api/vbt/results")
async def list_vbt_results(strategy: str = "", limit: int = 50): async def list_vbt_results(
"""List VectorBT backtest results with full metrics.""" strategy: str = "",
interval: str = "",
sort: str = "date",
limit: int = 100,
):
"""List VectorBT backtest results with full metrics and filtering."""
results = [] results = []
for d in [BACKTEST_DIR, HISTORICAL_DIR]: for d in [BACKTEST_DIR, HISTORICAL_DIR]:
if not os.path.isdir(d): 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: with open(fpath) as f:
data = json.load(f) data = json.load(f)
n = _normalize_vbt_fields(data) 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({ results.append({
"filename": fname, "filename": fname,
"strategy": n.get("strategy", "unknown"), "strategy": n.get("strategy", "unknown"),
"asset": asset,
"engine": n.get("engine", "vectorbt"), "engine": n.get("engine", "vectorbt"),
"interval": n.get("interval", "1h"), "interval": n.get("interval", "1h"),
"sharpe": n.get("sharpe", 0), "sharpe": n.get("sharpe", 0),
@@ -564,10 +574,52 @@ async def list_vbt_results(strategy: str = "", limit: int = 50):
pass pass
if len(results) >= limit: if len(results) >= limit:
break break
# 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) results.sort(key=lambda r: r.get("generated_at", ""), reverse=True)
return JSONResponse(results[:limit]) 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}") @app.get("/api/vbt/result/{filename}")
async def get_vbt_result(filename: str): async def get_vbt_result(filename: str):
"""Get full VBT backtest result including equity curve.""" """Get full VBT backtest result including equity curve."""
@@ -591,6 +643,7 @@ async def run_vbt_backtest(
strategy: str = "pairs", strategy: str = "pairs",
interval: str = "1h", interval: str = "1h",
limit: int = 500, limit: int = 500,
coin: str = "",
testnet: bool = False, testnet: bool = False,
): ):
"""Run a new VectorBT backtest and return results.""" """Run a new VectorBT backtest and return results."""
@@ -599,11 +652,14 @@ async def run_vbt_backtest(
runner = VBTBacktestRunner() runner = VBTBacktestRunner()
from datetime import datetime from datetime import datetime
ts = datetime.now().strftime("%Y%m%d-%H%M%S") ts = datetime.now().strftime("%Y%m%d-%H%M%S")
coin_suffix = f"_{coin}" if coin else ""
result = runner.run_strategy( result = runner.run_strategy(
strategy=strategy, interval=interval, testnet=testnet, limit=limit strategy=strategy, interval=interval, testnet=testnet, limit=limit
) )
if result: 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) fpath = os.path.join(BACKTEST_DIR, fname)
with open(fpath, "w") as f: with open(fpath, "w") as f:
json.dump(result, f, default=str) json.dump(result, f, default=str)
+132 -70
View File
@@ -8,49 +8,48 @@
<style> <style>
*{margin:0;padding:0;box-sizing:border-box} *{margin:0;padding:0;box-sizing:border-box}
body{font-family:'SF Mono','Cascadia Code','Ubuntu Mono',monospace;background:#090d14;color:#a8b2c0;min-height:100vh;overflow:hidden} body{font-family:'SF Mono','Cascadia Code','Ubuntu Mono',monospace;background:#090d14;color:#a8b2c0;min-height:100vh;overflow:hidden}
.topbar{background:#0d1321;border-bottom:1px solid #1a2332;padding:10px 20px;display:flex;justify-content:space-between;align-items:center;height:44px} .topbar{background:#0d1321;border-bottom:1px solid #1a2332;padding:8px 20px;display:flex;justify-content:space-between;align-items:center;height:40px}
.topbar h1{font-size:14px;font-weight:600;color:#e0e6ed;letter-spacing:1px} .topbar h1{font-size:13px;font-weight:600;color:#e0e6ed;letter-spacing:1px}
.topbar .dot{display:inline-block;width:7px;height:7px;background:#22c55e;border-radius:50%;margin-right:6px;animation:pulse 2s infinite} .topbar .dot{display:inline-block;width:6px;height:6px;background:#22c55e;border-radius:50%;margin-right:5px;animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.topbar .status{font-size:11px;color:#4ade80} .topbar .status{font-size:10px;color:#4ade80}
.main{display:grid;grid-template-columns:280px 1fr;height:calc(100vh - 44px)} .main{display:grid;grid-template-columns:310px 1fr;height:calc(100vh - 40px)}
.sidebar{background:#0b1019;border-right:1px solid #1a2332;overflow-y:auto;padding:8px} .sidebar{background:#0b1019;border-right:1px solid #1a2332;overflow-y:auto;padding:6px}
.sidebar h3{font-size:10px;text-transform:uppercase;color:#445265;margin:10px 0 6px;letter-spacing:2px} .controls{padding:6px;border-bottom:1px solid #1a2332;margin-bottom:4px}
.toolbar{display:flex;gap:6px;flex-direction:column;margin-bottom:8px} .controls .row{display:flex;gap:4px;margin-bottom:4px}
select{background:#111827;border:1px solid #1e293b;color:#c8d6e5;border-radius:4px;padding:7px 10px;font-size:12px;font-family:inherit;width:100%;cursor:pointer} .controls .row:last-child{margin-bottom:0}
select{background:#111827;border:1px solid #1e293b;color:#c8d6e5;border-radius:3px;padding:5px 6px;font-size:10px;font-family:inherit;cursor:pointer;flex:1;min-width:0}
select:focus{outline:none;border-color:#3b82f6} select:focus{outline:none;border-color:#3b82f6}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:7px 12px;border-radius:4px;font-size:12px;cursor:pointer;border:1px solid #1e293b;background:#111827;color:#a8b2c0;font-family:inherit;transition:all .12s;width:100%} .btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;padding:5px 8px;border-radius:3px;font-size:10px;cursor:pointer;border:1px solid #1e293b;background:#111827;color:#a8b2c0;font-family:inherit;transition:all .12s;white-space:nowrap}
.btn:hover{background:#1a2744;border-color:#3b82f6} .btn:hover{background:#1a2744;border-color:#3b82f6}
.btn-primary{background:#1d4ed8;border-color:#2563eb;color:#e0e6ed} .btn-primary{background:#1d4ed8;border-color:#2563eb;color:#e0e6ed}
.btn-primary:hover{background:#1e40af} .btn-primary:hover{background:#1e40af}
.result-item{background:#111827;border:1px solid #1a2332;border-left:3px solid transparent;border-radius:4px;padding:8px 10px;margin-bottom:4px;cursor:pointer;transition:all .12s} .btn:disabled{opacity:.5;cursor:not-allowed}
.result-item{background:#111827;border:1px solid #1a2332;border-left:3px solid transparent;border-radius:3px;padding:7px 8px;margin-bottom:3px;cursor:pointer;transition:all .12s}
.result-item:hover{border-color:#334155;background:#151e30} .result-item:hover{border-color:#334155;background:#151e30}
.result-item.active{border-color:#3b82f6;border-left-color:#3b82f6;background:#121b2d} .result-item.active{border-color:#3b82f6;border-left-color:#3b82f6;background:#121b2d}
.result-item .name{font-size:12px;font-weight:600;color:#c8d6e5} .result-item .name{font-size:11px;font-weight:600;color:#c8d6e5;display:flex;justify-content:space-between;align-items:center}
.result-item .meta{font-size:10px;color:#445265;margin-top:2px} .result-item .name .asset{font-size:9px;color:#6366f1;background:#1e1b4b50;border:1px solid #312e81;border-radius:3px;padding:1px 5px}
.result-item .stats{display:flex;gap:12px;margin-top:4px;font-size:10px;font-family:monospace} .result-item .meta{font-size:9px;color:#445265;margin-top:2px}
.result-item .stats{display:flex;gap:10px;margin-top:3px;font-size:9px;font-family:monospace}
.pos{color:#4ade80}.neg{color:#f87171}.neu{color:#64748b} .pos{color:#4ade80}.neg{color:#f87171}.neu{color:#64748b}
.sidebar h3{font-size:9px;text-transform:uppercase;color:#445265;margin:8px 0 4px;letter-spacing:2px;padding:0 4px}
.badge{font-size:9px;border-radius:3px;padding:1px 4px;margin-left:3px;font-weight:400}
.content{padding:24px 32px;overflow-y:auto} .content{padding:20px 28px;overflow-y:auto}
.content h2{font-size:16px;color:#e0e6ed;margin-bottom:4px} .content h2{font-size:15px;color:#e0e6ed;margin-bottom:2px}
.content .sub{font-size:11px;color:#445265;margin-bottom:20px} .content .sub{font-size:10px;color:#445265;margin-bottom:16px}
.metrics-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px} .metrics-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:12px}
.metric{background:#0d1321;border:1px solid #1a2332;border-radius:6px;padding:14px 16px} .metric{background:#0d1321;border:1px solid #1a2332;border-radius:4px;padding:12px 14px}
.metric .label{font-size:9px;text-transform:uppercase;color:#445265;letter-spacing:1.5px;margin-bottom:4px} .metric .label{font-size:8px;text-transform:uppercase;color:#445265;letter-spacing:1.5px;margin-bottom:3px}
.metric .value{font-size:22px;font-weight:700;font-family:monospace} .metric .value{font-size:20px;font-weight:700;font-family:monospace}
.chart-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px}
.chart-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px} .chart-box{background:#0d1321;border:1px solid #1a2332;border-radius:4px;padding:12px}
.chart-box{background:#0d1321;border:1px solid #1a2332;border-radius:6px;padding:14px} .chart-box h4{font-size:9px;color:#445265;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:2px}
.chart-box h4{font-size:10px;color:#445265;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:4px}
.chart-box.full{grid-column:1/-1} .chart-box.full{grid-column:1/-1}
.trade-table{width:100%;border-collapse:collapse;font-size:11px;margin-top:8px;font-family:monospace}
.trade-table th{text-align:left;padding:6px 10px;border-bottom:1px solid #1e293b;color:#445265;font-weight:500;font-size:10px;text-transform:uppercase;letter-spacing:1px}
.trade-table td{padding:5px 10px;border-bottom:1px solid #0d1321}
.trade-table tr:hover{background:#0d1321}
.empty{text-align:center;padding:60px 20px;color:#334155} .empty{text-align:center;padding:60px 20px;color:#334155}
.empty h2{font-size:15px;margin-bottom:6px;color:#445265} .empty h2{font-size:14px;margin-bottom:6px;color:#445265}
.loading{text-align:center;padding:20px;color:#334155;font-size:11px} .loading{text-align:center;padding:14px;color:#334155;font-size:10px}
</style> </style>
</head> </head>
<body> <body>
@@ -60,8 +59,9 @@ select:focus{outline:none;border-color:#3b82f6}
</div> </div>
<div class="main"> <div class="main">
<div class="sidebar"> <div class="sidebar">
<div class="toolbar"> <div class="controls">
<select id="strategy-filter" onchange="loadResults()"> <div class="row">
<select id="filter-strategy" onchange="loadResults()">
<option value="">All Strategies</option> <option value="">All Strategies</option>
<option value="pairs">Pairs Trading</option> <option value="pairs">Pairs Trading</option>
<option value="hurst_vpin">Hurst VPIN</option> <option value="hurst_vpin">Hurst VPIN</option>
@@ -71,40 +71,101 @@ select:focus{outline:none;border-color:#3b82f6}
<option value="composite_mm">Composite MM</option> <option value="composite_mm">Composite MM</option>
<option value="iceberg">Iceberg Detection</option> <option value="iceberg">Iceberg Detection</option>
</select> </select>
<select id="filter-asset" onchange="loadResults()">
<option value="">All Assets</option>
<option value="BTC">BTC</option>
<option value="ETH">ETH</option>
<option value="BTC/ETH">BTC/ETH</option>
</select>
</div>
<div class="row">
<select id="filter-interval" onchange="loadResults()">
<option value="">All Intervals</option>
<option value="1m">1 minute</option>
<option value="5m">5 minutes</option>
<option value="15m">15 minutes</option>
<option value="1h" selected>1 hour</option>
<option value="4h">4 hours</option>
<option value="1d">1 day</option>
</select>
<select id="filter-sort" onchange="loadResults()">
<option value="date">Sort: Latest</option>
<option value="sharpe">Sort: Sharpe</option>
<option value="return">Sort: Return %</option>
<option value="dd">Sort: Min DD</option>
<option value="trades">Sort: Trades</option>
</select>
</div>
<div class="row">
<select id="run-interval">
<option value="1m">1m</option>
<option value="5m">5m</option>
<option value="15m">15m</option>
<option value="1h" selected>1h</option>
<option value="4h">4h</option>
<option value="1d">1d</option>
</select>
<select id="run-limit">
<option value="100">100 bars</option>
<option value="200">200 bars</option>
<option value="500" selected>500 bars</option>
<option value="1000">1000 bars</option>
<option value="2000">2000 bars</option>
<option value="5000">5000 bars</option>
</select>
<select id="run-coin">
<option value="">auto</option>
<option value="BTC">BTC</option>
<option value="ETH">ETH</option>
<option value="SOL">SOL</option>
</select>
</div>
<button class="btn btn-primary" onclick="runNewBacktest()">▶ Run Backtest</button> <button class="btn btn-primary" onclick="runNewBacktest()">▶ Run Backtest</button>
</div> </div>
<h3>Results</h3> <div style="font-size:9px;color:#334155;padding:2px 4px" id="result-count"></div>
<div id="results-list"><div class="loading">Loading...</div></div> <div id="results-list"><div class="loading">Loading...</div></div>
</div> </div>
<div class="content" id="content"> <div class="content" id="content">
<div class="empty"> <div class="empty">
<h2>SELECT A BACKTEST</h2> <h2>SELECT A BACKTEST</h2>
<p>Choose from sidebar or run a new VBT backtest on live HL data</p> <p style="font-size:11px">Choose from sidebar or configure params and run a new test</p>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
const API = ''; const API = '';
const HL_INTERVALS = ['1m','5m','15m','30m','1h','4h','8h','1d','1w'];
let currentResult = null; let currentResult = null;
let currentFilename = null; let currentFilename = null;
async function loadResults() { async function loadResults() {
const strat = document.getElementById('strategy-filter').value; const strat = document.getElementById('filter-strategy').value;
const url = strat ? `${API}/api/vbt/results?strategy=${strat}&limit=100` : `${API}/api/vbt/results?limit=100`; const interval = document.getElementById('filter-interval').value;
const sort = document.getElementById('filter-sort').value;
const asset = document.getElementById('filter-asset').value;
let url = `${API}/api/vbt/results?limit=200&strategy=${strat}&sort=${sort}`;
if (interval) url += '&interval=' + interval;
try { try {
const data = await (await fetch(url)).json(); const data = await (await fetch(url)).json();
const el = document.getElementById('results-list'); const el = document.getElementById('results-list');
if (!data.length) { el.innerHTML = '<div style="padding:10px;color:#445265;font-size:11px">No results. Run one.</div>'; return; } const filtered = asset ? data.filter(r => (r.asset||'').includes(asset)) : data;
el.innerHTML = data.map((r,i) => { document.getElementById('result-count').textContent = filtered.length + ' results';
if (!filtered.length) { el.innerHTML = '<div style="padding:8px;color:#445265;font-size:10px">No results</div>'; return; }
el.innerHTML = filtered.map((r,i) => {
const c = r.sharpe > 0.5 ? 'pos' : (r.sharpe < -0.5 ? 'neg' : 'neu'); const c = r.sharpe > 0.5 ? 'pos' : (r.sharpe < -0.5 ? 'neg' : 'neu');
const rc = r.total_return_pct >= 0 ? 'pos' : 'neg';
return `<div class="result-item${i===0&&!currentResult?' active':''}" onclick="selectFile('${r.filename}')" id="item-${r.filename}"> return `<div class="result-item${i===0&&!currentResult?' active':''}" onclick="selectFile('${r.filename}')" id="item-${r.filename}">
<div class="name">${r.strategy}</div> <div class="name">${r.strategy}<span class="asset">${r.asset||'?'}</span></div>
<div class="meta">${r.interval||'1h'} · ${r.total_trades||0}t · ${(r.n_bars||0)}b</div> <div class="meta">${r.interval||'1h'} · ${r.total_trades||0}t · ${(r.n_bars||0)}b</div>
<div class="stats"> <div class="stats">
<span class="${c}">S${(r.sharpe||0).toFixed(2)}</span> <span class="${c}">S${(r.sharpe||0).toFixed(2)}</span>
<span class="${r.total_return_pct>=0?'pos':'neg'}">${(r.total_return_pct||0).toFixed(1)}%</span> <span class="${rc}">${(r.total_return_pct||0).toFixed(1)}%</span>
<span class="neu">PF${(r.profit_factor||0).toFixed(2)}</span> <span class="neu">PF${(r.profit_factor||0).toFixed(2)}</span>
<span class="neu">DD${(r.max_drawdown_pct||0).toFixed(1)}%</span>
</div> </div>
</div>`; </div>`;
}).join(''); }).join('');
@@ -121,7 +182,7 @@ async function selectFile(filename) {
currentResult = r; currentResult = r;
renderDetail(r); renderDetail(r);
} catch(e) { } catch(e) {
document.getElementById('content').innerHTML = '<div class="empty"><h2>ERROR</h2><p>'+e.message+'</p></div>'; document.getElementById('content').innerHTML = '<div class="empty"><h2>ERROR</h2><p>'+e+'</p></div>';
} }
} }
@@ -135,59 +196,56 @@ function renderDetail(r) {
const tr = r.total_trades ?? 0; const tr = r.total_trades ?? 0;
const nb = r.n_bars ?? 0; const nb = r.n_bars ?? 0;
const so = r.sortino ?? 0; const so = r.sortino ?? 0;
const asset = r.asset || '-';
let html = ` document.getElementById('content').innerHTML = `
<h2>${r.strategy} <span style="font-size:12px;color:#445265;font-weight:400">VBT · ${r.interval||'1h'} · ${nb} bars</span></h2> <h2>${r.strategy} <span class="badge" style="font-size:10px;background:#1e1b4b50;color:#818cf8;border:1px solid #312e81">${asset}</span></h2>
<div class="sub">${tr} trades · ${r.generated_at||''}</div> <div class="sub">${r.interval||'1h'} · ${nb} bars · ${tr} trades · ${r.generated_at||''}</div>
<div class="metrics-grid"> <div class="metrics-grid">
<div class="metric"><div class="label">Total Return</div><div class="value ${ret>=0?'pos':'neg'}">${ret.toFixed(2)}%</div></div> <div class="metric"><div class="label">Total Return</div><div class="value ${ret>=0?'pos':'neg'}">${ret.toFixed(2)}%</div></div>
<div class="metric"><div class="label">Sharpe Ratio</div><div class="value ${sh>=0?'pos':'neg'}">${sh.toFixed(2)}</div></div> <div class="metric"><div class="label">Sharpe</div><div class="value ${sh>=0?'pos':'neg'}">${sh.toFixed(2)}</div></div>
<div class="metric"><div class="label">Max Drawdown</div><div class="value neg">${dd.toFixed(2)}%</div></div> <div class="metric"><div class="label">Max DD</div><div class="value neg">${dd.toFixed(2)}%</div></div>
<div class="metric"><div class="label">Win Rate</div><div class="value ${wr>=50?'pos':'neg'}">${wr.toFixed(0)}%</div></div> <div class="metric"><div class="label">Win Rate</div><div class="value ${wr>=50?'pos':'neg'}">${wr.toFixed(0)}%</div></div>
<div class="metric"><div class="label">Profit Factor</div><div class="value ${pf>=1?'pos':'neg'}">${pf.toFixed(2)}</div></div> <div class="metric"><div class="label">Profit Factor</div><div class="value ${pf>=1?'pos':'neg'}">${pf.toFixed(2)}</div></div>
<div class="metric"><div class="label">Total Trades</div><div class="value neu">${tr}</div></div> <div class="metric"><div class="label">Trades</div><div class="value neu">${tr}</div></div>
<div class="metric"><div class="label">End Equity</div><div class="value neu">$${(eq).toLocaleString()}</div></div> <div class="metric"><div class="label">End Equity</div><div class="value neu">$${eq.toLocaleString()}</div></div>
<div class="metric"><div class="label">Sortino</div><div class="value ${so>=0?'pos':'neg'}">${so.toFixed(2)}</div></div> <div class="metric"><div class="label">Sortino</div><div class="value ${so>=0?'pos':'neg'}">${so.toFixed(2)}</div></div>
</div> </div>
<div class="chart-row"> <div class="chart-row">
<div class="chart-box full"><h4>Equity Curve</h4><div id="chart-eq" style="height:280px"></div></div> <div class="chart-box full"><h4>Equity Curve</h4><div id="chart-eq" style="height:300px"></div></div>
</div> </div>
<div class="chart-row"> <div class="chart-row">
<div class="chart-box"><h4>Drawdown</h4><div id="chart-dd" style="height:240px"></div></div> <div class="chart-box"><h4>Drawdown</h4><div id="chart-dd" style="height:250px"></div></div>
<div class="chart-box"><h4>Period Returns</h4><div id="chart-ret" style="height:240px"></div></div> <div class="chart-box"><h4>Period Returns</h4><div id="chart-ret" style="height:250px"></div></div>
</div> </div>`;
`;
document.getElementById('content').innerHTML = html;
renderCharts(r); renderCharts(r);
} }
function renderCharts(r) { function renderCharts(r) {
const ec = r.equity_curve || []; const ec = r.equity_curve || [];
if (!ec.length) return; if (!ec.length) return;
const times = ec.map(p => p.t), values = ec.map(p => p.v);
const times = ec.map(p => p.t);
const values = ec.map(p => p.v);
Plotly.newPlot('chart-eq', [{ Plotly.newPlot('chart-eq', [{
x: times, y: values, type: 'scatter', mode: 'lines', x: times, y: values, type: 'scatter', mode: 'lines',
line: {color: '#3b82f6', width: 1.2}, line: {color: '#3b82f6', width: 1.2},
fill: 'tozeroy', fillcolor: 'rgba(59,130,246,0.06)' fill: 'tozeroy', fillcolor: 'rgba(59,130,246,0.06)'
}], { }], {
margin: {t:4,r:12,b:28,l:65}, height: 280, margin: {t:4,r:12,b:28,l:65}, height: 300,
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
xaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}, showgrid: true}, xaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}},
yaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}, showgrid: true, tickprefix: '$'}, yaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}, tickprefix: '$'},
showlegend: false showlegend: false
}, {responsive: true, displayModeBar: false}); }, {responsive: true, displayModeBar: false});
const peak = values.reduce((arr, v, i) => { arr.push(i===0?v:Math.max(arr[i-1], v)); return arr; }, []); const peak = values.reduce((a,v,i) => (a.push(i?Math.max(a[i-1],v):v),a), []);
const dd = values.map((v, i) => i===0 ? 0 : -((peak[i]-v)/peak[i])*100); const dd = values.map((v,i) => i?-(peak[i]-v)/peak[i]*100:0);
Plotly.newPlot('chart-dd', [{ Plotly.newPlot('chart-dd', [{
x: times, y: dd, type: 'scatter', mode: 'none', x: times, y: dd, type: 'scatter', mode: 'none',
fill: 'tozeroy', fillcolor: 'rgba(239,68,68,0.12)', fill: 'tozeroy', fillcolor: 'rgba(239,68,68,0.12)',
line: {color: '#ef4444', width: 0.8} line: {color: '#ef4444', width: 0.8}
}], { }], {
margin: {t:4,r:12,b:28,l:55}, height: 240, margin: {t:4,r:12,b:28,l:55}, height: 250,
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
xaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}}, xaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}},
yaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}, ticksuffix: '%'}, yaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}, ticksuffix: '%'},
@@ -200,7 +258,7 @@ function renderCharts(r) {
x: rets, type: 'histogram', nbinsx: 40, x: rets, type: 'histogram', nbinsx: 40,
marker: {color: '#6366f1', opacity: 0.6, line: {color: '#0d1321', width: 1}} marker: {color: '#6366f1', opacity: 0.6, line: {color: '#0d1321', width: 1}}
}], { }], {
margin: {t:4,r:12,b:28,l:45}, height: 240, margin: {t:4,r:12,b:28,l:45}, height: 250,
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
xaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}, ticksuffix: '%'}, xaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}, ticksuffix: '%'},
yaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}}, yaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}},
@@ -210,20 +268,24 @@ function renderCharts(r) {
} }
async function runNewBacktest() { async function runNewBacktest() {
const strat = document.getElementById('strategy-filter').value || 'pairs'; const strat = document.getElementById('filter-strategy').value || 'pairs';
const interval = document.getElementById('run-interval').value;
const limit = document.getElementById('run-limit').value;
const coin = document.getElementById('run-coin').value;
const btn = document.querySelector('.btn-primary'); const btn = document.querySelector('.btn-primary');
const orig = btn.textContent; const orig = btn.textContent;
btn.textContent = '⏳ Running...'; btn.textContent = '⏳ Running...';
btn.disabled = true; btn.disabled = true;
try { try {
const resp = await fetch(`${API}/api/vbt/run?strategy=${strat}&interval=1h&limit=500`); let url = `${API}/api/vbt/run?strategy=${strat}&interval=${interval}&limit=${limit}`;
if (coin) url += '&coin=' + coin;
const resp = await fetch(url);
const data = await resp.json(); const data = await resp.json();
if (data.error) { alert(data.error); btn.textContent = orig; btn.disabled = false; return; } if (data.error) { alert(data.error); btn.textContent = orig; btn.disabled = false; return; }
// Show result immediately from response data (no re-fetch needed)
currentResult = data; currentResult = data;
currentFilename = data.filename; currentFilename = data.filename;
renderDetail(data); renderDetail(data);
loadResults(); // Refresh sidebar loadResults();
} catch(e) { } catch(e) {
alert('Failed: ' + e.message); alert('Failed: ' + e.message);
} }