fix: P&L today-only date filter (IST), expiry tabs in positions table
This commit is contained in:
parent
805f25eb75
commit
acf8799880
2 changed files with 63 additions and 15 deletions
|
|
@ -132,7 +132,12 @@
|
||||||
.pcard.pt .pval { font-size: 1.7rem; }
|
.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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
|
|
@ -183,7 +188,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card collapsible" id="sec-pos" style="margin-bottom:16px">
|
<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">▼</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">▼</span></div>
|
||||||
<div class="collapse-body"><table>
|
<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&L</th></tr></thead>
|
<thead><tr><th>Symbol</th><th>Qty</th><th>LTP</th><th>Avg Cost</th><th>Unrealised</th><th>Realised</th><th>Total P&L</th></tr></thead>
|
||||||
<tbody id="pos-body"><tr><td colspan="7" style="text-align:center;padding:36px;color:var(--text3)">Loading…</td></tr></tbody>
|
<tbody id="pos-body"><tr><td colspan="7" style="text-align:center;padding:36px;color:var(--text3)">Loading…</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>';
|
(q.stale?'<div class="mstale">prev close'+(q.cachedAt?' \u00b7 '+q.cachedAt.slice(11,16)+' UTC':'')+'</div>':'')+'</div>';
|
||||||
}).join('');
|
}).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(){
|
async function loadPositions(){
|
||||||
const {data}=await fetch('/api/positions').then(r=>r.json());
|
const {data}=await fetch('/api/positions').then(r=>r.json());
|
||||||
const open=data.filter(p=>p.is_closed===0 && +p.netqty!==0);
|
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');
|
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;}
|
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=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);
|
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
|
// Update collapsed header badge
|
||||||
const phEl=document.getElementById('pos-hpnl');
|
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)');}
|
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('');
|
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='';
|
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);
|
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(){
|
async function loadAlerts(){
|
||||||
const {data}=await fetch('/api/alerts?limit=20').then(r=>r.json());
|
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();
|
loadConfig();refresh();
|
||||||
setInterval(refresh,60000);
|
setInterval(refresh,60000);
|
||||||
setInterval(loadMarket,15000);
|
setInterval(loadMarket,15000);
|
||||||
|
|
|
||||||
|
|
@ -105,12 +105,15 @@ export function createServer(): express.Application {
|
||||||
FROM positions WHERE is_closed = 0 AND netqty != 0
|
FROM positions WHERE is_closed = 0 AND netqty != 0
|
||||||
`).get() as any;
|
`).get() as any;
|
||||||
|
|
||||||
// Realised from FULLY closed positions only (netqty=0) — booked today
|
// Realised P&L from positions updated TODAY (IST) only.
|
||||||
// Do NOT include is_closed=1 with netqty!=0 (those are incorrectly marked / partially open)
|
// '+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(`
|
const closedRealised = db.prepare(`
|
||||||
SELECT COALESCE(SUM(realised_pnl),0) as r
|
SELECT COALESCE(SUM(realised_pnl),0) as r
|
||||||
FROM positions
|
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;
|
`).get() as any;
|
||||||
|
|
||||||
const totalUnrealised = liveOpen.u;
|
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 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(`
|
const bookedRealised = (db.prepare(`
|
||||||
SELECT COALESCE(SUM(realised_pnl),0) as r FROM positions
|
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;
|
`).get() as { r: number }).r;
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|
@ -156,19 +161,22 @@ export function createServer(): express.Application {
|
||||||
|
|
||||||
|
|
||||||
// ── GET /api/closed-positions ─────────────────────────────────────────────
|
// ── GET /api/closed-positions ─────────────────────────────────────────────
|
||||||
// Positions fully closed today (netqty=0, have realised PnL)
|
// Positions closed TODAY (IST). Groups by expiry so UI can show per-expiry view.
|
||||||
// Also shows incorrectly-flagged positions (is_closed=1 but netqty!=0) for transparency
|
|
||||||
app.get('/api/closed-positions', (_req, res) => {
|
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(`
|
const closed = db.prepare(`
|
||||||
SELECT tradingsymbol, exchange, producttype, instrumenttype,
|
SELECT tradingsymbol, exchange, producttype, instrumenttype,
|
||||||
netqty, avg_price, ltp, realised_pnl, updated_at
|
netqty, avg_price, ltp, realised_pnl, updated_at
|
||||||
FROM positions
|
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
|
ORDER BY ABS(realised_pnl) DESC
|
||||||
`).all();
|
`).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(`
|
const stale = db.prepare(`
|
||||||
SELECT tradingsymbol, exchange, netqty, unrealised_pnl, realised_pnl, total_pnl, updated_at
|
SELECT tradingsymbol, exchange, netqty, unrealised_pnl, realised_pnl, total_pnl, updated_at
|
||||||
FROM positions
|
FROM positions
|
||||||
|
|
@ -177,7 +185,7 @@ export function createServer(): express.Application {
|
||||||
ORDER BY ABS(total_pnl) DESC
|
ORDER BY ABS(total_pnl) DESC
|
||||||
`).all();
|
`).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 });
|
res.json({ ok: true, data: closed, stalePositions: stale, totalBooked });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue