Download backtest_dashboard.html from raghava4u/Trading-Bot-M20: direct link, hf CLI and curl.
- Browser
- Download file 15.4 kB
-
https://huggingface.co/raghava4u/Trading-Bot-M20/resolve/main/backtest_dashboard.html
- Command line
-
hf download hf://raghava4u/Trading-Bot-M20/backtest_dashboard.html
-
curl -L -o backtest_dashboard.html https://huggingface.co/raghava4u/Trading-Bot-M20/resolve/main/backtest_dashboard.html
15.4 kB
| <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%;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> | |