@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.
package/readme.md CHANGED
@@ -4,7 +4,49 @@
4
4
 
5
5
  Access live stock prices, cryptocurrencies, forex, commodities AND comprehensive German company data - all through a single, unified API.
6
6
 
7
- ## ⚠️ Breaking Change in v2.0
7
+ ## ⚠️ Breaking Changes
8
+
9
+ ### v2.1 - Enhanced Stock Market API (Current)
10
+
11
+ **The stock market API has been significantly enhanced with a new unified request system.**
12
+
13
+ **What Changed:**
14
+ - New discriminated union request types (`IStockDataRequest`)
15
+ - Enhanced `IStockPrice` interface with OHLCV data and metadata
16
+ - New `getData()` method replaces legacy `getPrice()` and `getPrices()`
17
+ - Historical data support with date ranges
18
+ - Exchange filtering via MIC codes
19
+ - Smart caching with data-type aware TTL
20
+
21
+ **Migration:**
22
+ ```typescript
23
+ // OLD (v2.0 and earlier)
24
+ const price = await service.getPrice({ ticker: 'AAPL' });
25
+ const prices = await service.getPrices({ tickers: ['AAPL', 'MSFT'] });
26
+
27
+ // NEW (v2.1+) - Unified API
28
+ const price = await service.getData({ type: 'current', ticker: 'AAPL' });
29
+ const prices = await service.getData({ type: 'batch', tickers: ['AAPL', 'MSFT'] });
30
+
31
+ // NEW - Historical data
32
+ const history = await service.getData({
33
+ type: 'historical',
34
+ ticker: 'AAPL',
35
+ from: new Date('2024-01-01'),
36
+ to: new Date('2024-12-31')
37
+ });
38
+
39
+ // NEW - Exchange filtering
40
+ const price = await service.getData({
41
+ type: 'current',
42
+ ticker: 'VOD',
43
+ exchange: 'XLON' // London Stock Exchange
44
+ });
45
+ ```
46
+
47
+ **Note:** Legacy `getPrice()` and `getPrices()` methods still work but are deprecated.
48
+
49
+ ### v2.0 - Directory Configuration
8
50
 
9
51
  **Directory paths are now MANDATORY when using German business data features.** The package no longer creates `.nogit/` directories automatically. You must explicitly configure all directory paths when instantiating `OpenData`:
10
52
 
@@ -28,17 +70,17 @@ pnpm add @fin.cx/opendata
28
70
 
29
71
  ## Quick Start
30
72
 
31
- ### 📈 Stock Market Data
73
+ ### 📈 Stock Market Data (v2.1+ Enhanced)
32
74
 
33
- Get market data with EOD (End-of-Day) pricing:
75
+ Get comprehensive market data with EOD, historical, and OHLCV data:
34
76
 
