526a40b01e
Renamed all "VIP N" to "Tier N" in config, server, and HTML dropdowns. Added recalc_equity_curve() that rebuilds the equity curve with new fee rates. Previously the equity curve was passed through unchanged when switching tiers, so the chart visually stayed identical even though PnL numbers changed. Now each tier produces its own curve. Example OBI historical: Tier 0 equity ends at 141.5, Tier 6 at 149.7 — the chart visibly shifts up as fees drop from 4.5bp to 2.4bp taker.
515 lines
34 KiB
HTML
515 lines
34 KiB
HTML
<!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> · 12 strategies · $100K paper · 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 (0.040/0.012%)</option>
|
||
<option value="2">Tier 2 (0.035/0.008%)</option>
|
||
<option value="3">Tier 3 (0.030/0.004%)</option>
|
||
<option value="4">Tier 4 (0.028/0.000%)</option>
|
||
<option value="5">Tier 5 (0.026/0.000%)</option>
|
||
<option value="6">Tier 6 (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+' · '+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 · $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+' · 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 (|ρ| > 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>
|