@fin.cx/opendata 3.1.0 → 3.2.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
@@ -14,64 +14,110 @@ pnpm add @fin.cx/opendata
14
14
 
15
15
  ## Quick Start
16
16
 
17
- ### 📈 Stock Market Data
17
+ ### ✨ Unified Stock Data API (Recommended)
18
18
 
19
- Get real-time prices with company information included automatically:
19
+ Get complete stock data with automatic enrichment - the elegant way:
20
20
 
21
21
  ```typescript
22
- import { StockPriceService, MarketstackProvider } from '@fin.cx/opendata';
22
+ import { StockDataService, YahooFinanceProvider, SecEdgarProvider } from '@fin.cx/opendata';
23
23
 
24
- // Initialize service with smart caching
25
- const stockService = new StockPriceService({
26
- ttl: 60000, // Cache TTL (historical cached forever)
27
- maxEntries: 10000
24
+ // Initialize unified service
25
+ const stockData = new StockDataService();
26
+
27
+ // Register providers
28
+ stockData.registerPriceProvider(new YahooFinanceProvider());
29
+ stockData.registerFundamentalsProvider(new SecEdgarProvider({
30
+ userAgent: 'YourCompany youremail@example.com'
31
+ }));
32
+
33
+ // ✨ Get complete stock data with ONE method call
34
+ const apple = await stockData.getStockData('AAPL');
35
+
36
+ console.log({
37
+ company: apple.fundamentals.companyName, // "Apple Inc."
38
+ price: apple.price.price, // $270.37
39
+ marketCap: apple.fundamentals.marketCap, // $4.13T (auto-calculated!)
40
+ peRatio: apple.fundamentals.priceToEarnings, // 28.42 (auto-calculated!)
41
+ pbRatio: apple.fundamentals.priceToBook, // 45.12 (auto-calculated!)
42
+ eps: apple.fundamentals.earningsPerShareDiluted, // $6.13
43
+ revenue: apple.fundamentals.revenue // $385.6B
28
44
  });
29
45
 
30
- // Register provider with API key
31
- stockService.register(new MarketstackProvider('YOUR_API_KEY'));
46
+ // Batch fetch with automatic enrichment
47
+ const stocks = await stockData.getBatchStockData(['AAPL', 'MSFT', 'GOOGL']);
32
48
 
33
- // Get current price with company name (zero extra API calls!)
34
- const apple = await stockService.getData({ type: 'current', ticker: 'AAPL' });
49
+ stocks.forEach(stock => {
50
+ console.log(`${stock.ticker}: $${stock.price.price.toFixed(2)}, ` +
51
+ `P/E ${stock.fundamentals?.priceToEarnings?.toFixed(2)}`);
52
+ });
53
+ // Output:
54
+ // AAPL: $270.37, P/E 28.42
55
+ // MSFT: $425.50, P/E 34.21
56
+ // GOOGL: $142.15, P/E 25.63
57
+ ```
58
+
59
+ **Why use the unified API?**
60
+ - ✅ **Single service** for both prices and fundamentals
61
+ - ✅ **Automatic enrichment** - Market cap, P/E, P/B calculated automatically
62
+ - ✅ **One method call** - No manual `enrichWithPrice()` calls
63
+ - ✅ **Simplified code** - Less boilerplate, more readable
64
+ - ✅ **Type-safe** - Full TypeScript support
65
+
66
+ ### 📈 Price-Only Data (Alternative)
35
67
 
68
+ If you only need prices without fundamentals:
69
+
70
+ ```typescript
71
+ import { StockDataService, YahooFinanceProvider } from '@fin.cx/opendata';
72
+
73
+ const stockData = new StockDataService();
74
+ stockData.registerPriceProvider(new YahooFinanceProvider());
75
+
76
+ // Get just the price
77
+ const price = await stockData.getPrice('AAPL');
78
+ console.log(`${price.ticker}: $${price.price}`);
79
+
80
+ // Or use StockPriceService directly for more control
81
+ import { StockPriceService } from '@fin.cx/opendata';
82
+
83
+ const stockService = new StockPriceService({ ttl: 60000 });
84
+ stockService.register(new YahooFinanceProvider());
85
+
86
+ const apple = await stockService.getData({ type: 'current', ticker: 'AAPL' });
36
87
  console.log(`${apple.companyFullName}: $${apple.price}`);
37
- // Output: "Apple Inc (NASDAQ:AAPL): $270.37"
38
88
  ```