35
77
  ```typescript
36
78
  import { StockPriceService, MarketstackProvider } from '@fin.cx/opendata';
37
79
 
38
- // Initialize the service with caching
80
+ // Initialize the service with smart caching
39
81
  const stockService = new StockPriceService({
40
- ttl: 60000, // Cache for 1 minute
41
- maxEntries: 1000 // Max cached symbols
82
+ ttl: 60000, // Default cache TTL (historical data cached forever)
83
+ maxEntries: 10000 // Increased for historical data
42
84
  });
43
85
 
44
86
  // Register Marketstack provider with API key
@@ -47,19 +89,55 @@ stockService.register(new MarketstackProvider('YOUR_API_KEY'), {
47
89
  retryAttempts: 3
48
90
  });
49
91
 
50
- // Get single stock price
51
- const apple = await stockService.getPrice({ ticker: 'AAPL' });
92
+ // Get current price (new unified API)
93
+ const apple = await stockService.getData({ type: 'current', ticker: 'AAPL' });
52
94
  console.log(`Apple: $${apple.price} (${apple.changePercent.toFixed(2)}%)`);
95
+ console.log(`OHLCV: O=${apple.open} H=${apple.high} L=${apple.low} V=${apple.volume}`);
96
+ console.log(`Company: ${apple.companyName}`); // "Apple Inc"
97
+ console.log(`Full: ${apple.companyFullName}`); // "Apple Inc (NASDAQ:AAPL)"
98
+
99
+ // Get historical data (1 year of daily prices)
100
+ const history = await stockService.getData({
101
+ type: 'historical',
102
+ ticker: 'AAPL',
103
+ from: new Date('2024-01-01'),
104
+ to: new Date('2024-12-31'),
105
+ sort: 'DESC' // Newest first
106
+ });
107
+ console.log(`Fetched ${history.length} trading days`);
53
108
 
54
- // Get multiple prices at once (batch fetching)
55
- const prices = await stockService.getPrices({
56
- tickers: ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA']
109
+ // Exchange-specific data (London vs NYSE)
110
+ const vodLondon = await stockService.getData({
111
+ type: 'current',
112
+ ticker: 'VOD',
113
+ exchange: 'XLON' // London Stock Exchange
57
114
  });
58
115
 
59
- // 125,000+ tickers across 72+ exchanges worldwide
60
- const internationalStocks = await stockService.getPrices({
61
- tickers: ['AAPL', 'VOD.LON', 'SAP.DEX', 'TM', 'BABA']
116
+ const vodNYSE = await stockService.getData({
117
+ type: 'current',
118
+ ticker: 'VOD',
119
+ exchange: 'XNYS' // New York Stock Exchange
62
120
  });
121
+
122
+ // Batch current prices with company names
123
+ const prices = await stockService.getData({
124
+ type: 'batch',
125
+ tickers: ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA']
126
+ });
127
+
128
+ // Display with company names (automatically included - zero extra API calls!)
129
+ for (const stock of prices) {
130
+ console.log(`${stock.companyName}: $${stock.price}`);
131
+ // Output:
132
+ // Apple Inc: $271.40
133
+ // Microsoft Corporation: $525.76
134
+ // Alphabet Inc - Class A: $281.48
135
+ // Amazon.com Inc: $222.86
136
+ // Tesla Inc: $440.10
137
+ }
138
+
139
+ // Use companyFullName for richer context
140
+ console.log(prices[0].companyFullName); // "Apple Inc (NASDAQ:AAPL)"
63
141
  ```
64
142
 
65
143
  ### 🏢 German Business Data
@@ -100,15 +178,19 @@ await openData.buildInitialDb();
100
178
 
101
179
  ## Features
102
180
 
103
- ### 🎯 Stock Market Module
181
+ ### 🎯 Stock Market Module (v2.1 Enhanced)
104
182
 
105
- - **Marketstack API** - End-of-Day (EOD) data for 125,000+ tickers across 72+ exchanges
106
- - **Stock prices** for stocks, ETFs, indices, and more
107
- - **Batch operations** - fetch 100+ symbols in one request
108
- - **Smart caching** - configurable TTL, automatic invalidation
109
- - **Extensible provider system** - easily add new data sources
110
- - **Retry logic** - configurable retry attempts and delays
111
- - **Type-safe** - full TypeScript support with detailed interfaces
183
+ - **Company Names** - Automatic company name extraction with zero extra API calls (e.g., "Apple Inc (NASDAQ:AAPL)")
184
+ - **Historical Data** - Up to 15 years of daily EOD prices with automatic pagination
185
+ - **Exchange Filtering** - Query specific exchanges via MIC codes (XNAS, XLON, XNYS, etc.)
186
+ - **OHLCV Data** - Open, High, Low, Close, Volume for comprehensive analysis
187
+ - **Smart Caching** - Data-type aware TTL (historical cached forever, EOD 24h, live 30s)
188
+ - **Marketstack API** - 500,000+ tickers across 72+ exchanges worldwide
189
+ - **Batch Operations** - Fetch 100+ symbols in one request
190
+ - **Unified API** - Discriminated union types for type-safe requests
191
+ - **Extensible Providers** - Easy to add new data sources
192
+ - **Retry Logic** - Configurable attempts and delays
193
+ - **Type-Safe** - Full TypeScript support with detailed interfaces
112
194
 
113
195
  ### 🇩🇪 German Business Intelligence
114
196
 
