Files
ftdt-quant-lab/dashboard/static/index.html
T
ramseshk 4457cdffc5 Comprehensive fix: live node resilience + CSS contrast + win_rate + equity curves
- Mainnet API fallback when testnet unavailable (prices, orderbook, instruments)
- Bypassed broken SDK instrument loading, uses raw mainnet meta API
- Dynamic BTC/ETH perp ID lookup (handles "-USD-PERP" suffix changes)
- Strategy-level equity tracking for per-strategy detail charts
- Win rate fixed: checks pnl_net/pnl_gross not just pnl field
- CSS contrast improved: --tx #6b6b7b→#9e9eae, borders/highlights brightened
- Equity curve recalculated on fee tier change (chart adjusts visually)
- Added Open Positions & Orders panel placeholder
2026-08-05 02:53:31 +00:00

394 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<title>FTDT Quant Lab — Professional Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lightweight-charts@4.2.3/dist/lightweight-charts.standalone.production.js"></script>
<style>
:root{--bg:#050508;--srf:#0b0b12;--ln:#1f1f30;--hr:#2a2a3c;--tx:#9e9eae;--hi:#e4e4f0;--gr:#22c55e;--rd:#ef4444;--bl:#3b82f6;--am:#f59e0b;--pu:#a855f7;--cy:#06b6d4;--pk:#ec4899;--ra:8px;--f:'Inter',system-ui,sans-serif;--m:'JetBrains Mono',monospace}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--hi);font-family:var(--f);min-height:100vh;-webkit-font-smoothing:antialiased}
.topbar{position:sticky;top:0;z-index:100;background:rgba(5,5,8,.95);backdrop-filter:blur(20px);border-bottom:1px solid var(--ln);padding:12px 24px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px}
.topbar h1{font-size:17px;font-weight:700;letter-spacing:-0.5px;display:flex;align-items:center;gap:8px}
.topbar h1 span{font-size:10px;color:var(--tx);font-weight:400}
.status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;background:var(--gr);animation:pulse 2s infinite}
.status-dot.off{background:var(--rd);animation:none}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.3}}
.tabs{display:flex;gap:0;padding:0 24px;border-bottom:1px solid var(--ln);position:sticky;top:52px;z-index:99;background:rgba(5,5,8,.95);backdrop-filter:blur(20px)}
.tab{padding:10px 20px;font-size:12px;font-weight:500;cursor:pointer;background:none;border:none;border-bottom:2px solid transparent;color:var(--tx);font-family:var(--f);transition:all .15s}
.tab:hover{color:var(--hi)}.tab.on{color:var(--hi);border-bottom-color:var(--bl)}
.badge{font-size:8px;padding:2px 7px;border-radius:3px;font-weight:600;margin-left:6px;text-transform:uppercase;letter-spacing:.5px}
.badge.test{background:rgba(245,158,11,.15);color:var(--am)}.badge.main{background:rgba(168,85,247,.15);color:var(--pu)}
.main-wrap{max-width:1440px;margin:0 auto;padding:20px 24px;display:flex;gap:20px}
.panel{display:none;flex:1;min-width:0}.panel.show{display:block}
.sgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;margin-bottom:20px}
.scard{background:var(--srf);border:1px solid var(--ln);border-radius:var(--ra);padding:16px;cursor:pointer;transition:all .2s;position:relative}
.scard:hover{border-color:var(--hr);transform:translateY(-1px);box-shadow:0 4px 20px rgba(0,0,0,.3)}
.scard.selected{border-color:var(--bl);box-shadow:0 0 0 1px rgba(59,130,246,.3)}
.scard .sh{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px}
.scard .sname{font-size:12px;font-weight:600;line-height:1.3;max-width:70%}
.scard .salloc{font-size:9px;color:var(--tx);margin-top:2px}
.scard .stag{font-size:8px;padding:2px 7px;border-radius:3px;font-weight:500;white-space:nowrap}
.scard .stag.run{background:rgba(34,197,94,.1);color:var(--gr)}
.scard .stag.idle{background:rgba(245,158,11,.1);color:var(--am)}
.scard .stag.maker{background:rgba(59,130,246,.1);color:var(--bl)}
.scard .stag.taker{background:rgba(239,68,68,.1);color:var(--rd)}
.scard .spnl{font-family:var(--m);font-size:20px;font-weight:700;margin-bottom:6px}
.scard .spnl.up{color:var(--gr)}.scard .spnl.dn{color:var(--rd)}
.scard .smeta{display:flex;gap:12px;font-size:9px;color:var(--tx);flex-wrap:wrap;margin-bottom:6px}
.scard .srisk{font-size:9px;color:var(--tx);padding:6px 8px;background:rgba(0,0,0,.2);border-radius:4px;margin-top:4px;line-height:1.6}
.scard .srisk b{color:var(--hi)}
.detail-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.6);z-index:200;display:none}
.detail-overlay.on{display:flex;align-items:flex-start;justify-content:center;padding-top:40px}
.detail-panel{background:var(--bg);border:1px solid var(--ln);border-radius:12px;width:95%;max-width:1100px;max-height:85vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.5)}
.detail-header{position:sticky;top:0;background:var(--srf);padding:16px 20px;border-bottom:1px solid var(--ln);display:flex;align-items:center;justify-content:space-between;z-index:5}
.detail-header h2{font-size:16px;font-weight:700}
.close-btn{background:none;border:1px solid var(--ln);color:var(--hi);padding:6px 14px;border-radius:6px;cursor:pointer;font-size:12px;font-family:var(--f);transition:all .15s}
.close-btn:hover{background:var(--hr)}
.detail-body{padding:20px}
.detail-body .chart-wrap{width:100%;height:280px;margin-bottom:16px;border-radius:var(--ra);overflow:hidden}
.detail-stats{display:grid;grid-template-columns:repeat(6,1fr);gap:8px;margin-bottom:16px}
.detail-section{margin-bottom:20px}
.detail-section h4{font-size:11px;font-weight:600;color:var(--tx);text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid var(--ln)}
.trade-table{width:100%;border-collapse:collapse;font-family:var(--m)}
.trade-table th{font-size:9px;font-weight:600;color:var(--tx);text-transform:uppercase;text-align:left;padding:8px 10px;border-bottom:1px solid var(--ln)}
.trade-table td{font-size:11px;padding:7px 10px;border-bottom:1px solid rgba(255,255,255,.02);color:var(--hi)}
.trade-table td.reason{font-size:10px;color:var(--tx);max-width:250px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:var(--f)}
.green{color:var(--gr)}.red{color:var(--rd)}
.desc-text{font-size:12px;color:var(--tx);line-height:1.6;padding:12px;background:var(--srf);border-radius:var(--ra);border:1px solid var(--ln);margin-bottom:16px}
.stat{background:var(--srf);border:1px solid var(--ln);border-radius:var(--ra);padding:12px 14px}
.stat .lbl{font-size:9px;color:var(--tx);text-transform:uppercase;letter-spacing:.5px;margin-bottom:3px}
.stat .val{font-family:var(--m);font-size:17px;font-weight:600}
.stat .val.up{color:var(--gr)}.stat .val.dn{color:var(--rd)}
footer{text-align:center;padding:30px;font-size:10px;color:#2a2a35}
footer a{color:#3f3f4a;text-decoration:none}footer a:hover{color:var(--tx)}
.risk-wrap{max-width:1440px;margin:0 auto 20px;padding:0 24px}
.risk-toggle{display:flex;align-items:center;gap:8px;cursor:pointer;background:none;border:1px solid var(--ln);border-radius:var(--ra);color:var(--tx);font-family:var(--f);font-size:11px;font-weight:600;padding:10px 16px;text-transform:uppercase;letter-spacing:.5px;transition:all .15s}
.risk-toggle:hover{color:var(--hi);border-color:var(--hr)}
.risk-toggle .arrow{display:inline-block;transition:transform .2s;font-size:10px}
.risk-toggle.open .arrow{transform:rotate(90deg)}
.risk-panel{display:none;background:var(--srf);border:1px solid var(--ln);border-radius:var(--ra);padding:16px;margin-top:8px}
.risk-panel.show{display:block}
@media(max-width:768px){
.topbar{padding:10px 14px;flex-direction:column;align-items:flex-start}
.tabs{padding:0 14px;top:88px;overflow-x:auto;white-space:nowrap}
.main-wrap{padding:12px 14px}
.sgrid{grid-template-columns:1fr 1fr}
.detail-stats{grid-template-columns:repeat(3,1fr)}
}
@media(max-width:380px){.sgrid{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="topbar">
<div style="display:flex;align-items:center;gap:10px">
<span class="status-dot" id="sdot"></span><div><h1>FTDT Quant Lab<span>Per-Strategy Dashboard</span></h1></div>
</div>
<div style="font-size:10px;color:var(--tx)" id="stsub"></div>
</div>
<div class="tabs">
<button class="tab on" id="tl-live" onclick="switchTab('live')">Live<span class="badge test">Testnet</span></button>
<button class="tab" id="tl-paper" onclick="switchTab('paper')">Paper<span class="badge main">$100K Mainnet</span></button>
<button class="tab" id="tl-backtest" onclick="switchTab('backtest')">Backtest</button>
<button class="tab" id="tl-historical" onclick="switchTab('historical')">Historical<span class="badge main">Real Data</span></button>
</div>
<div class="main-wrap">
<div class="panel show" id="pnl-live"><div class="sgrid" id="live-sgrid"></div></div>
<div class="panel" id="pnl-paper"><div class="sgrid" id="paper-sgrid"></div></div>
<div class="panel" id="pnl-backtest"><div class="sgrid" id="bt-sgrid"></div></div>
<div class="panel" id="pnl-historical"><div class="sgrid" id="hist-sgrid"></div></div>
</div>
<div class="risk-wrap">
<button class="risk-toggle" onclick="togglePositions()" id="pos-btn"><span class="arrow"></span> Open Positions & Orders (All Strategies)</button>
<div class="risk-panel" id="pos-panel">
<div class="detail-stats" id="pos-positions" style="font-size:11px;color:var(--tx)">No open positions</div>
<div style="font-size:11px;color:var(--tx)" id="pos-orders">No open orders</div>
</div>
</div>
<footer><a href="https://git.ftdt.io/rams/ftdt-quant-lab" target="_blank">rams/ftdt-quant-lab</a> &middot; 12 strategies &middot; $100K paper &middot; Hyperliquid</footer>
<div class="detail-overlay" id="detail-overlay" onclick="if(event.target===this)closeDetail()">
<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:8px;flex-wrap:wrap">
<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>
<select id="fee-tier-sel" style="display:none;font-size:10px;background:var(--srf);color:var(--hi);border:1px solid var(--ln);border-radius:4px;padding:3px 6px;font-family:var(--f)" onchange="onFeeTierChange()">
<option value="0">Tier 0 (0.045/0.015%)</option>
<option value="1">Tier 1 — >$5M (0.040/0.012%)</option>
<option value="2">Tier 2 — >$25M (0.035/0.008%)</option>
<option value="3">Tier 3 — >$100M (0.030/0.004%)</option>
<option value="4">Tier 4 — >$500M (0.028/0.000%)</option>
<option value="5">Tier 5 — >$2B (0.026/0.000%)</option>
<option value="6">Tier 6 — >$7B (0.024/0.000%)</option>
</select>
<select id="stake-tier-sel" style="display:none;font-size:10px;background:var(--srf);color:var(--hi);border:1px solid var(--ln);border-radius:4px;padding:3px 6px;font-family:var(--f)" onchange="onFeeTierChange()">
<option value="none">No Stake</option>
<option value="wood">Wood (×0.95)</option>
<option value="bronze">Bronze (×0.90)</option>
<option value="silver">Silver (×0.85)</option>
<option value="gold">Gold (×0.80)</option>
<option value="platinum">Platinum (×0.70)</option>
<option value="diamond">Diamond (×0.60)</option>
</select>
<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>
<div class="chart-wrap" id="det-chart-wrap"><div id="det-chart"></div></div>
<div class="detail-section"><h4>Trade History</h4>
<div style="overflow-x:auto"><table class="trade-table"><thead><tr><th>Time</th><th>Side</th><th>Size</th><th>Price</th><th>PnL</th><th>Fee</th><th>Reason / Signal</th></tr></thead><tbody id="det-trades"></tbody></table></div></div>
</div>
</div>
</div>
<script>
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 detChart=null, detSer=null;
function initDetChart(){
var el=document.getElementById('det-chart'); if(!el)return;
el.style.width='100%'; el.style.height='280px';
detChart=LightweightCharts.createChart(el,{
layout:{background:{color:'transparent'},textColor:'#d4d4e0'},
grid:{vertLines:{color:'rgba(255,255,255,.03)'},horzLines:{color:'rgba(255,255,255,.03)'}},
rightPriceScale:{borderColor:'rgba(255,255,255,.08)'},
timeScale:{borderColor:'rgba(255,255,255,.08)',timeVisible:true},
crosshair:{mode:0},width:el.offsetWidth,height:280
});
detSer=detChart.addAreaSeries({lineColor:'#3b82f6',topColor:'rgba(59,130,246,.15)',bottomColor:'rgba(59,130,246,.02)',lineWidth:2});
}
function switchTab(t){
currentTab=t;
['live','paper','backtest','historical'].forEach(function(x){document.getElementById('tl-'+x).className=t===x?'tab on':'tab'});
document.getElementById('pnl-live').className=t==='live'?'panel show':'panel';
document.getElementById('pnl-paper').className=t==='paper'?'panel show':'panel';
document.getElementById('pnl-backtest').className=t==='backtest'?'panel show':'panel';
document.getElementById('pnl-historical').className=t==='historical'?'panel show':'panel';
if(t==='live'&&lastData)renLive(lastData);
if(t==='paper'&&lastPaper)renPaper(lastPaper);
if(t==='backtest')loadBT();
if(t==='historical')loadHistBT();
}
function mapPositions(positions,orders){
var pm={},om={};
for(var i=0;i<(positions||[]).length;i++){
var p=positions[i],strat=p.strategy||'';
if(!pm[strat])pm[strat]=[]; pm[strat].push(p);
}
for(var j=0;j<(orders||[]).length;j++){
var o=orders[j],strat=o.strategy||'';
if(!om[strat])om[strat]=[]; om[strat].push(o);
}
return {pm:pm,om:om};
}
// Event delegation for card clicks
document.addEventListener('click',function(e){
var card=e.target.closest('.scard'); if(!card)return;
var name=card.dataset.strat, tab=card.dataset.tab;
if(name&&tab)openDetail(name,tab);
});
function renCards(sgridId,ss,baseEq,tab,positions,orders){
var keys=Object.keys(ss), pmo=mapPositions(positions,orders), h='';
for(var k=0;k<keys.length;k++){
var name=keys[k],s=ss[name],sp=s.pnl||0,eq=s.allocation+sp,cls=eq>=s.allocation?'up':'dn',pStr='$'+eq.toFixed(2);
var fm=s.fee_model||'taker';
var spos=(pmo.pm[name]||[]), sords=(pmo.om[name]||[]);
var riskHTML='<div class="srisk"><b>Allocation:</b> $'+s.allocation+' &middot; <b>Max pos:</b> '+(s.max_position||'\u2014')+' &middot; <b>Stop:</b> '+(s.stop_loss||'\u2014');
if(spos.length>0){
riskHTML+='<br><b>Positions:</b> ';
for(var pi=0;pi<spos.length;pi++){
var pp=spos[pi];
riskHTML+=Math.abs(pp.size||0).toFixed(4)+' @ $'+(pp.entry_px||0).toFixed(1)+(pp.pnl!==undefined?(pp.pnl>=0?' <span style=color:var(--gr)>+$':' <span style=color:var(--rd)>-$')+Math.abs(pp.pnl).toFixed(2)+'</span>':'')+(pi<spos.length-1?', ':'');
}
}
if(sords.length>0){riskHTML+='<br><b>Orders:</b> '+sords.length+' open';}
riskHTML+='</div>';
h+='<div class="scard" data-strat="'+name+'" data-tab="'+tab+'" id="scard-'+tab+'-'+name.replace(/\\s/g,'_')+'">'+
'<div class="sh"><div><div class="sname">'+name+'</div><div class="salloc">$'+s.allocation+' &middot; '+s.type+'</div></div>'+
'<div><span class="stag '+(s.status==='running'?'run':'idle')+'">'+(s.status==='running'?'RUNNING':'IDLE')+'</span>'+
'<span class="stag '+fm+'">'+fm.toUpperCase()+'</span></div></div>'+
'<div class="spnl '+cls+'">'+pStr+'</div>'+
'<div class="smeta"><span>PnL: <b class="'+(sp>=0?'green':'red')+'">'+(s.pnl_pct>=0?'+':'')+(s.pnl_pct||0).toFixed(2)+'%</b></span><span>Trades: <b>'+(s.trades_today||0)+'</b></span><span>Win: <b>'+Math.round((s.win_rate||0)*100)+'%</b></span><span>Pos: <b>'+(s.position||0).toFixed(4)+'</b></span></div>'+
riskHTML+'</div>';
}
document.getElementById(sgridId).innerHTML=h;
}
var currentBTName=null;
function toggleFees(){feeOn=document.getElementById('fee-toggle').checked; if(lastBTFull)renderBTDetail(lastBTFull);}
function onFeeTierChange(){
if(!currentBTName)return;
var ft=document.getElementById('fee-tier-sel').value, st=document.getElementById('stake-tier-sel').value;
document.getElementById('det-trades').innerHTML='<tr><td colspan="7" style="text-align:center;color:var(--tx);padding:20px">Recalculating with '+document.getElementById('fee-tier-sel').selectedOptions[0].text+'\u2026</td></tr>';
fetch('/cv/api/backtest/'+encodeURIComponent(currentBTName)+'/recalc?fee_tier='+ft+'&staking_tier='+st)
.then(function(r){return r.json()}).then(function(full){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">Recalc failed: '+e.message+'</td></tr>';});
}
function renderBTDetail(full){
var pnl=feeOn?(full.pnl_net||full.pnl||0):(full.pnl_gross||full.pnl||0);
var pnlPct=feeOn?(full.pnl_net_pct||full.pnl_pct||0):(full.pnl_gross_pct||full.pnl_pct||0);
var fees=full.fees_total||0, strat=full.strategy||'';
document.getElementById('det-name').textContent=strat+(feeOn?' (net of fees)':' (gross, no fees)');
document.getElementById('det-desc').textContent=strat+' \u2014 '+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>';
if(!detChart)initDetChart();
var pts=[],curve=full.equity_curve||[];
for(var i=0;i<curve.length;i++){if(curve[i]&&curve[i].t)var ct=curve[i].t;if(typeof ct==="string")ct=Math.floor(new Date(ct).getTime()/1000);pts.push({time:ct,value:curve[i].v})}
if(pts.length>0){detSer.setData(pts);detChart.timeScale().fitContent();setTimeout(function(){if(detChart){detChart.timeScale().fitContent();detChart.applyOptions({width:document.getElementById('det-chart').offsetWidth,height:280})}},250)}
var trows='',tlist=full.trades||[];
for(var j=Math.max(0,tlist.length-100);j<tlist.length;j++){
var t=tlist[j], tp=feeOn?(t.pnl_net||t.pnl||0):(t.pnl_gross||t.pnl||0), tf=t.fee||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="'+(tf>0?'red':'')+'">$'+tf.toFixed(4)+'</td><td class="reason">\u2014</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);
}
function openDetail(name,tab){
document.getElementById('detail-overlay').classList.add('on');
document.getElementById('det-name').textContent=name;
var ss=null, equity={}, trades=[];
if(tab==='paper'&&lastPaper){
ss=lastPaper.strategies||{}; equity=lastPaper.strategy_equity||{};
trades=(lastPaper.per_strategy_trades||{})[name]||[];
} else if(tab==='live'&&lastData){
ss=lastData.strategies||{};
equity=lastData.equity_history||[];
var allTrades=lastData.trades||[];
trades=allTrades.filter(function(t){return t.strategy===name||t.id===name});
} else if(tab==='backtest'&&lastBT&&lastBT[name]){
var b=lastBT[name]; currentBTName=b.name;
document.getElementById('fee-toggle-wrap').style.display='inline';
document.getElementById('fee-toggle').checked=true; feeOn=true;
document.getElementById('fee-tier-sel').style.display='inline';
document.getElementById('stake-tier-sel').style.display='inline';
document.getElementById('det-desc').textContent='';
document.getElementById('det-stats').innerHTML='<div class="stat"><div class="lbl">Loading</div><div class="val">\u2026</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\u2026</td></tr>';
fetch('/cv/api/backtest/'+encodeURIComponent(b.name)).then(function(r){return r.json()}).then(function(full){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: '+e.message+'</td></tr>';});
return;
}
var s=ss?ss[name]:null; if(!s){closeDetail();return}
document.getElementById('det-desc').textContent=s.description||'No description available.';
var sp=s.pnl||0;
document.getElementById('det-stats').innerHTML=
'<div class="stat"><div class="lbl">PnL</div><div class="val '+(sp>=0?'up':'dn')+'">'+(sp>=0?'+':'')+'$'+Math.abs(sp).toFixed(4)+'</div></div>'+
'<div class="stat"><div class="lbl">PnL%</div><div class="val '+(sp>=0?'up':'dn')+'">'+(s.pnl_pct>=0?'+':'')+(s.pnl_pct||0).toFixed(2)+'%</div></div>'+
'<div class="stat"><div class="lbl">Trades</div><div class="val">'+(s.trades_today||0)+'</div></div>'+
'<div class="stat"><div class="lbl">Win Rate</div><div class="val">'+Math.round((s.win_rate||0)*100)+'%</div></div>'+
'<div class="stat"><div class="lbl">Fees Paid</div><div class="val dn">$'+(s.fee_paid||0).toFixed(4)+'</div></div>'+
'<div class="stat"><div class="lbl">Position</div><div class="val">'+(s.position||0).toFixed(4)+'</div></div>';
if(!detChart)initDetChart();
var eqData=Array.isArray(equity)?equity:(equity[name]||[]);
if(eqData.length>0){
var pts=[];for(var i=0;i<eqData.length;i++){if(eqData[i]&&eqData[i].t){var edt=eqData[i].t;if(typeof edt==='string')edt=Math.floor(new Date(edt).getTime()/1000);pts.push({time:edt,value:eqData[i].v})}}
detSer.setData(pts);detChart.timeScale().fitContent();
}
var rows='';
for(var j=Math.max(0,trades.length-50);j<trades.length;j++){
var t=trades[j],tp=t.pnl||0;
rows+='<tr><td>'+t.time+'</td><td class="'+(t.side==='BUY'?'green':'red')+'">'+t.side+'</td><td>'+t.size+'</td><td>$'+t.price+'</td><td class="'+(tp>=0?'green':'red')+'">'+(tp>=0?'+':'')+'$'+Math.abs(tp).toFixed(4)+'</td><td class="red">$'+(t.fee||0).toFixed(4)+'</td><td class="reason" title="'+t.reason.replace(/"/g,'&quot;')+'">'+(t.reason||'\u2014')+'</td></tr>';
}
document.getElementById('det-trades').innerHTML=rows||'<tr><td colspan="7" style="text-align:center;color:var(--tx);padding:20px">No trades yet</td></tr>';
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');
document.getElementById('fee-toggle-wrap').style.display='none';
document.getElementById('fee-tier-sel').style.display='none';
document.getElementById('stake-tier-sel').style.display='none';
lastBTFull=null; currentBTName=null;
}
document.addEventListener('keydown',function(e){if(e.key==='Escape')closeDetail()});
var ws,wsPaper;
function connect(){
if(ws)try{ws.close()}catch(e){}
ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'/cv/ws');
ws.onopen=function(){document.getElementById('sdot').className='status-dot'};
ws.onclose=function(){document.getElementById('sdot').className='status-dot off';setTimeout(connect,5000)};
ws.onmessage=function(e){try{lastData=JSON.parse(e.data)}catch(ex){return};if(currentTab==='live')renLive(lastData)};
if(wsPaper)try{wsPaper.close()}catch(e){}
wsPaper=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'/cv/ws/paper');
wsPaper.onmessage=function(e){try{lastPaper=JSON.parse(e.data)}catch(ex){return};if(currentTab==='paper')renPaper(lastPaper)};
}
function renLive(d){
if(!d)return;
document.getElementById('stsub').textContent='Live Testnet \u2022 7 strategies active';
renCards('live-sgrid',d.strategies||{},d.base_equity||898,'live',d.open_positions||[],d.open_orders||[]);
var pos=d.open_positions||[],posH='';
if(pos.length>0){
for(var pi=0;pi<pos.length;pi++){
var pp=pos[pi];
posH+='<div class="stat"><div class="lbl">'+pp.strategy+'</div><div class="val">'+Math.abs(pp.size).toFixed(4)+' @ $'+(pp.entry_px||0).toFixed(1)+(pp.pnl!==undefined?(pp.pnl>=0?' <span style=color:var(--gr)>+$':' <span style=color:var(--rd)>-$')+Math.abs(pp.pnl).toFixed(2)+'</span>':'')+'</div></div>';
}
}else{posH='<div class="stat"><div class="lbl">No open positions</div></div>';}
document.getElementById('pos-positions').innerHTML=posH;
var ords=d.open_orders||[],ordH='';
if(ords.length>0){ordH='<table style="width:100%;font-size:10px"><tr style="color:var(--tx)"><th>Coin</th><th>Side</th><th>Size</th><th>Price</th></tr>';for(var oi=0;oi<ords.length;oi++){var oo=ords[oi];ordH+='<tr><td>'+oo.coin+'</td><td class="'+(oo.side==='B'?'green':'red')+'">'+(oo.side==='B'?'BUY':'SELL')+'</td><td>'+oo.sz+'</td><td>$'+(oo.limitPx||'MKT')+'</td></tr>'}ordH+='</table>'}else{ordH='No open orders';}
document.getElementById('pos-orders').innerHTML=ordH;
}
function renPaper(d){
if(!d)return;
document.getElementById('stsub').textContent='Paper \u2022 $'+(d.total_equity||'100K')+' \u2022 Regime: '+(d.regime||'\u2014');
renCards('paper-sgrid',d.strategies||{},d.base_equity||100000,'paper');
}
var lastBT={}, lastBTList=[];
function loadBT(){
fetch('/cv/api/backtests').then(function(r){return r.json()}).then(function(data){
lastBTList=data; lastBT={};
for(var i=0;i<data.length;i++){var b=data[i];if(!lastBT[b.strategy])lastBT[b.strategy]=b;}
var h='';
for(var s in lastBT){var b=lastBT[s];var pnl=b.pnl_pct||0;
h+='<div class="scard" data-strat="'+s+'" data-tab="backtest"><div class="sh"><div><div class="sname">'+s+'</div><div class="salloc">30-day &middot; $100</div></div><span class="stag run">BACKTEST</span></div><div class="spnl '+(pnl>=0?'up':'dn')+'">'+(pnl>=0?'+':'')+pnl.toFixed(2)+'%</div><div class="smeta"><span>Sharpe: <b>'+b.sharpe.toFixed(2)+'</b></span><span>DD: <b class="red">'+(b.max_dd*100).toFixed(2)+'%</b></span><span>Win: <b>'+Math.round(b.win_rate*100)+'%</b></span></div></div>';
}
document.getElementById('bt-sgrid').innerHTML=h||'<div style="padding:20px;color:var(--tx)">No backtests.</div>';
});
}
var lastHist={};
function loadHistBT(){
fetch('/cv/api/backtests/historical').then(function(r){return r.json()}).then(function(data){
lastHist={};
for(var i=0;i<data.length;i++){var b=data[i];if(!lastHist[b.strategy])lastHist[b.strategy]=b;}
var h='';
for(var s in lastHist){var b=lastHist[s];var pnl=b.pnl_pct||0;
h+='<div class="scard" data-strat="'+s+'" data-tab="historical"><div class="sh"><div><div class="sname">'+s+'</div><div class="salloc">30d '+b.coin+' &middot; Mainnet</div></div><span class="stag run">REAL DATA</span></div><div class="spnl '+(pnl>=0?'up':'dn')+'">'+(pnl>=0?'+':'')+pnl.toFixed(2)+'%</div><div class="smeta"><span>Sharpe: <b>'+b.sharpe.toFixed(2)+'</b></span><span>DD: <b class="red">'+(b.max_dd*100).toFixed(2)+'%</b></span><span>Win: <b>'+Math.round(b.win_rate*100)+'%</b></span></div></div>';
}
document.getElementById('hist-sgrid').innerHTML=h||'<div style="padding:20px;color:var(--tx)">No historical backtests. Run: python backtests/historical_runner.py --coin BTC --strategy all</div>';
});
}
function togglePositions(){
var p=document.getElementById('pos-panel'),b=document.getElementById('pos-btn');
p.classList.toggle('show');b.classList.toggle('open');
}
initDetChart();connect();loadBT();loadHistBT();
</script>
</body>
</html>