99 lines
2.9 KiB
TypeScript
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);
|
|
}
|