200 lines
9.1 KiB
HTML
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>
|