From acf87998803834dbb68337760e85e729193fb03f Mon Sep 17 00:00:00 2001 From: Manohar Date: Wed, 27 May 2026 13:17:44 +0530 Subject: [PATCH] fix: P&L today-only date filter (IST), expiry tabs in positions table --- public/index.html | 50 ++++++++++++++++++++++++++++++++++++++++++----- src/api/server.ts | 28 ++++++++++++++++---------- 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/public/index.html b/public/index.html index 6bfce3d..d8eb50e 100644 --- a/public/index.html +++ b/public/index.html @@ -132,7 +132,12 @@ .pcard.pt .pval { font-size: 1.7rem; } } - .pf-stat{padding:10px 14px;border-right:1px solid var(--border);text-align:center}.pf-stat:last-child{border-right:none}.pfs-l{font-size:.6rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--text3);margin-bottom:4px}.pfs-v{font-family:'DM Serif Display',serif;font-size:1.05rem;color:var(--text)} + .pf-stat{padding:10px 14px;border-right:1px solid var(--border);text-align:center}.pf-stat:last-child{border-right:none}.pfs-l{font-size:.6rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--text3);margin-bottom:4px}.pfs-v{font-family:'DM Serif Display',serif;font-size:1.05rem;color:var(--text)} + .exp-tab{padding:2px 9px;border-radius:7px;font-size:.65rem;font-weight:600;border:1px solid var(--border2);cursor:pointer;background:transparent;color:var(--text2);transition:all .12s;font-family:'Geist Mono',monospace;} + .exp-tab:hover{border-color:var(--coral);color:var(--text)} + .exp-tab.active{background:var(--coral);border-color:var(--coral);color:#fff} + +
@@ -183,7 +188,7 @@
-
Option Positions
Live
+
Option Positions
Live
@@ -339,19 +344,48 @@ async function loadMarket(){ (q.stale?'
prev close'+(q.cachedAt?' \u00b7 '+q.cachedAt.slice(11,16)+' UTC':'')+'
':'')+''; }).join(''); } +// Parse expiry label from symbol name +function expiryLabel(sym){ + // Format 1: UNDERLYING + YY + M(1) + DD + ... e.g. SENSEX2660476100CE -> Jun 04 + var m1=sym.match(/^[A-Z]+(\d{2})(\d{1})(\d{2})\d+(CE|PE)$/); + if(m1){ + var months=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + var mo=parseInt(m1[2])-1; + return (months[mo]||m1[2])+' '+m1[3]+' 20'+m1[1]; + } + // Format 2: UNDERLYING + YY + MON + DD + ... e.g. SENSEX26MAY76100CE -> May 28 + var m2=sym.match(/^[A-Z]+(\d{2})(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(\d{2})\d+(CE|PE)$/i); + if(m2){return m2[2].charAt(0)+m2[2].slice(1).toLowerCase()+' 20'+m2[1];} + return 'Other'; +} + +var _activeExpiry='ALL'; + async function loadPositions(){ const {data}=await fetch('/api/positions').then(r=>r.json()); const open=data.filter(p=>p.is_closed===0 && +p.netqty!==0); + + // Build expiry tabs + const expiries=['ALL',...new Set(open.map(p=>expiryLabel(p.tradingsymbol)))].filter(Boolean); + const tabsEl=document.getElementById('expiry-tabs'); + if(tabsEl&&expiries.length>2){ + tabsEl.innerHTML=expiries.map(e=> + '' + ).join(''); + } else if(tabsEl){ tabsEl.innerHTML=''; } + + // Filter by active expiry + const filtered=_activeExpiry==='ALL'?open:open.filter(p=>expiryLabel(p.tradingsymbol)===_activeExpiry); const tbody=document.getElementById('pos-body'),tfoot=document.getElementById('pos-foot'); - if(!open.length){tbody.innerHTML='';tfoot.style.display='none';return;} - const sU=open.reduce((s,p)=>s+p.unrealised_pnl,0),sR=open.reduce((s,p)=>s+p.realised_pnl,0),sT=open.reduce((s,p)=>s+p.total_pnl,0); + if(!filtered.length){tbody.innerHTML='';tfoot.style.display='none';return;} + const sU=filtered.reduce((s,p)=>s+p.unrealised_pnl,0),sR=filtered.reduce((s,p)=>s+p.realised_pnl,0),sT=filtered.reduce((s,p)=>s+p.total_pnl,0); // Update collapsed header badge const phEl=document.getElementById('pos-hpnl'); if(phEl){phEl.textContent=fmt(sT);phEl.className='';phEl.style.cssText='font-family:Geist Mono,monospace;font-size:.75rem;font-weight:600;display:inline;color:'+(sT>50?'var(--green)':sT<-50?'var(--red)':'var(--text2)');} tbody.innerHTML=open.map(p=>{const tte=parseTTE(p.tradingsymbol);return'';}).join(''); tfoot.style.display=''; const sf=(id,v)=>{const e=document.getElementById(id);e.textContent=fmt(v);e.className='mono '+cls(v)};sf('f-u',sU);sf('f-r',sR);sf('f-t',sT); - document.getElementById('override-body').innerHTML=open.map(p=>'').join(''); + document.getElementById('override-body').innerHTML=filtered.map(p=>'').join(''); } async function loadAlerts(){ const {data}=await fetch('/api/alerts?limit=20').then(r=>r.json()); @@ -709,6 +743,12 @@ async function loadClosedPositions() { } } + +function setExpiry(exp){ + _activeExpiry=exp; + // Re-render positions with new filter (re-use cached data via refresh) + loadPositions(); +} loadConfig();refresh(); setInterval(refresh,60000); setInterval(loadMarket,15000); diff --git a/src/api/server.ts b/src/api/server.ts index d7d2250..17285e8 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -105,12 +105,15 @@ export function createServer(): express.Application { FROM positions WHERE is_closed = 0 AND netqty != 0 `).get() as any; - // 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) + // Realised P&L from positions updated TODAY (IST) only. + // '+5h30m' converts UTC stored time -> IST date for filtering. + // This ensures we never include historical expiry P&L from previous days. const closedRealised = db.prepare(` SELECT COALESCE(SUM(realised_pnl),0) as r FROM positions - WHERE netqty = 0 AND realised_pnl != 0 + WHERE realised_pnl != 0 + AND date(updated_at, '+5 hours', '+30 minutes') + = date('now', '+5 hours', '+30 minutes') `).get() as any; const totalUnrealised = liveOpen.u; @@ -140,7 +143,9 @@ 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 netqty=0 AND realised_pnl!=0 + WHERE realised_pnl != 0 + AND date(updated_at, '+5 hours', '+30 minutes') + = date('now', '+5 hours', '+30 minutes') `).get() as { r: number }).r; res.json({ @@ -156,19 +161,22 @@ 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 + // Positions closed TODAY (IST). Groups by expiry so UI can show per-expiry view. app.get('/api/closed-positions', (_req, res) => { - // Truly closed (netqty=0 with realised PnL) + const todayIST = "date('now', '+5 hours', '+30 minutes')"; + + // Today's closed positions only — filtered by IST date 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 + WHERE realised_pnl != 0 + AND netqty = 0 + AND date(updated_at, '+5 hours', '+30 minutes') = date('now', '+5 hours', '+30 minutes') ORDER BY ABS(realised_pnl) DESC `).all(); - // Potentially stale (marked closed by system but still has qty — should not happen after fix) + // Stale positions (system incorrectly marked open positions as closed) const stale = db.prepare(` SELECT tradingsymbol, exchange, netqty, unrealised_pnl, realised_pnl, total_pnl, updated_at FROM positions @@ -177,7 +185,7 @@ export function createServer(): express.Application { ORDER BY ABS(total_pnl) DESC `).all(); - const totalBooked = closed.reduce((s: number, r: any) => s + r.realised_pnl, 0); + const totalBooked = (closed as any[]).reduce((s: number, r: any) => s + r.realised_pnl, 0); res.json({ ok: true, data: closed, stalePositions: stale, totalBooked }); });
SymbolQtyLTPAvg CostUnrealisedRealisedTotal P&L
Loading…
No open positions
No open positions'+(_activeExpiry!=='ALL'?' for '+_activeExpiry:'')+'
'+p.tradingsymbol+'
'+p.exchange+' \u00b7 '+p.producttype+'
'+(tte?'
'+tte+'
':'')+'
'+(p.netqty>0?'+':'')+p.netqty+'\u20b9'+(+p.ltp).toFixed(2)+'\u20b9'+(+p.avg_price).toFixed(2)+''+fmt(p.unrealised_pnl)+''+fmt(p.realised_pnl)+''+fmt(p.total_pnl)+'
'+p.tradingsymbol+'
'+p.tradingsymbol+'