@fin.cx/opendata 1.5.3 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,159 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import type { IStockProvider, IProviderConfig } from '../interfaces/provider.js';
3
+ import type { IStockPrice, IStockQuoteRequest, IStockBatchQuoteRequest } from '../interfaces/stockprice.js';
4
+
5
+ export class YahooFinanceProvider implements IStockProvider {
6
+ public name = 'Yahoo Finance';
7
+ public priority = 100;
8
+ public readonly requiresAuth = false;
9
+ public readonly rateLimit = {
10
+ requestsPerMinute: 100, // Conservative estimate
11
+ requestsPerDay: undefined
12
+ };
13
+
14
+ private logger = console;
15
+ private baseUrl = 'https://query1.finance.yahoo.com';
16
+ 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';
17
+
18
+ constructor(private config?: IProviderConfig) {}
19
+
20
+ public async fetchPrice(request: IStockQuoteRequest): Promise<IStockPrice> {
21
+ try {
22
+ const url = `${this.baseUrl}/v8/finance/chart/${request.ticker}`;
23
+ const response = await plugins.smartrequest.getJson(url, {
24
+ headers: {
25
+ 'User-Agent': this.userAgent
26
+ },
27
+ timeout: this.config?.timeout || 10000
28
+ });
29
+
30
+ const responseData = response.body as any;
31
+
32
+ if (!responseData?.chart?.result?.[0]) {
33
+ throw new Error(`No data found for ticker ${request.ticker}`);
34
+ }
35
+
36
+ const data = responseData.chart.result[0];
37
+ const meta = data.meta;
38
+
39
+ if (!meta.regularMarketPrice) {
40
+ throw new Error(`No price data available for ${request.ticker}`);
41
+ }
42
+
43
+ const stockPrice: IStockPrice = {
44
+ ticker: request.ticker.toUpperCase(),
45
+ price: meta.regularMarketPrice,
46
+ currency: meta.currency || 'USD',
47
+ change: meta.regularMarketPrice - meta.previousClose,
48
+ changePercent: ((meta.regularMarketPrice - meta.previousClose) / meta.previousClose) * 100,
49
+ previousClose: meta.previousClose,
50
+ timestamp: new Date(meta.regularMarketTime * 1000),
51
+ provider: this.name,
52
+ marketState: this.determineMarketState(meta),
53
+ exchange: meta.exchange,
54
+ exchangeName: meta.exchangeName
55
+ };
56
+
57
+ return stockPrice;
58
+ } catch (error) {
59
+ console.error(`Failed to fetch price for ${request.ticker}:`, error);
60
+ throw new Error(`Yahoo Finance: Failed to fetch price for ${request.ticker}: ${error.message}`);
61
+ }
62
+ }
63
+
64
+ public async fetchPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]> {
65
+ try {
66
+ const symbols = request.tickers.join(',');
67
+ const url = `${this.baseUrl}/v8/finance/spark?symbols=${symbols}&range=1d&interval=5m`;
68
+
69
+ const response = await plugins.smartrequest.getJson(url, {
70
+ headers: {
71
+ 'User-Agent': this.userAgent
72
+ },
73
+ timeout: this.config?.timeout || 15000
74
+ });
75
+
76
+ const responseData = response.body as any;
77
+ const prices: IStockPrice[] = [];
78
+
79
+ for (const [ticker, data] of Object.entries(responseData)) {
80
+ if (!data || typeof data !== 'object') continue;
81
+
82
+ const sparkData = data as any;
83
+ if (!sparkData.previousClose || !sparkData.close?.length) {
84
+ console.warn(`Incomplete data for ${ticker}, skipping`);
85
+ continue;
86
+ }
87
+
88
+ const currentPrice = sparkData.close[sparkData.close.length - 1];
89
+ const timestamp = sparkData.timestamp?.[sparkData.timestamp.length - 1];
90
+
91
+ prices.push({
92
+ ticker: ticker.toUpperCase(),
93
+ price: currentPrice,
94
+ currency: sparkData.currency || 'USD',
95
+ change: currentPrice - sparkData.previousClose,
96
+ changePercent: ((currentPrice - sparkData.previousClose) / sparkData.previousClose) * 100,
97
+ previousClose: sparkData.previousClose,
98
+ timestamp: timestamp ? new Date(timestamp * 1000) : new Date(),
99
+ provider: this.name,
100
+ marketState: sparkData.marketState || 'REGULAR',
101
+ exchange: sparkData.exchange,
102
+ exchangeName: sparkData.exchangeName
103
+ });
104
+ }
105
+
106
+ if (prices.length === 0) {
107
+ throw new Error('No valid price data received from batch request');
108
+ }
109
+
110
+ return prices;
111
+ } catch (error) {
112
+ console.error(`Failed to fetch batch prices:`, error);
113
+ throw new Error(`Yahoo Finance: Failed to fetch batch prices: ${error.message}`);
114
+ }
115
+ }
116
+
117
+ public async isAvailable(): Promise<boolean> {
118
+ try {
119
+ // Test with a well-known ticker
120
+ await this.fetchPrice({ ticker: 'AAPL' });
121
+ return true;
122
+ } catch (error) {
123
+ console.warn('Yahoo Finance provider is not available:', error);
124
+ return false;
125
+ }
126
+ }
127
+
128
+ public supportsMarket(market: string): boolean {
129
+ // Yahoo Finance supports most major markets
130
+ const supportedMarkets = ['US', 'UK', 'DE', 'FR', 'JP', 'CN', 'HK', 'AU', 'CA'];
131
+ return supportedMarkets.includes(market.toUpperCase());
132
+ }
133
+
134
+ public supportsTicker(ticker: string): boolean {
135
+ // Basic validation - Yahoo supports most tickers
136
+ return /^[A-Z0-9\.\-]{1,10}$/.test(ticker.toUpperCase());
137
+ }
138
+
139
+ private determineMarketState(meta: any): 'PRE' | 'REGULAR' | 'POST' | 'CLOSED' {
140
+ const marketState = meta.marketState?.toUpperCase();
141
+
142
+ switch (marketState) {
143
+ case 'PRE':
144
+ return 'PRE';
145
+ case 'POST':
146
+ return 'POST';
147
+ case 'REGULAR':
148
+ return 'REGULAR';
149
+ default:
150
+ // Check if market is currently open based on timestamps
151
+ const now = Date.now() / 1000;
152
+ const regularMarketTime = meta.regularMarketTime;
153
+ const timeDiff = now - regularMarketTime;
154
+
155
+ // If last update was more than 1 hour ago, market is likely closed
156
+ return timeDiff > 3600 ? 'CLOSED' : 'REGULAR';
157
+ }
158
+ }
159
+ }