From 496d192435691784995a14b22e9f41c980901b5d Mon Sep 17 00:00:00 2001 From: Manohar Date: Fri, 8 May 2026 17:45:35 +0000 Subject: [PATCH] fix: market.ts dual-source (Angel primary, Yahoo fallback for after hours) --- src/angel/market.ts | 208 +++++++++++++++++++++----------------------- 1 file changed, 98 insertions(+), 110 deletions(-) diff --git a/src/angel/market.ts b/src/angel/market.ts index 38c96ff..304739e 100644 --- a/src/angel/market.ts +++ b/src/angel/market.ts @@ -1,15 +1,7 @@ -import axios from "axios"; -import { getAuthHeaders } from "./auth.js"; +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" }, -}; +const BASE = 'https://apiconnect.angelbroking.com'; export interface MarketQuote { key: string; @@ -18,116 +10,112 @@ export interface MarketQuote { change: number; changePct: number; unit: string; + stale: boolean; // true = after-hours data from Yahoo fallback } -/** - * 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 { +// Fixed Angel tokens for major indices +const INDEX_TOKENS = [ + { key: 'NIFTY50', exchange: 'NSE', token: '26000', label: 'NIFTY 50', unit: 'pts' }, + { key: 'BANKNIFTY', exchange: 'NSE', token: '26009', label: 'BANK NIFTY', unit: 'pts' }, + { key: 'INDIAVIX', exchange: 'NSE', token: '26017', label: 'INDIA VIX', unit: '%' }, + { key: 'SENSEX', exchange: 'BSE', token: '1', label: 'SENSEX', unit: 'pts' }, +]; + +// Yahoo Finance symbols for fallback (after hours / Angel empty) +const YAHOO_SYMBOLS: Record = { + NIFTY50: { label: 'NIFTY 50', unit: 'pts', yahooSym: '^NSEI' }, + BANKNIFTY: { label: 'BANK NIFTY', unit: 'pts', yahooSym: '^NSEBANK' }, + INDIAVIX: { label: 'INDIA VIX', unit: '%', yahooSym: '^INDIAVIX' }, + SENSEX: { label: 'SENSEX', unit: 'pts', yahooSym: '^BSESN' }, + CRUDEOIL: { label: 'Crude Oil', unit: '\u20b9/bbl', yahooSym: 'CL=F' }, + USDINR: { label: 'INR/USD', unit: '\u20b9', yahooSym: 'USDINR=X' }, +}; + +async function fetchFromAngel(): 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; + + // Step 1: search for front-month crude and INR/USD tokens + async function search(exchange: string, query: string): Promise { + try { + const r = await axios.post( + BASE + '/rest/secure/angelbroking/order/v1/searchScrip', + { exchange, searchscrip: query }, + { headers, timeout: 5000 } + ); + return r.data?.data?.[0]?.symboltoken ?? null; + } 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"), + search('MCX', 'CRUDEOIL'), + search('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 2: build token map + const tokenMap: Record = { NSE: [], BSE: [] }; + const tokenIndex: Record = {}; - // 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, - }); - } + for (const idx of INDEX_TOKENS) { + tokenMap[idx.exchange] = tokenMap[idx.exchange] || []; + tokenMap[idx.exchange].push(idx.token); + tokenIndex[idx.token] = idx.key; } + if (crudeToken) { tokenMap['MCX'] = [crudeToken]; tokenIndex[crudeToken] = 'CRUDEOIL'; } + if (usdinrToken) { tokenMap['CDS'] = [usdinrToken]; tokenIndex[usdinrToken] = 'USDINR'; } - // 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", - }); - } + // Step 3: fetch quotes + const r = await axios.post( + BASE + '/rest/secure/angelbroking/market/v1/quote/', + { mode: 'FULL', exchangeTokens: tokenMap }, + { headers, timeout: 8000 } + ); - // 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: "₹", - }); - } + const fetched: any[] = r.data?.data?.fetched ?? []; + if (!fetched.length) return []; - return results; + return fetched.map((q: any) => { + const key = tokenIndex[q.symbolToken] ?? q.tradingSymbol; + const meta = YAHOO_SYMBOLS[key] ?? { label: q.tradingSymbol, unit: '' }; + const ltp = parseFloat(q.ltp) || 0; + const close = parseFloat(q.close) || 0; + const change = parseFloat(q.netChange) || (ltp - close); + const changePct = parseFloat(q.percentChange) || (close > 0 ? (change / close) * 100 : 0); + return { key, label: meta.label, price: ltp, change, changePct, unit: meta.unit, stale: false }; + }); +} + +async function fetchFromYahoo(): Promise { + const syms = Object.entries(YAHOO_SYMBOLS).map(([k, v]) => v.yahooSym).join(','); + const url = 'https://query1.finance.yahoo.com/v7/finance/quote?symbols=' + syms; + const r = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 8000 }); + const quotes: any[] = r.data?.quoteResponse?.result ?? []; + + const yahooKeyMap: Record = {}; + for (const [k, v] of Object.entries(YAHOO_SYMBOLS)) yahooKeyMap[v.yahooSym] = k; + + return quotes.map((q: any) => { + const key = yahooKeyMap[q.symbol] ?? q.symbol; + const meta = YAHOO_SYMBOLS[key] ?? { label: q.shortName ?? key, unit: '' }; + return { + key, label: meta.label, + price: q.regularMarketPrice ?? 0, + change: q.regularMarketChange ?? 0, + changePct: q.regularMarketChangePercent ?? 0, + unit: meta.unit, + stale: true, // Yahoo = previous session close after hours + }; + }); +} + +export async function fetchMarketData(): Promise { + try { + const angelData = await fetchFromAngel(); + if (angelData.length > 0) return angelData; + // Angel returned empty (market closed) — fall back to Yahoo + return await fetchFromYahoo(); + } catch (err) { + // Angel auth/network error — try Yahoo + try { return await fetchFromYahoo(); } + catch { return []; } + } }