@fin.cx/opendata 3.3.0 → 3.5.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/stocks/classes.stockservice.d.ts +9 -0
- package/dist_ts/stocks/classes.stockservice.js +126 -6
- package/dist_ts/stocks/interfaces/provider.d.ts +2 -0
- package/dist_ts/stocks/providers/provider.coingecko.js +34 -9
- package/dist_ts/stocks/providers/provider.marketstack.d.ts +73 -12
- package/dist_ts/stocks/providers/provider.marketstack.js +344 -64
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/stocks/classes.stockservice.ts +157 -5
- package/ts/stocks/interfaces/provider.ts +2 -0
- package/ts/stocks/providers/provider.coingecko.ts +42 -8
- package/ts/stocks/providers/provider.marketstack.ts +398 -70
|
@@ -10,32 +10,39 @@ import type {
|
|
|
10
10
|
} from '../interfaces/stockprice.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Marketstack API v2 Provider -
|
|
13
|
+
* Marketstack API v2 Provider - Professional Plan with Intelligent Intraday Support
|
|
14
14
|
* Documentation: https://docs.apilayer.com/marketstack/docs/marketstack-api-v2-v-2-0-0
|
|
15
15
|
*
|
|
16
16
|
* Features:
|
|
17
|
+
* - Intelligent endpoint selection based on market hours
|
|
18
|
+
* - Real-time intraday pricing with multiple intervals (1min, 5min, 10min, 15min, 30min, 1hour)
|
|
17
19
|
* - End-of-Day (EOD) stock prices with historical data
|
|
18
|
-
* -
|
|
20
|
+
* - Market state detection (PRE, REGULAR, POST, CLOSED)
|
|
19
21
|
* - Exchange filtering via MIC codes (XNAS, XNYS, XLON, etc.)
|
|
20
22
|
* - Supports 500,000+ tickers across 72+ exchanges worldwide
|
|
21
23
|
* - OHLCV data (Open, High, Low, Close, Volume)
|
|
22
24
|
* - Pagination for large datasets
|
|
25
|
+
* - Automatic fallback from intraday to EOD on errors
|
|
23
26
|
* - Requires API key authentication
|
|
24
27
|
*
|
|
28
|
+
* Intelligent Endpoint Selection:
|
|
29
|
+
* - During market hours (PRE/REGULAR/POST): Uses intraday endpoints for fresh data
|
|
30
|
+
* - After market close (CLOSED): Uses EOD endpoints to save API credits
|
|
31
|
+
* - Automatic fallback to EOD if intraday fails (rate limits, plan restrictions, etc.)
|
|
32
|
+
*
|
|
25
33
|
* Rate Limits:
|
|
26
34
|
* - Free Plan: 100 requests/month (EOD only)
|
|
27
|
-
* - Basic Plan: 10,000 requests/month
|
|
28
|
-
* - Professional Plan: 100,000 requests/month (intraday
|
|
35
|
+
* - Basic Plan: 10,000 requests/month (EOD only)
|
|
36
|
+
* - Professional Plan: 100,000 requests/month (intraday + EOD)
|
|
29
37
|
*
|
|
30
|
-
*
|
|
31
|
-
* -
|
|
32
|
-
* - Exchange
|
|
33
|
-
* -
|
|
34
|
-
* - Pagination handling
|
|
38
|
+
* Intraday Access:
|
|
39
|
+
* - Intervals below 15min (1min, 5min, 10min) require Professional Plan or higher
|
|
40
|
+
* - Real-time data from IEX Exchange for US tickers
|
|
41
|
+
* - Symbol formatting: Periods replaced with hyphens for intraday (BRK.B → BRK-B)
|
|
35
42
|
*/
|
|
36
43
|
export class MarketstackProvider implements IStockProvider {
|
|
37
44
|
public name = 'Marketstack';
|
|
38
|
-
public priority =
|
|
45
|
+
public priority = 90; // Increased from 80 - now supports real-time intraday data during market hours
|
|
39
46
|
public readonly requiresAuth = true;
|
|
40
47
|
public readonly rateLimit = {
|
|
41
48
|
requestsPerMinute: undefined, // No per-minute limit specified
|
|
@@ -73,39 +80,103 @@ export class MarketstackProvider implements IStockProvider {
|
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
/**
|
|
76
|
-
* Fetch current/latest
|
|
83
|
+
* Fetch current/latest price with intelligent endpoint selection
|
|
84
|
+
* Uses intraday during market hours (PRE, REGULAR, POST) for fresh data
|
|
85
|
+
* Uses EOD after market close (CLOSED) to save API credits
|
|
77
86
|
*/
|
|
78
87
|
private async fetchCurrentPrice(request: IStockCurrentRequest): Promise<IStockPrice> {
|
|
79
88
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
// Determine current market state
|
|
90
|
+
const marketState = this.getUsMarketState();
|
|
91
|
+
const useIntraday = this.shouldUseIntradayEndpoint(marketState);
|
|
92
|
+
|
|
93
|
+
if (useIntraday) {
|
|
94
|
+
// Use intraday endpoint for fresh data during market hours
|
|
95
|
+
return await this.fetchCurrentPriceIntraday(request, marketState);
|
|
96
|
+
} else {
|
|
97
|
+
// Use EOD endpoint for after-close data
|
|
98
|
+
return await this.fetchCurrentPriceEod(request);
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
// If intraday fails, fallback to EOD with warning
|
|
102
|
+
if (error.message?.includes('intraday') || error.message?.includes('Marketstack API error')) {
|
|
103
|
+
this.logger.warn(`Intraday endpoint failed for ${request.ticker}, falling back to EOD:`, error.message);
|
|
104
|
+
try {
|
|
105
|
+
return await this.fetchCurrentPriceEod(request);
|
|
106
|
+
} catch (eodError) {
|
|
107
|
+
// Both failed, throw original error
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
85
110
|
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
86
114
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Fetch current price using intraday endpoint (during market hours)
|
|
117
|
+
* Uses 1min interval for most recent data
|
|
118
|
+
*/
|
|
119
|
+
private async fetchCurrentPriceIntraday(
|
|
120
|
+
request: IStockCurrentRequest,
|
|
121
|
+
marketState: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED'
|
|
122
|
+
): Promise<IStockPrice> {
|
|
123
|
+
const formattedSymbol = this.formatSymbolForIntraday(request.ticker);
|
|
124
|
+
let url = `${this.baseUrl}/tickers/${formattedSymbol}/intraday/latest?access_key=${this.apiKey}`;
|
|
125
|
+
url += `&interval=1min`; // Use 1min for most recent data
|
|
126
|
+
|
|
127
|
+
// Add exchange filter if specified
|
|
128
|
+
if (request.exchange) {
|
|
129
|
+
url += `&exchange=${request.exchange}`;
|
|
130
|
+
}
|
|
91
131
|
|
|
92
|
-
|
|
132
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
133
|
+
.url(url)
|
|
134
|
+
.timeout(this.config?.timeout || 10000)
|
|
135
|
+
.get();
|
|
93
136
|
|
|
94
|
-
|
|
95
|
-
if (responseData.error) {
|
|
96
|
-
throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
|
|
97
|
-
}
|
|
137
|
+
const responseData = await response.json() as any;
|
|
98
138
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
139
|
+
// Check for API errors
|
|
140
|
+
if (responseData.error) {
|
|
141
|
+
throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
|
|
142
|
+
}
|
|
103
143
|
|
|
104
|
-
|
|
105
|
-
|
|
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}`);
|
|
144
|
+
if (!responseData || !responseData.close) {
|
|
145
|
+
throw new Error(`No intraday data found for ticker ${request.ticker}`);
|
|
108
146
|
}
|
|
147
|
+
|
|
148
|
+
return this.mapToStockPrice(responseData, 'intraday', marketState);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Fetch current price using EOD endpoint (after market close)
|
|
153
|
+
*/
|
|
154
|
+
private async fetchCurrentPriceEod(request: IStockCurrentRequest): Promise<IStockPrice> {
|
|
155
|
+
let url = `${this.baseUrl}/tickers/${request.ticker}/eod/latest?access_key=${this.apiKey}`;
|
|
156
|
+
|
|
157
|
+
// Add exchange filter if specified
|
|
158
|
+
if (request.exchange) {
|
|
159
|
+
url += `&exchange=${request.exchange}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
163
|
+
.url(url)
|
|
164
|
+
.timeout(this.config?.timeout || 10000)
|
|
165
|
+
.get();
|
|
166
|
+
|
|
167
|
+
const responseData = await response.json() as any;
|
|
168
|
+
|
|
169
|
+
// Check for API errors
|
|
170
|
+
if (responseData.error) {
|
|
171
|
+
throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// For single ticker endpoint, response is direct object (not wrapped in data field)
|
|
175
|
+
if (!responseData || !responseData.close) {
|
|
176
|
+
throw new Error(`No data found for ticker ${request.ticker}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return this.mapToStockPrice(responseData, 'eod');
|
|
109
180
|
}
|
|
110
181
|
|
|
111
182
|
/**
|
|
@@ -116,7 +187,7 @@ export class MarketstackProvider implements IStockProvider {
|
|
|
116
187
|
const allPrices: IStockPrice[] = [];
|
|
117
188
|
let offset = request.offset || 0;
|
|
118
189
|
const limit = request.limit || 1000; // Max per page
|
|
119
|
-
const maxRecords = 10000; // Safety limit
|
|
190
|
+
const maxRecords = this.config?.maxRecords || 10000; // Safety limit (configurable)
|
|
120
191
|
|
|
121
192
|
while (true) {
|
|
122
193
|
let url = `${this.baseUrl}/eod?access_key=${this.apiKey}`;
|
|
@@ -179,60 +250,221 @@ export class MarketstackProvider implements IStockProvider {
|
|
|
179
250
|
}
|
|
180
251
|
|
|
181
252
|
/**
|
|
182
|
-
* Fetch intraday prices with specified interval
|
|
253
|
+
* Fetch intraday prices with specified interval
|
|
254
|
+
* Supports intervals: 1min, 5min, 10min, 15min, 30min, 1hour
|
|
255
|
+
* Note: Intervals below 15min require Professional Plan or higher
|
|
183
256
|
*/
|
|
184
257
|
private async fetchIntradayPrices(request: IStockIntradayRequest): Promise<IStockPrice[]> {
|
|
185
|
-
|
|
258
|
+
try {
|
|
259
|
+
const allPrices: IStockPrice[] = [];
|
|
260
|
+
let offset = 0;
|
|
261
|
+
const limit = 1000; // Max per page for intraday
|
|
262
|
+
const maxRecords = this.config?.maxRecords || 10000; // Safety limit (configurable)
|
|
263
|
+
|
|
264
|
+
// Apply default limit if user didn't specify one (performance optimization)
|
|
265
|
+
const effectiveLimit = request.limit || this.config?.defaultIntradayLimit || 1000;
|
|
266
|
+
|
|
267
|
+
// Warn if fetching large amount of data without explicit limit
|
|
268
|
+
if (!request.limit && effectiveLimit > 1000) {
|
|
269
|
+
this.logger.warn(
|
|
270
|
+
`Intraday request for ${request.ticker} without explicit limit will fetch up to ${effectiveLimit} records. ` +
|
|
271
|
+
`Consider adding a limit to the request for better performance.`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Format symbol for intraday endpoint (replace . with -)
|
|
276
|
+
const formattedSymbol = this.formatSymbolForIntraday(request.ticker);
|
|
277
|
+
|
|
278
|
+
while (true) {
|
|
279
|
+
let url = `${this.baseUrl}/tickers/${formattedSymbol}/intraday?access_key=${this.apiKey}`;
|
|
280
|
+
url += `&interval=${request.interval}`;
|
|
281
|
+
url += `&limit=${limit}`;
|
|
282
|
+
url += `&offset=${offset}`;
|
|
283
|
+
|
|
284
|
+
// Add date filter if specified
|
|
285
|
+
if (request.date) {
|
|
286
|
+
url += `&date_from=${this.formatDate(request.date)}`;
|
|
287
|
+
url += `&date_to=${this.formatDate(request.date)}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Add exchange filter if specified
|
|
291
|
+
if (request.exchange) {
|
|
292
|
+
url += `&exchange=${request.exchange}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
296
|
+
.url(url)
|
|
297
|
+
.timeout(this.config?.timeout || 15000)
|
|
298
|
+
.get();
|
|
299
|
+
|
|
300
|
+
const responseData = await response.json() as any;
|
|
301
|
+
|
|
302
|
+
// Check for API errors
|
|
303
|
+
if (responseData.error) {
|
|
304
|
+
throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!responseData?.data || !Array.isArray(responseData.data)) {
|
|
308
|
+
throw new Error('Invalid response format from Marketstack API');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Map data to stock prices
|
|
312
|
+
for (const data of responseData.data) {
|
|
313
|
+
try {
|
|
314
|
+
allPrices.push(this.mapToStockPrice(data, 'intraday'));
|
|
315
|
+
} catch (error) {
|
|
316
|
+
this.logger.warn(`Failed to parse intraday data for ${data.symbol}:`, error);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check if we have more pages
|
|
321
|
+
const pagination = responseData.pagination;
|
|
322
|
+
const hasMore = pagination && offset + limit < pagination.total;
|
|
323
|
+
|
|
324
|
+
// Honor effective limit or safety maxRecords
|
|
325
|
+
if (!hasMore || allPrices.length >= effectiveLimit || allPrices.length >= maxRecords) {
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
offset += limit;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Apply effective limit
|
|
333
|
+
if (allPrices.length > effectiveLimit) {
|
|
334
|
+
return allPrices.slice(0, effectiveLimit);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return allPrices;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
this.logger.error(`Failed to fetch intraday prices for ${request.ticker}:`, error);
|
|
340
|
+
throw new Error(`Marketstack: Failed to fetch intraday prices for ${request.ticker}: ${error.message}`);
|
|
341
|
+
}
|
|
186
342
|
}
|
|
187
343
|
|
|
188
344
|
/**
|
|
189
|
-
* Fetch current prices for multiple tickers
|
|
345
|
+
* Fetch current prices for multiple tickers with intelligent endpoint selection
|
|
346
|
+
* Uses intraday during market hours (PRE, REGULAR, POST) for fresh data
|
|
347
|
+
* Uses EOD after market close (CLOSED) to save API credits
|
|
190
348
|
*/
|
|
191
349
|
private async fetchBatchCurrentPrices(request: IStockBatchCurrentRequest): Promise<IStockPrice[]> {
|
|
192
350
|
try {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
351
|
+
// Determine current market state
|
|
352
|
+
const marketState = this.getUsMarketState();
|
|
353
|
+
const useIntraday = this.shouldUseIntradayEndpoint(marketState);
|
|
354
|
+
|
|
355
|
+
if (useIntraday) {
|
|
356
|
+
return await this.fetchBatchCurrentPricesIntraday(request, marketState);
|
|
357
|
+
} else {
|
|
358
|
+
return await this.fetchBatchCurrentPricesEod(request);
|
|
198
359
|
}
|
|
360
|
+
} catch (error) {
|
|
361
|
+
// Fallback to EOD if intraday fails
|
|
362
|
+
if (error.message?.includes('intraday') || error.message?.includes('Marketstack API error')) {
|
|
363
|
+
this.logger.warn(`Intraday batch endpoint failed, falling back to EOD:`, error.message);
|
|
364
|
+
try {
|
|
365
|
+
return await this.fetchBatchCurrentPricesEod(request);
|
|
366
|
+
} catch (eodError) {
|
|
367
|
+
// Both failed, throw original error
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
199
374
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Fetch batch current prices using intraday endpoint
|
|
377
|
+
*/
|
|
378
|
+
private async fetchBatchCurrentPricesIntraday(
|
|
379
|
+
request: IStockBatchCurrentRequest,
|
|
380
|
+
marketState: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED'
|
|
381
|
+
): Promise<IStockPrice[]> {
|
|
382
|
+
// Format symbols for intraday (replace . with -)
|
|
383
|
+
const formattedSymbols = request.tickers.map(t => this.formatSymbolForIntraday(t)).join(',');
|
|
384
|
+
|
|
385
|
+
let url = `${this.baseUrl}/intraday/latest?access_key=${this.apiKey}`;
|
|
386
|
+
url += `&symbols=${formattedSymbols}`;
|
|
387
|
+
url += `&interval=1min`; // Use 1min for most recent data
|
|
388
|
+
|
|
389
|
+
if (request.exchange) {
|
|
390
|
+
url += `&exchange=${request.exchange}`;
|
|
391
|
+
}
|
|
204
392
|
|
|
205
|
-
|
|
393
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
394
|
+
.url(url)
|
|
395
|
+
.timeout(this.config?.timeout || 15000)
|
|
396
|
+
.get();
|
|
206
397
|
|
|
207
|
-
|
|
208
|
-
if (responseData.error) {
|
|
209
|
-
throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
|
|
210
|
-
}
|
|
398
|
+
const responseData = await response.json() as any;
|
|
211
399
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
400
|
+
if (responseData.error) {
|
|
401
|
+
throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
|
|
402
|
+
}
|
|
215
403
|
|
|
216
|
-
|
|
404
|
+
if (!responseData?.data || !Array.isArray(responseData.data)) {
|
|
405
|
+
throw new Error('Invalid response format from Marketstack API');
|
|
406
|
+
}
|
|
217
407
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
408
|
+
const prices: IStockPrice[] = [];
|
|
409
|
+
|
|
410
|
+
for (const data of responseData.data) {
|
|
411
|
+
try {
|
|
412
|
+
prices.push(this.mapToStockPrice(data, 'intraday', marketState));
|
|
413
|
+
} catch (error) {
|
|
414
|
+
this.logger.warn(`Failed to parse intraday data for ${data.symbol}:`, error);
|
|
225
415
|
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (prices.length === 0) {
|
|
419
|
+
throw new Error('No valid price data received from batch intraday request');
|
|
420
|
+
}
|
|
226
421
|
|
|
227
|
-
|
|
228
|
-
|
|
422
|
+
return prices;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Fetch batch current prices using EOD endpoint
|
|
427
|
+
*/
|
|
428
|
+
private async fetchBatchCurrentPricesEod(request: IStockBatchCurrentRequest): Promise<IStockPrice[]> {
|
|
429
|
+
const symbols = request.tickers.join(',');
|
|
430
|
+
let url = `${this.baseUrl}/eod/latest?access_key=${this.apiKey}&symbols=${symbols}`;
|
|
431
|
+
|
|
432
|
+
if (request.exchange) {
|
|
433
|
+
url += `&exchange=${request.exchange}`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
437
|
+
.url(url)
|
|
438
|
+
.timeout(this.config?.timeout || 15000)
|
|
439
|
+
.get();
|
|
440
|
+
|
|
441
|
+
const responseData = await response.json() as any;
|
|
442
|
+
|
|
443
|
+
// Check for API errors
|
|
444
|
+
if (responseData.error) {
|
|
445
|
+
throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!responseData?.data || !Array.isArray(responseData.data)) {
|
|
449
|
+
throw new Error('Invalid response format from Marketstack API');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const prices: IStockPrice[] = [];
|
|
453
|
+
|
|
454
|
+
for (const data of responseData.data) {
|
|
455
|
+
try {
|
|
456
|
+
prices.push(this.mapToStockPrice(data, 'eod'));
|
|
457
|
+
} catch (error) {
|
|
458
|
+
this.logger.warn(`Failed to parse data for ${data.symbol}:`, error);
|
|
459
|
+
// Continue processing other tickers
|
|
229
460
|
}
|
|
461
|
+
}
|
|
230
462
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
this.logger.error(`Failed to fetch batch current prices:`, error);
|
|
234
|
-
throw new Error(`Marketstack: Failed to fetch batch current prices: ${error.message}`);
|
|
463
|
+
if (prices.length === 0) {
|
|
464
|
+
throw new Error('No valid price data received from batch request');
|
|
235
465
|
}
|
|
466
|
+
|
|
467
|
+
return prices;
|
|
236
468
|
}
|
|
237
469
|
|
|
238
470
|
/**
|
|
@@ -283,8 +515,15 @@ export class MarketstackProvider implements IStockProvider {
|
|
|
283
515
|
|
|
284
516
|
/**
|
|
285
517
|
* Map Marketstack API response to IStockPrice interface
|
|
518
|
+
* @param data - API response data
|
|
519
|
+
* @param dataType - Type of data (eod, intraday, live)
|
|
520
|
+
* @param explicitMarketState - Override market state (used for intraday data fetched during known market hours)
|
|
286
521
|
*/
|
|
287
|
-
private mapToStockPrice(
|
|
522
|
+
private mapToStockPrice(
|
|
523
|
+
data: any,
|
|
524
|
+
dataType: 'eod' | 'intraday' | 'live' = 'eod',
|
|
525
|
+
explicitMarketState?: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED'
|
|
526
|
+
): IStockPrice {
|
|
288
527
|
if (!data.close) {
|
|
289
528
|
throw new Error('Missing required price data');
|
|
290
529
|
}
|
|
@@ -301,6 +540,23 @@ export class MarketstackProvider implements IStockProvider {
|
|
|
301
540
|
const timestamp = data.date ? new Date(data.date) : new Date();
|
|
302
541
|
const fetchedAt = new Date();
|
|
303
542
|
|
|
543
|
+
// Determine market state intelligently
|
|
544
|
+
let marketState: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED';
|
|
545
|
+
|
|
546
|
+
if (explicitMarketState) {
|
|
547
|
+
// Use provided market state (for intraday data fetched during known market hours)
|
|
548
|
+
marketState = explicitMarketState;
|
|
549
|
+
} else if (dataType === 'eod') {
|
|
550
|
+
// EOD data is always for closed markets
|
|
551
|
+
marketState = 'CLOSED';
|
|
552
|
+
} else if (dataType === 'intraday') {
|
|
553
|
+
// For intraday data without explicit state, determine from timestamp
|
|
554
|
+
marketState = this.getUsMarketState(timestamp.getTime());
|
|
555
|
+
} else {
|
|
556
|
+
// Default fallback
|
|
557
|
+
marketState = 'CLOSED';
|
|
558
|
+
}
|
|
559
|
+
|
|
304
560
|
const stockPrice: IStockPrice = {
|
|
305
561
|
ticker: data.symbol.toUpperCase(),
|
|
306
562
|
price: currentPrice,
|
|
@@ -310,7 +566,7 @@ export class MarketstackProvider implements IStockProvider {
|
|
|
310
566
|
previousClose: previousClose,
|
|
311
567
|
timestamp: timestamp,
|
|
312
568
|
provider: this.name,
|
|
313
|
-
marketState:
|
|
569
|
+
marketState: marketState, // Now dynamic based on data type, timestamp, and explicit state
|
|
314
570
|
exchange: data.exchange,
|
|
315
571
|
exchangeName: data.exchange_code || data.name,
|
|
316
572
|
|
|
@@ -373,4 +629,76 @@ export class MarketstackProvider implements IStockProvider {
|
|
|
373
629
|
const day = String(date.getDate()).padStart(2, '0');
|
|
374
630
|
return `${year}-${month}-${day}`;
|
|
375
631
|
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Get US market state based on Eastern Time
|
|
635
|
+
* Regular hours: 9:30 AM - 4:00 PM ET
|
|
636
|
+
* Pre-market: 4:00 AM - 9:30 AM ET
|
|
637
|
+
* After-hours: 4:00 PM - 8:00 PM ET
|
|
638
|
+
*
|
|
639
|
+
* @param timestampMs - Optional timestamp in milliseconds (defaults to current time)
|
|
640
|
+
* @returns Market state: PRE, REGULAR, POST, or CLOSED
|
|
641
|
+
*/
|
|
642
|
+
private getUsMarketState(timestampMs?: number): 'PRE' | 'REGULAR' | 'POST' | 'CLOSED' {
|
|
643
|
+
const now = timestampMs ? new Date(timestampMs) : new Date();
|
|
644
|
+
|
|
645
|
+
// Convert to ET (UTC-5 or UTC-4 depending on DST)
|
|
646
|
+
// For simplicity, we'll use a rough approximation
|
|
647
|
+
// TODO: Add proper timezone library for production use
|
|
648
|
+
const etOffset = -5; // Standard time, adjust for DST if needed
|
|
649
|
+
const etTime = new Date(now.getTime() + (etOffset * 60 * 60 * 1000));
|
|
650
|
+
|
|
651
|
+
// Get day of week (0 = Sunday, 6 = Saturday)
|
|
652
|
+
const dayOfWeek = etTime.getUTCDay();
|
|
653
|
+
|
|
654
|
+
// Check if weekend
|
|
655
|
+
if (dayOfWeek === 0 || dayOfWeek === 6) {
|
|
656
|
+
return 'CLOSED';
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Get hour and minute in ET
|
|
660
|
+
const hours = etTime.getUTCHours();
|
|
661
|
+
const minutes = etTime.getUTCMinutes();
|
|
662
|
+
const timeInMinutes = hours * 60 + minutes;
|
|
663
|
+
|
|
664
|
+
// Define market hours in minutes
|
|
665
|
+
const preMarketStart = 4 * 60; // 4:00 AM
|
|
666
|
+
const regularMarketStart = 9 * 60 + 30; // 9:30 AM
|
|
667
|
+
const regularMarketEnd = 16 * 60; // 4:00 PM
|
|
668
|
+
const afterHoursEnd = 20 * 60; // 8:00 PM
|
|
669
|
+
|
|
670
|
+
if (timeInMinutes >= preMarketStart && timeInMinutes < regularMarketStart) {
|
|
671
|
+
return 'PRE';
|
|
672
|
+
} else if (timeInMinutes >= regularMarketStart && timeInMinutes < regularMarketEnd) {
|
|
673
|
+
return 'REGULAR';
|
|
674
|
+
} else if (timeInMinutes >= regularMarketEnd && timeInMinutes < afterHoursEnd) {
|
|
675
|
+
return 'POST';
|
|
676
|
+
} else {
|
|
677
|
+
return 'CLOSED';
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Determine if intraday endpoint should be used based on market state
|
|
683
|
+
* Uses intraday for PRE, REGULAR, and POST market states
|
|
684
|
+
* Uses EOD for CLOSED state to save API credits
|
|
685
|
+
*
|
|
686
|
+
* @param marketState - Current market state
|
|
687
|
+
* @returns true if intraday endpoint should be used
|
|
688
|
+
*/
|
|
689
|
+
private shouldUseIntradayEndpoint(marketState: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED'): boolean {
|
|
690
|
+
return marketState !== 'CLOSED';
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Format ticker symbol for intraday endpoints
|
|
695
|
+
* Marketstack intraday API requires periods to be replaced with hyphens
|
|
696
|
+
* Example: BRK.B → BRK-B
|
|
697
|
+
*
|
|
698
|
+
* @param symbol - Original ticker symbol
|
|
699
|
+
* @returns Formatted symbol for intraday endpoints
|
|
700
|
+
*/
|
|
701
|
+
private formatSymbolForIntraday(symbol: string): string {
|
|
702
|
+
return symbol.replace(/\./g, '-');
|
|
703
|
+
}
|
|
376
704
|
}
|