39
89
 
40
- ### 💰 Fundamental Financial Data
90
+ ### 💰 Fundamentals-Only Data (Alternative)
41
91
 
42
- Access comprehensive financial metrics from SEC filings - completely FREE:
92
+ If you only need fundamentals without prices:
43
93
 
44
94
  ```typescript
45
- import { SecEdgarProvider, FundamentalsService } from '@fin.cx/opendata';
95
+ import { StockDataService, SecEdgarProvider } from '@fin.cx/opendata';
46
96
 
47
- // Setup SEC EDGAR provider (no API key required!)
48
- const secEdgar = new SecEdgarProvider({
97
+ const stockData = new StockDataService();
98
+ stockData.registerFundamentalsProvider(new SecEdgarProvider({
49
99
  userAgent: 'YourCompany youremail@example.com'
50
- });
51
-
52
- const fundamentalsService = new FundamentalsService();
53
- fundamentalsService.register(secEdgar);
100
+ }));
54
101
 
55
- // Fetch fundamentals for Apple
56
- const fundamentals = await fundamentalsService.getFundamentals('AAPL');
102
+ // Get just fundamentals
103
+ const fundamentals = await stockData.getFundamentals('AAPL');
57
104
 
58
105
  console.log({
59
- company: fundamentals.companyName, // "Apple Inc."
60
- eps: fundamentals.earningsPerShareDiluted, // $6.13
61
- revenue: fundamentals.revenue, // $385.6B
62
- sharesOutstanding: fundamentals.sharesOutstanding // 15.3B
106
+ company: fundamentals.companyName,
107
+ eps: fundamentals.earningsPerShareDiluted,
108
+ revenue: fundamentals.revenue,
109
+ sharesOutstanding: fundamentals.sharesOutstanding
63
110
  });
64
111
 
65
- // Calculate market cap and P/E ratio
66
- const price = await stockService.getData({ type: 'current', ticker: 'AAPL' });
67
- const enriched = await fundamentalsService.enrichWithPrice(fundamentals, price.price);
112
+ // Or use FundamentalsService directly
113
+ import { FundamentalsService } from '@fin.cx/opendata';
68
114
 
69
- console.log({
70
- marketCap: `$${(enriched.marketCap! / 1_000_000_000_000).toFixed(2)}T`,
71
- peRatio: enriched.priceToEarnings!.toFixed(2),
72
- pbRatio: enriched.priceToBook?.toFixed(2)
73
- });
74
- // Output: { marketCap: "$2.65T", peRatio: "28.42", pbRatio: "45.12" }
115
+ const fundamentalsService = new FundamentalsService();
116
+ fundamentalsService.register(new SecEdgarProvider({
117
+ userAgent: 'YourCompany youremail@example.com'
118
+ }));
119
+
120
+ const data = await fundamentalsService.getFundamentals('AAPL');
75
121
  ```
76
122
 
77
123
  ### 🏢 German Business Intelligence
