diff --git a/src/angel/market.ts b/src/angel/market.ts new file mode 100644 index 0000000..38c96ff --- /dev/null +++ b/src/angel/market.ts @@ -0,0 +1,133 @@ +import axios from "axios"; +import { getAuthHeaders } from "./auth.js"; + +const BASE = "https://apiconnect.angelbroking.com"; + +// Fixed tokens for cash indices — these never change +const FIXED_TOKENS: Record = { + "NIFTY50": { exchange: "NSE", token: "26000", label: "NIFTY 50", unit: "pts" }, + "BANKNIFTY": { exchange: "NSE", token: "26009", label: "BANK NIFTY", unit: "pts" }, + "INDIAVIX": { exchange: "NSE", token: "26017", label: "INDIA VIX", unit: "%" }, + "SENSEX": { exchange: "BSE", token: "1", label: "SENSEX", unit: "pts" }, +}; + +export interface MarketQuote { + key: string; + label: string; + price: number; + change: number; + changePct: number; + unit: string; +} + +/** + * Search Angel instrument list for the front-month futures token. + * Angel exposes a search API: POST /rest/secure/angelbroking/order/v1/searchScrip + */ +async function searchToken( + exchange: string, + query: string +): Promise { + const headers = await getAuthHeaders(); + try { + const resp = await axios.post( + `${BASE}/rest/secure/angelbroking/order/v1/searchScrip`, + { exchange, searchscrip: query }, + { headers } + ); + const data = resp.data?.data; + if (!data || !data.length) return null; + // First result is typically the nearest expiry + return data[0].symboltoken; + } catch { + return null; + } +} + +/** + * Fetch LTP + change for a set of exchange tokens in one API call. + * Mode "FULL" gives us ltp, close, change, changepct. + */ +async function fetchQuotes( + exchangeTokens: Record +): Promise> { + const headers = await getAuthHeaders(); + const resp = await axios.post( + `${BASE}/rest/secure/angelbroking/market/v1/quote/`, + { mode: "FULL", exchangeTokens }, + { headers } + ); + + const result: Record = {}; + const fetched = resp.data?.data?.fetched ?? []; + for (const q of fetched) { + const key = `${q.exchangeType}:${q.symbolToken}`; + result[key] = { + ltp: q.ltp ?? 0, + close: q.close ?? 0, + change: q.netChange ?? (q.ltp - q.close), + changePct: q.percentChange ?? (q.close > 0 ? ((q.ltp - q.close) / q.close) * 100 : 0), + }; + } + return result; +} + +/** + * Main export — fetches all 6 market indicators in one round-trip (2 API calls max). + */ +export async function fetchMarketData(): Promise { + // Step 1: Resolve dynamic tokens for crude oil and INR/USD + const [crudeToken, usdinrToken] = await Promise.all([ + searchToken("MCX", "CRUDEOIL"), + searchToken("CDS", "USDINR"), + ]); + + // Step 2: Build token map for batch quote fetch + const tokenMap: Record = { + NSE: ["26000", "26009", "26017"], + BSE: ["1"], + }; + if (crudeToken) tokenMap["MCX"] = [crudeToken]; + if (usdinrToken) tokenMap["CDS"] = [usdinrToken]; + + // Step 3: Fetch all quotes in one call + const quotes = await fetchQuotes(tokenMap); + + const results: MarketQuote[] = []; + + // Fixed index results + for (const [key, meta] of Object.entries(FIXED_TOKENS)) { + const lookupKey = `${meta.exchange}:${meta.token}`; + const q = quotes[lookupKey]; + if (q) { + results.push({ + key, + label: meta.label, + price: q.ltp, + change: q.change, + changePct: q.changePct, + unit: meta.unit, + }); + } + } + + // Crude oil + if (crudeToken) { + const q = quotes[`MCX:${crudeToken}`]; + if (q) results.push({ + key: "CRUDEOIL", label: "Crude Oil", price: q.ltp, + change: q.change, changePct: q.changePct, unit: "₹/bbl", + }); + } + + // INR/USD + if (usdinrToken) { + const q = quotes[`CDS:${usdinrToken}`]; + if (q) results.push({ + key: "USDINR", label: "INR/USD", price: q.ltp, + change: q.change, changePct: q.changePct, unit: "₹", + }); + } + + return results; +} diff --git a/src/api/server.ts b/src/api/server.ts index bd0104f..36b5050 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -126,6 +126,31 @@ export function createServer(): express.Application { }); }); + + // ── GET /api/market ────────────────────────────────────────────────────── + app.get('/api/market', async (_req, res) => { + try { + const { fetchMarketData } = await import('../angel/market.js'); + const data = await fetchMarketData(); + res.json({ ok: true, data }); + } catch (err) { + res.status(502).json({ ok: false, error: String(err) }); + } + }); + + // ── PUT /api/config/global ───────────────────────────────────────────── + app.put('/api/config/global', (req, res) => { + const { alertThresholdPct, alertMinAbsInr } = req.body; + db.prepare(` + INSERT INTO position_config (position_key, alert_threshold_pct, notes) + VALUES ('__global__', ?, 'global override') + ON CONFLICT(position_key) DO UPDATE SET alert_threshold_pct = excluded.alert_threshold_pct + `).run(alertThresholdPct ?? null); + if (alertThresholdPct != null) process.env.ALERT_THRESHOLD_PCT = String(alertThresholdPct); + if (alertMinAbsInr != null) process.env.ALERT_MIN_ABS_INR = String(alertMinAbsInr); + res.json({ ok: true, alertThresholdPct, alertMinAbsInr }); + }); + // Catch-all: serve index.html for SPA routing app.get('*', (_req, res) => { res.sendFile(path.join(publicDir, 'index.html'));