@fin.cx/opendata 3.5.1 → 4.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.
Files changed (87) hide show
  1. package/{npmextra.json → .smartconfig.json} +24 -18
  2. package/dist_ts/00_commitinfo_data.js +3 -3
  3. package/dist_ts/handelsregister/classes.handelsregisterclient.d.ts +64 -0
  4. package/dist_ts/handelsregister/classes.handelsregisterclient.js +397 -0
  5. package/dist_ts/handelsregister/classes.handelsregistererror.d.ts +56 -0
  6. package/dist_ts/handelsregister/classes.handelsregistererror.js +37 -0
  7. package/dist_ts/handelsregister/classes.portalsession.d.ts +46 -0
  8. package/dist_ts/handelsregister/classes.portalsession.js +160 -0
  9. package/dist_ts/handelsregister/classes.slidingwindowgate.d.ts +32 -0
  10. package/dist_ts/handelsregister/classes.slidingwindowgate.js +55 -0
  11. package/dist_ts/handelsregister/constants.d.ts +13 -0
  12. package/dist_ts/handelsregister/constants.js +19 -0
  13. package/dist_ts/handelsregister/data.codelists.d.ts +29 -0
  14. package/dist_ts/handelsregister/data.codelists.js +616 -0
  15. package/dist_ts/handelsregister/forms.d.ts +27 -0
  16. package/dist_ts/handelsregister/forms.js +77 -0
  17. package/dist_ts/handelsregister/html.d.ts +26 -0
  18. package/dist_ts/handelsregister/html.js +82 -0
  19. package/dist_ts/handelsregister/index.d.ts +9 -0
  20. package/dist_ts/handelsregister/index.js +10 -0
  21. package/dist_ts/handelsregister/interfaces.d.ts +240 -0
  22. package/dist_ts/handelsregister/interfaces.js +2 -0
  23. package/dist_ts/handelsregister/parse.searchresults.d.ts +35 -0
  24. package/dist_ts/handelsregister/parse.searchresults.js +175 -0
  25. package/dist_ts/handelsregister/parse.structuredcontent.d.ts +6 -0
  26. package/dist_ts/handelsregister/parse.structuredcontent.js +306 -0
  27. package/dist_ts/handelsregister/plugins.d.ts +4 -0
  28. package/dist_ts/handelsregister/plugins.js +8 -0
  29. package/dist_ts/handelsregister/references.d.ts +34 -0
  30. package/dist_ts/handelsregister/references.js +76 -0
  31. package/dist_ts/index.d.ts +0 -1
  32. package/dist_ts/index.js +1 -2
  33. package/dist_ts/plugins.d.ts +2 -14
  34. package/dist_ts/plugins.js +8 -16
  35. package/dist_ts/stocks/classes.baseproviderservice.js +8 -8
  36. package/dist_ts/stocks/classes.fundamentalsservice.js +9 -9
  37. package/dist_ts/stocks/classes.stockdataservice.js +23 -23
  38. package/dist_ts/stocks/classes.stockservice.js +9 -9
  39. package/dist_ts/stocks/providers/provider.coingecko.d.ts +1 -1
  40. package/dist_ts/stocks/providers/provider.coingecko.js +45 -39
  41. package/dist_ts/stocks/providers/provider.marketstack.d.ts +3 -3
  42. package/dist_ts/stocks/providers/provider.marketstack.js +20 -16
  43. package/dist_ts/stocks/providers/provider.secedgar.d.ts +1 -1
  44. package/dist_ts/stocks/providers/provider.secedgar.js +22 -22
  45. package/dist_ts/stocks/providers/provider.yahoo.d.ts +2 -2
  46. package/dist_ts/stocks/providers/provider.yahoo.js +14 -13
  47. package/package.json +30 -31
  48. package/readme.md +327 -676
  49. package/ts/00_commitinfo_data.ts +2 -2
  50. package/ts/handelsregister/classes.handelsregisterclient.ts +474 -0
  51. package/ts/handelsregister/classes.handelsregistererror.ts +82 -0
  52. package/ts/handelsregister/classes.portalsession.ts +191 -0
  53. package/ts/handelsregister/classes.slidingwindowgate.ts +74 -0
  54. package/ts/handelsregister/constants.ts +30 -0
  55. package/ts/handelsregister/data.codelists.ts +640 -0
  56. package/ts/handelsregister/forms.ts +99 -0
  57. package/ts/handelsregister/html.ts +113 -0
  58. package/ts/handelsregister/index.ts +27 -0
  59. package/ts/handelsregister/interfaces.ts +272 -0
  60. package/ts/handelsregister/parse.searchresults.ts +241 -0
  61. package/ts/handelsregister/parse.structuredcontent.ts +338 -0
  62. package/ts/handelsregister/plugins.ts +15 -0
  63. package/ts/handelsregister/references.ts +115 -0
  64. package/ts/index.ts +0 -1
  65. package/ts/plugins.ts +9 -29
  66. package/ts/stocks/classes.fundamentalsservice.ts +1 -1
  67. package/ts/stocks/classes.stockdataservice.ts +17 -7
  68. package/ts/stocks/classes.stockservice.ts +2 -2
  69. package/ts/stocks/providers/provider.marketstack.ts +9 -7
  70. package/ts/stocks/providers/provider.secedgar.ts +2 -2
  71. package/ts/stocks/providers/provider.yahoo.ts +3 -3
  72. package/dist_ts/classes.businessrecord.d.ts +0 -38
  73. package/dist_ts/classes.businessrecord.js +0 -47
  74. package/dist_ts/classes.germanbusinessdata.d.ts +0 -9
  75. package/dist_ts/classes.germanbusinessdata.js +0 -78
  76. package/dist_ts/classes.handelsregister.d.ts +0 -132
  77. package/dist_ts/classes.handelsregister.js +0 -288
  78. package/dist_ts/classes.jsonldata.d.ts +0 -43
  79. package/dist_ts/classes.jsonldata.js +0 -65
  80. package/dist_ts/classes.main.opendata.d.ts +0 -24
  81. package/dist_ts/classes.main.opendata.js +0 -58
  82. package/readme.hints.md +0 -89
  83. package/readme.plan.md +0 -193
  84. package/ts/classes.businessrecord.ts +0 -67
  85. package/ts/classes.handelsregister.ts +0 -360
  86. package/ts/classes.jsonldata.ts +0 -113
  87. package/ts/classes.main.opendata.ts +0 -87
package/readme.md CHANGED
@@ -1,787 +1,438 @@
1
1
  # @fin.cx/opendata
2
2
 
3
- 🚀 **Complete financial intelligence toolkit for TypeScript**
3
+ `@fin.cx/opendata` is a TypeScript toolbox for open business and market data: stock and crypto prices, SEC fundamentals, and lookups in the German registers through the register portal of the federal states. It is built for programmers who want scriptable APIs and real workflows instead of a thin wrapper around one remote endpoint.
4
4
 
5
- Access real-time stock prices, fundamental financial data, and comprehensive German company information - all through a single, unified API.
5
+ It gives you three practical lanes in one package:
6
+
7
+ - `StockPriceService` for stock and crypto prices with caching, retries, failover, historical ranges, and intraday support
8
+ - `FundamentalsService` and `StockDataService` for SEC fundamentals and combined price + fundamentals payloads
9
+ - `HandelsregisterClient`, from `@fin.cx/opendata/handelsregister`, for single lookups in the commercial, cooperative, partnership, association and civil-law partnership registers over plain HTTP, without any storage
10
+
11
+ ## Issue Reporting and Security
12
+
13
+ For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
6
14
 
