/** * Geopolitical Risk (GPR) Index client. * * Source: Dario Caldara and Matteo Iacoviello (2022), * "Measuring Geopolitical Risk," American Economic Review. * https://www.matteoiacoviello.com/gpr.htm * * Data is a daily Excel file (CC-BY-4.0), updated daily. * We download it, parse with xlsx, and return CommodityPricePoint[]. */ import { type CommodityPricePoint } from '@/lib/schemas/commodities.js'; import * as XLSX from 'xlsx'; const GPR_DAILY_URL = 'https://www.matteoiacoviello.com/gpr_files/data_gpr_daily_recent.xls'; const FETCH_TIMEOUT_MS = 30_000; /** * Fetch and parse the daily GPR index. * Returns CommodityPricePoint[] with commodity='gpr_daily'. */ export async function getGeopoliticalRiskIndex( options: { start?: string; end?: string; } = {}, ): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); let buffer: ArrayBuffer; try { const response = await fetch(GPR_DAILY_URL, { signal: controller.signal }); if (!response.ok) { throw new Error(`GPR download failed: HTTP ${response.status}`); } buffer = await response.arrayBuffer(); } finally { clearTimeout(timeoutId); } const workbook = XLSX.read(buffer, { type: 'array' }); const sheet = workbook.Sheets[workbook.SheetNames[0]!]; if (!sheet) { throw new Error('GPR Excel file has no sheets'); } const rows = XLSX.utils.sheet_to_json>(sheet); const startDate = options.start ? new Date(`${options.start}T00:00:00Z`) : null; const endDate = options.end ? new Date(`${options.end}T00:00:00Z`) : null; const results: CommodityPricePoint[] = []; for (const row of rows) { // The date column may be named "date" or "DATE" depending on the file version const dateVal = row['date'] ?? row['DATE']; const gprVal = row['GPRD'] ?? row['GPR_Daily'] ?? row['gprd']; if (dateVal === undefined || gprVal === undefined) continue; let timestamp: Date; if (typeof dateVal === 'number') { // Excel serial date number timestamp = excelDateToUTC(dateVal); } else if (typeof dateVal === 'string') { timestamp = new Date(`${dateVal}T00:00:00Z`); } else { continue; } if (isNaN(timestamp.getTime())) continue; const price = Number(gprVal); if (isNaN(price)) continue; if (startDate && timestamp < startDate) continue; if (endDate && timestamp > endDate) continue; results.push({ timestamp, commodity: 'gpr_daily', price, unit: 'Index', source: 'GPR', }); } return results; } /** Convert Excel serial date number to UTC Date */ function excelDateToUTC(serial: number): Date { // Excel epoch is 1900-01-01, but has a leap year bug (day 60 = Feb 29, 1900 which didn't exist) const utcDays = serial - 25569; // Days since Unix epoch return new Date(utcDays * 86400 * 1000); }