@fin.cx/opendata 1.6.1 → 2.0.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,200 @@
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
+ /**
6
+ * Marketstack API v2 Provider
7
+ * Documentation: https://marketstack.com/documentation_v2
8
+ *
9
+ * Features:
10
+ * - End-of-Day (EOD) stock prices
11
+ * - Supports 125,000+ tickers across 72+ exchanges worldwide
12
+ * - Requires API key authentication
13
+ *
14
+ * Rate Limits:
15
+ * - Free Plan: 100 requests/month (EOD only)
16
+ * - Basic Plan: 10,000 requests/month
17
+ * - Professional Plan: 100,000 requests/month
18
+ *
19
+ * Note: This provider returns EOD data, not real-time prices
20
+ */
21
+ export class MarketstackProvider implements IStockProvider {
22
+ public name = 'Marketstack';
23
+ public priority = 80; // Lower than Yahoo (100) due to rate limits and EOD-only data
24
+ public readonly requiresAuth = true;
25
+ public readonly rateLimit = {
26
+ requestsPerMinute: undefined, // No per-minute limit specified
27
+ requestsPerDay: undefined // Varies by plan
28
+ };
29
+
30
+ private logger = console;
31
+ private baseUrl = 'https://api.marketstack.com/v2';
32
+ private apiKey: string;
33
+
34
+ constructor(apiKey: string, private config?: IProviderConfig) {
35
+ if (!apiKey) {
36
+ throw new Error('API key is required for Marketstack provider');
37
+ }
38
+
39
+ this.apiKey = apiKey;
40
+ }
41
+
42
+ /**
43
+ * Fetch latest EOD price for a single ticker
44
+ */
45
+ public async fetchPrice(request: IStockQuoteRequest): Promise<IStockPrice> {
46
+ try {
47
+ const url = `${this.baseUrl}/tickers/${request.ticker}/eod/latest?access_key=${this.apiKey}`;
48
+
49
+ const response = await plugins.smartrequest.SmartRequest.create()
50
+ .url(url)
51
+ .timeout(this.config?.timeout || 10000)
52
+ .get();
53
+
54
+ const responseData = await response.json() as any;
55
+
56
+ // Check for API errors
57
+ if (responseData.error) {
58
+ throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
59
+ }
60
+
61
+ // For single ticker endpoint, response is direct object (not wrapped in data field)
62
+ if (!responseData || !responseData.close) {
63
+ throw new Error(`No data found for ticker ${request.ticker}`);
64
+ }
65
+
66
+ return this.mapToStockPrice(responseData);
67
+ } catch (error) {
68
+ this.logger.error(`Failed to fetch price for ${request.ticker}:`, error);
69
+ throw new Error(`Marketstack: Failed to fetch price for ${request.ticker}: ${error.message}`);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Fetch latest EOD prices for multiple tickers
75
+ */
76
+ public async fetchPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]> {
77
+ try {
78
+ const symbols = request.tickers.join(',');
79
+ const url = `${this.baseUrl}/eod/latest?access_key=${this.apiKey}&symbols=${symbols}`;
80
+
81
+ const response = await plugins.smartrequest.SmartRequest.create()
82
+ .url(url)
83
+ .timeout(this.config?.timeout || 15000)
84
+ .get();
85
+
86
+ const responseData = await response.json() as any;
87
+
88
+ // Check for API errors
89
+ if (responseData.error) {
90
+ throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
91
+ }
92
+
93
+ if (!responseData?.data || !Array.isArray(responseData.data)) {
94
+ throw new Error('Invalid response format from Marketstack API');
95
+ }
96
+
97
+ const prices: IStockPrice[] = [];
98
+
99
+ for (const data of responseData.data) {
100
+ try {
101
+ prices.push(this.mapToStockPrice(data));
102
+ } catch (error) {
103
+ this.logger.warn(`Failed to parse data for ${data.symbol}:`, error);
104
+ // Continue processing other tickers
105
+ }
106
+ }
107
+
108
+ if (prices.length === 0) {
109
+ throw new Error('No valid price data received from batch request');
110
+ }
111
+
112
+ return prices;
113
+ } catch (error) {
114
+ this.logger.error(`Failed to fetch batch prices:`, error);
115
+ throw new Error(`Marketstack: Failed to fetch batch prices: ${error.message}`);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Check if the Marketstack API is available and accessible
121
+ */
122
+ public async isAvailable(): Promise<boolean> {
123
+ try {
124
+ // Test with a well-known ticker
125
+ const url = `${this.baseUrl}/tickers/AAPL/eod/latest?access_key=${this.apiKey}`;
126
+
127
+ const response = await plugins.smartrequest.SmartRequest.create()
128
+ .url(url)
129
+ .timeout(5000)
130
+ .get();
131
+
132
+ const responseData = await response.json() as any;
133
+
134
+ // Check if we got valid data (not an error)
135
+ // Single ticker endpoint returns direct object, not wrapped in data field
136
+ return !responseData.error && responseData.close !== undefined;
137
+ } catch (error) {
138
+ this.logger.warn('Marketstack provider is not available:', error);
139
+ return false;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Check if a market is supported
145
+ * Marketstack supports 72+ exchanges worldwide
146
+ */
147
+ public supportsMarket(market: string): boolean {
148
+ // Marketstack has broad international coverage including:
149
+ // US, UK, DE, FR, JP, CN, HK, AU, CA, IN, etc.
150
+ const supportedMarkets = [
151
+ 'US', 'UK', 'GB', 'DE', 'FR', 'JP', 'CN', 'HK', 'AU', 'CA',
152
+ 'IN', 'BR', 'MX', 'IT', 'ES', 'NL', 'SE', 'CH', 'NO', 'DK'
153
+ ];
154
+ return supportedMarkets.includes(market.toUpperCase());
155
+ }
156
+
157
+ /**
158
+ * Check if a ticker format is supported
159
+ */
160
+ public supportsTicker(ticker: string): boolean {
161
+ // Basic validation - Marketstack supports most standard ticker formats
162
+ return /^[A-Z0-9\.\-]{1,10}$/.test(ticker.toUpperCase());
163
+ }
164
+
165
+ /**
166
+ * Map Marketstack API response to IStockPrice interface
167
+ */
168
+ private mapToStockPrice(data: any): IStockPrice {
169
+ if (!data.close) {
170
+ throw new Error('Missing required price data');
171
+ }
172
+
173
+ // Calculate change and change percent
174
+ // EOD data: previous close is typically open price of the same day
175
+ // For better accuracy, we'd need previous day's close, but that requires another API call
176
+ const currentPrice = data.close;
177
+ const previousClose = data.open;
178
+ const change = currentPrice - previousClose;
179
+ const changePercent = previousClose !== 0 ? (change / previousClose) * 100 : 0;
180
+
181
+ // Parse timestamp
182
+ const timestamp = data.date ? new Date(data.date) : new Date();
183
+
184
+ const stockPrice: IStockPrice = {
185
+ ticker: data.symbol.toUpperCase(),
186
+ price: currentPrice,
187
+ currency: data.price_currency || 'USD',
188
+ change: change,
189
+ changePercent: changePercent,
190
+ previousClose: previousClose,
191
+ timestamp: timestamp,
192
+ provider: this.name,
193
+ marketState: 'CLOSED', // EOD data is always for closed markets
194
+ exchange: data.exchange,
195
+ exchangeName: data.exchange_code || data.name
196
+ };
197
+
198
+ return stockPrice;
199
+ }
200
+ }