feat: new UI — P&L summary cards, intraday chart, totals footer

This commit is contained in:
Manohar 2026-05-08 17:10:26 +00:00
parent 247f750de4
commit e97a2ca643

View file

@ -5,17 +5,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Position Tracker</title>
<script src="https://cdn.tailwindcss.com"></script>
<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: 'Inter', system-ui, sans-serif; }
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; }
.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; }
.badge-eq { background: #1e3a5f; color: #93c5fd; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.5} }
.live-dot { animation: pulse 2s infinite; }
</style>
@ -23,7 +21,7 @@
<body class="min-h-screen p-4">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<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>
@ -34,39 +32,91 @@
</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>
<!-- 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>
</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-6 overflow-hidden">
<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">Open Positions</h2>
<div id="live-indicator" class="flex items-center gap-2 text-xs text-green-400">
<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
</div>
</span>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="text-slate-400 border-b border-slate-700">
<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 P&L</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 P&L</th>
<th class="text-center px-4 py-2">Type</th>
<th class="text-right px-4 py-2">Total</th>
</tr>
</thead>
<tbody id="positions-tbody">
<tr><td colspan="8" class="text-center py-8 text-slate-500">Loading...</td></tr>
<tr><td colspan="7" class="text-center py-8 text-slate-500">Loading...</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">
<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>
</tr>
</tfoot>
</table>
</div>
</div>
@ -79,7 +129,7 @@
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="text-slate-400 border-b border-slate-700">
<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>
@ -89,7 +139,7 @@
</tr>
</thead>
<tbody id="alerts-tbody">
<tr><td colspan="6" class="text-center py-8 text-slate-500">Loading...</td></tr>
<tr><td colspan="6" class="text-center py-8 text-slate-500">No alerts yet</td></tr>
</tbody>
</table>
</div>
@ -97,87 +147,160 @@
<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});
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 = 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>`;
};
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'});
// ── data fetchers ─────────────────────────────────────────────────────────
let chartInstance = null;
let currentHours = 8;
// ── 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';
});
const r = await fetch(`/api/pnl-history?hours=${hours}`);
const {summary, history} = await r.json();
// 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;
}
const canvas = document.getElementById('pnl-chart');
const empty = document.getElementById('chart-empty');
if (!history || history.length < 2) {
canvas.style.display = 'none';
empty.classList.remove('hidden');
return;
}
canvas.style.display = 'block';
empty.classList.add('hidden');
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: '#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')}`
}
}
},
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] } }
}
}
});
}
// ── 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');
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);
const tfoot = document.getElementById('positions-tfoot');
if (open.length === 0) {
tbody.innerHTML = `<tr><td colspan="8" class="text-center py-8 text-slate-500">No open positions</td></tr>`;
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);
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>
<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>
<td class="px-4 py-2.5 text-center">${instrumentBadge(p.instrumenttype)}</td>
</tr>
`).join('');
</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);
}
// ── 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);
const todayCount = data.filter(a => a.alerted_at.startsWith(today)).length;
document.getElementById('alert-count').textContent = todayCount;
document.getElementById('alert-count').textContent =
data.filter(a => a.alerted_at.startsWith(today)).length;
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('');
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('');
}
// ── health ────────────────────────────────────────────────────────────────
async function loadHealth() {
const r = await fetch('/api/health');
const d = await r.json();
@ -185,14 +308,15 @@
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';
document.getElementById('last-updated').textContent =
'Updated: ' + new Date().toLocaleTimeString('en-IN',{timeZone:'Asia/Kolkata',hour12:false}) + ' IST';
}
// ── main ──────────────────────────────────────────────────────────────────
async function refresh() {
await Promise.all([loadPositions(), loadAlerts(), loadHealth()]);
await Promise.all([loadPositions(), loadAlerts(), loadHealth(), loadChart(currentHours)]);
}
// Initial load + auto-refresh every 60s
refresh();
setInterval(refresh, 60_000);
</script>