Files
ramseshk e71b47ac56 refactor: VBT sidebar — cleaner UX with grouped sections + collapsible fee tiers
Sidebar reorganized into clear sections:
  [FILTERS] — Strategy + Asset + Interval + Sort
  ▶ Fee Tiers — collapsible (hidden by default), shows summary
    T0 + staking tier name when collapsed
  [NEW BACKTEST] — compact: Strategy (short names) + Interval + Limit
    Row 2: Coin + ▶ Run button (side by side) + ↻ Refresh
  [RESULTS] — count header + list + pagination

Removed:
  - Full-width run strategy row (now inline with interval/limit)
  - Duplicate fee tier labels in select options
  - Separate run/refresh button row (now same row as coin)

Added:
  - toggleFeeTiers() — expand/collapse fee settings
  - updateFeeSummary() — shows current tier in collapsed label
  - Fee changes auto-recalculate (no manual onchange needed)
  - Section headers: small uppercase labels
2026-08-10 12:58:37 +08:00

459 lines
33 KiB
HTML
Raw Permalink 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" class="">
<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>
:root{--bg:#0a0a0a;--bg2:#111;--bg3:#181818;--border:#222;--border2:#333;--text:#888;--text2:#ccc;--text3:#ddd;--text4:#555;--sidebar:#0d0d0d;--blue:#03A9F4;--red:#FF5252;--green:#4CAF50;--chart-bg:rgba(0,0,0,0);--chart-grid:#1a1a1a;--chart-tick:#444}
.light{--bg:#f5f5f5;--bg2:#fff;--bg3:#e8e8e8;--border:#d8d8d8;--border2:#bbb;--text:#777;--text2:#333;--text3:#111;--text4:#999;--sidebar:#fafafa;--blue:#0288d1;--red:#c62828;--green:#2e7d32;--chart-bg:rgba(255,255,255,0);--chart-grid:#eee;--chart-tick:#aaa}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'SF Mono','Cascadia Code','Ubuntu Mono',monospace;background:var(--bg);color:var(--text);min-height:100vh;overflow:hidden}
.pos{color:var(--blue)}.neg{color:var(--red)}.neu{color:var(--text)}.warn{color:#f59e0b}
.topbar{background:var(--bg2);border-bottom:1px solid var(--border);padding:8px 20px;display:flex;justify-content:space-between;align-items:center;height:40px}
.topbar h1{font-size:13px;font-weight:600;color:var(--text3);letter-spacing:1px}
.topbar .dot{display:inline-block;width:6px;height:6px;background:var(--blue);border-radius:50%;margin-right:5px;animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
.theme-btn{font-size:10px;cursor:pointer;background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:3px 8px;border-radius:3px;font-family:inherit;transition:all .12s}
.theme-btn:hover{background:var(--bg2);border-color:var(--border2)}
.main{display:grid;grid-template-columns:320px 1fr;height:calc(100vh - 40px)}
.sidebar{background:var(--sidebar);border-right:1px solid var(--border);overflow-y:auto;padding:6px;display:flex;flex-direction:column}
.controls{padding:6px;border-bottom:1px solid var(--border);margin-bottom:4px;flex-shrink:0}
.controls .row{display:flex;gap:3px;margin-bottom:3px}
select{background:var(--bg3);border:1px solid var(--border2);color:var(--text);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:var(--text4)}
.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 var(--border2);background:var(--bg3);color:var(--text);font-family:inherit;transition:all .12s;white-space:nowrap}
.btn:hover{background:var(--bg2);border-color:var(--text4)}
.btn-primary{background:var(--blue);border-color:var(--blue);color:#fff}
.btn-primary:hover{opacity:.85}
.btn-success{background:var(--green);border-color:var(--green);color:#fff}
.btn-success:hover{opacity:.85}
.btn:disabled{opacity:.4;cursor:not-allowed}
.result-item{background:var(--bg2);border:1px solid var(--border);border-left:3px solid transparent;border-radius:3px;padding:7px 8px;margin-bottom:2px;cursor:pointer;transition:all .12s}
.result-item:hover{border-color:var(--border2);background:var(--bg3)}
.result-item.active{border-color:var(--blue);border-left-color:var(--blue)}
.result-item .name{font-size:11px;font-weight:600;color:var(--text2);display:flex;justify-content:space-between;align-items:center}
.result-item .name .asset{font-size:9px;color:var(--blue);background:var(--bg3);border:1px solid var(--border);border-radius:3px;padding:1px 5px}
.result-item .meta{font-size:9px;color:var(--text4);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:var(--text4);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 var(--border);flex-shrink:0}
.pagination-row .count{font-size:9px;color:var(--text4)}
.pagination-row .nav-btn{font-size:9px;padding:2px 6px;cursor:pointer;border:1px solid var(--border2);background:var(--bg3);color:var(--text);border-radius:3px;font-family:inherit}
.pagination-row .nav-btn:hover{background:var(--bg2)}
.pagination-row .nav-btn:disabled{opacity:.3;cursor:default}
.content{padding:20px 28px;overflow-y:auto}
.content h2{font-size:15px;color:var(--text3);margin-bottom:2px}
.content .sub{font-size:10px;color:var(--text4);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:var(--bg2);border:1px solid var(--border);border-radius:4px;padding:10px 12px}
.metric .label{font-size:8px;text-transform:uppercase;color:var(--text4);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:var(--bg2);border:1px solid var(--border);border-radius:4px;padding:12px}
.chart-box h4{font-size:9px;color:var(--text4);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:var(--text4)}
.empty h2{font-size:14px;margin-bottom:6px;color:var(--text4)}
.loading{text-align:center;padding:14px;color:var(--text4);font-size:10px}
.error-box{background:var(--bg2);border:1px solid var(--red);border-radius:4px;padding:14px;text-align:center}
.error-box h3{color:var(--red);font-size:12px;margin-bottom:6px}
.params-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px;margin-bottom:10px}
.param{background:var(--bg2);border:1px solid var(--border);border-radius:3px;padding:8px 10px}
.param .k{font-size:9px;color:var(--text4);text-transform:uppercase;margin-bottom:2px}
.param .v{font-size:13px;color:var(--blue);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 var(--border);color:var(--text4);font-weight:500;font-size:9px;text-transform:uppercase;letter-spacing:1px;position:sticky;top:0;background:var(--bg2);z-index:1}
.trades-table td{padding:5px 10px;border-bottom:1px solid var(--border);color:var(--text)}
.trades-table tr:hover{background:var(--bg3)}
.trades-table .pnl-pos{color:var(--blue)}.trades-table .pnl-neg{color:var(--red)}
.trade-scroll{max-height:400px;overflow-y:auto;border:1px solid var(--border);border-radius:4px}
.export-row{display:flex;gap:6px;margin-bottom:6px}
.progress-bar{height:4px;background:var(--blue);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:var(--text4);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 var(--border);font-size:10px}
.heatmap-table td:first-child{text-align:left;color:var(--text);font-size:10px}
.heatmap-cell{cursor:pointer;transition:all .12s;border-radius:2px}
.heatmap-cell:hover{outline:1px solid var(--text4);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:var(--bg2);border:1px solid var(--border);border-radius:3px;padding:7px 9px;cursor:pointer;transition:all .12s}
.strat-card:hover{background:var(--bg3);border-color:var(--border2)}
.strat-card .strat-name{font-size:10px;font-weight:600;color:var(--text2);margin-bottom:4px}
.strat-card .strat-row{display:flex;justify-content:space-between;font-size:9px;padding:1px 0;color:var(--text4)}
.strat-card .strat-row .val{font-family:monospace;color:var(--text)}
.strat-card .strat-row .val.pos{color:var(--blue)}.strat-card .strat-row .val.neg{color:var(--red)}
</style>
</head>
<body>
<div class="topbar">
<h1><span class="dot"></span>FTDT QUANT LAB — VBT</h1>
<div style="display:flex;align-items:center;gap:8px">
<div class="status">639 results · Hyperliquid</div>
<button class="theme-btn" onclick="toggleTheme()" id="theme-btn">☀</button>
</div>
</div>
<div class="main">
<div class="sidebar">
<div class="controls">
<div style="font-size:8px;color:var(--text4);text-transform:uppercase;letter-spacing:1px;padding:0 4px 3px">Filters</div>
<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="sharpe" selected>Sharpe</option>
<option value="date">Latest</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 style="font-size:8px;color:var(--text4);cursor:pointer;padding:2px 4px;display:flex;align-items:center;gap:4px" onclick="toggleFeeTiers()">
<span id="fee-arrow">▶</span> Fee Tiers <span style="font-size:7px" id="fee-summary"></span>
</div>
<div id="fee-panel" style="display:none">
<div class="row">
<select id="fee-tier" style="flex:0.55">
<option value="0" selected>T0 (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/—)</option>
<option value="5">T5 >$2B (0.026/—)</option>
<option value="6">T6 >$7B (0.024/—)</option>
</select>
<select id="staking-tier" style="flex:0.45">
<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>
</div>
<div class="controls">
<div style="font-size:8px;color:var(--text4);text-transform:uppercase;letter-spacing:1px;padding:0 4px 3px">New Backtest</div>
<div class="row">
<select id="run-strategy">
<option value="pairs">Pairs</option><option value="hurst_vpin">Hurst VPIN</option>
<option value="as_mm">A-S</option><option value="obi">OBI</option>
<option value="grid_mm">Grid</option><option value="composite_mm">Composite</option>
<option value="iceberg">Iceberg</option><option value="momentum">Momentum</option>
<option value="mean_rev">Mean Rev</option>
</select>
<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>
</div>
<div class="row">
<select id="run-coin"><option value="">auto</option><option value="BTC">BTC</option><option value="ETH">ETH</option><option value="SOL">SOL</option></select>
<button class="btn btn-primary" onclick="runNewBacktest()" id="btn-run" style="flex:1">▶ Run</button>
<button class="btn" onclick="loadResults()" title="Refresh">↻</button>
</div>
<div id="run-status"></div>
</div>
<div style="font-size:8px;color:var(--text4);text-transform:uppercase;letter-spacing:1px;padding:4px 4px 2px"><span id="result-count"></span></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;font-weight:600;color:var(--text3)">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='',PAGE_SIZE=50;
let currentResult=null,currentFilename=null,currentOffset=0,currentTotal=0,runTimer=null;
function toggleTheme(){
const html=document.documentElement;
html.classList.toggle('light');
const isLight=html.classList.contains('light');
document.getElementById('theme-btn').textContent=isLight?'☾':'☀';
localStorage.setItem('vbt-theme',isLight?'light':'dark');
// Re-render charts if visible
if(currentResult)renderCharts(currentResult);
}
(function(){
const saved=localStorage.getItem('vbt-theme');
if(saved==='light'){
document.documentElement.classList.add('light');
document.getElementById('theme-btn').textContent='☾';
}
})();
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>';}
}
function renderResultsList(results,total,hasMore){
currentTotal=total;
document.getElementById('result-count').textContent=total+' results';
if(!results.length){document.getElementById('results-list').innerHTML='<div style="padding:8px;color:var(--text4);font-size:10px">No results</div>';document.getElementById('pagination').style.display='none';return;}
document.getElementById('results-list').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('');
document.getElementById('pagination').style.display=total>PAGE_SIZE?'flex':'none';
if(total>PAGE_SIZE){
document.getElementById('btn-prev').disabled=currentOffset===0;
document.getElementById('btn-next').disabled=!hasMore;
document.getElementById('page-info').textContent=(currentOffset+1)+'–'+Math.min(currentOffset+PAGE_SIZE,total)+' of '+total;
}
}
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'));
document.getElementById('item-'+filename)?.classList.add('active');
currentFilename=filename;currentResult=null;
window.location.hash=filename;
document.getElementById('vbt-overview').style.display='none';
document.getElementById('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){document.getElementById('content').innerHTML='<div class="error-box"><h3>Error</h3><p style="font-size:11px">'+r.error+'</p><button class="btn" onclick="selectFile(\''+filename+'\')">Retry</button></div>';return;}
currentResult=r;renderDetail(r);
}catch(e){document.getElementById('content').innerHTML='<div class="error-box"><h3>Error</h3><p style="font-size:11px">'+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 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=>{
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:var(--blue);background:var(--bg3);border:1px solid var(--border);border-radius:2px;padding:0 4px;margin-left:4px">'+t.asset+'</span>':'';
return '<tr><td style="font-size:10px;color:var(--text4)">'+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:var(--blue);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()">JSON</button><button class="btn" onclick="exportCSV()">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 isLight=document.documentElement.classList.contains('light');
const times=ec.map(p=>p.t),values=ec.map(p=>p.v);
const lineC=isLight?'#0288d1':'#03A9F4';
const fillC=isLight?'rgba(2,136,209,0.08)':'rgba(3,169,244,0.06)';
const gridC=isLight?'#e8e8e8':'#1a1a1a';
const tickC=isLight?'#999':'#444';
Plotly.newPlot('chart-eq',[{x:times,y:values,type:'scatter',mode:'lines',line:{color:lineC,width:1.2},fill:'tozeroy',fillcolor:fillC}],{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:gridC,tickfont:{color:tickC,size:9}},yaxis:{gridcolor:gridC,tickfont:{color:tickC,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:isLight?'rgba(200,0,0,0.08)':'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:gridC,tickfont:{color:tickC,size:9}},yaxis:{gridcolor:gridC,tickfont:{color:tickC,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:isLight?'rgba(200,0,0,0.08)':'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:gridC,tickfont:{color:tickC,size:9}},yaxis:{gridcolor:gridC,tickfont:{color:tickC,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:lineC,opacity:0.6,line:{color:isLight?'#eee':'#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:gridC,tickfont:{color:tickC,size:9},ticksuffix:'%'},yaxis:{gridcolor:gridC,tickfont:{color:tickC,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;
try{
const url=API+'/api/vbt/result/'+encodeURIComponent(currentFilename)+'/recalc?fee_tier='+feeTier+'&staking_tier='+staking;
const r=await(await fetch(url)).json();
if(r.error)return;
currentResult=r;renderDetail(r);
}catch(e){}
}
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'),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 e=Math.round((Date.now()-startTime)/1000);status.innerHTML='<div class="run-status">Running '+strat+' '+interval+'... '+e+'s</div><div class="progress-bar" style="width:'+Math.min(e*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:var(--red);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:var(--green);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:var(--red);padding:4px 0">Failed: '+e.message+'</div>';}
btn.textContent=orig;btn.disabled=false;
}
function applyFilter(){currentOffset=0;loadResults();}
function toggleFeeTiers(){
const panel=document.getElementById('fee-panel');
const arrow=document.getElementById('fee-arrow');
const open=panel.style.display==='block';
panel.style.display=open?'none':'block';
arrow.textContent=open?'▶':'▼';
}
function updateFeeSummary(){
const ft=document.getElementById('fee-tier');
const st=document.getElementById('staking-tier');
const stOpt=st.options[st.selectedIndex];
document.getElementById('fee-summary').textContent='· T'+ft.value+' '+stOpt.text.replace(/ .+/,'');
}
document.getElementById('fee-tier').addEventListener('change',()=>{updateFeeSummary();recalcIfSelected();});
document.getElementById('staking-tier').addEventListener('change',()=>{updateFeeSummary();recalcIfSelected();});
updateFeeSummary();
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="padding:10px;color:var(--text4)">'+d.error+'</div>';return;}
renderSummary(d);
}catch(e){document.getElementById('overview-content').innerHTML='<div style="padding:10px;color:var(--red)">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();
let cards='<div class="strat-summary">';
for(const s of stratNames){
const info=strategies[s];
const sh=info.best_sharpe||0;
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 '+(sh>0.5?'pos':(sh<-0.5?'neg':'neu'))+'">'+sh.toFixed(2)+'</span></div>'+
'<div class="strat-row"><span>Best Return</span><span class="val '+(info.best_return>=0?'pos':'neg')+'">'+(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>';
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='var(--bg2)',clr='var(--text4)';
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='#cc7777'}
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">'+sh.toFixed(1)+'</td>';
}
hmap+='</tr>';
}
hmap+='</tbody></table></div></div>';
document.getElementById('overview-content').innerHTML='<div style="font-size:10px;color:var(--text4);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;font-weight:600;color:var(--text3)">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>