austrian-cpi
Version:
Query and merge Austrian CPI time series data for economic modeling
179 lines (147 loc) • 6.42 kB
JavaScript
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 */