139 lines
4.7 KiB
JavaScript
139 lines
4.7 KiB
JavaScript
/**
|
|
* Yahoo Finance Service
|
|
* Fetches latest stock price data from Yahoo Finance API
|
|
*/
|
|
|
|
/**
|
|
* Fetch latest stock data from Yahoo Finance for a single symbol
|
|
* @param {string} symbol - Stock symbol (e.g., 'AAPL')
|
|
* @returns {Promise<Object|null>} Stock price data or null if failed
|
|
*/
|
|
export async function fetchYahooFinanceData(symbol) {
|
|
try {
|
|
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${symbol}?interval=1d&range=5d`;
|
|
const response = await fetch(url);
|
|
|
|
if (!response.ok) {
|
|
console.warn(`Yahoo Finance API error for ${symbol}: ${response.status} ${response.statusText}`);
|
|
return null;
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.chart?.result?.[0]) {
|
|
const result = data.chart.result[0];
|
|
const meta = result.meta;
|
|
const quotes = result.indicators?.quote?.[0];
|
|
const timestamps = result.timestamp || [];
|
|
|
|
// Extract historical volume data (last 5 days)
|
|
const volumeHistory = [];
|
|
if (quotes && quotes.volume && timestamps.length > 0) {
|
|
const volumes = quotes.volume || [];
|
|
const closes = quotes.close || [];
|
|
const opens = quotes.open || [];
|
|
const highs = quotes.high || [];
|
|
const lows = quotes.low || [];
|
|
|
|
// Get the last 5 days (or all available if less than 5)
|
|
const startIdx = Math.max(0, timestamps.length - 5);
|
|
for (let i = startIdx; i < timestamps.length; i++) {
|
|
if (volumes[i] !== null && volumes[i] !== undefined) {
|
|
const timestamp = timestamps[i];
|
|
const date = new Date(timestamp * 1000);
|
|
volumeHistory.push({
|
|
date: date.toISOString().split('T')[0], // Convert to YYYY-MM-DD
|
|
volume: Math.round(volumes[i]) || 0,
|
|
close: closes[i] || 0,
|
|
open: opens[i] || 0,
|
|
high: highs[i] || 0,
|
|
low: lows[i] || 0
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sort by date descending (most recent first)
|
|
volumeHistory.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
}
|
|
|
|
return {
|
|
symbol: symbol.toUpperCase(),
|
|
currentPrice: meta.regularMarketPrice || meta.previousClose,
|
|
previousClose: meta.previousClose,
|
|
open: meta.regularMarketOpen || meta.previousClose,
|
|
high: meta.regularMarketDayHigh,
|
|
low: meta.regularMarketDayLow,
|
|
volume: meta.regularMarketVolume,
|
|
marketCap: meta.marketCap,
|
|
changePercent: meta.regularMarketPrice && meta.previousClose
|
|
? ((meta.regularMarketPrice - meta.previousClose) / meta.previousClose) * 100
|
|
: 0,
|
|
change: meta.regularMarketPrice && meta.previousClose
|
|
? meta.regularMarketPrice - meta.previousClose
|
|
: 0,
|
|
// Get recent price history
|
|
recentPrices: quotes?.close?.slice(-5) || [],
|
|
// Get volume history (last 5 days)
|
|
volumeHistory: volumeHistory,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.warn(`Failed to fetch Yahoo Finance data for ${symbol}:`, error.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Batch fetch stock data for multiple symbols with concurrency control
|
|
* @param {string[]} symbols - Array of stock symbols
|
|
* @param {number} concurrency - Number of concurrent requests (default: 5)
|
|
* @returns {Promise<Object>} Map of symbol to price data
|
|
*/
|
|
export async function batchFetchYahooFinanceData(symbols, concurrency = 5) {
|
|
if (!symbols || symbols.length === 0) {
|
|
return {};
|
|
}
|
|
|
|
// Remove duplicates and normalize symbols
|
|
const uniqueSymbols = [...new Set(symbols.map(s => s.toUpperCase().trim()))].filter(Boolean);
|
|
|
|
if (uniqueSymbols.length === 0) {
|
|
return {};
|
|
}
|
|
|
|
const priceDataMap = {};
|
|
|
|
// Process in batches to avoid overwhelming the API
|
|
for (let i = 0; i < uniqueSymbols.length; i += concurrency) {
|
|
const batch = uniqueSymbols.slice(i, i + concurrency);
|
|
const batchPromises = batch.map(async (symbol) => {
|
|
const data = await fetchYahooFinanceData(symbol);
|
|
return { symbol, data };
|
|
});
|
|
|
|
const batchResults = await Promise.allSettled(batchPromises);
|
|
|
|
batchResults.forEach((result) => {
|
|
if (result.status === 'fulfilled') {
|
|
const { symbol, data } = result.value;
|
|
if (data) {
|
|
priceDataMap[symbol] = data;
|
|
}
|
|
} else {
|
|
console.warn(`Failed to fetch data for symbol in batch:`, result.reason);
|
|
}
|
|
});
|
|
|
|
// Small delay between batches to be respectful to the API
|
|
if (i + concurrency < uniqueSymbols.length) {
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
}
|
|
}
|
|
|
|
return priceDataMap;
|
|
}
|
|
|