fix: market.ts dual-source (Angel primary, Yahoo fallback for after hours)
This commit is contained in:
parent
a3bb07f7ef
commit
496d192435
1 changed files with 98 additions and 110 deletions
|
|
@ -1,15 +1,7 @@
|
||||||
import axios from "axios";
|
import axios from 'axios';
|
||||||
import { getAuthHeaders } from "./auth.js";
|
import { getAuthHeaders } from './auth.js';
|
||||||
|
|
||||||
const BASE = "https://apiconnect.angelbroking.com";
|
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 {
|
export interface MarketQuote {
|
||||||
key: string;
|
key: string;
|
||||||
|
|
@ -18,116 +10,112 @@ export interface MarketQuote {
|
||||||
change: number;
|
change: number;
|
||||||
changePct: number;
|
changePct: number;
|
||||||
unit: string;
|
unit: string;
|
||||||
|
stale: boolean; // true = after-hours data from Yahoo fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Fixed Angel tokens for major indices
|
||||||
* Search Angel instrument list for the front-month futures token.
|
const INDEX_TOKENS = [
|
||||||
* Angel exposes a search API: POST /rest/secure/angelbroking/order/v1/searchScrip
|
{ key: 'NIFTY50', exchange: 'NSE', token: '26000', label: 'NIFTY 50', unit: 'pts' },
|
||||||
*/
|
{ key: 'BANKNIFTY', exchange: 'NSE', token: '26009', label: 'BANK NIFTY', unit: 'pts' },
|
||||||
async function searchToken(
|
{ key: 'INDIAVIX', exchange: 'NSE', token: '26017', label: 'INDIA VIX', unit: '%' },
|
||||||
exchange: string,
|
{ key: 'SENSEX', exchange: 'BSE', token: '1', label: 'SENSEX', unit: 'pts' },
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Yahoo Finance symbols for fallback (after hours / Angel empty)
|
||||||
* Fetch LTP + change for a set of exchange tokens in one API call.
|
const YAHOO_SYMBOLS: Record<string, { label: string; unit: string; yahooSym: string }> = {
|
||||||
* Mode "FULL" gives us ltp, close, change, changepct.
|
NIFTY50: { label: 'NIFTY 50', unit: 'pts', yahooSym: '^NSEI' },
|
||||||
*/
|
BANKNIFTY: { label: 'BANK NIFTY', unit: 'pts', yahooSym: '^NSEBANK' },
|
||||||
async function fetchQuotes(
|
INDIAVIX: { label: 'INDIA VIX', unit: '%', yahooSym: '^INDIAVIX' },
|
||||||
exchangeTokens: Record<string, string[]>
|
SENSEX: { label: 'SENSEX', unit: 'pts', yahooSym: '^BSESN' },
|
||||||
): Promise<Record<string, { ltp: number; close: number; change: number; changePct: number }>> {
|
CRUDEOIL: { label: 'Crude Oil', unit: '\u20b9/bbl', yahooSym: 'CL=F' },
|
||||||
const headers = await getAuthHeaders();
|
USDINR: { label: 'INR/USD', unit: '\u20b9', yahooSym: 'USDINR=X' },
|
||||||
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;
|
async function fetchFromAngel(): Promise<MarketQuote[]> {
|
||||||
|
const headers = await getAuthHeaders();
|
||||||
|
|
||||||
|
// Step 1: search for front-month crude and INR/USD tokens
|
||||||
|
async function search(exchange: string, query: string): Promise<string | null> {
|
||||||
|
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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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([
|
const [crudeToken, usdinrToken] = await Promise.all([
|
||||||
searchToken("MCX", "CRUDEOIL"),
|
search('MCX', 'CRUDEOIL'),
|
||||||
searchToken("CDS", "USDINR"),
|
search('CDS', 'USDINR'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Step 2: Build token map for batch quote fetch
|
// Step 2: build token map
|
||||||
const tokenMap: Record<string, string[]> = {
|
const tokenMap: Record<string, string[]> = { NSE: [], BSE: [] };
|
||||||
NSE: ["26000", "26009", "26017"],
|
const tokenIndex: Record<string, string> = {};
|
||||||
BSE: ["1"],
|
|
||||||
};
|
|
||||||
if (crudeToken) tokenMap["MCX"] = [crudeToken];
|
|
||||||
if (usdinrToken) tokenMap["CDS"] = [usdinrToken];
|
|
||||||
|
|
||||||
// Step 3: Fetch all quotes in one call
|
for (const idx of INDEX_TOKENS) {
|
||||||
const quotes = await fetchQuotes(tokenMap);
|
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'; }
|
||||||
|
|
||||||
const results: MarketQuote[] = [];
|
// Step 3: fetch quotes
|
||||||
|
const r = await axios.post(
|
||||||
|
BASE + '/rest/secure/angelbroking/market/v1/quote/',
|
||||||
|
{ mode: 'FULL', exchangeTokens: tokenMap },
|
||||||
|
{ headers, timeout: 8000 }
|
||||||
|
);
|
||||||
|
|
||||||
// Fixed index results
|
const fetched: any[] = r.data?.data?.fetched ?? [];
|
||||||
for (const [key, meta] of Object.entries(FIXED_TOKENS)) {
|
if (!fetched.length) return [];
|
||||||
const lookupKey = `${meta.exchange}:${meta.token}`;
|
|
||||||
const q = quotes[lookupKey];
|
return fetched.map((q: any) => {
|
||||||
if (q) {
|
const key = tokenIndex[q.symbolToken] ?? q.tradingSymbol;
|
||||||
results.push({
|
const meta = YAHOO_SYMBOLS[key] ?? { label: q.tradingSymbol, unit: '' };
|
||||||
key,
|
const ltp = parseFloat(q.ltp) || 0;
|
||||||
label: meta.label,
|
const close = parseFloat(q.close) || 0;
|
||||||
price: q.ltp,
|
const change = parseFloat(q.netChange) || (ltp - close);
|
||||||
change: q.change,
|
const changePct = parseFloat(q.percentChange) || (close > 0 ? (change / close) * 100 : 0);
|
||||||
changePct: q.changePct,
|
return { key, label: meta.label, price: ltp, change, changePct, unit: meta.unit, stale: false };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchFromYahoo(): Promise<MarketQuote[]> {
|
||||||
|
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<string, string> = {};
|
||||||
|
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,
|
unit: meta.unit,
|
||||||
});
|
stale: true, // Yahoo = previous session close after hours
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
export async function fetchMarketData(): Promise<MarketQuote[]> {
|
||||||
if (usdinrToken) {
|
try {
|
||||||
const q = quotes[`CDS:${usdinrToken}`];
|
const angelData = await fetchFromAngel();
|
||||||
if (q) results.push({
|
if (angelData.length > 0) return angelData;
|
||||||
key: "USDINR", label: "INR/USD", price: q.ltp,
|
// Angel returned empty (market closed) — fall back to Yahoo
|
||||||
change: q.change, changePct: q.changePct, unit: "₹",
|
return await fetchFromYahoo();
|
||||||
});
|
} catch (err) {
|
||||||
|
// Angel auth/network error — try Yahoo
|
||||||
|
try { return await fetchFromYahoo(); }
|
||||||
|
catch { return []; }
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue