@fin.cx/opendata 2.0.0 → 3.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.
@@ -1,16 +1,15 @@
1
- import type { IStockPrice, IStockQuoteRequest, IStockBatchQuoteRequest } from './stockprice.js';
1
+ import type { IStockPrice, IStockDataRequest } from './stockprice.js';
2
2
 
3
3
  export interface IStockProvider {
4
4
  name: string;
5
5
  priority: number;
6
-
7
- fetchPrice(request: IStockQuoteRequest): Promise<IStockPrice>;
8
- fetchPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]>;
6
+
7
+ fetchData(request: IStockDataRequest): Promise<IStockPrice | IStockPrice[]>;
9
8
  isAvailable(): Promise<boolean>;
10
-
9
+
11
10
  supportsMarket?(market: string): boolean;
12
11
  supportsTicker?(ticker: string): boolean;
13
-
12
+
14
13
  readonly requiresAuth: boolean;
15
14
  readonly rateLimit?: {
16
15
  requestsPerMinute: number;
@@ -1,5 +1,4 @@
1
- import * as plugins from '../../plugins.js';
2
-
1
+ // Enhanced stock price interface with additional OHLCV data
3
2
  export interface IStockPrice {
4
3
  ticker: string;
5
4
  price: number;
@@ -12,11 +11,20 @@ export interface IStockPrice {
12
11
  marketState: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED';
13
12
  exchange?: string;
14
13
  exchangeName?: string;
14
+
15
+ // Phase 1 enhancements
16
+ volume?: number; // Trading volume
17
+ open?: number; // Opening price
18
+ high?: number; // Day high
19
+ low?: number; // Day low
20
+ adjusted?: boolean; // If price is split/dividend adjusted
21
+ dataType: 'eod' | 'intraday' | 'live'; // What kind of data this is
22
+ fetchedAt: Date; // When we fetched (vs data timestamp)
23
+
24
+ // Company identification
25
+ companyName?: string; // Company name (e.g., "Apple Inc.")
26
+ companyFullName?: string; // Full company name with exchange (e.g., "Apple Inc. (NASDAQ:AAPL)")
15
27
  }
16
- type CheckStockPrice = plugins.tsclass.typeFest.IsEqual<
17
- IStockPrice,
18
- plugins.tsclass.finance.IStockPrice
19
- >;
20
28
 
21
29
  export interface IStockPriceError {
22
30
  ticker: string;
@@ -25,12 +33,62 @@ export interface IStockPriceError {
25
33
  timestamp: Date;
26
34
  }
27
35
 
28
- export interface IStockQuoteRequest {
36
+ // Pagination support for large datasets
37
+ export interface IPaginatedResponse<T> {
38
+ data: T[];
39
+ pagination: {
40
+ currentPage: number;
41
+ totalPages: number;
42
+ totalRecords: number;
43
+ hasMore: boolean;
44
+ limit: number;
45
+ offset: number;
46
+ };
47
+ }
48
+
49
+ // Phase 1: Discriminated union types for different request types
50
+ export type TIntervalType = '1min' | '5min' | '10min' | '15min' | '30min' | '1hour';
51
+ export type TSortOrder = 'ASC' | 'DESC';
52
+
53
+ // Current price request (latest EOD or live)
54
+ export interface IStockCurrentRequest {
55
+ type: 'current';
56
+ ticker: string;
57
+ exchange?: string; // MIC code like 'XNAS', 'XNYS', 'XLON'
58
+ }
59
+
60
+ // Historical price request (date range)
61
+ export interface IStockHistoricalRequest {
62
+ type: 'historical';
63
+ ticker: string;
64
+ from: Date;
65
+ to: Date;
66
+ exchange?: string;
67
+ sort?: TSortOrder;
68
+ limit?: number; // Max results per page (default 1000)
69
+ offset?: number; // For pagination
70
+ }
71
+
72
+ // Intraday price request (real-time intervals)
73
+ export interface IStockIntradayRequest {
74
+ type: 'intraday';
29
75
  ticker: string;
30
- includeExtendedHours?: boolean;
76
+ interval: TIntervalType;
77
+ exchange?: string;
78
+ limit?: number; // Number of bars to return
79
+ date?: Date; // Specific date for historical intraday
31
80
  }
32
81
 
33
- export interface IStockBatchQuoteRequest {
82
+ // Batch current prices request
83
+ export interface IStockBatchCurrentRequest {
84
+ type: 'batch';
34
85
  tickers: string[];
35
- includeExtendedHours?: boolean;
86
+ exchange?: string;
36
87
  }
88
+
89
+ // Union type for all stock data requests
90
+ export type IStockDataRequest =
91
+ | IStockCurrentRequest
92
+ | IStockHistoricalRequest
93
+ | IStockIntradayRequest
94
+ | IStockBatchCurrentRequest;
@@ -1,22 +1,37 @@
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
+ IStockDataRequest,
6
+ IStockCurrentRequest,
7
+ IStockHistoricalRequest,
8
+ IStockIntradayRequest,
9
+ IStockBatchCurrentRequest
10
+ } from '../interfaces/stockprice.js';
4
11
 
5
12
  /**
6
- * Marketstack API v2 Provider
7
- * Documentation: https://marketstack.com/documentation_v2
13
+ * Marketstack API v2 Provider - Enhanced
14
+ * Documentation: https://docs.apilayer.com/marketstack/docs/marketstack-api-v2-v-2-0-0
8
15
  *
9
16
  * Features:
10
- * - End-of-Day (EOD) stock prices
11
- * - Supports 125,000+ tickers across 72+ exchanges worldwide
17
+ * - End-of-Day (EOD) stock prices with historical data
18
+ * - Intraday pricing with multiple intervals (1min, 5min, 15min, 30min, 1hour)
19
+ * - Exchange filtering via MIC codes (XNAS, XNYS, XLON, etc.)
20
+ * - Supports 500,000+ tickers across 72+ exchanges worldwide
21
+ * - OHLCV data (Open, High, Low, Close, Volume)
22
+ * - Pagination for large datasets
12
23
  * - Requires API key authentication
13
24
  *
14
25
  * Rate Limits:
15
26
  * - Free Plan: 100 requests/month (EOD only)
16
27
  * - Basic Plan: 10,000 requests/month
17
- * - Professional Plan: 100,000 requests/month
28
+ * - Professional Plan: 100,000 requests/month (intraday access)
18
29
  *
19
- * Note: This provider returns EOD data, not real-time prices
30
+ * Phase 1 Enhancements:
31
+ * - Historical data retrieval with date ranges
32
+ * - Exchange filtering
33
+ * - OHLCV data support
34
+ * - Pagination handling
20
35
  */
21
36
  export class MarketstackProvider implements IStockProvider {
22
37
  public name = 'Marketstack';
@@ -40,11 +55,34 @@ export class MarketstackProvider implements IStockProvider {
40
55
  }
41
56
 
42
57
  /**
43
- * Fetch latest EOD price for a single ticker
58
+ * Unified data fetching method supporting all request types
44
59
  */
45
- public async fetchPrice(request: IStockQuoteRequest): Promise<IStockPrice> {
60
+ public async fetchData(request: IStockDataRequest): Promise<IStockPrice[] | IStockPrice> {
61
+ switch (request.type) {
62
+ case 'current':
63
+ return this.fetchCurrentPrice(request);
64
+ case 'historical':
65
+ return this.fetchHistoricalPrices(request);
66
+ case 'intraday':
67
+ return this.fetchIntradayPrices(request);
68
+ case 'batch':
69
+ return this.fetchBatchCurrentPrices(request);
70
+ default:
71
+ throw new Error(`Unsupported request type: ${(request as any).type}`);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Fetch current/latest EOD price for a single ticker (new API)
77
+ */
78
+ private async fetchCurrentPrice(request: IStockCurrentRequest): Promise<IStockPrice> {
46
79
  try {
47
- const url = `${this.baseUrl}/tickers/${request.ticker}/eod/latest?access_key=${this.apiKey}`;
80
+ let url = `${this.baseUrl}/tickers/${request.ticker}/eod/latest?access_key=${this.apiKey}`;
81
+
82
+ // Add exchange filter if specified
83
+ if (request.exchange) {
84
+ url += `&exchange=${request.exchange}`;
85
+ }
48
86
 
49
87
  const response = await plugins.smartrequest.SmartRequest.create()
50
88
  .url(url)
@@ -63,20 +101,101 @@ export class MarketstackProvider implements IStockProvider {
63
101
  throw new Error(`No data found for ticker ${request.ticker}`);
64
102
  }
65
103
 
66
- return this.mapToStockPrice(responseData);
104
+ return this.mapToStockPrice(responseData, 'eod');
105
+ } catch (error) {
106
+ this.logger.error(`Failed to fetch current price for ${request.ticker}:`, error);
107
+ throw new Error(`Marketstack: Failed to fetch current price for ${request.ticker}: ${error.message}`);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Fetch historical EOD prices for a ticker with date range
113
+ */
114
+ private async fetchHistoricalPrices(request: IStockHistoricalRequest): Promise<IStockPrice[]> {
115
+ try {
116
+ const allPrices: IStockPrice[] = [];
117
+ let offset = request.offset || 0;
118
+ const limit = request.limit || 1000; // Max per page
119
+ const maxRecords = 10000; // Safety limit
120
+
121
+ while (true) {
122
+ let url = `${this.baseUrl}/eod?access_key=${this.apiKey}`;
123
+ url += `&symbols=${request.ticker}`;
124
+ url += `&date_from=${this.formatDate(request.from)}`;
125
+ url += `&date_to=${this.formatDate(request.to)}`;
126
+ url += `&limit=${limit}`;
127
+ url += `&offset=${offset}`;
128
+
129
+ if (request.exchange) {
130
+ url += `&exchange=${request.exchange}`;
131
+ }
132
+
133
+ if (request.sort) {
134
+ url += `&sort=${request.sort}`;
135
+ }
136
+
137
+ const response = await plugins.smartrequest.SmartRequest.create()
138
+ .url(url)
139
+ .timeout(this.config?.timeout || 15000)
140
+ .get();
141
+
142
+ const responseData = await response.json() as any;
143
+
144
+ // Check for API errors
145
+ if (responseData.error) {
146
+ throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
147
+ }
148
+
149
+ if (!responseData?.data || !Array.isArray(responseData.data)) {
150
+ throw new Error('Invalid response format from Marketstack API');
151
+ }
152
+
153
+ // Map data to stock prices
154
+ for (const data of responseData.data) {
155
+ try {
156
+ allPrices.push(this.mapToStockPrice(data, 'eod'));
157
+ } catch (error) {
158
+ this.logger.warn(`Failed to parse historical data for ${data.symbol}:`, error);
159
+ }
160
+ }
161
+
162
+ // Check if we have more pages
163
+ const pagination = responseData.pagination;
164
+ const hasMore = pagination && offset + limit < pagination.total;
165
+
166
+ // Safety check: don't fetch more than maxRecords
167
+ if (!hasMore || allPrices.length >= maxRecords) {
168
+ break;
169
+ }
170
+
171
+ offset += limit;
172
+ }
173
+
174
+ return allPrices;
67
175
  } 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}`);
176
+ this.logger.error(`Failed to fetch historical prices for ${request.ticker}:`, error);
177
+ throw new Error(`Marketstack: Failed to fetch historical prices for ${request.ticker}: ${error.message}`);
70
178
  }
71
179
  }
72
180
 
73
181
  /**
74
- * Fetch latest EOD prices for multiple tickers
182
+ * Fetch intraday prices with specified interval (Phase 2 placeholder)
183
+ */
184
+ private async fetchIntradayPrices(request: IStockIntradayRequest): Promise<IStockPrice[]> {
185
+ throw new Error('Intraday data support coming in Phase 2. For now, use EOD data with type: "current" or "historical"');
186
+ }
187
+
188
+ /**
189
+ * Fetch current prices for multiple tickers (new API)
75
190
  */
76
- public async fetchPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]> {
191
+ private async fetchBatchCurrentPrices(request: IStockBatchCurrentRequest): Promise<IStockPrice[]> {
77
192
  try {
78
193
  const symbols = request.tickers.join(',');
79
- const url = `${this.baseUrl}/eod/latest?access_key=${this.apiKey}&symbols=${symbols}`;
194
+ let url = `${this.baseUrl}/eod/latest?access_key=${this.apiKey}&symbols=${symbols}`;
195
+
196
+ if (request.exchange) {
197
+ url += `&exchange=${request.exchange}`;
198
+ }
80
199
 
81
200
  const response = await plugins.smartrequest.SmartRequest.create()
82
201
  .url(url)
@@ -98,7 +217,7 @@ export class MarketstackProvider implements IStockProvider {
98
217
 
99
218
  for (const data of responseData.data) {
100
219
  try {
101
- prices.push(this.mapToStockPrice(data));
220
+ prices.push(this.mapToStockPrice(data, 'eod'));
102
221
  } catch (error) {
103
222
  this.logger.warn(`Failed to parse data for ${data.symbol}:`, error);
104
223
  // Continue processing other tickers
@@ -111,8 +230,8 @@ export class MarketstackProvider implements IStockProvider {
111
230
 
112
231
  return prices;
113
232
  } catch (error) {
114
- this.logger.error(`Failed to fetch batch prices:`, error);
115
- throw new Error(`Marketstack: Failed to fetch batch prices: ${error.message}`);
233
+ this.logger.error(`Failed to fetch batch current prices:`, error);
234
+ throw new Error(`Marketstack: Failed to fetch batch current prices: ${error.message}`);
116
235
  }
117
236
  }
118
237
 
@@ -165,7 +284,7 @@ export class MarketstackProvider implements IStockProvider {
165
284
  /**
166
285
  * Map Marketstack API response to IStockPrice interface
167
286
  */
168
- private mapToStockPrice(data: any): IStockPrice {
287
+ private mapToStockPrice(data: any, dataType: 'eod' | 'intraday' | 'live' = 'eod'): IStockPrice {
169
288
  if (!data.close) {
170
289
  throw new Error('Missing required price data');
171
290
  }
@@ -174,12 +293,13 @@ export class MarketstackProvider implements IStockProvider {
174
293
  // EOD data: previous close is typically open price of the same day
175
294
  // For better accuracy, we'd need previous day's close, but that requires another API call
176
295
  const currentPrice = data.close;
177
- const previousClose = data.open;
296
+ const previousClose = data.open || currentPrice;
178
297
  const change = currentPrice - previousClose;
179
298
  const changePercent = previousClose !== 0 ? (change / previousClose) * 100 : 0;
180
299
 
181
300
  // Parse timestamp
182
301
  const timestamp = data.date ? new Date(data.date) : new Date();
302
+ const fetchedAt = new Date();
183
303
 
184
304
  const stockPrice: IStockPrice = {
185
305
  ticker: data.symbol.toUpperCase(),
@@ -192,9 +312,65 @@ export class MarketstackProvider implements IStockProvider {
192
312
  provider: this.name,
193
313
  marketState: 'CLOSED', // EOD data is always for closed markets
194
314
  exchange: data.exchange,
195
- exchangeName: data.exchange_code || data.name
315
+ exchangeName: data.exchange_code || data.name,
316
+
317
+ // Phase 1 enhancements: OHLCV data
318
+ volume: data.volume,
319
+ open: data.open,
320
+ high: data.high,
321
+ low: data.low,
322
+ adjusted: data.adj_close !== undefined, // If adj_close exists, price is adjusted
323
+ dataType: dataType,
324
+ fetchedAt: fetchedAt,
325
+
326
+ // Company identification
327
+ companyName: data.company_name || data.name || undefined,
328
+ companyFullName: this.buildCompanyFullName(data)
196
329
  };
197
330
 
198
331
  return stockPrice;
199
332
  }
333
+
334
+ /**
335
+ * Build full company name with exchange and ticker information
336
+ * Example: "Apple Inc (NASDAQ:AAPL)"
337
+ */
338
+ private buildCompanyFullName(data: any): string | undefined {
339
+ // Check if API already provides full name
340
+ if (data.full_name || data.long_name) {
341
+ return data.full_name || data.long_name;
342
+ }
343
+
344
+ // Build from available data
345
+ const companyName = data.company_name || data.name;
346
+ const exchangeCode = data.exchange_code; // e.g., "NASDAQ"
347
+ const symbol = data.symbol; // e.g., "AAPL"
348
+
349
+ if (!companyName) {
350
+ return undefined;
351
+ }
352
+
353
+ // If we have exchange and symbol, build full name: "Apple Inc (NASDAQ:AAPL)"
354
+ if (exchangeCode && symbol) {
355
+ return `${companyName} (${exchangeCode}:${symbol})`;
356
+ }
357
+
358
+ // If we only have symbol: "Apple Inc (AAPL)"
359
+ if (symbol) {
360
+ return `${companyName} (${symbol})`;
361
+ }
362
+
363
+ // Otherwise just return company name
364
+ return companyName;
365
+ }
366
+
367
+ /**
368
+ * Format date to YYYY-MM-DD for API requests
369
+ */
370
+ private formatDate(date: Date): string {
371
+ const year = date.getFullYear();
372
+ const month = String(date.getMonth() + 1).padStart(2, '0');
373
+ const day = String(date.getDate()).padStart(2, '0');
374
+ return `${year}-${month}-${day}`;
375
+ }
200
376
  }
@@ -1,6 +1,11 @@
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
+ IStockDataRequest,
6
+ IStockCurrentRequest,
7
+ IStockBatchCurrentRequest
8
+ } from '../interfaces/stockprice.js';
4
9
 
5
10
  export class YahooFinanceProvider implements IStockProvider {
6
11
  public name = 'Yahoo Finance';
@@ -17,7 +22,28 @@ export class YahooFinanceProvider implements IStockProvider {
17
22
 
18
23
  constructor(private config?: IProviderConfig) {}
19
24
 
20
- public async fetchPrice(request: IStockQuoteRequest): Promise<IStockPrice> {
25
+ /**
26
+ * Unified data fetching method
27
+ */
28
+ public async fetchData(request: IStockDataRequest): Promise<IStockPrice | IStockPrice[]> {
29
+ switch (request.type) {
30
+ case 'current':
31
+ return this.fetchCurrentPrice(request);
32
+ case 'batch':
33
+ return this.fetchBatchCurrentPrices(request);
34
+ case 'historical':
35
+ throw new Error('Yahoo Finance provider does not support historical data. Use Marketstack provider instead.');
36
+ case 'intraday':
37
+ throw new Error('Yahoo Finance provider does not support intraday data yet. Use Marketstack provider instead.');
38
+ default:
39
+ throw new Error(`Unsupported request type: ${(request as any).type}`);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Fetch current price for a single ticker
45
+ */
46
+ private async fetchCurrentPrice(request: IStockCurrentRequest): Promise<IStockPrice> {
21
47
  try {
22
48
  const url = `${this.baseUrl}/v8/finance/chart/${request.ticker}`;
23
49
  const response = await plugins.smartrequest.SmartRequest.create()
@@ -52,7 +78,9 @@ export class YahooFinanceProvider implements IStockProvider {
52
78
  provider: this.name,
53
79
  marketState: this.determineMarketState(meta),
54
80
  exchange: meta.exchange,
55
- exchangeName: meta.exchangeName
81
+ exchangeName: meta.exchangeName,
82
+ dataType: 'live', // Yahoo provides real-time/near real-time data
83
+ fetchedAt: new Date()
56
84
  };
57
85
 
58
86
  return stockPrice;
@@ -62,7 +90,10 @@ export class YahooFinanceProvider implements IStockProvider {
62
90
  }
63
91
  }
64
92
 
65
- public async fetchPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]> {
93
+ /**
94
+ * Fetch batch current prices
95
+ */
96
+ private async fetchBatchCurrentPrices(request: IStockBatchCurrentRequest): Promise<IStockPrice[]> {
66
97
  try {
67
98
  const symbols = request.tickers.join(',');
68
99
  const url = `${this.baseUrl}/v8/finance/spark?symbols=${symbols}&range=1d&interval=5m`;
@@ -101,7 +132,9 @@ export class YahooFinanceProvider implements IStockProvider {
101
132
  provider: this.name,
102
133
  marketState: sparkData.marketState || 'REGULAR',
103
134
  exchange: sparkData.exchange,
104
- exchangeName: sparkData.exchangeName
135
+ exchangeName: sparkData.exchangeName,
136
+ dataType: 'live', // Yahoo provides real-time/near real-time data
137
+ fetchedAt: new Date()
105
138
  });
106
139
  }
107
140
 
@@ -119,7 +152,7 @@ export class YahooFinanceProvider implements IStockProvider {
119
152
  public async isAvailable(): Promise<boolean> {
120
153
  try {
121
154
  // Test with a well-known ticker
122
- await this.fetchPrice({ ticker: 'AAPL' });
155
+ await this.fetchData({ type: 'current', ticker: 'AAPL' });
123
156
  return true;
124
157
  } catch (error) {
125
158
  console.warn('Yahoo Finance provider is not available:', error);