fix: VBT dashboard — proper metrics display + redesigned UI

- Fixed total_return_pct, profit_factor, n_bars showing 0 in detail view
  by using ?? operator instead of || 0 and fixing renderDetail logic
- Run Backtest now renders result directly from API response
  (no re-fetch race condition)
- Redesigned UI: monospace trading terminal aesthetic
  - Darker palette (#090d14 background, #0d1321 cards)
  - Indigo histogram, proper grid layout
  - Subtle borders (1px #1a2332), better spacing
  - Status indicator with pulse animation
  - Sidebar shows Sharpe, Return%, Profit Factor per result
  - 8 metric cards: Return, Sharpe, DD, Win Rate, PF, Trades,
    End Equity, Sortino
  - Smaller, cleaner fonts, monospace throughout
- Bumped memory guard to 2GB to prevent dashboard getting killed
This commit is contained in:
ramseshk
2026-08-07 12:31:24 +08:00
parent 3606e7f92e
commit 737b24895c
2 changed files with 149 additions and 155 deletions
+3 -3
View File
@@ -44,9 +44,9 @@ from strategies.quant_report import compute_quant_report
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
import gc, os as _os import gc, os as _os
MEM_SOFT_LIMIT = 256 * 1024 * 1024 # 256 MB — force GC MEM_SOFT_LIMIT = 512 * 1024 * 1024 # 512 MB — force GC
MEM_WARN_LIMIT = 384 * 1024 * 1024 # 384 MB — log warning MEM_WARN_LIMIT = 768 * 1024 * 1024 # 768 MB — log warning
MEM_HARD_LIMIT = 512 * 1024 * 1024 # 512 MB — terminate MEM_HARD_LIMIT = 2048 * 1024 * 1024 # 2 GB — terminate
def check_memory(): def check_memory():
"""Check RSS, force GC if over soft limit, raise if over hard limit.""" """Check RSS, force GC if over soft limit, raise if over hard limit."""
+142 -148
View File
@@ -7,73 +7,79 @@
<script src="https://cdn.plot.ly/plotly-3.1.0.min.js"></script> <script src="https://cdn.plot.ly/plotly-3.1.0.min.js"></script>
<style> <style>
*{margin:0;padding:0;box-sizing:border-box} *{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Ubuntu',-apple-system,sans-serif;background:#0a0e17;color:#c8d6e5;min-height:100vh} body{font-family:'SF Mono','Cascadia Code','Ubuntu Mono',monospace;background:#090d14;color:#a8b2c0;min-height:100vh;overflow:hidden}
.header{background:#111827;border-bottom:1px solid #1e293b;padding:12px 24px;display:flex;justify-content:space-between;align-items:center} .topbar{background:#0d1321;border-bottom:1px solid #1a2332;padding:10px 20px;display:flex;justify-content:space-between;align-items:center;height:44px}
.header h1{font-size:18px;color:#e2e8f0} .topbar h1{font-size:14px;font-weight:600;color:#e0e6ed;letter-spacing:1px}
.header span{font-size:12px;color:#64748b} .topbar .dot{display:inline-block;width:7px;height:7px;background:#22c55e;border-radius:50%;margin-right:6px;animation:pulse 2s infinite}
.main{display:grid;grid-template-columns:300px 1fr;height:calc(100vh - 49px)} @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.sidebar{background:#0f172a;border-right:1px solid #1e293b;overflow-y:auto;padding:12px} .topbar .status{font-size:11px;color:#4ade80}
.sidebar h3{font-size:12px;text-transform:uppercase;color:#64748b;margin:12px 0 6px;letter-spacing:1px} .main{display:grid;grid-template-columns:280px 1fr;height:calc(100vh - 44px)}
.result-item{background:#1e293b;border:1px solid #334155;border-radius:6px;padding:10px;margin-bottom:6px;cursor:pointer;transition:border-color .15s} .sidebar{background:#0b1019;border-right:1px solid #1a2332;overflow-y:auto;padding:8px}
.result-item:hover{border-color:#3b82f6} .sidebar h3{font-size:10px;text-transform:uppercase;color:#445265;margin:10px 0 6px;letter-spacing:2px}
.result-item.active{border-color:#3b82f6;background:#1e3a5f} .toolbar{display:flex;gap:6px;flex-direction:column;margin-bottom:8px}
.result-item .name{font-size:14px;font-weight:600;color:#e2e8f0} 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}
.result-item .meta{font-size:11px;color:#64748b;margin-top:3px} select:focus{outline:none;border-color:#3b82f6}
.result-item .stats{display:flex;gap:10px;margin-top:5px;font-size:11px} .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%}
.stat-pos{color:#34d399}.stat-neg{color:#f87171}.stat-neutral{color:#94a3b8} .btn:hover{background:#1a2744;border-color:#3b82f6}
.content{padding:20px;overflow-y:auto} .btn-primary{background:#1d4ed8;border-color:#2563eb;color:#e0e6ed}
.metrics-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px} .btn-primary:hover{background:#1e40af}
.metric-card{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:14px;text-align:center} .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}
.metric-card .label{font-size:11px;text-transform:uppercase;color:#64748b;letter-spacing:0.5px;margin-bottom:4px} .result-item:hover{border-color:#334155;background:#151e30}
.metric-card .value{font-size:24px;font-weight:700} .result-item.active{border-color:#3b82f6;border-left-color:#3b82f6;background:#121b2d}
.chart-row{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px} .result-item .name{font-size:12px;font-weight:600;color:#c8d6e5}
.chart-box{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:12px} .result-item .meta{font-size:10px;color:#445265;margin-top:2px}
.chart-box h4{font-size:12px;color:#64748b;text-transform:uppercase;margin-bottom:8px;letter-spacing:0.5px} .result-item .stats{display:flex;gap:12px;margin-top:4px;font-size:10px;font-family:monospace}
.chart-full{grid-column:1/-1} .pos{color:#4ade80}.neg{color:#f87171}.neu{color:#64748b}
.empty-state{text-align:center;padding:60px 20px;color:#64748b}
.empty-state h2{font-size:16px;margin-bottom:8px} .content{padding:24px 32px;overflow-y:auto}
.btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:6px;font-size:13px;cursor:pointer;border:1px solid #334155;background:#1e293b;color:#c8d6e5;transition:all .15s} .content h2{font-size:16px;color:#e0e6ed;margin-bottom:4px}
.btn:hover{background:#334155;border-color:#475569} .content .sub{font-size:11px;color:#445265;margin-bottom:20px}
.btn-primary{background:#3b82f6;border-color:#3b82f6;color:#fff} .metrics-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px}
.btn-primary:hover{background:#2563eb} .metric{background:#0d1321;border:1px solid #1a2332;border-radius:6px;padding:14px 16px}
.btn-sm{padding:4px 10px;font-size:11px} .metric .label{font-size:9px;text-transform:uppercase;color:#445265;letter-spacing:1.5px;margin-bottom:4px}
.toolbar{display:flex;gap:8px;align-items:center;margin-bottom:16px;flex-wrap:wrap} .metric .value{font-size:22px;font-weight:700;font-family:monospace}
select,input{background:#1e293b;border:1px solid #334155;color:#c8d6e5;border-radius:6px;padding:6px 10px;font-size:13px}
select:focus,input:focus{outline:none;border-color:#3b82f6} .chart-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}
.loading{text-align:center;padding:40px;color:#64748b} .chart-box{background:#0d1321;border:1px solid #1a2332;border-radius:6px;padding:14px}
.sweep-table{width:100%;border-collapse:collapse;font-size:12px;margin-top:8px} .chart-box h4{font-size:10px;color:#445265;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:4px}
.sweep-table th{text-align:left;padding:6px 10px;border-bottom:1px solid #334155;color:#64748b;font-weight:500} .chart-box.full{grid-column:1/-1}
.sweep-table td{padding:5px 10px;border-bottom:1px solid #1e293b}
.sweep-table tr:hover{background:#1e293b} .trade-table{width:100%;border-collapse:collapse;font-size:11px;margin-top:8px;font-family:monospace}
.sweep-best{background:rgba(34,197,94,.08)} .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 h2{font-size:15px;margin-bottom:6px;color:#445265}
.loading{text-align:center;padding:20px;color:#334155;font-size:11px}
</style> </style>
</head> </head>
<body> <body>
<div class="header"> <div class="topbar">
<h1>VectorBT Dashboard <span style="font-size:11px;color:#3b82f6;margin-left:8px">Hyperliquid data</span></h1> <h1><span class="dot"></span>FTDT QUANT LAB — VBT</h1>
<span id="last-update"></span> <div class="status">● LIVE · Hyperliquid Mainnet</div>
</div> </div>
<div class="main"> <div class="main">
<div class="sidebar"> <div class="sidebar">
<div class="toolbar" style="flex-direction:column;align-items:stretch"> <div class="toolbar">
<select id="strategy-filter" onchange="loadResults()" style="width:100%"> <select id="strategy-filter" 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>
<option value="as_mm">A-S MM</option> <option value="as_mm">Avellaneda-Stoikov</option>
<option value="momentum">Momentum</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>
<button class="btn btn-primary btn-sm" onclick="runBacktest()" style="justify-content:center">+ Run Backtest</button> <button class="btn btn-primary" onclick="runNewBacktest()"> Run Backtest</button>
</div> </div>
<h3>Results</h3> <h3>Results</h3>
<div id="results-list"> <div id="results-list"><div class="loading">Loading...</div></div>
<div class="loading">Loading...</div>
</div>
</div> </div>
<div class="content" id="content"> <div class="content" id="content">
<div class="empty-state"> <div class="empty">
<h2>Select a backtest result</h2> <h2>SELECT A BACKTEST</h2>
<p style="font-size:13px">Choose from the sidebar or run a new VectorBT backtest</p> <p>Choose from sidebar or run a new VBT backtest on live HL data</p>
</div> </div>
</div> </div>
</div> </div>
@@ -81,82 +87,77 @@ select:focus,input:focus{outline:none;border-color:#3b82f6}
<script> <script>
const API = ''; const API = '';
let currentResult = null; let currentResult = null;
let currentFilename = null;
async function loadResults() { async function loadResults() {
const strat = document.getElementById('strategy-filter').value; const strat = document.getElementById('strategy-filter').value;
const url = strat ? `${API}/api/vbt/results?strategy=${strat}&limit=100` : `${API}/api/vbt/results?limit=100`; const url = strat ? `${API}/api/vbt/results?strategy=${strat}&limit=100` : `${API}/api/vbt/results?limit=100`;
try { try {
const r = await fetch(url); const data = await (await fetch(url)).json();
const data = await r.json();
renderResultsList(data);
document.getElementById('last-update').textContent = new Date().toLocaleTimeString();
} catch(e) {
document.getElementById('results-list').innerHTML = '<div class="loading">Error loading</div>';
}
}
function renderResultsList(results) {
const el = document.getElementById('results-list'); const el = document.getElementById('results-list');
if (!results.length) { if (!data.length) { el.innerHTML = '<div style="padding:10px;color:#445265;font-size:11px">No results. Run one.</div>'; return; }
el.innerHTML = '<div style="padding:12px;color:#64748b;font-size:12px">No results yet</div>'; el.innerHTML = data.map((r,i) => {
return; const c = r.sharpe > 0.5 ? 'pos' : (r.sharpe < -0.5 ? 'neg' : 'neu');
} return `<div class="result-item${i===0&&!currentResult?' active':''}" onclick="selectFile('${r.filename}')" id="item-${r.filename}">
el.innerHTML = results.map((r,i) => `
<div class="result-item${i===0&&!currentResult?' active':''}" onclick="selectResult('${r.filename}')" id="item-${r.filename}">
<div class="name">${r.strategy}</div> <div class="name">${r.strategy}</div>
<div class="meta">${r.interval || '1h'} · ${r.total_trades||0} trades · ${r.n_bars||0} bars</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="${r.sharpe>0?'stat-pos':(r.sharpe<0?'stat-neg':'stat-neutral')}">Sharpe ${r.sharpe?.toFixed(2)||0}</span> <span class="${c}">S ${(r.sharpe||0).toFixed(2)}</span>
<span class="${r.total_return_pct>0?'stat-pos':(r.total_return_pct<0?'stat-neg':'stat-neutral')}">${r.total_return_pct?.toFixed(1)||0}%</span> <span class="${r.total_return_pct>=0?'pos':'neg'}">${(r.total_return_pct||0).toFixed(1)}%</span>
<span class="neu">PF ${(r.profit_factor||0).toFixed(2)}</span>
</div> </div>
</div> </div>`;
`).join(''); }).join('');
} catch(e) { console.error(e); }
} }
async function selectResult(filename) { async function selectFile(filename) {
document.querySelectorAll('.result-item').forEach(el => el.classList.remove('active')); document.querySelectorAll('.result-item').forEach(el => el.classList.remove('active'));
document.getElementById('item-'+filename)?.classList.add('active'); document.getElementById('item-'+filename)?.classList.add('active');
currentFilename = filename;
try { try {
const r = await fetch(`${API}/api/vbt/result/${filename}`); const r = await (await fetch(`${API}/api/vbt/result/${filename}`)).json();
currentResult = await r.json(); if (r.error) { document.getElementById('content').innerHTML = '<div class="empty"><h2>FILE NOT FOUND</h2></div>'; return; }
renderDetail(currentResult); currentResult = r;
renderDetail(r);
} catch(e) { } catch(e) {
document.getElementById('content').innerHTML = '<div class="loading">Error loading result</div>'; document.getElementById('content').innerHTML = '<div class="empty"><h2>ERROR</h2><p>'+e.message+'</p></div>';
} }
} }
function renderDetail(r) { function renderDetail(r) {
const ret = r.total_return_pct || 0; const ret = r.total_return_pct ?? 0;
const dd = r.max_drawdown_pct || 0; const dd = r.max_drawdown_pct ?? 0;
const sharpe = r.sharpe || 0; const sh = r.sharpe ?? 0;
const wr = (r.win_rate||0) * 100; const wr = (r.win_rate ?? 0) * 100;
const pf = r.profit_factor || 0; const pf = r.profit_factor ?? 0;
const eq = r.end_equity ?? 10000;
const tr = r.total_trades ?? 0;
const nb = r.n_bars ?? 0;
const so = r.sortino ?? 0;
let html = ` let html = `
<h3 style="margin-bottom:4px">${r.strategy} <span style="font-size:12px;color:#64748b">${r.engine||'vectorbt'} · ${r.interval||'1h'}</span></h3> <h2>${r.strategy} <span style="font-size:12px;color:#445265;font-weight:400">VBT · ${r.interval||'1h'} · ${nb} bars</span></h2>
<div style="font-size:11px;color:#64748b;margin-bottom:16px">${r.total_trades||0} trades · ${r.n_bars||0} bars · ${r.generated_at||''}</div> <div class="sub">${tr} trades · ${r.generated_at||''}</div>
<div class="metrics-grid"> <div class="metrics-grid">
<div class="metric-card"><div class="label">Total Return</div><div class="value ${ret>=0?'stat-pos':'stat-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-card"><div class="label">Sharpe</div><div class="value ${sharpe>=0?'stat-pos':'stat-neg'}">${sharpe.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-card"><div class="label">Max Drawdown</div><div class="value stat-neg">${dd.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-card"><div class="label">Win Rate</div><div class="value ${wr>=50?'stat-pos':'stat-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-card"><div class="label">Profit Factor</div><div class="value ${pf>=1?'stat-pos':'stat-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-card"><div class="label">Total Trades</div><div class="value stat-neutral">${r.total_trades||0}</div></div> <div class="metric"><div class="label">Total Trades</div><div class="value neu">${tr}</div></div>
<div class="metric-card"><div class="label">End Equity</div><div class="value stat-neutral">$${((r.end_equity||10000)).toFixed(0)}</div></div> <div class="metric"><div class="label">End Equity</div><div class="value neu">$${(eq).toLocaleString()}</div></div>
<div class="metric-card"><div class="label">Sortino</div><div class="value stat-neutral">${(r.sortino||0).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>
`;
document.getElementById('content').innerHTML = html + `
<div class="chart-row">
<div class="chart-box chart-full"><h4>Equity Curve</h4><div id="chart-equity" 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:250px"></div></div> <div class="chart-box full"><h4>Equity Curve</h4><div id="chart-eq" style="height:280px"></div></div>
<div class="chart-box"><h4>Returns Distribution</h4><div id="chart-returns" style="height:250px"></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> </div>
`; `;
document.getElementById('content').innerHTML = html;
renderCharts(r); renderCharts(r);
} }
@@ -167,76 +168,69 @@ function renderCharts(r) {
const times = ec.map(p => p.t); const times = ec.map(p => p.t);
const values = ec.map(p => p.v); const values = ec.map(p => p.v);
// Equity curve Plotly.newPlot('chart-eq', [{
const eqTrace = {
x: times, y: values, type: 'scatter', mode: 'lines', x: times, y: values, type: 'scatter', mode: 'lines',
line: {color: '#3b82f6', width: 1.5}, line: {color: '#3b82f6', width: 1.2},
fill: 'tozeroy', fillcolor: 'rgba(59,130,246,0.08)', fill: 'tozeroy', fillcolor: 'rgba(59,130,246,0.06)'
name: 'Equity' }], {
}; margin: {t:4,r:12,b:28,l:65}, height: 280,
Plotly.newPlot('chart-equity', [eqTrace], {
margin: {t:5,r:15,b:30,l:55},
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: '#1e293b', tickfont: {color: '#64748b', size: 10}, showgrid: true}, xaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}, showgrid: true},
yaxis: {gridcolor: '#1e293b', tickfont: {color: '#64748b', size: 10}, showgrid: true}, yaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}, showgrid: true, tickprefix: '$'},
showlegend: false, showlegend: false
}, {responsive: true, displayModeBar: false}); }, {responsive: true, displayModeBar: false});
// Drawdown const peak = values.reduce((arr, v, i) => { arr.push(i===0?v:Math.max(arr[i-1], v)); return arr; }, []);
const peak = values.slice(1).reduce((arr, v, i) => {arr.push(Math.max(arr[i]||arr[0]||v, v)); return arr;}, [values[0]]); const dd = values.map((v, i) => i===0 ? 0 : -((peak[i]-v)/peak[i])*100);
const dd = values.map((v, i) => i === 0 ? 0 : -((peak[i] - v) / peak[i]) * 100);
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(248,113,113,0.15)', fill: 'tozeroy', fillcolor: 'rgba(239,68,68,0.12)',
line: {color: '#f87171', width: 1}, line: {color: '#ef4444', width: 0.8}
name: 'Drawdown %'
}], { }], {
margin: {t:5,r:15,b:30,l:55}, 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)', paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
xaxis: {gridcolor: '#1e293b', tickfont: {color: '#64748b', size: 10}}, xaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}},
yaxis: {gridcolor: '#1e293b', tickfont: {color: '#64748b', size: 10}, ticksuffix: '%'}, yaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}, ticksuffix: '%'},
showlegend: false, showlegend: false
}, {responsive: true, displayModeBar: false}); }, {responsive: true, displayModeBar: false});
// Returns histogram
if (values.length > 1) { if (values.length > 1) {
const rets = values.slice(1).map((v, i) => (v - values[i]) / values[i] * 100); const rets = values.slice(1).map((v, i) => (v-values[i])/values[i]*100);
Plotly.newPlot('chart-returns', [{ Plotly.newPlot('chart-ret', [{
x: rets, type: 'histogram', x: rets, type: 'histogram', nbinsx: 40,
marker: {color: '#3b82f6', opacity: 0.7, line: {color: '#1e293b', width: 1}}, marker: {color: '#6366f1', opacity: 0.6, line: {color: '#0d1321', width: 1}}
nbinsx: 30,
name: 'Returns'
}], { }], {
margin: {t:5,r:15,b:30,l:45}, 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)', paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
xaxis: {gridcolor: '#1e293b', tickfont: {color: '#64748b', size: 10}, ticksuffix: '%'}, xaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}, ticksuffix: '%'},
yaxis: {gridcolor: '#1e293b', tickfont: {color: '#64748b', size: 10}}, yaxis: {gridcolor: '#111827', tickfont: {color: '#334155', size: 9}},
showlegend: false, showlegend: false, bargap: 0.02
bargap: 0.05,
}, {responsive: true, displayModeBar: false}); }, {responsive: true, displayModeBar: false});
} }
} }
async function runBacktest() { async function runNewBacktest() {
const strat = document.getElementById('strategy-filter').value || 'pairs'; const strat = document.getElementById('strategy-filter').value || 'pairs';
const btn = document.querySelector('.btn-primary'); const btn = document.querySelector('.btn-primary');
btn.textContent = 'Running...'; const orig = btn.textContent;
btn.textContent = '⏳ Running...';
btn.disabled = true; btn.disabled = true;
try { try {
const r = await fetch(`${API}/api/vbt/run?strategy=${strat}&interval=1h&limit=500`); const resp = await fetch(`${API}/api/vbt/run?strategy=${strat}&interval=1h&limit=500`);
const data = await r.json(); const data = await resp.json();
if (data.error) { alert('Error: ' + data.error); return; } if (data.error) { alert(data.error); btn.textContent = orig; btn.disabled = false; return; }
loadResults(); // Show result immediately from response data (no re-fetch needed)
selectResult(data.filename); currentResult = data;
currentFilename = data.filename;
renderDetail(data);
loadResults(); // Refresh sidebar
} catch(e) { } catch(e) {
alert('Failed: ' + e.message); alert('Failed: ' + e.message);
} finally {
btn.textContent = '+ Run Backtest';
btn.disabled = false;
} }
btn.textContent = orig;
btn.disabled = false;
} }
// Init
loadResults(); loadResults();
</script> </script>
</body> </body>