Files
ftdt-quant-lab/dashboard/static/index.html
T
ramseshk e6dd16908c shadcn/ui design system — Claude Blu 2 dark theme
- Full oklch color space from tweakcn/r/themes/cmmea3qbd000004jvb99v39cd
- Semantic tokens: --background, --foreground, --card, --primary, etc.
- Backward compat mappings (--bg, --srf, --ln, --tx, --hi, etc.)
- Dark shadows with proper opacity (--shadow-xs through --shadow-xl)
- 8px scrollbar, focus rings, backdrop blurs
- Cleaner card styling with subtle borders and hover lift
- All existing JS functionality preserved
2026-08-05 03:31:10 +00:00

675 lines
36 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>
/* ═══════════════════ shadcn/ui Design System — Claude Blu 2 Dark ═══════════════════ */
:root {
/* shadcn semantic tokens */
--background: oklch(0.1776 0 0);
--foreground: oklch(0.7905 0.0126 259.8241);
--card: oklch(0.2638 0.0024 247.9155);
--card-foreground: oklch(0.9755 0.0045 258.3245);
--popover: oklch(0.3046 0.0023 247.9001);
--popover-foreground: oklch(0.9176 0.0026 228.7865);
--primary: oklch(0.6576 0.1208 252.0832);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.2171 0.0025 247.9411);
--secondary-foreground: oklch(0.7559 0.0125 239.9659);
--muted: oklch(0.2171 0.0025 247.9411);
--muted-foreground: oklch(0.7559 0.0125 239.9659);
--accent: oklch(0.2038 0.0067 258.3676);
--accent-foreground: oklch(0.9590 0.0059 239.8204);
--destructive: oklch(0.6368 0.2078 25.3313);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.3506 0.0066 248.0169);
--input: oklch(0.4217 0.0084 248.0280);
--ring: oklch(0.6576 0.1208 252.0832);
/* Chart palette */
--chart-1: oklch(0.5098 0.1320 257.5458);
--chart-2: oklch(0.7360 0.1191 266.5038);
--chart-3: oklch(0.2038 0.0067 258.3676);
--chart-4: oklch(0.3361 0.0333 253.9085);
--chart-5: oklch(0.4893 0.1504 261.9302);
/* Semantic layer scale */
--layer-0: var(--background);
--layer-1: color-mix(in oklch, var(--card) 50%, var(--background));
--layer-2: var(--card);
--layer-3: var(--popover);
/* Old var name mappings (backward compat) */
--bg: var(--background);
--srf: var(--card);
--ln: var(--border);
--hr: color-mix(in oklch, var(--border) 70%, transparent);
--tx: var(--muted-foreground);
--hi: var(--foreground);
--bl: var(--primary);
--rd: var(--destructive);
--gr: oklch(0.7057 0.1946 143.1487); /* green */
--am: oklch(0.7610 0.1858 70.0831); /* amber */
--pu: oklch(0.6197 0.1996 312.7621); /* purple */
--cy: oklch(0.7150 0.1250 197.2349); /* cyan */
--pk: oklch(0.6686 0.2158 0.7958); /* pink */
/* Typography */
--radius: 0.5rem;
--radius-sm: calc(var(--radius) - 0.125rem);
--radius-lg: calc(var(--radius) + 0.125rem);
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
--f: var(--font-sans);
--m: var(--font-mono);
--ra: var(--radius);
/* Shadows (dark) */
--shadow-2xs: 0 1px 2px 0 hsl(0 0% 0% / 0.15);
--shadow-xs: 0 1px 2px 0 hsl(0 0% 0% / 0.15);
--shadow-sm: 0 1px 3px 0 hsl(0 0% 0% / 0.30), 0 1px 2px -1px hsl(0 0% 0% / 0.30);
--shadow: 0 1px 3px 0 hsl(0 0% 0% / 0.30), 0 1px 2px -1px hsl(0 0% 0% / 0.30);
--shadow-md: 0 4px 6px -1px hsl(0 0% 0% / 0.30), 0 2px 4px -2px hsl(0 0% 0% / 0.30);
--shadow-lg: 0 10px 15px -3px hsl(0 0% 0% / 0.30), 0 4px 6px -4px hsl(0 0% 0% / 0.30);
--shadow-xl: 0 20px 25px -5px hsl(0 0% 0% / 0.30), 0 8px 10px -6px hsl(0 0% 0% / 0.30);
}
/* ═══════════════════ Base ═══════════════════ */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--background);
color: var(--foreground);
font-family: var(--font-sans);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: "cv02","cv03","cv04","cv11";
}
::selection { background: color-mix(in oklch, var(--primary) 30%, transparent); }
/* ═══════════════════ Topbar ═══════════════════ */
.topbar {
position: sticky; top: 0; z-index: 100;
background: color-mix(in oklch, var(--background) 95%, transparent);
backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
border-bottom: 1px solid var(--border);
padding: 12px 24px;
display: flex; align-items: center; justify-content: space-between;
flex-wrap: wrap; gap: 12px;
}
.topbar h1 {
font-size: 15px; font-weight: 700; letter-spacing: -0.02em;
display: flex; align-items: center; gap: 10px;
}
.topbar h1 span { font-size: 10px; color: var(--muted-foreground); font-weight: 500; }
.status-dot {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
background: var(--gr); animation: pulse 2s infinite;
}
.status-dot.off { background: var(--destructive); animation: none; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
/* ═══════════════════ Tabs ═══════════════════ */
.tabs {
display: flex; gap: 0; padding: 0 24px;
border-bottom: 1px solid var(--border);
position: sticky; top: 52px; z-index: 99;
background: color-mix(in oklch, var(--background) 95%, transparent);
backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
}
.tab {
padding: 10px 20px; font-size: 12px; font-weight: 500; cursor: pointer;
background: none; border: none; border-bottom: 2px solid transparent;
color: var(--muted-foreground); font-family: var(--font-sans);
transition: all .15s ease;
}
.tab:hover { color: var(--foreground); }
.tab.on { color: var(--foreground); border-bottom-color: var(--primary); }
.badge {
font-size: 8px; padding: 2px 7px; border-radius: var(--radius-sm);
font-weight: 600; margin-left: 6px; text-transform: uppercase; letter-spacing: .5px;
}
.badge.test { background: color-mix(in oklch, var(--am) 15%, transparent); color: var(--am); }
.badge.main { background: color-mix(in oklch, var(--pu) 15%, transparent); color: var(--pu); }
/* ═══════════════════ Layout ═══════════════════ */
.main-wrap { max-width: 1440px; margin: 0 auto; padding: 24px 24px; }
.panel { display: none; }
.panel.show { display: block; }
/* ═══════════════════ Strategy Grid ═══════════════════ */
.sgrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px; margin-bottom: 24px;
}
/* ═══════════════════ shadcn-style Card ═══════════════════ */
.scard {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 18px 20px;
cursor: pointer;
transition: all .2s ease;
position: relative;
box-shadow: var(--shadow-2xs);
}
.scard:hover {
border-color: var(--ring);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.scard.selected {
border-color: var(--primary);
box-shadow: 0 0 0 1px color-mix(in oklch, var(--primary) 40%, transparent);
}
.scard .sh {
display: flex; justify-content: space-between; align-items: flex-start;
margin-bottom: 10px;
}
.scard .sname { font-size: 13px; font-weight: 600; line-height: 1.3; max-width: 70%; }
.scard .salloc { font-size: 9px; color: var(--muted-foreground); margin-top: 3px; }
.scard .stag {
font-size: 8px; padding: 3px 8px; border-radius: var(--radius-sm);
font-weight: 500; white-space: nowrap;
}
.scard .stag.run { background: color-mix(in oklch, var(--gr) 12%, transparent); color: var(--gr); }
.scard .stag.idle { background: color-mix(in oklch, var(--am) 12%, transparent); color: var(--am); }
.scard .stag.maker { background: color-mix(in oklch, var(--primary) 12%, transparent); color: var(--primary); }
.scard .stag.taker { background: color-mix(in oklch, var(--destructive) 12%, transparent); color: var(--destructive); }
.scard .spnl {
font-family: var(--font-mono); font-size: 22px; font-weight: 700;
margin-bottom: 8px; letter-spacing: -0.02em;
}
.scard .spnl.up { color: var(--gr); }
.scard .spnl.dn { color: var(--destructive); }
.scard .smeta {
display: flex; gap: 14px; font-size: 9px; color: var(--muted-foreground);
flex-wrap: wrap; margin-bottom: 8px;
}
.scard .srisk {
font-size: 9px; color: var(--muted-foreground);
padding: 8px 10px;
background: color-mix(in oklch, var(--muted) 50%, transparent);
border-radius: var(--radius-sm);
margin-top: 6px; line-height: 1.7;
}
.scard .srisk b { color: var(--foreground); font-weight: 600; }
/* ═══════════════════ Stats (detail & rows) ═══════════════════ */
.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 8px; margin-bottom: 16px;
}
.stat {
background: color-mix(in oklch, var(--card) 60%, var(--background));
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
}
.stat .lbl {
font-size: 9px; color: var(--muted-foreground);
text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 4px;
}
.stat .val {
font-family: var(--font-mono); font-size: 17px; font-weight: 600;
}
.stat .val.up { color: var(--gr); }
.stat .val.dn { color: var(--destructive); }
/* ═══════════════════ Detail Overlay ═══════════════════ */
.detail-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: oklch(0 0 0 / 0.65); z-index: 200; display: none;
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
}
.detail-overlay.on {
display: flex; align-items: flex-start; justify-content: center;
padding-top: 40px;
}
.detail-panel {
background: var(--layer-0);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
width: 95%; max-width: 1100px; max-height: 85vh;
overflow-y: auto;
box-shadow: var(--shadow-xl);
}
.detail-header {
position: sticky; top: 0;
background: color-mix(in oklch, var(--card) 80%, var(--background));
backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
padding: 16px 20px; border-bottom: 1px solid var(--border);
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(--border);
color: var(--foreground); padding: 6px 14px; border-radius: var(--radius);
cursor: pointer; font-size: 12px; font-family: var(--font-sans);
transition: all .15s;
}
.close-btn:hover { background: var(--accent); color: var(--accent-foreground); }
.detail-body { padding: 20px; }
.detail-body .chart-wrap {
width: 100%; height: 280px; margin-bottom: 16px;
border-radius: var(--radius); overflow: hidden;
background: color-mix(in oklch, var(--muted) 40%, transparent);
}
.detail-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 8px; margin-bottom: 16px;
}
.detail-section { margin-bottom: 20px; }
.detail-section h4 {
font-size: 10px; font-weight: 600; color: var(--muted-foreground);
text-transform: uppercase; letter-spacing: 0.05em;
margin-bottom: 10px; padding-bottom: 6px;
border-bottom: 1px solid var(--border);
}
/* ═══════════════════ Trade Table ═══════════════════ */
.trade-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); }
.trade-table th {
font-size: 9px; font-weight: 600; color: var(--muted-foreground);
text-transform: uppercase; text-align: left;
padding: 8px 10px; border-bottom: 1px solid var(--border);
}
.trade-table td {
font-size: 11px; padding: 7px 10px;
border-bottom: 1px solid color-mix(in oklch, var(--border) 30%, transparent);
color: var(--foreground);
}
.trade-table td.reason {
font-size: 10px; color: var(--muted-foreground);
max-width: 250px; white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; font-family: var(--font-sans);
}
.green { color: var(--gr); }
.red { color: var(--destructive); }
/* ═══════════════════ Description ═══════════════════ */
.desc-text {
font-size: 12px; color: var(--muted-foreground); line-height: 1.7;
padding: 14px; background: color-mix(in oklch, var(--muted) 60%, transparent);
border-radius: var(--radius); border: 1px solid var(--border);
margin-bottom: 16px;
}
/* ═══════════════════ Collapsible Panels (Risk, Positions) ═══════════════════ */
.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(--border);
border-radius: var(--radius); color: var(--muted-foreground);
font-family: var(--font-sans); font-size: 11px; font-weight: 600;
padding: 10px 16px; text-transform: uppercase; letter-spacing: .5px;
transition: all .15s;
}
.risk-toggle:hover { color: var(--foreground); border-color: var(--ring); }
.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(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px; margin-top: 8px;
}
.risk-panel.show { display: block; }
/* ═══════════════════ Footer ═══════════════════ */
footer { text-align: center; padding: 30px; font-size: 10px; color: var(--muted-foreground); }
footer a { color: var(--muted-foreground); text-decoration: none; }
footer a:hover { color: var(--primary); }
/* ═══════════════════ Select / Input Overrides ═══════════════════ */
select {
background: var(--card); color: var(--foreground);
border: 1px solid var(--border);
border-radius: var(--radius-sm); padding: 4px 8px;
font-family: var(--font-sans);
}
select:focus { outline: none; border-color: var(--ring); box-shadow: 0 0 0 2px color-mix(in oklch, var(--ring) 20%, transparent); }
/* ═══════════════════ Scrollbar ═══════════════════ */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--input); }
/* ═══════════════════ Responsive ═══════════════════ */
@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: 14px 14px; }
.sgrid { grid-template-columns: 1fr 1fr; }
.detail-stats { grid-template-columns: 1fr 1fr 1fr; }
.stats-row { grid-template-columns: 1fr 1fr 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(--muted-foreground)" 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(--muted-foreground)">No open positions</div>
<div style="font-size:11px;color:var(--muted-foreground)" 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> · 12 strategies · $100K paper · 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>