fix: correct booked PnL (netqty=0 only), closed positions card, no false is_closed on open qty
This commit is contained in:
parent
ade11564a8
commit
91fd17b158
3 changed files with 150 additions and 8 deletions
|
|
@ -191,6 +191,43 @@
|
||||||
</table></div>
|
</table></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="card collapsible" id="sec-closed" style="margin-bottom:16px">
|
||||||
|
<div class="card-head" onclick="toggleCard('sec-closed')">
|
||||||
|
<span class="card-title">Closed / Booked Positions</span>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
|
<span id="closed-total" style="font-family:'Geist Mono',monospace;font-size:.75rem;font-weight:600;display:none"></span>
|
||||||
|
<span style="font-size:.65rem;color:var(--text3)">netqty = 0 today</span>
|
||||||
|
</div>
|
||||||
|
<span class="collapse-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="collapse-body">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Symbol</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Strike</th>
|
||||||
|
<th>Expiry</th>
|
||||||
|
<th>Exit LTP</th>
|
||||||
|
<th>Realised P&L</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="closed-tbody">
|
||||||
|
<tr><td colspan="7" style="text-align:center;padding:28px;color:var(--text3)">Loading…</td></tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot id="closed-tfoot" style="display:none">
|
||||||
|
<tr>
|
||||||
|
<td colspan="5">Total Booked Today</td>
|
||||||
|
<td id="closed-foot" class="mono">—</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
<div id="closed-stale" style="display:none;padding:10px 18px;font-size:.7rem;color:var(--amber);border-top:1px solid var(--border)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card collapsible" id="sec-payoff" style="margin-bottom:16px">
|
<div class="card collapsible" id="sec-payoff" style="margin-bottom:16px">
|
||||||
<div class="card-head" onclick="toggleCard('sec-payoff')">
|
<div class="card-head" onclick="toggleCard('sec-payoff')">
|
||||||
<span class="card-title">Strategy Payoff at Expiry</span>
|
<span class="card-title">Strategy Payoff at Expiry</span>
|
||||||
|
|
@ -364,7 +401,7 @@ async function saveOverride(key){
|
||||||
const mute=document.getElementById('om-'+key)?.value;
|
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})});
|
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 ───────────────────────────────────────────────────────── */
|
/* ── 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 = '<tr><td colspan="7" style="text-align:center;padding:28px;color:var(--text3)">No closed positions today</td></tr>';
|
||||||
|
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'
|
||||||
|
? '<span style="background:rgba(52,152,219,.15);color:#3498DB;padding:1px 7px;border-radius:9999px;font-size:.68rem;font-weight:600">CE</span>'
|
||||||
|
: '<span style="background:rgba(155,89,182,.15);color:#9B59B6;padding:1px 7px;border-radius:9999px;font-size:.68rem;font-weight:600">PE</span>';
|
||||||
|
|
||||||
|
return '<tr>' +
|
||||||
|
'<td><div class="sym">' + p.tradingsymbol + '</div>' +
|
||||||
|
'<div class="sym-meta">' + p.exchange + ' \u00b7 ' + p.producttype + '</div></td>' +
|
||||||
|
'<td style="text-align:center">' + typeBadge + '</td>' +
|
||||||
|
'<td class="mono" style="text-align:center">₹' + parseInt(info.strike).toLocaleString('en-IN') + '</td>' +
|
||||||
|
'<td style="text-align:center;color:var(--text2);font-size:.75rem">' + info.expiry + '</td>' +
|
||||||
|
'<td class="mono">₹' + parseFloat(p.ltp).toFixed(2) + '</td>' +
|
||||||
|
'<td class="mono ' + cls(p.realised_pnl) + '" style="font-weight:600">' + fmt(p.realised_pnl) + '</td>' +
|
||||||
|
'<td><span style="background:rgba(46,204,113,.12);color:var(--green);padding:2px 8px;border-radius:9999px;font-size:.65rem;font-weight:600">✓ Booked</span></td>' +
|
||||||
|
'</tr>';
|
||||||
|
}).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();
|
loadConfig();refresh();
|
||||||
setInterval(refresh,60000);
|
setInterval(refresh,60000);
|
||||||
setInterval(loadMarket,15000);
|
setInterval(loadMarket,15000);
|
||||||
|
|
|
||||||
|
|
@ -105,12 +105,12 @@ 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 (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(`
|
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 (is_closed = 1 OR netqty = 0) AND realised_pnl != 0
|
WHERE netqty = 0 AND realised_pnl != 0
|
||||||
AND updated_at >= date('now')
|
|
||||||
`).get() as any;
|
`).get() as any;
|
||||||
|
|
||||||
const totalUnrealised = liveOpen.u;
|
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 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 (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;
|
`).get() as { r: number }).r;
|
||||||
|
|
||||||
res.json({
|
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 ──────────────────────────────────────────────────────
|
// ── GET /api/market ──────────────────────────────────────────────────────
|
||||||
app.get('/api/market', async (_req, res) => {
|
app.get('/api/market', async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,9 @@ export async function pollTick(): Promise<void> {
|
||||||
if (activeKeys.length > 0) {
|
if (activeKeys.length > 0) {
|
||||||
const placeholders = activeKeys.map(() => '?').join(',');
|
const placeholders = activeKeys.map(() => '?').join(',');
|
||||||
db.prepare(`UPDATE positions SET is_closed = 1, updated_at = datetime('now')
|
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 {
|
} 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
|
// Evaluate band alerts for each open position
|
||||||
|
|
@ -198,7 +198,7 @@ export async function forcePoll(): Promise<void> {
|
||||||
}
|
}
|
||||||
const activeKeys = positions.map(p => p.key);
|
const activeKeys = positions.map(p => p.key);
|
||||||
if (activeKeys.length > 0) {
|
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);
|
console.log(`[poll] Force fetch: ${positions.length} positions`); recordSnapshot(positions);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue