Add fee-toggle for backtests, CSV trade download, fee simulation in runner
Backtest runner: added per-trade fee simulation (maker 2bps, taker 5bps).
Each trade now records pnl_gross, pnl_net, and fee. New --no-fees flag
excludes fees from PnL. Output includes pnl_gross/pnl_gross_pct and
fees_total alongside existing pnl (net). Regenerated all 12 backtests.
Server: added /api/backtest/{name}/csv endpoint — returns trades as CSV
with columns time,side,size,price,pnl_gross,pnl_net,fee.
Content-Disposition: attachment triggers browser download.
Dashboard: added "Inc. fees" checkbox toggle in backtest detail panel.
Unchecking shows gross PnL (before fees). "↓ CSV" button downloads
the trade history. Both hidden when detail is closed.
This commit is contained in:
+56
-26
@@ -126,8 +126,14 @@ footer a{color:#3f3f4a;text-decoration:none}footer a:hover{color:var(--tx)}
|
||||
<div class="detail-panel" id="detail-panel">
|
||||
<div class="detail-header">
|
||||
<h2 id="det-name">Strategy Detail</h2>
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<label id="fee-toggle-wrap" style="display:none;font-size:11px;color:var(--tx);cursor:pointer;user-select:none">
|
||||
<input type="checkbox" id="fee-toggle" checked onchange="toggleFees()" style="cursor:pointer;margin-right:4px">Inc. fees
|
||||
</label>
|
||||
<a id="dl-csv" href="#" style="display:none;font-size:11px;color:var(--bl);text-decoration:none;padding:4px 10px;border:1px solid var(--ln);border-radius:5px" download>↓ CSV</a>
|
||||
<button class="close-btn" onclick="closeDetail()">✕ Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-body">
|
||||
<div class="desc-text" id="det-desc"></div>
|
||||
<div class="detail-stats" id="det-stats"></div>
|
||||
@@ -140,7 +146,7 @@ footer a{color:#3f3f4a;text-decoration:none}footer a:hover{color:var(--tx)}
|
||||
|
||||
<script>
|
||||
// ═══════════ State ═══════════
|
||||
var currentTab='live', lastData=null, lastPaper=null, lastBT=null;
|
||||
var currentTab='live', lastData=null, lastPaper=null, lastBT=null, lastBTFull=null, feeOn=true;
|
||||
var STRAT_COLORS=['#22c55e','#3b82f6','#a855f7','#f59e0b','#ef4444','#06b6d4','#ec4899','#84cc16','#6366f1','#14b8a6','#f97316','#8b5cf6'];
|
||||
|
||||
// ═══════════ Chart for detail view ═══════════
|
||||
@@ -198,6 +204,45 @@ function renCards(sgridId,ss,baseEq,tab,statsRowId){
|
||||
document.getElementById(sgridId).innerHTML=h;
|
||||
}
|
||||
|
||||
// ═══════════ Fee toggle ═══════════
|
||||
function toggleFees(){
|
||||
feeOn=document.getElementById('fee-toggle').checked;
|
||||
if(lastBTFull){renderBTDetail(lastBTFull)}
|
||||
}
|
||||
|
||||
// ═══════════ Render backtest detail with fee toggle ──
|
||||
function renderBTDetail(full){
|
||||
var pnl=feeOn?(full.pnl||0):(full.pnl_gross||full.pnl||0);
|
||||
var pnlPct=feeOn?(full.pnl_pct||0):(full.pnl_gross_pct||full.pnl_pct||0);
|
||||
var fees=full.fees_total||0;
|
||||
var strat=full.strategy||'';
|
||||
document.getElementById('det-name').textContent=strat+(feeOn?' (net of fees)':' (gross, no fees)');
|
||||
document.getElementById('det-desc').textContent=strat+' — '+full.num_periods+' periods, '+full.total_trades+' trades, fees $'+fees.toFixed(2)+', fee model: '+(full.fee_model||'taker');
|
||||
document.getElementById('det-stats').innerHTML=
|
||||
'<div class="stat"><div class="lbl">'+(feeOn?'Net PnL':'Gross PnL')+'</div><div class="val '+(pnlPct>=0?'up':'dn')+'">'+(pnlPct>=0?'+':'')+pnlPct.toFixed(2)+'%</div></div>'+
|
||||
'<div class="stat"><div class="lbl">Sharpe</div><div class="val">'+(full.sharpe||0).toFixed(2)+'</div></div>'+
|
||||
'<div class="stat"><div class="lbl">Sortino</div><div class="val">'+(full.sortino||0).toFixed(2)+'</div></div>'+
|
||||
'<div class="stat"><div class="lbl">Max DD</div><div class="val dn">'+(full.max_dd*100).toFixed(2)+'%</div></div>'+
|
||||
'<div class="stat"><div class="lbl">Win Rate</div><div class="val">'+Math.round((full.win_rate||0)*100)+'%</div></div>'+
|
||||
'<div class="stat"><div class="lbl">Fees</div><div class="val '+(feeOn?'dn':'')+'">$'+fees.toFixed(2)+(feeOn?'':' (excl)')+'</div></div>';
|
||||
// Equity chart
|
||||
if(!detChart)initDetChart();
|
||||
var pts=[],curve=full.equity_curve||[];
|
||||
for(var i=0;i<curve.length;i++){if(curve[i]&&curve[i].t)pts.push({time:curve[i].t,value:curve[i].v})}
|
||||
if(pts.length>0){detSer.setData(pts);detChart.timeScale().fitContent();setTimeout(function(){detChart.timeScale().fitContent()},200)}
|
||||
// Trades table (show pnl_net or pnl_gross based on toggle)
|
||||
var trows='',tlist=full.trades||[];
|
||||
for(var j=Math.max(0,tlist.length-100);j<tlist.length;j++){
|
||||
var t=tlist[j];
|
||||
var tp=feeOn?(t.pnl_net||t.pnl||0):(t.pnl_gross||t.pnl||0);
|
||||
var tf=t.fee||0;
|
||||
var tside=(t.side||'').toUpperCase();
|
||||
trows+='<tr><td>'+(t.time||'').substr(0,16)+'</td><td class="'+(tside.indexOf('BUY')>=0?'green':'red')+'">'+tside+'</td><td>'+t.size+'</td><td>$'+(t.price||0).toFixed(1)+'</td><td class="'+(tp>=0?'green':'red')+'">'+(tp>=0?'+':'')+'$'+Math.abs(tp).toFixed(4)+'</td><td class="'+(tf>0?'red':'')+'">$'+tf.toFixed(4)+'</td><td class="reason">—</td></tr>';
|
||||
}
|
||||
document.getElementById('det-trades').innerHTML=trows||'<tr><td colspan="7" style="text-align:center;color:var(--tx);padding:20px">No trades recorded</td></tr>';
|
||||
setTimeout(function(){if(detChart)detChart.applyOptions({width:document.getElementById('det-chart').offsetWidth,height:280})},300);
|
||||
}
|
||||
|
||||
// ═══════════ Open strategy detail ═══════════
|
||||
function openDetail(name,tab){
|
||||
document.getElementById('detail-overlay').classList.add('on');
|
||||
@@ -210,33 +255,18 @@ function openDetail(name,tab){
|
||||
ss=lastData.strategies||{};
|
||||
} else if(tab==='backtest'&&lastBT&&lastBT[name]){
|
||||
var b=lastBT[name];
|
||||
document.getElementById('det-desc').textContent=(b.strategy||'')+' — 30-day backtest, Sharpe '+(b.sharpe||0).toFixed(2)+', max DD '+(b.max_dd*100).toFixed(1)+'%. '+((b.total_trades||0)+' trades').replace('0 trades','');
|
||||
document.getElementById('det-stats').innerHTML='<div class="stat"><div class="lbl">PnL</div><div class="val '+(b.pnl_pct>=0?'up':'dn')+'">'+(b.pnl_pct>=0?'+':'')+(b.pnl_pct||0).toFixed(2)+'%</div></div>'+
|
||||
'<div class="stat"><div class="lbl">Sharpe</div><div class="val">'+(b.sharpe||0).toFixed(2)+'</div></div>'+
|
||||
'<div class="stat"><div class="lbl">Sortino</div><div class="val">'+(b.sortino||0).toFixed(2)+'</div></div>'+
|
||||
'<div class="stat"><div class="lbl">Max DD</div><div class="val dn">'+(b.max_dd*100).toFixed(2)+'%</div></div>'+
|
||||
'<div class="stat"><div class="lbl">Win Rate</div><div class="val">'+Math.round((b.win_rate||0)*100)+'%</div></div>'+
|
||||
'<div class="stat"><div class="lbl">Trades</div><div class="val">'+(b.total_trades||0)+'</div></div>';
|
||||
document.getElementById('fee-toggle-wrap').style.display='inline';
|
||||
document.getElementById('fee-toggle').checked=true; feeOn=true;
|
||||
document.getElementById('dl-csv').style.display='inline';
|
||||
document.getElementById('dl-csv').href='/cv/api/backtest/'+encodeURIComponent(b.name)+'/csv';
|
||||
document.getElementById('det-desc').textContent='';
|
||||
document.getElementById('det-stats').innerHTML='<div class="stat"><div class="lbl">Loading</div><div class="val">…</div></div>';
|
||||
if(detSer)detSer.setData([]);
|
||||
document.getElementById('det-trades').innerHTML='<tr><td colspan="7" style="text-align:center;color:var(--tx);padding:20px">Loading full trade data…</td></tr>';
|
||||
// Fetch full backtest data from API
|
||||
document.getElementById('det-trades').innerHTML='<tr><td colspan="7" style="text-align:center;color:var(--tx);padding:20px">Loading full trade data…</td></tr>';
|
||||
fetch('/cv/api/backtest/'+encodeURIComponent(b.name)).then(function(r){return r.json()}).then(function(full){
|
||||
document.getElementById('det-desc').textContent=(full.strategy||b.strategy)+' — '+full.num_periods+' periods, '+full.total_trades+' trades, ▲ $'+(full.pnl||0).toFixed(2);
|
||||
// Equity chart
|
||||
if(!detChart)initDetChart();
|
||||
var pts=[],curve=full.equity_curve||[];
|
||||
for(var i=0;i<curve.length;i++){if(curve[i]&&curve[i].t)pts.push({time:curve[i].t,value:curve[i].v})}
|
||||
if(pts.length>0){detSer.setData(pts);detChart.timeScale().fitContent();setTimeout(function(){detChart.timeScale().fitContent()},200)}
|
||||
// Trades table
|
||||
var trows='',tlist=full.trades||[];
|
||||
for(var j=Math.max(0,tlist.length-100);j<tlist.length;j++){
|
||||
var t=tlist[j],tp=t.pnl||0,tside=(t.side||'').toUpperCase();
|
||||
trows+='<tr><td>'+(t.time||'').substr(0,16)+'</td><td class="'+(tside.indexOf('BUY')>=0?'green':'red')+'">'+tside+'</td><td>'+t.size+'</td><td>$'+(t.price||0).toFixed(1)+'</td><td class="'+(tp>=0?'green':'red')+'">'+(tp>=0?'+':'')+'$'+Math.abs(tp).toFixed(4)+'</td><td class="red">—</td><td class="reason">—</td></tr>';
|
||||
}
|
||||
document.getElementById('det-trades').innerHTML=trows||'<tr><td colspan="7" style="text-align:center;color:var(--tx);padding:20px">No trades recorded</td></tr>';
|
||||
setTimeout(function(){if(detChart)detChart.applyOptions({width:document.getElementById('det-chart').offsetWidth,height:280})},300);
|
||||
lastBTFull=full; renderBTDetail(full);
|
||||
}).catch(function(e){
|
||||
document.getElementById('det-trades').innerHTML='<tr><td colspan="7" style="text-align:center;color:var(--rd);padding:20px">Failed to load trade data: '+e.message+'</td></tr>';
|
||||
document.getElementById('det-trades').innerHTML='<tr><td colspan="7" style="text-align:center;color:var(--rd);padding:20px">Failed to load: '+e.message+'</td></tr>';
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -275,7 +305,7 @@ function openDetail(name,tab){
|
||||
setTimeout(function(){if(detChart){detChart.applyOptions({width:document.getElementById('det-chart').offsetWidth,height:280});detChart.timeScale().fitContent()}},300);
|
||||
}
|
||||
|
||||
function closeDetail(){document.getElementById('detail-overlay').classList.remove('on')}
|
||||
function closeDetail(){document.getElementById('detail-overlay').classList.remove('on');document.getElementById('fee-toggle-wrap').style.display='none';document.getElementById('dl-csv').style.display='none';lastBTFull=null}
|
||||
document.addEventListener('keydown',function(e){if(e.key==='Escape')closeDetail()});
|
||||
|
||||
// ═══════════ WebSocket + render ═══════════
|
||||
|
||||
Reference in New Issue
Block a user