@@ -120,6 +202,151 @@ await openData.buildInitialDb();
120
202
 
121
203
  ## Advanced Examples
122
204
 
205
+ ### Phase 1: Historical Data Analysis
206
+
207
+ Fetch and analyze historical price data:
208
+
209
+ ```typescript
210
+ // Get 1 year of historical data
211
+ const history = await stockService.getData({
212
+ type: 'historical',
213
+ ticker: 'AAPL',
214
+ from: new Date('2024-01-01'),
215
+ to: new Date('2024-12-31'),
216
+ sort: 'DESC' // Newest first (default)
217
+ });
218
+
219
+ // Calculate statistics
220
+ const prices = history.map(p => p.price);
221
+ const high52Week = Math.max(...prices);
222
+ const low52Week = Math.min(...prices);
223
+ const avgPrice = prices.reduce((a, b) => a + b) / prices.length;
224
+
225
+ console.log(`52-Week Analysis for AAPL:`);
226
+ console.log(`High: $${high52Week.toFixed(2)}`);
227
+ console.log(`Low: $${low52Week.toFixed(2)}`);
228
+ console.log(`Average: $${avgPrice.toFixed(2)}`);
229
+ console.log(`Days: ${history.length}`);
230
+
231
+ // Calculate daily returns
232
+ for (let i = 0; i < history.length - 1; i++) {
233
+ const todayPrice = history[i].price;
234
+ const yesterdayPrice = history[i + 1].price;
235
+ const dailyReturn = ((todayPrice - yesterdayPrice) / yesterdayPrice) * 100;
236
+ console.log(`${history[i].timestamp.toISOString().split('T')[0]}: ${dailyReturn.toFixed(2)}%`);
237
+ }
238
+ ```
239
+
240
+ ### Phase 1: Exchange-Specific Trading
241
+
242
+ Compare prices across different exchanges:
243
+
244
+ ```typescript
245
+ // Vodafone trades on both London and NYSE
246
+ const exchanges = [
247
+ { mic: 'XLON', name: 'London Stock Exchange' },
248
+ { mic: 'XNYS', name: 'New York Stock Exchange' }
249
+ ];
250
+
251
+ for (const exchange of exchanges) {
252
+ try {
253
+ const price = await stockService.getData({
254
+ type: 'current',
255
+ ticker: 'VOD',
256
+ exchange: exchange.mic
257
+ });
258
+
259
+ console.log(`${exchange.name}:`);
260
+ console.log(` Price: ${price.price} ${price.currency}`);
261
+ console.log(` Volume: ${price.volume?.toLocaleString()}`);
262
+ console.log(` Exchange: ${price.exchangeName}`);
263
+ } catch (error) {
264
+ console.log(`${exchange.name}: Not available`);
265
+ }
266
+ }
267
+ ```
268
+
269
+ ### Phase 1: OHLCV Technical Analysis
270
+
271
+ Use OHLCV data for technical indicators:
272
+
273
+ ```typescript
274
+ const history = await stockService.getData({
275
+ type: 'historical',
276
+ ticker: 'TSLA',
277
+ from: new Date('2024-11-01'),
278
+ to: new Date('2024-11-30')
279
+ });
280
+
281
+ // Calculate daily trading range
282
+ for (const day of history) {
283
+ const range = day.high - day.low;
284
+ const rangePercent = (range / day.low) * 100;
285
+
286
+ console.log(`${day.timestamp.toISOString().split('T')[0]}:`);
287
+ console.log(` Open: $${day.open}`);
288
+ console.log(` High: $${day.high}`);
289
+ console.log(` Low: $${day.low}`);
290
+ console.log(` Close: $${day.price}`);
291
+ console.log(` Volume: ${day.volume?.toLocaleString()}`);
292
+ console.log(` Range: $${range.toFixed(2)} (${rangePercent.toFixed(2)}%)`);
293
+ }
294
+
295
+ // Calculate Simple Moving Average (SMA)
296
+ const calculateSMA = (data: IStockPrice[], period: number) => {
297
+ const sma: number[] = [];
298
+ for (let i = period - 1; i < data.length; i++) {
299
+ const sum = data.slice(i - period + 1, i + 1)
300
+ .reduce((acc, p) => acc + p.price, 0);
301
+ sma.push(sum / period);
302
+ }
303
+ return sma;
304
+ };
305
+
306
+ const sma20 = calculateSMA(history, 20);
307
+ const sma50 = calculateSMA(history, 50);
308
+
309
+ console.log(`20-day SMA: $${sma20[0].toFixed(2)}`);
310
+ console.log(`50-day SMA: $${sma50[0].toFixed(2)}`);
311
+ ```
312
+
313
+ ### Phase 1: Smart Caching Performance
314
+
315
+ Leverage smart caching for efficiency:
316
+
317
+ ```typescript
318
+ // Historical data is cached FOREVER (never changes)
319
+ console.time('First historical fetch');
320
+ const history1 = await stockService.getData({
321
+ type: 'historical',
322
+ ticker: 'AAPL',
323
+ from: new Date('2024-01-01'),
324
+ to: new Date('2024-12-31')
325
+ });
326
+ console.timeEnd('First historical fetch');
327
+ // Output: First historical fetch: 2341ms
328
+
329
+ console.time('Second historical fetch (cached)');
330
+ const history2 = await stockService.getData({
331
+ type: 'historical',
332
+ ticker: 'AAPL',
333
+ from: new Date('2024-01-01'),
334
+ to: new Date('2024-12-31')
335
+ });
336
+ console.timeEnd('Second historical fetch (cached)');
337
+ // Output: Second historical fetch (cached): 2ms (1000x faster!)
338
+
339
+ // EOD data cached for 24 hours
340
+ const currentPrice = await stockService.getData({
341
+ type: 'current',
342
+ ticker: 'MSFT'
343
+ });
344
+ // Subsequent calls within 24h served from cache
345
+
346
+ // Cache statistics
347
+ console.log(`Cache size: ${stockService['cache'].size} entries`);
348
+ ```
349
+
123
350
  ### Market Dashboard
