159 lines
5.6 KiB
TypeScript
159 lines
5.6 KiB
TypeScript
|
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<IStockPrice> {
|
||
|
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<IStockPrice[]> {
|
||
|
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<boolean> {
|
||
|
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';
|
||
|
}
|
||
|
}
|
||
|
}
|