26-source OSINT intelligence engine with live Jarvis dashboard, auto-refresh via SSE, optional LLM layer (4 providers), delta/memory system, and Telegram breaking news alerts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
159 lines
5.2 KiB
JavaScript
159 lines
5.2 KiB
JavaScript
// EIA — US Energy Information Administration
|
|
// Oil prices, natural gas, crude inventories. Free API key required.
|
|
// Gracefully degrades without key.
|
|
|
|
import { safeFetch } from '../utils/fetch.mjs';
|
|
import '../utils/env.mjs';
|
|
|
|
const BASE = 'https://api.eia.gov/v2';
|
|
|
|
// Series definitions with their v2 API paths
|
|
const OIL_SERIES = {
|
|
wti: {
|
|
label: 'WTI Crude Oil ($/bbl)',
|
|
path: '/petroleum/pri/spt/data/',
|
|
params: { frequency: 'daily', 'data[0]': 'value', facets: { series: ['RWTC'] } },
|
|
},
|
|
brent: {
|
|
label: 'Brent Crude Oil ($/bbl)',
|
|
path: '/petroleum/pri/spt/data/',
|
|
params: { frequency: 'daily', 'data[0]': 'value', facets: { series: ['RBRTE'] } },
|
|
},
|
|
};
|
|
|
|
const GAS_SERIES = {
|
|
henryHub: {
|
|
label: 'Henry Hub Natural Gas ($/MMBtu)',
|
|
path: '/natural-gas/pri/fut/data/',
|
|
params: { frequency: 'daily', 'data[0]': 'value', facets: { series: ['RNGWHHD'] } },
|
|
},
|
|
};
|
|
|
|
const INVENTORY_SERIES = {
|
|
crudeStocks: {
|
|
label: 'US Crude Oil Inventories (thousand barrels)',
|
|
path: '/petroleum/stoc/wstk/data/',
|
|
params: { frequency: 'weekly', 'data[0]': 'value', facets: { series: ['WCESTUS1'] } },
|
|
},
|
|
};
|
|
|
|
// Build the URL for a v2 API query
|
|
function buildUrl(apiKey, path, params, length = 10) {
|
|
const url = new URL(`${BASE}${path}`);
|
|
url.searchParams.set('api_key', apiKey);
|
|
if (params.frequency) url.searchParams.set('frequency', params.frequency);
|
|
if (params['data[0]']) url.searchParams.set('data[0]', params['data[0]']);
|
|
url.searchParams.set('sort[0][column]', 'period');
|
|
url.searchParams.set('sort[0][direction]', 'desc');
|
|
url.searchParams.set('length', String(length));
|
|
|
|
// Add facets
|
|
if (params.facets) {
|
|
for (const [facetKey, facetValues] of Object.entries(params.facets)) {
|
|
facetValues.forEach((v, i) => {
|
|
url.searchParams.set(`facets[${facetKey}][]`, v);
|
|
});
|
|
}
|
|
}
|
|
|
|
return url.toString();
|
|
}
|
|
|
|
// Fetch a single EIA series
|
|
export async function fetchSeries(apiKey, seriesDef, length = 10) {
|
|
const url = buildUrl(apiKey, seriesDef.path, seriesDef.params, length);
|
|
return safeFetch(url);
|
|
}
|
|
|
|
// Extract latest value from EIA response
|
|
function extractLatest(resp) {
|
|
const data = resp?.response?.data;
|
|
if (!data?.length) return null;
|
|
return {
|
|
value: parseFloat(data[0].value),
|
|
period: data[0].period,
|
|
unit: data[0]['unit-name'] || data[0].unit || null,
|
|
};
|
|
}
|
|
|
|
// Extract recent values for trend analysis
|
|
function extractRecent(resp, count = 5) {
|
|
const data = resp?.response?.data;
|
|
if (!data?.length) return [];
|
|
return data.slice(0, count).map(d => ({
|
|
value: parseFloat(d.value),
|
|
period: d.period,
|
|
}));
|
|
}
|
|
|
|
// Briefing — oil prices, gas prices, inventories
|
|
export async function briefing(apiKey) {
|
|
if (!apiKey) {
|
|
return {
|
|
source: 'EIA',
|
|
error: 'No EIA API key. Register free at https://www.eia.gov/opendata/register.php',
|
|
hint: 'Set EIA_API_KEY environment variable',
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
const [wtiResp, brentResp, gasResp, inventoryResp] = await Promise.all([
|
|
fetchSeries(apiKey, OIL_SERIES.wti),
|
|
fetchSeries(apiKey, OIL_SERIES.brent),
|
|
fetchSeries(apiKey, GAS_SERIES.henryHub),
|
|
fetchSeries(apiKey, INVENTORY_SERIES.crudeStocks),
|
|
]);
|
|
|
|
const signals = [];
|
|
|
|
// Oil prices
|
|
const wti = extractLatest(wtiResp);
|
|
const brent = extractLatest(brentResp);
|
|
const wtiRecent = extractRecent(wtiResp, 5);
|
|
const brentRecent = extractRecent(brentResp, 5);
|
|
|
|
if (wti && wti.value > 100) signals.push(`WTI crude above $100 at $${wti.value}/bbl`);
|
|
if (wti && wti.value < 50) signals.push(`WTI crude below $50 at $${wti.value}/bbl — supply glut or demand destruction`);
|
|
if (brent && wti && (brent.value - wti.value) > 10) {
|
|
signals.push(`Brent-WTI spread wide at $${(brent.value - wti.value).toFixed(2)} — supply/logistics divergence`);
|
|
}
|
|
|
|
// Gas prices
|
|
const gas = extractLatest(gasResp);
|
|
if (gas && gas.value > 6) signals.push(`Natural gas elevated at $${gas.value}/MMBtu`);
|
|
if (gas && gas.value > 9) signals.push(`Natural gas crisis-level at $${gas.value}/MMBtu`);
|
|
|
|
// Inventories
|
|
const inv = extractLatest(inventoryResp);
|
|
const invRecent = extractRecent(inventoryResp, 5);
|
|
|
|
// Check week-over-week inventory change
|
|
if (invRecent.length >= 2) {
|
|
const weekChange = invRecent[0].value - invRecent[1].value;
|
|
if (Math.abs(weekChange) > 5000) {
|
|
const direction = weekChange > 0 ? 'build' : 'draw';
|
|
signals.push(`Large crude inventory ${direction}: ${weekChange > 0 ? '+' : ''}${(weekChange / 1000).toFixed(1)}M barrels`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
source: 'EIA',
|
|
timestamp: new Date().toISOString(),
|
|
oilPrices: {
|
|
wti: wti ? { ...wti, label: OIL_SERIES.wti.label, recent: wtiRecent } : null,
|
|
brent: brent ? { ...brent, label: OIL_SERIES.brent.label, recent: brentRecent } : null,
|
|
spread: wti && brent ? +(brent.value - wti.value).toFixed(2) : null,
|
|
},
|
|
gasPrice: gas ? { ...gas, label: GAS_SERIES.henryHub.label } : null,
|
|
inventories: {
|
|
crudeStocks: inv ? { ...inv, label: INVENTORY_SERIES.crudeStocks.label, recent: invRecent } : null,
|
|
},
|
|
signals,
|
|
};
|
|
}
|
|
|
|
if (process.argv[1]?.endsWith('eia.mjs')) {
|
|
const data = await briefing(process.env.EIA_API_KEY);
|
|
console.log(JSON.stringify(data, null, 2));
|
|
}
|