UNPKG

austrian-cpi

Version:

Query and merge Austrian CPI time series data for economic modeling

179 lines (147 loc) 6.42 kB
import { extractSeriesValues, normalizeDate } from "./cpiUtils.js"; import { getLatestValue } from "./getters.js"; /* ********************************************************************** */ /* Internal caches */ /* ********************************************************************** */ const tableCache = new Map(); const latestCache = new Map(); /* ********************************************************************** */ /* Helpers */ /* ********************************************************************** */ function getCpiTable(seriesKey = "index", frequency = "monthly") { const key = `${seriesKey}|${frequency}`; if (!tableCache.has(key)) { const table = extractSeriesValues(seriesKey, frequency).reduce( (acc, { date, value }) => { acc[normalizeDate(date)] = value; return acc; }, {}, ); tableCache.set(key, table); } return tableCache.get(key); } /** * Retrieve CPI for a given date with multi‑level fallback: * 1. Exact match in requested table (monthly → monthly). * 2. YYYY fallback inside same table (monthly row missing). * 3. If `frequency==='monthly'`, retry in *yearly* table. */ function getCpi(seriesKey, frequency, date) { const norm = normalizeDate(date); // 1‒2. Attempt inside requested table let table = getCpiTable(seriesKey, frequency); let val = table[norm] ?? (norm.length === 7 ? table[norm.slice(0, 4)] : undefined); if (val != null) return val; // 3. Cross‑table fallback (monthly → yearly) if (frequency === "monthly") { table = getCpiTable(seriesKey, "yearly"); return table[norm.slice(0, 4)] ?? null; } return null; } /** Latest CPI with monthly→yearly fallback and caching. */ function getLatestCpi(seriesKey, frequency) { const key = `${seriesKey}|${frequency}`; if (latestCache.has(key)) return latestCache.get(key); let latest; try { latest = getLatestValue(seriesKey, frequency); } catch { latest = null; } if ((!latest || latest.value == null) && frequency === "monthly") { // fallback to yearly series latest = getLatestValue(seriesKey, "yearly"); } if (!latest) throw new Error(`No CPI data found for series “${seriesKey}”.`); const rec = { date: latest.date, value: latest.value }; latestCache.set(key, rec); return rec; } /* ********************************************************************** */ /* 1. Constant‑price conversion */ /* ********************************************************************** */ export function convertPrice(price, priceDate, opt = {}) { const { seriesKey = "index", frequency = "monthly", baseDate } = opt; const cpiAtPrice = getCpi(seriesKey, frequency, priceDate); if (cpiAtPrice == null) return null; const base = baseDate ?? getLatestCpi(seriesKey, frequency).date; let baseCpi = getCpi(seriesKey, frequency, base); if (baseCpi == null && frequency === "monthly") { baseCpi = getCpi(seriesKey, "yearly", base); } if (baseCpi == null) return null; return price * (baseCpi / cpiAtPrice); } /** Vectorised conversion of an array of { date, price }. */ export function convertPrices(rows, opt = {}) { const { seriesKey = "index", frequency = "monthly", baseDate } = opt; const resolvedBaseDate = baseDate ?? getLatestCpi(seriesKey, frequency).date; const baseCpi = getCpi(seriesKey, frequency, resolvedBaseDate) ?? (frequency === "monthly" ? getCpi(seriesKey, "yearly", resolvedBaseDate) : null); if (baseCpi == null) throw new Error(`No CPI for baseDate "${resolvedBaseDate}".`); return rows.map((r) => { const cpi = getCpi(seriesKey, frequency, r.date); return { ...r, cpi, real: cpi == null ? null : r.price * (baseCpi / cpi), }; }); } /* ********************************************************************** */ /* 2. Inflation measures */ /* ********************************************************************** */ export function inflationRate(date1, date2, opt = {}) { const { seriesKey = "index", frequency = "monthly" } = opt; const cpi1 = getCpi(seriesKey, frequency, date1); const cpi2 = getCpi(seriesKey, frequency, date2); if (cpi1 == null || cpi2 == null) return null; return (cpi2 / cpi1 - 1) * 100; } export function inflationSeries(dates, opt = {}) { if (!dates.length) return []; const out = [{ date: dates[0], inflation: null }]; const { seriesKey = "index", frequency = "monthly" } = opt; for (let i = 1; i < dates.length; i++) { const prev = getCpi(seriesKey, frequency, dates[i - 1]); const cur = getCpi(seriesKey, frequency, dates[i]); out.push({ date: dates[i], inflation: prev && cur ? (cur / prev - 1) * 100 : null, }); } return out; } export const annualiseRate = (r, perYear) => Math.pow(1 + r, perYear) - 1; /* ********************************************************************** */ /* 3. Real CAGR */ /* ********************************************************************** */ export function realCAGR(p0, d0, p1, d1, opt = {}) { const r0 = convertPrice(p0, d0, opt); const r1 = convertPrice(p1, d1, opt); if (r0 == null || r1 == null) return null; const years = new Date(normalizeDate(d1) + "-01").getFullYear() - new Date(normalizeDate(d0) + "-01").getFullYear(); return years > 0 ? Math.pow(r1 / r0, 1 / years) - 1 : null; } /* ********************************************************************** */ /* 4. Convenience wrapper */ /* ********************************************************************** */ export const deflateSeries = (prices, dates, opt = {}) => { if (prices.length !== dates.length) throw new Error("prices.length !== dates.length"); return prices.map((p, i) => convertPrice(p, dates[i], opt)); }; /* ********************************************************************** */ /* CHANGELOG */ /* ********************************************************************** */ /* 2025‑06‑25‑e: table‑level monthly→yearly fallback; stronger latestCPI */