@fin.cx/opendata 3.6.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.smartconfig.json +23 -20
- 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 +38 -0
- package/dist_ts/handelsregister/references.js +146 -0
- package/dist_ts/holidays/calendar.d.ts +32 -0
- package/dist_ts/holidays/calendar.js +200 -0
- package/dist_ts/holidays/classes.holidaycalendarerror.d.ts +14 -0
- package/dist_ts/holidays/classes.holidaycalendarerror.js +10 -0
- package/dist_ts/holidays/data.holidays.d.ts +46 -0
- package/dist_ts/holidays/data.holidays.js +124 -0
- package/dist_ts/holidays/index.d.ts +4 -0
- package/dist_ts/holidays/index.js +7 -0
- package/dist_ts/holidays/interfaces.d.ts +42 -0
- package/dist_ts/holidays/interfaces.js +2 -0
- package/dist_ts/index.d.ts +0 -2
- package/dist_ts/index.js +1 -3
- package/dist_ts/plugins.d.ts +2 -20
- package/dist_ts/plugins.js +3 -22
- package/package.json +40 -37
- package/readme.md +250 -119
- 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 +189 -0
- package/ts/holidays/calendar.ts +261 -0
- package/ts/holidays/classes.holidaycalendarerror.ts +20 -0
- package/ts/holidays/data.holidays.ts +177 -0
- package/ts/holidays/index.ts +18 -0
- package/ts/holidays/interfaces.ts +87 -0
- package/ts/index.ts +0 -2
- package/ts/plugins.ts +0 -39
- package/dist_ts/classes.businessrecord.d.ts +0 -38
- package/dist_ts/classes.businessrecord.js +0 -90
- package/dist_ts/classes.handelsregister.d.ts +0 -134
- package/dist_ts/classes.handelsregister.js +0 -307
- package/dist_ts/classes.jsonldata.d.ts +0 -43
- package/dist_ts/classes.jsonldata.js +0 -68
- package/dist_ts/classes.main.opendata.d.ts +0 -25
- package/dist_ts/classes.main.opendata.js +0 -91
- package/dist_ts/laws/classes.lawrecord.d.ts +0 -24
- package/dist_ts/laws/classes.lawrecord.js +0 -181
- package/dist_ts/laws/classes.lawservice.d.ts +0 -52
- package/dist_ts/laws/classes.lawservice.js +0 -740
- package/dist_ts/laws/index.d.ts +0 -3
- package/dist_ts/laws/index.js +0 -4
- package/dist_ts/laws/interfaces.law.d.ts +0 -35
- package/dist_ts/laws/interfaces.law.js +0 -2
- package/readme.hints.md +0 -89
- package/ts/classes.businessrecord.ts +0 -67
- package/ts/classes.handelsregister.ts +0 -385
- package/ts/classes.jsonldata.ts +0 -114
- package/ts/classes.main.opendata.ts +0 -120
- package/ts/laws/classes.lawrecord.ts +0 -81
- package/ts/laws/classes.lawservice.ts +0 -1010
- package/ts/laws/index.ts +0 -3
- package/ts/laws/interfaces.law.ts +0 -48
package/readme.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# @fin.cx/opendata
|
|
2
2
|
|
|
3
|
-
`@fin.cx/opendata` is a TypeScript toolbox for
|
|
3
|
+
`@fin.cx/opendata` is a TypeScript toolbox for open business and market data: stock and crypto prices, SEC fundamentals, lookups in the German registers through the register portal of the federal states, and the statutory public holidays of the German 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
|
-
It
|
|
5
|
+
It gives you four practical lanes in one package:
|
|
6
6
|
|
|
7
7
|
- `StockPriceService` for stock and crypto prices with caching, retries, failover, historical ranges, and intraday support
|
|
8
8
|
- `FundamentalsService` and `StockDataService` for SEC fundamentals and combined price + fundamentals payloads
|
|
9
|
-
- `
|
|
10
|
-
-
|
|
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
|
+
- the holiday calendar, from `@fin.cx/opendata/holidays`, for the statutory public holidays of each German federal state and the working days of § 193 BGB, computed per year without data files or requests
|
|
11
11
|
|
|
12
12
|
## Issue Reporting and Security
|
|
13
13
|
|
|
@@ -36,17 +36,28 @@ pnpm add @fin.cx/opendata
|
|
|
36
36
|
- `SecEdgarProvider`
|
|
37
37
|
Fundamentals provider backed by SEC EDGAR company facts.
|
|
38
38
|
|
|
39
|
-
### German
|
|
39
|
+
### German Registers
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
Starts a local embedded database, imports the public German company JSONL dump, and exposes a `handelsregister` helper for company lookup and file download.
|
|
41
|
+
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.
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
- `HandelsregisterClient`
|
|
44
|
+
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.
|
|
45
|
+
- `SlidingWindowGate`
|
|
46
|
+
Holds the portal's limit of 60 searches and retrievals per hour. The client asks it before every counted request.
|
|
47
|
+
- `parseStructuredContent()` and `parseSearchResults()`
|
|
48
|
+
The parsers on their own, for an SI file or a result page someone saved from the portal. They make no request.
|
|
49
|
+
- `registerCourts`, `findRegisterCourt()`, `parseRegisterReferenceText()`, `formatRegisterReference()`
|
|
50
|
+
The 150 register courts with their XJustiz codes, and helpers for register references as businesses print them (`Amtsgericht Bremen HRB 35230 HB`).
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
### German Public Holidays
|
|
53
|
+
|
|
54
|
+
Everything here comes from `@fin.cx/opendata/holidays`. That entry point imports nothing else of the package, has no dependencies, and makes no request.
|
|
55
|
+
|
|
56
|
+
- `getPublicHolidays(year, state)`, `getPublicHolidaysOn(date, state)`
|
|
57
|
+
The statutory public holidays of a federal state in a year or on a date, each with its legal basis.
|
|
58
|
+
- `isPublicHoliday()`, `isWorkingDay()`, `nextWorkingDay()`, `workingDayOnOrAfter()`
|
|
59
|
+
Holidays and working days in the sense of § 193 BGB: neither a Saturday, nor a Sunday, nor a statutory public holiday.
|
|
60
|
+
- `easterSunday(year)`, `germanStates`, `findGermanState()`
|
|
50
61
|
|
|
51
62
|
## Before You Start
|
|
52
63
|
|
|
@@ -54,10 +65,8 @@ pnpm add @fin.cx/opendata
|
|
|
54
65
|
- `MarketstackProvider` requires an API key.
|
|
55
66
|
- `SecEdgarProvider` requires a valid `User-Agent` string in the format `Company Name email@example.com`.
|
|
56
67
|
- `CoinGeckoProvider` works without an API key, but a key gives you better rate limits.
|
|
57
|
-
- `
|
|
58
|
-
-
|
|
59
|
-
- `LawService` stores data locally. It auto-starts on demand, but explicit `start()` and `stop()` keep lifecycle handling cleaner.
|
|
60
|
-
- EU law syncing and Handelsregister access use a headless browser under the hood.
|
|
68
|
+
- `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.
|
|
69
|
+
- 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).
|
|
61
70
|
- `StockDataService` defaults to a 24 hour price cache. If you want fresher quote caching, pass `cache.priceTTL` explicitly or use `StockPriceService` directly.
|
|
62
71
|
|
|
63
72
|
## Quick Start
|
|
@@ -194,118 +203,176 @@ console.log({
|
|
|
194
203
|
});
|
|
195
204
|
```
|
|
196
205
|
|
|
197
|
-
###
|
|
206
|
+
### Register Lookups (Handelsregister)
|
|
198
207
|
|
|
199
|
-
`
|
|
208
|
+
`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.
|
|
200
209
|
|
|
201
210
|
```ts
|
|
202
|
-
import {
|
|
203
|
-
|
|
211
|
+
import {
|
|
212
|
+
HandelsregisterClient,
|
|
213
|
+
HandelsregisterError,
|
|
214
|
+
SlidingWindowGate,
|
|
215
|
+
findRegisterCourt,
|
|
216
|
+
formatRegisterReference,
|
|
217
|
+
} from '@fin.cx/opendata/handelsregister';
|
|
218
|
+
|
|
219
|
+
// one gate for the whole installation: 60 searches or retrievals per hour
|
|
220
|
+
const gate = new SlidingWindowGate();
|
|
221
|
+
|
|
222
|
+
const register = new HandelsregisterClient({
|
|
223
|
+
gate,
|
|
224
|
+
userAgent: 'Example Books (+https://example.com/contact)',
|
|
225
|
+
});
|
|
204
226
|
|
|
205
|
-
|
|
227
|
+
// by name: every row the portal lists, in its order, never narrowed down to one
|
|
228
|
+
const found = await register.search({ name: 'Task Venture Capital', seat: 'Bremen' });
|
|
229
|
+
const choices = found.matches.map((match) => ({
|
|
230
|
+
reference: formatRegisterReference(match.reference), // 'Amtsgericht Bremen HRB 35230 HB'
|
|
231
|
+
name: match.name,
|
|
232
|
+
sheet: match.sheetStatus, // 'current' or 'closed'
|
|
233
|
+
formerNames: match.formerNames,
|
|
234
|
+
}));
|
|
235
|
+
if (found.truncated) {
|
|
236
|
+
// the portal stops at 100 hits: add the seat, the court or the register number
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// one register sheet, with its structured content
|
|
240
|
+
const court = findRegisterCourt('Amtsgericht Bremen');
|
|
241
|
+
if (court) {
|
|
242
|
+
const entry = await register.getEntry({ courtCode: court.code, type: 'HRB', number: '35230 HB' });
|
|
243
|
+
const content = entry.structuredContent; // null when the portal offers none for this entry
|
|
244
|
+
if (content) {
|
|
245
|
+
const facts = {
|
|
246
|
+
name: content.name,
|
|
247
|
+
legalForm: content.legalForm?.short, // 'GmbH'
|
|
248
|
+
address: content.address,
|
|
249
|
+
status: content.status?.state, // 'active', 'deleted', 'in_liquidation', 'insolvent', 'merged'
|
|
250
|
+
representatives: content.representatives, // [{ name, function, functionCode, kind }]
|
|
251
|
+
retrievedAt: entry.retrievedAt,
|
|
252
|
+
notice: entry.notice.en, // the portal's note: the structured content is not binding
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// every failure is typed
|
|
258
|
+
try {
|
|
259
|
+
await register.getEntry({ courtCode: 'H1101', type: 'HRB', number: '35230' });
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (error instanceof HandelsregisterError && error.code === 'rate_limited') {
|
|
262
|
+
const nextPossibleAt = new Date(error.retryAt ?? Date.now());
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
206
266
|
|
|
207
|
-
|
|
208
|
-
nogitDir,
|
|
209
|
-
downloadDir: path.join(nogitDir, 'downloads'),
|
|
210
|
-
germanBusinessDataDir: path.join(nogitDir, 'germanbusinessdata'),
|
|
211
|
-
});
|
|
267
|
+
Searches take a name or a register number:
|
|
212
268
|
|
|
213
|
-
|
|
269
|
+
- `{ name, match?: 'all' | 'any' | 'exact', seat?, type?, courtCode?, includeClosed? }`
|
|
270
|
+
- `{ 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.
|
|
214
271
|
|
|
215
|
-
|
|
272
|
+
`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.
|
|
216
273
|
|
|
217
|
-
|
|
218
|
-
court: 'Munich',
|
|
219
|
-
type: 'HRB',
|
|
220
|
-
number: '6684',
|
|
221
|
-
});
|
|
274
|
+
#### What counts against the 60 per hour
|
|
222
275
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
276
|
+
| Call | Counted requests | Also loaded |
|
|
277
|
+
| --- | --- | --- |
|
|
278
|
+
| `search()` | 1 search | the search form and the result list |
|
|
279
|
+
| `getEntry()` | 1 search, plus 1 retrieval when the portal offers structured content | the search form and the result list |
|
|
227
280
|
|
|
228
|
-
|
|
229
|
-
```
|
|
281
|
+
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.
|
|
230
282
|
|
|
231
|
-
|
|
283
|
+
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.
|
|
232
284
|
|
|
233
|
-
|
|
234
|
-
- `await openData.buildInitialDb()` to ingest the public German company JSONL dataset
|
|
235
|
-
- `await openData.handelsregister.searchCompany(name, limit)`
|
|
236
|
-
- `await openData.handelsregister.getSpecificCompany({ court, type, number })`
|
|
237
|
-
- `await openData.handelsregister.getSpecificCompanyByName(name)`
|
|
285
|
+
#### What a result holds
|
|
238
286
|
|
|
239
|
-
|
|
287
|
+
- `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.
|
|
288
|
+
- `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`.
|
|
289
|
+
- 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.
|
|
290
|
+
- The structured content states no date for the status itself; `lastEntryOn` is the date of the latest entry on the sheet.
|
|
291
|
+
- Of people the parser reads the name and the function only. Birth dates, places of residence and private addresses never leave it.
|
|
292
|
+
- 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.
|
|
240
293
|
|
|
241
|
-
|
|
294
|
+
#### What can happen
|
|
242
295
|
|
|
243
|
-
|
|
296
|
+
| Case | Result |
|
|
297
|
+
| --- | --- |
|
|
298
|
+
| No match | `search()` returns no matches; `getEntry()` fails with `not_found` |
|
|
299
|
+
| Several matches in different courts | every row, in the portal's order |
|
|
300
|
+
| More than 100 hits | the first 100 and `truncated: true` |
|
|
301
|
+
| Renamed entity | the current name, earlier names in `formerNames` |
|
|
302
|
+
| Deleted or moved entity | `sheetStatus: 'closed'`, no structured content |
|
|
303
|
+
| Dissolved or insolvent entity | `status.state` `in_liquidation` or `insolvent`, and the liquidators among the representatives |
|
|
304
|
+
| Branch of a company under foreign law | its foreign legal form and seat, and its permanent representatives |
|
|
305
|
+
| Partnership, cooperative, association, civil-law partnership | register types `PR`, `GnR`, `VR`, `GsR` |
|
|
306
|
+
| Number without suffix that fits several sheets | `ambiguous` with the candidates |
|
|
307
|
+
| Suffix that fits no sheet | `not_found` with the sheets of the same number |
|
|
308
|
+
| Structured content of another sheet | `unexpected_response` |
|
|
309
|
+
| Portal answers with an HTTP error (blocked address, overload) | `refused` with `status` |
|
|
310
|
+
| Portal shows its notice that it refuses retrievals from this address | `refused` with its words in `portalMessage` |
|
|
311
|
+
| Portal too slow, unreachable, or call aborted | `timeout`, `unreachable`, `aborted` |
|
|
312
|
+
| Portal changed its pages or answered in another language | `unexpected_response`, with the portal's own message in `portalMessage` when it showed one |
|
|
313
|
+
| Structured content that cannot be read | `invalid_structured_content` |
|
|
244
314
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
315
|
+
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.
|
|
316
|
+
|
|
317
|
+
#### Parsing without a request
|
|
248
318
|
|
|
249
319
|
```ts
|
|
250
|
-
import {
|
|
320
|
+
import { parseStructuredContent } from '@fin.cx/opendata/handelsregister';
|
|
251
321
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
govInfoApiKey: process.env.GOVINFO_API_KEY,
|
|
256
|
-
});
|
|
322
|
+
// an SI file a person downloaded from the portal themselves
|
|
323
|
+
const content = parseStructuredContent(xmlText);
|
|
324
|
+
```
|
|
257
325
|
|
|
258
|
-
|
|
326
|
+
#### Printed court names
|
|
259
327
|
|
|
260
|
-
|
|
261
|
-
jurisdiction: 'de',
|
|
262
|
-
identifier: 'aeg',
|
|
263
|
-
});
|
|
328
|
+
`findRegisterCourt()` takes a court's code or its name as the portal or a business prints it: `AG Bremen`, `Amtsgericht Bremen – Registergericht`, `Registergericht Bremen`, `Handelsregister B des Amtsgerichts Bremen`, `Amtsgericht Frankfurt a. M.`, `Frankfurt/O.`, `Kempten` for `Kempten (Allgäu)`, `Freiburg i. Br.`, and `AG Berlin` or `Charlottenburg` for the register court of Berlin. Umlauts may be spelled out (`Muenchen`). It returns a court only when exactly one fits: `Frankfurt` alone could be either Frankfurt am Main or Frankfurt (Oder) and names no court.
|
|
264
329
|
|
|
265
|
-
|
|
266
|
-
jurisdiction: 'eu',
|
|
267
|
-
identifier: '32024R1689',
|
|
268
|
-
language: 'EN',
|
|
269
|
-
});
|
|
330
|
+
### Public Holidays
|
|
270
331
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
332
|
+
```ts
|
|
333
|
+
import {
|
|
334
|
+
getPublicHolidays,
|
|
335
|
+
isWorkingDay,
|
|
336
|
+
nextWorkingDay,
|
|
337
|
+
workingDayOnOrAfter,
|
|
338
|
+
} from '@fin.cx/opendata/holidays';
|
|
275
339
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
limit: 5,
|
|
280
|
-
});
|
|
340
|
+
getPublicHolidays(2026, 'DE-NI');
|
|
341
|
+
// [{ date: '2026-01-01', id: 'neujahr', name: 'Neujahr', state: 'DE-NI', scope: 'state',
|
|
342
|
+
// legalBasis: '§ 2 Abs. 1 NFeiertagsG' }, …, { date: '2026-10-31', id: 'reformationstag', … }, …]
|
|
281
343
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
eu: euLaw.title,
|
|
285
|
-
us: usCode.title,
|
|
286
|
-
hits: results.length,
|
|
287
|
-
});
|
|
344
|
+
isWorkingDay('2026-06-04', 'DE-HE', { regionalHolidays: 'ignore' }); // false: Fronleichnam
|
|
345
|
+
nextWorkingDay('2026-04-02', 'DE-BY', { regionalHolidays: 'ignore' }); // '2026-04-07'
|
|
288
346
|
|
|
289
|
-
|
|
347
|
+
// § 193 BGB: a deadline that ends on a Saturday, Sunday or holiday ends on the next working day
|
|
348
|
+
workingDayOnOrAfter('2026-05-14', 'DE-BY', { regionalHolidays: 'ignore' }); // '2026-05-15'
|
|
290
349
|
```
|
|
291
350
|
|
|
292
|
-
`
|
|
351
|
+
States are ISO 3166-2 codes (`DE-BY`); `findGermanState('Bayern')` and `findGermanState('BY')` find one. Dates are plain calendar dates written `YYYY-MM-DD`, so no time zone can move them.
|
|
293
352
|
|
|
294
|
-
|
|
295
|
-
await laws.syncLaws({
|
|
296
|
-
jurisdiction: 'us',
|
|
297
|
-
usCollection: 'PLAW',
|
|
298
|
-
limit: 25,
|
|
299
|
-
since: new Date('2025-01-01'),
|
|
300
|
-
});
|
|
301
|
-
```
|
|
353
|
+
#### Regional holidays
|
|
302
354
|
|
|
303
|
-
|
|
355
|
+
Four holidays apply only in part of a state:
|
|
304
356
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
357
|
+
| Holiday | State | Where | Legal basis |
|
|
358
|
+
| --- | --- | --- | --- |
|
|
359
|
+
| Augsburger Friedensfest (8 August) | Bayern | Stadt Augsburg | Art. 1 Abs. 2 FTG |
|
|
360
|
+
| Mariä Himmelfahrt (15 August) | Bayern | municipalities with a predominantly Catholic population, as the Bayerisches Landesamt für Statistik determines them after the last census | Art. 1 Abs. 1 Nr. 2, Abs. 3 FTG |
|
|
361
|
+
| Fronleichnam | Sachsen | the municipalities listed in the annex of the FronleichnamsVO (former districts Bautzen, Hoyerswerda and Kamenz), after a merger only the respective part of the municipality | § 1 Abs. 1 SächsSFG, § 1 FronleichnamsVO |
|
|
362
|
+
| Fronleichnam | Thüringen | the parts of Thüringen where Fronleichnam was a statutory holiday in 1994 | § 2 Abs. 2, § 10 Abs. 1 ThürFGtG |
|
|
363
|
+
|
|
364
|
+
`getPublicHolidays()` lists them with `scope: 'regional'` and the `region` they apply to. The calendar holds no list of these municipalities: the Bavarian list follows the census and the Saxon and Thuringian ones name parts of municipalities, so a place cannot be matched to them reliably by a key. Every working-day question therefore says how to treat them:
|
|
365
|
+
|
|
366
|
+
- `{ regionalHolidays: 'count' }` treats them as holidays. A period then never ends on a day that is a holiday at the place in question, at the price of ending one day later where it is not. Use it when the place is unknown.
|
|
367
|
+
- `{ regionalHolidays: 'ignore' }` treats them as working days, when the place is known to lie outside that part.
|
|
368
|
+
|
|
369
|
+
#### What the calendar covers
|
|
370
|
+
|
|
371
|
+
- The statutory public holidays of the 16 federal states from 2018 on, and 3 October from Art. 2 Abs. 2 of the Unification Treaty. Every change since 2018 applies from its year: Reformationstag in Bremen, Hamburg, Niedersachsen and Schleswig-Holstein (2018), Frauentag in Berlin (2019) and Mecklenburg-Vorpommern (2023), Weltkindertag in Thüringen (2019), and the one-off holidays of Berlin on 8 May 2020, 8 May 2025 and 17 June 2028. Years before 2018 end in `HolidayCalendarError` with the code `unsupported_year`.
|
|
372
|
+
- Later years follow the laws as they stood when this version read them (`holidayLawsReadOn`). Several states allow their government to declare one-off holidays by ordinance; one declared after that day needs a new version.
|
|
373
|
+
- The Easter-based holidays follow Easter Sunday of the Gregorian calendar; Buß- und Bettag (Sachsen) is the Wednesday before the last Sunday before the first Sunday of Advent.
|
|
374
|
+
- The laws of Berlin and Hessen also count every Sunday as a public holiday, and Brandenburg names Ostersonntag and Pfingstsonntag. The calendar lists the Sundays Brandenburg names; for working days every Sunday is excluded anyway.
|
|
375
|
+
- Religious holidays that protect only church services or give employees time off (for example Fronleichnam outside its region in Sachsen, § 3 SächsSFG) and memorial days (Gedenktage) are no statutory public holidays and are not listed.
|
|
309
376
|
|
|
310
377
|
## API At A Glance
|
|
311
378
|
|
|
@@ -346,33 +413,97 @@ Useful request patterns:
|
|
|
346
413
|
- `getProviderStats()`
|
|
347
414
|
- `clearCache()`
|
|
348
415
|
|
|
349
|
-
### `
|
|
350
|
-
|
|
351
|
-
- `
|
|
352
|
-
- `
|
|
353
|
-
- `
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
- `
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
- `
|
|
365
|
-
- `
|
|
416
|
+
### `HandelsregisterClient` (`@fin.cx/opendata/handelsregister`)
|
|
417
|
+
|
|
418
|
+
- `new HandelsregisterClient({ gate, portalUrl?, userAgent?, requestTimeoutMs?, maxResponseBytes?, now? })`
|
|
419
|
+
- `search(query, { signal? })`
|
|
420
|
+
- `getEntry({ courtCode, type, number }, { signal? })`
|
|
421
|
+
|
|
422
|
+
### `SlidingWindowGate`
|
|
423
|
+
|
|
424
|
+
- `new SlidingWindowGate({ limit?, windowMs?, now? })`
|
|
425
|
+
- `acquire({ kind })`
|
|
426
|
+
- `remaining()`
|
|
427
|
+
- `nextAllowedAt()`
|
|
428
|
+
|
|
429
|
+
### Register helpers
|
|
430
|
+
|
|
431
|
+
- `parseStructuredContent(xml)`
|
|
432
|
+
- `parseSearchResults(html)`
|
|
433
|
+
- `registerCourts`
|
|
434
|
+
- `findRegisterCourt(codeOrName)`
|
|
435
|
+
- `parseRegisterReferenceText(text)`
|
|
436
|
+
- `parseRegisterNumber(text)`
|
|
437
|
+
- `formatRegisterReference(reference)`
|
|
438
|
+
- `structuredContentNotice`
|
|
439
|
+
|
|
440
|
+
### Holiday calendar (`@fin.cx/opendata/holidays`)
|
|
441
|
+
|
|
442
|
+
- `getPublicHolidays(year, state)`
|
|
443
|
+
- `getPublicHolidaysOn(date, state)`
|
|
444
|
+
- `isPublicHoliday(date, state, { regionalHolidays })`
|
|
445
|
+
- `isWorkingDay(date, state, { regionalHolidays })`
|
|
446
|
+
- `nextWorkingDay(date, state, { regionalHolidays })`
|
|
447
|
+
- `workingDayOnOrAfter(date, state, { regionalHolidays })`
|
|
448
|
+
- `easterSunday(year)`
|
|
449
|
+
- `germanStates`, `findGermanState(codeOrName)`
|
|
450
|
+
- `holidayCalendarFirstYear`, `holidayLawsReadOn`
|
|
451
|
+
- `HolidayCalendarError` with the codes `invalid_date`, `unsupported_year`, `unknown_state` and `invalid_options`
|
|
452
|
+
|
|
453
|
+
## Data Sources
|
|
454
|
+
|
|
455
|
+
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.
|
|
456
|
+
|
|
457
|
+
The package carries two kinds of data: the holiday rules of `@fin.cx/opendata/holidays` (`ts/holidays/data.holidays.ts`), described at the end of this section, and the code tables that `@fin.cx/opendata/handelsregister` uses to decode the structured register content (`ts/handelsregister/data.codelists.ts`). The code tables are official code lists, published on XRepository (https://www.xrepository.de), and were taken on 2026-09-23:
|
|
458
|
+
|
|
459
|
+
| Table | Code list | Issued by |
|
|
460
|
+
| --- | --- | --- |
|
|
461
|
+
| `legalFormLabels` | `urn:xoev-de:xunternehmen:codeliste:rechtsformen_2.4` (Rechtsformen) | XUnternehmen |
|
|
462
|
+
| `roleLabels` | `urn:xoev-de:xjustiz:codeliste:gds.rollenbezeichnung_3.6` (GDS.Rollenbezeichnung) | BLK-AG IT-Standards in der Justiz |
|
|
463
|
+
| `entityStatusLabels` | `urn:xoev-de:xjustiz:codeliste:reg.status-rechtstraeger_2.0` (REG.Status_Rechtstraeger) | BLK-AG IT-Standards in der Justiz |
|
|
464
|
+
| `stateKeyToIso` | `urn:xoev-de:bund:bfj:codeliste:bfj.staat_7.0` (BfJ Staat) | Bundesamt für Justiz, Bonn |
|
|
465
|
+
| `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 |
|
|
466
|
+
|
|
467
|
+
Codes and designations are reproduced as published, without changes. The tables hold a selection of each list, never an edited entry:
|
|
468
|
+
|
|
469
|
+
- `legalFormLabels`: the concrete legal forms that one of the justice registers can carry, with the short and the long designation;
|
|
470
|
+
- `roleLabels`: the 71 roles that occur in register content;
|
|
471
|
+
- `entityStatusLabels`: the whole list;
|
|
472
|
+
- `stateKeyToIso`: the key and the ISO 3166-1 alpha-2 code of each state or territory that has one;
|
|
473
|
+
- `registerCourts`: the 150 register courts as the portal's search form named them on the same day.
|
|
474
|
+
|
|
475
|
+
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.
|
|
476
|
+
|
|
477
|
+
The holiday calendar is derived from the holiday laws of the federal states, read on 2026-09-24 in their official portals, from Art. 2 Abs. 2 of the Unification Treaty for 3 October, and it answers the question of § 193 BGB:
|
|
478
|
+
|
|
479
|
+
| State | Law | Provision |
|
|
480
|
+
| --- | --- | --- |
|
|
481
|
+
| all | [Einigungsvertrag](https://www.gesetze-im-internet.de/einigvtr/art_2.html) | Art. 2 Abs. 2 (Tag der Deutschen Einheit) |
|
|
482
|
+
| Baden-Württemberg | [Feiertagsgesetz (FTG)](https://www.landesrecht-bw.de/perma?j=FeiertG_BW_!_1) | § 1 |
|
|
483
|
+
| Bayern | [Feiertagsgesetz (FTG)](https://www.gesetze-bayern.de/Content/Document/BayFTG-1) | Art. 1 |
|
|
484
|
+
| Berlin | [Gesetz über die Sonn- und Feiertage](https://gesetze.berlin.de/perma?j=FeiertG_BE_!_1) | § 1 Abs. 1 |
|
|
485
|
+
| Brandenburg | [Feiertagsgesetz (FTG)](https://bravors.brandenburg.de/gesetze/ftg_2015) | § 2 Abs. 1 |
|
|
486
|
+
| Bremen | [Gesetz über die Sonn-, Gedenk- und Feiertage](https://www.transparenz.bremen.de/sixcms/detail.php?gsid=bremen2014_tp.c.145882.de) | § 2 Abs. 1 |
|
|
487
|
+
| Hamburg | [Feiertagsgesetz](https://www.landesrecht-hamburg.de/perma?d=jlr-NNLHH00006CB9NN00000000002) | § 1 |
|
|
488
|
+
| Hessen | [Hessisches Feiertagsgesetz (HFeiertagsG)](https://www.rv.hessenrecht.hessen.de/perma?j=FeiertG_HE_!_1) | § 1 Abs. 1 |
|
|
489
|
+
| Mecklenburg-Vorpommern | [Feiertagsgesetz (FTG M-V)](https://www.landesrecht-mv.de/perma?d=jlr-NNLMV00007A2FNN00000000003) | § 2 Abs. 1 |
|
|
490
|
+
| Niedersachsen | [Niedersächsisches Gesetz über die Feiertage (NFeiertagsG)](https://voris.wolterskluwer-online.de/browse/document/f74bc6e7-6ded-3c2c-9afb-b34620456e56) | § 2 Abs. 1 |
|
|
491
|
+
| Nordrhein-Westfalen | [Gesetz über die Sonn- und Feiertage (Feiertagsgesetz NW)](https://recht.nrw.de/lrgv/gesetz/01012000-bekanntmachung-der-neufassung-des-gesetzes-ueber-die-sonn-und-feiertage) | § 2 Abs. 1 |
|
|
492
|
+
| Rheinland-Pfalz | [Feiertagsgesetz (LFtG)](https://landesrecht.rlp.de/perma?d=jlr-NNLRP00006125NN00000000004) | § 2 Abs. 1 |
|
|
493
|
+
| Saarland | [Feiertagsgesetz (SFG)](https://recht.saarland.de/perma?d=jlr-NNLSL0000A2D6NN00000000007) | § 2 Abs. 1 |
|
|
494
|
+
| Sachsen | [Sächsisches Sonn- und Feiertagsgesetz (SächsSFG)](https://www.revosax.sachsen.de/vorschrift/3997-SaechsSFG), [FronleichnamsVO](https://www.revosax.sachsen.de/vorschrift/4142) | § 1 Abs. 1; § 1 and annex |
|
|
495
|
+
| Sachsen-Anhalt | [Gesetz über die Sonn- und Feiertage (FeiertG LSA)](https://www.landesrecht.sachsen-anhalt.de/perma?d=jlr-NNLST0000411CNN00000000003) | § 2 |
|
|
496
|
+
| Schleswig-Holstein | [Gesetz über Sonn- und Feiertage (SFTG)](https://www.gesetze-rechtsprechung.sh.juris.de/perma?j=FeiertG_SH_!_2) | § 2 Abs. 1 |
|
|
497
|
+
| Thüringen | [Thüringer Feier- und Gedenktagsgesetz (ThürFGtG)](https://landesrecht.thueringen.de/perma?j=FeiertG_TH_!_2) | § 2, § 10 Abs. 1 |
|
|
498
|
+
| all | [Bürgerliches Gesetzbuch](https://www.gesetze-im-internet.de/bgb/__193.html) | § 193 (Sonntag, Feiertag, Sonnabend) |
|
|
366
499
|
|
|
367
500
|
## Behavior And Caveats
|
|
368
501
|
|
|
369
502
|
- Historical market data is cached aggressively because it is treated as immutable.
|
|
370
503
|
- Intraday data uses interval-aware caching and tries incremental refreshes for recent data.
|
|
371
504
|
- `StockDataService` is convenience-first. `StockPriceService` is the better low-level choice if you care about request-shape-specific cache behavior.
|
|
372
|
-
-
|
|
373
|
-
-
|
|
374
|
-
- The most useful public `OpenData` features today are startup, JSONL import, and Handelsregister operations. Some extra validation/search helper methods exist in source but are still stubs.
|
|
375
|
-
- `LawService` persists synced laws locally, so repeated searches can stay offline once the data has been synced.
|
|
505
|
+
- 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.
|
|
506
|
+
- 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.
|
|
376
507
|
- There is no hosted backend in this package. Everything is meant to run from your own TypeScript/Node process.
|
|
377
508
|
|
|
378
509
|
## Testing
|
|
@@ -381,7 +512,7 @@ Useful request patterns:
|
|
|
381
512
|
pnpm test
|
|
382
513
|
```
|
|
383
514
|
|
|
384
|
-
Some tests hit live remote services, so they may depend on network access
|
|
515
|
+
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.
|
|
385
516
|
|
|
386
517
|
## License and Legal Information
|
|
387
518
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@fin.cx/opendata',
|
|
6
|
-
version: '
|
|
7
|
-
description: '
|
|
6
|
+
version: '4.1.0',
|
|
7
|
+
description: 'Open business and financial data for TypeScript: register lookups on the German register portal (handelsregister.de) without storage, the statutory public holidays of the German federal states, and stock and crypto prices and fundamentals from several providers.'
|
|
8
8
|
}
|