@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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/stocks/classes.stockservice.d.ts +40 -4
- package/dist_ts/stocks/classes.stockservice.js +126 -69
- package/dist_ts/stocks/interfaces/provider.d.ts +2 -3
- package/dist_ts/stocks/interfaces/stockprice.d.ts +47 -4
- package/dist_ts/stocks/interfaces/stockprice.js +2 -2
- package/dist_ts/stocks/providers/provider.marketstack.d.ts +40 -11
- package/dist_ts/stocks/providers/provider.marketstack.js +167 -22
- package/dist_ts/stocks/providers/provider.yahoo.d.ts +13 -3
- package/dist_ts/stocks/providers/provider.yahoo.js +33 -6
- package/package.json +1 -1
- package/readme.md +249 -22
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/stocks/classes.stockservice.ts +162 -98
- package/ts/stocks/interfaces/provider.ts +5 -6
- package/ts/stocks/interfaces/stockprice.ts +68 -10
- package/ts/stocks/providers/provider.marketstack.ts +198 -22
- package/ts/stocks/providers/provider.yahoo.ts +39 -6
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
|
|
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
|
|
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,
|
|
41
|
-
maxEntries:
|
|
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
|
|
51
|
-
const apple = await stockService.
|
|
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
|
-
//
|
|
55
|
-
const
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
- **
|
|
106
|
-
- **
|
|
107
|
-
- **
|
|
108
|
-
- **
|
|
109
|
-
- **
|
|
110
|
-
- **
|
|
111
|
-
- **
|
|
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:
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@fin.cx/opendata',
|
|
6
|
-
version: '
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
77
|
-
|
|
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
|
|
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
|
|
97
|
-
() => provider.
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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
|
|
203
|
+
`Failed to fetch ${this.getRequestDescription(request)} from all providers. Last error: ${lastError?.message}`
|
|
119
204
|
);
|
|
120
205
|
}
|
|
121
206
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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 >
|
|
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
|
}
|