@fin.cx/opendata 2.0.0 → 2.1.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.
@@ -1,22 +1,41 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { IStockProvider, IProviderConfig } from '../interfaces/provider.js';
3
- import type { IStockPrice, IStockQuoteRequest, IStockBatchQuoteRequest } from '../interfaces/stockprice.js';
3
+ import type {
4
+ IStockPrice,
5
+ IStockQuoteRequest,
6
+ IStockBatchQuoteRequest,
7
+ IStockDataRequest,
8
+ IStockCurrentRequest,
9
+ IStockHistoricalRequest,
10
+ IStockIntradayRequest,
11
+ IStockBatchCurrentRequest,
12
+ IPaginatedResponse,
13
+ TSortOrder
14
+ } from '../interfaces/stockprice.js';
4
15
 
5
16
  /**
6
- * Marketstack API v2 Provider
7
- * Documentation: https://marketstack.com/documentation_v2
17
+ * Marketstack API v2 Provider - Enhanced
18
+ * Documentation: https://docs.apilayer.com/marketstack/docs/marketstack-api-v2-v-2-0-0
8
19
  *
9
20
  * Features:
10
- * - End-of-Day (EOD) stock prices
11
- * - Supports 125,000+ tickers across 72+ exchanges worldwide
21
+ * - End-of-Day (EOD) stock prices with historical data
22
+ * - Intraday pricing with multiple intervals (1min, 5min, 15min, 30min, 1hour)
23
+ * - Exchange filtering via MIC codes (XNAS, XNYS, XLON, etc.)
24
+ * - Supports 500,000+ tickers across 72+ exchanges worldwide
25
+ * - OHLCV data (Open, High, Low, Close, Volume)
26
+ * - Pagination for large datasets
12
27
  * - Requires API key authentication
13
28
  *
14
29
  * Rate Limits:
15
30
  * - Free Plan: 100 requests/month (EOD only)
16
31
  * - Basic Plan: 10,000 requests/month
17
- * - Professional Plan: 100,000 requests/month
32
+ * - Professional Plan: 100,000 requests/month (intraday access)
18
33
  *
19
- * Note: This provider returns EOD data, not real-time prices
34
+ * Phase 1 Enhancements:
35
+ * - Historical data retrieval with date ranges
36
+ * - Exchange filtering
37
+ * - OHLCV data support
38
+ * - Pagination handling
20
39
  */
