import { z } from 'zod'; export const CommodityType = z.enum([ // Energy commodities 'natural_gas', 'wti_crude', 'coal', // Petroleum 'brent_crude', 'dubai_crude', 'gasoline', 'diesel', 'heating_oil', // Natural gas futures 'ng_futures_1', 'ng_futures_2', 'ng_futures_3', 'ng_futures_4', // Market indicators 'ovx', 'vix', 'dxy', 'financial_stress', // Supply metrics 'spr_level', 'us_crude_production', // Geopolitical risk 'gpr_daily', ]); export type CommodityType = z.infer; export const CommodityPriceSchema = z.object({ commodity: CommodityType, price: z.number(), unit: z.string(), timestamp: z.date(), source: z.string(), }); export type CommodityPrice = z.infer; export const COMMODITY_UNITS: Record = { natural_gas: '$/Million BTU', wti_crude: '$/Barrel', coal: '$/Metric Ton', brent_crude: '$/Barrel', dubai_crude: '$/Barrel', gasoline: '$/Gallon', diesel: '$/Gallon', heating_oil: '$/Gallon', ng_futures_1: '$/Million BTU', ng_futures_2: '$/Million BTU', ng_futures_3: '$/Million BTU', ng_futures_4: '$/Million BTU', ovx: 'Index', vix: 'Index', dxy: 'Index', financial_stress: 'Index', spr_level: 'Million Barrels', us_crude_production: 'Million Barrels/Day', gpr_daily: 'Index', }; export const COMMODITY_DISPLAY_NAMES: Record = { natural_gas: 'Natural Gas', wti_crude: 'WTI Crude', coal: 'Coal', brent_crude: 'Brent Crude', dubai_crude: 'Dubai Crude', gasoline: 'Gasoline', diesel: 'Diesel', heating_oil: 'Heating Oil', ng_futures_1: 'NG Futures (Month 1)', ng_futures_2: 'NG Futures (Month 2)', ng_futures_3: 'NG Futures (Month 3)', ng_futures_4: 'NG Futures (Month 4)', ovx: 'Oil Volatility (OVX)', vix: 'Market Volatility (VIX)', dxy: 'US Dollar Index', financial_stress: 'Financial Stress', spr_level: 'SPR Level', us_crude_production: 'US Crude Production', gpr_daily: 'Geopolitical Risk', }; export const COMMODITY_CATEGORIES = { petroleum: ['wti_crude', 'brent_crude', 'dubai_crude', 'gasoline', 'diesel', 'heating_oil'], energy_commodity: ['natural_gas', 'coal'], futures: ['ng_futures_1', 'ng_futures_2', 'ng_futures_3', 'ng_futures_4'], market_indicator: ['ovx', 'vix', 'dxy', 'financial_stress'], supply: ['spr_level', 'us_crude_production'], geopolitical: ['gpr_daily'], } as const; /** Coerce EIA string values to numbers, treating empty/null as null */ const eiaNumericValue = z.union([z.string(), z.number(), z.null()]).transform((val): number | null => { if (val === null || val === '') return null; const num = Number(val); return Number.isNaN(num) ? null : num; }); /** * EIA natural gas price row. * Endpoint: /v2/natural-gas/pri/fut/data/ * Series: RNGWHHD (Henry Hub Natural Gas Spot Price) */ export const eiaNaturalGasRowSchema = z.object({ period: z.string(), 'series-description': z.string(), value: eiaNumericValue, units: z.string().optional(), }); export type EiaNaturalGasRow = z.infer; /** * EIA petroleum/crude oil price row. * Endpoint: /v2/petroleum/pri/spt/data/ * Series: RWTC (WTI Cushing Oklahoma Spot Price) */ export const eiaWtiCrudeRowSchema = z.object({ period: z.string(), 'series-description': z.string().optional(), value: eiaNumericValue, units: z.string().optional(), }); export type EiaWtiCrudeRow = z.infer; /** * Parsed commodity price data point (from EIA/FRED/GPR). */ export interface CommodityPricePoint { timestamp: Date; commodity: CommodityType; price: number | null; unit: string; source: string; } /** * Generic EIA API response wrapper for commodity endpoints. */ export function eiaCommodityResponseSchema(dataRowSchema: T) { return z.object({ response: z.object({ total: z.union([z.string(), z.number()]).transform(Number), data: z.array(dataRowSchema), }), }); } export const eiaNaturalGasResponseSchema = eiaCommodityResponseSchema(eiaNaturalGasRowSchema); export const eiaWtiCrudeResponseSchema = eiaCommodityResponseSchema(eiaWtiCrudeRowSchema); export type EiaNaturalGasResponse = z.infer; export type EiaWtiCrudeResponse = z.infer; /** * Parse an EIA daily period string to a UTC Date. * Commodity data uses daily format: "2026-02-02" */ export function parseEiaCommodityPeriod(period: string): Date { if (/^\d{4}-\d{2}-\d{2}$/.test(period)) { return new Date(`${period}T00:00:00Z`); } return new Date(period); }