e6dd16908c
- 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
675 lines
36 KiB
HTML
675 lines
36 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>
|
||
|
||
/* ═══════════════════ 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+' · <b>Max pos:</b> '+(s.max_position||'\u2014')+' · <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+' · '+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,'"')+'">'+(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 · $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+' · 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> |