7
15
  ## Installation
8
16
 
9
17
  ```bash
10
- npm install @fin.cx/opendata
11
- # or
12
18
  pnpm add @fin.cx/opendata
13
19
  ```
14
20
 
15
- ## Quick Start
21
+ ## What You Get
22
+
23
+ ### Market Data
24
+
25
+ - `StockPriceService`
26
+ Fetches current, batch, historical, and intraday price data.
27
+ - `FundamentalsService`
28
+ Fetches SEC EDGAR fundamentals for US-listed companies.
29
+ - `StockDataService`
30
+ Combines price + fundamentals and enriches fundamentals with metrics like `marketCap`, `priceToEarnings`, and `priceToBook`.
31
+ - `MarketstackProvider`
32
+ Authenticated stock market provider with current, batch, historical, and intraday support.
33
+ - `CoinGeckoProvider`
34
+ Crypto market provider behind the same stock-price interface.
35
+ - `SecEdgarProvider`
36
+ Fundamentals provider backed by SEC EDGAR company facts.
37
+
38
+ ### German Registers
39
+
40
+ Everything here comes from `@fin.cx/opendata/handelsregister`. That entry point imports nothing else of the package, needs no database, no browser and no download folder, and keeps nothing between calls.
41
+
42
+ - `HandelsregisterClient`
43
+ Searches the register portal (handelsregister.de) by name or register number and retrieves one register sheet with its structured register content (SI) parsed into a typed result.
44
+ - `SlidingWindowGate`
45
+ Holds the portal's limit of 60 searches and retrievals per hour. The client asks it before every counted request.
46
+ - `parseStructuredContent()` and `parseSearchResults()`
47
+ The parsers on their own, for an SI file or a result page someone saved from the portal. They make no request.
48
+ - `registerCourts`, `findRegisterCourt()`, `parseRegisterReferenceText()`, `formatRegisterReference()`
49
+ The 150 register courts with their XJustiz codes, and helpers for register references as businesses print them (`Amtsgericht Bremen HRB 35230 HB`).
50
+
51
+ ## Before You Start
52
+
53
+ - No stock or fundamentals providers are pre-registered. You must register them before calling the services.
54
+ - `MarketstackProvider` requires an API key.
55
+ - `SecEdgarProvider` requires a valid `User-Agent` string in the format `Company Name email@example.com`.
56
+ - `CoinGeckoProvider` works without an API key, but a key gives you better rate limits.
57
+ - `HandelsregisterClient` needs a gate. The portal's terms allow at most 60 searches or retrievals of entities per hour from one installation, so share one `SlidingWindowGate` (or a gate of your own) among every client that reaches the portal from the same address.
58
+ - The portal's terms allow single lookups for information purposes. Systematic retrieval to build or update a copy of the registers is not allowed, and neither is publishing the data under the name "Handelsregister" (§ 8 Abs. 2 HGB).
59
+ - `StockDataService` defaults to a 24 hour price cache. If you want fresher quote caching, pass `cache.priceTTL` explicitly or use `StockPriceService` directly.
16
60
 
17
- ### ✨ Unified Stock Data API (Recommended)
61
+ ## Quick Start
18
62
 
19
- Get complete stock data with automatic enrichment - the elegant way:
63
+ ### Combined Stock Data
20
64
 
21
- ```typescript
22
- import { StockDataService, SecEdgarProvider } from '@fin.cx/opendata';
65
+ Use `StockDataService` when you want one call that returns both the latest price and SEC fundamentals.
23
66
 
24
- // Initialize unified service
25
- const stockData = new StockDataService();
67
+ ```ts
68
+ import {
69
+ MarketstackProvider,
70
+ SecEdgarProvider,
71
+ StockDataService,
72
+ } from '@fin.cx/opendata';
26
73
 
27
- // Register providers
28
- stockData.registerFundamentalsProvider(new SecEdgarProvider({
29
- userAgent: 'YourCompany youremail@example.com'
30
- }));
74
+ const stocks = new StockDataService({
75
+ cache: {
76
+ priceTTL: 60_000,
77
+ },
78
+ });
31
79
 
32
- // ✨ Get complete stock data with ONE method call
33
- const apple = await stockData.getStockData('AAPL');
80
+ stocks.registerPriceProvider(
81
+ new MarketstackProvider(process.env.MARKETSTACK_API_KEY!)
82
+ );
34
83
 
35
- console.log({
36
- company: apple.fundamentals.companyName, // "Apple Inc."
37
- price: apple.price.price, // $270.37
38
- marketCap: apple.fundamentals.marketCap, // $4.13T (auto-calculated!)
39
- peRatio: apple.fundamentals.priceToEarnings, // 28.42 (auto-calculated!)
40
- pbRatio: apple.fundamentals.priceToBook, // 45.12 (auto-calculated!)
41
- eps: apple.fundamentals.earningsPerShareDiluted, // $6.13
42
- revenue: apple.fundamentals.revenue // $385.6B
43
- });
84
+ stocks.registerFundamentalsProvider(
85
+ new SecEdgarProvider({
86
+ userAgent: 'Acme Inc. dev@acme.dev',
87
+ })
88
+ );
44
89
 
45
- // Batch fetch with automatic enrichment
46
- const stocks = await stockData.getBatchStockData(['AAPL', 'MSFT', 'GOOGL']);
90
+ const apple = await stocks.getStockData('AAPL');
47
91
 
