feat: P&L snapshots table, /api/pnl-history, intraday chart + totals row in UI

This commit is contained in:
Manohar 2026-05-08 17:10:17 +00:00
parent f809fbdaa1
commit 247f750de4
3 changed files with 52 additions and 2 deletions

View file

@ -86,6 +86,30 @@ export function createServer(): express.Application {
res.json({ ok: true, message: 'Refresh complete' }); res.json({ ok: true, message: 'Refresh complete' });
}); });
// ── GET /api/pnl-history ──────────────────────────────────────────────────
// Returns P&L snapshots for charting. ?hours=N (default 8, max 72)
app.get('/api/pnl-history', (_req, res) => {
const hours = Math.min(parseInt(_req.query.hours as string) || 8, 72);
const rows = db.prepare(`
SELECT total_unrealised, total_realised, total_pnl, open_positions, recorded_at
FROM pnl_snapshots
WHERE recorded_at >= datetime('now', '-' || ? || ' hours')
ORDER BY recorded_at ASC
`).all(hours);
const latest = rows[rows.length - 1] as any;
res.json({
ok: true,
summary: latest ? {
totalUnrealised: latest.total_unrealised,
totalRealised: latest.total_realised,
totalPnl: latest.total_pnl,
openPositions: latest.open_positions,
asOf: latest.recorded_at,
} : null,
history: rows,
});
});
app.get('/api/health', (_req, res) => { app.get('/api/health', (_req, res) => {
const lastError = db.prepare( const lastError = db.prepare(
`SELECT error, occurred_at FROM poll_errors ORDER BY occurred_at DESC LIMIT 1` `SELECT error, occurred_at FROM poll_errors ORDER BY occurred_at DESC LIMIT 1`

View file

@ -79,6 +79,17 @@ export function initDb(): void {
CREATE INDEX IF NOT EXISTS idx_alerts_position ON alerts(position_key); CREATE INDEX IF NOT EXISTS idx_alerts_position ON alerts(position_key);
CREATE INDEX IF NOT EXISTS idx_alerts_alerted_at ON alerts(alerted_at DESC); CREATE INDEX IF NOT EXISTS idx_alerts_alerted_at ON alerts(alerted_at DESC);
CREATE INDEX IF NOT EXISTS idx_positions_closed ON positions(is_closed); CREATE INDEX IF NOT EXISTS idx_positions_closed ON positions(is_closed);
-- Portfolio-level P&L snapshots (recorded every poll tick)
CREATE TABLE IF NOT EXISTS pnl_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
total_unrealised REAL NOT NULL,
total_realised REAL NOT NULL,
total_pnl REAL NOT NULL, -- unrealised + realised
open_positions INTEGER NOT NULL,
recorded_at TEXT NOT NULL -- UTC datetime
);
CREATE INDEX IF NOT EXISTS idx_snapshots_recorded ON pnl_snapshots(recorded_at DESC);
`); `);
console.log(`[db] Initialised at ${DB_PATH}`); console.log(`[db] Initialised at ${DB_PATH}`);

View file

@ -76,7 +76,7 @@ export async function pollTick(): Promise<void> {
} }
// Evaluate band alerts for each open position // Evaluate band alerts for each open position
await evaluateAlerts(positions, today); await evaluateAlerts(positions, today); recordSnapshot(positions);
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : String(err); const msg = err instanceof Error ? err.message : String(err);
@ -198,8 +198,23 @@ export async function forcePoll(): Promise<void> {
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(",")})`).run(...activeKeys);
} }
console.log(`[poll] Force fetch: ${positions.length} positions`); console.log(`[poll] Force fetch: ${positions.length} positions`); recordSnapshot(positions);
} catch (err) { } catch (err) {
console.error(`[poll] Force fetch error: ${err instanceof Error ? err.message : err}`); console.error(`[poll] Force fetch error: ${err instanceof Error ? err.message : err}`);
} }
} }
/**
* Record a portfolio-level P&L snapshot after every fetch.
* Called by both pollTick (market hours) and forcePoll (manual/startup).
*/
function recordSnapshot(positions: import(../angel/types.js).Position[]): void {
const totalUnrealised = positions.reduce((s, p) => s + p.unrealisedPnl, 0);
const totalRealised = positions.reduce((s, p) => s + p.realisedPnl, 0);
const totalPnl = totalUnrealised + totalRealised;
db.prepare(`
INSERT INTO pnl_snapshots (total_unrealised, total_realised, total_pnl, open_positions, recorded_at)
VALUES (?, ?, ?, ?, datetime(now))
`).run(totalUnrealised, totalRealised, totalPnl, positions.length);
}