124
351
 
125
352
  Create an EOD market overview:
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/opendata',
6
- version: '2.0.0',
6
+ version: '3.0.0',
7
7
  description: 'A comprehensive TypeScript library for accessing business data and real-time financial information. Features include German company data management with MongoDB integration, JSONL bulk processing, automated Handelsregister interactions, and real-time stock market data from multiple providers.'
8
8
  }
@@ -1,6 +1,24 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import type { IStockProvider, IProviderConfig, IProviderRegistry } from './interfaces/provider.js';
3
- import type { IStockPrice, IStockQuoteRequest, IStockBatchQuoteRequest, IStockPriceError } from './interfaces/stockprice.js';
3
+ import type {
4
+ IStockPrice,
5
+ IStockPriceError,
6
+ IStockDataRequest,
7
+ IStockCurrentRequest,
8
+ IStockHistoricalRequest,
9
+ IStockIntradayRequest,
10
+ IStockBatchCurrentRequest,
11
+ TIntervalType
12
+ } from './interfaces/stockprice.js';
13
+
14
+ // Simple request interfaces for convenience methods
15
+ interface ISimpleQuoteRequest {
16
+ ticker: string;
17
+ }
18
+
19
+ interface ISimpleBatchRequest {
20
+ tickers: string[];
21
+ }
4
22
 
