ECB Euro Reference Rates and Deposit Rate: Fetches ECB daily euro reference exchange rates against a chosen currency, and the ECB deposit facility rate history, from the ECB data portal. No key needed.
| 1 | import { defineSource } from '@jaspers-ai/sdk' |
| 2 | import { z } from 'zod' |
| 3 | |
| 4 | |
| 5 | // The European Central Bank's data portal. No key. Euro area reference rates and policy rates, in |
| 6 | // SDMX-JSON, which puts its numbers in one object and their dates in another. |
| 7 | |
| 8 | const HOST = 'data-api.ecb.europa.eu' |
| 9 | |
| 10 | interface Sdmx { |
| 11 | dataSets: { series: Record<string, { observations: Record<string, (number | null)[]> }> }[] |
| 12 | structure: { dimensions: { observation: { id: string; values: { id: string; name?: string }[] }[] } } |
| 13 | } |
| 14 | |
| 15 | /** |
| 16 | * SDMX-JSON as rows. The observations are keyed by position, and the positions index the time |
| 17 | * dimension's values, so the two are read together. |
| 18 | */ |
| 19 | async function read(ctx: { fetch: typeof fetch }, path: string, query: string): Promise<{ date: string; value: number }[]> { |
| 20 | const response = await ctx.fetch(`https://${HOST}/service/data/${path}?${query}&format=jsondata`) |
| 21 | if (!response.ok) { |
| 22 | if (response.status === 404) throw new Error('The ECB has no series at that key.') |
| 23 | throw new Error(`The ECB answered ${response.status}.`) |
| 24 | } |
| 25 | const body = (await response.json()) as Sdmx |
| 26 | const dates = body.structure.dimensions.observation[0]?.values.map((one) => one.id) ?? [] |
| 27 | const series = Object.values(body.dataSets[0]?.series ?? {})[0] |
| 28 | if (!series) throw new Error('The ECB answered with no series.') |
| 29 | return Object.entries(series.observations) |
| 30 | .map(([at, values]) => ({ date: dates[Number(at)] ?? at, value: values[0] })) |
| 31 | .filter((one): one is { date: string; value: number } => typeof one.value === 'number') |
| 32 | .sort((a, b) => a.date.localeCompare(b.date)) |
| 33 | } |
| 34 | |
| 35 | export const fx = defineSource({ |
| 36 | description: |
| 37 | 'The ECB euro reference rate against one currency, daily: how many units of that currency one euro buys. currency is a three letter code like USD, GBP, JPY, or CHF.', |
| 38 | hosts: [HOST], |
| 39 | input: z.object({ |
| 40 | currency: z.string().default('USD').describe('A three letter currency code.'), |
| 41 | days: z.number().int().min(1).max(10000).default(250), |
| 42 | }), |
| 43 | async run({ currency, days }, ctx) { |
| 44 | const code = currency.trim().toUpperCase() |
| 45 | const rows = await read(ctx, `EXR/D.${code}.EUR.SP00.A`, `lastNObservations=${days}`) |
| 46 | return { pair: `EUR/${code}`, rows: rows.map((one) => ({ date: one.date, pair: `EUR/${code}`, rate: one.value })) } |
| 47 | }, |
| 48 | }) |
| 49 | |
| 50 | export const policyRate = defineSource({ |
| 51 | description: 'The ECB deposit facility rate, the euro area policy rate, every time it changed.', |
| 52 | hosts: [HOST], |
| 53 | input: z.object({ days: z.number().int().min(1).max(10000).default(400) }), |
| 54 | async run({ days }, ctx) { |
| 55 | const rows = await read(ctx, 'FM/D.U2.EUR.4F.KR.DFR.LEV', `lastNObservations=${days}`) |
| 56 | return { rows: rows.map((one) => ({ date: one.date, rate: one.value })) } |
| 57 | }, |
| 58 | }) |