Trading-Bot-M20 / backtest_dashboard.html
raghava4u's picture
Upload folder using huggingface_hub
d53dc44 verified
Raw History Blame Contribute Delete
15.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Backtest Results β€” Trading System</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<style>
:root{--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#c9d1d9;--dim:#8b949e;--green:#3fb950;--red:#f85149;--blue:#58a6ff;--yellow:#d29922;--accent:#388bfd}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;background:var(--bg);color:var(--text);overflow-x:hidden}
.header{background:var(--card);border-bottom:1px solid var(--border);padding:16px 24px;display:flex;align-items:center;gap:16px;position:sticky;top:0;z-index:100}
.header h1{font-size:20px;font-weight:600}
.header a{color:var(--blue);font-size:13px;text-decoration:none}
.header a:hover{text-decoration:underline}
select{background:var(--bg);color:var(--text);border:1px solid var(--border);padding:8px 12px;border-radius:6px;font-size:13px;max-width:400px}
.container{max-width:1400px;margin:0 auto;padding:20px 24px}
.kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-bottom:20px}
.kpi{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:14px 16px}
.kpi .label{font-size:11px;text-transform:uppercase;color:var(--dim);letter-spacing:.5px;margin-bottom:4px}
.kpi .value{font-size:22px;font-weight:700}
.kpi .sub{font-size:11px;color:var(--dim);margin-top:2px}
.pos{color:var(--green)}.neg{color:var(--red)}
.charts-row{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px}
.chart-card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:16px}
.chart-card h3{font-size:13px;color:var(--dim);margin-bottom:10px;text-transform:uppercase;letter-spacing:.5px}
canvas{width:100%!important;max-height:280px}
.tbl-card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:20px;overflow-x:auto}
.tbl-card h3{font-size:13px;color:var(--dim);margin-bottom:10px;text-transform:uppercase;letter-spacing:.5px}
table{width:100%;border-collapse:collapse;font-size:13px}
th{text-align:left;padding:8px 10px;border-bottom:2px solid var(--border);color:var(--dim);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap;cursor:pointer;user-select:none}
th:hover{color:var(--blue)}
th.sorted-asc::after{content:" β–²";color:var(--blue)}
th.sorted-desc::after{content:" β–Ό";color:var(--blue)}
td{padding:7px 10px;border-bottom:1px solid var(--border);white-space:nowrap}
tr:hover{background:rgba(56,139,253,.06)}
.pill{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600}
.pill-buy{background:rgba(63,185,80,.15);color:var(--green)}
.pill-sell{background:rgba(248,81,73,.15);color:var(--red)}
.filter-row{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:16px;align-items:center}
.filter-row label{font-size:12px;color:var(--dim)}
.filter-row input,.filter-row select{background:var(--bg);color:var(--text);border:1px solid var(--border);padding:5px 8px;border-radius:4px;font-size:12px}
.loading{text-align:center;padding:60px;color:var(--dim);font-size:16px}
.badge{display:inline-block;background:var(--accent);color:#fff;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;margin-left:8px}
</style>
</head>
<body>
<div class="header">
<h1>πŸ“Š Backtest Results</h1>
<select id="runSelect"><option value="">Loading…</option></select>
<a href="/">← Live Dashboard</a>
</div>
<div class="container" id="app">
<div class="loading" id="loadingMsg">Select a backtest run to view results</div>
</div>
<script>
const API = '';
const $ = s => document.querySelector(s);
const $$ = s => document.querySelectorAll(s);
let trades = [], dailyPnl = [], currentSort = {col: null, asc: true};
// ── Load run list ──
async function loadRuns(){
const res = await fetch(`${API}/api/backtest/list`);
const runs = await res.json();
const sel = $('#runSelect');
sel.innerHTML = '<option value="">β€” Select backtest run β€”</option>';
runs.sort((a,b) => b.end.localeCompare(a.end));
for(const r of runs){
const o = document.createElement('option');
o.value = r.key;
o.textContent = `${r.symbol_count} symbols | ${r.start} β†’ ${r.end}`;
sel.appendChild(o);
}
// Auto-select the latest (largest symbol count)
const best = runs.reduce((a,b) => b.symbol_count > a.symbol_count ? b : a, runs[0]);
if(best){ sel.value = best.key; loadRun(best.key); }
}
async function loadRun(key){
if(!key){ $('#app').innerHTML = '<div class="loading">Select a backtest run to view results</div>'; return; }
$('#app').innerHTML = '<div class="loading">Loading backtest data…</div>';
const [tRes, pRes] = await Promise.all([
fetch(`${API}/api/backtest/trades/${encodeURIComponent(key)}`),
fetch(`${API}/api/backtest/daily_pnl/${encodeURIComponent(key)}`)
]);
trades = await tRes.json();
dailyPnl = await pRes.json();
render();
}
function render(){
// ── Compute stats ──
const symbols = [...new Set(trades.map(t=>t.symbol))];
const totalPnl = trades.reduce((s,t)=>s+t.pnl,0);
const wins = trades.filter(t=>t.pnl>0);
const losses = trades.filter(t=>t.pnl<=0);
const winRate = trades.length ? (wins.length/trades.length*100) : 0;
const avgWin = wins.length ? wins.reduce((s,t)=>s+t.pnl,0)/wins.length : 0;
const avgLoss = losses.length ? losses.reduce((s,t)=>s+t.pnl,0)/losses.length : 0;
const avgDuration = trades.length ? trades.reduce((s,t)=>s+(t.duration_min||0),0)/trades.length : 0;
// Per-symbol stats
const bySymbol = {};
for(const t of trades){
if(!bySymbol[t.symbol]) bySymbol[t.symbol] = {trades:0,pnl:0,wins:0,buys:0,sells:0};
bySymbol[t.symbol].trades++;
bySymbol[t.symbol].pnl += t.pnl;
if(t.pnl > 0) bySymbol[t.symbol].wins++;
if(t.side==='buy') bySymbol[t.symbol].buys++;
else bySymbol[t.symbol].sells++;
}
// Equity curve from daily PnL
let cumPnl = 0;
const eqCurve = dailyPnl.map(d => { cumPnl += d.pnl; return {date:d.date, cum:cumPnl}; });
// Max drawdown
let peak = 0, maxDD = 0;
for(const e of eqCurve){
if(e.cum > peak) peak = e.cum;
const dd = peak - e.cum;
if(dd > maxDD) maxDD = dd;
}
// Daily stats
const profitDays = dailyPnl.filter(d=>d.pnl>0).length;
const lossDays = dailyPnl.filter(d=>d.pnl<=0).length;
const bestDay = dailyPnl.length ? Math.max(...dailyPnl.map(d=>d.pnl)) : 0;
const worstDay = dailyPnl.length ? Math.min(...dailyPnl.map(d=>d.pnl)) : 0;
// Exit reasons
const exitReasons = {};
for(const t of trades){ exitReasons[t.exit_reason] = (exitReasons[t.exit_reason]||0) + 1; }
const pnlClass = totalPnl >= 0 ? 'pos' : 'neg';
$('#app').innerHTML = `
<div class="kpi-row">
<div class="kpi"><div class="label">Total P&L</div><div class="value ${pnlClass}">$${totalPnl.toFixed(2)}</div></div>
<div class="kpi"><div class="label">Total Trades</div><div class="value">${trades.length}</div><div class="sub">${symbols.length} symbols</div></div>
<div class="kpi"><div class="label">Win Rate</div><div class="value">${winRate.toFixed(1)}%</div><div class="sub">${wins.length}W / ${losses.length}L</div></div>
<div class="kpi"><div class="label">Avg Win</div><div class="value pos">$${avgWin.toFixed(2)}</div></div>
<div class="kpi"><div class="label">Avg Loss</div><div class="value neg">$${avgLoss.toFixed(2)}</div></div>
<div class="kpi"><div class="label">Max Drawdown</div><div class="value neg">$${maxDD.toFixed(2)}</div></div>
<div class="kpi"><div class="label">Profit Days</div><div class="value pos">${profitDays}</div><div class="sub">of ${dailyPnl.length} days</div></div>
<div class="kpi"><div class="label">Best / Worst Day</div><div class="value"><span class="pos">$${bestDay.toFixed(2)}</span> / <span class="neg">$${worstDay.toFixed(2)}</span></div></div>
</div>
<div class="charts-row">
<div class="chart-card"><h3>Equity Curve (Cumulative P&L)</h3><canvas id="eqChart"></canvas></div>
<div class="chart-card"><h3>Daily P&L</h3><canvas id="dailyChart"></canvas></div>
</div>
<div class="charts-row">
<div class="chart-card"><h3>P&L by Symbol</h3><canvas id="symbolChart"></canvas></div>
<div class="chart-card"><h3>Exit Reasons</h3><canvas id="exitChart"></canvas></div>
</div>
<div class="tbl-card">
<h3>Per-Symbol Summary <span class="badge">${symbols.length} symbols</span></h3>
<table id="symbolTable">
<thead><tr>
<th data-col="symbol">Symbol</th>
<th data-col="trades">Trades</th>
<th data-col="pnl">P&L</th>
<th data-col="winRate">Win Rate</th>
<th data-col="buys">Buys</th>
<th data-col="sells">Sells</th>
</tr></thead>
<tbody id="symbolBody"></tbody>
</table>
</div>
<div class="tbl-card">
<h3>Trade Log <span class="badge">${trades.length} trades</span></h3>
<div class="filter-row">
<label>Symbol:</label>
<select id="filterSymbol"><option value="">All</option></select>
<label>Side:</label>
<select id="filterSide"><option value="">All</option><option value="buy">Buy</option><option value="sell">Sell</option></select>
<label>P&L:</label>
<select id="filterPnl"><option value="">All</option><option value="win">Winners</option><option value="loss">Losers</option></select>
</div>
<table>
<thead><tr>
<th>Symbol</th><th>Side</th><th>Entry</th><th>Exit</th><th>Qty</th><th>P&L</th><th>Entry Time</th><th>Exit Time</th><th>Duration</th><th>Exit Reason</th>
</tr></thead>
<tbody id="tradeBody"></tbody>
</table>
</div>
`;
// ── Charts ──
// Equity curve
new Chart($('#eqChart'), {type:'line', data:{labels:eqCurve.map(e=>e.date),datasets:[{
label:'Cumulative P&L ($)',data:eqCurve.map(e=>e.cum),borderColor:'#58a6ff',backgroundColor:'rgba(88,166,255,.1)',fill:true,tension:.3,pointRadius:2
}]},options:{responsive:true,plugins:{legend:{display:false}},scales:{x:{ticks:{color:'#8b949e',maxTicksLimit:10},grid:{color:'#21262d'}},y:{ticks:{color:'#8b949e',callback:v=>'$'+v.toFixed(2)},grid:{color:'#21262d'}}}}});
// Daily PnL bar
new Chart($('#dailyChart'), {type:'bar', data:{labels:dailyPnl.map(d=>d.date),datasets:[{
label:'Daily P&L ($)',data:dailyPnl.map(d=>d.pnl),backgroundColor:dailyPnl.map(d=>d.pnl>=0?'rgba(63,185,80,.7)':'rgba(248,81,73,.7)')
}]},options:{responsive:true,plugins:{legend:{display:false}},scales:{x:{ticks:{color:'#8b949e',maxTicksLimit:12},grid:{color:'#21262d'}},y:{ticks:{color:'#8b949e',callback:v=>'$'+v.toFixed(2)},grid:{color:'#21262d'}}}}});
// Symbol PnL horizontal bar
const symEntries = Object.entries(bySymbol).sort((a,b)=>b[1].pnl-a[1].pnl);
new Chart($('#symbolChart'), {type:'bar', data:{labels:symEntries.map(e=>e[0]),datasets:[{
label:'P&L ($)',data:symEntries.map(e=>e[1].pnl),backgroundColor:symEntries.map(e=>e[1].pnl>=0?'rgba(63,185,80,.7)':'rgba(248,81,73,.7)')
}]},options:{indexAxis:'y',responsive:true,plugins:{legend:{display:false}},scales:{x:{ticks:{color:'#8b949e',callback:v=>'$'+v.toFixed(2)},grid:{color:'#21262d'}},y:{ticks:{color:'#8b949e',font:{size:10}},grid:{display:false}}}}});
// Exit reasons doughnut
const erLabels = Object.keys(exitReasons);
const erColors = ['#58a6ff','#3fb950','#f85149','#d29922','#bc8cff','#f0883e','#8b949e'];
new Chart($('#exitChart'), {type:'doughnut', data:{labels:erLabels,datasets:[{
data:Object.values(exitReasons),backgroundColor:erColors.slice(0,erLabels.length)
}]},options:{responsive:true,plugins:{legend:{position:'bottom',labels:{color:'#c9d1d9',font:{size:11}}}}}});
// ── Symbol table ──
const symRows = Object.entries(bySymbol).map(([sym,d])=>({
symbol:sym, trades:d.trades, pnl:d.pnl, winRate:d.trades?d.wins/d.trades*100:0, buys:d.buys, sells:d.sells
}));
renderSymbolTable(symRows);
$$('#symbolTable th').forEach(th => th.addEventListener('click', ()=> sortSymbolTable(th.dataset.col, symRows)));
// ── Filters ──
const fSel = $('#filterSymbol');
symbols.sort().forEach(s => { const o=document.createElement('option'); o.value=s; o.textContent=s; fSel.appendChild(o); });
$('#filterSymbol').addEventListener('change', renderTradeTable);
$('#filterSide').addEventListener('change', renderTradeTable);
$('#filterPnl').addEventListener('change', renderTradeTable);
renderTradeTable();
}
function renderSymbolTable(rows){
const body = $('#symbolBody');
body.innerHTML = rows.map(r=>`<tr>
<td><strong>${r.symbol}</strong></td>
<td>${r.trades}</td>
<td class="${r.pnl>=0?'pos':'neg'}">$${r.pnl.toFixed(2)}</td>
<td>${r.winRate.toFixed(1)}%</td>
<td>${r.buys}</td>
<td>${r.sells}</td>
</tr>`).join('');
}
function sortSymbolTable(col, rows){
$$('#symbolTable th').forEach(th=>th.classList.remove('sorted-asc','sorted-desc'));
if(currentSort.col===col) currentSort.asc=!currentSort.asc;
else { currentSort.col=col; currentSort.asc=col==='symbol'; }
rows.sort((a,b)=>{
let va=a[col],vb=b[col];
if(typeof va==='string') return currentSort.asc?va.localeCompare(vb):vb.localeCompare(va);
return currentSort.asc?va-vb:vb-va;
});
const th = $(`#symbolTable th[data-col="${col}"]`);
th.classList.add(currentSort.asc?'sorted-asc':'sorted-desc');
renderSymbolTable(rows);
}
function renderTradeTable(){
const symF = $('#filterSymbol').value;
const sideF = $('#filterSide').value;
const pnlF = $('#filterPnl').value;
let filtered = trades;
if(symF) filtered = filtered.filter(t=>t.symbol===symF);
if(sideF) filtered = filtered.filter(t=>t.side===sideF);
if(pnlF==='win') filtered = filtered.filter(t=>t.pnl>0);
if(pnlF==='loss') filtered = filtered.filter(t=>t.pnl<=0);
// Show max 200 rows for perf
const shown = filtered.slice(0, 200);
const body = $('#tradeBody');
body.innerHTML = shown.map(t=>`<tr>
<td><strong>${t.symbol}</strong></td>
<td><span class="pill ${t.side==='buy'?'pill-buy':'pill-sell'}">${t.side.toUpperCase()}</span></td>
<td>$${Number(t.entry_price).toFixed(2)}</td>
<td>$${Number(t.exit_price).toFixed(2)}</td>
<td>${Number(t.qty).toFixed(4)}</td>
<td class="${t.pnl>=0?'pos':'neg'}">$${Number(t.pnl).toFixed(2)}</td>
<td>${(t.entry_time||'').replace('+00:00','Z').slice(0,19)}</td>
<td>${(t.exit_time||'').replace('+00:00','Z').slice(0,19)}</td>
<td>${Number(t.duration_min).toFixed(0)}m</td>
<td>${t.exit_reason||''}</td>
</tr>`).join('');
if(filtered.length>200) body.innerHTML += `<tr><td colspan="10" style="color:var(--dim);text-align:center">… showing 200 of ${filtered.length} trades</td></tr>`;
}
$('#runSelect').addEventListener('change', e => loadRun(e.target.value));
loadRuns();
</script>
</body>
</html>