position-tracker/src/api/server.ts

103 lines
4.1 KiB
TypeScript

import express from 'express';
import path from 'path';
import { db } from '../db/client.js';
import { isMarketOpen } from '../tracker/market-hours.js';
export function createServer(): express.Application {
const app = express();
app.use(express.json());
// Serve the static UI from /public
const publicDir = path.resolve(__dirname, '../../public');
app.use(express.static(publicDir));
// ── GET /api/positions ────────────────────────────────────────────────────
// Returns all currently tracked positions (open + recently closed)
app.get('/api/positions', (_req, res) => {
const rows = db.prepare(`
SELECT key, exchange, tradingsymbol, instrumenttype, producttype,
netqty, ltp, avg_price, unrealised_pnl, realised_pnl, total_pnl,
source, is_closed, updated_at
FROM positions
ORDER BY ABS(total_pnl) DESC
`).all();
res.json({ ok: true, data: rows });
});
// ── GET /api/alerts ───────────────────────────────────────────────────────
// Alert history, newest first, optional ?limit=N&symbol=X
app.get('/api/alerts', (req, res) => {
const limit = Math.min(parseInt(req.query.limit as string) || 50, 200);
const symbol = req.query.symbol as string | undefined;
let query = `SELECT * FROM alerts`;
const params: (string | number)[] = [];
if (symbol) {
query += ` WHERE tradingsymbol = ?`;
params.push(symbol);
}
query += ` ORDER BY alerted_at DESC LIMIT ?`;
params.push(limit);
const rows = db.prepare(query).all(...params);
res.json({ ok: true, data: rows });
});
// ── GET /api/config ───────────────────────────────────────────────────────
app.get('/api/config', (_req, res) => {
const rows = db.prepare(`SELECT * FROM position_config`).all();
res.json({
ok: true,
global: {
alertThresholdPct: parseFloat(process.env.ALERT_THRESHOLD_PCT || '5'),
alertMinAbsInr: parseFloat(process.env.ALERT_MIN_ABS_INR || '100'),
pollIntervalSeconds: parseInt(process.env.POLL_INTERVAL_SECONDS || '60'),
},
overrides: rows,
});
});
// ── PUT /api/config/:key ──────────────────────────────────────────────────
// Set per-position threshold or mute
app.put('/api/config/:key', (req, res) => {
const { key } = req.params;
const { alert_threshold_pct, muted_until, notes } = req.body;
db.prepare(`
INSERT INTO position_config (position_key, alert_threshold_pct, muted_until, notes)
VALUES (?, ?, ?, ?)
ON CONFLICT(position_key) DO UPDATE SET
alert_threshold_pct = excluded.alert_threshold_pct,
muted_until = excluded.muted_until,
notes = excluded.notes
`).run(key, alert_threshold_pct ?? null, muted_until ?? null, notes ?? null);
res.json({ ok: true });
});
// ── GET /api/health ───────────────────────────────────────────────────────
app.get('/api/health', (_req, res) => {
const lastError = db.prepare(
`SELECT error, occurred_at FROM poll_errors ORDER BY occurred_at DESC LIMIT 1`
).get() as { error: string; occurred_at: string } | undefined;
const posCount = (db.prepare(`SELECT COUNT(*) as n FROM positions WHERE is_closed = 0`).get() as { n: number }).n;
res.json({
ok: true,
marketOpen: isMarketOpen(),
openPositions: posCount,
lastError: lastError ?? null,
uptime: Math.floor(process.uptime()),
});
});
// Catch-all: serve index.html for SPA routing
app.get('*', (_req, res) => {
res.sendFile(path.join(publicDir, 'index.html'));
});
return app;
}