@@ -136,44 +182,70 @@ const details = await openData.handelsregister.getSpecificCompany({
136
182
 
137
183
  ## Advanced Examples
138
184
 
139
- ### Combined Market Analysis
185
+ ### Combined Market Analysis (Unified API)
140
186
 
141
- Combine price data with fundamentals for comprehensive analysis:
187
+ Analyze multiple companies with automatic enrichment:
142
188
 
143
189
  ```typescript
144
- import { StockPriceService, MarketstackProvider, SecEdgarProvider, FundamentalsService } from '@fin.cx/opendata';
145
-
146
- // Setup services
147
- const stockService = new StockPriceService({ ttl: 60000 });
148
- stockService.register(new MarketstackProvider('YOUR_API_KEY'));
190
+ import { StockDataService, YahooFinanceProvider, SecEdgarProvider } from '@fin.cx/opendata';
149
191
 
150
- const fundamentalsService = new FundamentalsService();
151
- fundamentalsService.register(new SecEdgarProvider({
192
+ // Setup unified service
193
+ const stockData = new StockDataService();
194
+ stockData.registerPriceProvider(new YahooFinanceProvider());
195
+ stockData.registerFundamentalsProvider(new SecEdgarProvider({
152
196
  userAgent: 'YourCompany youremail@example.com'
153
197
  }));
154
198
 
155
- // Analyze multiple companies
199
+ // Analyze multiple companies with ONE call
156
200
  const tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN'];
201
+ const stocks = await stockData.getBatchStockData(tickers);
202
+
203
+ // All metrics are automatically calculated!
204
+ stocks.forEach(stock => {
205
+ if (stock.fundamentals) {
206
+ console.log(`\n${stock.fundamentals.companyName} (${stock.ticker})`);
207
+ console.log(` Price: $${stock.price.price.toFixed(2)}`);
208
+ console.log(` Market Cap: $${(stock.fundamentals.marketCap! / 1e9).toFixed(2)}B`);
209
+ console.log(` P/E Ratio: ${stock.fundamentals.priceToEarnings!.toFixed(2)}`);
210
+ console.log(` Revenue: $${(stock.fundamentals.revenue! / 1e9).toFixed(2)}B`);
211
+ console.log(` EPS: $${stock.fundamentals.earningsPerShareDiluted!.toFixed(2)}`);
212
+ }
213
+ });
157
214
 
215
+ // Or analyze one-by-one with automatic enrichment
158
216
  for (const ticker of tickers) {
159
- // Get price and fundamentals in parallel
160
- const [price, fundamentals] = await Promise.all([
161
- stockService.getData({ type: 'current', ticker }),
162
- fundamentalsService.getFundamentals(ticker)
163
- ]);
164
-
165
- // Calculate metrics
166
- const enriched = await fundamentalsService.enrichWithPrice(fundamentals, price.price);
167
-
168
- console.log(`\n${fundamentals.companyName} (${ticker})`);
169
- console.log(` Price: $${price.price.toFixed(2)}`);
170
- console.log(` Market Cap: $${(enriched.marketCap! / 1e9).toFixed(2)}B`);
171
- console.log(` P/E Ratio: ${enriched.priceToEarnings!.toFixed(2)}`);
172
- console.log(` Revenue: $${(fundamentals.revenue! / 1e9).toFixed(2)}B`);
173
- console.log(` EPS: $${fundamentals.earningsPerShareDiluted!.toFixed(2)}`);
217
+ const stock = await stockData.getStockData(ticker);
218
+
219
+ // Everything is already enriched - no manual calculations needed!
220
+ console.log(`${ticker}: P/E ${stock.fundamentals?.priceToEarnings?.toFixed(2)}`);
174
221
  }
175
222
  ```
176
223
 
224
+ ### Fundamental Data Screening
225
+
226
+ Screen stocks by financial metrics:
227
+
228
+ ```typescript
229
+ // Fetch data for multiple tickers
230
+ const tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META', 'NVDA', 'TSLA'];
231
+ const stocks = await stockData.getBatchStockData(tickers);
232
+
233
+ // Filter by criteria (all metrics auto-calculated!)
234
+ const valueStocks = stocks.filter(stock => {
235
+ const f = stock.fundamentals;
236
+ return f &&
237
+ f.priceToEarnings! < 30 && // P/E under 30
238
+ f.priceToBook! < 10 && // P/B under 10
239
+ f.revenue! > 100_000_000_000; // Revenue > $100B
240
+ });
241
+
242
+ console.log('\n💎 Value Stocks:');
243
+ valueStocks.forEach(stock => {
244
+ console.log(`${stock.ticker}: P/E ${stock.fundamentals!.priceToEarnings!.toFixed(2)}, ` +
245
+ `P/B ${stock.fundamentals!.priceToBook!.toFixed(2)}`);
246
+ });
247
+ ```
248
+
177
249
  ### Historical Data Analysis
178
250
 
179
251
  Fetch and analyze historical price trends:
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/opendata',
6
- version: '3.1.0',
6
+ version: '3.2.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,296 @@
1
+ import * as plugins from '../plugins.js';
2
+
3
+ /**
4
+ * Base provider entry for tracking provider state
5
+ */
6
+ export interface IBaseProviderEntry<TProvider> {
7
+ provider: TProvider;
8
+ config: IBaseProviderConfig;
9
+ lastError?: Error;
10
+ lastErrorTime?: Date;
11
+ successCount: number;
12
+ errorCount: number;
13
+ }
14
+
15
+ /**
16
+ * Base provider configuration
17
+ */
18
+ export interface IBaseProviderConfig {
19
+ enabled: boolean;
20
+ priority: number;
21
+ timeout?: number;
22
+ retryAttempts?: number;
23
+ retryDelay?: number;
24
+ cacheTTL?: number;
25
+ }
26
+
27
+ /**
28
+ * Base provider interface
29
+ */
30
+ export interface IBaseProvider {
31
+ name: string;
32
+ priority: number;
33
+ isAvailable(): Promise<boolean>;
34
+ readonly requiresAuth: boolean;
35
+ readonly rateLimit?: {
36
+ requestsPerMinute: number;
37
+ requestsPerDay?: number;
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Cache entry for any data type
43
+ */
44
+ export interface IBaseCacheEntry<TData> {
45
+ data: TData;
46
+ timestamp: Date;
47
+ ttl: number;
48
+ }
49
+
50
+ /**
51
+ * Base service for managing data providers with caching
52
+ * Shared logic extracted from StockPriceService and FundamentalsService
53
+ */
54
+ export abstract class BaseProviderService<TProvider extends IBaseProvider, TData> {
55
+ protected providers = new Map<string, IBaseProviderEntry<TProvider>>();
56
+ protected cache = new Map<string, IBaseCacheEntry<TData>>();
57
+ protected logger = console;
58
+
59
+ protected cacheConfig = {
60
+ ttl: 60000, // Default 60 seconds
61
+ maxEntries: 10000
62
+ };
63
+
64
+ constructor(cacheConfig?: { ttl?: number; maxEntries?: number }) {
65
+ if (cacheConfig) {
66
+ this.cacheConfig = { ...this.cacheConfig, ...cacheConfig };
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Register a provider
72
+ */
73
+ public register(provider: TProvider, config?: Partial<IBaseProviderConfig>): void {
74
+ const defaultConfig: IBaseProviderConfig = {
75
+ enabled: true,
76
+ priority: provider.priority,
77
+ timeout: 30000,
78
+ retryAttempts: 2,
79
+ retryDelay: 1000,
80
+ cacheTTL: this.cacheConfig.ttl
81
+ };
82
+
83
+ const mergedConfig = { ...defaultConfig, ...config };
84
+
85
+ this.providers.set(provider.name, {
86
+ provider,
87
+ config: mergedConfig,
88
+ successCount: 0,
89
+ errorCount: 0
90
+ });
91
+
92
+ console.log(`Registered provider: ${provider.name}`);
93
+ }
94
+
95
+ /**
96
+ * Unregister a provider
97
+ */
98
+ public unregister(providerName: string): void {
99
+ this.providers.delete(providerName);
100
+ console.log(`Unregistered provider: ${providerName}`);
101
+ }
102
+
103
+ /**
104
+ * Get a specific provider by name
105
+ */
106
+ public getProvider(name: string): TProvider | undefined {
107
+ return this.providers.get(name)?.provider;
108
+ }
109
+
110
+ /**
111
+ * Get all registered providers
112
+ */
113
+ public getAllProviders(): TProvider[] {
114
+ return Array.from(this.providers.values()).map(entry => entry.provider);
115
+ }
116
+
117
+ /**
118
+ * Get enabled providers sorted by priority
119
+ */
120
+ public getEnabledProviders(): TProvider[] {
121
+ return Array.from(this.providers.values())
122
+ .filter(entry => entry.config.enabled)
123
+ .sort((a, b) => (b.config.priority || 0) - (a.config.priority || 0))
124
+ .map(entry => entry.provider);
125
+ }
126
+
127
+ /**
128
+ * Check health of all providers
129
+ */
130
+ public async checkProvidersHealth(): Promise<Map<string, boolean>> {
131
+ const health = new Map<string, boolean>();
132
+
133
+ for (const [name, entry] of this.providers) {
134
+ if (!entry.config.enabled) {
135
+ health.set(name, false);
136
+ continue;
137
+ }
138
+
139
+ try {
140
+ const isAvailable = await entry.provider.isAvailable();
141
+ health.set(name, isAvailable);
142
+ } catch (error) {
143
+ health.set(name, false);
144
+ console.error(`Health check failed for ${name}:`, error);
145
+ }
146
+ }
147
+
148
+ return health;
149
+ }
150
+
151
+ /**
152
+ * Get provider statistics
153
+ */
154
+ public getProviderStats(): Map<
155
+ string,
156
+ {
157
+ successCount: number;
158
+ errorCount: number;
159
+ lastError?: string;
160
+ lastErrorTime?: Date;
161
+ }
162
+ > {
163
+ const stats = new Map();
164
+
165
+ for (const [name, entry] of this.providers) {
166
+ stats.set(name, {
167
+ successCount: entry.successCount,
168
+ errorCount: entry.errorCount,
169
+ lastError: entry.lastError?.message,
170
+ lastErrorTime: entry.lastErrorTime
171
+ });
172
+ }
173
+
174
+ return stats;
175
+ }
176
+
177
+ /**
178
+ * Clear all cached data
179
+ */
180
+ public clearCache(): void {
181
+ this.cache.clear();
182
+ console.log('Cache cleared');
183
+ }
184
+
185
+ /**
186
+ * Set cache TTL
187
+ */
188
+ public setCacheTTL(ttl: number): void {
189
+ this.cacheConfig.ttl = ttl;
190
+ console.log(`Cache TTL set to ${ttl}ms`);
191
+ }
192
+
193
+ /**
194
+ * Get cache statistics
195
+ */
196
+ public getCacheStats(): {
197
+ size: number;
198
+ maxEntries: number;
199
+ ttl: number;
200
+ } {
201
+ return {
202
+ size: this.cache.size,
203
+ maxEntries: this.cacheConfig.maxEntries,
204
+ ttl: this.cacheConfig.ttl
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Fetch with retry logic
210
+ */
211
+ protected async fetchWithRetry<T>(
212
+ fetchFn: () => Promise<T>,
213
+ config: IBaseProviderConfig
214
+ ): Promise<T> {
215
+ const maxAttempts = config.retryAttempts || 1;
216
+ let lastError: Error | undefined;
217
+
218
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
219
+ try {
220
+ return await fetchFn();
221
+ } catch (error) {
222
+ lastError = error as Error;
223
+
224
+ if (attempt < maxAttempts) {
225
+ const delay = (config.retryDelay || 1000) * attempt;
226
+ console.log(`Retry attempt ${attempt} after ${delay}ms`);
227
+ await plugins.smartdelay.delayFor(delay);
228
+ }
229
+ }
230
+ }
231
+
232
+ throw lastError || new Error('Unknown error during fetch');
233
+ }
234
+
235
+ /**
236
+ * Get from cache if not expired
237
+ */
238
+ protected getFromCache(key: string): TData | null {
239
+ const entry = this.cache.get(key);
240
+
241
+ if (!entry) {
242
+ return null;
243
+ }
244
+
245
+ // Check if cache entry has expired
246
+ const age = Date.now() - entry.timestamp.getTime();
247
+ if (entry.ttl !== Infinity && age > entry.ttl) {
248
+ this.cache.delete(key);
249
+ return null;
250
+ }
251
+
252
+ return entry.data;
253
+ }
254
+
255
+ /**
256
+ * Add to cache with TTL
257
+ */
258
+ protected addToCache(key: string, data: TData, ttl?: number): void {
259
+ // Enforce max entries limit
260
+ if (this.cache.size >= this.cacheConfig.maxEntries) {
261
+ // Remove oldest entry
262
+ const oldestKey = this.cache.keys().next().value;
263
+ if (oldestKey) {
264
+ this.cache.delete(oldestKey);
265
+ }
266
+ }
267
+
268
+ this.cache.set(key, {
269
+ data,
270
+ timestamp: new Date(),
271
+ ttl: ttl || this.cacheConfig.ttl
272
+ });
273
+ }
274
+
275
+ /**
276
+ * Track successful fetch for provider
277
+ */
278
+ protected trackSuccess(providerName: string): void {
279
+ const entry = this.providers.get(providerName);
280
+ if (entry) {
281
+ entry.successCount++;
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Track failed fetch for provider
287
+ */
288
+ protected trackError(providerName: string, error: Error): void {
289
+ const entry = this.providers.get(providerName);
290
+ if (entry) {
291
+ entry.errorCount++;
292
+ entry.lastError = error;
293
+ entry.lastErrorTime = new Date();
294
+ }
295
+ }
296
+ }