design: premium fintech UI — DM Serif, dark/light toggle, coral accents, glassmorphism nav
This commit is contained in:
parent
03035b3965
commit
30884f10e0
1 changed files with 573 additions and 269 deletions
|
|
@ -1,324 +1,628 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Position Tracker</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<title>Angel · Position Tracker</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<style>
|
||||
body { background: #0f172a; color: #e2e8f0; font-family: system-ui, sans-serif; }
|
||||
.card { background: #1e293b; border: 1px solid #334155; border-radius: 0.75rem; }
|
||||
.pnl-up { color: #4ade80; } .pnl-down { color: #f87171; } .pnl-flat { color: #94a3b8; }
|
||||
.badge { padding: 2px 8px; border-radius: 9999px; font-size: 0.7rem; font-weight: 600; }
|
||||
.badge-opt { background: #3b1f5f; color: #d8b4fe; }
|
||||
.badge-fut { background: #1f3b2a; color: #86efac; }
|
||||
.badge-eq { background: #1e3a5f; color: #93c5fd; }
|
||||
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.5} }
|
||||
.live-dot { animation: pulse 2s infinite; }
|
||||
/* ── Design tokens ─────────────────────────────────────────────────── */
|
||||
:root {
|
||||
--coral: #FF6B4A;
|
||||
--coral-dim: rgba(255,107,74,0.12);
|
||||
--amber: #F5A623;
|
||||
--amber-dim: rgba(245,166,35,0.12);
|
||||
--green: #2ECC71;
|
||||
--green-dim: rgba(46,204,113,0.12);
|
||||
--red: #E74C3C;
|
||||
--red-dim: rgba(231,76,60,0.12);
|
||||
--purple: #9B59B6;
|
||||
--transition: 0.25s cubic-bezier(0.4,0,0.2,1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg: #0D1117;
|
||||
--bg2: #161B22;
|
||||
--bg3: #1C2330;
|
||||
--border: rgba(255,255,255,0.07);
|
||||
--border2: rgba(255,255,255,0.12);
|
||||
--text: #E6EDF3;
|
||||
--text2: #8B949E;
|
||||
--text3: #484F58;
|
||||
--card-bg: rgba(22,27,34,0.95);
|
||||
--card-glow: rgba(255,107,74,0.04);
|
||||
--shadow: 0 4px 24px rgba(0,0,0,0.4);
|
||||
--shadow-lg: 0 8px 48px rgba(0,0,0,0.6);
|
||||
--glass: rgba(22,27,34,0.8);
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--bg: #FAF7F4;
|
||||
--bg2: #FFFFFF;
|
||||
--bg3: #F4EFE9;
|
||||
--border: rgba(0,0,0,0.06);
|
||||
--border2: rgba(0,0,0,0.12);
|
||||
--text: #1A1410;
|
||||
--text2: #6B6056;
|
||||
--text3: #B8AFA8;
|
||||
--card-bg: rgba(255,255,255,0.95);
|
||||
--card-glow: rgba(255,107,74,0.03);
|
||||
--shadow: 0 4px 24px rgba(0,0,0,0.08);
|
||||
--shadow-lg: 0 8px 48px rgba(0,0,0,0.12);
|
||||
--glass: rgba(255,255,255,0.85);
|
||||
}
|
||||
|
||||
/* ── Reset & base ──────────────────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: 'Geist', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
transition: background var(--transition), color var(--transition);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Background texture */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed; inset: 0; pointer-events: none; z-index: 0;
|
||||
background-image:
|
||||
radial-gradient(ellipse 80vw 60vh at 10% 0%, rgba(255,107,74,0.06) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 60vw 50vh at 90% 100%, rgba(245,166,35,0.04) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
.wrap { max-width: 1280px; margin: 0 auto; padding: 24px 20px; position: relative; z-index: 1; }
|
||||
|
||||
/* ── Typography ────────────────────────────────────────────────────── */
|
||||
.serif { font-family: 'DM Serif Display', serif; }
|
||||
.mono { font-family: 'Geist Mono', monospace; }
|
||||
|
||||
/* ── Nav ───────────────────────────────────────────────────────────── */
|
||||
nav {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 16px 24px; margin-bottom: 32px;
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.nav-left { display: flex; align-items: center; gap: 12px; }
|
||||
|
||||
.nav-logo {
|
||||
font-family: 'DM Serif Display', serif;
|
||||
font-size: 1.2rem;
|
||||
color: var(--coral);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.nav-divider { width: 1px; height: 20px; background: var(--border2); }
|
||||
|
||||
.nav-subtitle { font-size: 0.75rem; font-weight: 500; color: var(--text2); letter-spacing: 0.06em; text-transform: uppercase; }
|
||||
|
||||
.nav-right { display: flex; align-items: center; gap: 10px; }
|
||||
|
||||
.status-pill {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 5px 12px; border-radius: 9999px;
|
||||
font-size: 0.72rem; font-weight: 600; letter-spacing: 0.04em;
|
||||
border: 1px solid var(--border2);
|
||||
color: var(--text2);
|
||||
background: var(--bg3);
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.status-pill.open { color: var(--green); border-color: rgba(46,204,113,0.3); background: var(--green-dim); }
|
||||
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||||
.status-pill.open .status-dot { animation: pulse 2s infinite; }
|
||||
|
||||
@keyframes pulse { 0%,100%{opacity:1; transform:scale(1)} 50%{opacity:0.5; transform:scale(0.8)} }
|
||||
|
||||
.last-updated { font-size: 0.72rem; color: var(--text3); font-family: 'Geist Mono', monospace; }
|
||||
|
||||
.icon-btn {
|
||||
width: 36px; height: 36px; border-radius: 10px;
|
||||
border: 1px solid var(--border2);
|
||||
background: var(--bg3);
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: all var(--transition);
|
||||
font-size: 16px;
|
||||
}
|
||||
.icon-btn:hover { background: var(--bg2); color: var(--text); border-color: var(--coral); transform: translateY(-1px); }
|
||||
.icon-btn:active { transform: translateY(0); }
|
||||
|
||||
/* ── P&L Hero ──────────────────────────────────────────────────────── */
|
||||
.pnl-hero {
|
||||
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pnl-card {
|
||||
padding: 24px 24px 20px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
box-shadow: var(--shadow);
|
||||
position: relative; overflow: hidden;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.pnl-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); }
|
||||
|
||||
/* Accent bar at top */
|
||||
.pnl-card::before {
|
||||
content: ''; position: absolute; top: 0; left: 0; right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--accent-a), var(--accent-b));
|
||||
border-radius: 20px 20px 0 0;
|
||||
}
|
||||
|
||||
/* Subtle glow blob */
|
||||
.pnl-card::after {
|
||||
content: ''; position: absolute; top: -40px; right: -40px;
|
||||
width: 120px; height: 120px; border-radius: 50%;
|
||||
background: var(--accent-a);
|
||||
opacity: 0.08; filter: blur(30px); pointer-events: none;
|
||||
}
|
||||
|
||||
.pnl-card.unrealised { --accent-a: #2ECC71; --accent-b: #1abc9c; }
|
||||
.pnl-card.realised { --accent-a: #3498DB; --accent-b: #2980b9; }
|
||||
.pnl-card.total { --accent-a: #FF6B4A; --accent-b: #F5A623; }
|
||||
|
||||
.pnl-label {
|
||||
font-size: 0.7rem; font-weight: 600; letter-spacing: 0.08em;
|
||||
text-transform: uppercase; color: var(--text2); margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pnl-value {
|
||||
font-family: 'DM Serif Display', serif;
|
||||
font-size: 2.4rem; line-height: 1;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 8px;
|
||||
transition: color var(--transition);
|
||||
}
|
||||
.pnl-card.total .pnl-value { font-size: 3rem; }
|
||||
|
||||
.pnl-sub { font-size: 0.72rem; color: var(--text3); }
|
||||
|
||||
.up { color: var(--green); }
|
||||
.down { color: var(--red); }
|
||||
.flat { color: var(--text2); }
|
||||
|
||||
/* ── Stats row ─────────────────────────────────────────────────────── */
|
||||
.stats-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px; }
|
||||
|
||||
.stat-card {
|
||||
padding: 16px 18px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
box-shadow: var(--shadow);
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.stat-label { font-size: 0.68rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text2); margin-bottom: 6px; }
|
||||
.stat-value { font-family: 'DM Serif Display', serif; font-size: 1.6rem; color: var(--text); }
|
||||
.stat-value.amber { color: var(--amber); }
|
||||
.stat-value.red { color: var(--red); }
|
||||
|
||||
/* ── Chart card ────────────────────────────────────────────────────── */
|
||||
.chart-card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 18px 24px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.chart-title { font-size: 0.82rem; font-weight: 600; letter-spacing: 0.04em; color: var(--text); }
|
||||
|
||||
.range-btns { display: flex; gap: 4px; }
|
||||
.range-btn {
|
||||
padding: 4px 12px; border-radius: 8px; font-size: 0.72rem; font-weight: 600;
|
||||
border: 1px solid var(--border2); cursor: pointer; background: transparent;
|
||||
color: var(--text2); transition: all 0.15s;
|
||||
}
|
||||
.range-btn:hover { color: var(--text); border-color: var(--coral); }
|
||||
.range-btn.active { background: var(--coral); border-color: var(--coral); color: #fff; }
|
||||
|
||||
.chart-body { padding: 16px 20px 20px; height: 200px; position: relative; }
|
||||
.chart-empty { display: none; position: absolute; inset: 0; align-items: center; justify-content: center; color: var(--text3); font-size: 0.8rem; }
|
||||
.chart-empty.show { display: flex; }
|
||||
|
||||
/* ── Table card ────────────────────────────────────────────────────── */
|
||||
.table-card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 18px 24px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.table-title { font-size: 0.82rem; font-weight: 600; color: var(--text); }
|
||||
.live-badge {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
font-size: 0.68rem; font-weight: 600; letter-spacing: 0.06em;
|
||||
color: var(--green); text-transform: uppercase;
|
||||
}
|
||||
.live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead th {
|
||||
padding: 10px 20px; text-align: right;
|
||||
font-size: 0.66rem; font-weight: 600; letter-spacing: 0.08em;
|
||||
text-transform: uppercase; color: var(--text3);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
thead th:first-child { text-align: left; }
|
||||
|
||||
tbody tr { transition: background 0.12s; }
|
||||
tbody tr:hover { background: rgba(255,107,74,0.03); }
|
||||
tbody tr + tr { border-top: 1px solid var(--border); }
|
||||
|
||||
td {
|
||||
padding: 13px 20px; font-size: 0.82rem;
|
||||
text-align: right; color: var(--text);
|
||||
}
|
||||
td:first-child { text-align: left; }
|
||||
|
||||
.sym-name { font-weight: 600; font-size: 0.8rem; color: var(--text); }
|
||||
.sym-meta { font-size: 0.67rem; color: var(--text3); margin-top: 2px; font-family: 'Geist Mono', monospace; }
|
||||
|
||||
.qty-long { color: var(--green); font-weight: 600; font-family: 'Geist Mono', monospace; }
|
||||
.qty-short { color: var(--red); font-weight: 600; font-family: 'Geist Mono', monospace; }
|
||||
|
||||
.mono-val { font-family: 'Geist Mono', monospace; font-size: 0.8rem; }
|
||||
|
||||
tfoot tr { border-top: 2px solid var(--border2); background: var(--bg3); }
|
||||
tfoot td { padding: 12px 20px; font-weight: 600; font-size: 0.82rem; }
|
||||
tfoot td:first-child { font-size: 0.68rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text2); }
|
||||
|
||||
/* ── Alerts card ───────────────────────────────────────────────────── */
|
||||
.alert-row-up { border-left: 3px solid var(--green) !important; }
|
||||
.alert-row-down { border-left: 3px solid var(--red) !important; }
|
||||
|
||||
/* ── Utility ───────────────────────────────────────────────────────── */
|
||||
.section-gap { margin-bottom: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen p-4">
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">📊 Position Tracker</h1>
|
||||
<p class="text-slate-400 text-sm" id="last-updated">Loading...</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span id="market-status" class="badge"></span>
|
||||
<button onclick="refresh()" class="bg-slate-700 hover:bg-slate-600 text-white text-sm px-3 py-1.5 rounded-lg">↺ Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Nav -->
|
||||
<nav>
|
||||
<div class="nav-left">
|
||||
<span class="nav-logo">Angel</span>
|
||||
<div class="nav-divider"></div>
|
||||
<span class="nav-subtitle">Position Tracker</span>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<span class="last-updated" id="last-updated">—</span>
|
||||
<div class="status-pill" id="market-status">
|
||||
<span class="status-dot"></span>
|
||||
<span id="market-label">Checking</span>
|
||||
</div>
|
||||
<button class="icon-btn" id="theme-toggle" title="Toggle theme" onclick="toggleTheme()">🌙</button>
|
||||
<button class="icon-btn" title="Refresh" onclick="refresh()">↺</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- P&L Summary — 3 big cards -->
|
||||
<div class="grid grid-cols-3 gap-3 mb-4">
|
||||
<div class="card p-4 border-l-4 border-green-500">
|
||||
<p class="text-slate-400 text-xs mb-1">Unrealised P&L</p>
|
||||
<p class="text-2xl font-bold" id="total-unrealised">—</p>
|
||||
<p class="text-slate-500 text-xs mt-1">Open positions mark-to-market</p>
|
||||
</div>
|
||||
<div class="card p-4 border-l-4 border-blue-500">
|
||||
<p class="text-slate-400 text-xs mb-1">Realised P&L</p>
|
||||
<p class="text-2xl font-bold" id="total-realised">—</p>
|
||||
<p class="text-slate-500 text-xs mt-1">Closed legs today</p>
|
||||
</div>
|
||||
<div class="card p-4 border-l-4 border-purple-500">
|
||||
<p class="text-slate-400 text-xs mb-1">Total P&L</p>
|
||||
<p class="text-3xl font-bold" id="total-pnl">—</p>
|
||||
<p class="text-slate-500 text-xs mt-1">Unrealised + Realised</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secondary stats row -->
|
||||
<div class="grid grid-cols-3 gap-3 mb-4">
|
||||
<div class="card p-3">
|
||||
<p class="text-slate-400 text-xs mb-1">Open Positions</p>
|
||||
<p class="text-xl font-bold text-white" id="open-count">—</p>
|
||||
</div>
|
||||
<div class="card p-3">
|
||||
<p class="text-slate-400 text-xs mb-1">Alerts Today</p>
|
||||
<p class="text-xl font-bold text-amber-400" id="alert-count">—</p>
|
||||
</div>
|
||||
<div class="card p-3">
|
||||
<p class="text-slate-400 text-xs mb-1">Last Error</p>
|
||||
<p class="text-sm text-red-400 truncate" id="last-error">None</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Intraday P&L chart -->
|
||||
<div class="card p-4 mb-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="font-semibold text-white">Intraday P&L</h2>
|
||||
<div class="flex gap-2 text-xs">
|
||||
<button onclick="loadChart(2)" id="btn-2h" class="px-2 py-1 rounded bg-slate-600 text-slate-300 hover:bg-slate-500">2H</button>
|
||||
<button onclick="loadChart(8)" id="btn-8h" class="px-2 py-1 rounded bg-indigo-600 text-white">8H</button>
|
||||
<button onclick="loadChart(24)" id="btn-24h" class="px-2 py-1 rounded bg-slate-600 text-slate-300 hover:bg-slate-500">24H</button>
|
||||
<button onclick="loadChart(72)" id="btn-72h" class="px-2 py-1 rounded bg-slate-600 text-slate-300 hover:bg-slate-500">3D</button>
|
||||
<!-- P&L Hero -->
|
||||
<div class="pnl-hero">
|
||||
<div class="pnl-card unrealised">
|
||||
<div class="pnl-label">Unrealised P&L</div>
|
||||
<div class="pnl-value flat" id="val-unrealised">—</div>
|
||||
<div class="pnl-sub">Open positions MTM</div>
|
||||
</div>
|
||||
<div class="pnl-card realised">
|
||||
<div class="pnl-label">Realised P&L</div>
|
||||
<div class="pnl-value flat" id="val-realised">—</div>
|
||||
<div class="pnl-sub">Closed legs today</div>
|
||||
</div>
|
||||
<div class="pnl-card total">
|
||||
<div class="pnl-label">Total P&L</div>
|
||||
<div class="pnl-value flat" id="val-total">—</div>
|
||||
<div class="pnl-sub">Unrealised + Realised</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height:200px; position:relative;">
|
||||
<canvas id="pnl-chart"></canvas>
|
||||
<p id="chart-empty" class="hidden text-center text-slate-500 text-sm pt-16">No history yet — populates after first poll cycle</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Positions table -->
|
||||
<div class="card mb-4 overflow-hidden">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-700">
|
||||
<h2 class="font-semibold text-white">Option Positions</h2>
|
||||
<span class="flex items-center gap-2 text-xs text-green-400">
|
||||
<span class="live-dot w-2 h-2 rounded-full bg-green-400 inline-block"></span> Live
|
||||
</span>
|
||||
<!-- Stats -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Open Positions</div>
|
||||
<div class="stat-value" id="stat-positions">—</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Alerts Today</div>
|
||||
<div class="stat-value amber" id="stat-alerts">—</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Last Error</div>
|
||||
<div class="stat-value red" id="stat-error" style="font-size:0.9rem;font-family:'Geist',sans-serif;margin-top:4px">None</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
|
||||
<!-- Chart -->
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">Intraday P&L Curve</span>
|
||||
<div class="range-btns">
|
||||
<button class="range-btn" onclick="loadChart(2)" id="btn-2h">2H</button>
|
||||
<button class="range-btn active" onclick="loadChart(8)" id="btn-8h">8H</button>
|
||||
<button class="range-btn" onclick="loadChart(24)" id="btn-24h">24H</button>
|
||||
<button class="range-btn" onclick="loadChart(72)" id="btn-72h">3D</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-body">
|
||||
<canvas id="pnl-chart"></canvas>
|
||||
<div class="chart-empty" id="chart-empty">No history yet — populates after first poll cycle</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Positions -->
|
||||
<div class="table-card section-gap">
|
||||
<div class="table-header">
|
||||
<span class="table-title">Option Positions</span>
|
||||
<div class="live-badge"><span class="live-dot"></span> Live</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="text-slate-400 border-b border-slate-700 text-xs">
|
||||
<th class="text-left px-4 py-2">Symbol</th>
|
||||
<th class="text-right px-4 py-2">Qty</th>
|
||||
<th class="text-right px-4 py-2">LTP</th>
|
||||
<th class="text-right px-4 py-2">Avg</th>
|
||||
<th class="text-right px-4 py-2">Unrealised</th>
|
||||
<th class="text-right px-4 py-2">Realised</th>
|
||||
<th class="text-right px-4 py-2">Total</th>
|
||||
<tr>
|
||||
<th>Symbol</th>
|
||||
<th>Qty</th>
|
||||
<th>LTP</th>
|
||||
<th>Avg Cost</th>
|
||||
<th>Unrealised</th>
|
||||
<th>Realised</th>
|
||||
<th>Total P&L</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="positions-tbody">
|
||||
<tr><td colspan="7" class="text-center py-8 text-slate-500">Loading...</td></tr>
|
||||
<tbody id="pos-tbody">
|
||||
<tr><td colspan="7" style="text-align:center;padding:40px;color:var(--text3)">Loading positions…</td></tr>
|
||||
</tbody>
|
||||
<!-- Totals row -->
|
||||
<tfoot id="positions-tfoot" class="hidden border-t-2 border-slate-600 bg-slate-800/60 font-semibold text-sm">
|
||||
<tfoot id="pos-tfoot" style="display:none">
|
||||
<tr>
|
||||
<td class="px-4 py-2 text-slate-300" colspan="4">Portfolio Total</td>
|
||||
<td class="px-4 py-2 text-right" id="foot-unrealised">—</td>
|
||||
<td class="px-4 py-2 text-right" id="foot-realised">—</td>
|
||||
<td class="px-4 py-2 text-right" id="foot-total">—</td>
|
||||
<td colspan="4">Portfolio Total</td>
|
||||
<td id="foot-u" class="mono-val">—</td>
|
||||
<td id="foot-r" class="mono-val">—</td>
|
||||
<td id="foot-t" class="mono-val">—</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent alerts -->
|
||||
<div class="card overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-slate-700">
|
||||
<h2 class="font-semibold text-white">Recent Alerts</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<!-- Alerts -->
|
||||
<div class="table-card">
|
||||
<div class="table-header">
|
||||
<span class="table-title">Alert History</span>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="text-slate-400 border-b border-slate-700 text-xs">
|
||||
<th class="text-left px-4 py-2">Symbol</th>
|
||||
<th class="text-right px-4 py-2">P&L at Alert</th>
|
||||
<th class="text-right px-4 py-2">From Anchor</th>
|
||||
<th class="text-right px-4 py-2">Move</th>
|
||||
<th class="text-right px-4 py-2">LTP</th>
|
||||
<th class="text-right px-4 py-2">Time (IST)</th>
|
||||
<tr>
|
||||
<th>Symbol</th>
|
||||
<th>P&L at Alert</th>
|
||||
<th>Anchor</th>
|
||||
<th>Move</th>
|
||||
<th>LTP</th>
|
||||
<th>Time (IST)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="alerts-tbody">
|
||||
<tr><td colspan="6" class="text-center py-8 text-slate-500">No alerts yet</td></tr>
|
||||
<tbody id="alert-tbody">
|
||||
<tr><td colspan="6" style="text-align:center;padding:32px;color:var(--text3)">No alerts yet</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /wrap -->
|
||||
|
||||
<script>
|
||||
// ── helpers ──────────────────────────────────────────────────────────────
|
||||
const fmt = (n, d=0) => {
|
||||
if (n == null) return '—';
|
||||
const s = Math.abs(n).toLocaleString('en-IN', {maximumFractionDigits:d});
|
||||
return (n >= 0 ? '+' : '-') + '₹' + s;
|
||||
};
|
||||
const pnlClass = n => n > 0 ? 'pnl-up' : n < 0 ? 'pnl-down' : 'pnl-flat';
|
||||
const toIST = s => new Date(s+(s.includes('Z')?'':'Z')).toLocaleString('en-IN',
|
||||
{timeZone:'Asia/Kolkata', hour12:false, month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'});
|
||||
// ── Theme ─────────────────────────────────────────────────────────────────
|
||||
let currentTheme = localStorage.getItem('theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', currentTheme);
|
||||
document.getElementById('theme-toggle').textContent = currentTheme === 'dark' ? '☀️' : '🌙';
|
||||
|
||||
let chartInstance = null;
|
||||
let currentHours = 8;
|
||||
function toggleTheme() {
|
||||
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', currentTheme);
|
||||
document.getElementById('theme-toggle').textContent = currentTheme === 'dark' ? '☀️' : '🌙';
|
||||
localStorage.setItem('theme', currentTheme);
|
||||
if (chartInstance) { chartInstance.destroy(); chartInstance = null; loadChart(currentHours); }
|
||||
}
|
||||
|
||||
// ── chart ─────────────────────────────────────────────────────────────────
|
||||
async function loadChart(hours=8) {
|
||||
currentHours = hours;
|
||||
['2','8','24','72'].forEach(h => {
|
||||
const b = document.getElementById(`btn-${h}h`);
|
||||
b.className = h == hours
|
||||
? 'px-2 py-1 rounded bg-indigo-600 text-white'
|
||||
: 'px-2 py-1 rounded bg-slate-600 text-slate-300 hover:bg-slate-500';
|
||||
});
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
const fmt = (n, d=0) => {
|
||||
if (n == null) return '—';
|
||||
const abs = Math.abs(n).toLocaleString('en-IN', {maximumFractionDigits: d});
|
||||
return (n >= 0 ? '+₹' : '-₹') + abs;
|
||||
};
|
||||
const pnlCls = n => n > 50 ? 'up' : n < -50 ? 'down' : 'flat';
|
||||
const toIST = s => new Date(s + (s.includes('Z') ? '' : 'Z'))
|
||||
.toLocaleString('en-IN', {timeZone:'Asia/Kolkata', hour12:false,
|
||||
month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'});
|
||||
|
||||
const r = await fetch(`/api/pnl-history?hours=${hours}`);
|
||||
const {summary, history} = await r.json();
|
||||
// ── Chart ──────────────────────────────────────────────────────────────────
|
||||
let chartInstance = null;
|
||||
let currentHours = 8;
|
||||
|
||||
// Update summary cards from latest snapshot
|
||||
if (summary) {
|
||||
const setCard = (id, val) => {
|
||||
const el = document.getElementById(id);
|
||||
el.textContent = fmt(val);
|
||||
el.className = 'text-2xl font-bold ' + pnlClass(val);
|
||||
};
|
||||
setCard('total-unrealised', summary.totalUnrealised);
|
||||
setCard('total-realised', summary.totalRealised);
|
||||
const tp = document.getElementById('total-pnl');
|
||||
tp.textContent = fmt(summary.totalPnl);
|
||||
tp.className = 'text-3xl font-bold ' + pnlClass(summary.totalPnl);
|
||||
document.getElementById('open-count').textContent = summary.openPositions;
|
||||
}
|
||||
async function loadChart(h = 8) {
|
||||
currentHours = h;
|
||||
document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active'));
|
||||
const btnMap = {2:'btn-2h', 8:'btn-8h', 24:'btn-24h', 72:'btn-72h'};
|
||||
document.getElementById(btnMap[h])?.classList.add('active');
|
||||
|
||||
const canvas = document.getElementById('pnl-chart');
|
||||
const empty = document.getElementById('chart-empty');
|
||||
const r = await fetch(`/api/pnl-history?hours=${h}`);
|
||||
const { summary, history } = await r.json();
|
||||
|
||||
if (!history || history.length < 2) {
|
||||
canvas.style.display = 'none';
|
||||
empty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
canvas.style.display = 'block';
|
||||
empty.classList.add('hidden');
|
||||
if (summary) {
|
||||
const set = (id, val) => {
|
||||
const el = document.getElementById(id);
|
||||
el.textContent = fmt(val);
|
||||
el.className = 'pnl-value ' + pnlCls(val);
|
||||
};
|
||||
set('val-unrealised', summary.totalUnrealised);
|
||||
set('val-realised', summary.totalRealised);
|
||||
set('val-total', summary.totalPnl);
|
||||
document.getElementById('stat-positions').textContent = summary.openPositions;
|
||||
}
|
||||
|
||||
const labels = history.map(h => toIST(h.recorded_at));
|
||||
const unrealised = history.map(h => h.total_unrealised);
|
||||
const realised = history.map(h => h.total_realised);
|
||||
const total = history.map(h => h.total_pnl);
|
||||
const canvas = document.getElementById('pnl-chart');
|
||||
const empty = document.getElementById('chart-empty');
|
||||
|
||||
if (chartInstance) chartInstance.destroy();
|
||||
chartInstance = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{ label: 'Total P&L', data: total, borderColor: '#a78bfa', backgroundColor: 'rgba(167,139,250,0.08)', tension: 0.3, pointRadius: 0, borderWidth: 2, fill: true },
|
||||
{ label: 'Unrealised', data: unrealised, borderColor: '#4ade80', backgroundColor: 'transparent', tension: 0.3, pointRadius: 0, borderWidth: 1.5, borderDash: [] },
|
||||
{ label: 'Realised', data: realised, borderColor: '#60a5fa', backgroundColor: 'transparent', tension: 0.3, pointRadius: 0, borderWidth: 1.5, borderDash: [4,3] },
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
animation: false,
|
||||
interaction: { intersect: false, mode: 'index' },
|
||||
plugins: {
|
||||
legend: { labels: { color: '#94a3b8', boxWidth: 12, font: { size: 11 } } },
|
||||
tooltip: {
|
||||
backgroundColor: '#1e293b', borderColor: '#475569', borderWidth: 1,
|
||||
titleColor: '#94a3b8', bodyColor: '#e2e8f0',
|
||||
callbacks: {
|
||||
label: ctx => ` ${ctx.dataset.label}: ${ctx.raw >= 0 ? '+' : ''}₹${Math.round(ctx.raw).toLocaleString('en-IN')}`
|
||||
}
|
||||
if (!history || history.length < 2) {
|
||||
canvas.style.display = 'none'; empty.classList.add('show'); return;
|
||||
}
|
||||
canvas.style.display = 'block'; empty.classList.remove('show');
|
||||
|
||||
const isDark = currentTheme === 'dark';
|
||||
const gridColor = isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)';
|
||||
const textColor = isDark ? '#484F58' : '#B8AFA8';
|
||||
|
||||
const labels = history.map(h => toIST(h.recorded_at));
|
||||
const unrealised = history.map(h => h.total_unrealised);
|
||||
const realised = history.map(h => h.total_realised);
|
||||
const total = history.map(h => h.total_pnl);
|
||||
|
||||
if (chartInstance) chartInstance.destroy();
|
||||
chartInstance = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{ label: 'Total P&L', data: total,
|
||||
borderColor: '#FF6B4A', backgroundColor: 'rgba(255,107,74,0.08)',
|
||||
tension: 0.4, pointRadius: 0, borderWidth: 2, fill: true },
|
||||
{ label: 'Unrealised', data: unrealised,
|
||||
borderColor: '#2ECC71', backgroundColor: 'transparent',
|
||||
tension: 0.4, pointRadius: 0, borderWidth: 1.5 },
|
||||
{ label: 'Realised', data: realised,
|
||||
borderColor: '#3498DB', backgroundColor: 'transparent',
|
||||
tension: 0.4, pointRadius: 0, borderWidth: 1.5, borderDash: [4,3] },
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false, animation: false,
|
||||
interaction: { intersect: false, mode: 'index' },
|
||||
plugins: {
|
||||
legend: { labels: { color: textColor, boxWidth: 10, font: { size: 11, family: 'Geist' } } },
|
||||
tooltip: {
|
||||
backgroundColor: isDark ? '#1C2330' : '#fff',
|
||||
borderColor: isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
|
||||
borderWidth: 1, titleColor: textColor,
|
||||
bodyColor: isDark ? '#E6EDF3' : '#1A1410',
|
||||
padding: 10, cornerRadius: 8,
|
||||
callbacks: {
|
||||
label: ctx => ` ${ctx.dataset.label}: ${ctx.raw >= 0 ? '+' : ''}₹${Math.round(ctx.raw).toLocaleString('en-IN')}`
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: { ticks: { color: '#475569', maxTicksLimit: 8, font:{size:10} }, grid: { color: '#1e293b' } },
|
||||
y: { ticks: { color: '#475569', font:{size:10},
|
||||
callback: v => (v>=0?'+':'')+'₹'+Math.round(v).toLocaleString('en-IN') },
|
||||
grid: { color: '#334155' },
|
||||
border: { dash: [4,4] } }
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: { ticks: { color: textColor, maxTicksLimit: 8, font: {size:10, family:'Geist Mono'} }, grid: { color: gridColor } },
|
||||
y: { ticks: { color: textColor, font: {size:10, family:'Geist Mono'},
|
||||
callback: v => (v>=0?'+':'')+'₹'+Math.round(v).toLocaleString('en-IN') },
|
||||
grid: { color: gridColor } }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── positions ─────────────────────────────────────────────────────────────
|
||||
async function loadPositions() {
|
||||
const r = await fetch('/api/positions');
|
||||
const {data} = await r.json();
|
||||
const open = data.filter(p => p.is_closed === 0);
|
||||
const tbody = document.getElementById('positions-tbody');
|
||||
const tfoot = document.getElementById('positions-tfoot');
|
||||
|
||||
if (open.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="7" class="text-center py-8 text-slate-500">No open positions</td></tr>`;
|
||||
tfoot.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const sumU = open.reduce((s,p) => s + p.unrealised_pnl, 0);
|
||||
const sumR = open.reduce((s,p) => s + p.realised_pnl, 0);
|
||||
const sumT = open.reduce((s,p) => s + p.total_pnl, 0);
|
||||
// ── Positions ──────────────────────────────────────────────────────────────
|
||||
async function loadPositions() {
|
||||
const r = await fetch('/api/positions');
|
||||
const { data } = await r.json();
|
||||
const open = data.filter(p => p.is_closed === 0);
|
||||
const tbody = document.getElementById('pos-tbody');
|
||||
const tfoot = document.getElementById('pos-tfoot');
|
||||
|
||||
tbody.innerHTML = open.map(p => `
|
||||
<tr class="border-b border-slate-800 hover:bg-slate-800/40">
|
||||
<td class="px-4 py-2.5 font-medium text-white text-xs">
|
||||
${p.tradingsymbol}<br>
|
||||
<span class="text-slate-500">${p.exchange} · ${p.producttype}</span>
|
||||
</td>
|
||||
<td class="px-4 py-2.5 text-right ${p.netqty < 0 ? 'text-red-300' : 'text-green-300'} font-medium">${p.netqty}</td>
|
||||
<td class="px-4 py-2.5 text-right text-white">₹${(+p.ltp).toFixed(2)}</td>
|
||||
<td class="px-4 py-2.5 text-right text-slate-300">₹${(+p.avg_price).toFixed(2)}</td>
|
||||
<td class="px-4 py-2.5 text-right font-medium ${pnlClass(p.unrealised_pnl)}">${fmt(p.unrealised_pnl)}</td>
|
||||
<td class="px-4 py-2.5 text-right ${pnlClass(p.realised_pnl)}">${fmt(p.realised_pnl)}</td>
|
||||
<td class="px-4 py-2.5 text-right font-bold ${pnlClass(p.total_pnl)}">${fmt(p.total_pnl)}</td>
|
||||
</tr>`).join('');
|
||||
|
||||
// Totals footer
|
||||
tfoot.classList.remove('hidden');
|
||||
const set = (id, v) => { const el=document.getElementById(id); el.textContent=fmt(v); el.className=`px-4 py-2 text-right ${pnlClass(v)}`; };
|
||||
set('foot-unrealised', sumU);
|
||||
set('foot-realised', sumR);
|
||||
set('foot-total', sumT);
|
||||
if (!open.length) {
|
||||
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;padding:40px;color:var(--text3)">No open positions</td></tr>`;
|
||||
tfoot.style.display = 'none'; return;
|
||||
}
|
||||
|
||||
// ── alerts ────────────────────────────────────────────────────────────────
|
||||
async function loadAlerts() {
|
||||
const r = await fetch('/api/alerts?limit=20');
|
||||
const {data} = await r.json();
|
||||
const tbody = document.getElementById('alerts-tbody');
|
||||
const today = new Date().toISOString().slice(0,10);
|
||||
document.getElementById('alert-count').textContent =
|
||||
data.filter(a => a.alerted_at.startsWith(today)).length;
|
||||
const sumU = open.reduce((s,p) => s + p.unrealised_pnl, 0);
|
||||
const sumR = open.reduce((s,p) => s + p.realised_pnl, 0);
|
||||
const sumT = open.reduce((s,p) => s + p.total_pnl, 0);
|
||||
|
||||
if (!data.length) return;
|
||||
tbody.innerHTML = data.map(a => `
|
||||
<tr class="border-b border-slate-800 hover:bg-slate-800/40">
|
||||
<td class="px-4 py-2.5 font-medium text-white">${a.direction==='up'?'🟢':'🔴'} ${a.tradingsymbol}</td>
|
||||
<td class="px-4 py-2.5 text-right font-medium ${pnlClass(a.current_pnl)}">${fmt(a.current_pnl)}</td>
|
||||
<td class="px-4 py-2.5 text-right text-slate-300">${fmt(a.anchor_pnl)}</td>
|
||||
<td class="px-4 py-2.5 text-right ${pnlClass(a.delta_abs)}">${fmt(a.delta_abs)} (${a.delta_pct>0?'+':''}${(+a.delta_pct).toFixed(1)}%)</td>
|
||||
<td class="px-4 py-2.5 text-right text-slate-300">₹${(+a.ltp).toFixed(2)}</td>
|
||||
<td class="px-4 py-2.5 text-right text-slate-400">${toIST(a.alerted_at)}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
tbody.innerHTML = open.map(p => `
|
||||
<tr>
|
||||
<td>
|
||||
<div class="sym-name">${p.tradingsymbol}</div>
|
||||
<div class="sym-meta">${p.exchange} · ${p.producttype}</div>
|
||||
</td>
|
||||
<td class="${p.netqty < 0 ? 'qty-short' : 'qty-long'}">${p.netqty > 0 ? '+' : ''}${p.netqty}</td>
|
||||
<td class="mono-val">₹${(+p.ltp).toFixed(2)}</td>
|
||||
<td class="mono-val" style="color:var(--text2)">₹${(+p.avg_price).toFixed(2)}</td>
|
||||
<td class="mono-val ${pnlCls(p.unrealised_pnl)}">${fmt(p.unrealised_pnl)}</td>
|
||||
<td class="mono-val ${pnlCls(p.realised_pnl)}">${fmt(p.realised_pnl)}</td>
|
||||
<td class="mono-val ${pnlCls(p.total_pnl)}" style="font-weight:600">${fmt(p.total_pnl)}</td>
|
||||
</tr>`).join('');
|
||||
|
||||
// ── health ────────────────────────────────────────────────────────────────
|
||||
async function loadHealth() {
|
||||
const r = await fetch('/api/health');
|
||||
const d = await r.json();
|
||||
const ms = document.getElementById('market-status');
|
||||
ms.textContent = d.marketOpen ? '🟢 Market Open' : '🔴 Market Closed';
|
||||
ms.className = 'badge ' + (d.marketOpen ? 'bg-green-900 text-green-300' : 'bg-slate-700 text-slate-400');
|
||||
document.getElementById('last-error').textContent = d.lastError ? d.lastError.error.slice(0,40) : 'None';
|
||||
document.getElementById('last-updated').textContent =
|
||||
'Updated: ' + new Date().toLocaleTimeString('en-IN',{timeZone:'Asia/Kolkata',hour12:false}) + ' IST';
|
||||
}
|
||||
tfoot.style.display = '';
|
||||
const sf = (id,v) => {
|
||||
const el = document.getElementById(id);
|
||||
el.textContent = fmt(v);
|
||||
el.className = `mono-val ${pnlCls(v)}`;
|
||||
};
|
||||
sf('foot-u', sumU); sf('foot-r', sumR); sf('foot-t', sumT);
|
||||
}
|
||||
|
||||
// ── main ──────────────────────────────────────────────────────────────────
|
||||
async function refresh() {
|
||||
await Promise.all([loadPositions(), loadAlerts(), loadHealth(), loadChart(currentHours)]);
|
||||
}
|
||||
// ── Alerts ─────────────────────────────────────────────────────────────────
|
||||
async function loadAlerts() {
|
||||
const r = await fetch('/api/alerts?limit=20');
|
||||
const { data } = await r.json();
|
||||
const today = new Date().toISOString().slice(0,10);
|
||||
const todayCount = data.filter(a => a.alerted_at.startsWith(today)).length;
|
||||
document.getElementById('stat-alerts').textContent = todayCount;
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 60_000);
|
||||
if (!data.length) return;
|
||||
document.getElementById('alert-tbody').innerHTML = data.map(a => `
|
||||
<tr class="${a.direction==='up' ? 'alert-row-up' : 'alert-row-down'}">
|
||||
<td><div class="sym-name">${a.direction==='up'?'▲':'▼'} ${a.tradingsymbol}</div></td>
|
||||
<td class="mono-val ${pnlCls(a.current_pnl)}">${fmt(a.current_pnl)}</td>
|
||||
<td class="mono-val" style="color:var(--text2)">${fmt(a.anchor_pnl)}</td>
|
||||
<td class="mono-val ${pnlCls(a.delta_abs)}">${fmt(a.delta_abs)} <span style="opacity:0.6;font-size:0.75em">(${a.delta_pct>0?'+':''}${(+a.delta_pct).toFixed(1)}%)</span></td>
|
||||
<td class="mono-val">₹${(+a.ltp).toFixed(2)}</td>
|
||||
<td style="color:var(--text3);font-family:'Geist Mono',monospace;font-size:0.75rem">${toIST(a.alerted_at)}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
// ── Health ─────────────────────────────────────────────────────────────────
|
||||
async function loadHealth() {
|
||||
const r = await fetch('/api/health');
|
||||
const d = await r.json();
|
||||
const pill = document.getElementById('market-status');
|
||||
const label = document.getElementById('market-label');
|
||||
label.textContent = d.marketOpen ? 'Market Open' : 'Market Closed';
|
||||
pill.className = 'status-pill' + (d.marketOpen ? ' open' : '');
|
||||
document.getElementById('stat-error').textContent = d.lastError ? d.lastError.error.slice(0,30)+'…' : 'None';
|
||||
document.getElementById('last-updated').textContent =
|
||||
new Date().toLocaleTimeString('en-IN',{timeZone:'Asia/Kolkata',hour12:false}) + ' IST';
|
||||
}
|
||||
|
||||
// ── Main loop ──────────────────────────────────────────────────────────────
|
||||
async function refresh() {
|
||||
await Promise.all([loadPositions(), loadAlerts(), loadHealth(), loadChart(currentHours)]);
|
||||
}
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 60_000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue