2026-04-05 12:56:40 -04:00

99 lines
2.9 KiB
TypeScript

/**
* 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<CommodityPricePoint[]> {
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<Record<string, unknown>>(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);
}