feat(feeds): US Natural Gas Storage weekly seeder (EIA NW2_EPG0_SWO_R48_BCF) (#2353)

* feat(feeds): US Natural Gas Storage seeder via EIA (NW2_EPG0_SWO_R48_BCF)

Adds weekly EIA natural gas working gas storage for the Lower-48 states
(series NW2_EPG0_SWO_R48_BCF, in Bcf), mirroring the crude inventories
pattern exactly. Companion dataset to EU gas storage (GIE AGSI+).

- proto: GetNatGasStorage RPC + NatGasStorageWeek message
- seed-economy.mjs: fetchNatGasStorage() in Promise.allSettled, writes
  economic:nat-gas-storage:v1 with 21-day TTL (3x weekly cadence)
- server handler: getNatGasStorage reads seeded key from Redis
- gateway: /api/economic/v1/get-nat-gas-storage → static tier
- health.js: BOOTSTRAP_KEYS + SEED_META (14-day maxStaleMin)
- bootstrap.js: KEYS + SLOW_KEYS
- cache-keys.ts: BOOTSTRAP_CACHE_KEYS + BOOTSTRAP_TIERS (slow)

* feat(feeds): add fetchNatGasStorageRpc consumer in economic service

Adds getHydratedData('natGasStorage') consumer required by bootstrap
key registry test, plus circuit breaker and RPC wrapper mirroring
fetchCrudeInventoriesRpc pattern.
This commit is contained in:
Elie Habib
2026-03-27 11:48:44 +04:00
committed by GitHub
parent 2b03256677
commit 9480b547d5
14 changed files with 309 additions and 2 deletions

2
api/bootstrap.js vendored
View File

@@ -71,6 +71,7 @@ const BOOTSTRAP_CACHE_KEYS = {
marketImplications: 'intelligence:market-implications:v1',
fearGreedIndex: 'market:fear-greed:v1',
crudeInventories: 'economic:crude-inventories:v1',
natGasStorage: 'economic:nat-gas-storage:v1',
ecbFxRates: 'economic:ecb-fx-rates:v1',
euFsi: 'economic:fsi-eu:v1',
};
@@ -96,6 +97,7 @@ const SLOW_KEYS = new Set([
'marketImplications',
'fearGreedIndex',
'crudeInventories',
'natGasStorage',
'ecbFxRates',
'euFsi',
]);

View File

@@ -64,6 +64,7 @@ const BOOTSTRAP_KEYS = {
econCalendar: 'economic:econ-calendar:v1',
cotPositioning: 'market:cot:v1',
crudeInventories: 'economic:crude-inventories:v1',
natGasStorage: 'economic:nat-gas-storage:v1',
ecbFxRates: 'economic:ecb-fx-rates:v1',
eurostatCountryData: 'economic:eurostat-country-data:v1',
euGasStorage: 'economic:eu-gas-storage:v1',
@@ -201,6 +202,7 @@ const SEED_META = {
econCalendar: { key: 'seed-meta:economic:econ-calendar', maxStaleMin: 1440 }, // 12h cron; 1440min = 24h = 2x interval
cotPositioning: { key: 'seed-meta:market:cot', maxStaleMin: 14400 }, // weekly CFTC release; 14400min = 10d = 1.4x interval (weekend + delay buffer)
crudeInventories: { key: 'seed-meta:economic:crude-inventories', maxStaleMin: 20160 }, // weekly EIA data; 20160min = 14 days = 2x weekly cadence
natGasStorage: { key: 'seed-meta:economic:nat-gas-storage', maxStaleMin: 20160 }, // weekly EIA data; 20160min = 14 days = 2x weekly cadence
ecbFxRates: { key: 'seed-meta:economic:ecb-fx-rates', maxStaleMin: 2880 }, // daily seed; 2880min = 48h = 2x interval
eurostatCountryData: { key: 'seed-meta:economic:eurostat-country-data', maxStaleMin: 4320 }, // daily seed; 4320min = 3 days = 3x interval
euGasStorage: { key: 'seed-meta:economic:eu-gas-storage', maxStaleMin: 2880 }, // daily seed (T+1); 2880min = 48h = 2x interval

File diff suppressed because one or more lines are too long

View File

@@ -520,6 +520,32 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/economic/v1/get-nat-gas-storage:
get:
tags:
- EconomicService
summary: GetNatGasStorage
description: GetNatGasStorage retrieves the 8 most recent weeks of US natural gas working gas storage from EIA (NW2_EPG0_SWO_R48_BCF).
operationId: GetNatGasStorage
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetNatGasStorageResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/economic/v1/get-ecb-fx-rates:
get:
tags:
@@ -1586,6 +1612,37 @@ components:
Week-over-week change in millions of barrels. Positive = build (bearish), negative = draw (bullish).
Absent for the oldest week when no prior week is available for comparison.
description: CrudeInventoryWeek represents one week of US crude oil stockpile data from EIA WCRSTUS1.
GetNatGasStorageRequest:
type: object
description: GetNatGasStorageRequest is the request message for GetNatGasStorage.
GetNatGasStorageResponse:
type: object
properties:
weeks:
type: array
items:
$ref: '#/components/schemas/NatGasStorageWeek'
latestPeriod:
type: string
description: Timestamp of the most recent EIA data point (ISO 8601).
description: GetNatGasStorageResponse contains the 8 most recent weeks of US natural gas storage data.
NatGasStorageWeek:
type: object
properties:
period:
type: string
description: ISO week period (YYYY-MM-DD, Monday of the EIA report week).
storBcf:
type: number
format: double
description: Working gas in underground storage, Lower-48 States, in Bcf.
weeklyChangeBcf:
type: number
format: double
description: |-
Week-over-week change in Bcf. Positive = build (bearish for gas prices), negative = draw (bullish).
Absent for the oldest week when no prior week is available for comparison.
description: NatGasStorageWeek represents one week of US natural gas working gas storage data from EIA.
GetEcbFxRatesRequest:
type: object
description: GetEcbFxRatesRequest is empty; returns all tracked EUR pairs.

View File

@@ -0,0 +1,27 @@
syntax = "proto3";
package worldmonitor.economic.v1;
import "sebuf/http/annotations.proto";
// NatGasStorageWeek represents one week of US natural gas working gas storage data from EIA.
message NatGasStorageWeek {
// ISO week period (YYYY-MM-DD, Monday of the EIA report week).
string period = 1;
// Working gas in underground storage, Lower-48 States, in Bcf.
double stor_bcf = 2;
// Week-over-week change in Bcf. Positive = build (bearish for gas prices), negative = draw (bullish).
// Absent for the oldest week when no prior week is available for comparison.
optional double weekly_change_bcf = 3;
}
// GetNatGasStorageRequest is the request message for GetNatGasStorage.
message GetNatGasStorageRequest {}
// GetNatGasStorageResponse contains the 8 most recent weeks of US natural gas storage data.
message GetNatGasStorageResponse {
// The 8 most recent weeks, sorted newest-first.
repeated NatGasStorageWeek weeks = 1;
// Timestamp of the most recent EIA data point (ISO 8601).
string latest_period = 2;
}

View File

@@ -19,6 +19,7 @@ import "worldmonitor/economic/v1/list_fuel_prices.proto";
import "worldmonitor/economic/v1/get_bls_series.proto";
import "worldmonitor/economic/v1/get_economic_calendar.proto";
import "worldmonitor/economic/v1/get_crude_inventories.proto";
import "worldmonitor/economic/v1/get_nat_gas_storage.proto";
import "worldmonitor/economic/v1/get_ecb_fx_rates.proto";
import "worldmonitor/economic/v1/get_eurostat_country_data.proto";
import "worldmonitor/economic/v1/get_eu_gas_storage.proto";
@@ -109,6 +110,11 @@ service EconomicService {
option (sebuf.http.config) = {path: "/get-crude-inventories", method: HTTP_METHOD_GET};
}
// GetNatGasStorage retrieves the 8 most recent weeks of US natural gas working gas storage from EIA (NW2_EPG0_SWO_R48_BCF).
rpc GetNatGasStorage(GetNatGasStorageRequest) returns (GetNatGasStorageResponse) {
option (sebuf.http.config) = {path: "/get-nat-gas-storage", method: HTTP_METHOD_GET};
}
// GetEcbFxRates retrieves daily ECB official reference rates for EUR/major currency pairs.
rpc GetEcbFxRates(GetEcbFxRatesRequest) returns (GetEcbFxRatesResponse) {
option (sebuf.http.config) = {path: "/get-ecb-fx-rates", method: HTTP_METHOD_GET};

View File

@@ -12,6 +12,7 @@ const KEYS = {
energyCapacity: 'economic:capacity:v1:COL,SUN,WND:20',
macroSignals: 'economic:macro-signals:v1',
crudeInventories: 'economic:crude-inventories:v1',
natGasStorage: 'economic:nat-gas-storage:v1',
};
const FRED_KEY_PREFIX = 'economic:fred:v1';
@@ -21,6 +22,8 @@ const CAPACITY_TTL = 86400;
const MACRO_TTL = 21600; // 6h — survive extended Yahoo outages
const CRUDE_INVENTORIES_TTL = 1_814_400; // 21 days — EIA publishes weekly; 3x cadence per gold standard
const CRUDE_MIN_WEEKS = 4; // require at least 4 weeks to guard against quota-hit empty responses
const NAT_GAS_TTL = 1_814_400; // 21 days — EIA publishes weekly; 3x cadence per gold standard
const NAT_GAS_MIN_WEEKS = 4; // require at least 4 weeks to guard against quota-hit empty responses
const FRED_SERIES = ['WALCL', 'FEDFUNDS', 'T10Y2Y', 'UNRATE', 'CPIAUCSL', 'DGS10', 'VIXCLS', 'GDP', 'M2SL', 'DCOILWTICO', 'BAMLH0A0HYM2', 'ICSA', 'MORTGAGE30US', 'BAMLC0A0CM', 'SOFR', 'DGS1MO', 'DGS3MO', 'DGS6MO', 'DGS1', 'DGS2', 'DGS5', 'DGS30'];
@@ -456,17 +459,72 @@ async function fetchCrudeInventories() {
return { weeks, latestPeriod };
}
// ─── EIA Natural Gas Storage (NW2_EPG0_SWO_R48_BCF) ───
async function fetchNatGasStorage() {
const apiKey = process.env.EIA_API_KEY;
if (!apiKey) throw new Error('Missing EIA_API_KEY');
const params = new URLSearchParams({
api_key: apiKey,
'facets[series][]': 'NW2_EPG0_SWO_R48_BCF',
frequency: 'weekly',
'data[]': 'value',
'sort[0][column]': 'period',
'sort[0][direction]': 'desc',
length: '9', // fetch 9 so the oldest of 8 has a prior week for weeklyChangeBcf
});
const resp = await fetch(`https://api.eia.gov/v2/natural-gas/stor/wkly/data/?${params}`, {
headers: { Accept: 'application/json', 'User-Agent': CHROME_UA },
signal: AbortSignal.timeout(10_000),
});
if (!resp.ok) throw new Error(`EIA NW2_EPG0_SWO_R48_BCF: HTTP ${resp.status}`);
const data = await resp.json();
const rows = data.response?.data;
if (!rows || rows.length === 0) throw new Error('EIA NW2_EPG0_SWO_R48_BCF: no data rows');
// rows are sorted newest-first; compute weeklyChangeBcf for each week vs. next (older)
const weeks = [];
for (let i = 0; i < Math.min(rows.length, 9); i++) {
const row = rows[i];
const storBcf = row.value != null ? parseFloat(String(row.value)) : null;
if (storBcf == null || !Number.isFinite(storBcf)) continue;
const period = typeof row.period === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(row.period) ? row.period : '';
const olderRow = rows[i + 1];
let weeklyChangeBcf = null;
if (olderRow?.value != null) {
const olderStor = parseFloat(String(olderRow.value));
if (Number.isFinite(olderStor)) weeklyChangeBcf = +(storBcf - olderStor).toFixed(3);
}
weeks.push({
period,
storBcf: +storBcf.toFixed(3),
weeklyChangeBcf,
});
if (weeks.length === 8) break; // only return 8 weeks to client
}
if (weeks.length < NAT_GAS_MIN_WEEKS) throw new Error(`EIA NW2_EPG0_SWO_R48_BCF: only ${weeks.length} valid rows (need >= ${NAT_GAS_MIN_WEEKS})`);
const latestPeriod = weeks[0]?.period ?? '';
console.log(` Nat gas storage: ${weeks.length} weeks, latest=${latestPeriod}`);
return { weeks, latestPeriod };
}
// ─── Main: seed all economic data ───
// NOTE: runSeed() calls process.exit(0) after writing the primary key.
// All secondary keys MUST be written inside fetchAll() before returning.
async function fetchAll() {
const [energyPrices, energyCapacity, fredResults, macroSignals, crudeInventories] = await Promise.allSettled([
const [energyPrices, energyCapacity, fredResults, macroSignals, crudeInventories, natGasStorage] = await Promise.allSettled([
fetchEnergyPrices(),
fetchEnergyCapacity(),
fetchFredSeries(),
fetchMacroSignals(),
fetchCrudeInventories(),
fetchNatGasStorage(),
]);
const ep = energyPrices.status === 'fulfilled' ? energyPrices.value : null;
@@ -474,12 +532,14 @@ async function fetchAll() {
const fr = fredResults.status === 'fulfilled' ? fredResults.value : null;
const ms = macroSignals.status === 'fulfilled' ? macroSignals.value : null;
const ci = crudeInventories.status === 'fulfilled' ? crudeInventories.value : null;
const ng = natGasStorage.status === 'fulfilled' ? natGasStorage.value : null;
if (energyPrices.status === 'rejected') console.warn(` EnergyPrices failed: ${energyPrices.reason?.message || energyPrices.reason}`);
if (energyCapacity.status === 'rejected') console.warn(` EnergyCapacity failed: ${energyCapacity.reason?.message || energyCapacity.reason}`);
if (fredResults.status === 'rejected') console.warn(` FRED failed: ${fredResults.reason?.message || fredResults.reason}`);
if (macroSignals.status === 'rejected') console.warn(` MacroSignals failed: ${macroSignals.reason?.message || macroSignals.reason}`);
if (crudeInventories.status === 'rejected') console.warn(` CrudeInventories failed: ${crudeInventories.reason?.message || crudeInventories.reason}`);
if (natGasStorage.status === 'rejected') console.warn(` NatGasStorage failed: ${natGasStorage.reason?.message || natGasStorage.reason}`);
if (!ep && !fr && !ms) throw new Error('All economic fetches failed');
@@ -501,6 +561,13 @@ async function fetchAll() {
console.warn(` CrudeInventories: skipped write — ${ci.weeks?.length ?? 0} weeks or schema invalid`);
}
const isValidNgWeek = (w) => typeof w.period === 'string' && typeof w.storBcf === 'number' && Number.isFinite(w.storBcf);
if (ng?.weeks?.length >= NAT_GAS_MIN_WEEKS && ng.weeks.every(isValidNgWeek)) {
await writeExtraKeyWithMeta(KEYS.natGasStorage, ng, NAT_GAS_TTL, ng.weeks.length);
} else if (ng) {
console.warn(` NatGasStorage: skipped write — ${ng.weeks?.length ?? 0} weeks or schema invalid`);
}
return ep || { prices: [] };
}

View File

@@ -72,6 +72,7 @@ export const BOOTSTRAP_CACHE_KEYS: Record<string, string> = {
marketImplications: 'intelligence:market-implications:v1',
fearGreedIndex: 'market:fear-greed:v1',
crudeInventories: 'economic:crude-inventories:v1',
natGasStorage: 'economic:nat-gas-storage:v1',
ecbFxRates: 'economic:ecb-fx-rates:v1',
euGasStorage: 'economic:eu-gas-storage:v1',
euFsi: 'economic:fsi-eu:v1',
@@ -108,6 +109,7 @@ export const BOOTSTRAP_TIERS: Record<string, 'slow' | 'fast'> = {
marketImplications: 'slow',
fearGreedIndex: 'slow',
crudeInventories: 'slow',
natGasStorage: 'slow',
ecbFxRates: 'slow',
euGasStorage: 'slow',
euFsi: 'slow',

View File

@@ -132,6 +132,7 @@ const RPC_CACHE_TIER: Record<string, CacheTier> = {
'/api/economic/v1/list-bigmac-prices': 'static',
'/api/economic/v1/list-fuel-prices': 'static',
'/api/economic/v1/get-crude-inventories': 'static',
'/api/economic/v1/get-nat-gas-storage': 'static',
'/api/economic/v1/get-eu-yield-curve': 'daily',
'/api/supply-chain/v1/get-critical-minerals': 'daily',
'/api/military/v1/get-aircraft-details': 'static',

View File

@@ -0,0 +1,29 @@
/**
* RPC: getNatGasStorage -- reads seeded EIA NW2_EPG0_SWO_R48_BCF natural gas storage data.
* All external EIA API calls happen in seed-economy.mjs on Railway.
*/
import type {
ServerContext,
GetNatGasStorageRequest,
GetNatGasStorageResponse,
} from '../../../../src/generated/server/worldmonitor/economic/v1/service_server';
import { getCachedJson } from '../../../_shared/redis';
const SEED_CACHE_KEY = 'economic:nat-gas-storage:v1';
export async function getNatGasStorage(
_ctx: ServerContext,
_req: GetNatGasStorageRequest,
): Promise<GetNatGasStorageResponse> {
try {
// true = raw key: seed scripts write without Vercel env prefix
const result = await getCachedJson(SEED_CACHE_KEY, true) as GetNatGasStorageResponse | null;
if (!result?.weeks?.length) return { weeks: [], latestPeriod: '' };
return result;
} catch (err) {
console.error('[getNatGasStorage] Redis read failed:', err);
return { weeks: [], latestPeriod: '' };
}
}

View File

@@ -16,6 +16,7 @@ import { listFuelPrices } from './list-fuel-prices';
import { getBlsSeries } from './get-bls-series';
import { getEconomicCalendar } from './get-economic-calendar';
import { getCrudeInventories } from './get-crude-inventories';
import { getNatGasStorage } from './get-nat-gas-storage';
import { getEcbFxRates } from './get-ecb-fx-rates';
import { getEurostatCountryData } from './get-eurostat-country-data';
import { getEuGasStorage } from './get-eu-gas-storage';
@@ -39,6 +40,7 @@ export const economicHandler: EconomicServiceHandler = {
getBlsSeries,
getEconomicCalendar,
getCrudeInventories,
getNatGasStorage,
getEcbFxRates,
getEurostatCountryData,
getEuGasStorage,

View File

@@ -401,6 +401,20 @@ export interface CrudeInventoryWeek {
weeklyChangeMb?: number;
}
export interface GetNatGasStorageRequest {
}
export interface GetNatGasStorageResponse {
weeks: NatGasStorageWeek[];
latestPeriod: string;
}
export interface NatGasStorageWeek {
period: string;
storBcf: number;
weeklyChangeBcf?: number;
}
export interface GetEcbFxRatesRequest {
}
@@ -928,6 +942,29 @@ export class EconomicServiceClient {
return await resp.json() as GetCrudeInventoriesResponse;
}
async getNatGasStorage(req: GetNatGasStorageRequest, options?: EconomicServiceCallOptions): Promise<GetNatGasStorageResponse> {
let path = "/api/economic/v1/get-nat-gas-storage";
const url = this.baseURL + path;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...this.defaultHeaders,
...options?.headers,
};
const resp = await this.fetchFn(url, {
method: "GET",
headers,
signal: options?.signal,
});
if (!resp.ok) {
return this.handleError(resp);
}
return await resp.json() as GetNatGasStorageResponse;
}
async getEcbFxRates(req: GetEcbFxRatesRequest, options?: EconomicServiceCallOptions): Promise<GetEcbFxRatesResponse> {
let path = "/api/economic/v1/get-ecb-fx-rates";
const url = this.baseURL + path;

View File

@@ -401,6 +401,20 @@ export interface CrudeInventoryWeek {
weeklyChangeMb?: number;
}
export interface GetNatGasStorageRequest {
}
export interface GetNatGasStorageResponse {
weeks: NatGasStorageWeek[];
latestPeriod: string;
}
export interface NatGasStorageWeek {
period: string;
storBcf: number;
weeklyChangeBcf?: number;
}
export interface GetEcbFxRatesRequest {
}
@@ -552,6 +566,7 @@ export interface EconomicServiceHandler {
getBlsSeries(ctx: ServerContext, req: GetBlsSeriesRequest): Promise<GetBlsSeriesResponse>;
getEconomicCalendar(ctx: ServerContext, req: GetEconomicCalendarRequest): Promise<GetEconomicCalendarResponse>;
getCrudeInventories(ctx: ServerContext, req: GetCrudeInventoriesRequest): Promise<GetCrudeInventoriesResponse>;
getNatGasStorage(ctx: ServerContext, req: GetNatGasStorageRequest): Promise<GetNatGasStorageResponse>;
getEcbFxRates(ctx: ServerContext, req: GetEcbFxRatesRequest): Promise<GetEcbFxRatesResponse>;
getEurostatCountryData(ctx: ServerContext, req: GetEurostatCountryDataRequest): Promise<GetEurostatCountryDataResponse>;
getEuGasStorage(ctx: ServerContext, req: GetEuGasStorageRequest): Promise<GetEuGasStorageResponse>;
@@ -1230,6 +1245,43 @@ export function createEconomicServiceRoutes(
}
},
},
{
method: "GET",
path: "/api/economic/v1/get-nat-gas-storage",
handler: async (req: Request): Promise<Response> => {
try {
const pathParams: Record<string, string> = {};
const body = {} as GetNatGasStorageRequest;
const ctx: ServerContext = {
request: req,
pathParams,
headers: Object.fromEntries(req.headers.entries()),
};
const result = await handler.getNatGasStorage(ctx, body);
return new Response(JSON.stringify(result as GetNatGasStorageResponse), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (err: unknown) {
if (err instanceof ValidationError) {
return new Response(JSON.stringify({ violations: err.violations }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
if (options?.onError) {
return options.onError(err, req);
}
const message = err instanceof Error ? err.message : String(err);
return new Response(JSON.stringify({ message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
},
},
{
method: "GET",
path: "/api/economic/v1/get-ecb-fx-rates",

View File

@@ -29,6 +29,8 @@ import {
type GetBlsSeriesResponse,
type GetCrudeInventoriesResponse,
type CrudeInventoryWeek,
type GetNatGasStorageResponse,
type NatGasStorageWeek,
type GetEcbFxRatesResponse,
type EcbFxRate,
} from '@/generated/client/worldmonitor/economic/v1/service_client';
@@ -74,6 +76,8 @@ const emptyWbFallback: ListWorldBankIndicatorsResponse = { data: [], pagination:
const emptyEiaFallback: GetEnergyPricesResponse = { prices: [] };
const emptyCrudeFallback: GetCrudeInventoriesResponse = { weeks: [], latestPeriod: '' };
const crudeBreaker = createCircuitBreaker<GetCrudeInventoriesResponse>({ name: 'EIA Crude Inventories', cacheTtlMs: 60 * 60 * 1000, persistCache: true });
const emptyNatGasFallback: GetNatGasStorageResponse = { weeks: [], latestPeriod: '' };
const natGasBreaker = createCircuitBreaker<GetNatGasStorageResponse>({ name: 'EIA Nat Gas Storage', cacheTtlMs: 60 * 60 * 1000, persistCache: true });
const emptyCapacityFallback: GetEnergyCapacityResponse = { series: [] };
const emptyBisPolicyFallback: GetBisPolicyRatesResponse = { rates: [] };
const emptyBisEerFallback: GetBisExchangeRatesResponse = { rates: [] };
@@ -423,6 +427,25 @@ export async function fetchCrudeInventoriesRpc(): Promise<GetCrudeInventoriesRes
}
}
// ========================================================================
// EIA Natural Gas Storage (NW2_EPG0_SWO_R48_BCF) -- weekly storage data
// ========================================================================
export type { NatGasStorageWeek };
export async function fetchNatGasStorageRpc(): Promise<GetNatGasStorageResponse> {
if (!isFeatureAvailable('energyEia')) return emptyNatGasFallback;
const hydrated = getHydratedData('natGasStorage') as GetNatGasStorageResponse | undefined;
if (hydrated?.weeks?.length) return hydrated;
try {
return await natGasBreaker.execute(async () => {
return client.getNatGasStorage({}, { signal: AbortSignal.timeout(20_000) });
}, emptyNatGasFallback);
} catch {
return emptyNatGasFallback;
}
}
// ========================================================================
// EIA Capacity -- installed generation capacity (solar, wind, coal)
// ========================================================================