position-tracker/public/index.html
2026-05-08 11:22:05 +00:00

200 lines
9.1 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<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>
<style>
body { background: #0f172a; color: #e2e8f0; font-family: 'Inter', 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-eq { background: #1e3a5f; color: #93c5fd; }
.badge-opt { background: #3b1f5f; color: #d8b4fe; }
.badge-fut { background: #1f3b2a; color: #86efac; }
.badge-closed { background: #374151; color: #9ca3af; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.5} }
.live-dot { animation: pulse 2s infinite; }
</style>
</head>
<body class="min-h-screen p-4">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<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>
<!-- Summary cards -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6" id="summary-cards">
<div class="card p-4"><p class="text-slate-400 text-xs mb-1">Total P&L</p><p class="text-2xl font-bold" id="total-pnl"></p></div>
<div class="card p-4"><p class="text-slate-400 text-xs mb-1">Open Positions</p><p class="text-2xl font-bold text-white" id="open-count"></p></div>
<div class="card p-4"><p class="text-slate-400 text-xs mb-1">Alerts Today</p><p class="text-2xl font-bold text-amber-400" id="alert-count"></p></div>
<div class="card p-4"><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>
<!-- Positions table -->
<div class="card mb-6 overflow-hidden">
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-700">
<h2 class="font-semibold text-white">Open Positions</h2>
<div id="live-indicator" 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
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="text-slate-400 border-b border-slate-700">
<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 P&L</th>
<th class="text-right px-4 py-2">Realised</th>
<th class="text-right px-4 py-2">Total P&L</th>
<th class="text-center px-4 py-2">Type</th>
</tr>
</thead>
<tbody id="positions-tbody">
<tr><td colspan="8" class="text-center py-8 text-slate-500">Loading...</td></tr>
</tbody>
</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">
<thead>
<tr class="text-slate-400 border-b border-slate-700">
<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>
</thead>
<tbody id="alerts-tbody">
<tr><td colspan="6" class="text-center py-8 text-slate-500">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<script>
// ── helpers ──────────────────────────────────────────────────────────────
const fmt = (n, decimals=0) => {
if (n === null || n === undefined) return '—';
const abs = Math.abs(n);
const s = abs.toLocaleString('en-IN', {maximumFractionDigits: decimals});
return (n >= 0 ? '+' : '-') + '₹' + s;
};
const pnlClass = n => n > 0 ? 'pnl-up' : n < 0 ? 'pnl-down' : 'pnl-flat';
const toIST = utcStr => {
const d = new Date(utcStr + (utcStr.includes('Z') ? '' : 'Z'));
return d.toLocaleString('en-IN', {timeZone:'Asia/Kolkata', hour12:false,
month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'});
};
const instrumentBadge = t => {
if (!t) return '';
if (t.includes('OPT')) return `<span class="badge badge-opt">${t}</span>`;
if (t.includes('FUT')) return `<span class="badge badge-fut">${t}</span>`;
return `<span class="badge badge-eq">${t}</span>`;
};
// ── data fetchers ─────────────────────────────────────────────────────────
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');
document.getElementById('open-count').textContent = open.length;
const totalPnl = open.reduce((s, p) => s + p.total_pnl, 0);
const el = document.getElementById('total-pnl');
el.textContent = fmt(totalPnl);
el.className = 'text-2xl font-bold ' + pnlClass(totalPnl);
if (open.length === 0) {
tbody.innerHTML = `<tr><td colspan="8" class="text-center py-8 text-slate-500">No open positions</td></tr>`;
return;
}
tbody.innerHTML = open.map(p => `
<tr class="border-b border-slate-800 hover:bg-slate-800/50">
<td class="px-4 py-2.5 font-medium text-white">${p.tradingsymbol}<br>
<span class="text-slate-500 text-xs">${p.exchange} · ${p.producttype}</span></td>
<td class="px-4 py-2.5 text-right text-white">${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>
<td class="px-4 py-2.5 text-center">${instrumentBadge(p.instrumenttype)}</td>
</tr>
`).join('');
}
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);
const todayCount = data.filter(a => a.alerted_at.startsWith(today)).length;
document.getElementById('alert-count').textContent = todayCount;
if (data.length === 0) {
tbody.innerHTML = `<tr><td colspan="6" class="text-center py-8 text-slate-500">No alerts yet</td></tr>`;
return;
}
tbody.innerHTML = data.map(a => {
const dir = a.direction === 'up' ? '🟢' : '🔴';
return `
<tr class="border-b border-slate-800 hover:bg-slate-800/50">
<td class="px-4 py-2.5 font-medium text-white">${dir} ${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('');
}
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'}) + ' IST';
}
async function refresh() {
await Promise.all([loadPositions(), loadAlerts(), loadHealth()]);
}
// Initial load + auto-refresh every 60s
refresh();
setInterval(refresh, 60_000);
</script>
</body>
</html>