21
40
  export class MarketstackProvider implements IStockProvider {
22
41
  public name = 'Marketstack';
@@ -40,11 +59,34 @@ export class MarketstackProvider implements IStockProvider {
40
59
  }
41
60
 
42
61
  /**
43
- * Fetch latest EOD price for a single ticker
62
+ * Unified data fetching method supporting all request types
44
63
  */
45
- public async fetchPrice(request: IStockQuoteRequest): Promise<IStockPrice> {
64
+ public async fetchData(request: IStockDataRequest): Promise<IStockPrice[] | IStockPrice> {
65
+ switch (request.type) {
66
+ case 'current':
67
+ return this.fetchCurrentPrice(request);
68
+ case 'historical':
69
+ return this.fetchHistoricalPrices(request);
70
+ case 'intraday':
71
+ return this.fetchIntradayPrices(request);
72
+ case 'batch':
73
+ return this.fetchBatchCurrentPrices(request);
74
+ default:
75
+ throw new Error(`Unsupported request type: ${(request as any).type}`);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Fetch current/latest EOD price for a single ticker (new API)
81
+ */
82
+ private async fetchCurrentPrice(request: IStockCurrentRequest): Promise<IStockPrice> {
46
83
  try {
47
- const url = `${this.baseUrl}/tickers/${request.ticker}/eod/latest?access_key=${this.apiKey}`;
84
+ let url = `${this.baseUrl}/tickers/${request.ticker}/eod/latest?access_key=${this.apiKey}`;
85
+
86
+ // Add exchange filter if specified
87
+ if (request.exchange) {
88
+ url += `&exchange=${request.exchange}`;
89
+ }
48
90
 
49
91
  const response = await plugins.smartrequest.SmartRequest.create()
50
92
  .url(url)
@@ -63,20 +105,101 @@ export class MarketstackProvider implements IStockProvider {
63
105
  throw new Error(`No data found for ticker ${request.ticker}`);
64
106
  }
65
107
 
66
- return this.mapToStockPrice(responseData);
108
+ return this.mapToStockPrice(responseData, 'eod');
67
109
  } 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}`);
110
+ this.logger.error(`Failed to fetch current price for ${request.ticker}:`, error);
111
+ throw new Error(`Marketstack: Failed to fetch current price for ${request.ticker}: ${error.message}`);
70
112
  }
71
113
  }
72
114
 
73
115
  /**
74
- * Fetch latest EOD prices for multiple tickers
116
+ * Fetch historical EOD prices for a ticker with date range
75
117
  */
76
- public async fetchPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]> {
118
+ private async fetchHistoricalPrices(request: IStockHistoricalRequest): Promise<IStockPrice[]> {
119
+ try {
120
+ const allPrices: IStockPrice[] = [];
121
+ let offset = request.offset || 0;
122
+ const limit = request.limit || 1000; // Max per page
123
+ const maxRecords = 10000; // Safety limit
124
+
125
+ while (true) {
126
+ let url = `${this.baseUrl}/eod?access_key=${this.apiKey}`;
127
+ url += `&symbols=${request.ticker}`;
128
+ url += `&date_from=${this.formatDate(request.from)}`;
129
+ url += `&date_to=${this.formatDate(request.to)}`;
130
+ url += `&limit=${limit}`;
131
+ url += `&offset=${offset}`;
132
+
133
+ if (request.exchange) {
134
+ url += `&exchange=${request.exchange}`;
135
+ }
136
+
137
+ if (request.sort) {
138
+ url += `&sort=${request.sort}`;
139
+ }
140
+
141
+ const response = await plugins.smartrequest.SmartRequest.create()
142
+ .url(url)
143
+ .timeout(this.config?.timeout || 15000)
144
+ .get();
145
+
146
+ const responseData = await response.json() as any;
147
+
148
+ // Check for API errors
149
+ if (responseData.error) {
150
+ throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
151
+ }
152
+
153
+ if (!responseData?.data || !Array.isArray(responseData.data)) {
154
+ throw new Error('Invalid response format from Marketstack API');
155
+ }
156
+
157
+ // Map data to stock prices
158
+ for (const data of responseData.data) {
159
+ try {
160
+ allPrices.push(this.mapToStockPrice(data, 'eod'));
161
+ } catch (error) {
162
+ this.logger.warn(`Failed to parse historical data for ${data.symbol}:`, error);
163
+ }
164
+ }
165
+
166
+ // Check if we have more pages
167
+ const pagination = responseData.pagination;
168
+ const hasMore = pagination && offset + limit < pagination.total;
169
+
170
+ // Safety check: don't fetch more than maxRecords
171
+ if (!hasMore || allPrices.length >= maxRecords) {
172
+ break;
173
+ }
174
+
175
+ offset += limit;
176
+ }
177
+
178
+ return allPrices;
179
+ } catch (error) {
180
+ this.logger.error(`Failed to fetch historical prices for ${request.ticker}:`, error);
181
+ throw new Error(`Marketstack: Failed to fetch historical prices for ${request.ticker}: ${error.message}`);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Fetch intraday prices with specified interval (Phase 2 placeholder)
187
+ */
188
+ private async fetchIntradayPrices(request: IStockIntradayRequest): Promise<IStockPrice[]> {
189
+ throw new Error('Intraday data support coming in Phase 2. For now, use EOD data with type: "current" or "historical"');
190
+ }
191
+
192
+ /**
193
+ * Fetch current prices for multiple tickers (new API)
194
+ */
195
+ private async fetchBatchCurrentPrices(request: IStockBatchCurrentRequest): Promise<IStockPrice[]> {
77
196
  try {
78
197
  const symbols = request.tickers.join(',');
79
- const url = `${this.baseUrl}/eod/latest?access_key=${this.apiKey}&symbols=${symbols}`;
198
+ let url = `${this.baseUrl}/eod/latest?access_key=${this.apiKey}&symbols=${symbols}`;
199
+
200
+ if (request.exchange) {
201
+ url += `&exchange=${request.exchange}`;
202
+ }
80
203
 
81
204
  const response = await plugins.smartrequest.SmartRequest.create()
82
205
  .url(url)
@@ -98,7 +221,7 @@ export class MarketstackProvider implements IStockProvider {
98
221
 
99
222
  for (const data of responseData.data) {
100
223
  try {
101
- prices.push(this.mapToStockPrice(data));
224
+ prices.push(this.mapToStockPrice(data, 'eod'));
102
225
  } catch (error) {
103
226
  this.logger.warn(`Failed to parse data for ${data.symbol}:`, error);
104
227
  // Continue processing other tickers
@@ -111,11 +234,35 @@ export class MarketstackProvider implements IStockProvider {
111
234
 
112
235
  return prices;
113
236
  } catch (error) {
114
- this.logger.error(`Failed to fetch batch prices:`, error);
115
- throw new Error(`Marketstack: Failed to fetch batch prices: ${error.message}`);
237
+ this.logger.error(`Failed to fetch batch current prices:`, error);
238
+ throw new Error(`Marketstack: Failed to fetch batch current prices: ${error.message}`);
116
239
  }
117
240
  }
118
241
 
242
+ /**
243
+ * Legacy: Fetch latest EOD price for a single ticker
244
+ * @deprecated Use fetchData with IStockDataRequest instead
245
+ */
246
+ public async fetchPrice(request: IStockQuoteRequest): Promise<IStockPrice> {
247
+ // Map legacy request to new format
248
+ return this.fetchCurrentPrice({
249
+ type: 'current',
250
+ ticker: request.ticker
251
+ });
252
+ }
253
+
254
+ /**
255
+ * Legacy: Fetch latest EOD prices for multiple tickers
256
+ * @deprecated Use fetchData with IStockDataRequest instead
257
+ */
258
+ public async fetchPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]> {
259
+ // Map legacy request to new format
260
+ return this.fetchBatchCurrentPrices({
261
+ type: 'batch',
262
+ tickers: request.tickers
263
+ });
264
+ }
265
+
119
266
  /**
120
267
  * Check if the Marketstack API is available and accessible
121
268
  */
@@ -165,7 +312,7 @@ export class MarketstackProvider implements IStockProvider {
165
312
  /**
166
313
  * Map Marketstack API response to IStockPrice interface
167
314
  */
168
- private mapToStockPrice(data: any): IStockPrice {
315
+ private mapToStockPrice(data: any, dataType: 'eod' | 'intraday' | 'live' = 'eod'): IStockPrice {
169
316
  if (!data.close) {
170
317
  throw new Error('Missing required price data');
171
318
  }
@@ -174,12 +321,13 @@ export class MarketstackProvider implements IStockProvider {
174
321
  // EOD data: previous close is typically open price of the same day
175
322
  // For better accuracy, we'd need previous day's close, but that requires another API call
176
323
  const currentPrice = data.close;
177
- const previousClose = data.open;
324
+ const previousClose = data.open || currentPrice;
178
325
  const change = currentPrice - previousClose;
179
326
  const changePercent = previousClose !== 0 ? (change / previousClose) * 100 : 0;
180
327
 
181
328
  // Parse timestamp
182
329
  const timestamp = data.date ? new Date(data.date) : new Date();
330
+ const fetchedAt = new Date();
183
331
 
184
332
  const stockPrice: IStockPrice = {
185
333
  ticker: data.symbol.toUpperCase(),
@@ -192,9 +340,28 @@ export class MarketstackProvider implements IStockProvider {
192
340
  provider: this.name,
193
341
  marketState: 'CLOSED', // EOD data is always for closed markets
194
342
  exchange: data.exchange,
195
- exchangeName: data.exchange_code || data.name
343
+ exchangeName: data.exchange_code || data.name,
344
+
345
+ // Phase 1 enhancements: OHLCV data
346
+ volume: data.volume,
347
+ open: data.open,
348
+ high: data.high,
349
+ low: data.low,
350
+ adjusted: data.adj_close !== undefined, // If adj_close exists, price is adjusted
351
+ dataType: dataType,
352
+ fetchedAt: fetchedAt
196
353
  };
197
354
 
198
355
  return stockPrice;
199
356
  }
357
+
358
+ /**
359
+ * Format date to YYYY-MM-DD for API requests
360
+ */
361
+ private formatDate(date: Date): string {
362
+ const year = date.getFullYear();
363
+ const month = String(date.getMonth() + 1).padStart(2, '0');
364
+ const day = String(date.getDate()).padStart(2, '0');
365
+ return `${year}-${month}-${day}`;
366
+ }
200
367
  }
@@ -52,7 +52,9 @@ export class YahooFinanceProvider implements IStockProvider {
52
52
  provider: this.name,
53
53
  marketState: this.determineMarketState(meta),
54
54
  exchange: meta.exchange,
55
- exchangeName: meta.exchangeName
55
+ exchangeName: meta.exchangeName,
56
+ dataType: 'live', // Yahoo provides real-time/near real-time data
57
+ fetchedAt: new Date()
56
58
  };
57
59
 
58
60
  return stockPrice;
@@ -101,7 +103,9 @@ export class YahooFinanceProvider implements IStockProvider {
101
103
  provider: this.name,
102
104
  marketState: sparkData.marketState || 'REGULAR',
103
105
  exchange: sparkData.exchange,
104
- exchangeName: sparkData.exchangeName
106
+ exchangeName: sparkData.exchangeName,
107
+ dataType: 'live', // Yahoo provides real-time/near real-time data
108
+ fetchedAt: new Date()
105
109
  });
106
110
  }
107
111