Files
ftdt-quant-lab/dashboard/static/vbt.html
T
ramseshk b7c7fbb0af feat: VBT dashboard overview — strategy cards + heatmap + /api/vbt/summary
dashboard/server.py — new /api/vbt/summary endpoint:
  - Aggregated per-strategy best stats (Sharpe, Return, Calmar, trades,
    positive-Sharpe %, top combo details)
  - Heatmap: strategy × interval matrix with best Sharpe per cell

dashboard/static/vbt.html — overview panel replaces empty state:
  - Strategy cards grid: best Sharpe, best return, top combo,
    positive-Sharpe %, run count. Click to filter sidebar.
  - Heatmap: color-coded strategy × interval table (green=good,
    red=bad). Hover for tooltip, click to load result.
  - Back-to-overview button when viewing a strategy detail
  - Overview re-shown on page load, auto-loads from /api/vbt/summary

Dashboard opens to overview → select strategy → drill into details
2026-08-10 12:03:49 +08:00

476 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FTDT Quant Lab — VBT Dashboard</title>
<script src="https://cdn.plot.ly/plotly-3.1.0.min.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'SF Mono','Cascadia Code','Ubuntu Mono',monospace;background:#0a0a0a;color:#888;min-height:100vh;overflow:hidden}
.pos{color:#03A9F4}.neg{color:#FF5252}.neu{color:#777}.warn{color:#f59e0b}
.topbar{background:#111;border-bottom:1px solid #222;padding:8px 20px;display:flex;justify-content:space-between;align-items:center;height:40px}
.topbar h1{font-size:13px;font-weight:600;color:#ddd;letter-spacing:1px}
.topbar .dot{display:inline-block;width:6px;height:6px;background:#fff;border-radius:50%;margin-right:5px;animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
.topbar .status{font-size:10px;color:#999}
.main{display:grid;grid-template-columns:320px 1fr;height:calc(100vh - 40px)}
.sidebar{background:#0d0d0d;border-right:1px solid #222;overflow-y:auto;padding:6px;display:flex;flex-direction:column}
.controls{padding:6px;border-bottom:1px solid #222;margin-bottom:4px;flex-shrink:0}
.controls .row{display:flex;gap:3px;margin-bottom:3px}
select{background:#181818;border:1px solid #333;color:#aaa;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:#555}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;padding:4px 7px;border-radius:3px;font-size:10px;cursor:pointer;border:1px solid #333;background:#181818;color:#aaa;font-family:inherit;transition:all .12s;white-space:nowrap}
.btn:hover{background:#222;border-color:#555}
.btn-primary{background:#03A9F4;border-color:#03A9F4;color:#fff}
.btn-primary:hover{background:#0288d1}
.btn-success{background:#4CAF50;border-color:#4CAF50;color:#fff}
.btn-success:hover{background:#388E3C}
.btn:disabled{opacity:.4;cursor:not-allowed}
.result-item{background:#111;border:1px solid #222;border-left:3px solid transparent;border-radius:3px;padding:7px 8px;margin-bottom:2px;cursor:pointer;transition:all .12s}
.result-item:hover{border-color:#333;background:#161616}
.result-item.active{border-color:#03A9F4;border-left-color:#03A9F4;background:#121212}
.result-item .name{font-size:11px;font-weight:600;color:#ccc;display:flex;justify-content:space-between;align-items:center}
.result-item .name .asset{font-size:9px;color:#03A9F4;background:#0d1f2b;border:1px solid #1a3a4a;border-radius:3px;padding:1px 5px}
.result-item .meta{font-size:9px;color:#555;margin-top:2px}
.result-item .stats{display:flex;gap:8px;margin-top:3px;font-size:9px;font-family:monospace;flex-wrap:wrap}
.sidebar h3{font-size:9px;text-transform:uppercase;color:#555;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}
#results-list{flex:1;overflow-y:auto;min-height:0}
.pagination-row{display:flex;align-items:center;justify-content:space-between;padding:4px 4px 2px;border-top:1px solid #1a1a1a;flex-shrink:0}
.pagination-row .count{font-size:9px;color:#555}
.pagination-row .nav-btn{font-size:9px;padding:2px 6px;cursor:pointer;border:1px solid #333;background:#181818;color:#aaa;border-radius:3px;font-family:inherit}
.pagination-row .nav-btn:hover{background:#222;border-color:#555}
.pagination-row .nav-btn:disabled{opacity:.3;cursor:default}
.content{padding:20px 28px;overflow-y:auto}
.content h2{font-size:15px;color:#ddd;margin-bottom:2px}
.content .sub{font-size:10px;color:#555;margin-bottom:8px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.metrics-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;margin-bottom:10px}
.metric{background:#111;border:1px solid #222;border-radius:4px;padding:10px 12px}
.metric .label{font-size:8px;text-transform:uppercase;color:#555;letter-spacing:1.5px;margin-bottom:3px}
.metric .value{font-size:18px;font-weight:700;font-family:monospace}
.chart-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px}
.chart-box{background:#111;border:1px solid #222;border-radius:4px;padding:12px}
.chart-box h4{font-size:9px;color:#555;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:2px}
.chart-box.full{grid-column:1/-1}
.empty{text-align:center;padding:60px 20px;color:#333}
.empty h2{font-size:14px;margin-bottom:6px;color:#555}
.loading{text-align:center;padding:14px;color:#333;font-size:10px}
.error-box{background:#1a0a0a;border:1px solid #551111;border-radius:4px;padding:14px;text-align:center}
.error-box h3{color:#FF5252;font-size:12px;margin-bottom:6px}
.error-box button{margin-top:8px}
.params-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px;margin-bottom:10px}
.param{background:#111;border:1px solid #222;border-radius:3px;padding:8px 10px}
.param .k{font-size:9px;color:#555;text-transform:uppercase;margin-bottom:2px}
.param .v{font-size:13px;color:#03A9F4;font-family:monospace;font-weight:600}
.trades-table{width:100%;border-collapse:collapse;font-size:11px;font-family:monospace}
.trades-table th{text-align:left;padding:7px 10px;border-bottom:1px solid #222;color:#555;font-weight:500;font-size:9px;text-transform:uppercase;letter-spacing:1px;position:sticky;top:0;background:#111;z-index:1}
.trades-table td{padding:5px 10px;border-bottom:1px solid #1a1a1a;color:#aaa}
.trades-table tr:hover{background:#151515}
.trades-table .pnl-pos{color:#03A9F4}.trades-table .pnl-neg{color:#FF5252}
.trade-scroll{max-height:400px;overflow-y:auto;border:1px solid #222;border-radius:4px}
.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}
</style>
</head>
<body>
<div class="topbar">
<h1><span class="dot"></span>FTDT QUANT LAB — VBT</h1>
<div class="status">Hyperliquid · VectorBT</div>
</div>
<div class="main">
<div class="sidebar">
<div class="controls">
<div class="row">
<select id="filter-strategy">
<option value="">All Strategies</option>
<option value="pairs">Pairs Trading</option>
<option value="hurst_vpin">Hurst VPIN</option>
<option value="as_mm">Avellaneda-Stoikov</option>
<option value="obi">Order Book Imbalance</option>
<option value="grid_mm">Grid Market Making</option>
<option value="composite_mm">Composite MM</option>
<option value="iceberg">Iceberg Detection</option>
<option value="momentum">Momentum Breakout</option>
<option value="mean_rev">Mean Reversion</option>
</select>
<select id="filter-asset">
<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">
<option value="">All Intervals</option>
<option value="1m">1m</option><option value="5m">5m</option><option value="15m">15m</option>
<option value="1h">1h</option><option value="4h">4h</option><option value="1d">1d</option>
</select>
<select id="filter-sort">
<option value="date">Latest</option>
<option value="sharpe">Sharpe</option>
<option value="return">Return %</option>
<option value="calmar">Calmar</option>
<option value="dd">Min DD</option>
<option value="trades">Trades</option>
</select>
</div>
<div class="row" style="margin-top:2px">
<select id="fee-tier" style="flex:0.6" onchange="recalcIfSelected()">
<option value="0" selected>T0 (Base 0.045%/0.015%)</option>
<option value="1">T1 (>$5M 0.040/0.012)</option>
<option value="2">T2 (>$25M 0.035/0.008)</option>
<option value="3">T3 (>$100M 0.030/0.004)</option>
<option value="4">T4 (>$500M 0.028/0.000)</option>
<option value="5">T5 (>$2B 0.026/0.000)</option>
<option value="6">T6 (>$7B 0.024/0.000)</option>
</select>
<select id="staking-tier" style="flex:0.4" onchange="recalcIfSelected()">
<option value="none">No Stake</option>
<option value="wood">Wood (-5%)</option>
<option value="bronze">Bronze (-10%)</option>
<option value="silver">Silver (-15%)</option>
<option value="gold">Gold (-20%)</option>
<option value="platinum">Platinum (-30%)</option>
<option value="diamond">Diamond (-40%)</option>
</select>
</div>
<div class="row">
<select id="run-strategy">
<option value="pairs">Pairs Trading</option>
<option value="hurst_vpin">Hurst VPIN</option>
<option value="as_mm">Avellaneda-Stoikov</option>
<option value="obi">Order Book Imbalance</option>
<option value="grid_mm">Grid Market Making</option>
<option value="composite_mm">Composite MM</option>
<option value="iceberg">Iceberg Detection</option>
<option value="momentum">Momentum Breakout</option>
<option value="mean_rev">Mean Reversion</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">1K bars</option><option value="2000">2K bars</option><option value="5000">5K 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>
<div style="display:flex;gap:4px">
<button class="btn btn-primary" onclick="runNewBacktest()" id="btn-run">▶ Run Backtest</button>
<button class="btn" onclick="loadResults()" id="btn-refresh">↻</button>
</div>
<div id="run-status"></div>
</div>
<div style="font-size:9px;color:#444;padding:2px 4px" id="result-count"></div>
<div id="results-list"><div class="loading">Loading...</div></div>
<div class="pagination-row" id="pagination" style="display:none">
<button class="nav-btn" id="btn-prev" onclick="loadPage(-1)" disabled>← Prev</button>
<span class="count" id="page-info"></span>
<button class="nav-btn" id="btn-next" onclick="loadPage(1)" disabled>Next →</button>
</div>
</div>
<div class="content" id="content">
<div id="vbt-overview">
<div style="padding:10px 0"><span style="font-size:12px;color:#ddd;font-weight:600">OVERVIEW</span><span class="loading" id="overview-loading" style="display:inline;margin-left:10px;font-size:10px">Loading...</span></div>
<div id="overview-content"></div>
</div>
</div>
</div>
<script>
const API='';
const PAGE_SIZE=50;
let currentResult=null, currentFilename=null, currentOffset=0, currentTotal=0, runTimer=null;
function buildUrl(){let p=new URLSearchParams();const s=document.getElementById('filter-strategy').value,i=document.getElementById('filter-interval').value,a=document.getElementById('filter-asset').value,so=document.getElementById('filter-sort').value;p.set('limit',PAGE_SIZE);p.set('offset',currentOffset);if(s)p.set('strategy',s);if(i)p.set('interval',i);if(a)p.set('asset',a);p.set('sort',so);return`${API}/api/vbt/results?`+p;}
async function loadResults(){
document.getElementById('results-list').innerHTML='<div class="loading">Loading...</div>';
try{
const resp=await fetch(buildUrl());const data=await resp.json();
if(data&&Array.isArray(data.results)){
renderResultsList(data.results,data.total||0,data.has_more??false);
}else if(Array.isArray(data)){
renderResultsList(data,data.length,false);
}else{
throw new Error('Unexpected response');
}
}catch(e){document.getElementById('results-list').innerHTML='<div class="error-box" style="padding:8px;font-size:10px">Failed to load results</div><div style="font-size:9px;color:#555;text-align:center;margin-top:4px">'+e+'</div>';}
}
function renderResultsList(results,total,hasMore){
currentTotal=total;
const el=document.getElementById('results-list');
document.getElementById('result-count').textContent=total+' results';
if(!results.length){el.innerHTML='<div style="padding:8px;color:#555;font-size:10px">No results</div>';document.getElementById('pagination').style.display='none';return;}
el.innerHTML=results.map((r,i)=>{
const c=(r.sharpe||0)>0.5?'pos':((r.sharpe||0)<-0.5?'neg':'neu');
const rc=(r.total_return_pct||0)>=0?'pos':'neg';
const activeCls=r.filename===currentFilename?' active':'';
return '<div class="result-item'+activeCls+'" onclick="selectFile(\''+r.filename+'\')" id="item-'+r.filename+'"><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 · '+(r.generated_at||'').substring(0,10)+'</div><div class="stats"><span class="'+c+'">S'+(r.sharpe||0).toFixed(2)+'</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">WR'+(Math.round((r.win_rate||0)*100))+'%</span></div></div>';
}).join('');
const pagination=document.getElementById('pagination');
if(total>PAGE_SIZE){
pagination.style.display='flex';
document.getElementById('btn-prev').disabled=currentOffset===0;
document.getElementById('btn-next').disabled=!hasMore;
const end=Math.min(currentOffset+PAGE_SIZE,total);
document.getElementById('page-info').textContent=(currentOffset+1)+'–'+end+' of '+total;
}else{pagination.style.display='none';}
document.getElementById('pagination').style.display=total>PAGE_SIZE?'flex':'none';
}
function loadPage(dir){
currentOffset=Math.max(0,currentOffset+dir*PAGE_SIZE);
loadResults();
}
async function selectFile(filename){
document.querySelectorAll('.result-item').forEach(el=>el.classList.remove('active'));
const item=document.getElementById('item-'+filename);
if(item)item.classList.add('active');
currentFilename=filename;
currentResult=null;
window.location.hash=filename;
const overview=document.getElementById('vbt-overview');
if(overview)overview.style.display='none';
const content=document.getElementById('content');
content.innerHTML='<div class="loading" style="padding:40px">Loading '+filename+'...</div>';
try{
const r=await(await fetch(API+'/api/vbt/result/'+encodeURIComponent(filename))).json();
if(r.error||!r.strategy){content.innerHTML='<div class="error-box"><h3>Error</h3><p style="font-size:11px;color:#999">'+r.error+'</p><button class="btn" onclick="selectFile(\''+filename+'\')">Retry</button></div>';return;}
currentResult=r;renderDetail(r);
}catch(e){content.innerHTML='<div class="error-box"><h3>Error</h3><p style="font-size:11px;color:#999">'+e+'</p><button class="btn" onclick="selectFile(\''+filename+'\')">Retry</button></div>';}
}
function renderDetail(r){
const ret=r.total_return_pct??0,dd=r.max_drawdown_pct??0,sh=r.sharpe??0;
const wr=(r.win_rate??0)*100,pf=r.profit_factor??0,eq=r.end_equity??10000;
const tr=r.total_trades??0,nb=r.n_bars??0,so=r.sortino??0;
const asset=r.asset||'-';const params=r.params||{};
const calmar=r.calmar||((dd)?(ret/Math.abs(dd)):0);
const expec=r.expectancy??(tr?(r.pnl??0)/tr:0);
let avgDur='';
const trades=r.trades||[];
if(trades.length){const durations=trades.map(t=>String(t.duration||'')).filter(d=>d);if(durations.length)avgDur=durations[0];}
let params_html='';
for(const[k,v]of Object.entries(params)){
let cls='neu';
if(typeof v==='number'){if(k.includes('entry')||k.includes('threshold'))cls=v>0.5?'pos':'neu';else cls=v>0?'pos':(v<0?'neg':'neu');}
params_html+='<div class="param"><div class="k">'+k.replace(/_/g,' ')+'</div><div class="v '+cls+'">'+(typeof v==='number'?v.toFixed(3):v)+'</div></div>';
}
let trades_html='';
if(trades.length){
const rows=trades.map(t=>{
const pnl_net=Number(t.pnl_net??t.pnl??0);
const fee=Number(t.fee??0);
const pnlCls=pnl_net>=0?'pnl-pos':'pnl-neg';
const assetBadge = t.asset ? '<span style="font-size:9px;color:#03A9F4;background:#0d1f2b;border:1px solid #1a3a4a;border-radius:2px;padding:0 4px;margin-left:4px">'+t.asset+'</span>' : '';
return '<tr><td style="color:#777;font-size:10px">'+String(t.time||'').substring(0,19)+'</td><td class="'+(String(t.side||'').includes('BUY')?'pos':'neg')+'">'+String(t.side||'')+assetBadge+'</td><td>'+Number(t.size||0).toFixed(6)+'</td><td>$'+Number(t.entry_px||0).toFixed(1)+'</td><td>$'+Number(t.exit_px||0).toFixed(1)+'</td><td class="neg" style="font-size:10px">$'+fee.toFixed(6)+'</td><td class="'+pnlCls+'">$'+pnl_net.toFixed(4)+'</td><td style="font-size:10px">'+String(t.duration||'')+'</td></tr>';
}).join('');
const feeRate = trades[0]?.fee_rate_pct ? ' · '+trades[0].fee_rate_pct+'%' : '';
trades_html='<div class="chart-box full"><div style="display:flex;justify-content:space-between;align-items:center"><h4>Trade Log ('+trades.length+' trades'+feeRate+')</h4></div><div class="trade-scroll"><table class="trades-table"><thead><tr><th>Time</th><th>Side</th><th>Size</th><th>Entry</th><th>Exit</th><th>Fee</th><th>PnL (net)</th><th>Duration</th></tr></thead><tbody>'+rows+'</tbody></table></div></div>';
}
document.getElementById('content').innerHTML=
'<div style="margin-bottom:8px"><button class="btn" onclick="showOverview()">← Overview</button></div>'+
'<h2>'+r.strategy+' <span class="badge" style="font-size:10px;background:#0d1f2b;color:#03A9F4;border:1px solid #1a3a4a">'+asset+'</span></h2>'+
'<div class="sub"><span>'+(r.interval||'1h')+'</span><span>'+nb+' bars</span><span>'+tr+' trades</span><span>'+(r.generated_at||'').substring(0,19)+'</span>'+
'<div class="export-row">'+
'<button class="btn" onclick="exportJSON()" title="Download full result as JSON">JSON</button>'+
'<button class="btn" onclick="exportCSV()" title="Download trades as CSV">CSV</button>'+
'</div></div>'+
'<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">Sharpe</div><div class="value '+(sh>=0?'pos':'neg')+'">'+sh.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 class="metric"><div class="label">Calmar</div><div class="value '+(calmar>=0?'pos':'neg')+'">'+calmar.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">Profit Factor</div><div class="value '+(pf>=1?'pos':'neg')+'">'+pf.toFixed(2)+'</div></div>'+
'<div class="metric"><div class="label">Expectancy</div><div class="value '+(expec>=0?'pos':'neg')+'">$'+expec.toFixed(4)+'</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">$'+parseInt(eq).toLocaleString()+'</div></div>'+
'</div>'+
'<div class="chart-box full"><h4>Strategy Parameters</h4><div class="params-grid">'+params_html+'</div></div>'+
'<div class="chart-row">'+
'<div class="chart-box full"><h4>Equity Curve</h4><div id="chart-eq" style="height:300px"></div></div>'+
'</div>'+
'<div class="chart-row">'+
'<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:250px"></div></div>'+
'</div>'+
trades_html;
renderCharts(r);
}
function renderCharts(r){
const ec=r.equity_curve||[];
if(!ec.length)return;
const times=ec.map(p=>p.t),values=ec.map(p=>p.v);
Plotly.newPlot('chart-eq',[{x:times,y:values,type:'scatter',mode:'lines',line:{color:'#03A9F4',width:1.2},fill:'tozeroy',fillcolor:'rgba(3,169,244,0.06)'}],{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)',xaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9}},yaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9},tickprefix:'$'},showlegend:false},{responsive:true,displayModeBar:false});
const ddData=r.drawdown_curve||[];
if(ddData.length){Plotly.newPlot('chart-dd',[{x:times,y:ddData,type:'scatter',mode:'none',fill:'tozeroy',fillcolor:'rgba(255,82,82,0.10)',line:{color:'#FF5252',width:0.8}}],{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)',xaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9}},yaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9},ticksuffix:'%'},showlegend:false},{responsive:true,displayModeBar:false});}else{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?-(peak[i]-v)/peak[i]*100:0);Plotly.newPlot('chart-dd',[{x:times,y:dd,type:'scatter',mode:'none',fill:'tozeroy',fillcolor:'rgba(255,82,82,0.10)',line:{color:'#FF5252',width:0.8}}],{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)',xaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9}},yaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9},ticksuffix:'%'},showlegend:false},{responsive:true,displayModeBar:false});}
if(values.length>1){const rets=values.slice(1).map((v,i)=>(v-values[i])/values[i]*100);Plotly.newPlot('chart-ret',[{x:rets,type:'histogram',nbinsx:40,marker:{color:'#03A9F4',opacity:0.6,line:{color:'#111',width:1}}}],{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)',xaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9},ticksuffix:'%'},yaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9}},showlegend:false,bargap:0.02},{responsive:true,displayModeBar:false});}
}
function exportJSON(){if(!currentResult)return;const blob=new Blob([JSON.stringify(currentResult,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=(currentFilename||'backtest')+'.json';a.click();}
function exportCSV(){if(!currentFilename)return;const a=document.createElement('a');a.href=API+'/api/vbt/result/'+encodeURIComponent(currentFilename)+'/csv';a.download=currentFilename.replace('.json','')+'_trades.csv';a.click();}
async function recalcIfSelected(){
if(!currentFilename)return;
const feeTier=document.getElementById('fee-tier').value;
const staking=document.getElementById('staking-tier').value;
const makerRebate=0;
try{
const url=API+'/api/vbt/result/'+encodeURIComponent(currentFilename)+'/recalc?fee_tier='+feeTier+'&staking_tier='+staking+'&maker_rebate='+makerRebate;
const r=await(await fetch(url)).json();
if(r.error){return;}
currentResult=r;renderDetail(r);
}catch(e){/* ignore */}
}
async function runNewBacktest(){
const strat=document.getElementById('run-strategy').value;
const interval=document.getElementById('run-interval').value;
const limit=document.getElementById('run-limit').value;
const coin=document.getElementById('run-coin').value;
const feeTier=document.getElementById('fee-tier').value;
const staking=document.getElementById('staking-tier').value;
const btn=document.getElementById('btn-run');
const status=document.getElementById('run-status');
const orig=btn.textContent;btn.textContent='⏳ Running...';btn.disabled=true;
status.innerHTML='<div class="run-status">Running '+strat+' '+interval+'...</div><div class="progress-bar" style="width:0%" id="progress-bar"></div>';
const startTime=Date.now();
runTimer=setInterval(()=>{
const elapsed=Math.round((Date.now()-startTime)/1000);
status.innerHTML='<div class="run-status">Running '+strat+' '+interval+'... '+elapsed+'s</div><div class="progress-bar" style="width:'+Math.min(elapsed*10,95)+'%"></div>';
},1000);
try{
let url=API+'/api/vbt/run?strategy='+strat+'&interval='+interval+'&limit='+limit+'&fee_tier='+feeTier+'&staking_tier='+staking;
if(coin)url+='&coin='+coin;
const resp=await fetch(url);
const data=await resp.json();
clearInterval(runTimer);runTimer=null;
if(data.error){status.innerHTML='<div style="font-size:10px;color:#FF5252;padding:4px 0">Error: '+data.error+'</div>';btn.textContent=orig;btn.disabled=false;return;}
currentResult=data;currentFilename=data.filename;
renderDetail(data);
currentOffset=0;loadResults();
status.innerHTML='<div style="font-size:10px;color:#4CAF50;padding:4px 0">Done in '+Math.round((Date.now()-startTime)/1000)+'s</div>';
}catch(e){clearInterval(runTimer);runTimer=null;status.innerHTML='<div style="font-size:10px;color:#FF5252;padding:4px 0">Failed: '+e.message+'</div>';}
btn.textContent=orig;btn.disabled=false;
}
function applyFilter(){currentOffset=0;loadResults();}
document.getElementById('filter-strategy').addEventListener('change',applyFilter);
document.getElementById('filter-interval').addEventListener('change',applyFilter);
document.getElementById('filter-asset').addEventListener('change',applyFilter);
document.getElementById('filter-sort').addEventListener('change',applyFilter);
async function loadSummary(){
document.getElementById('overview-loading').style.display='inline';
try{
const resp=await fetch(API+'/api/vbt/summary');
const d=await resp.json();
if(d.error){document.getElementById('overview-content').innerHTML='<div style="color:#555;padding:10px">'+d.error+'</div>';return;}
renderSummary(d);
}catch(e){document.getElementById('overview-content').innerHTML='<div style="color:#FF5252;padding:10px">Failed to load summary</div>';}
document.getElementById('overview-loading').style.display='none';
}
function renderSummary(d){
const strategies=d.best_by_strategy||{};
const heatmap=d.heatmap_best_sharpe||{};
const intervals=d.intervals||[];
const stratNames=Object.keys(strategies).sort();
// Strategy cards
let cards='<div class="strat-summary">';
for(const s of stratNames){
const info=strategies[s];
const sh=info.best_sharpe||0;
const shCls=sh>0.5?'pos':(sh<-0.5?'neg':'neu');
const retCls=info.best_return>=0?'pos':'neg';
cards+='<div class="strat-card" onclick="document.getElementById(\'filter-strategy\').value=\''+s+'\';applyFilter()"><div class="strat-name">'+s+'</div>'+
'<div class="strat-row"><span>Best Sharpe</span><span class="val '+shCls+'">'+sh.toFixed(2)+'</span></div>'+
'<div class="strat-row"><span>Best Return</span><span class="val '+retCls+'">'+(info.best_return||0).toFixed(1)+'%</span></div>'+
'<div class="strat-row"><span>Best Combo</span><span class="val">'+info.top_combo.interval+' '+info.top_combo.limit+'b</span></div>'+
'<div class="strat-row"><span>+Sharpe %</span><span class="val">'+(info.positive_sharpe_pct||0).toFixed(0)+'%</span></div>'+
'<div class="strat-row"><span>Runs</span><span class="val">'+info.count+'</span></div>'+
'</div>';
}
cards+='</div>';
// Heatmap
let hmap='<div class="chart-box full"><h4>Strategy × Interval Heatmap (Best Sharpe)</h4><div style="overflow-x:auto"><table class="heatmap-table"><thead><tr><th></th>';
for(const iv of intervals)hmap+='<th>'+iv+'</th>';
hmap+='</tr></thead><tbody>';
for(const s of stratNames){
hmap+='<tr><td>'+s+'</td>';
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+='<td class="heatmap-cell" style="background:'+bg+';color:'+clr+'" title="'+s+' '+iv+': S='+sh.toFixed(2)+' ret='+(cell.return_pct||0).toFixed(1)+'% '+cell.trades+'t" onclick="document.getElementById(\'filter-strategy\').value=\''+s+'\';document.getElementById(\'filter-interval\').value=\''+iv+'\';selectFile(\''+(cell.filename||(s+'_BTC_'+iv+'_500_'+(new Date().toISOString().slice(0,10).replace(/-/g,''))+'.json'))+'\')">'+sh.toFixed(1)+'</td>';
}
hmap+='</tr>';
}
hmap+='</tbody></table></div></div>';
document.getElementById('overview-content').innerHTML=
'<div style="font-size:10px;color:#555;margin-bottom:6px">'+d.total_results+' results across '+d.strategy_count+' strategies</div>'+
cards+hmap;
}
function restoreFromHash(){
const hash=window.location.hash.replace('#','');
if(hash&&hash.endsWith('.json')){selectFile(hash);}
}
function showOverview(){
currentFilename=null;currentResult=null;
window.location.hash='';
document.getElementById('vbt-overview').style.display='block';
document.getElementById('content').innerHTML='<div id="vbt-overview"><div style="padding:10px 0"><span style="font-size:12px;color:#ddd;font-weight:600">OVERVIEW</span></div><div id="overview-content"></div></div>';
document.querySelectorAll('.result-item').forEach(el=>el.classList.remove('active'));
loadSummary();
}
window.addEventListener('hashchange',restoreFromHash);
loadSummary();
loadResults();
setTimeout(restoreFromHash,100);
</script>
</body>
</html>