institutional-trader/backend/src/services/yahooFinanceService.js

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