Files
worldmonitor/scripts/seed-imf-macro.mjs
Elie Habib 96482c7073 fix(seeds): migrate IMF seeders to SDMX 3.0 API (#3020)
* fix(seeds): migrate IMF seeders from blocked DataMapper to SDMX 3.0 API

IMF DataMapper API (www.imf.org/external/datamapper/api/v1) is now
blocked by Akamai WAF via JA3 TLS fingerprinting. The old SDMX endpoint
(dataservices.imf.org) was decommissioned in 2025.

Switch all 3 IMF-consuming seeders to the new SDMX 3.0 API at
api.imf.org/external/sdmx/3.0/ which is accessible without proxy.

- Add imfSdmxFetchIndicator() to _seed-utils.mjs
- Migrate seed-recovery-fiscal-space.mjs (FM indicators to WEO equivalents)
- Migrate seed-imf-macro.mjs
- Migrate seed-national-debt.mjs

* fix: address PR review — dynamic COUNTRY position, retry, parallel fetches

- Use findIndex for COUNTRY dimension position instead of hardcoding 0
  (fixes potential wrong mappings for non-WEO databases like FM)
- Wrap SDMX fetch in withRetry(fn, 2, 2000) for transient network errors
- Switch fiscal-space to Promise.all (no rate limit on api.imf.org)
2026-04-12 21:29:37 +04:00

89 lines
2.8 KiB
JavaScript

#!/usr/bin/env node
import { loadEnvFile, runSeed, loadSharedConfig, imfSdmxFetchIndicator } from './_seed-utils.mjs';
loadEnvFile(import.meta.url);
const CANONICAL_KEY = 'economic:imf:macro:v2';
const CACHE_TTL = 35 * 24 * 3600; // 35 days — monthly IMF WEO release
const ISO2_TO_ISO3 = loadSharedConfig('iso2-to-iso3.json');
const ISO3_TO_ISO2 = Object.fromEntries(Object.entries(ISO2_TO_ISO3).map(([k, v]) => [v, k]));
const AGGREGATE_CODES = new Set([
'ADVEC', 'EMEDE', 'EURO', 'MECA', 'OEMDC', 'WEOWORLD', 'EU',
'AS5', 'DA', 'EDE', 'MAE', 'OAE', 'SSA', 'WE', 'EMDE', 'G20',
]);
function isAggregate(code) {
if (!code || code.length !== 3) return true;
return AGGREGATE_CODES.has(code) || code.endsWith('Q');
}
function weoYears() {
const y = new Date().getFullYear();
return [`${y}`, `${y - 1}`, `${y - 2}`];
}
function latestValue(byYear) {
for (const year of weoYears()) {
const v = Number(byYear?.[year]);
if (Number.isFinite(v)) return { value: v, year: Number(year) };
}
return null;
}
async function fetchImfMacro() {
const years = weoYears();
const [inflationData, currentAccountData, govRevenueData] = await Promise.all([
imfSdmxFetchIndicator('PCPIPCH', { years }),
imfSdmxFetchIndicator('BCA_NGDPD', { years }),
imfSdmxFetchIndicator('GGR_NGDP', { years }),
]);
const countries = {};
const allIso3 = new Set([
...Object.keys(inflationData),
...Object.keys(currentAccountData),
...Object.keys(govRevenueData),
]);
for (const iso3 of allIso3) {
if (isAggregate(iso3)) continue;
const iso2 = ISO3_TO_ISO2[iso3];
if (!iso2) continue;
const infl = latestValue(inflationData[iso3]);
const ca = latestValue(currentAccountData[iso3]);
const rev = latestValue(govRevenueData[iso3]);
if (!infl && !ca && !rev) continue;
countries[iso2] = {
inflationPct: infl?.value ?? null,
currentAccountPct: ca?.value ?? null,
govRevenuePct: rev?.value ?? null,
year: infl?.year ?? ca?.year ?? rev?.year ?? null,
};
}
return { countries, seededAt: new Date().toISOString() };
}
function validate(data) {
return typeof data?.countries === 'object' && Object.keys(data.countries).length >= 150;
}
// Guard: only run when executed directly, not when imported by tests
if (process.argv[1]?.endsWith('seed-imf-macro.mjs')) {
runSeed('economic', 'imf-macro', CANONICAL_KEY, fetchImfMacro, {
validateFn: validate,
ttlSeconds: CACHE_TTL,
sourceVersion: `imf-sdmx-weo-${new Date().getFullYear()}`,
recordCount: (data) => Object.keys(data?.countries ?? {}).length,
}).catch((err) => {
const _cause = err.cause ? ` (cause: ${err.cause.message || err.cause.code || err.cause})` : '';
console.error('FATAL:', (err.message || err) + _cause);
process.exit(1);
});
}