stats: core-7 economy page with CPI index levels, rebase and categories
All checks were successful
Deploy production / deploy (push) Successful in 1m13s

- /stats/economy defaults multi-country charts to CAN KOR CHN JPN USA NLD CUW,
  with add-a-country chips and an EU 27 comparison line
- CPI index levels rebased to a chosen base year, index/YoY toggle,
  categories (food, energy, housing, core) where the source has them
- new no-key collectors: oecd (CPI categories, 10y yields), statcan (CPI);
  bis WS_LONG_CPI, FRED US CPI categories, Eurostat prc_hicp_minr, WB FP.CPI.TOTL
- Canada added as a region; CBS Curacao fallbacks for growth/unemployment
- timers stats-oecd and stats-statcan
This commit is contained in:
2026-10-26 00:34:27 +00:00
parent 28140bd425
commit 45841d6f6c
18 changed files with 926 additions and 122 deletions

View File

@@ -1,4 +1,5 @@
// A5 BIS central bank policy rates (WS_CBPOL), monthly and daily, in one call each.
// A5 BIS central bank policy rates (WS_CBPOL), monthly and daily, in one call each; plus the BIS
// long consumer price series (WS_LONG_CPI, index 2010 = 100, monthly) → `bis:cpi:<area>`.
import { getText, log, run, sql, writeSeries, type ObsRow, type SeriesRow } from './common';
const AREAS: Record<string, { name: string; region: string | null }> = {
@@ -7,7 +8,26 @@ const AREAS: Record<string, { name: string; region: string | null }> = {
KR: { name: 'Bank of Korea', region: 'KOR' },
CN: { name: "People's Bank of China", region: 'CHN' },
GB: { name: 'Bank of England', region: 'GBR' },
JP: { name: 'Bank of Japan', region: 'JPN' }
JP: { name: 'Bank of Japan', region: 'JPN' },
CA: { name: 'Bank of Canada', region: 'CAN' }
};
/** WS_LONG_CPI areas (no Curaçao: its CPI comes from CBS Curaçao). */
const CPI_AREAS: Record<string, string> = {
CA: 'CAN',
CN: 'CHN',
JP: 'JPN',
KR: 'KOR',
US: 'USA',
NL: 'NLD',
XM: 'EA',
DE: 'DEU',
FR: 'FRA',
BE: 'BEL',
GB: 'GBR',
IN: 'IND',
BR: 'BRA',
RU: 'RUS'
};
function parseCsv(text: string): Record<string, string>[] {
@@ -73,6 +93,35 @@ export async function bis(opts: { job: string }) {
log(`bis ${freq}: ${rows.length} rows`);
total += rows.length;
}
// Long consumer prices: unit 628 = index, 2010 = 100.
const cpiCsv = await getText(
`https://stats.bis.org/api/v2/data/dataflow/BIS/WS_LONG_CPI/1.0/M.${Object.keys(CPI_AREAS).join('+')}.628?format=csv`
);
const cpiObs: ObsRow[] = parseCsv(cpiCsv)
.filter((r) => r.REF_AREA in CPI_AREAS && /^\d{4}-\d{2}/.test(r.TIME_PERIOD ?? ''))
.filter((r) => r.OBS_VALUE !== '' && r.OBS_VALUE !== 'NaN')
.map((r) => ({
series_id: `bis:cpi:${r.REF_AREA}`,
period: `${r.TIME_PERIOD.slice(0, 7)}-01`,
value: Number(r.OBS_VALUE),
status: r.OBS_STATUS && r.OBS_STATUS !== 'A' ? r.OBS_STATUS : null
}));
const cpiSeries: SeriesRow[] = Object.entries(CPI_AREAS)
.filter(([code]) => cpiObs.some((o) => o.series_id === `bis:cpi:${code}`))
.map(([code, region]) => ({
series_id: `bis:cpi:${code}`,
source_id: 'bis',
source_code: `WS_LONG_CPI/M.${code}.628`,
title: 'Consumer prices (BIS long series)',
unit: 'Index 2010=100',
freq: 'M',
region_code: region,
theme: 'economy',
meta: { url: 'https://data.bis.org/topics/CPI' }
}));
await writeSeries(cpiSeries, cpiObs);
log(`bis cpi: ${cpiObs.length} obs`);
total += cpiObs.length;
return total;
});
}