@financica/ecb-client 0.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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/index.cjs +291 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +168 -0
- package/dist/index.d.ts +168 -0
- package/dist/index.js +282 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
- package/src/client.ts +246 -0
- package/src/csv.ts +67 -0
- package/src/currencies.ts +54 -0
- package/src/errors.ts +35 -0
- package/src/index.ts +29 -0
- package/src/types.ts +52 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { BASE_CURRENCY } from "./currencies.js";
|
|
2
|
+
import { parseCsv } from "./csv.js";
|
|
3
|
+
import { EcbError, EcbHttpError, NoRateError } from "./errors.js";
|
|
4
|
+
import type {
|
|
5
|
+
ConvertResult,
|
|
6
|
+
CurrencyCode,
|
|
7
|
+
IsoDate,
|
|
8
|
+
RateSnapshot,
|
|
9
|
+
ReferenceRate,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The subset of the WHATWG `fetch` contract this client relies on. The global
|
|
14
|
+
* `fetch` satisfies it, so callers rarely need to pass one — it exists to make
|
|
15
|
+
* the client trivially testable and proxy-friendly.
|
|
16
|
+
*/
|
|
17
|
+
export type FetchLike = (
|
|
18
|
+
input: string,
|
|
19
|
+
init?: { headers?: Record<string, string>; signal?: AbortSignal },
|
|
20
|
+
) => Promise<{ ok: boolean; status: number; text: () => Promise<string> }>;
|
|
21
|
+
|
|
22
|
+
/** A cache for resolved snapshots, keyed by request URL. */
|
|
23
|
+
export interface RateCache {
|
|
24
|
+
get(key: string): RateSnapshot | undefined;
|
|
25
|
+
set(key: string, value: RateSnapshot): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface EcbClientOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Base URL of the ECB data API.
|
|
31
|
+
* Default: `https://data-api.ecb.europa.eu/service`.
|
|
32
|
+
*/
|
|
33
|
+
baseUrl?: string;
|
|
34
|
+
/** Custom fetch implementation. Defaults to the global `fetch`. */
|
|
35
|
+
fetch?: FetchLike;
|
|
36
|
+
/**
|
|
37
|
+
* Snapshot cache. Defaults to an unbounded in-process `Map`. Pass `null` to
|
|
38
|
+
* disable caching entirely.
|
|
39
|
+
*/
|
|
40
|
+
cache?: RateCache | null;
|
|
41
|
+
/** Request timeout in milliseconds. Default `15000`; `0` disables it. */
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DEFAULT_BASE_URL = "https://data-api.ecb.europa.eu/service";
|
|
46
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A small client for the European Central Bank's euro foreign-exchange
|
|
50
|
+
* reference rates, served from the ECB data API (dataflow `EXR`).
|
|
51
|
+
*
|
|
52
|
+
* Every rate is quoted against the euro (units of the currency per 1 EUR).
|
|
53
|
+
* Requests for a date that is not a TARGET business day resolve to the most
|
|
54
|
+
* recent prior business day, via the data API's `lastNObservations` parameter,
|
|
55
|
+
* and the resolved date is reported on each {@link ReferenceRate}.
|
|
56
|
+
*/
|
|
57
|
+
export class EcbClient {
|
|
58
|
+
private readonly baseUrl: string;
|
|
59
|
+
private readonly fetchImpl: FetchLike;
|
|
60
|
+
private readonly cache: RateCache | null;
|
|
61
|
+
private readonly timeoutMs: number;
|
|
62
|
+
|
|
63
|
+
constructor(options: EcbClientOptions = {}) {
|
|
64
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
65
|
+
const resolvedFetch =
|
|
66
|
+
options.fetch ?? (globalThis.fetch as FetchLike | undefined);
|
|
67
|
+
if (!resolvedFetch) {
|
|
68
|
+
throw new EcbError(
|
|
69
|
+
"No fetch implementation available; pass `fetch` in EcbClientOptions",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
this.fetchImpl = resolvedFetch;
|
|
73
|
+
this.cache =
|
|
74
|
+
options.cache === undefined ? new MapCache() : options.cache;
|
|
75
|
+
this.timeoutMs = options.timeoutMs ?? 15000;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the reference rate for a single currency on (or before) a date.
|
|
80
|
+
* `EUR` returns a unit rate without a network call.
|
|
81
|
+
*/
|
|
82
|
+
async getRate(
|
|
83
|
+
currency: CurrencyCode,
|
|
84
|
+
date: Date | IsoDate,
|
|
85
|
+
): Promise<ReferenceRate> {
|
|
86
|
+
const requestedDate = normalizeDate(date);
|
|
87
|
+
if (currency === BASE_CURRENCY) {
|
|
88
|
+
return { currency: BASE_CURRENCY, rate: 1, date: requestedDate };
|
|
89
|
+
}
|
|
90
|
+
const snapshot = await this.fetchSnapshot([currency], requestedDate);
|
|
91
|
+
const rate = snapshot.rates.find((r) => r.currency === currency);
|
|
92
|
+
if (!rate) throw new NoRateError(currency, requestedDate);
|
|
93
|
+
return rate;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve reference rates on (or before) a date. Pass `currencies` to limit
|
|
98
|
+
* the request, or omit it to fetch every series the ECB publishes (note
|
|
99
|
+
* that discontinued series resolve to their last published date).
|
|
100
|
+
*/
|
|
101
|
+
async getRates(
|
|
102
|
+
date: Date | IsoDate,
|
|
103
|
+
currencies?: readonly CurrencyCode[],
|
|
104
|
+
): Promise<RateSnapshot> {
|
|
105
|
+
const requestedDate = normalizeDate(date);
|
|
106
|
+
const wanted = currencies?.filter((c) => c !== BASE_CURRENCY);
|
|
107
|
+
const snapshot =
|
|
108
|
+
wanted && wanted.length === 0
|
|
109
|
+
? { requestedDate, rates: [] }
|
|
110
|
+
: await this.fetchSnapshot(wanted ?? null, requestedDate);
|
|
111
|
+
if (currencies?.includes(BASE_CURRENCY)) {
|
|
112
|
+
return {
|
|
113
|
+
requestedDate,
|
|
114
|
+
rates: [
|
|
115
|
+
{ currency: BASE_CURRENCY, rate: 1, date: requestedDate },
|
|
116
|
+
...snapshot.rates,
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return snapshot;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Convert `amount` from one currency to another using the reference rates on
|
|
125
|
+
* (or before) `date`. Non-euro pairs are crossed through the euro.
|
|
126
|
+
*/
|
|
127
|
+
async convert(params: {
|
|
128
|
+
amount: number;
|
|
129
|
+
from: CurrencyCode;
|
|
130
|
+
to: CurrencyCode;
|
|
131
|
+
date: Date | IsoDate;
|
|
132
|
+
}): Promise<ConvertResult> {
|
|
133
|
+
const { amount, from, to } = params;
|
|
134
|
+
const requestedDate = normalizeDate(params.date);
|
|
135
|
+
if (from === to) {
|
|
136
|
+
return {
|
|
137
|
+
amount,
|
|
138
|
+
rate: 1,
|
|
139
|
+
from,
|
|
140
|
+
to,
|
|
141
|
+
requestedDate,
|
|
142
|
+
rateDate: requestedDate,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const needed = [from, to].filter(
|
|
146
|
+
(c): c is CurrencyCode => c !== BASE_CURRENCY,
|
|
147
|
+
);
|
|
148
|
+
const snapshot = await this.fetchSnapshot(needed, requestedDate);
|
|
149
|
+
const fromLeg = this.legFor(from, snapshot, requestedDate);
|
|
150
|
+
const toLeg = this.legFor(to, snapshot, requestedDate);
|
|
151
|
+
const rate = toLeg.rate / fromLeg.rate;
|
|
152
|
+
return {
|
|
153
|
+
amount: amount * rate,
|
|
154
|
+
rate,
|
|
155
|
+
from,
|
|
156
|
+
to,
|
|
157
|
+
requestedDate,
|
|
158
|
+
rateDate: fromLeg.date > toLeg.date ? fromLeg.date : toLeg.date,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private legFor(
|
|
163
|
+
currency: CurrencyCode,
|
|
164
|
+
snapshot: RateSnapshot,
|
|
165
|
+
requestedDate: IsoDate,
|
|
166
|
+
): ReferenceRate {
|
|
167
|
+
if (currency === BASE_CURRENCY) {
|
|
168
|
+
return { currency: BASE_CURRENCY, rate: 1, date: requestedDate };
|
|
169
|
+
}
|
|
170
|
+
const leg = snapshot.rates.find((r) => r.currency === currency);
|
|
171
|
+
if (!leg) throw new NoRateError(currency, requestedDate);
|
|
172
|
+
return leg;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async fetchSnapshot(
|
|
176
|
+
currencies: readonly CurrencyCode[] | null,
|
|
177
|
+
requestedDate: IsoDate,
|
|
178
|
+
): Promise<RateSnapshot> {
|
|
179
|
+
const key = currencies && currencies.length ? currencies.join("+") : "";
|
|
180
|
+
const url = `${this.baseUrl}/data/EXR/D.${key}.EUR.SP00.A?endPeriod=${requestedDate}&lastNObservations=1`;
|
|
181
|
+
|
|
182
|
+
const cached = this.cache?.get(url);
|
|
183
|
+
if (cached) return cached;
|
|
184
|
+
|
|
185
|
+
const text = await this.get(url);
|
|
186
|
+
const rates: ReferenceRate[] = [];
|
|
187
|
+
for (const record of parseCsv(text)) {
|
|
188
|
+
const currency = record["CURRENCY"];
|
|
189
|
+
const observedAt = record["TIME_PERIOD"];
|
|
190
|
+
const value = Number(record["OBS_VALUE"]);
|
|
191
|
+
if (!currency || !observedAt || !Number.isFinite(value)) continue;
|
|
192
|
+
rates.push({ currency, rate: value, date: observedAt });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const snapshot: RateSnapshot = { requestedDate, rates };
|
|
196
|
+
this.cache?.set(url, snapshot);
|
|
197
|
+
return snapshot;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async get(url: string): Promise<string> {
|
|
201
|
+
const controller =
|
|
202
|
+
this.timeoutMs > 0 ? new AbortController() : undefined;
|
|
203
|
+
const timer = controller
|
|
204
|
+
? setTimeout(() => controller.abort(), this.timeoutMs)
|
|
205
|
+
: undefined;
|
|
206
|
+
try {
|
|
207
|
+
const response = await this.fetchImpl(url, {
|
|
208
|
+
headers: { Accept: "text/csv" },
|
|
209
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
210
|
+
});
|
|
211
|
+
const body = await response.text();
|
|
212
|
+
if (!response.ok) throw new EcbHttpError(response.status, body);
|
|
213
|
+
return body;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error instanceof EcbError) throw error;
|
|
216
|
+
throw new EcbError(`ECB data API request failed: ${url}`, {
|
|
217
|
+
cause: error,
|
|
218
|
+
});
|
|
219
|
+
} finally {
|
|
220
|
+
if (timer) clearTimeout(timer);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
class MapCache implements RateCache {
|
|
226
|
+
private readonly store = new Map<string, RateSnapshot>();
|
|
227
|
+
get(key: string): RateSnapshot | undefined {
|
|
228
|
+
return this.store.get(key);
|
|
229
|
+
}
|
|
230
|
+
set(key: string, value: RateSnapshot): void {
|
|
231
|
+
this.store.set(key, value);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const normalizeDate = (date: Date | IsoDate): IsoDate => {
|
|
236
|
+
if (date instanceof Date) {
|
|
237
|
+
if (Number.isNaN(date.getTime())) {
|
|
238
|
+
throw new EcbError("Invalid Date passed for the rate date");
|
|
239
|
+
}
|
|
240
|
+
return date.toISOString().slice(0, 10);
|
|
241
|
+
}
|
|
242
|
+
if (!DATE_RE.test(date)) {
|
|
243
|
+
throw new EcbError(`Invalid date "${date}"; expected YYYY-MM-DD`);
|
|
244
|
+
}
|
|
245
|
+
return date;
|
|
246
|
+
};
|
package/src/csv.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal RFC 4180 CSV parser, sufficient for the ECB data API's `text/csv`
|
|
3
|
+
* responses. Handles quoted fields containing commas, line breaks and escaped
|
|
4
|
+
* (doubled) quotes — the ECB's `TITLE_COMPL` column, for instance, embeds
|
|
5
|
+
* commas inside quotes.
|
|
6
|
+
*/
|
|
7
|
+
export const parseCsv = (text: string): Record<string, string>[] => {
|
|
8
|
+
const rows = splitRows(text);
|
|
9
|
+
const header = rows.shift();
|
|
10
|
+
if (!header) return [];
|
|
11
|
+
const records: Record<string, string>[] = [];
|
|
12
|
+
for (const row of rows) {
|
|
13
|
+
// Skip blank trailing lines (a single empty field, no real data).
|
|
14
|
+
if (row.length === 1 && row[0] === "") continue;
|
|
15
|
+
const record: Record<string, string> = {};
|
|
16
|
+
for (let i = 0; i < header.length; i++) {
|
|
17
|
+
record[header[i] ?? `column_${i}`] = row[i] ?? "";
|
|
18
|
+
}
|
|
19
|
+
records.push(record);
|
|
20
|
+
}
|
|
21
|
+
return records;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const splitRows = (text: string): string[][] => {
|
|
25
|
+
const rows: string[][] = [];
|
|
26
|
+
let row: string[] = [];
|
|
27
|
+
let field = "";
|
|
28
|
+
let inQuotes = false;
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < text.length; i++) {
|
|
31
|
+
const char = text[i];
|
|
32
|
+
if (inQuotes) {
|
|
33
|
+
if (char === '"') {
|
|
34
|
+
if (text[i + 1] === '"') {
|
|
35
|
+
field += '"';
|
|
36
|
+
i++;
|
|
37
|
+
} else {
|
|
38
|
+
inQuotes = false;
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
field += char;
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (char === '"') {
|
|
46
|
+
inQuotes = true;
|
|
47
|
+
} else if (char === ",") {
|
|
48
|
+
row.push(field);
|
|
49
|
+
field = "";
|
|
50
|
+
} else if (char === "\n" || char === "\r") {
|
|
51
|
+
if (char === "\r" && text[i + 1] === "\n") i++;
|
|
52
|
+
row.push(field);
|
|
53
|
+
rows.push(row);
|
|
54
|
+
row = [];
|
|
55
|
+
field = "";
|
|
56
|
+
} else {
|
|
57
|
+
field += char;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Flush a final row when the text does not end with a line break.
|
|
62
|
+
if (field !== "" || row.length > 0) {
|
|
63
|
+
row.push(field);
|
|
64
|
+
rows.push(row);
|
|
65
|
+
}
|
|
66
|
+
return rows;
|
|
67
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The currencies for which the European Central Bank publishes daily euro
|
|
3
|
+
* foreign-exchange reference rates, as of the ECB's current daily reference
|
|
4
|
+
* list. Every rate is quoted as units of the currency per 1 EUR.
|
|
5
|
+
*
|
|
6
|
+
* The ECB occasionally adds or suspends currencies. This list is a convenience
|
|
7
|
+
* for callers and the source of the {@link EcbCurrency} literal union; the
|
|
8
|
+
* client never restricts lookups to it, so any ISO 4217 code the ECB exposes
|
|
9
|
+
* (including discontinued series) can still be requested.
|
|
10
|
+
*/
|
|
11
|
+
export const ECB_CURRENCIES = [
|
|
12
|
+
"AUD",
|
|
13
|
+
"BGN",
|
|
14
|
+
"BRL",
|
|
15
|
+
"CAD",
|
|
16
|
+
"CHF",
|
|
17
|
+
"CNY",
|
|
18
|
+
"CZK",
|
|
19
|
+
"DKK",
|
|
20
|
+
"GBP",
|
|
21
|
+
"HKD",
|
|
22
|
+
"HUF",
|
|
23
|
+
"IDR",
|
|
24
|
+
"ILS",
|
|
25
|
+
"INR",
|
|
26
|
+
"ISK",
|
|
27
|
+
"JPY",
|
|
28
|
+
"KRW",
|
|
29
|
+
"MXN",
|
|
30
|
+
"MYR",
|
|
31
|
+
"NOK",
|
|
32
|
+
"NZD",
|
|
33
|
+
"PHP",
|
|
34
|
+
"PLN",
|
|
35
|
+
"RON",
|
|
36
|
+
"SEK",
|
|
37
|
+
"SGD",
|
|
38
|
+
"THB",
|
|
39
|
+
"TRY",
|
|
40
|
+
"USD",
|
|
41
|
+
"ZAR",
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
/** A currency on the ECB's daily reference list. */
|
|
45
|
+
export type EcbCurrency = (typeof ECB_CURRENCIES)[number];
|
|
46
|
+
|
|
47
|
+
/** The base currency of every ECB reference rate. */
|
|
48
|
+
export const BASE_CURRENCY = "EUR";
|
|
49
|
+
|
|
50
|
+
const ECB_CURRENCY_SET: ReadonlySet<string> = new Set(ECB_CURRENCIES);
|
|
51
|
+
|
|
52
|
+
/** Narrowing guard: is `code` on the ECB daily reference list? */
|
|
53
|
+
export const isEcbCurrency = (code: string): code is EcbCurrency =>
|
|
54
|
+
ECB_CURRENCY_SET.has(code);
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CurrencyCode, IsoDate } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Base class for every error thrown by this package. */
|
|
4
|
+
export class EcbError extends Error {
|
|
5
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
6
|
+
super(message, options);
|
|
7
|
+
this.name = "EcbError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The ECB data API returned a non-2xx response. */
|
|
12
|
+
export class EcbHttpError extends EcbError {
|
|
13
|
+
readonly status: number;
|
|
14
|
+
readonly body: string;
|
|
15
|
+
|
|
16
|
+
constructor(status: number, body: string) {
|
|
17
|
+
super(`ECB data API returned HTTP ${status}`);
|
|
18
|
+
this.name = "EcbHttpError";
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.body = body;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** No reference rate exists for the currency on or before the requested date. */
|
|
25
|
+
export class NoRateError extends EcbError {
|
|
26
|
+
readonly currency: CurrencyCode;
|
|
27
|
+
readonly date: IsoDate;
|
|
28
|
+
|
|
29
|
+
constructor(currency: CurrencyCode, date: IsoDate) {
|
|
30
|
+
super(`No ECB reference rate for ${currency} on or before ${date}`);
|
|
31
|
+
this.name = "NoRateError";
|
|
32
|
+
this.currency = currency;
|
|
33
|
+
this.date = date;
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry point for @financica/ecb-client.
|
|
3
|
+
*
|
|
4
|
+
* A small, dependency-free client for the European Central Bank's euro
|
|
5
|
+
* foreign-exchange reference rates (data API dataflow `EXR`), with first-class
|
|
6
|
+
* support for historical single-day lookups and last-business-day fallback.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { EcbClient } from "./client.js";
|
|
10
|
+
export type {
|
|
11
|
+
EcbClientOptions,
|
|
12
|
+
FetchLike,
|
|
13
|
+
RateCache,
|
|
14
|
+
} from "./client.js";
|
|
15
|
+
export {
|
|
16
|
+
BASE_CURRENCY,
|
|
17
|
+
ECB_CURRENCIES,
|
|
18
|
+
isEcbCurrency,
|
|
19
|
+
} from "./currencies.js";
|
|
20
|
+
export type { EcbCurrency } from "./currencies.js";
|
|
21
|
+
export { EcbError, EcbHttpError, NoRateError } from "./errors.js";
|
|
22
|
+
export { parseCsv } from "./csv.js";
|
|
23
|
+
export type {
|
|
24
|
+
ConvertResult,
|
|
25
|
+
CurrencyCode,
|
|
26
|
+
IsoDate,
|
|
27
|
+
RateSnapshot,
|
|
28
|
+
ReferenceRate,
|
|
29
|
+
} from "./types.js";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { EcbCurrency } from "./currencies.js";
|
|
2
|
+
|
|
3
|
+
/** An ISO 8601 calendar date, formatted `YYYY-MM-DD`. */
|
|
4
|
+
export type IsoDate = string;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A currency code. ECB reference currencies are strongly typed for
|
|
8
|
+
* autocomplete; any other ISO 4217 code is still accepted. The `string & {}`
|
|
9
|
+
* member preserves literal autocomplete while widening to accept any string.
|
|
10
|
+
*/
|
|
11
|
+
export type CurrencyCode = EcbCurrency | "EUR" | (string & {});
|
|
12
|
+
|
|
13
|
+
/** A single euro reference-rate observation. */
|
|
14
|
+
export interface ReferenceRate {
|
|
15
|
+
/** ISO 4217 code of the quoted currency. */
|
|
16
|
+
currency: CurrencyCode;
|
|
17
|
+
/** Units of {@link currency} per 1 EUR. */
|
|
18
|
+
rate: number;
|
|
19
|
+
/**
|
|
20
|
+
* The date the observation actually applies to. Equal to the requested date
|
|
21
|
+
* on a TARGET business day, or the most recent prior business day when the
|
|
22
|
+
* requested date had no published rate (weekend, holiday).
|
|
23
|
+
*/
|
|
24
|
+
date: IsoDate;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A set of reference rates resolved for a single requested date. */
|
|
28
|
+
export interface RateSnapshot {
|
|
29
|
+
/** The date that was requested, normalized to `YYYY-MM-DD`. */
|
|
30
|
+
requestedDate: IsoDate;
|
|
31
|
+
/**
|
|
32
|
+
* One entry per currency. Each entry's effective {@link ReferenceRate.date}
|
|
33
|
+
* may differ — discontinued series resolve to their last published date.
|
|
34
|
+
*/
|
|
35
|
+
rates: ReferenceRate[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The outcome of converting an amount between two currencies. */
|
|
39
|
+
export interface ConvertResult {
|
|
40
|
+
/** The converted amount, expressed in {@link to}. Not rounded. */
|
|
41
|
+
amount: number;
|
|
42
|
+
/** The effective {@link from} → {@link to} rate applied. */
|
|
43
|
+
rate: number;
|
|
44
|
+
/** Source currency. */
|
|
45
|
+
from: CurrencyCode;
|
|
46
|
+
/** Target currency. */
|
|
47
|
+
to: CurrencyCode;
|
|
48
|
+
/** The requested date, normalized to `YYYY-MM-DD`. */
|
|
49
|
+
requestedDate: IsoDate;
|
|
50
|
+
/** The effective observation date of the rate(s) applied. */
|
|
51
|
+
rateDate: IsoDate;
|
|
52
|
+
}
|