48
- stocks.forEach(stock => {
49
- console.log(`${stock.ticker}: $${stock.price.price.toFixed(2)}, ` +
50
- `P/E ${stock.fundamentals?.priceToEarnings?.toFixed(2)}`);
92
+ console.log({
93
+ ticker: apple.ticker,
94
+ price: apple.price.price,
95
+ provider: apple.price.provider,
96
+ companyName: apple.fundamentals?.companyName,
97
+ marketCap: apple.fundamentals?.marketCap,
98
+ priceToEarnings: apple.fundamentals?.priceToEarnings,
99
+ fetchedAt: apple.fetchedAt,
51
100
  });
52
- // Output:
53
- // AAPL: $270.37, P/E 28.42
54
- // MSFT: $425.50, P/E 34.21
55
- // GOOGL: $142.15, P/E 25.63
56
101
  ```
57
102
 
58
- **Why use the unified API?**
59
- - ✅ **Single service** for both prices and fundamentals
60
- - ✅ **Automatic enrichment** - Market cap, P/E, P/B calculated automatically
61
- - ✅ **One method call** - No manual `enrichWithPrice()` calls
62
- - ✅ **Simplified code** - Less boilerplate, more readable
63
- - ✅ **Type-safe** - Full TypeScript support
64
-
65
- ### 📈 Price-Only Data (Alternative)
66
-
67
- If you only need prices without fundamentals:
68
-
69
- ```typescript
70
- import { StockDataService, MarketstackProvider } from '@fin.cx/opendata';
71
-
72
- const stockData = new StockDataService();
73
- stockData.registerPriceProvider(new MarketstackProvider('YOUR_API_KEY'));
74
-
75
- // Get just the price
76
- const price = await stockData.getPrice('AAPL');
77
- console.log(`${price.ticker}: $${price.price}`);
78
-
79
- // Or use StockPriceService directly for more control
80
- import { StockPriceService } from '@fin.cx/opendata';
103
+ You can also batch this:
81
104
 
82
- const stockService = new StockPriceService({ ttl: 60000 });
83
- stockService.register(new MarketstackProvider('YOUR_API_KEY'));
84
-
85
- const apple = await stockService.getData({ type: 'current', ticker: 'AAPL' });
86
- console.log(`${apple.companyFullName}: $${apple.price}`);
105
+ ```ts
106
+ const batch = await stocks.getBatchStockData(['AAPL', 'MSFT', 'NVDA']);
87
107
  ```
88
108
 
89
- ### 🪙 Cryptocurrency Prices
90
-
91
- Get real-time crypto prices with the CoinGecko provider:
92
-
93
- ```typescript
94
- import { StockPriceService, CoinGeckoProvider } from '@fin.cx/opendata';
109
+ ### Price-Only Stocks And Intraday Data
95
110
 
96
- const stockService = new StockPriceService({ ttl: 30000 });
111
+ Use `StockPriceService` if you want smarter per-request caching and direct access to current, historical, and intraday request types.
97
112
 
98
- // Optional: Pass API key for higher rate limits
99
- // const provider = new CoinGeckoProvider('your-api-key');
100
- const provider = new CoinGeckoProvider();
113
+ ```ts
114
+ import { MarketstackProvider, StockPriceService } from '@fin.cx/opendata';
101
115
 
102
- stockService.register(provider);
116
+ const prices = new StockPriceService();
117
+ prices.register(new MarketstackProvider(process.env.MARKETSTACK_API_KEY!));
103
118
 
104
- // Fetch single crypto price (using ticker symbol)
105
- const btc = await stockService.getPrice({ ticker: 'BTC' });
106
- console.log(`${btc.ticker}: $${btc.price.toLocaleString()}`);
107
- console.log(`24h Change: ${btc.changePercent.toFixed(2)}%`);
108
- console.log(`24h Volume: $${btc.volume?.toLocaleString()}`);
119
+ const current = await prices.getPrice({ ticker: 'AAPL' });
109
120
 
110
- // Or use CoinGecko ID directly
111
- const ethereum = await stockService.getPrice({ ticker: 'ethereum' });
112
-
113
- // Batch fetch multiple cryptos
114
- const cryptos = await stockService.getPrices({
115
- tickers: ['BTC', 'ETH', 'USDT', 'BNB', 'SOL']
116
- });
117
-
118
- cryptos.forEach(crypto => {
119
- console.log(`${crypto.ticker}: $${crypto.price.toFixed(2)} (${crypto.changePercent >= 0 ? '+' : ''}${crypto.changePercent.toFixed(2)}%)`);
120
- });
121
-
122
- // Historical crypto prices
123
- const history = await stockService.getData({
121
+ const historical = await prices.getData({
124
122
  type: 'historical',
125
- ticker: 'BTC',
123
+ ticker: 'AAPL',
126
124
  from: new Date('2025-01-01'),
127
- to: new Date('2025-01-31')
125
+ to: new Date('2025-01-31'),
126
+ sort: 'DESC',
128
127
  });
129
128
 
130
- // Intraday crypto prices (hourly)
131
- const intraday = await stockService.getData({
129
+ const intraday = await prices.getData({
132
130
  type: 'intraday',
133
- ticker: 'ETH',
131
+ ticker: 'AAPL',
134
132
  interval: '1hour',
135
- limit: 24 // Last 24 hours
133
+ limit: 24,
136
134
  });
137
- ```
138
-
139
- ### 💰 Fundamentals-Only Data (Alternative)
140
-
141
- If you only need fundamentals without prices:
142
-
143
- ```typescript
144
- import { StockDataService, SecEdgarProvider } from '@fin.cx/opendata';
145
-
146
- const stockData = new StockDataService();
147
- stockData.registerFundamentalsProvider(new SecEdgarProvider({
148
- userAgent: 'YourCompany youremail@example.com'
149
- }));
150
-
151
- // Get just fundamentals
152
- const fundamentals = await stockData.getFundamentals('AAPL');
153
135
 
154
136
  console.log({
155
- company: fundamentals.companyName,
156
- eps: fundamentals.earningsPerShareDiluted,
157
- revenue: fundamentals.revenue,
158
- sharesOutstanding: fundamentals.sharesOutstanding
137
+ live: current.price,
138
+ candles: intraday.length,
139
+ historicalRows: historical.length,
159
140
  });
160
-
161
- // Or use FundamentalsService directly
162
- import { FundamentalsService } from '@fin.cx/opendata';
163
-
164
- const fundamentalsService = new FundamentalsService();
165
- fundamentalsService.register(new SecEdgarProvider({
166
- userAgent: 'YourCompany youremail@example.com'
167
- }));
168
-
169
- const data = await fundamentalsService.getFundamentals('AAPL');
170
141
  ```
171
142
 
172
- ### 🏢 German Business Intelligence
143
+ Supported stock request shapes:
173
144
 
174
- Access comprehensive German company data:
145
+ - `{ type: 'current', ticker, exchange? }`
146
+ - `{ type: 'batch', tickers, exchange? }`
147
+ - `{ type: 'historical', ticker, from, to, exchange?, sort?, limit?, offset? }`
148
+ - `{ type: 'intraday', ticker, interval, exchange?, limit?, date? }`
175
149
 
176
- ```typescript
177
- import { OpenData } from '@fin.cx/opendata';
178
- import * as path from 'path';
150
+ `StockPriceService` also exposes operational helpers like `checkProvidersHealth()`, `getProviderStats()`, `clearCache()`, and `getCacheStats()`.
179
151
 
180
- // Configure directory paths
181
- const openData = new OpenData({
182
- nogitDir: path.join(process.cwd(), '.nogit'),
183
- downloadDir: path.join(process.cwd(), '.nogit', 'downloads'),
184
- germanBusinessDataDir: path.join(process.cwd(), '.nogit', 'germanbusinessdata')
185
- });
186
- await openData.start();
187
-
188
- // Search for companies
189
- const results = await openData.handelsregister.searchCompany("Siemens AG");
190
-
191
- // Get detailed information with documents
192
- const details = await openData.handelsregister.getSpecificCompany({
193
- court: "Munich",
194
- type: "HRB",
195
- number: "6684"
196
- });
197
- ```
152
+ ### Crypto Prices With The Same Interface
198
153
 
199
- ## Features
154
+ `CoinGeckoProvider` plugs into `StockPriceService`, so crypto lookups use the same request model as stock lookups.
200
155
 
201
- ### 📊 Stock & Crypto Market Module
156
+ ```ts
157
+ import { CoinGeckoProvider, StockPriceService } from '@fin.cx/opendata';
202
158
 
203
- - **Real-Time Prices** - Live and EOD prices from Marketstack and CoinGecko
204
- - **Cryptocurrency Support** - 13M+ crypto tokens with 24/7 market data via CoinGecko
205
- - **Company Names** - Automatic company name extraction (e.g., "Apple Inc (NASDAQ:AAPL)")
206
- - **Historical Data** - Up to 15 years of daily EOD prices with pagination
207
- - **OHLCV Data** - Open, High, Low, Close, Volume for technical analysis
208
- - **Exchange Filtering** - Query specific exchanges via MIC codes (XNAS, XLON, XNYS)
209
- - **Smart Caching** - Data-type aware TTL (historical cached forever, EOD 24h, live 30s)
210
- - **Batch Operations** - Fetch 100+ symbols in one request
211
- - **Type-Safe API** - Full TypeScript support with discriminated unions
212
- - **Multi-Provider** - Automatic fallback between providers
159
+ const prices = new StockPriceService({ ttl: 30_000 });
160
+ prices.register(new CoinGeckoProvider(process.env.COINGECKO_API_KEY));
213
161
 
214
- ### 💰 Fundamental Data Module
162
+ const btc = await prices.getPrice({ ticker: 'BTC' });
163
+ const top = await prices.getPrices({ tickers: ['BTC', 'ETH', 'SOL'] });
215
164
 
216
- - **SEC EDGAR Integration** - FREE fundamental data directly from SEC filings
217
- - **Comprehensive Metrics** - EPS, Revenue, Assets, Liabilities, Cash Flow, and more
218
- - **All US Public Companies** - Complete coverage of SEC-registered companies
219
- - **Historical Filings** - Data back to ~2009
220
- - **CIK Lookup** - Automatic ticker-to-CIK mapping with smart caching
221
- - **Calculated Ratios** - Market Cap, P/E, P/B ratios when combined with prices
222
- - **No API Key Required** - Direct access to SEC's public API
223
- - **Rate Limit Management** - Built-in 10 req/sec rate limiting
224
-
225
- ### 🇩🇪 German Business Intelligence
165
+ console.log(btc.price, top.map((item) => item.ticker));
166
+ ```
226
167
 
227
- - **MongoDB Integration** - Scalable data storage for millions of records
228
- - **Bulk JSONL Import** - Process multi-GB datasets efficiently
229
- - **Handelsregister Automation** - Automated document retrieval
230
- - **CRUD Operations** - Full database management with validation
231
- - **Streaming Processing** - Handle large datasets without memory issues
168
+ You can also pass CoinGecko ids like `bitcoin` or `ethereum` instead of ticker symbols.
232
169
 
233
- ## Advanced Examples
170
+ ### Fundamentals Only
234
171
 
235
- ### Combined Market Analysis (Unified API)
172
+ Use `FundamentalsService` when you only care about SEC filing data.
236
173
 
237
- Analyze multiple companies with automatic enrichment:
174
+ ```ts
175
+ import { FundamentalsService, SecEdgarProvider } from '@fin.cx/opendata';
238
176
 
239
- ```typescript
240
- import { StockDataService, MarketstackProvider, SecEdgarProvider } from '@fin.cx/opendata';
177
+ const fundamentals = new FundamentalsService();
178
+ fundamentals.register(
179
+ new SecEdgarProvider({
180
+ userAgent: 'Acme Inc. dev@acme.dev',
181
+ })
182
+ );
241
183
 
242
- // Setup unified service
243
- const stockData = new StockDataService();
244
- stockData.registerPriceProvider(new MarketstackProvider('YOUR_API_KEY'));
245
- stockData.registerFundamentalsProvider(new SecEdgarProvider({
246
- userAgent: 'YourCompany youremail@example.com'
247
- }));
184
+ const apple = await fundamentals.getFundamentals('AAPL');
248
185
 
249
- // Analyze multiple companies with ONE call
250
- const tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN'];
251
- const stocks = await stockData.getBatchStockData(tickers);
252
-
253
- // All metrics are automatically calculated!
254
- stocks.forEach(stock => {
255
- if (stock.fundamentals) {
256
- console.log(`\n${stock.fundamentals.companyName} (${stock.ticker})`);
257
- console.log(` Price: $${stock.price.price.toFixed(2)}`);
258
- console.log(` Market Cap: $${(stock.fundamentals.marketCap! / 1e9).toFixed(2)}B`);
259
- console.log(` P/E Ratio: ${stock.fundamentals.priceToEarnings!.toFixed(2)}`);
260
- console.log(` Revenue: $${(stock.fundamentals.revenue! / 1e9).toFixed(2)}B`);
261
- console.log(` EPS: $${stock.fundamentals.earningsPerShareDiluted!.toFixed(2)}`);
262
- }
186
+ console.log({
187
+ companyName: apple.companyName,
188
+ revenue: apple.revenue,
189
+ netIncome: apple.netIncome,
190
+ earningsPerShareDiluted: apple.earningsPerShareDiluted,
191
+ filingDate: apple.filingDate,
263
192
  });
264
-
265
- // Or analyze one-by-one with automatic enrichment
266
- for (const ticker of tickers) {
267
- const stock = await stockData.getStockData(ticker);
268
-
269
- // Everything is already enriched - no manual calculations needed!
270
- console.log(`${ticker}: P/E ${stock.fundamentals?.priceToEarnings?.toFixed(2)}`);
271
- }
272
193
  ```
273
194
 
274
- ### Fundamental Data Screening
195
+ ### Register Lookups (Handelsregister)
275
196
 
276
- Screen stocks by financial metrics:
197
+ `HandelsregisterClient` speaks to the register portal of the federal states over plain HTTP, the way a browser would: it opens the search form, sends the search, reads the result list and, for one entry, downloads the structured register content (SI, an XJustiz XML file) and parses it.
277
198
 
278
- ```typescript
279
- // Fetch data for multiple tickers
280
- const tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META', 'NVDA', 'TSLA'];
281
- const stocks = await stockData.getBatchStockData(tickers);
199
+ ```ts
200
+ import {
201
+ HandelsregisterClient,
202
+ HandelsregisterError,
203
+ SlidingWindowGate,
204
+ findRegisterCourt,
205
+ formatRegisterReference,
206
+ } from '@fin.cx/opendata/handelsregister';
282
207
 
283
- // Filter by criteria (all metrics auto-calculated!)
284
- const valueStocks = stocks.filter(stock => {
285
- const f = stock.fundamentals;
286
- return f &&
287
- f.priceToEarnings! < 30 && // P/E under 30
288
- f.priceToBook! < 10 && // P/B under 10
289
- f.revenue! > 100_000_000_000; // Revenue > $100B
290
- });
208
+ // one gate for the whole installation: 60 searches or retrievals per hour
209
+ const gate = new SlidingWindowGate();
291
210
 
292
- console.log('\n💎 Value Stocks:');
293
- valueStocks.forEach(stock => {
294
- console.log(`${stock.ticker}: P/E ${stock.fundamentals!.priceToEarnings!.toFixed(2)}, ` +
295
- `P/B ${stock.fundamentals!.priceToBook!.toFixed(2)}`);
211
+ const register = new HandelsregisterClient({
212
+ gate,
213
+ userAgent: 'Example Books (+https://example.com/contact)',
296
214
  });
297
- ```
298
-
299
- ### Historical Data Analysis
300
-
301
- Fetch and analyze historical price trends:
302
215
 
303
- ```typescript
304
- // Get 1 year of historical data
305
- const history = await stockService.getData({
306
- type: 'historical',
307
- ticker: 'AAPL',
308
- from: new Date('2024-01-01'),
309
- to: new Date('2024-12-31'),
310
- sort: 'DESC' // Newest first
311
- });
216
+ // by name: every row the portal lists, in its order, never narrowed down to one
217
+ const found = await register.search({ name: 'Task Venture Capital', seat: 'Bremen' });
218
+ const choices = found.matches.map((match) => ({
219
+ reference: formatRegisterReference(match.reference), // 'Amtsgericht Bremen HRB 35230 HB'
220
+ name: match.name,
221
+ sheet: match.sheetStatus, // 'current' or 'closed'
222
+ formerNames: match.formerNames,
223
+ }));
224
+ if (found.truncated) {
225
+ // the portal stops at 100 hits: add the seat, the court or the register number
226
+ }
312
227
 
313
- // Calculate statistics
314
- const prices = history.map(p => p.price);
315
- const high52Week = Math.max(...prices);
316
- const low52Week = Math.min(...prices);
317
- const avgPrice = prices.reduce((a, b) => a + b) / prices.length;
318
-
319
- console.log(`52-Week Analysis for AAPL:`);
320
- console.log(` High: $${high52Week.toFixed(2)}`);
321
- console.log(` Low: $${low52Week.toFixed(2)}`);
322
- console.log(` Average: $${avgPrice.toFixed(2)}`);
323
- console.log(` Days: ${history.length}`);
324
-
325
- // Calculate Simple Moving Average
326
- const calculateSMA = (data: IStockPrice[], period: number) => {
327
- const sma: number[] = [];
328
- for (let i = period - 1; i < data.length; i++) {
329
- const sum = data.slice(i - period + 1, i + 1)
330
- .reduce((acc, p) => acc + p.price, 0);
331
- sma.push(sum / period);
228
+ // one register sheet, with its structured content
229
+ const court = findRegisterCourt('Amtsgericht Bremen');
230
+ if (court) {
231
+ const entry = await register.getEntry({ courtCode: court.code, type: 'HRB', number: '35230 HB' });
232
+ const content = entry.structuredContent; // null when the portal offers none for this entry
233
+ if (content) {
234
+ const facts = {
235
+ name: content.name,
236
+ legalForm: content.legalForm?.short, // 'GmbH'
237
+ address: content.address,
238
+ status: content.status?.state, // 'active', 'deleted', 'in_liquidation', 'insolvent', 'merged'
239
+ representatives: content.representatives, // [{ name, function, functionCode, kind }]
240
+ retrievedAt: entry.retrievedAt,
241
+ notice: entry.notice.en, // the portal's note: the structured content is not binding
242
+ };
332
243
  }
333
- return sma;
334
- };
335
-
336
- const sma20 = calculateSMA(history, 20);
337
- const sma50 = calculateSMA(history, 50);
338
-
339
- console.log(`\nMoving Averages:`);
340
- console.log(` 20-day SMA: $${sma20[0].toFixed(2)}`);
341
- console.log(` 50-day SMA: $${sma50[0].toFixed(2)}`);
342
- ```
343
-
344
- ### OHLCV Technical Analysis
345
-
346
- Use OHLCV data for technical indicators:
347
-
348
- ```typescript
349
- const history = await stockService.getData({
350
- type: 'historical',
351
- ticker: 'TSLA',
352
- from: new Date('2024-11-01'),
353
- to: new Date('2024-11-30')
354
- });
355
-
356
- // Calculate daily trading range
357
- for (const day of history) {
358
- const range = day.high! - day.low!;
359
- const rangePercent = (range / day.low!) * 100;
360
-
361
- console.log(`${day.timestamp.toISOString().split('T')[0]}:`);
362
- console.log(` Open: $${day.open}`);
363
- console.log(` High: $${day.high}`);
364
- console.log(` Low: $${day.low}`);
365
- console.log(` Close: $${day.price}`);
366
- console.log(` Volume: ${day.volume?.toLocaleString()}`);
367
- console.log(` Range: $${range.toFixed(2)} (${rangePercent.toFixed(2)}%)`);
368
244
  }
369
- ```
370
-
371
- ### Fundamental Data Screening
372
-
373
- Screen stocks based on fundamental metrics:
374
-
375
- ```typescript
376
- const tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'META', 'NVDA'];
377
-
378
- // Fetch fundamentals for all tickers
379
- const allFundamentals = await fundamentalsService.getBatchFundamentals(tickers);
380
-
381
- // Get current prices
382
- const prices = await stockService.getData({
383
- type: 'batch',
384
- tickers: tickers
385
- });
386
-
387
- // Create price map
388
- const priceMap = new Map(prices.map(p => [p.ticker, p.price]));
389
-
390
- // Enrich with prices
391
- const enriched = await fundamentalsService.enrichBatchWithPrices(
392
- allFundamentals,
393
- priceMap
394
- );
395
-
396
- // Screen for value stocks (P/E < 25, P/B < 5)
397
- const valueStocks = enriched.filter(f =>
398
- f.priceToEarnings && f.priceToEarnings < 25 &&
399
- f.priceToBook && f.priceToBook < 5
400
- );
401
-
402
- console.log('Value Stocks:');
403
- valueStocks.forEach(stock => {
404
- console.log(`\n${stock.companyName} (${stock.ticker})`);
405
- console.log(` P/E Ratio: ${stock.priceToEarnings!.toFixed(2)}`);
406
- console.log(` P/B Ratio: ${stock.priceToBook!.toFixed(2)}`);
407
- console.log(` Market Cap: $${(stock.marketCap! / 1e9).toFixed(2)}B`);
408
- });
409
- ```
410
-
411
- ### Market Dashboard
412
245
 
413
- Create a comprehensive market overview:
414
-
415
- ```typescript
416
- const indicators = [
417
- { ticker: 'AAPL', name: 'Apple' },
418
- { ticker: 'MSFT', name: 'Microsoft' },
419
- { ticker: 'GOOGL', name: 'Alphabet' },
420
- { ticker: 'AMZN', name: 'Amazon' },
421
- { ticker: 'TSLA', name: 'Tesla' }
422
- ];
423
-
424
- const prices = await stockService.getData({
425
- type: 'batch',
426
- tickers: indicators.map(i => i.ticker)
427
- });
428
-
429
- // Display with color-coded changes
430
- prices.forEach(price => {
431
- const indicator = indicators.find(i => i.ticker === price.ticker);
432
- const arrow = price.change >= 0 ? '↑' : '↓';
433
- const color = price.change >= 0 ? '\x1b[32m' : '\x1b[31m';
434
-
435
- console.log(
436
- `${price.companyName!.padEnd(25)} $${price.price.toFixed(2).padStart(8)} ` +
437
- `${color}${arrow} ${price.changePercent.toFixed(2)}%\x1b[0m`
438
- );
439
- });
440
- ```
441
-
442
- ### Exchange-Specific Trading
443
-
444
- Compare prices across different exchanges:
445
-
446
- ```typescript
447
- // Vodafone trades on both London and NYSE
448
- const exchanges = [
449
- { mic: 'XLON', name: 'London Stock Exchange' },
450
- { mic: 'XNYS', name: 'New York Stock Exchange' }
451
- ];
452
-
453
- for (const exchange of exchanges) {
454
- try {
455
- const price = await stockService.getData({
456
- type: 'current',
457
- ticker: 'VOD',
458
- exchange: exchange.mic
459
- });
460
-
461
- console.log(`${exchange.name}:`);
462
- console.log(` Price: ${price.price} ${price.currency}`);
463
- console.log(` Volume: ${price.volume?.toLocaleString()}`);
464
- console.log(` Exchange: ${price.exchangeName}`);
465
- } catch (error) {
466
- console.log(`${exchange.name}: Not available`);
246
+ // every failure is typed
247
+ try {
248
+ await register.getEntry({ courtCode: 'H1101', type: 'HRB', number: '35230' });
249
+ } catch (error) {
250
+ if (error instanceof HandelsregisterError && error.code === 'rate_limited') {
251
+ const nextPossibleAt = new Date(error.retryAt ?? Date.now());
467
252
  }
468
253
  }
469
254
  ```
470
255
 
471
- ## Configuration
256
+ Searches take a name or a register number:
472
257
 
473
- ### Stock Service Options
258
+ - `{ name, match?: 'all' | 'any' | 'exact', seat?, type?, courtCode?, includeClosed? }`
259
+ - `{ type, number, courtCode?, includeClosed? }` where `type` is `HRA`, `HRB`, `GnR`, `PR`, `VR` or `GsR` and `number` may carry the location suffix (`999924 B`, `999928 NM`). The portal is searched by the digits, so rows with any suffix come back. Without `courtCode`, every court's entry with that number is returned.
474
260
 
475
- ```typescript
476
- const stockService = new StockPriceService({
477
- ttl: 60000, // Default cache TTL in ms
478
- maxEntries: 10000 // Max cached entries
479
- });
261
+ `getEntry({ courtCode, type, number })` identifies one sheet. It includes closed sheets, compares the location suffix when you give one, and never picks one of several: it fails with `not_found` or `ambiguous` and lists the candidates.
480
262
 
481
- // Marketstack - EOD data (requires API key)
482
- stockService.register(new MarketstackProvider('YOUR_API_KEY'), {
483
- enabled: true,
484
- priority: 100,
485
- timeout: 10000,
486
- retryAttempts: 3,
487
- retryDelay: 1000
488
- });
489
- ```
263
+ #### What counts against the 60 per hour
490
264
 
491
- ### Fundamentals Service Options
265
+ | Call | Counted requests | Also loaded |
266
+ | --- | --- | --- |
267
+ | `search()` | 1 search | the search form and the result list |
268
+ | `getEntry()` | 1 search, plus 1 retrieval when the portal offers structured content | the search form and the result list |
492
269
 
493
- ```typescript
494
- const fundamentalsService = new FundamentalsService({
495
- ttl: 90 * 24 * 60 * 60 * 1000, // 90 days (quarterly refresh)
496
- maxEntries: 10000
497
- });
270
+ The client asks the gate before each counted request and before that step sends anything, so a refusal costs no traffic. A slot taken for a step that then fails before its request is sent stays taken; the count errs on the safe side. The client never repeats a request by itself, never caches, and never writes to the console. Every call opens its own visit to the portal and forgets it afterwards.
498
271
 
499
- // SEC EDGAR provider (FREE - no API key!)
500
- fundamentalsService.register(new SecEdgarProvider({
501
- userAgent: 'YourCompany youremail@example.com',
502
- cikCacheTTL: 30 * 24 * 60 * 60 * 1000, // 30 days
503
- fundamentalsCacheTTL: 90 * 24 * 60 * 60 * 1000, // 90 days
504
- timeout: 30000
505
- }));
506
- ```
272
+ A refusal of `SlidingWindowGate` is a `HandelsregisterError` with the code `rate_limited` and `retryAt`, the moment the next request is possible. Like every error of the client it carries the refused `step` (`search` or `structured_content`) and the `countedRequests` made before it: a `getEntry()` refused at the retrieval has already counted its search. A gate of your own may refuse with any error; a `HandelsregisterError` gains the step and the count the same way, and any other error reaches the caller unchanged.
507
273
 
508
- ### Directory Configuration (German Business Data)
274
+ #### What a result holds
509
275
 
510
- All directory paths are mandatory when using German business data features:
276
+ - `IRegisterMatch`, one row of the result list: `reference` (court with code and name, register type, number with suffix), `formerCourt`, `federalState`, `name`, `seat`, `sheetStatus` (`current`, `closed` or `other`, with the portal's word in `sheetStatusText`), `formerNames` and the `documents` the portal offers.
277
+ - `IRegisterStructuredContent`, the parsed SI: `name`, `legalForm` (XUnternehmen code with short and long designation), `seat`, the business `address`, `register`, `status` (XJustiz status of the legal entity), `lastEntryOn`, `representatives` and `representationStated`, `producedAt`, `binding: false` and the portal's `notice`.
278
+ - A closed sheet ("Geschlossenes Registerblatt") means the entry was deleted, or its sheet was closed when the entity moved to another court, changed its legal form or merged. The result list does not say which, and the portal offers no structured content for it.
279
+ - The structured content states no date for the status itself; `lastEntryOn` is the date of the latest entry on the sheet.
280
+ - Of people the parser reads the name and the function only. Birth dates, places of residence and private addresses never leave it.
281
+ - Codes are decoded with tables taken from the code lists on XRepository on 2026-09-23. A code the tables do not know is kept as a code, without a designation.
511
282
 
512
- ```typescript
513
- import { OpenData } from '@fin.cx/opendata';
514
- import * as path from 'path';
283
+ #### What can happen
515
284
 
516
- // Development
517
- const openData = new OpenData({
518
- nogitDir: path.join(process.cwd(), '.nogit'),
519
- downloadDir: path.join(process.cwd(), '.nogit', 'downloads'),
520
- germanBusinessDataDir: path.join(process.cwd(), '.nogit', 'germanbusinessdata')
521
- });
285
+ | Case | Result |
286
+ | --- | --- |
287
+ | No match | `search()` returns no matches; `getEntry()` fails with `not_found` |
288
+ | Several matches in different courts | every row, in the portal's order |
289
+ | More than 100 hits | the first 100 and `truncated: true` |
290
+ | Renamed entity | the current name, earlier names in `formerNames` |
291
+ | Deleted or moved entity | `sheetStatus: 'closed'`, no structured content |
292
+ | Dissolved or insolvent entity | `status.state` `in_liquidation` or `insolvent`, and the liquidators among the representatives |
293
+ | Branch of a company under foreign law | its foreign legal form and seat, and its permanent representatives |
294
+ | Partnership, cooperative, association, civil-law partnership | register types `PR`, `GnR`, `VR`, `GsR` |
295
+ | Number without suffix that fits several sheets | `ambiguous` with the candidates |
296
+ | Suffix that fits no sheet | `not_found` with the sheets of the same number |
297
+ | Structured content of another sheet | `unexpected_response` |
298
+ | Portal answers with an HTTP error (blocked address, overload) | `refused` with `status` |
299
+ | Portal shows its notice that it refuses retrievals from this address | `refused` with its words in `portalMessage` |
300
+ | Portal too slow, unreachable, or call aborted | `timeout`, `unreachable`, `aborted` |
301
+ | Portal changed its pages or answered in another language | `unexpected_response`, with the portal's own message in `portalMessage` when it showed one |
302
+ | Structured content that cannot be read | `invalid_structured_content` |
522
303
 
523
- // Production
524
- const openDataProd = new OpenData({
525
- nogitDir: '/var/lib/myapp/data',
526
- downloadDir: '/var/lib/myapp/data/downloads',
527
- germanBusinessDataDir: '/var/lib/myapp/data/germanbusinessdata'
528
- });
529
- ```
304
+ Errors of the client carry `countedRequests` and the `step` that failed, so you can say whether a request may have counted even when its answer did not arrive. Error messages contain no register data.
530
305
 
531
- ### Environment Variables
306
+ #### Parsing without a request
532
307
 
533
- Set environment variables for API keys and database:
308
+ ```ts
309
+ import { parseStructuredContent } from '@fin.cx/opendata/handelsregister';
534
310
 
535
- ```env
536
- # Marketstack API (for EOD stock data)
537
- MARKETSTACK_COM_TOKEN=your_api_key_here
538
-
539
- # MongoDB (for German business data)
540
- MONGODB_URL=mongodb://localhost:27017
541
- MONGODB_NAME=opendata
542
- MONGODB_USER=myuser
543
- MONGODB_PASS=mypass
544
- ```
545
-
546
- ## API Reference
547
-
548
- ### Stock Price Interfaces
549
-
550
- ```typescript
551
- interface IStockPrice {
552
- ticker: string;
553
- price: number;
554
- currency: string;
555
- change: number;
556
- changePercent: number;
557
- previousClose: number;
558
- timestamp: Date;
559
- provider: string;
560
- marketState: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED';
561
- exchange?: string;
562
- exchangeName?: string;
563
-
564
- // OHLCV data
565
- volume?: number;
566
- open?: number;
567
- high?: number;
568
- low?: number;
569
- adjusted?: boolean;
570
- dataType: 'eod' | 'intraday' | 'live';
571
- fetchedAt: Date;
572
-
573
- // Company identification
574
- companyName?: string; // "Apple Inc"
575
- companyFullName?: string; // "Apple Inc (NASDAQ:AAPL)"
576
- }
577
- ```
578
-
579
- ### Fundamental Data Interfaces
580
-
581
- ```typescript
582
- interface IStockFundamentals {
583
- ticker: string;
584
- cik: string;
585
- companyName: string;
586
- provider: string;
587
- timestamp: Date;
588
- fetchedAt: Date;
589
-
590
- // Per-share metrics
591
- earningsPerShareBasic?: number;
592
- earningsPerShareDiluted?: number;
593
- sharesOutstanding?: number;
594
-
595
- // Income statement (annual USD)
596
- revenue?: number;
597
- netIncome?: number;
598
- operatingIncome?: number;
599
- grossProfit?: number;
600
-
601
- // Balance sheet (annual USD)
602
- assets?: number;
603
- liabilities?: number;
604
- stockholdersEquity?: number;
605
- cash?: number;
606
- propertyPlantEquipment?: number;
607
-
608
- // Calculated metrics (requires price)
609
- marketCap?: number; // price × sharesOutstanding
610
- priceToEarnings?: number; // price / EPS
611
- priceToBook?: number; // marketCap / stockholdersEquity
612
-
613
- // Metadata
614
- fiscalYear?: string;
615
- fiscalQuarter?: string;
616
- filingDate?: Date;
617
- form?: '10-K' | '10-Q' | string;
618
- }
619
- ```
620
-
621
- ### Key Methods
622
-
623
- **StockPriceService**
624
- - `getData(request)` - Unified method for all stock data (current, historical, batch)
625
- - `getPrice(request)` - Convenience method for single current price
626
- - `getPrices(request)` - Convenience method for batch current prices
627
- - `register(provider, config)` - Add data provider with priority and retry config
628
- - `checkProvidersHealth()` - Test all providers and return health status
629
- - `getProviderStats()` - Get success/error statistics for each provider
630
- - `clearCache()` - Clear price cache
631
- - `setCacheTTL(ttl)` - Update cache TTL dynamically
632
-
633
- **FundamentalsService**
634
- - `getFundamentals(ticker)` - Get fundamental data for single ticker
635
- - `getBatchFundamentals(tickers)` - Get fundamentals for multiple tickers
636
- - `enrichWithPrice(fundamentals, price)` - Calculate market cap, P/E, P/B ratios
637
- - `enrichBatchWithPrices(fundamentals, priceMap)` - Batch enrich with prices
638
- - `register(provider, config)` - Add fundamentals provider
639
- - `checkProvidersHealth()` - Test all providers
640
- - `getProviderStats()` - Get success/error statistics
641
- - `clearCache()` - Clear fundamentals cache
642
-
643
- **SecEdgarProvider**
644
- - ✅ FREE - No API key required
645
- - ✅ All US public companies
646
- - ✅ Comprehensive US GAAP financial metrics
647
- - ✅ Historical data back to ~2009
648
- - ✅ Direct access to SEC filings (10-K, 10-Q)
649
- - ✅ Smart caching (CIK: 30 days, Fundamentals: 90 days)
650
- - ✅ Rate limiting (10 requests/second)
651
- - ℹ️ Requires User-Agent header in format: "Company Name Email"
652
-
653
- **MarketstackProvider**
654
- - ✅ End-of-Day (EOD) stock prices
655
- - ✅ 500,000+ tickers across 72+ exchanges worldwide
656
- - ✅ Historical data with pagination
657
- - ✅ Batch fetching support
658
- - ✅ OHLCV data (Open, High, Low, Close, Volume)
659
- - ✅ Company names included automatically
660
- - ⚠️ Requires API key (free tier: 100 requests/month)
661
-
662
- **CoinGeckoProvider**
663
- - ✅ Cryptocurrency prices (Bitcoin, Ethereum, 13M+ tokens)
664
- - ✅ Current, historical, and intraday data
665
- - ✅ 24/7 market data (crypto never closes)
666
- - ✅ OHLCV data with market cap and volume
667
- - ✅ Supports both ticker symbols (BTC, ETH) and CoinGecko IDs (bitcoin, ethereum)
668
- - ✅ 240+ networks, 1600+ exchanges
669
- - ℹ️ Optional API key (free tier: 30 requests/min, 10K/month)
670
-
671
- **OpenData**
672
- - `start()` - Initialize MongoDB connection
673
- - `buildInitialDb()` - Import bulk data
674
- - `CBusinessRecord` - Business record class
675
- - `handelsregister` - German registry automation
676
-
677
- ## Provider Architecture
678
-
679
- Add custom data providers easily:
680
-
681
- ```typescript
682
- class MyCustomProvider implements IStockProvider {
683
- name = 'My Provider';
684
- priority = 50;
685
- requiresAuth = true;
686
- rateLimit = { requestsPerMinute: 60 };
687
-
688
- async fetchData(request: IStockDataRequest): Promise<IStockPrice | IStockPrice[]> {
689
- // Implement unified data fetching
690
- switch (request.type) {
691
- case 'current':
692
- return this.fetchCurrentPrice(request);
693
- case 'batch':
694
- return this.fetchBatchPrices(request);
695
- case 'historical':
696
- return this.fetchHistoricalPrices(request);
697
- default:
698
- throw new Error(`Unsupported request type`);
699
- }
700
- }
701
-
702
- async isAvailable(): Promise<boolean> {
703
- // Health check
704
- return true;
705
- }
706
-
707
- supportsMarket(market: string): boolean {
708
- return ['US', 'UK', 'DE'].includes(market);
709
- }
710
-
711
- supportsTicker(ticker: string): boolean {
712
- return /^[A-Z]{1,5}$/.test(ticker);
713
- }
714
- }
715
-
716
- stockService.register(new MyCustomProvider());
311
+ // an SI file a person downloaded from the portal themselves
312
+ const content = parseStructuredContent(xmlText);
717
313
  ```
718
314
 
719
- ## Performance
720
-
721
- - **Batch Fetching**: Get 100+ prices in one API request
722
- - **Smart Caching**: Data-type aware TTL (historical cached forever, EOD 24h, live 30s)
723
- - **Rate Limit Management**: Automatic retry logic for API limits
724
- - **Concurrent Processing**: Handle 1000+ records/second
725
- - **Streaming**: Process GB-sized datasets without memory issues
726
- - **Provider Fallback**: Automatic failover between data sources
315
+ ## API At A Glance
316
+
317
+ ### `StockPriceService`
318
+
319
+ - `register(provider, config?)`
320
+ - `unregister(providerName)`
321
+ - `getPrice({ ticker })`
322
+ - `getPrices({ tickers })`
323
+ - `getData(request)`
324
+ - `checkProvidersHealth()`
325
+ - `getProviderStats()`
326
+ - `clearCache()`
327
+ - `getCacheStats()`
328
+
329
+ ### `FundamentalsService`
330
+
331
+ - `register(provider, config?)`
332
+ - `getFundamentals(ticker)`
333
+ - `getBatchFundamentals(tickers)`
334
+ - `getData(request)`
335
+ - `enrichWithPrice(fundamentals, price)`
336
+ - `checkProvidersHealth()`
337
+ - `getProviderStats()`
338
+ - `clearCache()`
339
+
340
+ ### `StockDataService`
341
+
342
+ - `registerPriceProvider(provider, config?)`
343
+ - `registerFundamentalsProvider(provider, config?)`
344
+ - `getPrice(ticker)`
345
+ - `getPrices(tickers)`
346
+ - `getFundamentals(ticker)`
347
+ - `getBatchFundamentals(tickers)`
348
+ - `getStockData(ticker | request)`
349
+ - `getBatchStockData(tickers | request)`
350
+ - `checkProvidersHealth()`
351
+ - `getProviderStats()`
352
+ - `clearCache()`
353
+
354
+ ### `HandelsregisterClient` (`@fin.cx/opendata/handelsregister`)
355
+
356
+ - `new HandelsregisterClient({ gate, portalUrl?, userAgent?, requestTimeoutMs?, maxResponseBytes?, now? })`
357
+ - `search(query, { signal? })`
358
+ - `getEntry({ courtCode, type, number }, { signal? })`
359
+
360
+ ### `SlidingWindowGate`
361
+
362
+ - `new SlidingWindowGate({ limit?, windowMs?, now? })`
363
+ - `acquire({ kind })`
364
+ - `remaining()`
365
+ - `nextAllowedAt()`
366
+
367
+ ### Register helpers
368
+
369
+ - `parseStructuredContent(xml)`
370
+ - `parseSearchResults(html)`
371
+ - `registerCourts`
372
+ - `findRegisterCourt(codeOrName)`
373
+ - `parseRegisterReferenceText(text)`
374
+ - `parseRegisterNumber(text)`
375
+ - `formatRegisterReference(reference)`
376
+ - `structuredContentNotice`
377
+
378
+ ## Data Sources
379
+
380
+ The package ships no market or register data. Prices and fundamentals come at run time from the providers you register (Marketstack, CoinGecko, SEC EDGAR), and register entries come at run time from the register portal of the federal states (handelsregister.de), one lookup at a time.
381
+
382
+ The only data the package carries are the code tables that `@fin.cx/opendata/handelsregister` uses to decode the structured register content (`ts/handelsregister/data.codelists.ts`). They are official code lists, published on XRepository (https://www.xrepository.de), and were taken on 2026-09-23:
383
+
384
+ | Table | Code list | Issued by |
385
+ | --- | --- | --- |
386
+ | `legalFormLabels` | `urn:xoev-de:xunternehmen:codeliste:rechtsformen_2.4` (Rechtsformen) | XUnternehmen |
387
+ | `roleLabels` | `urn:xoev-de:xjustiz:codeliste:gds.rollenbezeichnung_3.6` (GDS.Rollenbezeichnung) | BLK-AG IT-Standards in der Justiz |
388
+ | `entityStatusLabels` | `urn:xoev-de:xjustiz:codeliste:reg.status-rechtstraeger_2.0` (REG.Status_Rechtstraeger) | BLK-AG IT-Standards in der Justiz |
389
+ | `stateKeyToIso` | `urn:xoev-de:bund:bfj:codeliste:bfj.staat_7.0` (BfJ Staat) | Bundesamt für Justiz, Bonn |
390
+ | `registerCourts` | the register courts of the portal's search form, with their codes from `urn:xoev-de:xjustiz:codeliste:gds.gerichte` | BLK-AG IT-Standards in der Justiz |
391
+
392
+ Codes and designations are reproduced as published, without changes. The tables hold a selection of each list, never an edited entry:
393
+
394
+ - `legalFormLabels`: the concrete legal forms that one of the justice registers can carry, with the short and the long designation;
395
+ - `roleLabels`: the 71 roles that occur in register content;
396
+ - `entityStatusLabels`: the whole list;
397
+ - `stateKeyToIso`: the key and the ISO 3166-1 alpha-2 code of each state or territory that has one;
398
+ - `registerCourts`: the 150 register courts as the portal's search form named them on the same day.
399
+
400
+ A later version of a code list may add codes. The parser keeps a code it does not know as a code, without a designation, and never guesses one.
401
+
402
+ ## Behavior And Caveats
403
+
404
+ - Historical market data is cached aggressively because it is treated as immutable.
405
+ - Intraday data uses interval-aware caching and tries incremental refreshes for recent data.
406
+ - `StockDataService` is convenience-first. `StockPriceService` is the better low-level choice if you care about request-shape-specific cache behavior.
407
+ - The register lookup reads the portal's German pages and asks for German explicitly. It depends on the portal's current page structure: when that changes, calls end in `unexpected_response` rather than in guessed data.
408
+ - The structured register content is a non-binding service of the portal; the current printout is binding. Results carry `binding: false` and the portal's note.
409
+ - There is no hosted backend in this package. Everything is meant to run from your own TypeScript/Node process.
727
410
 
728
411
  ## Testing
729
412
 
730
- Run the comprehensive test suite:
731
-
732
413
  ```bash
733
414
  pnpm test
734
415
  ```
735
416
 
736
- Test specific modules:
737
-
738
- ```bash
739
- # Stock price providers
740
- pnpm tstest test/test.marketstack.node.ts --verbose
741
- pnpm tstest test/test.stocks.ts --verbose
742
-
743
- # Fundamental data
744
- pnpm tstest test/test.secedgar.provider.node.ts --verbose
745
- pnpm tstest test/test.fundamentals.service.node.ts --verbose
746
-
747
- # German business data
748
- pnpm tstest test/test.handelsregister.ts --verbose
749
- ```
750
-
751
- ## Getting API Keys
752
-
753
- ### Marketstack (EOD Stock Data)
754
-
755
- 1. Visit [marketstack.com](https://marketstack.com)
756
- 2. Sign up for a free account (100 requests/month)
757
- 3. Get your API key from the dashboard
758
- 4. Set environment variable: `MARKETSTACK_COM_TOKEN=your_key_here`
759
-
760
- ### SEC EDGAR (Fundamental Data)
761
-
762
- **No API key required!** SEC EDGAR is completely free and public. Just provide your company name and email in the User-Agent:
763
-
764
- ```typescript
765
- new SecEdgarProvider({
766
- userAgent: 'YourCompany youremail@example.com'
767
- });
768
- ```
417
+ Some market data tests hit live remote services, so they may depend on network access or optional API credentials. The register lookup is tested against minimal hand-written pages and structured content with synthetic values only; the tests never contact the register portal.
769
418
 
770
419
  ## License and Legal Information
771
420
 
772
- This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
421
+ This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./license) file.
773
422
 
774
423
  **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
775
424
 
776
425
  ### Trademarks
777
426
 
778
- This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
427
+ This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
428
+
429
+ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
779
430
 
780
431
  ### Company Information
781
432
 
782
- Task Venture Capital GmbH
783
- Registered at District court Bremen HRB 35230 HB, Germany
433
+ Task Venture Capital GmbH
434
+ Registered at District Court Bremen HRB 35230 HB, Germany
784
435
 
785
- For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
436
+ For any legal inquiries or further information, please contact us via email at hello@task.vc.
786
437
 
787
438
  By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.