Files
ftdt-quant-lab/dashboard/static/vbt.html
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

245 lines
16 KiB
HTML

<!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}
.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:310px 1fr;height:calc(100vh - 40px)}
.sidebar{background:#0d0d0d;border-right:1px solid #222;overflow-y:auto;padding:6px}
.controls{padding:6px;border-bottom:1px solid #222;margin-bottom:4px}
.controls .row{display:flex;gap:4px;margin-bottom:4px}
.controls .row:last-child{margin-bottom:0}
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:5px 8px;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: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:3px;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:10px;margin-top:3px;font-size:9px;font-family:monospace}
.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}
.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:16px}
.metrics-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:12px}
.metric{background:#111;border:1px solid #222;border-radius:4px;padding:12px 14px}
.metric .label{font-size:8px;text-transform:uppercase;color:#555;letter-spacing:1.5px;margin-bottom:3px}
.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-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}
.params-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px;margin-bottom:12px}
.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}
</style>
</head>
<body>
<div class="topbar">
<h1><span class="dot"></span>FTDT QUANT LAB — VBT</h1>
<div class="status">● LIVE · Hyperliquid Mainnet</div>
</div>
<div class="main">
<div class="sidebar">
<div class="controls">
<div class="row">
<select id="filter-strategy" onchange="loadResults()">
<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>
</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">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="filter-sort" onchange="loadResults()">
<option value="date">Latest</option>
<option value="sharpe">Sharpe</option>
<option value="return">Return %</option>
<option value="dd">Min DD</option>
<option value="trades">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">100b</option><option value="200">200b</option><option value="500" selected>500b</option><option value="1000">1Kb</option><option value="2000">2Kb</option><option value="5000">5Kb</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>
</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>
<div class="content" id="content">
<div class="empty">
<h2>SELECT A BACKTEST</h2>
<p style="font-size:11px">Choose from sidebar or configure and run a new test</p>
</div>
</div>
</div>
<script>
const API='';
let currentResult=null, currentFilename=null;
async function loadResults(){
const strat=document.getElementById('filter-strategy').value;
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{
const data=await (await fetch(url)).json();
const el=document.getElementById('results-list');
const filtered=asset?data.filter(r=>(r.asset||'').includes(asset)):data;
document.getElementById('result-count').textContent=filtered.length+' results';
if(!filtered.length){el.innerHTML='<div style="padding:8px;color:#555;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 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+'"><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="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></div></div>';
}).join('');
}catch(e){console.error(e);}
}
async function selectFile(filename){
document.querySelectorAll('.result-item').forEach(el=>el.classList.remove('active'));
document.getElementById('item-'+filename)?.classList.add('active');
currentFilename=filename;
try{
const r=await (await fetch('${API}/api/vbt/result/'+filename)).json();
if(r.error){document.getElementById('content').innerHTML='<div class="empty"><h2>FILE NOT FOUND</h2></div>';return;}
currentResult=r;renderDetail(r);
}catch(e){document.getElementById('content').innerHTML='<div class="empty"><h2>ERROR</h2><p>'+e+'</p></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||{};
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='';
const trades=r.trades||[];
if(trades.length){
const rows=trades.map(t=>'<tr><td style="color:#777;font-size:10px">'+String(t.time||'').substring(0,19)+'</td><td class="'+(t.side==='BUY'?'pos':'neg')+'">'+String(t.side||'')+'</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="'+(Number(t.pnl||0)>=0?'pnl-pos':'pnl-neg')+'">$'+Number(t.pnl||0).toFixed(4)+'</td><td>'+String(t.duration||'')+'</td></tr>').join('');
trades_html='<div class="chart-box full"><h4>Trade Log ('+trades.length+' trades)</h4><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>PnL</th><th>Duration</th></tr></thead><tbody>'+rows+'</tbody></table></div></div>';
}
document.getElementById('content').innerHTML=
'<h2>'+r.strategy+' <span class="badge" style="font-size:10px;background:#0d1f2b;color:#03A9F4;border:1px solid #1a3a4a">'+asset+'</span></h2>'+
'<div class="sub">'+(r.interval||'1h')+' · '+nb+' bars · '+tr+' trades · '+(r.generated_at||'')+'</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">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">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">Sortino</div><div class="value '+(so>=0?'pos':'neg')+'">'+so.toFixed(2)+'</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:280px"></div></div>'+
'</div>'+
'<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>Period Returns</h4><div id="chart-ret" style="height:240px"></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:280,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 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:240,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:240,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});
}
}
async function runNewBacktest(){
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 orig=btn.textContent;btn.textContent='⏳ Running...';btn.disabled=true;
try{
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();
if(data.error){alert(data.error);btn.textContent=orig;btn.disabled=false;return;}
currentResult=data;currentFilename=data.filename;
renderDetail(data);loadResults();
}catch(e){alert('Failed: '+e.message);}
btn.textContent=orig;btn.disabled=false;
}
loadResults();
</script>
</body>
</html>