@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.
- package/{npmextra.json → .smartconfig.json} +24 -18
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/handelsregister/classes.handelsregisterclient.d.ts +64 -0
- package/dist_ts/handelsregister/classes.handelsregisterclient.js +397 -0
- package/dist_ts/handelsregister/classes.handelsregistererror.d.ts +56 -0
- package/dist_ts/handelsregister/classes.handelsregistererror.js +37 -0
- package/dist_ts/handelsregister/classes.portalsession.d.ts +46 -0
- package/dist_ts/handelsregister/classes.portalsession.js +160 -0
- package/dist_ts/handelsregister/classes.slidingwindowgate.d.ts +32 -0
- package/dist_ts/handelsregister/classes.slidingwindowgate.js +55 -0
- package/dist_ts/handelsregister/constants.d.ts +13 -0
- package/dist_ts/handelsregister/constants.js +19 -0
- package/dist_ts/handelsregister/data.codelists.d.ts +29 -0
- package/dist_ts/handelsregister/data.codelists.js +616 -0
- package/dist_ts/handelsregister/forms.d.ts +27 -0
- package/dist_ts/handelsregister/forms.js +77 -0
- package/dist_ts/handelsregister/html.d.ts +26 -0
- package/dist_ts/handelsregister/html.js +82 -0
- package/dist_ts/handelsregister/index.d.ts +9 -0
- package/dist_ts/handelsregister/index.js +10 -0
- package/dist_ts/handelsregister/interfaces.d.ts +240 -0
- package/dist_ts/handelsregister/interfaces.js +2 -0
- package/dist_ts/handelsregister/parse.searchresults.d.ts +35 -0
- package/dist_ts/handelsregister/parse.searchresults.js +175 -0
- package/dist_ts/handelsregister/parse.structuredcontent.d.ts +6 -0
- package/dist_ts/handelsregister/parse.structuredcontent.js +306 -0
- package/dist_ts/handelsregister/plugins.d.ts +4 -0
- package/dist_ts/handelsregister/plugins.js +8 -0
- package/dist_ts/handelsregister/references.d.ts +34 -0
- package/dist_ts/handelsregister/references.js +76 -0
- package/dist_ts/index.d.ts +0 -1
- package/dist_ts/index.js +1 -2
- package/dist_ts/plugins.d.ts +2 -14
- package/dist_ts/plugins.js +8 -16
- package/dist_ts/stocks/classes.baseproviderservice.js +8 -8
- package/dist_ts/stocks/classes.fundamentalsservice.js +9 -9
- package/dist_ts/stocks/classes.stockdataservice.js +23 -23
- package/dist_ts/stocks/classes.stockservice.js +9 -9
- package/dist_ts/stocks/providers/provider.coingecko.d.ts +1 -1
- package/dist_ts/stocks/providers/provider.coingecko.js +45 -39
- package/dist_ts/stocks/providers/provider.marketstack.d.ts +3 -3
- package/dist_ts/stocks/providers/provider.marketstack.js +20 -16
- package/dist_ts/stocks/providers/provider.secedgar.d.ts +1 -1
- package/dist_ts/stocks/providers/provider.secedgar.js +22 -22
- package/dist_ts/stocks/providers/provider.yahoo.d.ts +2 -2
- package/dist_ts/stocks/providers/provider.yahoo.js +14 -13
- package/package.json +30 -31
- package/readme.md +327 -676
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/handelsregister/classes.handelsregisterclient.ts +474 -0
- package/ts/handelsregister/classes.handelsregistererror.ts +82 -0
- package/ts/handelsregister/classes.portalsession.ts +191 -0
- package/ts/handelsregister/classes.slidingwindowgate.ts +74 -0
- package/ts/handelsregister/constants.ts +30 -0
- package/ts/handelsregister/data.codelists.ts +640 -0
- package/ts/handelsregister/forms.ts +99 -0
- package/ts/handelsregister/html.ts +113 -0
- package/ts/handelsregister/index.ts +27 -0
- package/ts/handelsregister/interfaces.ts +272 -0
- package/ts/handelsregister/parse.searchresults.ts +241 -0
- package/ts/handelsregister/parse.structuredcontent.ts +338 -0
- package/ts/handelsregister/plugins.ts +15 -0
- package/ts/handelsregister/references.ts +115 -0
- package/ts/index.ts +0 -1
- package/ts/plugins.ts +9 -29
- package/ts/stocks/classes.fundamentalsservice.ts +1 -1
- package/ts/stocks/classes.stockdataservice.ts +17 -7
- package/ts/stocks/classes.stockservice.ts +2 -2
- package/ts/stocks/providers/provider.marketstack.ts +9 -7
- package/ts/stocks/providers/provider.secedgar.ts +2 -2
- package/ts/stocks/providers/provider.yahoo.ts +3 -3
- package/dist_ts/classes.businessrecord.d.ts +0 -38
- package/dist_ts/classes.businessrecord.js +0 -47
- package/dist_ts/classes.germanbusinessdata.d.ts +0 -9
- package/dist_ts/classes.germanbusinessdata.js +0 -78
- package/dist_ts/classes.handelsregister.d.ts +0 -132
- package/dist_ts/classes.handelsregister.js +0 -288
- package/dist_ts/classes.jsonldata.d.ts +0 -43
- package/dist_ts/classes.jsonldata.js +0 -65
- package/dist_ts/classes.main.opendata.d.ts +0 -24
- package/dist_ts/classes.main.opendata.js +0 -58
- package/readme.hints.md +0 -89
- package/readme.plan.md +0 -193
- package/ts/classes.businessrecord.ts +0 -67
- package/ts/classes.handelsregister.ts +0 -360
- package/ts/classes.jsonldata.ts +0 -113
- package/ts/classes.main.opendata.ts +0 -87
package/readme.md
CHANGED
|
@@ -1,787 +1,438 @@
|
|
|
1
1
|
# @fin.cx/opendata
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
61
|
+
## Quick Start
|
|
18
62
|
|
|
19
|
-
|
|
63
|
+
### Combined Stock Data
|
|
20
64
|
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
67
|
+
```ts
|
|
68
|
+
import {
|
|
69
|
+
MarketstackProvider,
|
|
70
|
+
SecEdgarProvider,
|
|
71
|
+
StockDataService,
|
|
72
|
+
} from '@fin.cx/opendata';
|
|
26
73
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
74
|
+
const stocks = new StockDataService({
|
|
75
|
+
cache: {
|
|
76
|
+
priceTTL: 60_000,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
31
79
|
|
|
32
|
-
|
|
33
|
-
|
|
80
|
+
stocks.registerPriceProvider(
|
|
81
|
+
new MarketstackProvider(process.env.MARKETSTACK_API_KEY!)
|
|
82
|
+
);
|
|
34
83
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
46
|
-
const stocks = await stockData.getBatchStockData(['AAPL', 'MSFT', 'GOOGL']);
|
|
90
|
+
const apple = await stocks.getStockData('AAPL');
|
|
47
91
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
111
|
+
Use `StockPriceService` if you want smarter per-request caching and direct access to current, historical, and intraday request types.
|
|
97
112
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const provider = new CoinGeckoProvider();
|
|
113
|
+
```ts
|
|
114
|
+
import { MarketstackProvider, StockPriceService } from '@fin.cx/opendata';
|
|
101
115
|
|
|
102
|
-
|
|
116
|
+
const prices = new StockPriceService();
|
|
117
|
+
prices.register(new MarketstackProvider(process.env.MARKETSTACK_API_KEY!));
|
|
103
118
|
|
|
104
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
131
|
-
const intraday = await stockService.getData({
|
|
129
|
+
const intraday = await prices.getData({
|
|
132
130
|
type: 'intraday',
|
|
133
|
-
ticker: '
|
|
131
|
+
ticker: 'AAPL',
|
|
134
132
|
interval: '1hour',
|
|
135
|
-
limit: 24
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
143
|
+
Supported stock request shapes:
|
|
173
144
|
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
+
`CoinGeckoProvider` plugs into `StockPriceService`, so crypto lookups use the same request model as stock lookups.
|
|
200
155
|
|
|
201
|
-
|
|
156
|
+
```ts
|
|
157
|
+
import { CoinGeckoProvider, StockPriceService } from '@fin.cx/opendata';
|
|
202
158
|
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
162
|
+
const btc = await prices.getPrice({ ticker: 'BTC' });
|
|
163
|
+
const top = await prices.getPrices({ tickers: ['BTC', 'ETH', 'SOL'] });
|
|
215
164
|
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
+
### Fundamentals Only
|
|
234
171
|
|
|
235
|
-
|
|
172
|
+
Use `FundamentalsService` when you only care about SEC filing data.
|
|
236
173
|
|
|
237
|
-
|
|
174
|
+
```ts
|
|
175
|
+
import { FundamentalsService, SecEdgarProvider } from '@fin.cx/opendata';
|
|
238
176
|
|
|
239
|
-
|
|
240
|
-
|
|
177
|
+
const fundamentals = new FundamentalsService();
|
|
178
|
+
fundamentals.register(
|
|
179
|
+
new SecEdgarProvider({
|
|
180
|
+
userAgent: 'Acme Inc. dev@acme.dev',
|
|
181
|
+
})
|
|
182
|
+
);
|
|
241
183
|
|
|
242
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
###
|
|
195
|
+
### Register Lookups (Handelsregister)
|
|
275
196
|
|
|
276
|
-
|
|
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
|
-
```
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
199
|
+
```ts
|
|
200
|
+
import {
|
|
201
|
+
HandelsregisterClient,
|
|
202
|
+
HandelsregisterError,
|
|
203
|
+
SlidingWindowGate,
|
|
204
|
+
findRegisterCourt,
|
|
205
|
+
formatRegisterReference,
|
|
206
|
+
} from '@fin.cx/opendata/handelsregister';
|
|
282
207
|
|
|
283
|
-
//
|
|
284
|
-
const
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
//
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
256
|
+
Searches take a name or a register number:
|
|
472
257
|
|
|
473
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
+
#### What a result holds
|
|
509
275
|
|
|
510
|
-
|
|
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
|
-
|
|
513
|
-
import { OpenData } from '@fin.cx/opendata';
|
|
514
|
-
import * as path from 'path';
|
|
283
|
+
#### What can happen
|
|
515
284
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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
|
-
|
|
306
|
+
#### Parsing without a request
|
|
532
307
|
|
|
533
|
-
|
|
308
|
+
```ts
|
|
309
|
+
import { parseStructuredContent } from '@fin.cx/opendata/handelsregister';
|
|
534
310
|
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
##
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
-
|
|
724
|
-
-
|
|
725
|
-
-
|
|
726
|
-
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
433
|
+
Task Venture Capital GmbH
|
|
434
|
+
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
784
435
|
|
|
785
|
-
For any legal inquiries or
|
|
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.
|