feat: P&L snapshots table, /api/pnl-history, intraday chart + totals row in UI
This commit is contained in:
parent
f809fbdaa1
commit
247f750de4
3 changed files with 52 additions and 2 deletions
|
|
@ -86,6 +86,30 @@ export function createServer(): express.Application {
|
|||
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) => {
|
||||
const lastError = db.prepare(
|
||||
`SELECT error, occurred_at FROM poll_errors ORDER BY occurred_at DESC LIMIT 1`
|
||||
|
|
|
|||
|
|
@ -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_alerted_at ON alerts(alerted_at DESC);
|
||||
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}`);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export async function pollTick(): Promise<void> {
|
|||
}
|
||||
|
||||
// Evaluate band alerts for each open position
|
||||
await evaluateAlerts(positions, today);
|
||||
await evaluateAlerts(positions, today); recordSnapshot(positions);
|
||||
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
|
|
@ -198,8 +198,23 @@ export async function forcePoll(): Promise<void> {
|
|||
if (activeKeys.length > 0) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue