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' });
|
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`
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue