fix: P&L today-only date filter (IST), expiry tabs in positions table

This commit is contained in:
Manohar 2026-05-27 13:17:44 +05:30
parent 805f25eb75
commit acf8799880
2 changed files with 63 additions and 15 deletions

View file

@ -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)}</style>
.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}
</style>
</head>
<body>
<div class="wrap">
@ -183,7 +188,7 @@
</div>
</div>
<div class="card collapsible" id="sec-pos" style="margin-bottom:16px">
<div class="card-head" onclick="toggleCard('sec-pos')"><span class="card-title">Option Positions</span><div style="display:flex;align-items:center;gap:10px"><div class="live-badge"><span class="ldot"></span>Live</div><span id="pos-hpnl" style="font-family:'Geist Mono',monospace;font-size:.75rem;font-weight:600;display:none"></span></div><span class="collapse-arrow">&#9660;</span></div>
<div class="card-head" onclick="toggleCard('sec-pos')"><span class="card-title">Option Positions</span><div style="display:flex;align-items:center;gap:10px"><div class="live-badge"><span class="ldot"></span>Live</div><div id="expiry-tabs" style="display:flex;gap:4px;flex-wrap:wrap"></div><span id="pos-hpnl" style="font-family:'Geist Mono',monospace;font-size:.75rem;font-weight:600;display:none"></span></div><span class="collapse-arrow">&#9660;</span></div>
<div class="collapse-body"><table>
<thead><tr><th>Symbol</th><th>Qty</th><th>LTP</th><th>Avg Cost</th><th>Unrealised</th><th>Realised</th><th>Total P&amp;L</th></tr></thead>
<tbody id="pos-body"><tr><td colspan="7" style="text-align:center;padding:36px;color:var(--text3)">Loading&#8230;</td></tr></tbody>
@ -339,19 +344,48 @@ async function loadMarket(){
(q.stale?'<div class="mstale">prev close'+(q.cachedAt?' \u00b7 '+q.cachedAt.slice(11,16)+' UTC':'')+'</div>':'')+'</div>';
}).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=>
'<button class="exp-tab'+(e===_activeExpiry?' active':'')+'" onclick="setExpiry(\''+e+'\')">'+(e==='ALL'?'All Expiries':e)+'</button>'
).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='<tr><td colspan="7" style="text-align:center;padding:36px;color:var(--text3)">No open positions</td></tr>';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='<tr><td colspan="7" style="text-align:center;padding:36px;color:var(--text3)">No open positions'+(_activeExpiry!=='ALL'?' for '+_activeExpiry:'')+'</td></tr>';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'<tr><td><div class="sym">'+p.tradingsymbol+'</div><div class="sym-meta">'+p.exchange+' \u00b7 '+p.producttype+'</div>'+(tte?'<div class="tte">'+tte+'</div>':'')+'</td><td class="'+(p.netqty<0?'qs':'ql')+'">'+(p.netqty>0?'+':'')+p.netqty+'</td><td class="mono">\u20b9'+(+p.ltp).toFixed(2)+'</td><td class="mono" style="color:var(--text2)">\u20b9'+(+p.avg_price).toFixed(2)+'</td><td class="mono '+cls(p.unrealised_pnl)+'">'+fmt(p.unrealised_pnl)+'</td><td class="mono '+cls(p.realised_pnl)+'">'+fmt(p.realised_pnl)+'</td><td class="mono '+cls(p.total_pnl)+'" style="font-weight:600">'+fmt(p.total_pnl)+'</td></tr>';}).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=>'<tr><td><div class="sym" style="font-size:.75rem">'+p.tradingsymbol+'</div></td><td><input type="number" min="1" max="50" step="0.5" placeholder="Global default" id="op-'+p.key+'" style="width:130px;padding:4px 8px;border-radius:7px;border:1px solid var(--border2);background:var(--bg3);color:var(--text);font-size:.75rem;font-family:\'Geist Mono\',monospace;outline:none"/></td><td><input type="datetime-local" id="om-'+p.key+'" style="padding:4px 8px;border-radius:7px;border:1px solid var(--border2);background:var(--bg3);color:var(--text);font-size:.72rem;outline:none"/></td><td><button onclick="saveOverride(\''+p.key+'\')" style="padding:3px 10px;border-radius:7px;background:var(--bg3);border:1px solid var(--border2);color:var(--text2);font-size:.72rem;cursor:pointer">Save</button></td></tr>').join('');
document.getElementById('override-body').innerHTML=filtered.map(p=>'<tr><td><div class="sym" style="font-size:.75rem">'+p.tradingsymbol+'</div></td><td><input type="number" min="1" max="50" step="0.5" placeholder="Global default" id="op-'+p.key+'" style="width:130px;padding:4px 8px;border-radius:7px;border:1px solid var(--border2);background:var(--bg3);color:var(--text);font-size:.75rem;font-family:\'Geist Mono\',monospace;outline:none"/></td><td><input type="datetime-local" id="om-'+p.key+'" style="padding:4px 8px;border-radius:7px;border:1px solid var(--border2);background:var(--bg3);color:var(--text);font-size:.72rem;outline:none"/></td><td><button onclick="saveOverride(\''+p.key+'\')" style="padding:3px 10px;border-radius:7px;background:var(--bg3);border:1px solid var(--border2);color:var(--text2);font-size:.72rem;cursor:pointer">Save</button></td></tr>').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);

View file

@ -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 });
});