feat: Angel SmartAPI market data endpoint (NIFTY/SENSEX/VIX/crude/INR) + global alert config

This commit is contained in:
Manohar 2026-05-08 17:42:43 +00:00
parent 30884f10e0
commit a3bb07f7ef
2 changed files with 158 additions and 0 deletions

133
src/angel/market.ts Normal file
View file

@ -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<string, { exchange: string; token: string; label: string; unit: string }> = {
"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<string | null> {
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<string, string[]>
): Promise<Record<string, { ltp: number; close: number; change: number; changePct: number }>> {
const headers = await getAuthHeaders();
const resp = await axios.post(
`${BASE}/rest/secure/angelbroking/market/v1/quote/`,
{ mode: "FULL", exchangeTokens },
{ headers }
);
const result: Record<string, any> = {};
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<MarketQuote[]> {
// 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<string, string[]> = {
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;
}

View file

@ -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'));