import * as plugins from '../../plugins.js'; import type { IStockProvider, IProviderConfig } from '../interfaces/provider.js'; import type { IStockPrice, IStockQuoteRequest, IStockBatchQuoteRequest } from '../interfaces/stockprice.js'; export class YahooFinanceProvider implements IStockProvider { public name = 'Yahoo Finance'; public priority = 100; public readonly requiresAuth = false; public readonly rateLimit = { requestsPerMinute: 100, // Conservative estimate requestsPerDay: undefined }; private logger = console; private baseUrl = 'https://query1.finance.yahoo.com'; private userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; constructor(private config?: IProviderConfig) {} public async fetchPrice(request: IStockQuoteRequest): Promise { try { const url = `${this.baseUrl}/v8/finance/chart/${request.ticker}`; const response = await plugins.smartrequest.getJson(url, { headers: { 'User-Agent': this.userAgent }, timeout: this.config?.timeout || 10000 }); const responseData = response.body as any; if (!responseData?.chart?.result?.[0]) { throw new Error(`No data found for ticker ${request.ticker}`); } const data = responseData.chart.result[0]; const meta = data.meta; if (!meta.regularMarketPrice) { throw new Error(`No price data available for ${request.ticker}`); } const stockPrice: IStockPrice = { ticker: request.ticker.toUpperCase(), price: meta.regularMarketPrice, currency: meta.currency || 'USD', change: meta.regularMarketPrice - meta.previousClose, changePercent: ((meta.regularMarketPrice - meta.previousClose) / meta.previousClose) * 100, previousClose: meta.previousClose, timestamp: new Date(meta.regularMarketTime * 1000), provider: this.name, marketState: this.determineMarketState(meta), exchange: meta.exchange, exchangeName: meta.exchangeName }; return stockPrice; } catch (error) { console.error(`Failed to fetch price for ${request.ticker}:`, error); throw new Error(`Yahoo Finance: Failed to fetch price for ${request.ticker}: ${error.message}`); } } public async fetchPrices(request: IStockBatchQuoteRequest): Promise { try { const symbols = request.tickers.join(','); const url = `${this.baseUrl}/v8/finance/spark?symbols=${symbols}&range=1d&interval=5m`; const response = await plugins.smartrequest.getJson(url, { headers: { 'User-Agent': this.userAgent }, timeout: this.config?.timeout || 15000 }); const responseData = response.body as any; const prices: IStockPrice[] = []; for (const [ticker, data] of Object.entries(responseData)) { if (!data || typeof data !== 'object') continue; const sparkData = data as any; if (!sparkData.previousClose || !sparkData.close?.length) { console.warn(`Incomplete data for ${ticker}, skipping`); continue; } const currentPrice = sparkData.close[sparkData.close.length - 1]; const timestamp = sparkData.timestamp?.[sparkData.timestamp.length - 1]; prices.push({ ticker: ticker.toUpperCase(), price: currentPrice, currency: sparkData.currency || 'USD', change: currentPrice - sparkData.previousClose, changePercent: ((currentPrice - sparkData.previousClose) / sparkData.previousClose) * 100, previousClose: sparkData.previousClose, timestamp: timestamp ? new Date(timestamp * 1000) : new Date(), provider: this.name, marketState: sparkData.marketState || 'REGULAR', exchange: sparkData.exchange, exchangeName: sparkData.exchangeName }); } if (prices.length === 0) { throw new Error('No valid price data received from batch request'); } return prices; } catch (error) { console.error(`Failed to fetch batch prices:`, error); throw new Error(`Yahoo Finance: Failed to fetch batch prices: ${error.message}`); } } public async isAvailable(): Promise { try { // Test with a well-known ticker await this.fetchPrice({ ticker: 'AAPL' }); return true; } catch (error) { console.warn('Yahoo Finance provider is not available:', error); return false; } } public supportsMarket(market: string): boolean { // Yahoo Finance supports most major markets const supportedMarkets = ['US', 'UK', 'DE', 'FR', 'JP', 'CN', 'HK', 'AU', 'CA']; return supportedMarkets.includes(market.toUpperCase()); } public supportsTicker(ticker: string): boolean { // Basic validation - Yahoo supports most tickers return /^[A-Z0-9\.\-]{1,10}$/.test(ticker.toUpperCase()); } private determineMarketState(meta: any): 'PRE' | 'REGULAR' | 'POST' | 'CLOSED' { const marketState = meta.marketState?.toUpperCase(); switch (marketState) { case 'PRE': return 'PRE'; case 'POST': return 'POST'; case 'REGULAR': return 'REGULAR'; default: // Check if market is currently open based on timestamps const now = Date.now() / 1000; const regularMarketTime = meta.regularMarketTime; const timeDiff = now - regularMarketTime; // If last update was more than 1 hour ago, market is likely closed return timeDiff > 3600 ? 'CLOSED' : 'REGULAR'; } } }