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:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+42
-12
@@ -11,6 +11,10 @@ from common.metrics import sharpe, sortino, max_drawdown, win_rate
|
|||||||
RESULTS_DIR = Path(__file__).resolve().parent / "results"
|
RESULTS_DIR = Path(__file__).resolve().parent / "results"
|
||||||
os.makedirs(RESULTS_DIR, exist_ok=True)
|
os.makedirs(RESULTS_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Fee rates for backtest simulation (matching paper trader)
|
||||||
|
TAKER_FEE = 0.0005 # 5 bps per side
|
||||||
|
MAKER_FEE = 0.0002 # 2 bps per side
|
||||||
|
|
||||||
CONFIGS = {
|
CONFIGS = {
|
||||||
"ofi": {"name":"Order Book Imbalance","desc":"L2 bid/ask skew — buys when bids dominate","alloc":100.0,"daily_ret":0.0012,"daily_vol":0.014,"fee_model":"taker"},
|
"ofi": {"name":"Order Book Imbalance","desc":"L2 bid/ask skew — buys when bids dominate","alloc":100.0,"daily_ret":0.0012,"daily_vol":0.014,"fee_model":"taker"},
|
||||||
"iceberg": {"name":"Iceberg Detection","desc":"Whale TWAP accumulation detection","alloc":100.0,"daily_ret":0.0008,"daily_vol":0.012,"fee_model":"taker"},
|
"iceberg": {"name":"Iceberg Detection","desc":"Whale TWAP accumulation detection","alloc":100.0,"daily_ret":0.0008,"daily_vol":0.012,"fee_model":"taker"},
|
||||||
@@ -26,30 +30,51 @@ CONFIGS = {
|
|||||||
"gueant": {"name":"Guéant Market Making","desc":"Closed-form asymptotic MM — adverse selection handling","alloc":100.0,"daily_ret":0.0018,"daily_vol":0.005,"fee_model":"maker"},
|
"gueant": {"name":"Guéant Market Making","desc":"Closed-form asymptotic MM — adverse selection handling","alloc":100.0,"daily_ret":0.0018,"daily_vol":0.005,"fee_model":"maker"},
|
||||||
}
|
}
|
||||||
|
|
||||||
def simulate(key, periods=720):
|
def simulate(key, periods=720, include_fees=True):
|
||||||
# Deterministic seed per strategy (hash() is randomized per Python process)
|
# Deterministic seed per strategy (hash() is randomized per Python process)
|
||||||
_fixed_seeds = {"ofi":42,"iceberg":43,"funding_arb":44,"pairs":45,"avellaneda":46,
|
_fixed_seeds = {"ofi":42,"iceberg":43,"funding_arb":44,"pairs":45,"avellaneda":46,
|
||||||
"momentum":47,"mean_rev":48,"hawkes":49,"deep_lob":50,
|
"momentum":47,"mean_rev":48,"hawkes":49,"deep_lob":50,
|
||||||
"cartea":51,"queue_imb":52,"gueant":53}
|
"cartea":51,"queue_imb":52,"gueant":53}
|
||||||
random.seed(_fixed_seeds.get(key, 42))
|
random.seed(_fixed_seeds.get(key, 42))
|
||||||
cfg = CONFIGS[key]
|
cfg = CONFIGS[key]
|
||||||
|
fee_rate = MAKER_FEE if cfg.get("fee_model") == "maker" else TAKER_FEE
|
||||||
hr = cfg["daily_ret"]/24; hv = cfg["daily_vol"]/(24**0.5)
|
hr = cfg["daily_ret"]/24; hv = cfg["daily_vol"]/(24**0.5)
|
||||||
eq=100.0; curve=[]; rets=[]; trades=[]
|
eq_gross=100.0; eq_net=100.0; curve_gross=[]; curve_net=[]; rets=[]; trades=[]
|
||||||
|
total_fees=0.0
|
||||||
dt=datetime.now()-timedelta(days=30)
|
dt=datetime.now()-timedelta(days=30)
|
||||||
for i in range(periods):
|
for i in range(periods):
|
||||||
r = random.gauss(hr,hv)
|
r = random.gauss(hr,hv)
|
||||||
if random.random()<0.02: r*=random.uniform(2,5)
|
if random.random()<0.02: r*=random.uniform(2,5)
|
||||||
before=eq; eq*=(1+r); rets.append(r)
|
before_gross=eq_gross; before_net=eq_net
|
||||||
curve.append({"t":dt.isoformat(),"v":round(eq,4)})
|
eq_gross*=(1+r); eq_net*=(1+r); rets.append(r)
|
||||||
|
curve_gross.append({"t":dt.isoformat(),"v":round(eq_gross,4)})
|
||||||
|
curve_net.append({"t":dt.isoformat(),"v":round(eq_net,4)})
|
||||||
if abs(r)>hv:
|
if abs(r)>hv:
|
||||||
trades.append({"time":dt.strftime("%Y-%m-%d %H:%M"),"side":"BUY" if r>0 else "SELL","size":round(random.uniform(0.0005,0.002),4),"price":round(random.uniform(60000,65000),1),"pnl":round(eq-before,4)})
|
sz=round(random.uniform(0.0005,0.002),4)
|
||||||
|
px=round(random.uniform(60000,65000),1)
|
||||||
|
fee=sz*px*fee_rate*2 # entry + exit fee
|
||||||
|
total_fees+=fee
|
||||||
|
trades.append({"time":dt.strftime("%Y-%m-%d %H:%M"),
|
||||||
|
"side":"BUY" if r>0 else "SELL","size":sz,"price":px,
|
||||||
|
"pnl_gross":round(eq_gross-before_gross,4),
|
||||||
|
"pnl_net":round(eq_gross-before_gross-fee,4),
|
||||||
|
"fee":round(fee,6)})
|
||||||
dt+=timedelta(hours=1)
|
dt+=timedelta(hours=1)
|
||||||
padded=[100.0]*10+[p["v"] for p in curve]
|
padded=[100.0]*10+[p["v"] for p in curve_net]
|
||||||
total_ret=eq-100.0
|
total_ret_gross=eq_gross-100.0
|
||||||
|
total_ret_net=eq_net-100.0-total_fees if include_fees else eq_net-100.0
|
||||||
|
# Rebuild net equity curve with fees if fees included
|
||||||
|
if include_fees:
|
||||||
|
curve = [{"t":c["t"],"v":round(c["v"]-total_fees*(i/periods),4)} for i,c in enumerate(curve_net)]
|
||||||
|
else:
|
||||||
|
curve = curve_net
|
||||||
return {
|
return {
|
||||||
"strategy":cfg["name"],"strategy_key":key,"description":cfg["desc"],"allocation":cfg["alloc"],
|
"strategy":cfg["name"],"strategy_key":key,"description":cfg["desc"],"allocation":cfg["alloc"],
|
||||||
"start_time":curve[0]["t"],"end_time":curve[-1]["t"],"start_equity":100.0,"end_equity":round(eq,4),
|
"start_time":curve[0]["t"],"end_time":curve[-1]["t"],"start_equity":100.0,"end_equity":round(curve[-1]["v"],4),
|
||||||
"pnl":round(total_ret,4),"pnl_pct":round(total_ret,4),"ann_return_pct":round(total_ret*12,2),
|
"pnl":round(total_ret_net,4),"pnl_pct":round(total_ret_net,4),
|
||||||
|
"pnl_gross":round(total_ret_gross,4),"pnl_gross_pct":round(total_ret_gross,4),
|
||||||
|
"fees_total":round(total_fees,4),"fee_model":cfg.get("fee_model","taker"),
|
||||||
|
"ann_return_pct":round(total_ret_net*12,2),
|
||||||
"sharpe":round(sharpe(rets,periods=8760),4),"sortino":round(sortino(rets,periods=8760),4),
|
"sharpe":round(sharpe(rets,periods=8760),4),"sortino":round(sortino(rets,periods=8760),4),
|
||||||
"max_dd":round(max_drawdown(padded),4),"max_dd_pct":round(max_drawdown(padded)*100,2),
|
"max_dd":round(max_drawdown(padded),4),"max_dd_pct":round(max_drawdown(padded)*100,2),
|
||||||
"win_rate":round(win_rate(trades),4),"total_trades":len(trades),
|
"win_rate":round(win_rate(trades),4),"total_trades":len(trades),
|
||||||
@@ -66,13 +91,18 @@ def save(r):
|
|||||||
def main():
|
def main():
|
||||||
p=argparse.ArgumentParser()
|
p=argparse.ArgumentParser()
|
||||||
p.add_argument("--strategy","-s",choices=list(CONFIGS)+["all"],default="all")
|
p.add_argument("--strategy","-s",choices=list(CONFIGS)+["all"],default="all")
|
||||||
|
p.add_argument("--no-fees",action="store_true",help="Exclude simulated fees from PnL")
|
||||||
a=p.parse_args()
|
a=p.parse_args()
|
||||||
keys=list(CONFIGS) if a.strategy=="all" else [a.strategy]
|
keys=list(CONFIGS) if a.strategy=="all" else [a.strategy]
|
||||||
print("="*60); print(f" FTDT Quant Lab — Backtest Runner ({len(keys)} strategies)"); print("="*60)
|
include_fees=not a.no_fees
|
||||||
|
print("="*60)
|
||||||
|
print(f" FTDT Quant Lab — Backtest Runner ({len(keys)} strategies)")
|
||||||
|
print(f" Fees: {'INCLUDED (default)' if include_fees else 'EXCLUDED (--no-fees)'}")
|
||||||
|
print("="*60)
|
||||||
for k in keys:
|
for k in keys:
|
||||||
cfg=CONFIGS[k]; print(f"\n Running: {cfg['name']}...")
|
cfg=CONFIGS[k]; print(f"\n Running: {cfg['name']}...")
|
||||||
r=simulate(k); save(r)
|
r=simulate(k, include_fees=include_fees); save(r)
|
||||||
print(f" PnL: {r['pnl_pct']:+.2f}% | Sharpe: {r['sharpe']:.2f} | DD: {r['max_dd_pct']:.2f}% | Win: {r['win_rate']:.0%}")
|
print(f" Net PnL: {r['pnl_pct']:+.2f}% | Gross: {r['pnl_gross_pct']:+.2f}% | Fees: ${r['fees_total']:.2f} | Sharpe: {r['sharpe']:.2f} | Win: {r['win_rate']:.0%}")
|
||||||
print("\n"+"="*60); print(" Results in backtests/results/"); print(" View at: https://ftdt.io/cv (Backtest tab)"); print("="*60)
|
print("\n"+"="*60); print(" Results in backtests/results/"); print(" View at: https://ftdt.io/cv (Backtest tab)"); print("="*60)
|
||||||
|
|
||||||
if __name__=="__main__": main()
|
if __name__=="__main__": main()
|
||||||
|
|||||||
@@ -195,6 +195,29 @@ async def get_backtest(name: str):
|
|||||||
return JSONResponse({"error": "not found"}, status_code=404)
|
return JSONResponse({"error": "not found"}, status_code=404)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/backtest/{name}/csv")
|
||||||
|
async def get_backtest_csv(name: str):
|
||||||
|
"""Download backtest trades as CSV."""
|
||||||
|
from fastapi.responses import Response
|
||||||
|
fpath = os.path.join(BACKTEST_DIR, f"{name}.json")
|
||||||
|
if not os.path.exists(fpath):
|
||||||
|
return JSONResponse({"error": "not found"}, status_code=404)
|
||||||
|
with open(fpath) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
trades = data.get("trades", [])
|
||||||
|
# Build CSV with headers
|
||||||
|
header = "time,side,size,price,pnl_gross,pnl_net,fee\n"
|
||||||
|
rows = []
|
||||||
|
for t in trades:
|
||||||
|
rows.append(f"{t.get('time','')},{t.get('side','')},{t.get('size','')},{t.get('price','')},{t.get('pnl_gross',t.get('pnl',''))},{t.get('pnl_net',t.get('pnl',''))},{t.get('fee','0')}")
|
||||||
|
csv_content = header + "\n".join(rows)
|
||||||
|
return Response(
|
||||||
|
content=csv_content,
|
||||||
|
media_type="text/csv",
|
||||||
|
headers={"Content-Disposition": f"attachment; filename={name}_trades.csv"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
# Static
|
# Static
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|||||||
+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-panel" id="detail-panel">
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<h2 id="det-name">Strategy Detail</h2>
|
<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>
|
<button class="close-btn" onclick="closeDetail()">✕ Close</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="detail-body">
|
<div class="detail-body">
|
||||||
<div class="desc-text" id="det-desc"></div>
|
<div class="desc-text" id="det-desc"></div>
|
||||||
<div class="detail-stats" id="det-stats"></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>
|
<script>
|
||||||
// ═══════════ State ═══════════
|
// ═══════════ 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'];
|
var STRAT_COLORS=['#22c55e','#3b82f6','#a855f7','#f59e0b','#ef4444','#06b6d4','#ec4899','#84cc16','#6366f1','#14b8a6','#f97316','#8b5cf6'];
|
||||||
|
|
||||||
// ═══════════ Chart for detail view ═══════════
|
// ═══════════ Chart for detail view ═══════════
|
||||||
@@ -198,6 +204,45 @@ function renCards(sgridId,ss,baseEq,tab,statsRowId){
|
|||||||
document.getElementById(sgridId).innerHTML=h;
|
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 ═══════════
|
// ═══════════ Open strategy detail ═══════════
|
||||||
function openDetail(name,tab){
|
function openDetail(name,tab){
|
||||||
document.getElementById('detail-overlay').classList.add('on');
|
document.getElementById('detail-overlay').classList.add('on');
|
||||||
@@ -210,33 +255,18 @@ function openDetail(name,tab){
|
|||||||
ss=lastData.strategies||{};
|
ss=lastData.strategies||{};
|
||||||
} else if(tab==='backtest'&&lastBT&&lastBT[name]){
|
} else if(tab==='backtest'&&lastBT&&lastBT[name]){
|
||||||
var b=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('fee-toggle-wrap').style.display='inline';
|
||||||
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>'+
|
document.getElementById('fee-toggle').checked=true; feeOn=true;
|
||||||
'<div class="stat"><div class="lbl">Sharpe</div><div class="val">'+(b.sharpe||0).toFixed(2)+'</div></div>'+
|
document.getElementById('dl-csv').style.display='inline';
|
||||||
'<div class="stat"><div class="lbl">Sortino</div><div class="val">'+(b.sortino||0).toFixed(2)+'</div></div>'+
|
document.getElementById('dl-csv').href='/cv/api/backtest/'+encodeURIComponent(b.name)+'/csv';
|
||||||
'<div class="stat"><div class="lbl">Max DD</div><div class="val dn">'+(b.max_dd*100).toFixed(2)+'%</div></div>'+
|
document.getElementById('det-desc').textContent='';
|
||||||
'<div class="stat"><div class="lbl">Win Rate</div><div class="val">'+Math.round((b.win_rate||0)*100)+'%</div></div>'+
|
document.getElementById('det-stats').innerHTML='<div class="stat"><div class="lbl">Loading</div><div class="val">…</div></div>';
|
||||||
'<div class="stat"><div class="lbl">Trades</div><div class="val">'+(b.total_trades||0)+'</div></div>';
|
|
||||||
if(detSer)detSer.setData([]);
|
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>';
|
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
|
|
||||||
fetch('/cv/api/backtest/'+encodeURIComponent(b.name)).then(function(r){return r.json()}).then(function(full){
|
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);
|
lastBTFull=full; renderBTDetail(full);
|
||||||
// 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);
|
|
||||||
}).catch(function(e){
|
}).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;
|
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);
|
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()});
|
document.addEventListener('keydown',function(e){if(e.key==='Escape')closeDetail()});
|
||||||
|
|
||||||
// ═══════════ WebSocket + render ═══════════
|
// ═══════════ WebSocket + render ═══════════
|
||||||
|
|||||||
Reference in New Issue
Block a user