@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.
@@ -10,32 +10,39 @@ import type {
10
10
  } from '../interfaces/stockprice.js';
11
11
 
12
12
  /**
13
- * Marketstack API v2 Provider - Enhanced
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
- * - Intraday pricing with multiple intervals (1min, 5min, 15min, 30min, 1hour)
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 access)
35
+ * - Basic Plan: 10,000 requests/month (EOD only)
36
+ * - Professional Plan: 100,000 requests/month (intraday + EOD)
29
37
  *
30
- * Phase 1 Enhancements:
31
- * - Historical data retrieval with date ranges
32
- * - Exchange filtering
33
- * - OHLCV data support
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 = 80; // Lower than Yahoo (100) due to rate limits and EOD-only data
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 EOD price for a single ticker (new API)
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
- 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}`;
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
- const response = await plugins.smartrequest.SmartRequest.create()
88
- .url(url)
89
- .timeout(this.config?.timeout || 10000)
90
- .get();
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
- const responseData = await response.json() as any;
132
+ const response = await plugins.smartrequest.SmartRequest.create()
133
+ .url(url)
134
+ .timeout(this.config?.timeout || 10000)
135
+ .get();
93
136
 
94
- // Check for API errors
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
- // For single ticker endpoint, response is direct object (not wrapped in data field)
100
- if (!responseData || !responseData.close) {
101
- throw new Error(`No data found for ticker ${request.ticker}`);
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
- 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}`);
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 (Phase 2 placeholder)
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
- throw new Error('Intraday data support coming in Phase 2. For now, use EOD data with type: "current" or "historical"');
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 (new API)
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
- const symbols = request.tickers.join(',');
194
- let url = `${this.baseUrl}/eod/latest?access_key=${this.apiKey}&symbols=${symbols}`;
195
-
196
- if (request.exchange) {
197
- url += `&exchange=${request.exchange}`;
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
- const response = await plugins.smartrequest.SmartRequest.create()
201
- .url(url)
202
- .timeout(this.config?.timeout || 15000)
203
- .get();
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
- const responseData = await response.json() as any;
393
+ const response = await plugins.smartrequest.SmartRequest.create()
394
+ .url(url)
395
+ .timeout(this.config?.timeout || 15000)
396
+ .get();
206
397
 
207
- // Check for API errors
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
- if (!responseData?.data || !Array.isArray(responseData.data)) {
213
- throw new Error('Invalid response format from Marketstack API');
214
- }
400
+ if (responseData.error) {
401
+ throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
402
+ }
215
403
 
216
- const prices: IStockPrice[] = [];
404
+ if (!responseData?.data || !Array.isArray(responseData.data)) {
405
+ throw new Error('Invalid response format from Marketstack API');
406
+ }
217
407
 
218
- for (const data of responseData.data) {
219
- try {
220
- prices.push(this.mapToStockPrice(data, 'eod'));
221
- } catch (error) {
222
- this.logger.warn(`Failed to parse data for ${data.symbol}:`, error);
223
- // Continue processing other tickers
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
- if (prices.length === 0) {
228
- throw new Error('No valid price data received from batch request');
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
- return prices;
232
- } catch (error) {
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(data: any, dataType: 'eod' | 'intraday' | 'live' = 'eod'): IStockPrice {
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: 'CLOSED', // EOD data is always for closed markets
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
  }