/** * 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} 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} 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; }