Files
ftdt-quant-lab/dashboard/static/index.html.old
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

515 lines
34 KiB
Plaintext
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:#181825;--hr:#222230;--tx:#6b6b7b;--hi:#d4d4e0;--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}}
.portfolio{text-align:right;min-width:140px}
.portfolio .pnl{font-family:var(--m);font-size:28px;font-weight:700;letter-spacing:-1px}
.portfolio .pnl.up{color:var(--gr)}.portfolio .pnl.dn{color:var(--rd)}
.portfolio .sub{font-size:10px;color:var(--tx);text-transform:uppercase;letter-spacing:.5px}
.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}
/* Summary stats */
.stats-row{display:grid;grid-template-columns:repeat(6,1fr);gap:8px;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)}
/* Strategy grid */
.sgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,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}
/* Detail panel */
.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}
.main-chart{width:100%;height:220px;margin:8px 0 0;border-radius:var(--ra);overflow:hidden;background:rgba(0,0,0,.25)}
.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}
/* Footer */
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 Analytics panel — collapsible */
.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}
.risk-corr{font-family:var(--m);font-size:10px;color:var(--tx);line-height:1.8;margin-top:12px;padding:10px;background:rgba(0,0,0,.2);border-radius:6px;max-height:200px;overflow-y:auto}
.risk-corr .corr-high{color:var(--rd)}
.risk-corr .corr-med{color:var(--am)}
.risk-corr .corr-low{color:var(--tx)}
@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}
.stats-row{grid-template-columns:repeat(3,1fr)}.sgrid{grid-template-columns:1fr 1fr}
.detail-stats{grid-template-columns:repeat(3,1fr)}
.portfolio .pnl{font-size:22px}
}
@media(max-width:380px){.stats-row{grid-template-columns:repeat(2,1fr)}.sgrid{grid-template-columns:1fr}}
</style>
</head>
<body>
<!-- Top bar -->
<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>Professional Quant Dashboard</span></h1></div>
</div>
<div class="portfolio">
<div style="font-size:9px;color:var(--tx);text-transform:uppercase;letter-spacing:.5px">Portfolio Equity</div>
<div class="pnl" id="stpnl">$0.00</div>
<div class="sub" id="stpct">—</div>
</div>
</div>
<!-- Tabs -->
<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>
<!-- Main -->
<div class="main-wrap">
<div class="panel show" id="pnl-live">
<div class="stats-row" id="live-stats"></div>
<div class="sgrid" id="live-sgrid"></div>
<div class="chart-wrap main-chart" id="chart-live-wrap"><div id="chart-live"></div></div>
</div>
<div class="panel" id="pnl-paper">
<div class="stats-row" id="paper-stats"></div>
<div class="sgrid" id="paper-sgrid"></div>
<div class="chart-wrap main-chart" id="chart-paper-wrap"><div id="chart-paper"></div></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>
<!-- Risk Analytics -->
<div class="risk-wrap">
<button class="risk-toggle" onclick="toggleRisk()" id="risk-btn"><span class="arrow">▶</span> Risk Analytics</button>
<div class="risk-panel" id="risk-panel">
<div class="stats-row" id="risk-stats" style="margin-bottom:12px"></div>
<div class="risk-corr" id="risk-corr"></div>
</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>
<!-- Detail Overlay -->
<div class="detail-overlay" id="detail-overlay" onclick="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>
<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>
<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>
// ═══════════ State ═══════════
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 ═══════════
var detChart=null, detSer=null;
// ═══════════ Main area charts ═══════════
var chartLive=null, serLive=null, chartPaper=null, serPaper=null;
function initMainCharts(){
[{el:'chart-live',ch:'chartLive',sr:'serLive'},{el:'chart-paper',ch:'chartPaper',sr:'serPaper'}].forEach(function(c){
var el=document.getElementById(c.el);if(!el)return;
el.style.width='100%';el.style.height='220px';
window[c.ch]=LightweightCharts.createChart(el,{
layout:{background:{color:'transparent'},textColor:'#a0a0b0'},
grid:{vertLines:{color:'rgba(255,255,255,.02)'},horzLines:{color:'rgba(255,255,255,.03)'}},
rightPriceScale:{borderColor:'rgba(255,255,255,.08)',autoScale:true},
timeScale:{borderColor:'rgba(255,255,255,.08)',timeVisible:false},
crosshair:{mode:0},width:el.offsetWidth,height:220
});
window[c.sr]=window[c.ch].addAreaSeries({lineColor:'#3b82f6',topColor:'rgba(59,130,246,.15)',bottomColor:'rgba(59,130,246,.02)',lineWidth:2});
});
}
function pushEquity(chart,ser,data){
if(!chart||!ser||!data||!data.length)return;
var pts=[];
for(var i=0;i<data.length;i++){
var t=data[i].t||data[i].time||data[i][0];
var v=data[i].v||data[i].value||data[i].equity||data[i][1];
if(typeof t==='number'){
if(t>1e12)t=Math.floor(t/1000);
pts.push({time:t,value:v});
}
}
if(pts.length>0){ser.setData(pts);chart.timeScale().fitContent()}
}
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});
}
// ═══════════ Tab switching ═══════════
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();
}
// ═══════════ Render strategy cards ═══════════
function renCards(sgridId,ss,baseEq,tab,statsRowId){
var keys=Object.keys(ss),totalPnl=0,trades=0,fees=0,active=0;
for(var i=0;i<keys.length;i++){var s=ss[keys[i]];totalPnl+=s.pnl||0;trades+=s.trades_today||0;fees+=s.fee_paid||0;if(s.status==='running')active++}
if(statsRowId){
document.getElementById(statsRowId).innerHTML='<div class="stat"><div class="lbl">Equity</div><div class="val">$'+((baseEq||0)+totalPnl).toFixed(0)+'</div></div>'+
'<div class="stat"><div class="lbl">PnL</div><div class="val '+(totalPnl>=0?'up':'dn')+'">'+(totalPnl>=0?'+':'')+'$'+Math.abs(totalPnl).toFixed(2)+'</div></div>'+
'<div class="stat"><div class="lbl">Trades</div><div class="val">'+trades+'</div></div>'+
'<div class="stat"><div class="lbl">Fees</div><div class="val dn">$'+fees.toFixed(2)+'</div></div>'+
'<div class="stat"><div class="lbl">Active</div><div class="val">'+active+'/'+keys.length+'</div></div>'+
'<div class="stat"><div class="lbl">Alloc</div><div class="val">$'+(keys[0]?ss[keys[0]].allocation||0:0)+'k/strat</div></div>';
}
var h='';
for(var k=0;k<keys.length;k++){
var name=keys[k],s=ss[name],sp=s.pnl||0,cls=sp>=0?'up':'dn',pStr=(sp>=0?'+':'')+'$'+Math.abs(sp).toFixed(2);
var fm=s.fee_model||'taker';
h+='<div class="scard" onclick="openDetail(\''+name+'\',\''+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>'+
'</div>';
}
document.getElementById(sgridId).innerHTML=h;
}
// ═══════════ Fee toggle ═══════════
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;
var 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+'…</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>';
});
}
// ═══════════ Render backtest detail with fee toggle ──
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;
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)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)}
// 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');
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||{};
// Live node doesn't send per-strategy equity — use overall equity_history
equity=lastData.equity_history||[];
// Filter trades by strategy name
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('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('/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}
// Description
document.getElementById('det-desc').textContent=s.description||'No description available.';
// Stats
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>';
// Equity chart
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();
}
// Trades
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+'">'+(t.reason||'—')+'</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>';
// Resize chart
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';document.getElementById('dl-csv').style.display='none';lastBTFull=null;currentBTName=null}
document.addEventListener('keydown',function(e){if(e.key==='Escape')closeDetail()});
// ═══════════ WebSocket + render ═══════════
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;var p=d.total_pnl||0;document.getElementById('stpnl').textContent=(p>=0?'+':'')+'$'+Math.abs(p).toFixed(2);document.getElementById('stpnl').className='pnl '+(p>=0?'up':'dn');document.getElementById('stpct').textContent='Testnet · Equity: $'+((d.base_equity||898)+p).toFixed(2);renCards("live-sgrid",d.strategies||{},d.base_equity||898,"live","live-stats");if(d.equity_history&&chartLive)pushEquity(chartLive,serLive,d.equity_history)}
function renPaper(d){if(!d)return;var p=d.total_pnl||0;document.getElementById('stpnl').textContent=(p>=0?'+':'')+'$'+Math.abs(p).toFixed(2);document.getElementById('stpnl').className='pnl '+(p>=0?'up':'dn');document.getElementById('stpct').textContent='Paper · '+d.total_equity+' · Regime: '+(d.regime||'—');renCards("paper-sgrid",d.strategies||{},d.base_equity||100000,"paper","paper-stats");if(d.equity_history&&chartPaper)pushEquity(chartPaper,serPaper,d.equity_history)}
// ═══════════ Backtests ═══════════
var lastBT={}, lastBTList=[];
function loadBT(){
fetch('/cv/api/backtests').then(function(r){return r.json()}).then(function(data){
lastBTList=data; lastBT={};
// Keep latest backtest per strategy (sorted by time desc — first wins)
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\" onclick=\"openDetail(\''+s+'\',\'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>';
})
}
// ═══════════ Historical backtests ═══════════
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+'" onclick="openHistDetail(this.dataset.strat)"><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 openHistDetail(strat){
var b=lastHist[strat];if(!b)return;
document.getElementById('detail-overlay').classList.add('on');
document.getElementById('fee-toggle-wrap').style.display='inline';
document.getElementById('fee-tier-sel').style.display='inline';
document.getElementById('stake-tier-sel').style.display='inline';
document.getElementById('dl-csv').style.display='none';
document.getElementById('fee-toggle').checked=true; feeOn=true; currentBTName=b.name;
document.getElementById('det-name').textContent=strat+' (Historical '+b.coin+')';
fetch('/cv/api/backtest/historical/'+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: '+e.message+'</td></tr>';
});
}
// ═══════════ Init ═══════════
initDetChart();initMainCharts();connect();loadBT();loadHistBT();
// ═══════════ Risk Analytics ═══════════
function toggleRisk(){
var p=document.getElementById('risk-panel'),b=document.getElementById('risk-btn');
p.classList.toggle('show');b.classList.toggle('open');
if(p.classList.contains('show')&&!p.dataset.loaded){loadRisk();p.dataset.loaded='1'}
}
function loadRisk(){
fetch('/cv/api/risk').then(function(r){return r.json()}).then(function(d){
if(d.error){document.getElementById('risk-stats').innerHTML='<div style="color:var(--tx);padding:8px">'+d.error+'</div>';return}
var pf=d.portfolio||{};
document.getElementById('risk-stats').innerHTML=
'<div class="stat"><div class="lbl">VaR 95%</div><div class="val dn">'+(pf.var_95*100).toFixed(2)+'%</div></div>'+
'<div class="stat"><div class="lbl">CVaR 95%</div><div class="val dn">'+(pf.cvar_95*100).toFixed(2)+'%</div></div>'+
'<div class="stat"><div class="lbl">Max DD</div><div class="val dn">'+(pf.max_drawdown*100).toFixed(2)+'%</div></div>'+
'<div class="stat"><div class="lbl">Calmar</div><div class="val '+(pf.calmar_ratio>=0?'up':'dn')+'">'+pf.calmar_ratio.toFixed(2)+'</div></div>'+
'<div class="stat"><div class="lbl">Sharpe</div><div class="val '+(pf.sharpe>=0?'up':'dn')+'">'+pf.sharpe.toFixed(2)+'</div></div>'+
'<div class="stat"><div class="lbl">Sortino</div><div class="val">'+pf.sortino.toFixed(2)+'</div></div>';
// Correlation summary
var cs=d.correlation_summary||[];
var ch='<div style="font-size:10px;color:var(--tx);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px">Strategy Correlations (|ρ| &gt; 0.3)</div>';
if(cs.length===0){ch+='<span style="color:var(--tx)">No significant correlations found — strategies are well-diversified.</span>'}
else{for(var i=0;i<cs.length;i++){var c=cs[i],cls=c.level==='high'?'corr-high':'corr-med';ch+='<div><span class="'+cls+'">ρ='+(c.correlation>=0?'+':'')+c.correlation.toFixed(3)+'</span> '+c.pair+'</div>'}}
document.getElementById('risk-corr').innerHTML=ch;
// Mark loaded + store timestamp
window._riskLoaded=Date.now();
}).catch(function(e){document.getElementById('risk-stats').innerHTML='<div style="color:var(--rd);padding:8px">Failed: '+e.message+'</div>'})
}
// Auto-refresh risk panel when paper data updates (throttled to every 30s)
var _origRenPaper=renPaper;
renPaper=function(d){
_origRenPaper(d);
var p=document.getElementById('risk-panel');
if(p&&p.classList.contains('show')&&(!window._riskLoaded||Date.now()-window._riskLoaded>30000)){
loadRisk();
}
};
</script>
</body>
</html>