@fin.cx/opendata 4.3.0 → 4.4.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 +15 -4
- 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 +35 -0
- package/dist_ts/cpi/indexmath.js +163 -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 +2 -3
- package/dist_ts/plugins.js +3 -4
- package/dist_ts/stocks/classes.baseproviderservice.js +2 -2
- package/dist_ts/stocks/classes.fundamentalsservice.js +2 -2
- package/dist_ts/stocks/classes.stockdataservice.js +2 -2
- package/dist_ts/stocks/classes.stockservice.js +2 -2
- package/dist_ts/stocks/providers/provider.coingecko.js +3 -3
- package/dist_ts/stocks/providers/provider.secedgar.js +2 -2
- package/package.json +16 -10
- package/readme.md +84 -4
- 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 +182 -0
- package/ts/cpi/interfaces.ts +137 -0
- package/ts/cpi/request.ts +49 -0
- package/ts/plugins.ts +2 -4
- package/ts/stocks/classes.baseproviderservice.ts +1 -1
- package/ts/stocks/classes.fundamentalsservice.ts +1 -1
- package/ts/stocks/classes.stockdataservice.ts +1 -1
- package/ts/stocks/classes.stockservice.ts +1 -1
- package/ts/stocks/providers/provider.coingecko.ts +2 -2
- package/ts/stocks/providers/provider.secedgar.ts +1 -1
- /package/{license → license.md} +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** A calendar month, `YYYY-MM`. */
|
|
2
|
+
export type TIsoMonth = string;
|
|
3
|
+
|
|
4
|
+
/** A calendar year, `YYYY`. */
|
|
5
|
+
export type TIsoYear = string;
|
|
6
|
+
|
|
7
|
+
/** The period of an index value: a month (`YYYY-MM`) or a year (`YYYY`, the annual average). */
|
|
8
|
+
export type TCpiPeriod = TIsoMonth | TIsoYear;
|
|
9
|
+
|
|
10
|
+
/** Which price index a value belongs to: `de-vpi`, the Verbraucherpreisindex für Deutschland of the Statistisches Bundesamt. */
|
|
11
|
+
export type TCpiIndex = 'de-vpi';
|
|
12
|
+
|
|
13
|
+
export type TCpiFrequency = 'monthly' | 'annual';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* - `final`: the value as published, final until the next change of the base year;
|
|
17
|
+
* - `estimated`: a value the source marks as an estimate (the Bundesbank's `OBS_STATUS` `E`, for the
|
|
18
|
+
* latest month at times). It is no published index value and is never used in a calculation.
|
|
19
|
+
*/
|
|
20
|
+
export type TCpiValueStatus = 'final' | 'estimated';
|
|
21
|
+
|
|
22
|
+
/** One index value as its source states it. */
|
|
23
|
+
export interface ICpiValue {
|
|
24
|
+
index: TCpiIndex;
|
|
25
|
+
period: TCpiPeriod;
|
|
26
|
+
/**
|
|
27
|
+
* The value as a decimal with a point and exactly the digits published, trailing zeros included
|
|
28
|
+
* (`'100.0'`, `'113.5'`). Never a float.
|
|
29
|
+
*/
|
|
30
|
+
value: string;
|
|
31
|
+
/** the base the value is stated on, as the source writes it: `'2020=100'` */
|
|
32
|
+
base: string;
|
|
33
|
+
status: TCpiValueStatus;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A source's answer as it arrived: its HTTP status and its body as text. */
|
|
37
|
+
export interface ICpiServiceAnswer {
|
|
38
|
+
httpStatus: number;
|
|
39
|
+
text: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Where a series comes from and how it must be credited. The figures are those of the Statistisches
|
|
44
|
+
* Bundesamt (Destatis) under the Datenlizenz Deutschland – Namensnennung – Version 2.0.
|
|
45
|
+
*/
|
|
46
|
+
export interface ICpiSourceNote {
|
|
47
|
+
source: 'bundesbank';
|
|
48
|
+
/** who delivered the figures: `'Deutsche Bundesbank'` */
|
|
49
|
+
provider: string;
|
|
50
|
+
/** whose figures they are: `'Statistisches Bundesamt (Destatis)'` */
|
|
51
|
+
dataOrigin: string;
|
|
52
|
+
/** the series' title as the answer gives it */
|
|
53
|
+
title: string;
|
|
54
|
+
/** the series key as the answer gives it, its base included: `'BBDP1.M.DE.N.VPI.C.A00000.I20.A'` */
|
|
55
|
+
key: string;
|
|
56
|
+
/** the request the figures were read from */
|
|
57
|
+
uri: string;
|
|
58
|
+
licence: string;
|
|
59
|
+
licenceUri: string;
|
|
60
|
+
/** the source note to show wherever the figures are shown */
|
|
61
|
+
attribution: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A range of index values of one series and one base, as one answer gave them. */
|
|
65
|
+
export interface ICpiSeries {
|
|
66
|
+
index: TCpiIndex;
|
|
67
|
+
frequency: TCpiFrequency;
|
|
68
|
+
/** `'2020=100'` */
|
|
69
|
+
base: string;
|
|
70
|
+
/** `2020` */
|
|
71
|
+
baseYear: number;
|
|
72
|
+
/** the published values, oldest first */
|
|
73
|
+
values: ICpiValue[];
|
|
74
|
+
/** the values the source marks as estimates, oldest first; kept apart so that no calculation takes one */
|
|
75
|
+
estimates: ICpiValue[];
|
|
76
|
+
source: ICpiSourceNote;
|
|
77
|
+
answer: ICpiServiceAnswer;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One published value with where it comes from and the answer it was read from. */
|
|
81
|
+
export interface ICpiReading extends ICpiValue {
|
|
82
|
+
source: ICpiSourceNote;
|
|
83
|
+
answer: ICpiServiceAnswer;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The thresholds of a percent rule: the reference value times (1 ± percent / 100), rounded to its digits. */
|
|
87
|
+
export interface ICpiThresholds {
|
|
88
|
+
index: TCpiIndex;
|
|
89
|
+
base: string;
|
|
90
|
+
/** the percentage asked for, as given */
|
|
91
|
+
percent: string;
|
|
92
|
+
upper: string;
|
|
93
|
+
lower: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ICpiReachOptions {
|
|
97
|
+
/** `up`: the upper threshold, `down`: the lower one, `either`: whichever comes first */
|
|
98
|
+
direction: 'up' | 'down' | 'either';
|
|
99
|
+
/** `reached`: the threshold itself counts; `exceeded`: only a value beyond it counts */
|
|
100
|
+
reach: 'reached' | 'exceeded';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ICpiScaleOptions {
|
|
104
|
+
/**
|
|
105
|
+
* - `index_ratio` (default): amount × new index / old index, as the Statistisches Bundesamt writes it;
|
|
106
|
+
* - `rounded_percent`: amount × (100 + the change in percent rounded to one decimal) / 100.
|
|
107
|
+
*/
|
|
108
|
+
basis?: 'index_ratio' | 'rounded_percent';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A `fetch` as the WHATWG standard defines it; injectable for tests and for a fetch that reaches only public addresses. */
|
|
112
|
+
export type TCpiFetch = (input: string, init: RequestInit) => Promise<Response>;
|
|
113
|
+
|
|
114
|
+
export interface ICpiClientOptions {
|
|
115
|
+
/** default: the runtime's `fetch` */
|
|
116
|
+
fetch?: TCpiFetch;
|
|
117
|
+
/** default: the official address (`bundesbankCpiBaseUrl`); a test's own server otherwise */
|
|
118
|
+
baseUrl?: string;
|
|
119
|
+
/** how long one request may take, the answer included; default `defaultCpiTimeoutMs` */
|
|
120
|
+
timeoutMs?: number;
|
|
121
|
+
/** sent as `User-Agent` where given */
|
|
122
|
+
userAgent?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ICpiMonthlyInput {
|
|
126
|
+
from: TIsoMonth;
|
|
127
|
+
/** default: whatever the source has published after `from` */
|
|
128
|
+
to?: TIsoMonth;
|
|
129
|
+
signal?: AbortSignal;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface ICpiAnnualInput {
|
|
133
|
+
from: TIsoYear;
|
|
134
|
+
/** default: whatever the source has published after `from` */
|
|
135
|
+
to?: TIsoYear;
|
|
136
|
+
signal?: AbortSignal;
|
|
137
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { CpiError } from './classes.cpierror.js';
|
|
2
|
+
import { defaultCpiTimeoutMs } from './constants.js';
|
|
3
|
+
import type { ICpiClientOptions, ICpiServiceAnswer, TCpiFetch } from './interfaces.js';
|
|
4
|
+
|
|
5
|
+
/** The time a client gives one request: its option, or the default; anything else than a positive whole number of milliseconds is refused. */
|
|
6
|
+
export const timeoutOf = (optionsArg: ICpiClientOptions): number => {
|
|
7
|
+
const timeout = optionsArg.timeoutMs ?? defaultCpiTimeoutMs;
|
|
8
|
+
if (!Number.isSafeInteger(timeout) || timeout <= 0) {
|
|
9
|
+
throw new CpiError('invalid_input', `the time limit is a positive whole number of milliseconds, got ${String(optionsArg.timeoutMs)}`);
|
|
10
|
+
}
|
|
11
|
+
return timeout;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sends one `GET` for CSV and reads the answer whole, within the client's time. The caller's signal
|
|
16
|
+
* ends it as `aborted`, the time as `timeout`, and a request that could not be sent or answered as
|
|
17
|
+
* `unreachable`; nothing is retried. The timer ends with the request, answered or not.
|
|
18
|
+
*/
|
|
19
|
+
export const getText = async (
|
|
20
|
+
optionsArg: ICpiClientOptions,
|
|
21
|
+
urlArg: string,
|
|
22
|
+
signalArg: AbortSignal | undefined,
|
|
23
|
+
): Promise<ICpiServiceAnswer> => {
|
|
24
|
+
const fetchFn: TCpiFetch = optionsArg.fetch ?? ((inputArg, initArg) => fetch(inputArg, initArg));
|
|
25
|
+
if (signalArg?.aborted) {
|
|
26
|
+
throw new CpiError('aborted', 'the request was ended by its caller', { service: 'bundesbank' }, { cause: signalArg.reason });
|
|
27
|
+
}
|
|
28
|
+
const timeout = new AbortController();
|
|
29
|
+
const timer = setTimeout(() => timeout.abort(new Error('the client\'s time ran out')), timeoutOf(optionsArg));
|
|
30
|
+
const signal = signalArg ? AbortSignal.any([signalArg, timeout.signal]) : timeout.signal;
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetchFn(urlArg, {
|
|
33
|
+
method: 'GET',
|
|
34
|
+
headers: { accept: 'text/csv', ...(optionsArg.userAgent ? { 'user-agent': optionsArg.userAgent } : {}) },
|
|
35
|
+
signal,
|
|
36
|
+
});
|
|
37
|
+
return { httpStatus: response.status, text: await response.text() };
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (signalArg?.aborted) {
|
|
40
|
+
throw new CpiError('aborted', 'the request was ended by its caller', { service: 'bundesbank' }, { cause: error });
|
|
41
|
+
}
|
|
42
|
+
if (timeout.signal.aborted) {
|
|
43
|
+
throw new CpiError('timeout', 'the source did not answer in time', { service: 'bundesbank' }, { cause: error });
|
|
44
|
+
}
|
|
45
|
+
throw new CpiError('unreachable', 'the source could not be reached', { service: 'bundesbank' }, { cause: error });
|
|
46
|
+
} finally {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
}
|
|
49
|
+
};
|
package/ts/plugins.ts
CHANGED
|
@@ -6,9 +6,8 @@ export {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
// @push.rocks scope
|
|
9
|
-
import * as qenv from '@push.rocks/qenv';
|
|
10
|
-
import * as smartdelay from '@push.rocks/smartdelay';
|
|
11
9
|
import * as smartpath from '@push.rocks/smartpath';
|
|
10
|
+
import * as smartpromise from '@push.rocks/smartpromise';
|
|
12
11
|
import * as smartrequest from '@push.rocks/smartrequest';
|
|
13
12
|
|
|
14
13
|
const getErrorMessage = (error: unknown) => {
|
|
@@ -21,8 +20,7 @@ const getErrorMessage = (error: unknown) => {
|
|
|
21
20
|
|
|
22
21
|
export {
|
|
23
22
|
getErrorMessage,
|
|
24
|
-
qenv,
|
|
25
|
-
smartdelay,
|
|
26
23
|
smartpath,
|
|
24
|
+
smartpromise,
|
|
27
25
|
smartrequest,
|
|
28
26
|
}
|
|
@@ -224,7 +224,7 @@ export abstract class BaseProviderService<TProvider extends IBaseProvider, TData
|
|
|
224
224
|
if (attempt < maxAttempts) {
|
|
225
225
|
const delay = (config.retryDelay || 1000) * attempt;
|
|
226
226
|
console.log(`Retry attempt ${attempt} after ${delay}ms`);
|
|
227
|
-
await plugins.
|
|
227
|
+
await plugins.smartpromise.delayFor(delay);
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
}
|
|
@@ -321,7 +321,7 @@ export class FundamentalsService implements IFundamentalsProviderRegistry {
|
|
|
321
321
|
if (attempt < maxAttempts) {
|
|
322
322
|
const delay = (config.retryDelay || 1000) * attempt;
|
|
323
323
|
console.log(`Retry attempt ${attempt} after ${delay}ms`);
|
|
324
|
-
await plugins.
|
|
324
|
+
await plugins.smartpromise.delayFor(delay);
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
327
|
}
|
|
@@ -494,7 +494,7 @@ export class StockDataService {
|
|
|
494
494
|
if (attempt < maxAttempts) {
|
|
495
495
|
const delay = (config.retryDelay || 1000) * attempt;
|
|
496
496
|
console.log(`Retry attempt ${attempt} after ${delay}ms`);
|
|
497
|
-
await plugins.
|
|
497
|
+
await plugins.smartpromise.delayFor(delay);
|
|
498
498
|
}
|
|
499
499
|
}
|
|
500
500
|
}
|
|
@@ -449,7 +449,7 @@ export class StockPriceService implements IProviderRegistry {
|
|
|
449
449
|
if (attempt < maxAttempts) {
|
|
450
450
|
const delay = (config.retryDelay || 1000) * attempt;
|
|
451
451
|
console.log(`Retry attempt ${attempt} after ${delay}ms`);
|
|
452
|
-
await plugins.
|
|
452
|
+
await plugins.smartpromise.delayFor(delay);
|
|
453
453
|
}
|
|
454
454
|
}
|
|
455
455
|
}
|
|
@@ -48,7 +48,7 @@ class RateLimiter {
|
|
|
48
48
|
if (this.requestTimes.length >= this.maxRequestsPerMinute) {
|
|
49
49
|
const oldestRequest = this.requestTimes[0];
|
|
50
50
|
const waitTime = 60000 - (now - oldestRequest) + 100; // +100ms buffer
|
|
51
|
-
await plugins.
|
|
51
|
+
await plugins.smartpromise.delayFor(waitTime);
|
|
52
52
|
return this.waitForSlot(); // Recursively check again
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -773,7 +773,7 @@ export class CoinGeckoProvider implements IStockProvider {
|
|
|
773
773
|
);
|
|
774
774
|
|
|
775
775
|
if (attempt < maxRetries - 1) {
|
|
776
|
-
await plugins.
|
|
776
|
+
await plugins.smartpromise.delayFor(error.waitTime);
|
|
777
777
|
continue;
|
|
778
778
|
} else {
|
|
779
779
|
this.logger.error(`Max retries (${maxRetries}) exceeded for ${operationName} due to rate limiting`);
|
|
@@ -36,7 +36,7 @@ class RateLimiter {
|
|
|
36
36
|
if (this.requestTimes.length >= this.maxRequestsPerSecond) {
|
|
37
37
|
const oldestRequest = this.requestTimes[0];
|
|
38
38
|
const waitTime = 1000 - (now - oldestRequest) + 10; // +10ms buffer
|
|
39
|
-
await plugins.
|
|
39
|
+
await plugins.smartpromise.delayFor(waitTime);
|
|
40
40
|
return this.waitForSlot(); // Recursively check again
|
|
41
41
|
}
|
|
42
42
|
|
/package/{license → license.md}
RENAMED
|
File without changes
|