@fin.cx/opendata 4.3.1 → 4.5.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 +5 -2
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/cpi/classes.bundesbankcpiclient.d.ts +25 -0
- package/dist_ts/cpi/classes.bundesbankcpiclient.js +250 -0
- package/dist_ts/cpi/classes.cpierror.d.ts +44 -0
- package/dist_ts/cpi/classes.cpierror.js +30 -0
- package/dist_ts/cpi/constants.d.ts +24 -0
- package/dist_ts/cpi/constants.js +25 -0
- package/dist_ts/cpi/decimal.d.ts +13 -0
- package/dist_ts/cpi/decimal.js +33 -0
- package/dist_ts/cpi/index.d.ts +5 -0
- package/dist_ts/cpi/index.js +9 -0
- package/dist_ts/cpi/indexmath.d.ts +47 -0
- package/dist_ts/cpi/indexmath.js +198 -0
- package/dist_ts/cpi/interfaces.d.ts +120 -0
- package/dist_ts/cpi/interfaces.js +2 -0
- package/dist_ts/cpi/request.d.ts +9 -0
- package/dist_ts/cpi/request.js +45 -0
- package/dist_ts/plugins.d.ts +1 -2
- package/dist_ts/plugins.js +2 -3
- package/package.json +14 -7
- package/readme.md +86 -3
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/cpi/classes.bundesbankcpiclient.ts +292 -0
- package/ts/cpi/classes.cpierror.ts +76 -0
- package/ts/cpi/constants.ts +28 -0
- package/ts/cpi/decimal.ts +45 -0
- package/ts/cpi/index.ts +16 -0
- package/ts/cpi/indexmath.ts +230 -0
- package/ts/cpi/interfaces.ts +137 -0
- package/ts/cpi/request.ts +49 -0
- package/ts/plugins.ts +0 -2
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { CpiError } from './classes.cpierror.js';
|
|
2
|
+
import { atScale, divideRounded, formatUnits, parseDecimal, powerOfTen } from './decimal.js';
|
|
3
|
+
// The arithmetic of index clauses as the Statistisches Bundesamt describes it ("Anleitung für die
|
|
4
|
+
// Berechnung von Schwellenwerten und Veränderungsraten für Wertsicherungsklauseln", Stand Januar 2024;
|
|
5
|
+
// "Allgemeine Informationen zum Verbraucherpreisindex"). Every calculation is exact: values are integers
|
|
6
|
+
// of their published digits, and every rounding is half away from zero ("kaufmännisch").
|
|
7
|
+
const MONTH = /^\d{4}-(0[1-9]|1[0-2])$/u;
|
|
8
|
+
const YEAR = /^\d{4}$/u;
|
|
9
|
+
/** The frequency a period is written in, or `undefined` for something that is no period. */
|
|
10
|
+
export const frequencyOf = (periodArg) => {
|
|
11
|
+
if (typeof periodArg !== 'string') {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return MONTH.test(periodArg) ? 'monthly' : YEAR.test(periodArg) ? 'annual' : undefined;
|
|
15
|
+
};
|
|
16
|
+
/** A published value, read as an exact decimal; an estimate or something that is no index value is refused. */
|
|
17
|
+
const publishedValueOf = (valueArg) => {
|
|
18
|
+
if (!frequencyOf(valueArg.period)) {
|
|
19
|
+
throw new CpiError('invalid_input', `${JSON.stringify(valueArg.period)} is no period written YYYY-MM or YYYY.`);
|
|
20
|
+
}
|
|
21
|
+
const decimal = parseDecimal(valueArg.value);
|
|
22
|
+
if (!decimal || decimal.units <= 0n) {
|
|
23
|
+
throw new CpiError('invalid_input', `${JSON.stringify(valueArg.value)} is no index value: a positive decimal written with a point.`, { period: valueArg.period });
|
|
24
|
+
}
|
|
25
|
+
if (valueArg.status !== 'final') {
|
|
26
|
+
throw new CpiError('not_published', `the value for ${valueArg.period} is an estimate, not a published index value.`, { period: valueArg.period, estimate: valueArg });
|
|
27
|
+
}
|
|
28
|
+
return decimal;
|
|
29
|
+
};
|
|
30
|
+
/** Values of one index, one base and one frequency; anything else is never compared. */
|
|
31
|
+
const requireComparable = (leftArg, rightArg) => {
|
|
32
|
+
if (leftArg.index !== rightArg.index || leftArg.base !== rightArg.base || frequencyOf(leftArg.period) !== frequencyOf(rightArg.period)) {
|
|
33
|
+
throw new CpiError('base_mismatch', `${leftArg.index} ${leftArg.period} (${leftArg.base}) and ${rightArg.index} ${rightArg.period} (${rightArg.base}) are not values of one series and one base; read both on the current base.`);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/** The change in percent, in tenths of a percent: ((new / old) × 100) − 100, rounded to one decimal. */
|
|
37
|
+
const changeTenthsOf = (oldArg, newArg) => {
|
|
38
|
+
const oldDecimal = publishedValueOf(oldArg);
|
|
39
|
+
const newDecimal = publishedValueOf(newArg);
|
|
40
|
+
requireComparable(oldArg, newArg);
|
|
41
|
+
const scale = Math.max(oldDecimal.scale, newDecimal.scale);
|
|
42
|
+
const oldUnits = atScale(oldDecimal, scale);
|
|
43
|
+
return divideRounded((atScale(newDecimal, scale) - oldUnits) * 1000n, oldUnits);
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* The change from one value to another in percent, as the Statistisches Bundesamt computes it:
|
|
47
|
+
* ((new / old) × 100) − 100, rounded half away from zero to one decimal (`'13.0'`, `'-2.2'`).
|
|
48
|
+
* It does not depend on the base, but both values must be read on the same one.
|
|
49
|
+
*/
|
|
50
|
+
export const cpiChangePercent = (oldArg, newArg) => formatUnits(changeTenthsOf(oldArg, newArg), 1);
|
|
51
|
+
/**
|
|
52
|
+
* The change from one value to another in index points (new − old), with the digits of the more
|
|
53
|
+
* precise value (`'12.2'`, `'-1.0'`). Points depend on the base, so they are only given within one.
|
|
54
|
+
*/
|
|
55
|
+
export const cpiChangePoints = (oldArg, newArg) => {
|
|
56
|
+
const oldDecimal = publishedValueOf(oldArg);
|
|
57
|
+
const newDecimal = publishedValueOf(newArg);
|
|
58
|
+
requireComparable(oldArg, newArg);
|
|
59
|
+
const scale = Math.max(oldDecimal.scale, newDecimal.scale);
|
|
60
|
+
return formatUnits(atScale(newDecimal, scale) - atScale(oldDecimal, scale), scale);
|
|
61
|
+
};
|
|
62
|
+
/** The units of the thresholds at the reference value's digits. */
|
|
63
|
+
const thresholdUnitsOf = (referenceArg, percentArg) => {
|
|
64
|
+
const reference = publishedValueOf(referenceArg);
|
|
65
|
+
const percent = parseDecimal(percentArg);
|
|
66
|
+
const hundred = percent ? 100n * powerOfTen(percent.scale) : 0n;
|
|
67
|
+
if (!percent || percent.units >= hundred) {
|
|
68
|
+
throw new CpiError('invalid_input', `${JSON.stringify(percentArg)} is no percentage from 0 to below 100, written with digits and a point.`);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
upper: divideRounded(reference.units * (hundred + percent.units), hundred),
|
|
72
|
+
lower: divideRounded(reference.units * (hundred - percent.units), hundred),
|
|
73
|
+
scale: reference.scale,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The thresholds of a percent rule: reference × (1 + percent / 100) and reference × (1 − percent / 100),
|
|
78
|
+
* rounded half away from zero to the reference value's digits (99.7 and 5 % give 104.7 and 94.7).
|
|
79
|
+
*/
|
|
80
|
+
export const cpiThresholds = (referenceArg, percentArg) => {
|
|
81
|
+
const { upper, lower, scale } = thresholdUnitsOf(referenceArg, percentArg);
|
|
82
|
+
return {
|
|
83
|
+
index: referenceArg.index,
|
|
84
|
+
base: referenceArg.base,
|
|
85
|
+
percent: percentArg,
|
|
86
|
+
upper: formatUnits(upper, scale),
|
|
87
|
+
lower: formatUnits(lower, scale),
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
/** The reach options as the clause gives them; anything else is refused. */
|
|
91
|
+
const requireReachOptions = (optionsArg) => {
|
|
92
|
+
if (!['up', 'down', 'either'].includes(optionsArg.direction) || !['reached', 'exceeded'].includes(optionsArg.reach)) {
|
|
93
|
+
throw new CpiError('invalid_input', 'the direction is up, down or either, and the reach is reached or exceeded.');
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* The first value after the reference period at or beyond the upper threshold or at or below the lower
|
|
98
|
+
* one, as the options say. The thresholds are units of 10^-scale; the lower one may be zero or less, which
|
|
99
|
+
* no index value reaches. All values must be published values of the reference value's series and base;
|
|
100
|
+
* their order does not matter, a period given twice is refused.
|
|
101
|
+
*/
|
|
102
|
+
const firstReachingUnits = (valuesArg, referenceArg, thresholdsArg, optionsArg) => {
|
|
103
|
+
const periods = new Set();
|
|
104
|
+
const candidates = [];
|
|
105
|
+
for (const value of valuesArg) {
|
|
106
|
+
const decimal = publishedValueOf(value);
|
|
107
|
+
requireComparable(referenceArg, value);
|
|
108
|
+
if (periods.has(value.period)) {
|
|
109
|
+
throw new CpiError('invalid_input', `${value.period} is given twice.`, { period: value.period });
|
|
110
|
+
}
|
|
111
|
+
periods.add(value.period);
|
|
112
|
+
if (value.period > referenceArg.period) {
|
|
113
|
+
candidates.push({ value, decimal });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
candidates.sort((leftArg, rightArg) => leftArg.value.period.localeCompare(rightArg.value.period));
|
|
117
|
+
const exceeding = optionsArg.reach === 'exceeded';
|
|
118
|
+
for (const { value, decimal } of candidates) {
|
|
119
|
+
const scale = Math.max(decimal.scale, thresholdsArg.scale);
|
|
120
|
+
const units = atScale(decimal, scale);
|
|
121
|
+
const upper = thresholdsArg.upper * powerOfTen(scale - thresholdsArg.scale);
|
|
122
|
+
const lower = thresholdsArg.lower * powerOfTen(scale - thresholdsArg.scale);
|
|
123
|
+
const up = exceeding ? units > upper : units >= upper;
|
|
124
|
+
const down = exceeding ? units < lower : units <= lower;
|
|
125
|
+
if ((optionsArg.direction !== 'down' && up) || (optionsArg.direction !== 'up' && down)) {
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* The first value after the reference period that reaches a threshold of a percent rule
|
|
133
|
+
* (`cpiThresholds`), or `undefined` while none does. Whether the threshold itself counts (`reached`) or
|
|
134
|
+
* only a value beyond it (`exceeded`), and which direction, is the clause's to say. All values must be
|
|
135
|
+
* published values of the reference value's series and base; their order does not matter, a period
|
|
136
|
+
* given twice is refused.
|
|
137
|
+
*/
|
|
138
|
+
export const cpiFirstReaching = (valuesArg, referenceArg, percentArg, optionsArg) => {
|
|
139
|
+
requireReachOptions(optionsArg);
|
|
140
|
+
return firstReachingUnits(valuesArg, referenceArg, thresholdUnitsOf(referenceArg, percentArg), optionsArg);
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* The first value after the reference period whose change from the reference in index points
|
|
144
|
+
* (`cpiChangePoints`) reaches a points rule's threshold, or `undefined` while none does: a value at or
|
|
145
|
+
* above reference + points (`up`), at or below reference − points (`down`), or whichever comes first
|
|
146
|
+
* (`either`). The points are a decimal written with digits and a point, from 0 upward, with any number
|
|
147
|
+
* of digits (`'5'`, `'2.5'`); nothing is rounded, so a threshold with more digits than the values is
|
|
148
|
+
* compared exactly. Whether the threshold itself counts (`reached`) or only a value beyond it
|
|
149
|
+
* (`exceeded`) is the clause's to say. Points depend on the base, so all values must be published
|
|
150
|
+
* values of the reference value's series and base (`base_mismatch` otherwise); their order does not
|
|
151
|
+
* matter, a period given twice is refused.
|
|
152
|
+
*/
|
|
153
|
+
export const cpiFirstReachingPoints = (valuesArg, referenceArg, pointsArg, optionsArg) => {
|
|
154
|
+
requireReachOptions(optionsArg);
|
|
155
|
+
const reference = publishedValueOf(referenceArg);
|
|
156
|
+
const points = parseDecimal(pointsArg);
|
|
157
|
+
if (!points) {
|
|
158
|
+
throw new CpiError('invalid_input', `${JSON.stringify(pointsArg)} is no number of index points from 0 upward, written with digits and a point.`);
|
|
159
|
+
}
|
|
160
|
+
const scale = Math.max(reference.scale, points.scale);
|
|
161
|
+
const referenceUnits = atScale(reference, scale);
|
|
162
|
+
const pointsUnits = atScale(points, scale);
|
|
163
|
+
return firstReachingUnits(valuesArg, referenceArg, { upper: referenceUnits + pointsUnits, lower: referenceUnits - pointsUnits, scale }, optionsArg);
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* An amount in cents scaled from one index value to another, rounded half away from zero to whole cents:
|
|
167
|
+
* by the index ratio (amount × new / old, the default, as the Statistisches Bundesamt writes it) or by
|
|
168
|
+
* the change in percent rounded to one decimal (`basis: 'rounded_percent'`). Which one applies is the
|
|
169
|
+
* contract's to say. The amount and the result are safe integers; a negative amount (a credit) scales
|
|
170
|
+
* the same way.
|
|
171
|
+
*/
|
|
172
|
+
export const cpiScaleAmount = (amountCentsArg, oldArg, newArg, optionsArg = {}) => {
|
|
173
|
+
const basis = optionsArg.basis ?? 'index_ratio';
|
|
174
|
+
if (!Number.isSafeInteger(amountCentsArg)) {
|
|
175
|
+
throw new CpiError('invalid_input', `${amountCentsArg} is no amount in whole cents.`);
|
|
176
|
+
}
|
|
177
|
+
if (basis !== 'index_ratio' && basis !== 'rounded_percent') {
|
|
178
|
+
throw new CpiError('invalid_input', `${JSON.stringify(basis)} is no basis; it is index_ratio or rounded_percent.`);
|
|
179
|
+
}
|
|
180
|
+
const amount = BigInt(amountCentsArg);
|
|
181
|
+
let scaled;
|
|
182
|
+
if (basis === 'rounded_percent') {
|
|
183
|
+
scaled = divideRounded(amount * (1000n + changeTenthsOf(oldArg, newArg)), 1000n);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const oldDecimal = publishedValueOf(oldArg);
|
|
187
|
+
const newDecimal = publishedValueOf(newArg);
|
|
188
|
+
requireComparable(oldArg, newArg);
|
|
189
|
+
const scale = Math.max(oldDecimal.scale, newDecimal.scale);
|
|
190
|
+
scaled = divideRounded(amount * atScale(newDecimal, scale), atScale(oldDecimal, scale));
|
|
191
|
+
}
|
|
192
|
+
const result = Number(scaled);
|
|
193
|
+
if (!Number.isSafeInteger(result)) {
|
|
194
|
+
throw new CpiError('invalid_input', `the scaled amount of ${amountCentsArg} cents is beyond the safe integers.`);
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
};
|
|
198
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXhtYXRoLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvY3BpL2luZGV4bWF0aC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDakQsT0FBTyxFQUFFLE9BQU8sRUFBRSxhQUFhLEVBQUUsV0FBVyxFQUFFLFlBQVksRUFBRSxVQUFVLEVBQXVCLE1BQU0sY0FBYyxDQUFDO0FBR2xILGtHQUFrRztBQUNsRyx1R0FBdUc7QUFDdkcseUdBQXlHO0FBQ3pHLHlGQUF5RjtBQUV6RixNQUFNLEtBQUssR0FBRywwQkFBMEIsQ0FBQztBQUN6QyxNQUFNLElBQUksR0FBRyxVQUFVLENBQUM7QUFFeEIsNEZBQTRGO0FBQzVGLE1BQU0sQ0FBQyxNQUFNLFdBQVcsR0FBRyxDQUFDLFNBQWtCLEVBQTZCLEVBQUU7SUFDM0UsSUFBSSxPQUFPLFNBQVMsS0FBSyxRQUFRLEVBQUUsQ0FBQztRQUNsQyxPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0lBQ0QsT0FBTyxLQUFLLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDO0FBQ3pGLENBQUMsQ0FBQztBQUVGLCtHQUErRztBQUMvRyxNQUFNLGdCQUFnQixHQUFHLENBQUMsUUFBbUIsRUFBa0IsRUFBRTtJQUMvRCxJQUFJLENBQUMsV0FBVyxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1FBQ2xDLE1BQU0sSUFBSSxRQUFRLENBQUMsZUFBZSxFQUFFLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLHdDQUF3QyxDQUFDLENBQUM7SUFDbEgsQ0FBQztJQUNELE1BQU0sT0FBTyxHQUFHLFlBQVksQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDN0MsSUFBSSxDQUFDLE9BQU8sSUFBSSxPQUFPLENBQUMsS0FBSyxJQUFJLEVBQUUsRUFBRSxDQUFDO1FBQ3BDLE1BQU0sSUFBSSxRQUFRLENBQUMsZUFBZSxFQUFFLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLDhEQUE4RCxFQUFFLEVBQUUsTUFBTSxFQUFFLFFBQVEsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO0lBQ3BLLENBQUM7SUFDRCxJQUFJLFFBQVEsQ0FBQyxNQUFNLEtBQUssT0FBTyxFQUFFLENBQUM7UUFDaEMsTUFBTSxJQUFJLFFBQVEsQ0FBQyxlQUFlLEVBQUUsaUJBQWlCLFFBQVEsQ0FBQyxNQUFNLCtDQUErQyxFQUFFLEVBQUUsTUFBTSxFQUFFLFFBQVEsQ0FBQyxNQUFNLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUM7SUFDeEssQ0FBQztJQUNELE9BQU8sT0FBTyxDQUFDO0FBQ2pCLENBQUMsQ0FBQztBQUVGLHdGQUF3RjtBQUN4RixNQUFNLGlCQUFpQixHQUFHLENBQUMsT0FBa0IsRUFBRSxRQUFtQixFQUFRLEVBQUU7SUFDMUUsSUFBSSxPQUFPLENBQUMsS0FBSyxLQUFLLFFBQVEsQ0FBQyxLQUFLLElBQUksT0FBTyxDQUFDLElBQUksS0FBSyxRQUFRLENBQUMsSUFBSSxJQUFJLFdBQVcsQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLEtBQUssV0FBVyxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1FBQ3ZJLE1BQU0sSUFBSSxRQUFRLENBQ2hCLGVBQWUsRUFDZixHQUFHLE9BQU8sQ0FBQyxLQUFLLElBQUksT0FBTyxDQUFDLE1BQU0sS0FBSyxPQUFPLENBQUMsSUFBSSxTQUFTLFFBQVEsQ0FBQyxLQUFLLElBQUksUUFBUSxDQUFDLE1BQU0sS0FBSyxRQUFRLENBQUMsSUFBSSw2RUFBNkUsQ0FDN0wsQ0FBQztJQUNKLENBQUM7QUFDSCxDQUFDLENBQUM7QUFFRix3R0FBd0c7QUFDeEcsTUFBTSxjQUFjLEdBQUcsQ0FBQyxNQUFpQixFQUFFLE1BQWlCLEVBQVUsRUFBRTtJQUN0RSxNQUFNLFVBQVUsR0FBRyxnQkFBZ0IsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUM1QyxNQUFNLFVBQVUsR0FBRyxnQkFBZ0IsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUM1QyxpQkFBaUIsQ0FBQyxNQUFNLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDbEMsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxVQUFVLENBQUMsS0FBSyxFQUFFLFVBQVUsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUMzRCxNQUFNLFFBQVEsR0FBRyxPQUFPLENBQUMsVUFBVSxFQUFFLEtBQUssQ0FBQyxDQUFDO0lBQzVDLE9BQU8sYUFBYSxDQUFDLENBQUMsT0FBTyxDQUFDLFVBQVUsRUFBRSxLQUFLLENBQUMsR0FBRyxRQUFRLENBQUMsR0FBRyxLQUFLLEVBQUUsUUFBUSxDQUFDLENBQUM7QUFDbEYsQ0FBQyxDQUFDO0FBRUY7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxNQUFNLGdCQUFnQixHQUFHLENBQUMsTUFBaUIsRUFBRSxNQUFpQixFQUFVLEVBQUUsQ0FBQyxXQUFXLENBQUMsY0FBYyxDQUFDLE1BQU0sRUFBRSxNQUFNLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQztBQUVqSTs7O0dBR0c7QUFDSCxNQUFNLENBQUMsTUFBTSxlQUFlLEdBQUcsQ0FBQyxNQUFpQixFQUFFLE1BQWlCLEVBQVUsRUFBRTtJQUM5RSxNQUFNLFVBQVUsR0FBRyxnQkFBZ0IsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUM1QyxNQUFNLFVBQVUsR0FBRyxnQkFBZ0IsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUM1QyxpQkFBaUIsQ0FBQyxNQUFNLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDbEMsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxVQUFVLENBQUMsS0FBSyxFQUFFLFVBQVUsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUMzRCxPQUFPLFdBQVcsQ0FBQyxPQUFPLENBQUMsVUFBVSxFQUFFLEtBQUssQ0FBQyxHQUFHLE9BQU8sQ0FBQyxVQUFVLEVBQUUsS0FBSyxDQUFDLEVBQUUsS0FBSyxDQUFDLENBQUM7QUFDckYsQ0FBQyxDQUFDO0FBRUYsbUVBQW1FO0FBQ25FLE1BQU0sZ0JBQWdCLEdBQUcsQ0FBQyxZQUF1QixFQUFFLFVBQWtCLEVBQW1ELEVBQUU7SUFDeEgsTUFBTSxTQUFTLEdBQUcsZ0JBQWdCLENBQUMsWUFBWSxDQUFDLENBQUM7SUFDakQsTUFBTSxPQUFPLEdBQUcsWUFBWSxDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQ3pDLE1BQU0sT0FBTyxHQUFHLE9BQU8sQ0FBQyxDQUFDLENBQUMsSUFBSSxHQUFHLFVBQVUsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUNoRSxJQUFJLENBQUMsT0FBTyxJQUFJLE9BQU8sQ0FBQyxLQUFLLElBQUksT0FBTyxFQUFFLENBQUM7UUFDekMsTUFBTSxJQUFJLFFBQVEsQ0FBQyxlQUFlLEVBQUUsR0FBRyxJQUFJLENBQUMsU0FBUyxDQUFDLFVBQVUsQ0FBQyx5RUFBeUUsQ0FBQyxDQUFDO0lBQzlJLENBQUM7SUFDRCxPQUFPO1FBQ0wsS0FBSyxFQUFFLGFBQWEsQ0FBQyxTQUFTLENBQUMsS0FBSyxHQUFHLENBQUMsT0FBTyxHQUFHLE9BQU8sQ0FBQyxLQUFLLENBQUMsRUFBRSxPQUFPLENBQUM7UUFDMUUsS0FBSyxFQUFFLGFBQWEsQ0FBQyxTQUFTLENBQUMsS0FBSyxHQUFHLENBQUMsT0FBTyxHQUFHLE9BQU8sQ0FBQyxLQUFLLENBQUMsRUFBRSxPQUFPLENBQUM7UUFDMUUsS0FBSyxFQUFFLFNBQVMsQ0FBQyxLQUFLO0tBQ3ZCLENBQUM7QUFDSixDQUFDLENBQUM7QUFFRjs7O0dBR0c7QUFDSCxNQUFNLENBQUMsTUFBTSxhQUFhLEdBQUcsQ0FBQyxZQUF1QixFQUFFLFVBQWtCLEVBQWtCLEVBQUU7SUFDM0YsTUFBTSxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLEdBQUcsZ0JBQWdCLENBQUMsWUFBWSxFQUFFLFVBQVUsQ0FBQyxDQUFDO0lBQzNFLE9BQU87UUFDTCxLQUFLLEVBQUUsWUFBWSxDQUFDLEtBQUs7UUFDekIsSUFBSSxFQUFFLFlBQVksQ0FBQyxJQUFJO1FBQ3ZCLE9BQU8sRUFBRSxVQUFVO1FBQ25CLEtBQUssRUFBRSxXQUFXLENBQUMsS0FBSyxFQUFFLEtBQUssQ0FBQztRQUNoQyxLQUFLLEVBQUUsV0FBVyxDQUFDLEtBQUssRUFBRSxLQUFLLENBQUM7S0FDakMsQ0FBQztBQUNKLENBQUMsQ0FBQztBQUVGLDRFQUE0RTtBQUM1RSxNQUFNLG1CQUFtQixHQUFHLENBQUMsVUFBNEIsRUFBUSxFQUFFO0lBQ2pFLElBQUksQ0FBQyxDQUFDLElBQUksRUFBRSxNQUFNLEVBQUUsUUFBUSxDQUFDLENBQUMsUUFBUSxDQUFDLFVBQVUsQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLENBQUMsU0FBUyxFQUFFLFVBQVUsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUNwSCxNQUFNLElBQUksUUFBUSxDQUFDLGVBQWUsRUFBRSw0RUFBNEUsQ0FBQyxDQUFDO0lBQ3BILENBQUM7QUFDSCxDQUFDLENBQUM7QUFFRjs7Ozs7R0FLRztBQUNILE1BQU0sa0JBQWtCLEdBQUcsQ0FDekIsU0FBK0IsRUFDL0IsWUFBdUIsRUFDdkIsYUFBOEQsRUFDOUQsVUFBNEIsRUFDTCxFQUFFO0lBQ3pCLE1BQU0sT0FBTyxHQUFHLElBQUksR0FBRyxFQUFVLENBQUM7SUFDbEMsTUFBTSxVQUFVLEdBQW9ELEVBQUUsQ0FBQztJQUN2RSxLQUFLLE1BQU0sS0FBSyxJQUFJLFNBQVMsRUFBRSxDQUFDO1FBQzlCLE1BQU0sT0FBTyxHQUFHLGdCQUFnQixDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ3hDLGlCQUFpQixDQUFDLFlBQVksRUFBRSxLQUFLLENBQUMsQ0FBQztRQUN2QyxJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUM7WUFDOUIsTUFBTSxJQUFJLFFBQVEsQ0FBQyxlQUFlLEVBQUUsR0FBRyxLQUFLLENBQUMsTUFBTSxrQkFBa0IsRUFBRSxFQUFFLE1BQU0sRUFBRSxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQztRQUNuRyxDQUFDO1FBQ0QsT0FBTyxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDMUIsSUFBSSxLQUFLLENBQUMsTUFBTSxHQUFHLFlBQVksQ0FBQyxNQUFNLEVBQUUsQ0FBQztZQUN2QyxVQUFVLENBQUMsSUFBSSxDQUFDLEVBQUUsS0FBSyxFQUFFLE9BQU8sRUFBRSxDQUFDLENBQUM7UUFDdEMsQ0FBQztJQUNILENBQUM7SUFDRCxVQUFVLENBQUMsSUFBSSxDQUFDLENBQUMsT0FBTyxFQUFFLFFBQVEsRUFBRSxFQUFFLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQztJQUNsRyxNQUFNLFNBQVMsR0FBRyxVQUFVLENBQUMsS0FBSyxLQUFLLFVBQVUsQ0FBQztJQUNsRCxLQUFLLE1BQU0sRUFBRSxLQUFLLEVBQUUsT0FBTyxFQUFFLElBQUksVUFBVSxFQUFFLENBQUM7UUFDNUMsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLGFBQWEsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUMzRCxNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsT0FBTyxFQUFFLEtBQUssQ0FBQyxDQUFDO1FBQ3RDLE1BQU0sS0FBSyxHQUFHLGFBQWEsQ0FBQyxLQUFLLEdBQUcsVUFBVSxDQUFDLEtBQUssR0FBRyxhQUFhLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDNUUsTUFBTSxLQUFLLEdBQUcsYUFBYSxDQUFDLEtBQUssR0FBRyxVQUFVLENBQUMsS0FBSyxHQUFHLGFBQWEsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUM1RSxNQUFNLEVBQUUsR0FBRyxTQUFTLENBQUMsQ0FBQyxDQUFDLEtBQUssR0FBRyxLQUFLLENBQUMsQ0FBQyxDQUFDLEtBQUssSUFBSSxLQUFLLENBQUM7UUFDdEQsTUFBTSxJQUFJLEdBQUcsU0FBUyxDQUFDLENBQUMsQ0FBQyxLQUFLLEdBQUcsS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLElBQUksS0FBSyxDQUFDO1FBQ3hELElBQUksQ0FBQyxVQUFVLENBQUMsU0FBUyxLQUFLLE1BQU0sSUFBSSxFQUFFLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxTQUFTLEtBQUssSUFBSSxJQUFJLElBQUksQ0FBQyxFQUFFLENBQUM7WUFDdkYsT0FBTyxLQUFLLENBQUM7UUFDZixDQUFDO0lBQ0gsQ0FBQztJQUNELE9BQU8sU0FBUyxDQUFDO0FBQ25CLENBQUMsQ0FBQztBQUVGOzs7Ozs7R0FNRztBQUNILE1BQU0sQ0FBQyxNQUFNLGdCQUFnQixHQUFHLENBQzlCLFNBQStCLEVBQy9CLFlBQXVCLEVBQ3ZCLFVBQWtCLEVBQ2xCLFVBQTRCLEVBQ0wsRUFBRTtJQUN6QixtQkFBbUIsQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUNoQyxPQUFPLGtCQUFrQixDQUFDLFNBQVMsRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLENBQUMsWUFBWSxFQUFFLFVBQVUsQ0FBQyxFQUFFLFVBQVUsQ0FBQyxDQUFDO0FBQzdHLENBQUMsQ0FBQztBQUVGOzs7Ozs7Ozs7O0dBVUc7QUFDSCxNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBRyxDQUNwQyxTQUErQixFQUMvQixZQUF1QixFQUN2QixTQUFpQixFQUNqQixVQUE0QixFQUNMLEVBQUU7SUFDekIsbUJBQW1CLENBQUMsVUFBVSxDQUFDLENBQUM7SUFDaEMsTUFBTSxTQUFTLEdBQUcsZ0JBQWdCLENBQUMsWUFBWSxDQUFDLENBQUM7SUFDakQsTUFBTSxNQUFNLEdBQUcsWUFBWSxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQ3ZDLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUNaLE1BQU0sSUFBSSxRQUFRLENBQUMsZUFBZSxFQUFFLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQyxTQUFTLENBQUMsK0VBQStFLENBQUMsQ0FBQztJQUNuSixDQUFDO0lBQ0QsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxTQUFTLENBQUMsS0FBSyxFQUFFLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUN0RCxNQUFNLGNBQWMsR0FBRyxPQUFPLENBQUMsU0FBUyxFQUFFLEtBQUssQ0FBQyxDQUFDO0lBQ2pELE1BQU0sV0FBVyxHQUFHLE9BQU8sQ0FBQyxNQUFNLEVBQUUsS0FBSyxDQUFDLENBQUM7SUFDM0MsT0FBTyxrQkFBa0IsQ0FBQyxTQUFTLEVBQUUsWUFBWSxFQUFFLEVBQUUsS0FBSyxFQUFFLGNBQWMsR0FBRyxXQUFXLEVBQUUsS0FBSyxFQUFFLGNBQWMsR0FBRyxXQUFXLEVBQUUsS0FBSyxFQUFFLEVBQUUsVUFBVSxDQUFDLENBQUM7QUFDdEosQ0FBQyxDQUFDO0FBRUY7Ozs7OztHQU1HO0FBQ0gsTUFBTSxDQUFDLE1BQU0sY0FBYyxHQUFHLENBQUMsY0FBc0IsRUFBRSxNQUFpQixFQUFFLE1BQWlCLEVBQUUsYUFBK0IsRUFBRSxFQUFVLEVBQUU7SUFDeEksTUFBTSxLQUFLLEdBQUcsVUFBVSxDQUFDLEtBQUssSUFBSSxhQUFhLENBQUM7SUFDaEQsSUFBSSxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsY0FBYyxDQUFDLEVBQUUsQ0FBQztRQUMxQyxNQUFNLElBQUksUUFBUSxDQUFDLGVBQWUsRUFBRSxHQUFHLGNBQWMsK0JBQStCLENBQUMsQ0FBQztJQUN4RixDQUFDO0lBQ0QsSUFBSSxLQUFLLEtBQUssYUFBYSxJQUFJLEtBQUssS0FBSyxpQkFBaUIsRUFBRSxDQUFDO1FBQzNELE1BQU0sSUFBSSxRQUFRLENBQUMsZUFBZSxFQUFFLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMscURBQXFELENBQUMsQ0FBQztJQUNySCxDQUFDO0lBQ0QsTUFBTSxNQUFNLEdBQUcsTUFBTSxDQUFDLGNBQWMsQ0FBQyxDQUFDO0lBQ3RDLElBQUksTUFBYyxDQUFDO0lBQ25CLElBQUksS0FBSyxLQUFLLGlCQUFpQixFQUFFLENBQUM7UUFDaEMsTUFBTSxHQUFHLGFBQWEsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxLQUFLLEdBQUcsY0FBYyxDQUFDLE1BQU0sRUFBRSxNQUFNLENBQUMsQ0FBQyxFQUFFLEtBQUssQ0FBQyxDQUFDO0lBQ25GLENBQUM7U0FBTSxDQUFDO1FBQ04sTUFBTSxVQUFVLEdBQUcsZ0JBQWdCLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDNUMsTUFBTSxVQUFVLEdBQUcsZ0JBQWdCLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDNUMsaUJBQWlCLENBQUMsTUFBTSxFQUFFLE1BQU0sQ0FBQyxDQUFDO1FBQ2xDLE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDLEtBQUssRUFBRSxVQUFVLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDM0QsTUFBTSxHQUFHLGFBQWEsQ0FBQyxNQUFNLEdBQUcsT0FBTyxDQUFDLFVBQVUsRUFBRSxLQUFLLENBQUMsRUFBRSxPQUFPLENBQUMsVUFBVSxFQUFFLEtBQUssQ0FBQyxDQUFDLENBQUM7SUFDMUYsQ0FBQztJQUNELE1BQU0sTUFBTSxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUM5QixJQUFJLENBQUMsTUFBTSxDQUFDLGFBQWEsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1FBQ2xDLE1BQU0sSUFBSSxRQUFRLENBQUMsZUFBZSxFQUFFLHdCQUF3QixjQUFjLHFDQUFxQyxDQUFDLENBQUM7SUFDbkgsQ0FBQztJQUNELE9BQU8sTUFBTSxDQUFDO0FBQ2hCLENBQUMsQ0FBQyJ9
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** A calendar month, `YYYY-MM`. */
|
|
2
|
+
export type TIsoMonth = string;
|
|
3
|
+
/** A calendar year, `YYYY`. */
|
|
4
|
+
export type TIsoYear = string;
|
|
5
|
+
/** The period of an index value: a month (`YYYY-MM`) or a year (`YYYY`, the annual average). */
|
|
6
|
+
export type TCpiPeriod = TIsoMonth | TIsoYear;
|
|
7
|
+
/** Which price index a value belongs to: `de-vpi`, the Verbraucherpreisindex für Deutschland of the Statistisches Bundesamt. */
|
|
8
|
+
export type TCpiIndex = 'de-vpi';
|
|
9
|
+
export type TCpiFrequency = 'monthly' | 'annual';
|
|
10
|
+
/**
|
|
11
|
+
* - `final`: the value as published, final until the next change of the base year;
|
|
12
|
+
* - `estimated`: a value the source marks as an estimate (the Bundesbank's `OBS_STATUS` `E`, for the
|
|
13
|
+
* latest month at times). It is no published index value and is never used in a calculation.
|
|
14
|
+
*/
|
|
15
|
+
export type TCpiValueStatus = 'final' | 'estimated';
|
|
16
|
+
/** One index value as its source states it. */
|
|
17
|
+
export interface ICpiValue {
|
|
18
|
+
index: TCpiIndex;
|
|
19
|
+
period: TCpiPeriod;
|
|
20
|
+
/**
|
|
21
|
+
* The value as a decimal with a point and exactly the digits published, trailing zeros included
|
|
22
|
+
* (`'100.0'`, `'113.5'`). Never a float.
|
|
23
|
+
*/
|
|
24
|
+
value: string;
|
|
25
|
+
/** the base the value is stated on, as the source writes it: `'2020=100'` */
|
|
26
|
+
base: string;
|
|
27
|
+
status: TCpiValueStatus;
|
|
28
|
+
}
|
|
29
|
+
/** A source's answer as it arrived: its HTTP status and its body as text. */
|
|
30
|
+
export interface ICpiServiceAnswer {
|
|
31
|
+
httpStatus: number;
|
|
32
|
+
text: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Where a series comes from and how it must be credited. The figures are those of the Statistisches
|
|
36
|
+
* Bundesamt (Destatis) under the Datenlizenz Deutschland – Namensnennung – Version 2.0.
|
|
37
|
+
*/
|
|
38
|
+
export interface ICpiSourceNote {
|
|
39
|
+
source: 'bundesbank';
|
|
40
|
+
/** who delivered the figures: `'Deutsche Bundesbank'` */
|
|
41
|
+
provider: string;
|
|
42
|
+
/** whose figures they are: `'Statistisches Bundesamt (Destatis)'` */
|
|
43
|
+
dataOrigin: string;
|
|
44
|
+
/** the series' title as the answer gives it */
|
|
45
|
+
title: string;
|
|
46
|
+
/** the series key as the answer gives it, its base included: `'BBDP1.M.DE.N.VPI.C.A00000.I20.A'` */
|
|
47
|
+
key: string;
|
|
48
|
+
/** the request the figures were read from */
|
|
49
|
+
uri: string;
|
|
50
|
+
licence: string;
|
|
51
|
+
licenceUri: string;
|
|
52
|
+
/** the source note to show wherever the figures are shown */
|
|
53
|
+
attribution: string;
|
|
54
|
+
}
|
|
55
|
+
/** A range of index values of one series and one base, as one answer gave them. */
|
|
56
|
+
export interface ICpiSeries {
|
|
57
|
+
index: TCpiIndex;
|
|
58
|
+
frequency: TCpiFrequency;
|
|
59
|
+
/** `'2020=100'` */
|
|
60
|
+
base: string;
|
|
61
|
+
/** `2020` */
|
|
62
|
+
baseYear: number;
|
|
63
|
+
/** the published values, oldest first */
|
|
64
|
+
values: ICpiValue[];
|
|
65
|
+
/** the values the source marks as estimates, oldest first; kept apart so that no calculation takes one */
|
|
66
|
+
estimates: ICpiValue[];
|
|
67
|
+
source: ICpiSourceNote;
|
|
68
|
+
answer: ICpiServiceAnswer;
|
|
69
|
+
}
|
|
70
|
+
/** One published value with where it comes from and the answer it was read from. */
|
|
71
|
+
export interface ICpiReading extends ICpiValue {
|
|
72
|
+
source: ICpiSourceNote;
|
|
73
|
+
answer: ICpiServiceAnswer;
|
|
74
|
+
}
|
|
75
|
+
/** The thresholds of a percent rule: the reference value times (1 ± percent / 100), rounded to its digits. */
|
|
76
|
+
export interface ICpiThresholds {
|
|
77
|
+
index: TCpiIndex;
|
|
78
|
+
base: string;
|
|
79
|
+
/** the percentage asked for, as given */
|
|
80
|
+
percent: string;
|
|
81
|
+
upper: string;
|
|
82
|
+
lower: string;
|
|
83
|
+
}
|
|
84
|
+
export interface ICpiReachOptions {
|
|
85
|
+
/** `up`: the upper threshold, `down`: the lower one, `either`: whichever comes first */
|
|
86
|
+
direction: 'up' | 'down' | 'either';
|
|
87
|
+
/** `reached`: the threshold itself counts; `exceeded`: only a value beyond it counts */
|
|
88
|
+
reach: 'reached' | 'exceeded';
|
|
89
|
+
}
|
|
90
|
+
export interface ICpiScaleOptions {
|
|
91
|
+
/**
|
|
92
|
+
* - `index_ratio` (default): amount × new index / old index, as the Statistisches Bundesamt writes it;
|
|
93
|
+
* - `rounded_percent`: amount × (100 + the change in percent rounded to one decimal) / 100.
|
|
94
|
+
*/
|
|
95
|
+
basis?: 'index_ratio' | 'rounded_percent';
|
|
96
|
+
}
|
|
97
|
+
/** A `fetch` as the WHATWG standard defines it; injectable for tests and for a fetch that reaches only public addresses. */
|
|
98
|
+
export type TCpiFetch = (input: string, init: RequestInit) => Promise<Response>;
|
|
99
|
+
export interface ICpiClientOptions {
|
|
100
|
+
/** default: the runtime's `fetch` */
|
|
101
|
+
fetch?: TCpiFetch;
|
|
102
|
+
/** default: the official address (`bundesbankCpiBaseUrl`); a test's own server otherwise */
|
|
103
|
+
baseUrl?: string;
|
|
104
|
+
/** how long one request may take, the answer included; default `defaultCpiTimeoutMs` */
|
|
105
|
+
timeoutMs?: number;
|
|
106
|
+
/** sent as `User-Agent` where given */
|
|
107
|
+
userAgent?: string;
|
|
108
|
+
}
|
|
109
|
+
export interface ICpiMonthlyInput {
|
|
110
|
+
from: TIsoMonth;
|
|
111
|
+
/** default: whatever the source has published after `from` */
|
|
112
|
+
to?: TIsoMonth;
|
|
113
|
+
signal?: AbortSignal;
|
|
114
|
+
}
|
|
115
|
+
export interface ICpiAnnualInput {
|
|
116
|
+
from: TIsoYear;
|
|
117
|
+
/** default: whatever the source has published after `from` */
|
|
118
|
+
to?: TIsoYear;
|
|
119
|
+
signal?: AbortSignal;
|
|
120
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ICpiClientOptions, ICpiServiceAnswer } from './interfaces.js';
|
|
2
|
+
/** The time a client gives one request: its option, or the default; anything else than a positive whole number of milliseconds is refused. */
|
|
3
|
+
export declare const timeoutOf: (optionsArg: ICpiClientOptions) => number;
|
|
4
|
+
/**
|
|
5
|
+
* Sends one `GET` for CSV and reads the answer whole, within the client's time. The caller's signal
|
|
6
|
+
* ends it as `aborted`, the time as `timeout`, and a request that could not be sent or answered as
|
|
7
|
+
* `unreachable`; nothing is retried. The timer ends with the request, answered or not.
|
|
8
|
+
*/
|
|
9
|
+
export declare const getText: (optionsArg: ICpiClientOptions, urlArg: string, signalArg: AbortSignal | undefined) => Promise<ICpiServiceAnswer>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CpiError } from './classes.cpierror.js';
|
|
2
|
+
import { defaultCpiTimeoutMs } from './constants.js';
|
|
3
|
+
/** The time a client gives one request: its option, or the default; anything else than a positive whole number of milliseconds is refused. */
|
|
4
|
+
export const timeoutOf = (optionsArg) => {
|
|
5
|
+
const timeout = optionsArg.timeoutMs ?? defaultCpiTimeoutMs;
|
|
6
|
+
if (!Number.isSafeInteger(timeout) || timeout <= 0) {
|
|
7
|
+
throw new CpiError('invalid_input', `the time limit is a positive whole number of milliseconds, got ${String(optionsArg.timeoutMs)}`);
|
|
8
|
+
}
|
|
9
|
+
return timeout;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Sends one `GET` for CSV and reads the answer whole, within the client's time. The caller's signal
|
|
13
|
+
* ends it as `aborted`, the time as `timeout`, and a request that could not be sent or answered as
|
|
14
|
+
* `unreachable`; nothing is retried. The timer ends with the request, answered or not.
|
|
15
|
+
*/
|
|
16
|
+
export const getText = async (optionsArg, urlArg, signalArg) => {
|
|
17
|
+
const fetchFn = optionsArg.fetch ?? ((inputArg, initArg) => fetch(inputArg, initArg));
|
|
18
|
+
if (signalArg?.aborted) {
|
|
19
|
+
throw new CpiError('aborted', 'the request was ended by its caller', { service: 'bundesbank' }, { cause: signalArg.reason });
|
|
20
|
+
}
|
|
21
|
+
const timeout = new AbortController();
|
|
22
|
+
const timer = setTimeout(() => timeout.abort(new Error('the client\'s time ran out')), timeoutOf(optionsArg));
|
|
23
|
+
const signal = signalArg ? AbortSignal.any([signalArg, timeout.signal]) : timeout.signal;
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetchFn(urlArg, {
|
|
26
|
+
method: 'GET',
|
|
27
|
+
headers: { accept: 'text/csv', ...(optionsArg.userAgent ? { 'user-agent': optionsArg.userAgent } : {}) },
|
|
28
|
+
signal,
|
|
29
|
+
});
|
|
30
|
+
return { httpStatus: response.status, text: await response.text() };
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (signalArg?.aborted) {
|
|
34
|
+
throw new CpiError('aborted', 'the request was ended by its caller', { service: 'bundesbank' }, { cause: error });
|
|
35
|
+
}
|
|
36
|
+
if (timeout.signal.aborted) {
|
|
37
|
+
throw new CpiError('timeout', 'the source did not answer in time', { service: 'bundesbank' }, { cause: error });
|
|
38
|
+
}
|
|
39
|
+
throw new CpiError('unreachable', 'the source could not be reached', { service: 'bundesbank' }, { cause: error });
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVxdWVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2NwaS9yZXF1ZXN0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUNqRCxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUdyRCw4SUFBOEk7QUFDOUksTUFBTSxDQUFDLE1BQU0sU0FBUyxHQUFHLENBQUMsVUFBNkIsRUFBVSxFQUFFO0lBQ2pFLE1BQU0sT0FBTyxHQUFHLFVBQVUsQ0FBQyxTQUFTLElBQUksbUJBQW1CLENBQUM7SUFDNUQsSUFBSSxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsT0FBTyxDQUFDLElBQUksT0FBTyxJQUFJLENBQUMsRUFBRSxDQUFDO1FBQ25ELE1BQU0sSUFBSSxRQUFRLENBQUMsZUFBZSxFQUFFLGtFQUFrRSxNQUFNLENBQUMsVUFBVSxDQUFDLFNBQVMsQ0FBQyxFQUFFLENBQUMsQ0FBQztJQUN4SSxDQUFDO0lBQ0QsT0FBTyxPQUFPLENBQUM7QUFDakIsQ0FBQyxDQUFDO0FBRUY7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxNQUFNLE9BQU8sR0FBRyxLQUFLLEVBQzFCLFVBQTZCLEVBQzdCLE1BQWMsRUFDZCxTQUFrQyxFQUNOLEVBQUU7SUFDOUIsTUFBTSxPQUFPLEdBQWMsVUFBVSxDQUFDLEtBQUssSUFBSSxDQUFDLENBQUMsUUFBUSxFQUFFLE9BQU8sRUFBRSxFQUFFLENBQUMsS0FBSyxDQUFDLFFBQVEsRUFBRSxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQ2pHLElBQUksU0FBUyxFQUFFLE9BQU8sRUFBRSxDQUFDO1FBQ3ZCLE1BQU0sSUFBSSxRQUFRLENBQUMsU0FBUyxFQUFFLHFDQUFxQyxFQUFFLEVBQUUsT0FBTyxFQUFFLFlBQVksRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLFNBQVMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO0lBQy9ILENBQUM7SUFDRCxNQUFNLE9BQU8sR0FBRyxJQUFJLGVBQWUsRUFBRSxDQUFDO0lBQ3RDLE1BQU0sS0FBSyxHQUFHLFVBQVUsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLElBQUksS0FBSyxDQUFDLDRCQUE0QixDQUFDLENBQUMsRUFBRSxTQUFTLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQztJQUM5RyxNQUFNLE1BQU0sR0FBRyxTQUFTLENBQUMsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxTQUFTLEVBQUUsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUM7SUFDekYsSUFBSSxDQUFDO1FBQ0gsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsTUFBTSxFQUFFO1lBQ3JDLE1BQU0sRUFBRSxLQUFLO1lBQ2IsT0FBTyxFQUFFLEVBQUUsTUFBTSxFQUFFLFVBQVUsRUFBRSxHQUFHLENBQUMsVUFBVSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsRUFBRSxZQUFZLEVBQUUsVUFBVSxDQUFDLFNBQVMsRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsRUFBRTtZQUN4RyxNQUFNO1NBQ1AsQ0FBQyxDQUFDO1FBQ0gsT0FBTyxFQUFFLFVBQVUsRUFBRSxRQUFRLENBQUMsTUFBTSxFQUFFLElBQUksRUFBRSxNQUFNLFFBQVEsQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDO0lBQ3RFLENBQUM7SUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1FBQ2YsSUFBSSxTQUFTLEVBQUUsT0FBTyxFQUFFLENBQUM7WUFDdkIsTUFBTSxJQUFJLFFBQVEsQ0FBQyxTQUFTLEVBQUUscUNBQXFDLEVBQUUsRUFBRSxPQUFPLEVBQUUsWUFBWSxFQUFFLEVBQUUsRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztRQUNwSCxDQUFDO1FBQ0QsSUFBSSxPQUFPLENBQUMsTUFBTSxDQUFDLE9BQU8sRUFBRSxDQUFDO1lBQzNCLE1BQU0sSUFBSSxRQUFRLENBQUMsU0FBUyxFQUFFLG1DQUFtQyxFQUFFLEVBQUUsT0FBTyxFQUFFLFlBQVksRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7UUFDbEgsQ0FBQztRQUNELE1BQU0sSUFBSSxRQUFRLENBQUMsYUFBYSxFQUFFLGlDQUFpQyxFQUFFLEVBQUUsT0FBTyxFQUFFLFlBQVksRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7SUFDcEgsQ0FBQztZQUFTLENBQUM7UUFDVCxZQUFZLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDdEIsQ0FBQztBQUNILENBQUMsQ0FBQyJ9
|
package/dist_ts/plugins.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
export { path, };
|
|
3
|
-
import * as qenv from '@push.rocks/qenv';
|
|
4
3
|
import * as smartpath from '@push.rocks/smartpath';
|
|
5
4
|
import * as smartpromise from '@push.rocks/smartpromise';
|
|
6
5
|
import * as smartrequest from '@push.rocks/smartrequest';
|
|
7
6
|
declare const getErrorMessage: (error: unknown) => string;
|
|
8
|
-
export { getErrorMessage,
|
|
7
|
+
export { getErrorMessage, smartpath, smartpromise, smartrequest, };
|
package/dist_ts/plugins.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
export { path, };
|
|
4
4
|
// @push.rocks scope
|
|
5
|
-
import * as qenv from '@push.rocks/qenv';
|
|
6
5
|
import * as smartpath from '@push.rocks/smartpath';
|
|
7
6
|
import * as smartpromise from '@push.rocks/smartpromise';
|
|
8
7
|
import * as smartrequest from '@push.rocks/smartrequest';
|
|
@@ -12,5 +11,5 @@ const getErrorMessage = (error) => {
|
|
|
12
11
|
}
|
|
13
12
|
return String(error);
|
|
14
13
|
};
|
|
15
|
-
export { getErrorMessage,
|
|
16
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
14
|
+
export { getErrorMessage, smartpath, smartpromise, smartrequest, };
|
|
15
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsb0JBQW9CO0FBQ3BCLE9BQU8sS0FBSyxJQUFJLE1BQU0sV0FBVyxDQUFDO0FBRWxDLE9BQU8sRUFDTCxJQUFJLEdBQ0wsQ0FBQTtBQUVELG9CQUFvQjtBQUNwQixPQUFPLEtBQUssU0FBUyxNQUFNLHVCQUF1QixDQUFDO0FBQ25ELE9BQU8sS0FBSyxZQUFZLE1BQU0sMEJBQTBCLENBQUM7QUFDekQsT0FBTyxLQUFLLFlBQVksTUFBTSwwQkFBMEIsQ0FBQztBQUV6RCxNQUFNLGVBQWUsR0FBRyxDQUFDLEtBQWMsRUFBRSxFQUFFO0lBQ3pDLElBQUksS0FBSyxZQUFZLEtBQUssRUFBRSxDQUFDO1FBQzNCLE9BQU8sS0FBSyxDQUFDLE9BQU8sQ0FBQztJQUN2QixDQUFDO0lBRUQsT0FBTyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7QUFDdkIsQ0FBQyxDQUFDO0FBRUYsT0FBTyxFQUNMLGVBQWUsRUFDZixTQUFTLEVBQ1QsWUFBWSxFQUNaLFlBQVksR0FDYixDQUFBIn0=
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fin.cx/opendata",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"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, the base rate of § 247 BGB, and stock and crypto prices and fundamentals from several providers.",
|
|
5
|
+
"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, the base rate of § 247 BGB, the German consumer price index with the arithmetic of index clauses, and stock and crypto prices and fundamentals from several providers.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
7
7
|
"typings": "dist_ts/index.d.ts",
|
|
8
8
|
"exports": {
|
|
@@ -26,19 +26,23 @@
|
|
|
26
26
|
"types": "./dist_ts/vatid/index.d.ts",
|
|
27
27
|
"import": "./dist_ts/vatid/index.js"
|
|
28
28
|
},
|
|
29
|
+
"./cpi": {
|
|
30
|
+
"types": "./dist_ts/cpi/index.d.ts",
|
|
31
|
+
"import": "./dist_ts/cpi/index.js"
|
|
32
|
+
},
|
|
29
33
|
"./package.json": "./package.json"
|
|
30
34
|
},
|
|
31
35
|
"type": "module",
|
|
32
36
|
"author": "Task Venture Capital GmbH",
|
|
33
37
|
"license": "MIT",
|
|
34
38
|
"devDependencies": {
|
|
35
|
-
"@git.zone/tsbuild": "^5.
|
|
39
|
+
"@git.zone/tsbuild": "^5.1.2",
|
|
36
40
|
"@git.zone/tsrun": "^3.0.2",
|
|
37
|
-
"@git.zone/tstest": "^
|
|
38
|
-
"@
|
|
41
|
+
"@git.zone/tstest": "^7.1.0",
|
|
42
|
+
"@push.rocks/qenv": "^6.1.3",
|
|
43
|
+
"@types/node": "^26.6.3"
|
|
39
44
|
},
|
|
40
45
|
"dependencies": {
|
|
41
|
-
"@push.rocks/qenv": "^6.1.3",
|
|
42
46
|
"@push.rocks/smartpath": "^6.0.0",
|
|
43
47
|
"@push.rocks/smartpromise": "^4.3.0",
|
|
44
48
|
"@push.rocks/smartrequest": "^5.0.1",
|
|
@@ -86,7 +90,10 @@
|
|
|
86
90
|
"Feiertage",
|
|
87
91
|
"working days",
|
|
88
92
|
"Basiszinssatz",
|
|
89
|
-
"base rate"
|
|
93
|
+
"base rate",
|
|
94
|
+
"consumer price index",
|
|
95
|
+
"Verbraucherpreisindex",
|
|
96
|
+
"index clause"
|
|
90
97
|
],
|
|
91
98
|
"scripts": {
|
|
92
99
|
"test": "(tstest test/ --verbose)",
|
package/readme.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @fin.cx/opendata
|
|
2
2
|
|
|
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, the statutory public holidays of the German federal states,
|
|
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, the statutory public holidays of the German federal states, the base rate of § 247 BGB, and the German consumer price index for index clauses. 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 gives you
|
|
5
|
+
It gives you seven 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
|
|
@@ -10,6 +10,7 @@ It gives you six practical lanes in one package:
|
|
|
10
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
|
- the base rate, from `@fin.cx/opendata/baserate`, for the Basiszinssatz of § 247 BGB per half-year as the Deutsche Bundesbank publishes it, for default interest under § 288 BGB
|
|
12
12
|
- VAT ID confirmation, from `@fin.cx/opendata/vatid`, over the BZSt's eVatR REST API (the confirmation of § 18e UStG, simple or qualified) and the European Commission's VIES REST API, without any storage
|
|
13
|
+
- the consumer price index, from `@fin.cx/opendata/cpi`, for the Verbraucherpreisindex für Deutschland of the Statistisches Bundesamt as the Deutsche Bundesbank republishes it, with the arithmetic of index clauses (Wertsicherungsklauseln, § 557b BGB), without any storage
|
|
13
14
|
|
|
14
15
|
## Issue Reporting and Security
|
|
15
16
|
|
|
@@ -105,6 +106,77 @@ try {
|
|
|
105
106
|
}
|
|
106
107
|
```
|
|
107
108
|
|
|
109
|
+
### The Consumer Price Index
|
|
110
|
+
|
|
111
|
+
Everything here comes from `@fin.cx/opendata/cpi`. That entry point imports nothing else of the package and has no dependencies. It keeps nothing: each answer goes to the caller with its source note and the answer as it arrived (`answer`: the HTTP status and the body), which the caller keeps as the evidence of an index adjustment.
|
|
112
|
+
|
|
113
|
+
- `BundesbankCpiClient#getMonthly({ from, to? })`, `#getAnnual({ from, to? })`, `#getMonth(month)`
|
|
114
|
+
The Verbraucherpreisindex für Deutschland (VPI) of the Statistisches Bundesamt, overall index, unadjusted, monthly and as annual averages, as the Deutsche Bundesbank republishes it in its time series API. No registration is needed. Values are decimal strings with the digits published (`'113.5'`), never floats. Each series names the base it is stated on (`base: '2020=100'`), read from the answer.
|
|
115
|
+
- `cpiChangePercent(old, new)`, `cpiChangePoints(old, new)`
|
|
116
|
+
The change in percent as the Statistisches Bundesamt computes it, ((new / old) × 100) − 100, rounded to one decimal; and the change in index points.
|
|
117
|
+
- `cpiThresholds(reference, percent)`, `cpiFirstReaching(values, reference, percent, { direction, reach })`
|
|
118
|
+
The thresholds of a percent rule (reference × (1 ± percent / 100), rounded to the reference's digits), and the first month after the reference month that reaches one. Whether the threshold itself counts (`reached`) or only a value beyond it (`exceeded`) is the clause's to say.
|
|
119
|
+
- `cpiFirstReachingPoints(values, reference, points, { direction, reach })`
|
|
120
|
+
The same for a points rule: the first month after the reference month whose change in index points reaches the threshold, at or above reference + points (`up`), at or below reference − points (`down`), or whichever comes first (`either`). The points are a decimal from 0 upward (`'5'`, `'2.5'`); the comparison is exact and nothing is rounded.
|
|
121
|
+
- `cpiScaleAmount(amountCents, old, new, { basis? })`
|
|
122
|
+
An amount in cents scaled from one index value to another: by the index ratio (amount × new / old, the default) or, with `basis: 'rounded_percent'`, by the change in percent rounded to one decimal.
|
|
123
|
+
- `CpiError` with the codes `invalid_input`, `before_first_period`, `not_published`, `base_mismatch`, `series_withdrawn`, `busy`, `unavailable`, `unreachable`, `timeout`, `aborted` and `unexpected_answer`
|
|
124
|
+
|
|
125
|
+
The client takes a `fetch` (for example `createPublicOnlyFetch` of `@push.rocks/smartrequest`), a `baseUrl` for a test server, a `timeoutMs` (30 seconds by default; its timer ends with the request), a `userAgent` and a caller's `signal`. Nothing is retried.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { BundesbankCpiClient, CpiError, cpiChangePercent, cpiFirstReaching, cpiFirstReachingPoints, cpiScaleAmount } from '@fin.cx/opendata/cpi';
|
|
129
|
+
|
|
130
|
+
const client = new BundesbankCpiClient();
|
|
131
|
+
const series = await client.getMonthly({ from: '2020-01' });
|
|
132
|
+
// { index: 'de-vpi', frequency: 'monthly', base: '2020=100', baseYear: 2020,
|
|
133
|
+
// values: [{ period: '2020-01', value: '99.8', status: 'final', ... }, ...],
|
|
134
|
+
// estimates: [{ period: '2026-09', value: '126.6', status: 'estimated', ... }],
|
|
135
|
+
// source: { attribution: 'Quelle: Deutsche Bundesbank, ...', ... }, answer: { httpStatus: 200, text: '...' } }
|
|
136
|
+
|
|
137
|
+
const may2020 = series.values.find((value) => value.period === '2020-05')!; // 100.4
|
|
138
|
+
const october2022 = series.values.find((value) => value.period === '2022-10')!; // 113.5
|
|
139
|
+
cpiChangePercent(may2020, october2022); // '13.0'
|
|
140
|
+
cpiScaleAmount(100_000, may2020, october2022); // 113048 cents
|
|
141
|
+
cpiFirstReaching(series.values, series.values.find((value) => value.period === '2020-08')!, '5', {
|
|
142
|
+
direction: 'up',
|
|
143
|
+
reach: 'reached',
|
|
144
|
+
}); // the value of 2021-12 (104.7)
|
|
145
|
+
cpiFirstReachingPoints(series.values, october2022, '5', { direction: 'up', reach: 'reached' }); // the value of 2024-03 (118.6); 5 % first in 2024-04
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await client.getMonth('2026-09');
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error instanceof CpiError && error.code === 'not_published') {
|
|
151
|
+
console.log(error.estimate?.value); // '126.6', the Bundesbank's estimate, not a published value
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### Published values and estimates
|
|
157
|
+
|
|
158
|
+
The Statistisches Bundesamt publishes a flash estimate of a month one or two working days before the month ends, and the final value about two weeks after it. For the latest month the Bundesbank's series at times carries its own estimate, marked `E`. The client keeps such values apart (`estimates`, `status: 'estimated'`): `values` holds only published ones, `getMonth()` of a month with only an estimate ends in `not_published` with the estimate in the error, and every calculation refuses an estimate with `not_published`. A range or a month with no value yet ends in `not_published` too.
|
|
159
|
+
|
|
160
|
+
#### Base years
|
|
161
|
+
|
|
162
|
+
The Statistisches Bundesamt moves the index to a new base year about every five years (2020=100 since the results for January 2023). It recomputes the values from January of the new base year and converts the earlier ones to the new base, so that a value read on one base differs from the same month read on another. It publishes no factors to convert between bases, and has not done so since 2003. So:
|
|
163
|
+
|
|
164
|
+
- every value carries its base, and the calculations refuse values of different bases, indices or frequencies with `base_mismatch`; nothing is converted;
|
|
165
|
+
- for a percent rule the Statistisches Bundesamt calculates with the values of the current base for both months, even where the contract names an earlier base, so read both again on the current base;
|
|
166
|
+
- points depend on the base. `cpiChangePoints()` and `cpiFirstReachingPoints()` give them within one base only; a points rule on an earlier base is the contract's matter;
|
|
167
|
+
- the client leaves the base open in its request, and the Bundesbank answers with the series of the current base. The key of the former base 2015=100 answers 404 today; an answer of 404 ends in `series_withdrawn`, and an answer that holds two bases in `unexpected_answer`.
|
|
168
|
+
|
|
169
|
+
#### Attribution
|
|
170
|
+
|
|
171
|
+
The figures are the Statistisches Bundesamt's, under the Datenlizenz Deutschland – Namensnennung – Version 2.0 (`https://www.govdata.de/dl-de/by-2-0`), delivered by the Deutsche Bundesbank. Wherever you show an index value, show its source note (`source.attribution`), which names:
|
|
172
|
+
|
|
173
|
+
- "Quelle: Deutsche Bundesbank" with the series key;
|
|
174
|
+
- the Statistisches Bundesamt (Destatis) as the provider of the data, with the index and its base;
|
|
175
|
+
- the licence "Datenlizenz Deutschland – Namensnennung – Version 2.0" with its link;
|
|
176
|
+
- the request the values were read from.
|
|
177
|
+
|
|
178
|
+
A change in percent, a threshold or a scaled amount is not a published figure. Mark it as such, for example "eigene Berechnung", next to the source note.
|
|
179
|
+
|
|
108
180
|
## Before You Start
|
|
109
181
|
|
|
110
182
|
- No stock or fundamentals providers are pre-registered. You must register them before calling the services.
|
|
@@ -509,6 +581,13 @@ Four holidays apply only in part of a state:
|
|
|
509
581
|
- `baseRatePeriods`, `baseRateFirstDay`, `baseRatesReadOn`, `baseRateSource`
|
|
510
582
|
- `BaseRateError` with the codes `invalid_date`, `invalid_range`, `before_first_period` and `not_published`
|
|
511
583
|
|
|
584
|
+
### Consumer price index (`@fin.cx/opendata/cpi`)
|
|
585
|
+
|
|
586
|
+
- `BundesbankCpiClient` with `getMonthly({ from, to? })`, `getAnnual({ from, to? })` and `getMonth(month)`
|
|
587
|
+
- `cpiChangePercent(old, new)`, `cpiChangePoints(old, new)`, `cpiThresholds(reference, percent)`, `cpiFirstReaching(values, reference, percent, options)`, `cpiFirstReachingPoints(values, reference, points, options)`, `cpiScaleAmount(amountCents, old, new, options?)`
|
|
588
|
+
- `bundesbankCpiBaseUrl`, `bundesbankVpiMonthlyKey`, `bundesbankVpiAnnualKey`, `bundesbankVpiFirstMonth`, `bundesbankVpiFirstYear`, `destatisLicence`, `defaultCpiTimeoutMs`
|
|
589
|
+
- `CpiError` with the codes `invalid_input`, `before_first_period`, `not_published`, `base_mismatch`, `series_withdrawn`, `busy`, `unavailable`, `unreachable`, `timeout`, `aborted` and `unexpected_answer`
|
|
590
|
+
|
|
512
591
|
## Data Sources
|
|
513
592
|
|
|
514
593
|
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.
|
|
@@ -564,6 +643,10 @@ The VAT ID clients of `@fin.cx/opendata/vatid` ask two official services at run
|
|
|
564
643
|
- The European Commission's VIES REST API (`https://ec.europa.eu/taxation_customs/vies/rest-api`, contract `swagger_publicVAT.yaml`): `POST /check-vat-number`. Its errors arrive with HTTP 200 and `actionSucceed: false`, as its test service (`/check-vat-test-service`) shows.
|
|
565
644
|
- Not verified: the terms of use of either service, whether eVatR needs a registration (its documentation names none), how many qualified confirmations the BZSt allows per session (`evatr-0008`), and the form of `gueltigAb`/`gueltigBis`, which are passed on as the BZSt writes them. The OpenAPI document lists `evatr-0008` under HTTP 400 and the status list under 403, so the client decides by the code.
|
|
566
645
|
|
|
646
|
+
The consumer price index of `@fin.cx/opendata/cpi` is read at run time, one request at a time, from the Deutsche Bundesbank's time series API (`https://api.statistiken.bundesbank.de/rest/data/BBDP1/`), read on 2026-10-05: the series `M.DE.N.VPI.C.A00000..A` (monthly) and `A.DE.N.VPI.C.A00000..A` (annual averages), whose base dimension is left open so that the answer names the current base (`BBDP1.M.DE.N.VPI.C.A00000.I20.A`, 2020=100). The series begins in January 1991. The Bundesbank names the Statistisches Bundesamt as the source of the figures. The arithmetic follows the Statistisches Bundesamt's "Anleitung für die Berechnung von Schwellenwerten und Veränderungsraten für Wertsicherungsklauseln" (Stand Januar 2024) and its notes on points and percent rules. Every rounding is half away from zero; the Statistisches Bundesamt asks for one decimal without naming a rounding mode, and its examples are consistent with it.
|
|
647
|
+
|
|
648
|
+
- Not verified: limits on the number of requests to the Bundesbank's API (none is documented) and terms of use specific to it. The Bundesbank's terms for reusing statistics exclude data of third parties, so the figures are used under the Statistisches Bundesamt's licence. Whether the Bundesbank keeps the series of an old base for a while after a change of base year is not known; today the key of 2015=100 answers 404.
|
|
649
|
+
|
|
567
650
|
## Behavior And Caveats
|
|
568
651
|
|
|
569
652
|
- Historical market data is cached aggressively because it is treated as immutable.
|
|
@@ -579,7 +662,7 @@ The VAT ID clients of `@fin.cx/opendata/vatid` ask two official services at run
|
|
|
579
662
|
pnpm test
|
|
580
663
|
```
|
|
581
664
|
|
|
582
|
-
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. The VAT ID clients are tested against the answers the two services gave on 2026-09-27 to their documented test values (`test/fixtures/vatid/`); the tests never contact them.
|
|
665
|
+
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. The VAT ID clients are tested against the answers the two services gave on 2026-09-27 to their documented test values (`test/fixtures/vatid/`); the tests never contact them. The consumer price index client is tested against the answers the Bundesbank's API gave on 2026-10-05 and two answers written by hand from them (`test/fixtures/cpi/`); the tests never contact it.
|
|
583
666
|
|
|
584
667
|
## License and Legal Information
|
|
585
668
|
|
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: '4.
|
|
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, the base rate of § 247 BGB, and stock and crypto prices and fundamentals from several providers.'
|
|
6
|
+
version: '4.5.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, the base rate of § 247 BGB, the German consumer price index with the arithmetic of index clauses, and stock and crypto prices and fundamentals from several providers.'
|
|
8
8
|
}
|