@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.
Files changed (30) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/stocks/classes.fundamentalsservice.d.ts +104 -0
  3. package/dist_ts/stocks/classes.fundamentalsservice.js +298 -0
  4. package/dist_ts/stocks/classes.stockservice.d.ts +16 -7
  5. package/dist_ts/stocks/classes.stockservice.js +17 -98
  6. package/dist_ts/stocks/index.d.ts +3 -0
  7. package/dist_ts/stocks/index.js +5 -2
  8. package/dist_ts/stocks/interfaces/fundamentals.d.ts +82 -0
  9. package/dist_ts/stocks/interfaces/fundamentals.js +6 -0
  10. package/dist_ts/stocks/interfaces/provider.d.ts +2 -3
  11. package/dist_ts/stocks/interfaces/stockprice.d.ts +2 -10
  12. package/dist_ts/stocks/interfaces/stockprice.js +2 -2
  13. package/dist_ts/stocks/providers/provider.marketstack.d.ts +6 -11
  14. package/dist_ts/stocks/providers/provider.marketstack.js +32 -24
  15. package/dist_ts/stocks/providers/provider.secedgar.d.ts +102 -0
  16. package/dist_ts/stocks/providers/provider.secedgar.js +343 -0
  17. package/dist_ts/stocks/providers/provider.yahoo.d.ts +13 -3
  18. package/dist_ts/stocks/providers/provider.yahoo.js +27 -4
  19. package/package.json +1 -1
  20. package/readme.md +383 -388
  21. package/ts/00_commitinfo_data.ts +1 -1
  22. package/ts/stocks/classes.fundamentalsservice.ts +404 -0
  23. package/ts/stocks/classes.stockservice.ts +28 -140
  24. package/ts/stocks/index.ts +5 -2
  25. package/ts/stocks/interfaces/fundamentals.ts +102 -0
  26. package/ts/stocks/interfaces/provider.ts +5 -6
  27. package/ts/stocks/interfaces/stockprice.ts +4 -19
  28. package/ts/stocks/providers/provider.marketstack.ts +39 -30
  29. package/ts/stocks/providers/provider.secedgar.ts +429 -0
  30. package/ts/stocks/providers/provider.yahoo.ts +33 -4
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/opendata',
6
- version: '2.1.0',
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
- public async getPrice(request: IStockQuoteRequest): Promise<IStockPrice> {
126
- const cacheKey = this.getCacheKey(request);
127
- const cached = this.getFromCache(cacheKey) as IStockPrice | null;
128
-
129
- if (cached) {
130
- console.log(`Cache hit for ${request.ticker}`);
131
- return cached;
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
- public async getPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]> {
176
- const cachedPrices: IStockPrice[] = [];
177
- const tickersToFetch: string[] = [];
178
-
179
- // Check cache for each ticker
180
- for (const ticker of request.tickers) {
181
- const cacheKey = this.getCacheKey({ ticker, includeExtendedHours: request.includeExtendedHours });
182
- const cached = this.getFromCache(cacheKey) as IStockPrice | null;
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
- () => (provider as any).fetchData(request),
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
  */
@@ -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 service
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';