324 lines
15 KiB
HTML
324 lines
15 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>
|
|
<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; }
|
|
</style>
|
|
</head>
|
|
<body class="min-h-screen p-4">
|
|
|
|
<!-- 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>
|
|
|
|
<!-- 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-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>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<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>
|
|
</thead>
|
|
<tbody id="positions-tbody">
|
|
<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>
|
|
|
|
<!-- 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 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>
|
|
</thead>
|
|
<tbody id="alerts-tbody">
|
|
<tr><td colspan="6" class="text-center py-8 text-slate-500">No alerts yet</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<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'});
|
|
|
|
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');
|
|
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);
|
|
|
|
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);
|
|
}
|
|
|
|
// ── 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;
|
|
|
|
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();
|
|
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';
|
|
}
|
|
|
|
// ── main ──────────────────────────────────────────────────────────────────
|
|
async function refresh() {
|
|
await Promise.all([loadPositions(), loadAlerts(), loadHealth(), loadChart(currentHours)]);
|
|
}
|
|
|
|
refresh();
|
|
setInterval(refresh, 60_000);
|
|
</script>
|
|
</body>
|
|
</html>
|