5
23
  interface IProviderEntry {
6
24
  provider: IStockProvider;
@@ -12,18 +30,19 @@ interface IProviderEntry {
12
30
  }
13
31
 
14
32
  interface ICacheEntry {
15
- price: IStockPrice;
33
+ price: IStockPrice | IStockPrice[];
16
34
  timestamp: Date;
35
+ ttl: number; // Specific TTL for this entry
17
36
  }
18
37
 
19
38
  export class StockPriceService implements IProviderRegistry {
20
39
  private providers = new Map<string, IProviderEntry>();
21
40
  private cache = new Map<string, ICacheEntry>();
22
41
  private logger = console;
23
-
42
+
24
43
  private cacheConfig = {
25
- ttl: 60000, // 60 seconds default
26
- maxEntries: 1000
44
+ ttl: 60000, // 60 seconds default (for backward compatibility)
45
+ maxEntries: 10000 // Increased for historical data
27
46
  };
28
47
 
29
48
  constructor(cacheConfig?: { ttl?: number; maxEntries?: number }) {
@@ -32,6 +51,43 @@ export class StockPriceService implements IProviderRegistry {
32
51
  }
33
52
  }
34
53
 
54
+ /**
55
+ * Get data-type aware TTL for smart caching
56
+ */
57
+ private getCacheTTL(dataType: 'eod' | 'historical' | 'intraday' | 'live', interval?: TIntervalType): number {
58
+ switch (dataType) {
59
+ case 'historical':
60
+ return Infinity; // Historical data never changes
61
+ case 'eod':
62
+ return 24 * 60 * 60 * 1000; // 24 hours (EOD is static after market close)
63
+ case 'intraday':
64
+ // Match cache TTL to interval
65
+ return this.getIntervalMs(interval);
66
+ case 'live':
67
+ return 30 * 1000; // 30 seconds for live data
68
+ default:
69
+ return this.cacheConfig.ttl; // Fallback to default
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Convert interval to milliseconds
75
+ */
76
+ private getIntervalMs(interval?: TIntervalType): number {
77
+ if (!interval) return 60 * 1000; // Default 1 minute
78
+
79
+ const intervalMap: Record<TIntervalType, number> = {
80
+ '1min': 60 * 1000,
81
+ '5min': 5 * 60 * 1000,
82
+ '10min': 10 * 60 * 1000,
83
+ '15min': 15 * 60 * 1000,
84
+ '30min': 30 * 60 * 1000,
85
+ '1hour': 60 * 60 * 1000
86
+ };
87
+
88
+ return intervalMap[interval] || 60 * 1000;
89
+ }
90
+
35
91
  public register(provider: IStockProvider, config?: IProviderConfig): void {
36
92
  const defaultConfig: IProviderConfig = {
37
93
  enabled: true,
@@ -73,12 +129,37 @@ export class StockPriceService implements IProviderRegistry {
73
129
  .map(entry => entry.provider);
74
130
  }
75
131
 
76
- public async getPrice(request: IStockQuoteRequest): Promise<IStockPrice> {
77
- const cacheKey = this.getCacheKey(request);
132
+ /**
133
+ * Convenience method: Get current price for a single ticker
134
+ */
135
+ public async getPrice(request: ISimpleQuoteRequest): Promise<IStockPrice> {
136
+ const result = await this.getData({
137
+ type: 'current',
138
+ ticker: request.ticker
139
+ });
140
+ return result as IStockPrice;
141
+ }
142
+
143
+ /**
144
+ * Convenience method: Get current prices for multiple tickers
145
+ */
146
+ public async getPrices(request: ISimpleBatchRequest): Promise<IStockPrice[]> {
147
+ const result = await this.getData({
148
+ type: 'batch',
149
+ tickers: request.tickers
150
+ });
151
+ return result as IStockPrice[];
152
+ }
153
+
154
+ /**
155
+ * New unified data fetching method supporting all request types
156
+ */
157
+ public async getData(request: IStockDataRequest): Promise<IStockPrice | IStockPrice[]> {
158
+ const cacheKey = this.getDataCacheKey(request);
78
159
  const cached = this.getFromCache(cacheKey);
79
-
160
+
80
161
  if (cached) {
81
- console.log(`Cache hit for ${request.ticker}`);
162
+ console.log(`Cache hit for ${this.getRequestDescription(request)}`);
82
163
  return cached;
83
164
  }
84
165
 
@@ -91,109 +172,72 @@ export class StockPriceService implements IProviderRegistry {
91
172
 
92
173
  for (const provider of providers) {
93
174
  const entry = this.providers.get(provider.name)!;
94
-
175
+
95
176
  try {
96
- const price = await this.fetchWithRetry(
97
- () => provider.fetchPrice(request),
177
+ const result = await this.fetchWithRetry(
178
+ () => provider.fetchData(request),
98
179
  entry.config
99
- );
180
+ ) as IStockPrice | IStockPrice[];
100
181
 
101
182
  entry.successCount++;
102
- this.addToCache(cacheKey, price);
103
- console.log(`Successfully fetched ${request.ticker} from ${provider.name}`);
104
- return price;
183
+
184
+ // Determine TTL based on request type
185
+ const ttl = this.getRequestTTL(request, result);
186
+ this.addToCache(cacheKey, result, ttl);
187
+
188
+ console.log(`Successfully fetched ${this.getRequestDescription(request)} from ${provider.name}`);
189
+ return result;
105
190
  } catch (error) {
106
191
  entry.errorCount++;
107
192
  entry.lastError = error as Error;
108
193
  entry.lastErrorTime = new Date();
109
194
  lastError = error as Error;
110
-
195
+
111
196
  console.warn(
112
- `Provider ${provider.name} failed for ${request.ticker}: ${error.message}`
197
+ `Provider ${provider.name} failed for ${this.getRequestDescription(request)}: ${error.message}`
113
198
  );
114
199
  }
115
200
  }
116
201
 
117
202
  throw new Error(
118
- `Failed to fetch price for ${request.ticker} from all providers. Last error: ${lastError?.message}`
203
+ `Failed to fetch ${this.getRequestDescription(request)} from all providers. Last error: ${lastError?.message}`
119
204
  );
120
205
  }
121
206
 
122
- public async getPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]> {
123
- const cachedPrices: IStockPrice[] = [];
124
- const tickersToFetch: string[] = [];
125
-
126
- // Check cache for each ticker
127
- for (const ticker of request.tickers) {
128
- const cacheKey = this.getCacheKey({ ticker, includeExtendedHours: request.includeExtendedHours });
129
- const cached = this.getFromCache(cacheKey);
130
-
131
- if (cached) {
132
- cachedPrices.push(cached);
133
- } else {
134
- tickersToFetch.push(ticker);
135
- }
136
- }
137
-
138
- if (tickersToFetch.length === 0) {
139
- console.log(`All ${request.tickers.length} tickers served from cache`);
140
- return cachedPrices;
141
- }
142
-
143
- const providers = this.getEnabledProviders();
144
- if (providers.length === 0) {
145
- throw new Error('No stock price providers available');
146
- }
147
-
148
- let lastError: Error | undefined;
149
- let fetchedPrices: IStockPrice[] = [];
150
-
151
- for (const provider of providers) {
152
- const entry = this.providers.get(provider.name)!;
153
-
154
- try {
155
- fetchedPrices = await this.fetchWithRetry(
156
- () => provider.fetchPrices({
157
- tickers: tickersToFetch,
158
- includeExtendedHours: request.includeExtendedHours
159
- }),
160
- entry.config
161
- );
162
-
163
- entry.successCount++;
164
-
165
- // Cache the fetched prices
166
- for (const price of fetchedPrices) {
167
- const cacheKey = this.getCacheKey({
168
- ticker: price.ticker,
169
- includeExtendedHours: request.includeExtendedHours
170
- });
171
- this.addToCache(cacheKey, price);
172
- }
173
-
174
- console.log(
175
- `Successfully fetched ${fetchedPrices.length} prices from ${provider.name}`
176
- );
177
- break;
178
- } catch (error) {
179
- entry.errorCount++;
180
- entry.lastError = error as Error;
181
- entry.lastErrorTime = new Date();
182
- lastError = error as Error;
183
-
184
- console.warn(
185
- `Provider ${provider.name} failed for batch request: ${error.message}`
186
- );
187
- }
207
+ /**
208
+ * Get TTL based on request type and result
209
+ */
210
+ private getRequestTTL(request: IStockDataRequest, result: IStockPrice | IStockPrice[]): number {
211
+ switch (request.type) {
212
+ case 'historical':
213
+ return Infinity; // Historical data never changes
214
+ case 'current':
215
+ return this.getCacheTTL('eod');
216
+ case 'batch':
217
+ return this.getCacheTTL('eod');
218
+ case 'intraday':
219
+ return this.getCacheTTL('intraday', request.interval);
220
+ default:
221
+ return this.cacheConfig.ttl;
188
222
  }
223
+ }
189
224
 
190
- if (fetchedPrices.length === 0 && lastError) {
191
- throw new Error(
192
- `Failed to fetch prices from all providers. Last error: ${lastError.message}`
193
- );
225
+ /**
226
+ * Get human-readable description of request
227
+ */
228
+ private getRequestDescription(request: IStockDataRequest): string {
229
+ switch (request.type) {
230
+ case 'current':
231
+ return `current price for ${request.ticker}${request.exchange ? ` on ${request.exchange}` : ''}`;
232
+ case 'historical':
233
+ return `historical prices for ${request.ticker} from ${request.from.toISOString().split('T')[0]} to ${request.to.toISOString().split('T')[0]}`;
234
+ case 'intraday':
235
+ return `intraday ${request.interval} prices for ${request.ticker}`;
236
+ case 'batch':
237
+ return `batch prices for ${request.tickers.length} tickers`;
238
+ default:
239
+ return 'data';
194
240
  }
195
-
196
- return [...cachedPrices, ...fetchedPrices];
197
241
  }
198
242
 
199
243
  public async checkProvidersHealth(): Promise<Map<string, boolean>> {
@@ -271,19 +315,38 @@ export class StockPriceService implements IProviderRegistry {
271
315
  throw lastError || new Error('Unknown error during fetch');
272
316
  }
273
317
 
274
- private getCacheKey(request: IStockQuoteRequest): string {
275
- return `${request.ticker}:${request.includeExtendedHours || false}`;
318
+ /**
319
+ * New cache key generation for discriminated union requests
320
+ */
321
+ private getDataCacheKey(request: IStockDataRequest): string {
322
+ switch (request.type) {
323
+ case 'current':
324
+ return `current:${request.ticker}${request.exchange ? `:${request.exchange}` : ''}`;
325
+ case 'historical':
326
+ const fromStr = request.from.toISOString().split('T')[0];
327
+ const toStr = request.to.toISOString().split('T')[0];
328
+ return `historical:${request.ticker}:${fromStr}:${toStr}${request.exchange ? `:${request.exchange}` : ''}`;
329
+ case 'intraday':
330
+ const dateStr = request.date ? request.date.toISOString().split('T')[0] : 'latest';
331
+ return `intraday:${request.ticker}:${request.interval}:${dateStr}${request.exchange ? `:${request.exchange}` : ''}`;
332
+ case 'batch':
333
+ const tickers = request.tickers.sort().join(',');
334
+ return `batch:${tickers}${request.exchange ? `:${request.exchange}` : ''}`;
335
+ default:
336
+ return `unknown:${JSON.stringify(request)}`;
337
+ }
276
338
  }
277
339
 
278
- private getFromCache(key: string): IStockPrice | null {
340
+ private getFromCache(key: string): IStockPrice | IStockPrice[] | null {
279
341
  const entry = this.cache.get(key);
280
-
342
+
281
343
  if (!entry) {
282
344
  return null;
283
345
  }
284
346
 
347
+ // Check if cache entry has expired
285
348
  const age = Date.now() - entry.timestamp.getTime();
286
- if (age > this.cacheConfig.ttl) {
349
+ if (entry.ttl !== Infinity && age > entry.ttl) {
287
350
  this.cache.delete(key);
288
351
  return null;
289
352
  }
@@ -291,7 +354,7 @@ export class StockPriceService implements IProviderRegistry {
291
354
  return entry.price;
292
355
  }
293
356
 
294
- private addToCache(key: string, price: IStockPrice): void {
357
+ private addToCache(key: string, price: IStockPrice | IStockPrice[], ttl?: number): void {
295
358
  // Enforce max entries limit
296
359
  if (this.cache.size >= this.cacheConfig.maxEntries) {
297
360
  // Remove oldest entry
@@ -303,7 +366,8 @@ export class StockPriceService implements IProviderRegistry {
303
366
 
304
367
  this.cache.set(key, {
305
368
  price,
306
- timestamp: new Date()
369
+ timestamp: new Date(),
370
+ ttl: ttl || this.cacheConfig.ttl
307
371
  });
308
372
  }
309
373
  }