mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
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:
2
api/bootstrap.js
vendored
2
api/bootstrap.js
vendored
@@ -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',
|
||||
]);
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
27
proto/worldmonitor/economic/v1/get_nat_gas_storage.proto
Normal file
27
proto/worldmonitor/economic/v1/get_nat_gas_storage.proto
Normal 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;
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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: [] };
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
29
server/worldmonitor/economic/v1/get-nat-gas-storage.ts
Normal file
29
server/worldmonitor/economic/v1/get-nat-gas-storage.ts
Normal 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: '' };
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
// ========================================================================
|
||||
|
||||
Reference in New Issue
Block a user