From 91fd17b158bf2094da26d80931144e409b446c9e Mon Sep 17 00:00:00 2001 From: Manohar Date: Tue, 12 May 2026 04:57:18 +0000 Subject: [PATCH] fix: correct booked PnL (netqty=0 only), closed positions card, no false is_closed on open qty --- public/index.html | 116 +++++++++++++++++++++++++++++++++++++++++++- src/api/server.ts | 36 ++++++++++++-- src/tracker/poll.ts | 6 +-- 3 files changed, 150 insertions(+), 8 deletions(-) diff --git a/public/index.html b/public/index.html index 81a3f5a..a4a7ee1 100644 --- a/public/index.html +++ b/public/index.html @@ -191,6 +191,43 @@ + +
+
+ Closed / Booked Positions +
+ + netqty = 0 today +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + +
SymbolTypeStrikeExpiryExit LTPRealised P&LStatus
Loading…
+ +
+
Strategy Payoff at Expiry @@ -364,7 +401,7 @@ async function saveOverride(key){ const mute=document.getElementById('om-'+key)?.value; await fetch('/api/config/'+encodeURIComponent(key),{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({alert_threshold_pct:pct?parseFloat(pct):null,muted_until:mute?new Date(mute).toISOString():null})}); } -async function refresh(){await Promise.all([loadPositions(),loadAlerts(),loadHealth(),loadChart(curH),loadMarket(),loadPayoff()]);} +async function refresh(){await Promise.all([loadPositions(),loadAlerts(),loadHealth(),loadChart(curH),loadMarket(),loadPayoff(),loadClosedPositions()]);} @@ -593,6 +630,83 @@ async function loadPayoff() { } /* ── End Payoff ───────────────────────────────────────────────────────── */ + +async function loadClosedPositions() { + var res = await fetch('/api/closed-positions').then(function(r){return r.json();}).catch(function(){return {ok:false,data:[]};}); + var tbody = document.getElementById('closed-tbody'); + var tfoot = document.getElementById('closed-tfoot'); + var totalEl = document.getElementById('closed-total'); + var staleEl = document.getElementById('closed-stale'); + if (!tbody) return; + + var data = res.data || []; + var totalBooked = res.totalBooked || 0; + + // Update collapsed header badge + if (totalEl) { + if (data.length > 0) { + totalEl.textContent = fmt(totalBooked); + totalEl.style.display = 'inline'; + totalEl.style.color = totalBooked >= 0 ? 'var(--green)' : 'var(--red)'; + } else { + totalEl.style.display = 'none'; + } + } + + // Update stat card + var bEl = document.getElementById('s-booked'); + if (bEl && data.length > 0) { + bEl.textContent = fmt(totalBooked); + bEl.className = 'sval ' + cls(totalBooked); + } + + if (!data.length) { + tbody.innerHTML = 'No closed positions today'; + if (tfoot) tfoot.style.display = 'none'; + return; + } + + // Parse option symbol helper + function parseForDisplay(sym) { + var m = sym.match(/^([A-Z]+)(\d{2})(\d{1})(\d{2})(\d+)(CE|PE)$/); + if (m) return { ul: m[1], expiry: m[3].padStart(2,'0')+'/'+m[4]+'/20'+m[2], strike: m[5], type: m[6] }; + return { ul: sym, expiry: '—', strike: '—', type: '—' }; + } + + tbody.innerHTML = data.map(function(p) { + var info = parseForDisplay(p.tradingsymbol); + var typeBadge = info.type === 'CE' + ? 'CE' + : 'PE'; + + return '' + + '
' + p.tradingsymbol + '
' + + '
' + p.exchange + ' \u00b7 ' + p.producttype + '
' + + '' + typeBadge + '' + + '₹' + parseInt(info.strike).toLocaleString('en-IN') + '' + + '' + info.expiry + '' + + '₹' + parseFloat(p.ltp).toFixed(2) + '' + + '' + fmt(p.realised_pnl) + '' + + '✓ Booked' + + ''; + }).join(''); + + if (tfoot) { + tfoot.style.display = ''; + var footEl = document.getElementById('closed-foot'); + if (footEl) { footEl.textContent = fmt(totalBooked); footEl.className = 'mono ' + cls(totalBooked); } + } + + // Show stale positions warning if any + var stale = res.stalePositions || []; + if (stale.length > 0 && staleEl) { + staleEl.style.display = 'block'; + staleEl.textContent = '\u26a0\ufe0f ' + stale.length + ' position(s) temporarily disappeared from Angel API but still have open qty — they will reappear on next successful poll.'; + } else if (staleEl) { + staleEl.style.display = 'none'; + } +} + loadConfig();refresh(); setInterval(refresh,60000); setInterval(loadMarket,15000); diff --git a/src/api/server.ts b/src/api/server.ts index e6c632e..d7d2250 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -105,12 +105,12 @@ export function createServer(): express.Application { FROM positions WHERE is_closed = 0 AND netqty != 0 `).get() as any; - // Realised from fully-closed positions (netqty=0, is_closed=1) — booked today + // Realised from FULLY closed positions only (netqty=0) — booked today + // Do NOT include is_closed=1 with netqty!=0 (those are incorrectly marked / partially open) const closedRealised = db.prepare(` SELECT COALESCE(SUM(realised_pnl),0) as r FROM positions - WHERE (is_closed = 1 OR netqty = 0) AND realised_pnl != 0 - AND updated_at >= date('now') + WHERE netqty = 0 AND realised_pnl != 0 `).get() as any; const totalUnrealised = liveOpen.u; @@ -140,7 +140,7 @@ export function createServer(): express.Application { const posCount = (db.prepare(`SELECT COUNT(*) as n FROM positions WHERE is_closed = 0 AND netqty != 0`).get() as { n: number }).n; const bookedRealised = (db.prepare(` SELECT COALESCE(SUM(realised_pnl),0) as r FROM positions - WHERE (is_closed=1 OR netqty=0) AND realised_pnl!=0 AND updated_at>=date('now') + WHERE netqty=0 AND realised_pnl!=0 `).get() as { r: number }).r; res.json({ @@ -154,6 +154,34 @@ export function createServer(): express.Application { }); + + // ── GET /api/closed-positions ───────────────────────────────────────────── + // Positions fully closed today (netqty=0, have realised PnL) + // Also shows incorrectly-flagged positions (is_closed=1 but netqty!=0) for transparency + app.get('/api/closed-positions', (_req, res) => { + // Truly closed (netqty=0 with realised PnL) + const closed = db.prepare(` + SELECT tradingsymbol, exchange, producttype, instrumenttype, + netqty, avg_price, ltp, realised_pnl, updated_at + FROM positions + WHERE netqty = 0 AND realised_pnl != 0 + ORDER BY ABS(realised_pnl) DESC + `).all(); + + // Potentially stale (marked closed by system but still has qty — should not happen after fix) + const stale = db.prepare(` + SELECT tradingsymbol, exchange, netqty, unrealised_pnl, realised_pnl, total_pnl, updated_at + FROM positions + WHERE is_closed = 1 AND netqty != 0 + AND tradingsymbol NOT LIKE '%-EQ' + ORDER BY ABS(total_pnl) DESC + `).all(); + + const totalBooked = closed.reduce((s: number, r: any) => s + r.realised_pnl, 0); + + res.json({ ok: true, data: closed, stalePositions: stale, totalBooked }); + }); + // ── GET /api/market ────────────────────────────────────────────────────── app.get('/api/market', async (_req, res) => { try { diff --git a/src/tracker/poll.ts b/src/tracker/poll.ts index d2bf6c8..58bd82c 100644 --- a/src/tracker/poll.ts +++ b/src/tracker/poll.ts @@ -70,9 +70,9 @@ export async function pollTick(): Promise { if (activeKeys.length > 0) { const placeholders = activeKeys.map(() => '?').join(','); db.prepare(`UPDATE positions SET is_closed = 1, updated_at = datetime('now') - WHERE key NOT IN (${placeholders})`).run(...activeKeys); + WHERE key NOT IN (${placeholders}) AND netqty = 0`).run(...activeKeys); } else { - db.prepare(`UPDATE positions SET is_closed = 1, updated_at = datetime('now')`).run(); + db.prepare(`UPDATE positions SET is_closed = 1, updated_at = datetime('now') WHERE netqty = 0`).run(); } // Evaluate band alerts for each open position @@ -198,7 +198,7 @@ export async function forcePoll(): Promise { } const activeKeys = positions.map(p => p.key); if (activeKeys.length > 0) { - db.prepare(`UPDATE positions SET is_closed = 1 WHERE key NOT IN (${activeKeys.map(() => "?").join(",")})`).run(...activeKeys); + db.prepare(`UPDATE positions SET is_closed = 1 WHERE key NOT IN (${activeKeys.map(() => "?").join(",")}) AND netqty = 0`).run(...activeKeys); } console.log(`[poll] Force fetch: ${positions.length} positions`); recordSnapshot(positions); } catch (err) {