@fin.cx/opendata 3.2.2 → 3.4.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/index.d.ts +1 -0
- package/dist_ts/stocks/index.js +2 -1
- package/dist_ts/stocks/providers/provider.coingecko.d.ts +101 -0
- package/dist_ts/stocks/providers/provider.coingecko.js +596 -0
- package/dist_ts/stocks/providers/provider.marketstack.d.ts +73 -12
- package/dist_ts/stocks/providers/provider.marketstack.js +336 -63
- package/package.json +1 -1
- package/readme.hints.md +45 -1
- package/readme.md +62 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/stocks/index.ts +2 -1
- package/ts/stocks/providers/provider.coingecko.ts +757 -0
- package/ts/stocks/providers/provider.marketstack.ts +386 -69
|
@@ -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
|
/**
|
|
@@ -179,60 +250,210 @@ 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 = 10000; // Safety limit
|
|
263
|
+
|
|
264
|
+
// Format symbol for intraday endpoint (replace . with -)
|
|
265
|
+
const formattedSymbol = this.formatSymbolForIntraday(request.ticker);
|
|
266
|
+
|
|
267
|
+
while (true) {
|
|
268
|
+
let url = `${this.baseUrl}/tickers/${formattedSymbol}/intraday?access_key=${this.apiKey}`;
|
|
269
|
+
url += `&interval=${request.interval}`;
|
|
270
|
+
url += `&limit=${limit}`;
|
|
271
|
+
url += `&offset=${offset}`;
|
|
272
|
+
|
|
273
|
+
// Add date filter if specified
|
|
274
|
+
if (request.date) {
|
|
275
|
+
url += `&date_from=${this.formatDate(request.date)}`;
|
|
276
|
+
url += `&date_to=${this.formatDate(request.date)}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Add exchange filter if specified
|
|
280
|
+
if (request.exchange) {
|
|
281
|
+
url += `&exchange=${request.exchange}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
285
|
+
.url(url)
|
|
286
|
+
.timeout(this.config?.timeout || 15000)
|
|
287
|
+
.get();
|
|
288
|
+
|
|
289
|
+
const responseData = await response.json() as any;
|
|
290
|
+
|
|
291
|
+
// Check for API errors
|
|
292
|
+
if (responseData.error) {
|
|
293
|
+
throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!responseData?.data || !Array.isArray(responseData.data)) {
|
|
297
|
+
throw new Error('Invalid response format from Marketstack API');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Map data to stock prices
|
|
301
|
+
for (const data of responseData.data) {
|
|
302
|
+
try {
|
|
303
|
+
allPrices.push(this.mapToStockPrice(data, 'intraday'));
|
|
304
|
+
} catch (error) {
|
|
305
|
+
this.logger.warn(`Failed to parse intraday data for ${data.symbol}:`, error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check if we have more pages
|
|
310
|
+
const pagination = responseData.pagination;
|
|
311
|
+
const hasMore = pagination && offset + limit < pagination.total;
|
|
312
|
+
|
|
313
|
+
// Honor limit from request if specified, or safety limit
|
|
314
|
+
if (!hasMore || (request.limit && allPrices.length >= request.limit) || allPrices.length >= maxRecords) {
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
offset += limit;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Apply limit if specified
|
|
322
|
+
if (request.limit && allPrices.length > request.limit) {
|
|
323
|
+
return allPrices.slice(0, request.limit);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return allPrices;
|
|
327
|
+
} catch (error) {
|
|
328
|
+
this.logger.error(`Failed to fetch intraday prices for ${request.ticker}:`, error);
|
|
329
|
+
throw new Error(`Marketstack: Failed to fetch intraday prices for ${request.ticker}: ${error.message}`);
|
|
330
|
+
}
|
|
186
331
|
}
|
|
187
332
|
|
|
188
333
|
/**
|
|
189
|
-
* Fetch current prices for multiple tickers
|
|
334
|
+
* Fetch current prices for multiple tickers with intelligent endpoint selection
|
|
335
|
+
* Uses intraday during market hours (PRE, REGULAR, POST) for fresh data
|
|
336
|
+
* Uses EOD after market close (CLOSED) to save API credits
|
|
190
337
|
*/
|
|
191
338
|
private async fetchBatchCurrentPrices(request: IStockBatchCurrentRequest): Promise<IStockPrice[]> {
|
|
192
339
|
try {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
340
|
+
// Determine current market state
|
|
341
|
+
const marketState = this.getUsMarketState();
|
|
342
|
+
const useIntraday = this.shouldUseIntradayEndpoint(marketState);
|
|
343
|
+
|
|
344
|
+
if (useIntraday) {
|
|
345
|
+
return await this.fetchBatchCurrentPricesIntraday(request, marketState);
|
|
346
|
+
} else {
|
|
347
|
+
return await this.fetchBatchCurrentPricesEod(request);
|
|
198
348
|
}
|
|
349
|
+
} catch (error) {
|
|
350
|
+
// Fallback to EOD if intraday fails
|
|
351
|
+
if (error.message?.includes('intraday') || error.message?.includes('Marketstack API error')) {
|
|
352
|
+
this.logger.warn(`Intraday batch endpoint failed, falling back to EOD:`, error.message);
|
|
353
|
+
try {
|
|
354
|
+
return await this.fetchBatchCurrentPricesEod(request);
|
|
355
|
+
} catch (eodError) {
|
|
356
|
+
// Both failed, throw original error
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
199
363
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Fetch batch current prices using intraday endpoint
|
|
366
|
+
*/
|
|
367
|
+
private async fetchBatchCurrentPricesIntraday(
|
|
368
|
+
request: IStockBatchCurrentRequest,
|
|
369
|
+
marketState: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED'
|
|
370
|
+
): Promise<IStockPrice[]> {
|
|
371
|
+
// Format symbols for intraday (replace . with -)
|
|
372
|
+
const formattedSymbols = request.tickers.map(t => this.formatSymbolForIntraday(t)).join(',');
|
|
373
|
+
|
|
374
|
+
let url = `${this.baseUrl}/intraday/latest?access_key=${this.apiKey}`;
|
|
375
|
+
url += `&symbols=${formattedSymbols}`;
|
|
376
|
+
url += `&interval=1min`; // Use 1min for most recent data
|
|
377
|
+
|
|
378
|
+
if (request.exchange) {
|
|
379
|
+
url += `&exchange=${request.exchange}`;
|
|
380
|
+
}
|
|
204
381
|
|
|
205
|
-
|
|
382
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
383
|
+
.url(url)
|
|
384
|
+
.timeout(this.config?.timeout || 15000)
|
|
385
|
+
.get();
|
|
206
386
|
|
|
207
|
-
|
|
208
|
-
if (responseData.error) {
|
|
209
|
-
throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
|
|
210
|
-
}
|
|
387
|
+
const responseData = await response.json() as any;
|
|
211
388
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
389
|
+
if (responseData.error) {
|
|
390
|
+
throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
|
|
391
|
+
}
|
|
215
392
|
|
|
216
|
-
|
|
393
|
+
if (!responseData?.data || !Array.isArray(responseData.data)) {
|
|
394
|
+
throw new Error('Invalid response format from Marketstack API');
|
|
395
|
+
}
|
|
217
396
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
397
|
+
const prices: IStockPrice[] = [];
|
|
398
|
+
|
|
399
|
+
for (const data of responseData.data) {
|
|
400
|
+
try {
|
|
401
|
+
prices.push(this.mapToStockPrice(data, 'intraday', marketState));
|
|
402
|
+
} catch (error) {
|
|
403
|
+
this.logger.warn(`Failed to parse intraday data for ${data.symbol}:`, error);
|
|
225
404
|
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (prices.length === 0) {
|
|
408
|
+
throw new Error('No valid price data received from batch intraday request');
|
|
409
|
+
}
|
|
226
410
|
|
|
227
|
-
|
|
228
|
-
|
|
411
|
+
return prices;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Fetch batch current prices using EOD endpoint
|
|
416
|
+
*/
|
|
417
|
+
private async fetchBatchCurrentPricesEod(request: IStockBatchCurrentRequest): Promise<IStockPrice[]> {
|
|
418
|
+
const symbols = request.tickers.join(',');
|
|
419
|
+
let url = `${this.baseUrl}/eod/latest?access_key=${this.apiKey}&symbols=${symbols}`;
|
|
420
|
+
|
|
421
|
+
if (request.exchange) {
|
|
422
|
+
url += `&exchange=${request.exchange}`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
426
|
+
.url(url)
|
|
427
|
+
.timeout(this.config?.timeout || 15000)
|
|
428
|
+
.get();
|
|
429
|
+
|
|
430
|
+
const responseData = await response.json() as any;
|
|
431
|
+
|
|
432
|
+
// Check for API errors
|
|
433
|
+
if (responseData.error) {
|
|
434
|
+
throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!responseData?.data || !Array.isArray(responseData.data)) {
|
|
438
|
+
throw new Error('Invalid response format from Marketstack API');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const prices: IStockPrice[] = [];
|
|
442
|
+
|
|
443
|
+
for (const data of responseData.data) {
|
|
444
|
+
try {
|
|
445
|
+
prices.push(this.mapToStockPrice(data, 'eod'));
|
|
446
|
+
} catch (error) {
|
|
447
|
+
this.logger.warn(`Failed to parse data for ${data.symbol}:`, error);
|
|
448
|
+
// Continue processing other tickers
|
|
229
449
|
}
|
|
450
|
+
}
|
|
230
451
|
|
|
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}`);
|
|
452
|
+
if (prices.length === 0) {
|
|
453
|
+
throw new Error('No valid price data received from batch request');
|
|
235
454
|
}
|
|
455
|
+
|
|
456
|
+
return prices;
|
|
236
457
|
}
|
|
237
458
|
|
|
238
459
|
/**
|
|
@@ -283,8 +504,15 @@ export class MarketstackProvider implements IStockProvider {
|
|
|
283
504
|
|
|
284
505
|
/**
|
|
285
506
|
* Map Marketstack API response to IStockPrice interface
|
|
507
|
+
* @param data - API response data
|
|
508
|
+
* @param dataType - Type of data (eod, intraday, live)
|
|
509
|
+
* @param explicitMarketState - Override market state (used for intraday data fetched during known market hours)
|
|
286
510
|
*/
|
|
287
|
-
private mapToStockPrice(
|
|
511
|
+
private mapToStockPrice(
|
|
512
|
+
data: any,
|
|
513
|
+
dataType: 'eod' | 'intraday' | 'live' = 'eod',
|
|
514
|
+
explicitMarketState?: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED'
|
|
515
|
+
): IStockPrice {
|
|
288
516
|
if (!data.close) {
|
|
289
517
|
throw new Error('Missing required price data');
|
|
290
518
|
}
|
|
@@ -301,6 +529,23 @@ export class MarketstackProvider implements IStockProvider {
|
|
|
301
529
|
const timestamp = data.date ? new Date(data.date) : new Date();
|
|
302
530
|
const fetchedAt = new Date();
|
|
303
531
|
|
|
532
|
+
// Determine market state intelligently
|
|
533
|
+
let marketState: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED';
|
|
534
|
+
|
|
535
|
+
if (explicitMarketState) {
|
|
536
|
+
// Use provided market state (for intraday data fetched during known market hours)
|
|
537
|
+
marketState = explicitMarketState;
|
|
538
|
+
} else if (dataType === 'eod') {
|
|
539
|
+
// EOD data is always for closed markets
|
|
540
|
+
marketState = 'CLOSED';
|
|
541
|
+
} else if (dataType === 'intraday') {
|
|
542
|
+
// For intraday data without explicit state, determine from timestamp
|
|
543
|
+
marketState = this.getUsMarketState(timestamp.getTime());
|
|
544
|
+
} else {
|
|
545
|
+
// Default fallback
|
|
546
|
+
marketState = 'CLOSED';
|
|
547
|
+
}
|
|
548
|
+
|
|
304
549
|
const stockPrice: IStockPrice = {
|
|
305
550
|
ticker: data.symbol.toUpperCase(),
|
|
306
551
|
price: currentPrice,
|
|
@@ -310,7 +555,7 @@ export class MarketstackProvider implements IStockProvider {
|
|
|
310
555
|
previousClose: previousClose,
|
|
311
556
|
timestamp: timestamp,
|
|
312
557
|
provider: this.name,
|
|
313
|
-
marketState:
|
|
558
|
+
marketState: marketState, // Now dynamic based on data type, timestamp, and explicit state
|
|
314
559
|
exchange: data.exchange,
|
|
315
560
|
exchangeName: data.exchange_code || data.name,
|
|
316
561
|
|
|
@@ -373,4 +618,76 @@ export class MarketstackProvider implements IStockProvider {
|
|
|
373
618
|
const day = String(date.getDate()).padStart(2, '0');
|
|
374
619
|
return `${year}-${month}-${day}`;
|
|
375
620
|
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Get US market state based on Eastern Time
|
|
624
|
+
* Regular hours: 9:30 AM - 4:00 PM ET
|
|
625
|
+
* Pre-market: 4:00 AM - 9:30 AM ET
|
|
626
|
+
* After-hours: 4:00 PM - 8:00 PM ET
|
|
627
|
+
*
|
|
628
|
+
* @param timestampMs - Optional timestamp in milliseconds (defaults to current time)
|
|
629
|
+
* @returns Market state: PRE, REGULAR, POST, or CLOSED
|
|
630
|
+
*/
|
|
631
|
+
private getUsMarketState(timestampMs?: number): 'PRE' | 'REGULAR' | 'POST' | 'CLOSED' {
|
|
632
|
+
const now = timestampMs ? new Date(timestampMs) : new Date();
|
|
633
|
+
|
|
634
|
+
// Convert to ET (UTC-5 or UTC-4 depending on DST)
|
|
635
|
+
// For simplicity, we'll use a rough approximation
|
|
636
|
+
// TODO: Add proper timezone library for production use
|
|
637
|
+
const etOffset = -5; // Standard time, adjust for DST if needed
|
|
638
|
+
const etTime = new Date(now.getTime() + (etOffset * 60 * 60 * 1000));
|
|
639
|
+
|
|
640
|
+
// Get day of week (0 = Sunday, 6 = Saturday)
|
|
641
|
+
const dayOfWeek = etTime.getUTCDay();
|
|
642
|
+
|
|
643
|
+
// Check if weekend
|
|
644
|
+
if (dayOfWeek === 0 || dayOfWeek === 6) {
|
|
645
|
+
return 'CLOSED';
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Get hour and minute in ET
|
|
649
|
+
const hours = etTime.getUTCHours();
|
|
650
|
+
const minutes = etTime.getUTCMinutes();
|
|
651
|
+
const timeInMinutes = hours * 60 + minutes;
|
|
652
|
+
|
|
653
|
+
// Define market hours in minutes
|
|
654
|
+
const preMarketStart = 4 * 60; // 4:00 AM
|
|
655
|
+
const regularMarketStart = 9 * 60 + 30; // 9:30 AM
|
|
656
|
+
const regularMarketEnd = 16 * 60; // 4:00 PM
|
|
657
|
+
const afterHoursEnd = 20 * 60; // 8:00 PM
|
|
658
|
+
|
|
659
|
+
if (timeInMinutes >= preMarketStart && timeInMinutes < regularMarketStart) {
|
|
660
|
+
return 'PRE';
|
|
661
|
+
} else if (timeInMinutes >= regularMarketStart && timeInMinutes < regularMarketEnd) {
|
|
662
|
+
return 'REGULAR';
|
|
663
|
+
} else if (timeInMinutes >= regularMarketEnd && timeInMinutes < afterHoursEnd) {
|
|
664
|
+
return 'POST';
|
|
665
|
+
} else {
|
|
666
|
+
return 'CLOSED';
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Determine if intraday endpoint should be used based on market state
|
|
672
|
+
* Uses intraday for PRE, REGULAR, and POST market states
|
|
673
|
+
* Uses EOD for CLOSED state to save API credits
|
|
674
|
+
*
|
|
675
|
+
* @param marketState - Current market state
|
|
676
|
+
* @returns true if intraday endpoint should be used
|
|
677
|
+
*/
|
|
678
|
+
private shouldUseIntradayEndpoint(marketState: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED'): boolean {
|
|
679
|
+
return marketState !== 'CLOSED';
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Format ticker symbol for intraday endpoints
|
|
684
|
+
* Marketstack intraday API requires periods to be replaced with hyphens
|
|
685
|
+
* Example: BRK.B → BRK-B
|
|
686
|
+
*
|
|
687
|
+
* @param symbol - Original ticker symbol
|
|
688
|
+
* @returns Formatted symbol for intraday endpoints
|
|
689
|
+
*/
|
|
690
|
+
private formatSymbolForIntraday(symbol: string): string {
|
|
691
|
+
return symbol.replace(/\./g, '-');
|
|
692
|
+
}
|
|
376
693
|
}
|