103 lines
4.1 KiB
TypeScript
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;
|
|
}
|