@fin.cx/opendata 2.1.0 → 3.1.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.fundamentalsservice.d.ts +104 -0
- package/dist_ts/stocks/classes.fundamentalsservice.js +298 -0
- package/dist_ts/stocks/classes.stockservice.d.ts +16 -7
- package/dist_ts/stocks/classes.stockservice.js +17 -98
- package/dist_ts/stocks/index.d.ts +3 -0
- package/dist_ts/stocks/index.js +5 -2
- package/dist_ts/stocks/interfaces/fundamentals.d.ts +82 -0
- package/dist_ts/stocks/interfaces/fundamentals.js +6 -0
- package/dist_ts/stocks/interfaces/provider.d.ts +2 -3
- package/dist_ts/stocks/interfaces/stockprice.d.ts +2 -10
- package/dist_ts/stocks/interfaces/stockprice.js +2 -2
- package/dist_ts/stocks/providers/provider.marketstack.d.ts +6 -11
- package/dist_ts/stocks/providers/provider.marketstack.js +32 -24
- package/dist_ts/stocks/providers/provider.secedgar.d.ts +102 -0
- package/dist_ts/stocks/providers/provider.secedgar.js +343 -0
- package/dist_ts/stocks/providers/provider.yahoo.d.ts +13 -3
- package/dist_ts/stocks/providers/provider.yahoo.js +27 -4
- package/package.json +1 -1
- package/readme.md +383 -388
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/stocks/classes.fundamentalsservice.ts +404 -0
- package/ts/stocks/classes.stockservice.ts +28 -140
- package/ts/stocks/index.ts +5 -2
- package/ts/stocks/interfaces/fundamentals.ts +102 -0
- package/ts/stocks/interfaces/provider.ts +5 -6
- package/ts/stocks/interfaces/stockprice.ts +4 -19
- package/ts/stocks/providers/provider.marketstack.ts +39 -30
- package/ts/stocks/providers/provider.secedgar.ts +429 -0
- package/ts/stocks/providers/provider.yahoo.ts +33 -4
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.1.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
|
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type {
|
|
3
|
+
IFundamentalsProvider,
|
|
4
|
+
IFundamentalsProviderConfig,
|
|
5
|
+
IFundamentalsProviderRegistry,
|
|
6
|
+
IStockFundamentals,
|
|
7
|
+
IFundamentalsRequest
|
|
8
|
+
} from './interfaces/fundamentals.js';
|
|
9
|
+
|
|
10
|
+
interface IProviderEntry {
|
|
11
|
+
provider: IFundamentalsProvider;
|
|
12
|
+
config: IFundamentalsProviderConfig;
|
|
13
|
+
lastError?: Error;
|
|
14
|
+
lastErrorTime?: Date;
|
|
15
|
+
successCount: number;
|
|
16
|
+
errorCount: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ICacheEntry {
|
|
20
|
+
fundamentals: IStockFundamentals | IStockFundamentals[];
|
|
21
|
+
timestamp: Date;
|
|
22
|
+
ttl: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Service for managing fundamental data providers and caching
|
|
27
|
+
* Parallel to StockPriceService but for fundamental data instead of prices
|
|
28
|
+
*/
|
|
29
|
+
export class FundamentalsService implements IFundamentalsProviderRegistry {
|
|
30
|
+
private providers = new Map<string, IProviderEntry>();
|
|
31
|
+
private cache = new Map<string, ICacheEntry>();
|
|
32
|
+
private logger = console;
|
|
33
|
+
|
|
34
|
+
private cacheConfig = {
|
|
35
|
+
ttl: 90 * 24 * 60 * 60 * 1000, // 90 days default (fundamentals change quarterly)
|
|
36
|
+
maxEntries: 10000
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
constructor(cacheConfig?: { ttl?: number; maxEntries?: number }) {
|
|
40
|
+
if (cacheConfig) {
|
|
41
|
+
this.cacheConfig = { ...this.cacheConfig, ...cacheConfig };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Register a fundamentals provider
|
|
47
|
+
*/
|
|
48
|
+
public register(provider: IFundamentalsProvider, config?: IFundamentalsProviderConfig): void {
|
|
49
|
+
const defaultConfig: IFundamentalsProviderConfig = {
|
|
50
|
+
enabled: true,
|
|
51
|
+
priority: provider.priority,
|
|
52
|
+
timeout: 30000, // Longer timeout for fundamental data
|
|
53
|
+
retryAttempts: 2,
|
|
54
|
+
retryDelay: 1000,
|
|
55
|
+
cacheTTL: this.cacheConfig.ttl
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const mergedConfig = { ...defaultConfig, ...config };
|
|
59
|
+
|
|
60
|
+
this.providers.set(provider.name, {
|
|
61
|
+
provider,
|
|
62
|
+
config: mergedConfig,
|
|
63
|
+
successCount: 0,
|
|
64
|
+
errorCount: 0
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
console.log(`Registered fundamentals provider: ${provider.name}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Unregister a provider
|
|
72
|
+
*/
|
|
73
|
+
public unregister(providerName: string): void {
|
|
74
|
+
this.providers.delete(providerName);
|
|
75
|
+
console.log(`Unregistered fundamentals provider: ${providerName}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get a specific provider by name
|
|
80
|
+
*/
|
|
81
|
+
public getProvider(name: string): IFundamentalsProvider | undefined {
|
|
82
|
+
return this.providers.get(name)?.provider;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get all registered providers
|
|
87
|
+
*/
|
|
88
|
+
public getAllProviders(): IFundamentalsProvider[] {
|
|
89
|
+
return Array.from(this.providers.values()).map(entry => entry.provider);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get enabled providers sorted by priority
|
|
94
|
+
*/
|
|
95
|
+
public getEnabledProviders(): IFundamentalsProvider[] {
|
|
96
|
+
return Array.from(this.providers.values())
|
|
97
|
+
.filter(entry => entry.config.enabled)
|
|
98
|
+
.sort((a, b) => (b.config.priority || 0) - (a.config.priority || 0))
|
|
99
|
+
.map(entry => entry.provider);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get fundamental data for a single ticker
|
|
104
|
+
*/
|
|
105
|
+
public async getFundamentals(ticker: string): Promise<IStockFundamentals> {
|
|
106
|
+
const result = await this.getData({
|
|
107
|
+
type: 'fundamentals-current',
|
|
108
|
+
ticker
|
|
109
|
+
});
|
|
110
|
+
return result as IStockFundamentals;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get fundamental data for multiple tickers
|
|
115
|
+
*/
|
|
116
|
+
public async getBatchFundamentals(tickers: string[]): Promise<IStockFundamentals[]> {
|
|
117
|
+
const result = await this.getData({
|
|
118
|
+
type: 'fundamentals-batch',
|
|
119
|
+
tickers
|
|
120
|
+
});
|
|
121
|
+
return result as IStockFundamentals[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Unified data fetching method
|
|
126
|
+
*/
|
|
127
|
+
public async getData(
|
|
128
|
+
request: IFundamentalsRequest
|
|
129
|
+
): Promise<IStockFundamentals | IStockFundamentals[]> {
|
|
130
|
+
const cacheKey = this.getCacheKey(request);
|
|
131
|
+
const cached = this.getFromCache(cacheKey);
|
|
132
|
+
|
|
133
|
+
if (cached) {
|
|
134
|
+
console.log(`Cache hit for ${this.getRequestDescription(request)}`);
|
|
135
|
+
return cached;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const providers = this.getEnabledProviders();
|
|
139
|
+
if (providers.length === 0) {
|
|
140
|
+
throw new Error('No fundamentals providers available');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let lastError: Error | undefined;
|
|
144
|
+
|
|
145
|
+
for (const provider of providers) {
|
|
146
|
+
const entry = this.providers.get(provider.name)!;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const result = await this.fetchWithRetry(
|
|
150
|
+
() => provider.fetchData(request),
|
|
151
|
+
entry.config
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
entry.successCount++;
|
|
155
|
+
|
|
156
|
+
// Use provider-specific cache TTL or default
|
|
157
|
+
const ttl = entry.config.cacheTTL || this.cacheConfig.ttl;
|
|
158
|
+
this.addToCache(cacheKey, result, ttl);
|
|
159
|
+
|
|
160
|
+
console.log(`Successfully fetched ${this.getRequestDescription(request)} from ${provider.name}`);
|
|
161
|
+
return result;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
entry.errorCount++;
|
|
164
|
+
entry.lastError = error as Error;
|
|
165
|
+
entry.lastErrorTime = new Date();
|
|
166
|
+
lastError = error as Error;
|
|
167
|
+
|
|
168
|
+
console.warn(
|
|
169
|
+
`Provider ${provider.name} failed for ${this.getRequestDescription(request)}: ${error.message}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Failed to fetch ${this.getRequestDescription(request)} from all providers. Last error: ${lastError?.message}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Enrich fundamentals with calculated metrics using current price
|
|
181
|
+
*/
|
|
182
|
+
public async enrichWithPrice(
|
|
183
|
+
fundamentals: IStockFundamentals,
|
|
184
|
+
price: number
|
|
185
|
+
): Promise<IStockFundamentals> {
|
|
186
|
+
const enriched = { ...fundamentals };
|
|
187
|
+
|
|
188
|
+
// Calculate market cap: price × shares outstanding
|
|
189
|
+
if (fundamentals.sharesOutstanding) {
|
|
190
|
+
enriched.marketCap = price * fundamentals.sharesOutstanding;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Calculate P/E ratio: price / EPS
|
|
194
|
+
if (fundamentals.earningsPerShareDiluted && fundamentals.earningsPerShareDiluted > 0) {
|
|
195
|
+
enriched.priceToEarnings = price / fundamentals.earningsPerShareDiluted;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Calculate price-to-book: market cap / stockholders equity
|
|
199
|
+
if (enriched.marketCap && fundamentals.stockholdersEquity && fundamentals.stockholdersEquity > 0) {
|
|
200
|
+
enriched.priceToBook = enriched.marketCap / fundamentals.stockholdersEquity;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return enriched;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Enrich batch fundamentals with prices
|
|
208
|
+
*/
|
|
209
|
+
public async enrichBatchWithPrices(
|
|
210
|
+
fundamentalsList: IStockFundamentals[],
|
|
211
|
+
priceMap: Map<string, number>
|
|
212
|
+
): Promise<IStockFundamentals[]> {
|
|
213
|
+
return Promise.all(
|
|
214
|
+
fundamentalsList.map(fundamentals => {
|
|
215
|
+
const price = priceMap.get(fundamentals.ticker);
|
|
216
|
+
if (price) {
|
|
217
|
+
return this.enrichWithPrice(fundamentals, price);
|
|
218
|
+
}
|
|
219
|
+
return Promise.resolve(fundamentals);
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check health of all providers
|
|
226
|
+
*/
|
|
227
|
+
public async checkProvidersHealth(): Promise<Map<string, boolean>> {
|
|
228
|
+
const health = new Map<string, boolean>();
|
|
229
|
+
|
|
230
|
+
for (const [name, entry] of this.providers) {
|
|
231
|
+
if (!entry.config.enabled) {
|
|
232
|
+
health.set(name, false);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const isAvailable = await entry.provider.isAvailable();
|
|
238
|
+
health.set(name, isAvailable);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
health.set(name, false);
|
|
241
|
+
console.error(`Health check failed for ${name}:`, error);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return health;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get provider statistics
|
|
250
|
+
*/
|
|
251
|
+
public getProviderStats(): Map<
|
|
252
|
+
string,
|
|
253
|
+
{
|
|
254
|
+
successCount: number;
|
|
255
|
+
errorCount: number;
|
|
256
|
+
lastError?: string;
|
|
257
|
+
lastErrorTime?: Date;
|
|
258
|
+
}
|
|
259
|
+
> {
|
|
260
|
+
const stats = new Map();
|
|
261
|
+
|
|
262
|
+
for (const [name, entry] of this.providers) {
|
|
263
|
+
stats.set(name, {
|
|
264
|
+
successCount: entry.successCount,
|
|
265
|
+
errorCount: entry.errorCount,
|
|
266
|
+
lastError: entry.lastError?.message,
|
|
267
|
+
lastErrorTime: entry.lastErrorTime
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return stats;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Clear all cached data
|
|
276
|
+
*/
|
|
277
|
+
public clearCache(): void {
|
|
278
|
+
this.cache.clear();
|
|
279
|
+
console.log('Fundamentals cache cleared');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Set cache TTL
|
|
284
|
+
*/
|
|
285
|
+
public setCacheTTL(ttl: number): void {
|
|
286
|
+
this.cacheConfig.ttl = ttl;
|
|
287
|
+
console.log(`Fundamentals cache TTL set to ${ttl}ms`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get cache statistics
|
|
292
|
+
*/
|
|
293
|
+
public getCacheStats(): {
|
|
294
|
+
size: number;
|
|
295
|
+
maxEntries: number;
|
|
296
|
+
ttl: number;
|
|
297
|
+
} {
|
|
298
|
+
return {
|
|
299
|
+
size: this.cache.size,
|
|
300
|
+
maxEntries: this.cacheConfig.maxEntries,
|
|
301
|
+
ttl: this.cacheConfig.ttl
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Fetch with retry logic
|
|
307
|
+
*/
|
|
308
|
+
private async fetchWithRetry<T>(
|
|
309
|
+
fetchFn: () => Promise<T>,
|
|
310
|
+
config: IFundamentalsProviderConfig
|
|
311
|
+
): Promise<T> {
|
|
312
|
+
const maxAttempts = config.retryAttempts || 1;
|
|
313
|
+
let lastError: Error | undefined;
|
|
314
|
+
|
|
315
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
316
|
+
try {
|
|
317
|
+
return await fetchFn();
|
|
318
|
+
} catch (error) {
|
|
319
|
+
lastError = error as Error;
|
|
320
|
+
|
|
321
|
+
if (attempt < maxAttempts) {
|
|
322
|
+
const delay = (config.retryDelay || 1000) * attempt;
|
|
323
|
+
console.log(`Retry attempt ${attempt} after ${delay}ms`);
|
|
324
|
+
await plugins.smartdelay.delayFor(delay);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
throw lastError || new Error('Unknown error during fetch');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Generate cache key for request
|
|
334
|
+
*/
|
|
335
|
+
private getCacheKey(request: IFundamentalsRequest): string {
|
|
336
|
+
switch (request.type) {
|
|
337
|
+
case 'fundamentals-current':
|
|
338
|
+
return `fundamentals:${request.ticker}`;
|
|
339
|
+
case 'fundamentals-batch':
|
|
340
|
+
const tickers = request.tickers.sort().join(',');
|
|
341
|
+
return `fundamentals-batch:${tickers}`;
|
|
342
|
+
default:
|
|
343
|
+
return `unknown:${JSON.stringify(request)}`;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Get from cache if not expired
|
|
349
|
+
*/
|
|
350
|
+
private getFromCache(key: string): IStockFundamentals | IStockFundamentals[] | null {
|
|
351
|
+
const entry = this.cache.get(key);
|
|
352
|
+
|
|
353
|
+
if (!entry) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Check if cache entry has expired
|
|
358
|
+
const age = Date.now() - entry.timestamp.getTime();
|
|
359
|
+
if (entry.ttl !== Infinity && age > entry.ttl) {
|
|
360
|
+
this.cache.delete(key);
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return entry.fundamentals;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Add to cache with TTL
|
|
369
|
+
*/
|
|
370
|
+
private addToCache(
|
|
371
|
+
key: string,
|
|
372
|
+
fundamentals: IStockFundamentals | IStockFundamentals[],
|
|
373
|
+
ttl?: number
|
|
374
|
+
): void {
|
|
375
|
+
// Enforce max entries limit
|
|
376
|
+
if (this.cache.size >= this.cacheConfig.maxEntries) {
|
|
377
|
+
// Remove oldest entry
|
|
378
|
+
const oldestKey = this.cache.keys().next().value;
|
|
379
|
+
if (oldestKey) {
|
|
380
|
+
this.cache.delete(oldestKey);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
this.cache.set(key, {
|
|
385
|
+
fundamentals,
|
|
386
|
+
timestamp: new Date(),
|
|
387
|
+
ttl: ttl || this.cacheConfig.ttl
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get human-readable request description
|
|
393
|
+
*/
|
|
394
|
+
private getRequestDescription(request: IFundamentalsRequest): string {
|
|
395
|
+
switch (request.type) {
|
|
396
|
+
case 'fundamentals-current':
|
|
397
|
+
return `fundamentals for ${request.ticker}`;
|
|
398
|
+
case 'fundamentals-batch':
|
|
399
|
+
return `fundamentals for ${request.tickers.length} tickers`;
|
|
400
|
+
default:
|
|
401
|
+
return 'fundamentals data';
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -2,8 +2,6 @@ import * as plugins from '../plugins.js';
|
|
|
2
2
|
import type { IStockProvider, IProviderConfig, IProviderRegistry } from './interfaces/provider.js';
|
|
3
3
|
import type {
|
|
4
4
|
IStockPrice,
|
|
5
|
-
IStockQuoteRequest,
|
|
6
|
-
IStockBatchQuoteRequest,
|
|
7
5
|
IStockPriceError,
|
|
8
6
|
IStockDataRequest,
|
|
9
7
|
IStockCurrentRequest,
|
|
@@ -13,6 +11,15 @@ import type {
|
|
|
13
11
|
TIntervalType
|
|
14
12
|
} from './interfaces/stockprice.js';
|
|
15
13
|
|
|
14
|
+
// Simple request interfaces for convenience methods
|
|
15
|
+
interface ISimpleQuoteRequest {
|
|
16
|
+
ticker: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ISimpleBatchRequest {
|
|
20
|
+
tickers: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
interface IProviderEntry {
|
|
17
24
|
provider: IStockProvider;
|
|
18
25
|
config: IProviderConfig;
|
|
@@ -122,132 +129,26 @@ export class StockPriceService implements IProviderRegistry {
|
|
|
122
129
|
.map(entry => entry.provider);
|
|
123
130
|
}
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const providers = this.getEnabledProviders();
|
|
135
|
-
if (providers.length === 0) {
|
|
136
|
-
throw new Error('No stock price providers available');
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
let lastError: Error | undefined;
|
|
140
|
-
|
|
141
|
-
for (const provider of providers) {
|
|
142
|
-
const entry = this.providers.get(provider.name)!;
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const price = await this.fetchWithRetry(
|
|
146
|
-
() => provider.fetchPrice(request),
|
|
147
|
-
entry.config
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
entry.successCount++;
|
|
151
|
-
|
|
152
|
-
// Use smart TTL based on data type
|
|
153
|
-
const ttl = this.getCacheTTL(price.dataType);
|
|
154
|
-
this.addToCache(cacheKey, price, ttl);
|
|
155
|
-
|
|
156
|
-
console.log(`Successfully fetched ${request.ticker} from ${provider.name}`);
|
|
157
|
-
return price;
|
|
158
|
-
} catch (error) {
|
|
159
|
-
entry.errorCount++;
|
|
160
|
-
entry.lastError = error as Error;
|
|
161
|
-
entry.lastErrorTime = new Date();
|
|
162
|
-
lastError = error as Error;
|
|
163
|
-
|
|
164
|
-
console.warn(
|
|
165
|
-
`Provider ${provider.name} failed for ${request.ticker}: ${error.message}`
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
throw new Error(
|
|
171
|
-
`Failed to fetch price for ${request.ticker} from all providers. Last error: ${lastError?.message}`
|
|
172
|
-
);
|
|
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;
|
|
173
141
|
}
|
|
174
142
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (cached) {
|
|
185
|
-
cachedPrices.push(cached);
|
|
186
|
-
} else {
|
|
187
|
-
tickersToFetch.push(ticker);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (tickersToFetch.length === 0) {
|
|
192
|
-
console.log(`All ${request.tickers.length} tickers served from cache`);
|
|
193
|
-
return cachedPrices;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const providers = this.getEnabledProviders();
|
|
197
|
-
if (providers.length === 0) {
|
|
198
|
-
throw new Error('No stock price providers available');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
let lastError: Error | undefined;
|
|
202
|
-
let fetchedPrices: IStockPrice[] = [];
|
|
203
|
-
|
|
204
|
-
for (const provider of providers) {
|
|
205
|
-
const entry = this.providers.get(provider.name)!;
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
fetchedPrices = await this.fetchWithRetry(
|
|
209
|
-
() => provider.fetchPrices({
|
|
210
|
-
tickers: tickersToFetch,
|
|
211
|
-
includeExtendedHours: request.includeExtendedHours
|
|
212
|
-
}),
|
|
213
|
-
entry.config
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
entry.successCount++;
|
|
217
|
-
|
|
218
|
-
// Cache the fetched prices with smart TTL
|
|
219
|
-
for (const price of fetchedPrices) {
|
|
220
|
-
const cacheKey = this.getCacheKey({
|
|
221
|
-
ticker: price.ticker,
|
|
222
|
-
includeExtendedHours: request.includeExtendedHours
|
|
223
|
-
});
|
|
224
|
-
const ttl = this.getCacheTTL(price.dataType);
|
|
225
|
-
this.addToCache(cacheKey, price, ttl);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
console.log(
|
|
229
|
-
`Successfully fetched ${fetchedPrices.length} prices from ${provider.name}`
|
|
230
|
-
);
|
|
231
|
-
break;
|
|
232
|
-
} catch (error) {
|
|
233
|
-
entry.errorCount++;
|
|
234
|
-
entry.lastError = error as Error;
|
|
235
|
-
entry.lastErrorTime = new Date();
|
|
236
|
-
lastError = error as Error;
|
|
237
|
-
|
|
238
|
-
console.warn(
|
|
239
|
-
`Provider ${provider.name} failed for batch request: ${error.message}`
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (fetchedPrices.length === 0 && lastError) {
|
|
245
|
-
throw new Error(
|
|
246
|
-
`Failed to fetch prices from all providers. Last error: ${lastError.message}`
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return [...cachedPrices, ...fetchedPrices];
|
|
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[];
|
|
251
152
|
}
|
|
252
153
|
|
|
253
154
|
/**
|
|
@@ -272,15 +173,9 @@ export class StockPriceService implements IProviderRegistry {
|
|
|
272
173
|
for (const provider of providers) {
|
|
273
174
|
const entry = this.providers.get(provider.name)!;
|
|
274
175
|
|
|
275
|
-
// Check if provider supports the new fetchData method
|
|
276
|
-
if (typeof (provider as any).fetchData !== 'function') {
|
|
277
|
-
console.warn(`Provider ${provider.name} does not support new API, skipping`);
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
176
|
try {
|
|
282
177
|
const result = await this.fetchWithRetry(
|
|
283
|
-
() =>
|
|
178
|
+
() => provider.fetchData(request),
|
|
284
179
|
entry.config
|
|
285
180
|
) as IStockPrice | IStockPrice[];
|
|
286
181
|
|
|
@@ -420,13 +315,6 @@ export class StockPriceService implements IProviderRegistry {
|
|
|
420
315
|
throw lastError || new Error('Unknown error during fetch');
|
|
421
316
|
}
|
|
422
317
|
|
|
423
|
-
/**
|
|
424
|
-
* Legacy cache key generation
|
|
425
|
-
*/
|
|
426
|
-
private getCacheKey(request: IStockQuoteRequest): string {
|
|
427
|
-
return `${request.ticker}:${request.includeExtendedHours || false}`;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
318
|
/**
|
|
431
319
|
* New cache key generation for discriminated union requests
|
|
432
320
|
*/
|
package/ts/stocks/index.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// Export all interfaces
|
|
2
2
|
export * from './interfaces/stockprice.js';
|
|
3
3
|
export * from './interfaces/provider.js';
|
|
4
|
+
export * from './interfaces/fundamentals.js';
|
|
4
5
|
|
|
5
|
-
// Export main
|
|
6
|
+
// Export main services
|
|
6
7
|
export * from './classes.stockservice.js';
|
|
8
|
+
export * from './classes.fundamentalsservice.js';
|
|
7
9
|
|
|
8
10
|
// Export providers
|
|
9
11
|
export * from './providers/provider.yahoo.js';
|
|
10
|
-
export * from './providers/provider.marketstack.js';
|
|
12
|
+
export * from './providers/provider.marketstack.js';
|
|
13
|
+
export * from './providers/provider.secedgar.js';
|