diff --git a/api/bootstrap.js b/api/bootstrap.js index e040fd6ba..aa561a96e 100644 --- a/api/bootstrap.js +++ b/api/bootstrap.js @@ -71,6 +71,7 @@ const BOOTSTRAP_CACHE_KEYS = { groceryBasket: 'economic:grocery-basket:v1', bigmac: 'economic:bigmac:v1', fuelPrices: 'economic:fuel-prices:v1', + faoFoodPriceIndex: 'economic:fao-ffpi:v1', nationalDebt: 'economic:national-debt:v1', euGasStorage: 'economic:eu-gas-storage:v1', eurostatCountryData: 'economic:eurostat-country-data:v1', @@ -103,6 +104,7 @@ const SLOW_KEYS = new Set([ 'groceryBasket', 'bigmac', 'fuelPrices', + 'faoFoodPriceIndex', 'nationalDebt', 'euGasStorage', 'eurostatCountryData', diff --git a/api/health.js b/api/health.js index 53d8c4abb..088076d92 100644 --- a/api/health.js +++ b/api/health.js @@ -54,6 +54,7 @@ const BOOTSTRAP_KEYS = { groceryBasket: 'economic:grocery-basket:v1', bigmac: 'economic:bigmac:v1', fuelPrices: 'economic:fuel-prices:v1', + faoFoodPriceIndex: 'economic:fao-ffpi:v1', nationalDebt: 'economic:national-debt:v1', defiTokens: 'market:defi-tokens:v1', aiTokens: 'market:ai-tokens:v1', @@ -198,6 +199,7 @@ const SEED_META = { groceryBasket: { key: 'seed-meta:economic:grocery-basket', maxStaleMin: 10080 }, // weekly seed; 10080 = 7 days bigmac: { key: 'seed-meta:economic:bigmac', maxStaleMin: 10080 }, // weekly seed; 10080 = 7 days fuelPrices: { key: 'seed-meta:economic:fuel-prices', maxStaleMin: 10080 }, // weekly seed; 10080 = 7 days + faoFoodPriceIndex: { key: 'seed-meta:economic:fao-ffpi', maxStaleMin: 86400 }, // monthly seed; 86400 = 60 days (2x interval) thermalEscalation: { key: 'seed-meta:thermal:escalation', maxStaleMin: 360 }, // cron every 2h; 360 = 3x interval (was 240 = 2x) nationalDebt: { key: 'seed-meta:economic:national-debt', maxStaleMin: 10080 }, // 7 days — monthly seed tariffTrendsUs: { key: 'seed-meta:trade:tariffs:v1:840:all:10', maxStaleMin: 900 }, diff --git a/docs/api/EconomicService.openapi.json b/docs/api/EconomicService.openapi.json index 62d82eda7..7a5b96e09 100644 --- a/docs/api/EconomicService.openapi.json +++ b/docs/api/EconomicService.openapi.json @@ -1 +1 @@ -{"components":{"schemas":{"BigMacCountryPrice":{"properties":{"available":{"type":"boolean"},"code":{"type":"string"},"currency":{"type":"string"},"flag":{"type":"string"},"fxRate":{"format":"double","type":"number"},"localPrice":{"format":"double","type":"number"},"name":{"type":"string"},"sourceSite":{"type":"string"},"usdPrice":{"format":"double","type":"number"},"wowPct":{"format":"double","type":"number"}},"type":"object"},"BisCreditToGdp":{"description":"BisCreditToGdp represents total credit as percentage of GDP from BIS.","properties":{"countryCode":{"description":"ISO 2-letter country code.","type":"string"},"countryName":{"description":"Country or region name.","type":"string"},"creditGdpRatio":{"description":"Total credit as percentage of GDP.","format":"double","type":"number"},"date":{"description":"Date as YYYY-QN.","type":"string"},"previousRatio":{"description":"Previous quarter ratio.","format":"double","type":"number"}},"type":"object"},"BisExchangeRate":{"description":"BisExchangeRate represents effective exchange rate indices from BIS.","properties":{"countryCode":{"description":"ISO 2-letter country code.","type":"string"},"countryName":{"description":"Country or region name.","type":"string"},"date":{"description":"Date as YYYY-MM.","type":"string"},"nominalEer":{"description":"Nominal effective exchange rate index.","format":"double","type":"number"},"realChange":{"description":"Percentage change from previous period (real).","format":"double","type":"number"},"realEer":{"description":"Real effective exchange rate index.","format":"double","type":"number"}},"type":"object"},"BisPolicyRate":{"description":"BisPolicyRate represents a central bank policy rate from BIS.","properties":{"centralBank":{"description":"Central bank name (e.g. \"Federal Reserve\").","type":"string"},"countryCode":{"description":"ISO 2-letter country code (US, GB, JP, etc.)","type":"string"},"countryName":{"description":"Country or region name.","type":"string"},"date":{"description":"Date as YYYY-MM.","type":"string"},"previousRate":{"description":"Previous period rate percentage.","format":"double","type":"number"},"rate":{"description":"Current policy rate percentage.","format":"double","type":"number"}},"type":"object"},"BlsObservation":{"description":"BlsObservation is a single BLS data point.","properties":{"period":{"description":"Period code (e.g. \"M01\" for January, \"A01\" for annual).","type":"string"},"periodName":{"description":"Human-readable period name.","type":"string"},"value":{"description":"Observed value.","type":"string"},"year":{"description":"Year of the observation.","type":"string"}},"type":"object"},"BlsSeries":{"description":"BlsSeries is a BLS time series with metadata and observations.","properties":{"observations":{"items":{"$ref":"#/components/schemas/BlsObservation"},"type":"array"},"seriesId":{"description":"BLS series ID (e.g. \"CES0500000001\").","type":"string"},"title":{"description":"Human-readable series title.","type":"string"},"units":{"description":"Unit of measure.","type":"string"}},"type":"object"},"CountriesEntry":{"properties":{"key":{"type":"string"},"value":{"$ref":"#/components/schemas/EurostatCountryEntry"}},"type":"object"},"CountryBasket":{"properties":{"code":{"type":"string"},"currency":{"type":"string"},"flag":{"type":"string"},"fxRate":{"format":"double","type":"number"},"items":{"items":{"$ref":"#/components/schemas/GroceryItemPrice"},"type":"array"},"name":{"type":"string"},"totalUsd":{"format":"double","type":"number"},"wowPct":{"format":"double","type":"number"}},"type":"object"},"CrudeInventoryWeek":{"description":"CrudeInventoryWeek represents one week of US crude oil stockpile data from EIA WCRSTUS1.","properties":{"period":{"description":"ISO week period (YYYY-MM-DD, Monday of the EIA report week).","type":"string"},"stocksMb":{"description":"Total crude oil stocks in millions of barrels.","format":"double","type":"number"},"weeklyChangeMb":{"description":"Week-over-week change in millions of barrels. Positive = build (bearish), negative = draw (bullish).\n Absent for the oldest week when no prior week is available for comparison.","format":"double","type":"number"}},"type":"object"},"EcbFxRate":{"description":"EcbFxRate is a single ECB official reference rate for a currency pair.","properties":{"change1d":{"description":"1-day change in rate (absolute).","format":"double","type":"number"},"date":{"description":"Date of the observation in YYYY-MM-DD format.","type":"string"},"pair":{"description":"Currency pair label, e.g. \"EURUSD\".","type":"string"},"rate":{"description":"Exchange rate (units of quote currency per 1 EUR).","format":"double","type":"number"}},"type":"object"},"EconomicEvent":{"properties":{"actual":{"type":"string"},"country":{"type":"string"},"date":{"type":"string"},"estimate":{"type":"string"},"event":{"type":"string"},"impact":{"type":"string"},"previous":{"type":"string"},"unit":{"type":"string"}},"type":"object"},"EconomicStressComponent":{"properties":{"id":{"type":"string"},"label":{"type":"string"},"missing":{"type":"boolean"},"rawValue":{"format":"double","type":"number"},"score":{"format":"double","type":"number"},"weight":{"format":"double","type":"number"}},"type":"object"},"EnergyCapacitySeries":{"properties":{"data":{"items":{"$ref":"#/components/schemas/EnergyCapacityYear"},"type":"array"},"energySource":{"type":"string"},"name":{"type":"string"}},"type":"object"},"EnergyCapacityYear":{"properties":{"capacityMw":{"format":"double","type":"number"},"year":{"format":"int32","type":"integer"}},"type":"object"},"EnergyPrice":{"description":"EnergyPrice represents a current energy commodity price from EIA.","properties":{"change":{"description":"Percentage change from previous period.","format":"double","type":"number"},"commodity":{"description":"Energy commodity identifier.","minLength":1,"type":"string"},"name":{"description":"Human-readable name (e.g., \"WTI Crude Oil\", \"Henry Hub Natural Gas\").","type":"string"},"price":{"description":"Current price in USD.","format":"double","type":"number"},"priceAt":{"description":"Price date, as Unix epoch milliseconds.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"unit":{"description":"Unit of measurement (e.g., \"$/barrel\", \"$/MMBtu\").","type":"string"}},"required":["commodity"],"type":"object"},"Error":{"description":"Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.","properties":{"message":{"description":"Error message (e.g., 'user not found', 'database connection failed')","type":"string"}},"type":"object"},"EuFsiObservation":{"properties":{"date":{"type":"string"},"value":{"format":"double","type":"number"}},"type":"object"},"EuGasStorageHistoryEntry":{"description":"EuGasStorageHistoryEntry represents one day of EU aggregate gas storage data.","properties":{"date":{"description":"Calendar date (YYYY-MM-DD).","type":"string"},"fillPct":{"description":"Storage fill level as a percentage of working gas volume capacity.","format":"double","type":"number"},"gasTwh":{"description":"Working gas volume in storage (TWh).","format":"double","type":"number"}},"type":"object"},"EuYieldCurveData":{"description":"EuYieldCurveData holds a single observation of the ECB Euro Area AAA yield curve.","properties":{"date":{"description":"Date of the observation (YYYY-MM or YYYY-MM-DD).","type":"string"},"rates":{"additionalProperties":{"format":"double","type":"number"},"description":"Spot rates by tenor. Keys: \"1Y\", \"2Y\", \"5Y\", \"10Y\", \"20Y\", \"30Y\".","type":"object"},"source":{"description":"Data source identifier (e.g. \"ecb-aaa\").","type":"string"},"updatedAt":{"description":"ISO 8601 timestamp when this was last seeded.","type":"string"}},"type":"object"},"EurostatCountryEntry":{"description":"EurostatCountryEntry holds all available metrics for one EU country.","properties":{"cpi":{"$ref":"#/components/schemas/EurostatMetric"},"gdpGrowth":{"$ref":"#/components/schemas/EurostatMetric"},"unemployment":{"$ref":"#/components/schemas/EurostatMetric"}},"type":"object"},"EurostatMetric":{"description":"EurostatMetric holds a single economic metric value for a country.","properties":{"date":{"description":"Period string (e.g. \"2024-01\" for monthly, \"2024-Q1\" for quarterly).","type":"string"},"hasPrior":{"description":"True when prior_value is present (proto3 can't distinguish 0 from absent).","type":"boolean"},"priorValue":{"description":"Prior period value for delta calculation (e.g. previous month/quarter).","format":"double","type":"number"},"unit":{"description":"Unit of measurement (e.g. \"%\").","type":"string"},"value":{"description":"Numeric value (e.g. 2.3 for 2.3%).","format":"double","type":"number"}},"type":"object"},"FearGreedHistoryEntry":{"description":"FearGreedHistoryEntry is a single day's Fear \u0026 Greed index reading.","properties":{"date":{"description":"Date string (YYYY-MM-DD).","type":"string"},"value":{"description":"Index value (0-100).","format":"int32","maximum":100,"minimum":0,"type":"integer"}},"type":"object"},"FearGreedSignal":{"description":"FearGreedSignal tracks the Crypto Fear \u0026 Greed index.","properties":{"history":{"items":{"$ref":"#/components/schemas/FearGreedHistoryEntry"},"type":"array"},"status":{"description":"Classification label (e.g., \"Extreme Fear\", \"Greed\").","type":"string"},"value":{"description":"Current index value (0-100).","format":"int32","type":"integer"}},"type":"object"},"FieldViolation":{"description":"FieldViolation describes a single validation error for a specific field.","properties":{"description":{"description":"Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')","type":"string"},"field":{"description":"The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')","type":"string"}},"required":["field","description"],"type":"object"},"FlowStructureSignal":{"description":"FlowStructureSignal compares BTC vs QQQ 5-day returns.","properties":{"btcReturn5":{"description":"BTC 5-day return percentage.","format":"double","type":"number"},"qqqReturn5":{"description":"QQQ 5-day return percentage.","format":"double","type":"number"},"status":{"description":"\"PASSIVE GAP\", \"ALIGNED\", or \"UNKNOWN\".","type":"string"}},"type":"object"},"FredObservation":{"description":"FredObservation represents a single data point from a FRED economic series.","properties":{"date":{"description":"Observation date as YYYY-MM-DD string.","type":"string"},"value":{"description":"Observation value.","format":"double","type":"number"}},"type":"object"},"FredSeries":{"description":"FredSeries represents a FRED time series with metadata.","properties":{"frequency":{"description":"Data frequency (e.g., \"Monthly\", \"Quarterly\").","type":"string"},"observations":{"items":{"$ref":"#/components/schemas/FredObservation"},"type":"array"},"seriesId":{"description":"Series identifier (e.g., \"GDP\", \"UNRATE\", \"CPIAUCSL\").","minLength":1,"type":"string"},"title":{"description":"Series title.","type":"string"},"units":{"description":"Unit of measurement.","type":"string"}},"required":["seriesId"],"type":"object"},"FuelCountryPrice":{"properties":{"code":{"type":"string"},"currency":{"type":"string"},"diesel":{"$ref":"#/components/schemas/FuelPrice"},"flag":{"type":"string"},"fxRate":{"format":"double","type":"number"},"gasoline":{"$ref":"#/components/schemas/FuelPrice"},"name":{"type":"string"}},"type":"object"},"FuelPrice":{"properties":{"available":{"type":"boolean"},"grade":{"type":"string"},"localPrice":{"format":"double","type":"number"},"observedAt":{"type":"string"},"source":{"type":"string"},"usdPrice":{"format":"double","type":"number"},"wowPct":{"format":"double","type":"number"}},"type":"object"},"GetBisCreditRequest":{"description":"GetBisCreditRequest requests credit-to-GDP ratio data.","type":"object"},"GetBisCreditResponse":{"description":"GetBisCreditResponse contains BIS credit-to-GDP data.","properties":{"entries":{"items":{"$ref":"#/components/schemas/BisCreditToGdp"},"type":"array"}},"type":"object"},"GetBisExchangeRatesRequest":{"description":"GetBisExchangeRatesRequest requests effective exchange rates.","type":"object"},"GetBisExchangeRatesResponse":{"description":"GetBisExchangeRatesResponse contains BIS effective exchange rate data.","properties":{"rates":{"items":{"$ref":"#/components/schemas/BisExchangeRate"},"type":"array"}},"type":"object"},"GetBisPolicyRatesRequest":{"description":"GetBisPolicyRatesRequest requests central bank policy rates.","type":"object"},"GetBisPolicyRatesResponse":{"description":"GetBisPolicyRatesResponse contains BIS policy rate data.","properties":{"rates":{"items":{"$ref":"#/components/schemas/BisPolicyRate"},"type":"array"}},"type":"object"},"GetBlsSeriesRequest":{"description":"GetBlsSeriesRequest specifies which BLS series to retrieve.","properties":{"limit":{"description":"Maximum number of observations to return. Defaults to 60.","format":"int32","type":"integer"},"seriesId":{"description":"BLS series ID (e.g. \"CES0500000001\", \"CIU1010000000000A\").","type":"string"}},"type":"object"},"GetBlsSeriesResponse":{"description":"GetBlsSeriesResponse contains the requested BLS series data.","properties":{"series":{"$ref":"#/components/schemas/BlsSeries"}},"type":"object"},"GetCrudeInventoriesRequest":{"description":"GetCrudeInventoriesRequest is the request message for GetCrudeInventories.","type":"object"},"GetCrudeInventoriesResponse":{"description":"GetCrudeInventoriesResponse contains the 8 most recent weeks of US crude oil inventory data.","properties":{"latestPeriod":{"description":"Timestamp of the most recent EIA data point (ISO 8601).","type":"string"},"weeks":{"items":{"$ref":"#/components/schemas/CrudeInventoryWeek"},"type":"array"}},"type":"object"},"GetEcbFxRatesRequest":{"description":"GetEcbFxRatesRequest is empty; returns all tracked EUR pairs.","type":"object"},"GetEcbFxRatesResponse":{"description":"GetEcbFxRatesResponse contains the latest ECB reference rates.","properties":{"rates":{"items":{"$ref":"#/components/schemas/EcbFxRate"},"type":"array"},"seededAt":{"description":"Unix ms when the data was last seeded.","format":"int64","type":"string"},"unavailable":{"description":"True when Redis key is missing or data is unavailable.","type":"boolean"},"updatedAt":{"description":"ISO 8601 timestamp of the ECB publication.","type":"string"}},"type":"object"},"GetEconomicCalendarRequest":{"properties":{"fromDate":{"type":"string"},"toDate":{"type":"string"}},"type":"object"},"GetEconomicCalendarResponse":{"properties":{"events":{"items":{"$ref":"#/components/schemas/EconomicEvent"},"type":"array"},"fromDate":{"type":"string"},"toDate":{"type":"string"},"total":{"format":"int32","type":"integer"},"unavailable":{"type":"boolean"}},"type":"object"},"GetEconomicStressRequest":{"type":"object"},"GetEconomicStressResponse":{"properties":{"components":{"items":{"$ref":"#/components/schemas/EconomicStressComponent"},"type":"array"},"compositeScore":{"format":"double","type":"number"},"label":{"type":"string"},"seededAt":{"type":"string"},"unavailable":{"type":"boolean"}},"type":"object"},"GetEnergyCapacityRequest":{"properties":{"energySources":{"items":{"description":"Energy source codes to query (e.g., \"SUN\", \"WND\", \"COL\").\n Empty returns all tracked sources (SUN, WND, COL).","type":"string"},"type":"array"},"years":{"description":"Number of years of historical data. Default 20 if not set.","format":"int32","type":"integer"}},"type":"object"},"GetEnergyCapacityResponse":{"properties":{"series":{"items":{"$ref":"#/components/schemas/EnergyCapacitySeries"},"type":"array"}},"type":"object"},"GetEnergyPricesRequest":{"description":"GetEnergyPricesRequest specifies which energy commodities to retrieve.","properties":{"commodities":{"items":{"description":"Optional commodity filter. Empty returns all tracked commodities.","type":"string"},"type":"array"}},"type":"object"},"GetEnergyPricesResponse":{"description":"GetEnergyPricesResponse contains energy price data.","properties":{"prices":{"items":{"$ref":"#/components/schemas/EnergyPrice"},"type":"array"}},"type":"object"},"GetEuFsiRequest":{"type":"object"},"GetEuFsiResponse":{"properties":{"history":{"items":{"$ref":"#/components/schemas/EuFsiObservation"},"type":"array"},"label":{"type":"string"},"latestDate":{"type":"string"},"latestValue":{"format":"double","type":"number"},"seededAt":{"type":"string"},"unavailable":{"type":"boolean"}},"type":"object"},"GetEuGasStorageRequest":{"description":"GetEuGasStorageRequest is empty — returns latest EU aggregate snapshot.","type":"object"},"GetEuGasStorageResponse":{"description":"GetEuGasStorageResponse contains the EU aggregate gas storage snapshot.","properties":{"fillPct":{"description":"Current storage fill level as a percentage of working gas volume (0–100).","format":"double","type":"number"},"fillPctChange1d":{"description":"1-day change in fill percentage (positive = injecting, negative = withdrawing).","format":"double","type":"number"},"gasDaysConsumption":{"description":"Approximate days of consumption remaining at average EU winter drawdown rate.","format":"double","type":"number"},"history":{"items":{"$ref":"#/components/schemas/EuGasStorageHistoryEntry"},"type":"array"},"seededAt":{"description":"UTC ms timestamp when seed data was written.","format":"int64","type":"string"},"trend":{"description":"Current storage trend: \"injecting\", \"withdrawing\", or \"stable\".","type":"string"},"unavailable":{"description":"True when upstream data is unavailable (fallback result).","type":"boolean"},"updatedAt":{"description":"Calendar date of the most recent data point (YYYY-MM-DD).","type":"string"}},"type":"object"},"GetEuYieldCurveRequest":{"description":"GetEuYieldCurveRequest fetches the ECB Euro Area AAA sovereign yield curve.","type":"object"},"GetEuYieldCurveResponse":{"description":"GetEuYieldCurveResponse contains the latest ECB Euro Area AAA sovereign yield curve.","properties":{"data":{"$ref":"#/components/schemas/EuYieldCurveData"},"unavailable":{"description":"True if data is not yet available in cache.","type":"boolean"}},"type":"object"},"GetEurostatCountryDataRequest":{"description":"GetEurostatCountryDataRequest requests Eurostat per-country economic data.","type":"object"},"GetEurostatCountryDataResponse":{"description":"GetEurostatCountryDataResponse contains per-country CPI, unemployment, and GDP growth.","properties":{"countries":{"additionalProperties":{"$ref":"#/components/schemas/EurostatCountryEntry"},"description":"Map of ISO2 country code to economic metrics (e.g. \"DE\", \"FR\", \"IT\").","type":"object"},"seededAt":{"description":"UTC ms timestamp when seed data was written.","format":"int64","type":"string"},"unavailable":{"description":"True when upstream data is unavailable (fallback result).","type":"boolean"}},"type":"object"},"GetFredSeriesBatchRequest":{"description":"GetFredSeriesBatchRequest looks up multiple FRED series in a single call.","properties":{"limit":{"description":"Maximum number of observations per series. Defaults to 120.","format":"int32","type":"integer"},"seriesIds":{"items":{"description":"FRED series IDs (e.g., \"WALCL\", \"FEDFUNDS\"). Max 10.","maxItems":10,"minItems":1,"type":"string"},"maxItems":10,"minItems":1,"type":"array"}},"type":"object"},"GetFredSeriesBatchResponse":{"description":"GetFredSeriesBatchResponse contains the requested FRED series data.","properties":{"fetched":{"description":"Number of series successfully fetched.","format":"int32","type":"integer"},"requested":{"description":"Number of series requested.","format":"int32","type":"integer"},"results":{"additionalProperties":{"$ref":"#/components/schemas/FredSeries"},"description":"Map of series_id -\u003e FRED series for found series.","type":"object"}},"type":"object"},"GetFredSeriesRequest":{"description":"GetFredSeriesRequest specifies which FRED series to retrieve.","properties":{"limit":{"description":"Maximum number of observations to return. Defaults to 120.","format":"int32","type":"integer"},"seriesId":{"description":"FRED series ID (e.g., \"GDP\", \"UNRATE\", \"CPIAUCSL\").","minLength":1,"type":"string"}},"required":["seriesId"],"type":"object"},"GetFredSeriesResponse":{"description":"GetFredSeriesResponse contains the requested FRED series data.","properties":{"series":{"$ref":"#/components/schemas/FredSeries"}},"type":"object"},"GetMacroSignalsRequest":{"description":"GetMacroSignalsRequest requests the current macro signal dashboard.","type":"object"},"GetMacroSignalsResponse":{"description":"GetMacroSignalsResponse contains the full macro signal dashboard with 7 signals and verdict.","properties":{"bullishCount":{"description":"Number of bullish signals.","format":"int32","type":"integer"},"meta":{"$ref":"#/components/schemas/MacroMeta"},"signals":{"$ref":"#/components/schemas/MacroSignals"},"timestamp":{"description":"ISO 8601 timestamp of computation.","type":"string"},"totalCount":{"description":"Total number of evaluated signals (excluding UNKNOWN).","format":"int32","type":"integer"},"unavailable":{"description":"True when upstream data is unavailable (fallback result).","type":"boolean"},"verdict":{"description":"Overall verdict: \"BUY\", \"CASH\", or \"UNKNOWN\".","type":"string"}},"type":"object"},"GetNatGasStorageRequest":{"description":"GetNatGasStorageRequest is the request message for GetNatGasStorage.","type":"object"},"GetNatGasStorageResponse":{"description":"GetNatGasStorageResponse contains the 8 most recent weeks of US natural gas storage data.","properties":{"latestPeriod":{"description":"Timestamp of the most recent EIA data point (ISO 8601).","type":"string"},"weeks":{"items":{"$ref":"#/components/schemas/NatGasStorageWeek"},"type":"array"}},"type":"object"},"GetNationalDebtRequest":{"description":"GetNationalDebtRequest requests national debt data for all countries.","type":"object"},"GetNationalDebtResponse":{"description":"GetNationalDebtResponse wraps the full list of national debt entries.","properties":{"entries":{"items":{"$ref":"#/components/schemas/NationalDebtEntry"},"type":"array"},"seededAt":{"description":"ISO 8601 timestamp when seed data was written.","type":"string"},"unavailable":{"description":"True when upstream data is unavailable (fallback result).","type":"boolean"}},"type":"object"},"GroceryItemPrice":{"properties":{"available":{"type":"boolean"},"currency":{"type":"string"},"itemId":{"type":"string"},"itemName":{"type":"string"},"localPrice":{"format":"double","type":"number"},"sourceSite":{"type":"string"},"unit":{"type":"string"},"usdPrice":{"format":"double","type":"number"}},"type":"object"},"HashRateSignal":{"description":"HashRateSignal tracks Bitcoin hash rate momentum.","properties":{"change30d":{"description":"Hash rate change over 30 days as percentage.","format":"double","type":"number"},"status":{"description":"\"GROWING\", \"DECLINING\", \"STABLE\", or \"UNKNOWN\".","type":"string"}},"type":"object"},"LiquiditySignal":{"description":"LiquiditySignal tracks JPY 30d rate of change as a liquidity proxy.","properties":{"sparkline":{"items":{"description":"Last 30 JPY close prices.","format":"double","type":"number"},"type":"array"},"status":{"description":"\"SQUEEZE\", \"NORMAL\", or \"UNKNOWN\".","type":"string"},"value":{"description":"JPY 30d ROC percentage, absent if unavailable.","format":"double","type":"number"}},"type":"object"},"ListBigMacPricesRequest":{"type":"object"},"ListBigMacPricesResponse":{"properties":{"cheapestCountry":{"type":"string"},"countries":{"items":{"$ref":"#/components/schemas/BigMacCountryPrice"},"type":"array"},"fetchedAt":{"type":"string"},"mostExpensiveCountry":{"type":"string"},"prevFetchedAt":{"type":"string"},"wowAvailable":{"type":"boolean"},"wowAvgPct":{"format":"double","type":"number"}},"type":"object"},"ListFuelPricesRequest":{"type":"object"},"ListFuelPricesResponse":{"properties":{"cheapestDiesel":{"type":"string"},"cheapestGasoline":{"type":"string"},"countries":{"items":{"$ref":"#/components/schemas/FuelCountryPrice"},"type":"array"},"countryCount":{"format":"int32","type":"integer"},"fetchedAt":{"type":"string"},"mostExpensiveDiesel":{"type":"string"},"mostExpensiveGasoline":{"type":"string"},"prevFetchedAt":{"type":"string"},"sourceCount":{"format":"int32","type":"integer"},"wowAvailable":{"type":"boolean"}},"type":"object"},"ListGroceryBasketPricesRequest":{"type":"object"},"ListGroceryBasketPricesResponse":{"properties":{"cheapestCountry":{"type":"string"},"countries":{"items":{"$ref":"#/components/schemas/CountryBasket"},"type":"array"},"fetchedAt":{"type":"string"},"mostExpensiveCountry":{"type":"string"},"prevFetchedAt":{"type":"string"},"upstreamUnavailable":{"type":"boolean"},"wowAvailable":{"type":"boolean"},"wowAvgPct":{"format":"double","type":"number"}},"type":"object"},"ListWorldBankIndicatorsRequest":{"description":"ListWorldBankIndicatorsRequest specifies filters for retrieving World Bank data.","properties":{"countryCode":{"description":"Optional country filter (ISO 3166-1 alpha-2).","type":"string"},"cursor":{"description":"Cursor for next page.","type":"string"},"indicatorCode":{"description":"World Bank indicator code (e.g., \"NY.GDP.MKTP.CD\").","minLength":1,"type":"string"},"pageSize":{"description":"Maximum items per page.","format":"int32","type":"integer"},"year":{"description":"Optional year filter. Defaults to latest available.","format":"int32","type":"integer"}},"required":["indicatorCode"],"type":"object"},"ListWorldBankIndicatorsResponse":{"description":"ListWorldBankIndicatorsResponse contains World Bank indicator data.","properties":{"data":{"items":{"$ref":"#/components/schemas/WorldBankCountryData"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationResponse"}},"type":"object"},"MacroMeta":{"description":"MacroMeta contains supplementary chart data.","properties":{"qqqSparkline":{"items":{"description":"Last 30 QQQ close prices for sparkline.","format":"double","type":"number"},"type":"array"}},"type":"object"},"MacroRegimeSignal":{"description":"MacroRegimeSignal compares QQQ vs XLP 20-day rate of change.","properties":{"qqqRoc20":{"description":"QQQ 20d ROC percentage.","format":"double","type":"number"},"status":{"description":"\"RISK-ON\", \"DEFENSIVE\", or \"UNKNOWN\".","type":"string"},"xlpRoc20":{"description":"XLP 20d ROC percentage.","format":"double","type":"number"}},"type":"object"},"MacroSignals":{"description":"MacroSignals contains all 7 individual signal computations.","properties":{"fearGreed":{"$ref":"#/components/schemas/FearGreedSignal"},"flowStructure":{"$ref":"#/components/schemas/FlowStructureSignal"},"hashRate":{"$ref":"#/components/schemas/HashRateSignal"},"liquidity":{"$ref":"#/components/schemas/LiquiditySignal"},"macroRegime":{"$ref":"#/components/schemas/MacroRegimeSignal"},"priceMomentum":{"$ref":"#/components/schemas/PriceMomentumSignal"},"technicalTrend":{"$ref":"#/components/schemas/TechnicalTrendSignal"}},"type":"object"},"NatGasStorageWeek":{"description":"NatGasStorageWeek represents one week of US natural gas working gas storage data from EIA.","properties":{"period":{"description":"ISO week period (YYYY-MM-DD, Monday of the EIA report week).","type":"string"},"storBcf":{"description":"Working gas in underground storage, Lower-48 States, in Bcf.","format":"double","type":"number"},"weeklyChangeBcf":{"description":"Week-over-week change in Bcf. Positive = build (bearish for gas prices), negative = draw (bullish).\n Absent for the oldest week when no prior week is available for comparison.","format":"double","type":"number"}},"type":"object"},"NationalDebtEntry":{"description":"NationalDebtEntry holds debt data for a single country.","properties":{"annualGrowth":{"description":"Year-over-year debt growth percent (2023-\u003e2024).","format":"double","type":"number"},"baselineTs":{"description":"UTC ms timestamp anchoring the debt_usd figure (2024-01-01T00:00:00Z).","format":"int64","type":"string"},"debtToGdp":{"description":"Debt as % of GDP.","format":"double","type":"number"},"debtUsd":{"description":"Total debt in USD at baseline_ts.","format":"double","type":"number"},"gdpUsd":{"description":"GDP in USD (nominal, latest year).","format":"double","type":"number"},"iso3":{"description":"ISO3 country code (e.g. \"USA\").","type":"string"},"perDayRate":{"description":"Deficit-derived accrual in USD per day.","format":"double","type":"number"},"perSecondRate":{"description":"Deficit-derived accrual in USD per second.","format":"double","type":"number"},"source":{"description":"Human-readable source string.","type":"string"}},"type":"object"},"PaginationResponse":{"description":"PaginationResponse contains pagination metadata returned alongside list results.","properties":{"nextCursor":{"description":"Cursor for fetching the next page. Empty string indicates no more pages.","type":"string"},"totalCount":{"description":"Total count of items matching the query, if known. Zero if the total is unknown.","format":"int32","type":"integer"}},"type":"object"},"PriceMomentumSignal":{"description":"PriceMomentumSignal uses the Mayer Multiple (price/SMA200) as a market-adaptive signal.","properties":{"status":{"description":"\"STRONG\", \"MODERATE\", \"WEAK\", or \"UNKNOWN\".","type":"string"}},"type":"object"},"RatesEntry":{"properties":{"key":{"type":"string"},"value":{"format":"double","type":"number"}},"type":"object"},"ResultsEntry":{"properties":{"key":{"type":"string"},"value":{"$ref":"#/components/schemas/FredSeries"}},"type":"object"},"TechnicalTrendSignal":{"description":"TechnicalTrendSignal evaluates BTC price vs moving averages and VWAP.","properties":{"btcPrice":{"description":"Current BTC price.","format":"double","type":"number"},"mayerMultiple":{"description":"Mayer multiple (BTC price / SMA200).","format":"double","type":"number"},"sma200":{"description":"200-day simple moving average.","format":"double","type":"number"},"sma50":{"description":"50-day simple moving average.","format":"double","type":"number"},"sparkline":{"items":{"description":"Last 30 BTC close prices.","format":"double","type":"number"},"type":"array"},"status":{"description":"\"BULLISH\", \"BEARISH\", \"NEUTRAL\", or \"UNKNOWN\".","type":"string"},"vwap30d":{"description":"30-day volume-weighted average price.","format":"double","type":"number"}},"type":"object"},"ValidationError":{"description":"ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.","properties":{"violations":{"description":"List of validation violations","items":{"$ref":"#/components/schemas/FieldViolation"},"type":"array"}},"required":["violations"],"type":"object"},"WorldBankCountryData":{"description":"WorldBankCountryData represents a World Bank indicator value for a country.","properties":{"countryCode":{"description":"ISO 3166-1 alpha-2 country code.","minLength":1,"type":"string"},"countryName":{"description":"Country name.","type":"string"},"indicatorCode":{"description":"World Bank indicator code (e.g., \"NY.GDP.MKTP.CD\").","minLength":1,"type":"string"},"indicatorName":{"description":"Indicator name.","type":"string"},"value":{"description":"Indicator value.","format":"double","type":"number"},"year":{"description":"Data year.","format":"int32","type":"integer"}},"required":["countryCode","indicatorCode"],"type":"object"}}},"info":{"title":"EconomicService API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/economic/v1/get-bis-credit":{"get":{"description":"GetBisCredit retrieves credit-to-GDP ratio data from BIS.","operationId":"GetBisCredit","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetBisCreditResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetBisCredit","tags":["EconomicService"]}},"/api/economic/v1/get-bis-exchange-rates":{"get":{"description":"GetBisExchangeRates retrieves effective exchange rates from BIS.","operationId":"GetBisExchangeRates","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetBisExchangeRatesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetBisExchangeRates","tags":["EconomicService"]}},"/api/economic/v1/get-bis-policy-rates":{"get":{"description":"GetBisPolicyRates retrieves central bank policy rates from BIS.","operationId":"GetBisPolicyRates","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetBisPolicyRatesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetBisPolicyRates","tags":["EconomicService"]}},"/api/economic/v1/get-bls-series":{"get":{"description":"GetBlsSeries retrieves BLS-only series not available on FRED (CES, LAUMT, CIU).","operationId":"GetBlsSeries","parameters":[{"description":"BLS series ID (e.g. \"CES0500000001\", \"CIU1010000000000A\").","in":"query","name":"series_id","required":false,"schema":{"type":"string"}},{"description":"Maximum number of observations to return. Defaults to 60.","in":"query","name":"limit","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetBlsSeriesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetBlsSeries","tags":["EconomicService"]}},"/api/economic/v1/get-crude-inventories":{"get":{"description":"GetCrudeInventories retrieves the 8 most recent weeks of US crude oil stockpile data from EIA (WCRSTUS1).","operationId":"GetCrudeInventories","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCrudeInventoriesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCrudeInventories","tags":["EconomicService"]}},"/api/economic/v1/get-ecb-fx-rates":{"get":{"description":"GetEcbFxRates retrieves daily ECB official reference rates for EUR/major currency pairs.","operationId":"GetEcbFxRates","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEcbFxRatesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEcbFxRates","tags":["EconomicService"]}},"/api/economic/v1/get-economic-calendar":{"get":{"description":"GetEconomicCalendar retrieves upcoming major economic events (FOMC, CPI, NFP, etc).","operationId":"GetEconomicCalendar","parameters":[{"in":"query","name":"fromDate","required":false,"schema":{"type":"string"}},{"in":"query","name":"toDate","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEconomicCalendarResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEconomicCalendar","tags":["EconomicService"]}},"/api/economic/v1/get-economic-stress":{"get":{"description":"GetEconomicStress retrieves the composite Economic Stress Index (0-100) from 6 FRED series.","operationId":"GetEconomicStress","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEconomicStressResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEconomicStress","tags":["EconomicService"]}},"/api/economic/v1/get-energy-capacity":{"get":{"description":"GetEnergyCapacity retrieves installed capacity data (solar, wind, coal) from EIA.","operationId":"GetEnergyCapacity","parameters":[{"description":"Energy source codes to query (e.g., \"SUN\", \"WND\", \"COL\").\n Empty returns all tracked sources (SUN, WND, COL).","in":"query","name":"energy_sources","required":false,"schema":{"type":"string"}},{"description":"Number of years of historical data. Default 20 if not set.","in":"query","name":"years","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEnergyCapacityResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEnergyCapacity","tags":["EconomicService"]}},"/api/economic/v1/get-energy-prices":{"get":{"description":"GetEnergyPrices retrieves current energy commodity prices from EIA.","operationId":"GetEnergyPrices","parameters":[{"description":"Optional commodity filter. Empty returns all tracked commodities.","in":"query","name":"commodities","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEnergyPricesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEnergyPrices","tags":["EconomicService"]}},"/api/economic/v1/get-eu-fsi":{"get":{"description":"GetEuFsi retrieves the ECB CISS (Composite Indicator of Systemic Stress) for the Euro area.","operationId":"GetEuFsi","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEuFsiResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEuFsi","tags":["EconomicService"]}},"/api/economic/v1/get-eu-gas-storage":{"get":{"description":"GetEuGasStorage retrieves EU aggregate natural gas storage fill % from GIE AGSI+.","operationId":"GetEuGasStorage","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEuGasStorageResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEuGasStorage","tags":["EconomicService"]}},"/api/economic/v1/get-eu-yield-curve":{"get":{"description":"GetEuYieldCurve retrieves the ECB Euro Area AAA sovereign yield curve (Svensson model spot rates).","operationId":"GetEuYieldCurve","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEuYieldCurveResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEuYieldCurve","tags":["EconomicService"]}},"/api/economic/v1/get-eurostat-country-data":{"get":{"description":"GetEurostatCountryData retrieves per-country CPI, unemployment, and GDP growth for 10 EU member states.","operationId":"GetEurostatCountryData","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEurostatCountryDataResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEurostatCountryData","tags":["EconomicService"]}},"/api/economic/v1/get-fred-series":{"get":{"description":"GetFredSeries retrieves time series data from the Federal Reserve Economic Data.","operationId":"GetFredSeries","parameters":[{"description":"FRED series ID (e.g., \"GDP\", \"UNRATE\", \"CPIAUCSL\").","in":"query","name":"series_id","required":false,"schema":{"type":"string"}},{"description":"Maximum number of observations to return. Defaults to 120.","in":"query","name":"limit","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetFredSeriesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetFredSeries","tags":["EconomicService"]}},"/api/economic/v1/get-fred-series-batch":{"post":{"description":"GetFredSeriesBatch retrieves multiple FRED series in a single call.","operationId":"GetFredSeriesBatch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetFredSeriesBatchRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetFredSeriesBatchResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetFredSeriesBatch","tags":["EconomicService"]}},"/api/economic/v1/get-macro-signals":{"get":{"description":"GetMacroSignals computes 7 macro signals from 6 upstream sources with BUY/CASH verdict.","operationId":"GetMacroSignals","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetMacroSignalsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetMacroSignals","tags":["EconomicService"]}},"/api/economic/v1/get-nat-gas-storage":{"get":{"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":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetNatGasStorageResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetNatGasStorage","tags":["EconomicService"]}},"/api/economic/v1/get-national-debt":{"get":{"description":"GetNationalDebt retrieves national debt clock data for all countries.","operationId":"GetNationalDebt","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetNationalDebtResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetNationalDebt","tags":["EconomicService"]}},"/api/economic/v1/list-bigmac-prices":{"get":{"description":"ListBigMacPrices retrieves Big Mac Index prices across Middle East countries.","operationId":"ListBigMacPrices","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListBigMacPricesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListBigMacPrices","tags":["EconomicService"]}},"/api/economic/v1/list-fuel-prices":{"get":{"description":"ListFuelPrices retrieves retail gasoline and diesel prices across 30+ countries.","operationId":"ListFuelPrices","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListFuelPricesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListFuelPrices","tags":["EconomicService"]}},"/api/economic/v1/list-grocery-basket-prices":{"get":{"description":"ListGroceryBasketPrices retrieves grocery basket price comparison across 24 countries worldwide.","operationId":"ListGroceryBasketPrices","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListGroceryBasketPricesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListGroceryBasketPrices","tags":["EconomicService"]}},"/api/economic/v1/list-world-bank-indicators":{"get":{"description":"ListWorldBankIndicators retrieves development indicator data from the World Bank.","operationId":"ListWorldBankIndicators","parameters":[{"description":"World Bank indicator code (e.g., \"NY.GDP.MKTP.CD\").","in":"query","name":"indicator_code","required":false,"schema":{"type":"string"}},{"description":"Optional country filter (ISO 3166-1 alpha-2).","in":"query","name":"country_code","required":false,"schema":{"type":"string"}},{"description":"Optional year filter. Defaults to latest available.","in":"query","name":"year","required":false,"schema":{"format":"int32","type":"integer"}},{"description":"Maximum items per page.","in":"query","name":"page_size","required":false,"schema":{"format":"int32","type":"integer"}},{"description":"Cursor for next page.","in":"query","name":"cursor","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListWorldBankIndicatorsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListWorldBankIndicators","tags":["EconomicService"]}}}} \ No newline at end of file +{"components":{"schemas":{"BigMacCountryPrice":{"properties":{"available":{"type":"boolean"},"code":{"type":"string"},"currency":{"type":"string"},"flag":{"type":"string"},"fxRate":{"format":"double","type":"number"},"localPrice":{"format":"double","type":"number"},"name":{"type":"string"},"sourceSite":{"type":"string"},"usdPrice":{"format":"double","type":"number"},"wowPct":{"format":"double","type":"number"}},"type":"object"},"BisCreditToGdp":{"description":"BisCreditToGdp represents total credit as percentage of GDP from BIS.","properties":{"countryCode":{"description":"ISO 2-letter country code.","type":"string"},"countryName":{"description":"Country or region name.","type":"string"},"creditGdpRatio":{"description":"Total credit as percentage of GDP.","format":"double","type":"number"},"date":{"description":"Date as YYYY-QN.","type":"string"},"previousRatio":{"description":"Previous quarter ratio.","format":"double","type":"number"}},"type":"object"},"BisExchangeRate":{"description":"BisExchangeRate represents effective exchange rate indices from BIS.","properties":{"countryCode":{"description":"ISO 2-letter country code.","type":"string"},"countryName":{"description":"Country or region name.","type":"string"},"date":{"description":"Date as YYYY-MM.","type":"string"},"nominalEer":{"description":"Nominal effective exchange rate index.","format":"double","type":"number"},"realChange":{"description":"Percentage change from previous period (real).","format":"double","type":"number"},"realEer":{"description":"Real effective exchange rate index.","format":"double","type":"number"}},"type":"object"},"BisPolicyRate":{"description":"BisPolicyRate represents a central bank policy rate from BIS.","properties":{"centralBank":{"description":"Central bank name (e.g. \"Federal Reserve\").","type":"string"},"countryCode":{"description":"ISO 2-letter country code (US, GB, JP, etc.)","type":"string"},"countryName":{"description":"Country or region name.","type":"string"},"date":{"description":"Date as YYYY-MM.","type":"string"},"previousRate":{"description":"Previous period rate percentage.","format":"double","type":"number"},"rate":{"description":"Current policy rate percentage.","format":"double","type":"number"}},"type":"object"},"BlsObservation":{"description":"BlsObservation is a single BLS data point.","properties":{"period":{"description":"Period code (e.g. \"M01\" for January, \"A01\" for annual).","type":"string"},"periodName":{"description":"Human-readable period name.","type":"string"},"value":{"description":"Observed value.","type":"string"},"year":{"description":"Year of the observation.","type":"string"}},"type":"object"},"BlsSeries":{"description":"BlsSeries is a BLS time series with metadata and observations.","properties":{"observations":{"items":{"$ref":"#/components/schemas/BlsObservation"},"type":"array"},"seriesId":{"description":"BLS series ID (e.g. \"CES0500000001\").","type":"string"},"title":{"description":"Human-readable series title.","type":"string"},"units":{"description":"Unit of measure.","type":"string"}},"type":"object"},"CountriesEntry":{"properties":{"key":{"type":"string"},"value":{"$ref":"#/components/schemas/EurostatCountryEntry"}},"type":"object"},"CountryBasket":{"properties":{"code":{"type":"string"},"currency":{"type":"string"},"flag":{"type":"string"},"fxRate":{"format":"double","type":"number"},"items":{"items":{"$ref":"#/components/schemas/GroceryItemPrice"},"type":"array"},"name":{"type":"string"},"totalUsd":{"format":"double","type":"number"},"wowPct":{"format":"double","type":"number"}},"type":"object"},"CrudeInventoryWeek":{"description":"CrudeInventoryWeek represents one week of US crude oil stockpile data from EIA WCRSTUS1.","properties":{"period":{"description":"ISO week period (YYYY-MM-DD, Monday of the EIA report week).","type":"string"},"stocksMb":{"description":"Total crude oil stocks in millions of barrels.","format":"double","type":"number"},"weeklyChangeMb":{"description":"Week-over-week change in millions of barrels. Positive = build (bearish), negative = draw (bullish).\n Absent for the oldest week when no prior week is available for comparison.","format":"double","type":"number"}},"type":"object"},"EcbFxRate":{"description":"EcbFxRate is a single ECB official reference rate for a currency pair.","properties":{"change1d":{"description":"1-day change in rate (absolute).","format":"double","type":"number"},"date":{"description":"Date of the observation in YYYY-MM-DD format.","type":"string"},"pair":{"description":"Currency pair label, e.g. \"EURUSD\".","type":"string"},"rate":{"description":"Exchange rate (units of quote currency per 1 EUR).","format":"double","type":"number"}},"type":"object"},"EconomicEvent":{"properties":{"actual":{"type":"string"},"country":{"type":"string"},"date":{"type":"string"},"estimate":{"type":"string"},"event":{"type":"string"},"impact":{"type":"string"},"previous":{"type":"string"},"unit":{"type":"string"}},"type":"object"},"EconomicStressComponent":{"properties":{"id":{"type":"string"},"label":{"type":"string"},"missing":{"type":"boolean"},"rawValue":{"format":"double","type":"number"},"score":{"format":"double","type":"number"},"weight":{"format":"double","type":"number"}},"type":"object"},"EnergyCapacitySeries":{"properties":{"data":{"items":{"$ref":"#/components/schemas/EnergyCapacityYear"},"type":"array"},"energySource":{"type":"string"},"name":{"type":"string"}},"type":"object"},"EnergyCapacityYear":{"properties":{"capacityMw":{"format":"double","type":"number"},"year":{"format":"int32","type":"integer"}},"type":"object"},"EnergyPrice":{"description":"EnergyPrice represents a current energy commodity price from EIA.","properties":{"change":{"description":"Percentage change from previous period.","format":"double","type":"number"},"commodity":{"description":"Energy commodity identifier.","minLength":1,"type":"string"},"name":{"description":"Human-readable name (e.g., \"WTI Crude Oil\", \"Henry Hub Natural Gas\").","type":"string"},"price":{"description":"Current price in USD.","format":"double","type":"number"},"priceAt":{"description":"Price date, as Unix epoch milliseconds.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"unit":{"description":"Unit of measurement (e.g., \"$/barrel\", \"$/MMBtu\").","type":"string"}},"required":["commodity"],"type":"object"},"Error":{"description":"Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.","properties":{"message":{"description":"Error message (e.g., 'user not found', 'database connection failed')","type":"string"}},"type":"object"},"EuFsiObservation":{"properties":{"date":{"type":"string"},"value":{"format":"double","type":"number"}},"type":"object"},"EuGasStorageHistoryEntry":{"description":"EuGasStorageHistoryEntry represents one day of EU aggregate gas storage data.","properties":{"date":{"description":"Calendar date (YYYY-MM-DD).","type":"string"},"fillPct":{"description":"Storage fill level as a percentage of working gas volume capacity.","format":"double","type":"number"},"gasTwh":{"description":"Working gas volume in storage (TWh).","format":"double","type":"number"}},"type":"object"},"EuYieldCurveData":{"description":"EuYieldCurveData holds a single observation of the ECB Euro Area AAA yield curve.","properties":{"date":{"description":"Date of the observation (YYYY-MM or YYYY-MM-DD).","type":"string"},"rates":{"additionalProperties":{"format":"double","type":"number"},"description":"Spot rates by tenor. Keys: \"1Y\", \"2Y\", \"5Y\", \"10Y\", \"20Y\", \"30Y\".","type":"object"},"source":{"description":"Data source identifier (e.g. \"ecb-aaa\").","type":"string"},"updatedAt":{"description":"ISO 8601 timestamp when this was last seeded.","type":"string"}},"type":"object"},"EurostatCountryEntry":{"description":"EurostatCountryEntry holds all available metrics for one EU country.","properties":{"cpi":{"$ref":"#/components/schemas/EurostatMetric"},"gdpGrowth":{"$ref":"#/components/schemas/EurostatMetric"},"unemployment":{"$ref":"#/components/schemas/EurostatMetric"}},"type":"object"},"EurostatMetric":{"description":"EurostatMetric holds a single economic metric value for a country.","properties":{"date":{"description":"Period string (e.g. \"2024-01\" for monthly, \"2024-Q1\" for quarterly).","type":"string"},"hasPrior":{"description":"True when prior_value is present (proto3 can't distinguish 0 from absent).","type":"boolean"},"priorValue":{"description":"Prior period value for delta calculation (e.g. previous month/quarter).","format":"double","type":"number"},"unit":{"description":"Unit of measurement (e.g. \"%\").","type":"string"},"value":{"description":"Numeric value (e.g. 2.3 for 2.3%).","format":"double","type":"number"}},"type":"object"},"FaoFoodPricePoint":{"properties":{"cereals":{"format":"double","type":"number"},"dairy":{"format":"double","type":"number"},"date":{"type":"string"},"ffpi":{"format":"double","type":"number"},"meat":{"format":"double","type":"number"},"oils":{"format":"double","type":"number"},"sugar":{"format":"double","type":"number"}},"type":"object"},"FearGreedHistoryEntry":{"description":"FearGreedHistoryEntry is a single day's Fear \u0026 Greed index reading.","properties":{"date":{"description":"Date string (YYYY-MM-DD).","type":"string"},"value":{"description":"Index value (0-100).","format":"int32","maximum":100,"minimum":0,"type":"integer"}},"type":"object"},"FearGreedSignal":{"description":"FearGreedSignal tracks the Crypto Fear \u0026 Greed index.","properties":{"history":{"items":{"$ref":"#/components/schemas/FearGreedHistoryEntry"},"type":"array"},"status":{"description":"Classification label (e.g., \"Extreme Fear\", \"Greed\").","type":"string"},"value":{"description":"Current index value (0-100).","format":"int32","type":"integer"}},"type":"object"},"FieldViolation":{"description":"FieldViolation describes a single validation error for a specific field.","properties":{"description":{"description":"Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')","type":"string"},"field":{"description":"The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')","type":"string"}},"required":["field","description"],"type":"object"},"FlowStructureSignal":{"description":"FlowStructureSignal compares BTC vs QQQ 5-day returns.","properties":{"btcReturn5":{"description":"BTC 5-day return percentage.","format":"double","type":"number"},"qqqReturn5":{"description":"QQQ 5-day return percentage.","format":"double","type":"number"},"status":{"description":"\"PASSIVE GAP\", \"ALIGNED\", or \"UNKNOWN\".","type":"string"}},"type":"object"},"FredObservation":{"description":"FredObservation represents a single data point from a FRED economic series.","properties":{"date":{"description":"Observation date as YYYY-MM-DD string.","type":"string"},"value":{"description":"Observation value.","format":"double","type":"number"}},"type":"object"},"FredSeries":{"description":"FredSeries represents a FRED time series with metadata.","properties":{"frequency":{"description":"Data frequency (e.g., \"Monthly\", \"Quarterly\").","type":"string"},"observations":{"items":{"$ref":"#/components/schemas/FredObservation"},"type":"array"},"seriesId":{"description":"Series identifier (e.g., \"GDP\", \"UNRATE\", \"CPIAUCSL\").","minLength":1,"type":"string"},"title":{"description":"Series title.","type":"string"},"units":{"description":"Unit of measurement.","type":"string"}},"required":["seriesId"],"type":"object"},"FuelCountryPrice":{"properties":{"code":{"type":"string"},"currency":{"type":"string"},"diesel":{"$ref":"#/components/schemas/FuelPrice"},"flag":{"type":"string"},"fxRate":{"format":"double","type":"number"},"gasoline":{"$ref":"#/components/schemas/FuelPrice"},"name":{"type":"string"}},"type":"object"},"FuelPrice":{"properties":{"available":{"type":"boolean"},"grade":{"type":"string"},"localPrice":{"format":"double","type":"number"},"observedAt":{"type":"string"},"source":{"type":"string"},"usdPrice":{"format":"double","type":"number"},"wowPct":{"format":"double","type":"number"}},"type":"object"},"GetBisCreditRequest":{"description":"GetBisCreditRequest requests credit-to-GDP ratio data.","type":"object"},"GetBisCreditResponse":{"description":"GetBisCreditResponse contains BIS credit-to-GDP data.","properties":{"entries":{"items":{"$ref":"#/components/schemas/BisCreditToGdp"},"type":"array"}},"type":"object"},"GetBisExchangeRatesRequest":{"description":"GetBisExchangeRatesRequest requests effective exchange rates.","type":"object"},"GetBisExchangeRatesResponse":{"description":"GetBisExchangeRatesResponse contains BIS effective exchange rate data.","properties":{"rates":{"items":{"$ref":"#/components/schemas/BisExchangeRate"},"type":"array"}},"type":"object"},"GetBisPolicyRatesRequest":{"description":"GetBisPolicyRatesRequest requests central bank policy rates.","type":"object"},"GetBisPolicyRatesResponse":{"description":"GetBisPolicyRatesResponse contains BIS policy rate data.","properties":{"rates":{"items":{"$ref":"#/components/schemas/BisPolicyRate"},"type":"array"}},"type":"object"},"GetBlsSeriesRequest":{"description":"GetBlsSeriesRequest specifies which BLS series to retrieve.","properties":{"limit":{"description":"Maximum number of observations to return. Defaults to 60.","format":"int32","type":"integer"},"seriesId":{"description":"BLS series ID (e.g. \"CES0500000001\", \"CIU1010000000000A\").","type":"string"}},"type":"object"},"GetBlsSeriesResponse":{"description":"GetBlsSeriesResponse contains the requested BLS series data.","properties":{"series":{"$ref":"#/components/schemas/BlsSeries"}},"type":"object"},"GetCrudeInventoriesRequest":{"description":"GetCrudeInventoriesRequest is the request message for GetCrudeInventories.","type":"object"},"GetCrudeInventoriesResponse":{"description":"GetCrudeInventoriesResponse contains the 8 most recent weeks of US crude oil inventory data.","properties":{"latestPeriod":{"description":"Timestamp of the most recent EIA data point (ISO 8601).","type":"string"},"weeks":{"items":{"$ref":"#/components/schemas/CrudeInventoryWeek"},"type":"array"}},"type":"object"},"GetEcbFxRatesRequest":{"description":"GetEcbFxRatesRequest is empty; returns all tracked EUR pairs.","type":"object"},"GetEcbFxRatesResponse":{"description":"GetEcbFxRatesResponse contains the latest ECB reference rates.","properties":{"rates":{"items":{"$ref":"#/components/schemas/EcbFxRate"},"type":"array"},"seededAt":{"description":"Unix ms when the data was last seeded.","format":"int64","type":"string"},"unavailable":{"description":"True when Redis key is missing or data is unavailable.","type":"boolean"},"updatedAt":{"description":"ISO 8601 timestamp of the ECB publication.","type":"string"}},"type":"object"},"GetEconomicCalendarRequest":{"properties":{"fromDate":{"type":"string"},"toDate":{"type":"string"}},"type":"object"},"GetEconomicCalendarResponse":{"properties":{"events":{"items":{"$ref":"#/components/schemas/EconomicEvent"},"type":"array"},"fromDate":{"type":"string"},"toDate":{"type":"string"},"total":{"format":"int32","type":"integer"},"unavailable":{"type":"boolean"}},"type":"object"},"GetEconomicStressRequest":{"type":"object"},"GetEconomicStressResponse":{"properties":{"components":{"items":{"$ref":"#/components/schemas/EconomicStressComponent"},"type":"array"},"compositeScore":{"format":"double","type":"number"},"label":{"type":"string"},"seededAt":{"type":"string"},"unavailable":{"type":"boolean"}},"type":"object"},"GetEnergyCapacityRequest":{"properties":{"energySources":{"items":{"description":"Energy source codes to query (e.g., \"SUN\", \"WND\", \"COL\").\n Empty returns all tracked sources (SUN, WND, COL).","type":"string"},"type":"array"},"years":{"description":"Number of years of historical data. Default 20 if not set.","format":"int32","type":"integer"}},"type":"object"},"GetEnergyCapacityResponse":{"properties":{"series":{"items":{"$ref":"#/components/schemas/EnergyCapacitySeries"},"type":"array"}},"type":"object"},"GetEnergyPricesRequest":{"description":"GetEnergyPricesRequest specifies which energy commodities to retrieve.","properties":{"commodities":{"items":{"description":"Optional commodity filter. Empty returns all tracked commodities.","type":"string"},"type":"array"}},"type":"object"},"GetEnergyPricesResponse":{"description":"GetEnergyPricesResponse contains energy price data.","properties":{"prices":{"items":{"$ref":"#/components/schemas/EnergyPrice"},"type":"array"}},"type":"object"},"GetEuFsiRequest":{"type":"object"},"GetEuFsiResponse":{"properties":{"history":{"items":{"$ref":"#/components/schemas/EuFsiObservation"},"type":"array"},"label":{"type":"string"},"latestDate":{"type":"string"},"latestValue":{"format":"double","type":"number"},"seededAt":{"type":"string"},"unavailable":{"type":"boolean"}},"type":"object"},"GetEuGasStorageRequest":{"description":"GetEuGasStorageRequest is empty — returns latest EU aggregate snapshot.","type":"object"},"GetEuGasStorageResponse":{"description":"GetEuGasStorageResponse contains the EU aggregate gas storage snapshot.","properties":{"fillPct":{"description":"Current storage fill level as a percentage of working gas volume (0–100).","format":"double","type":"number"},"fillPctChange1d":{"description":"1-day change in fill percentage (positive = injecting, negative = withdrawing).","format":"double","type":"number"},"gasDaysConsumption":{"description":"Approximate days of consumption remaining at average EU winter drawdown rate.","format":"double","type":"number"},"history":{"items":{"$ref":"#/components/schemas/EuGasStorageHistoryEntry"},"type":"array"},"seededAt":{"description":"UTC ms timestamp when seed data was written.","format":"int64","type":"string"},"trend":{"description":"Current storage trend: \"injecting\", \"withdrawing\", or \"stable\".","type":"string"},"unavailable":{"description":"True when upstream data is unavailable (fallback result).","type":"boolean"},"updatedAt":{"description":"Calendar date of the most recent data point (YYYY-MM-DD).","type":"string"}},"type":"object"},"GetEuYieldCurveRequest":{"description":"GetEuYieldCurveRequest fetches the ECB Euro Area AAA sovereign yield curve.","type":"object"},"GetEuYieldCurveResponse":{"description":"GetEuYieldCurveResponse contains the latest ECB Euro Area AAA sovereign yield curve.","properties":{"data":{"$ref":"#/components/schemas/EuYieldCurveData"},"unavailable":{"description":"True if data is not yet available in cache.","type":"boolean"}},"type":"object"},"GetEurostatCountryDataRequest":{"description":"GetEurostatCountryDataRequest requests Eurostat per-country economic data.","type":"object"},"GetEurostatCountryDataResponse":{"description":"GetEurostatCountryDataResponse contains per-country CPI, unemployment, and GDP growth.","properties":{"countries":{"additionalProperties":{"$ref":"#/components/schemas/EurostatCountryEntry"},"description":"Map of ISO2 country code to economic metrics (e.g. \"DE\", \"FR\", \"IT\").","type":"object"},"seededAt":{"description":"UTC ms timestamp when seed data was written.","format":"int64","type":"string"},"unavailable":{"description":"True when upstream data is unavailable (fallback result).","type":"boolean"}},"type":"object"},"GetFaoFoodPriceIndexRequest":{"type":"object"},"GetFaoFoodPriceIndexResponse":{"properties":{"currentFfpi":{"format":"double","type":"number"},"fetchedAt":{"type":"string"},"momPct":{"format":"double","type":"number"},"points":{"items":{"$ref":"#/components/schemas/FaoFoodPricePoint"},"type":"array"},"yoyPct":{"format":"double","type":"number"}},"type":"object"},"GetFredSeriesBatchRequest":{"description":"GetFredSeriesBatchRequest looks up multiple FRED series in a single call.","properties":{"limit":{"description":"Maximum number of observations per series. Defaults to 120.","format":"int32","type":"integer"},"seriesIds":{"items":{"description":"FRED series IDs (e.g., \"WALCL\", \"FEDFUNDS\"). Max 10.","maxItems":10,"minItems":1,"type":"string"},"maxItems":10,"minItems":1,"type":"array"}},"type":"object"},"GetFredSeriesBatchResponse":{"description":"GetFredSeriesBatchResponse contains the requested FRED series data.","properties":{"fetched":{"description":"Number of series successfully fetched.","format":"int32","type":"integer"},"requested":{"description":"Number of series requested.","format":"int32","type":"integer"},"results":{"additionalProperties":{"$ref":"#/components/schemas/FredSeries"},"description":"Map of series_id -\u003e FRED series for found series.","type":"object"}},"type":"object"},"GetFredSeriesRequest":{"description":"GetFredSeriesRequest specifies which FRED series to retrieve.","properties":{"limit":{"description":"Maximum number of observations to return. Defaults to 120.","format":"int32","type":"integer"},"seriesId":{"description":"FRED series ID (e.g., \"GDP\", \"UNRATE\", \"CPIAUCSL\").","minLength":1,"type":"string"}},"required":["seriesId"],"type":"object"},"GetFredSeriesResponse":{"description":"GetFredSeriesResponse contains the requested FRED series data.","properties":{"series":{"$ref":"#/components/schemas/FredSeries"}},"type":"object"},"GetMacroSignalsRequest":{"description":"GetMacroSignalsRequest requests the current macro signal dashboard.","type":"object"},"GetMacroSignalsResponse":{"description":"GetMacroSignalsResponse contains the full macro signal dashboard with 7 signals and verdict.","properties":{"bullishCount":{"description":"Number of bullish signals.","format":"int32","type":"integer"},"meta":{"$ref":"#/components/schemas/MacroMeta"},"signals":{"$ref":"#/components/schemas/MacroSignals"},"timestamp":{"description":"ISO 8601 timestamp of computation.","type":"string"},"totalCount":{"description":"Total number of evaluated signals (excluding UNKNOWN).","format":"int32","type":"integer"},"unavailable":{"description":"True when upstream data is unavailable (fallback result).","type":"boolean"},"verdict":{"description":"Overall verdict: \"BUY\", \"CASH\", or \"UNKNOWN\".","type":"string"}},"type":"object"},"GetNatGasStorageRequest":{"description":"GetNatGasStorageRequest is the request message for GetNatGasStorage.","type":"object"},"GetNatGasStorageResponse":{"description":"GetNatGasStorageResponse contains the 8 most recent weeks of US natural gas storage data.","properties":{"latestPeriod":{"description":"Timestamp of the most recent EIA data point (ISO 8601).","type":"string"},"weeks":{"items":{"$ref":"#/components/schemas/NatGasStorageWeek"},"type":"array"}},"type":"object"},"GetNationalDebtRequest":{"description":"GetNationalDebtRequest requests national debt data for all countries.","type":"object"},"GetNationalDebtResponse":{"description":"GetNationalDebtResponse wraps the full list of national debt entries.","properties":{"entries":{"items":{"$ref":"#/components/schemas/NationalDebtEntry"},"type":"array"},"seededAt":{"description":"ISO 8601 timestamp when seed data was written.","type":"string"},"unavailable":{"description":"True when upstream data is unavailable (fallback result).","type":"boolean"}},"type":"object"},"GroceryItemPrice":{"properties":{"available":{"type":"boolean"},"currency":{"type":"string"},"itemId":{"type":"string"},"itemName":{"type":"string"},"localPrice":{"format":"double","type":"number"},"sourceSite":{"type":"string"},"unit":{"type":"string"},"usdPrice":{"format":"double","type":"number"}},"type":"object"},"HashRateSignal":{"description":"HashRateSignal tracks Bitcoin hash rate momentum.","properties":{"change30d":{"description":"Hash rate change over 30 days as percentage.","format":"double","type":"number"},"status":{"description":"\"GROWING\", \"DECLINING\", \"STABLE\", or \"UNKNOWN\".","type":"string"}},"type":"object"},"LiquiditySignal":{"description":"LiquiditySignal tracks JPY 30d rate of change as a liquidity proxy.","properties":{"sparkline":{"items":{"description":"Last 30 JPY close prices.","format":"double","type":"number"},"type":"array"},"status":{"description":"\"SQUEEZE\", \"NORMAL\", or \"UNKNOWN\".","type":"string"},"value":{"description":"JPY 30d ROC percentage, absent if unavailable.","format":"double","type":"number"}},"type":"object"},"ListBigMacPricesRequest":{"type":"object"},"ListBigMacPricesResponse":{"properties":{"cheapestCountry":{"type":"string"},"countries":{"items":{"$ref":"#/components/schemas/BigMacCountryPrice"},"type":"array"},"fetchedAt":{"type":"string"},"mostExpensiveCountry":{"type":"string"},"prevFetchedAt":{"type":"string"},"wowAvailable":{"type":"boolean"},"wowAvgPct":{"format":"double","type":"number"}},"type":"object"},"ListFuelPricesRequest":{"type":"object"},"ListFuelPricesResponse":{"properties":{"cheapestDiesel":{"type":"string"},"cheapestGasoline":{"type":"string"},"countries":{"items":{"$ref":"#/components/schemas/FuelCountryPrice"},"type":"array"},"countryCount":{"format":"int32","type":"integer"},"fetchedAt":{"type":"string"},"mostExpensiveDiesel":{"type":"string"},"mostExpensiveGasoline":{"type":"string"},"prevFetchedAt":{"type":"string"},"sourceCount":{"format":"int32","type":"integer"},"wowAvailable":{"type":"boolean"}},"type":"object"},"ListGroceryBasketPricesRequest":{"type":"object"},"ListGroceryBasketPricesResponse":{"properties":{"cheapestCountry":{"type":"string"},"countries":{"items":{"$ref":"#/components/schemas/CountryBasket"},"type":"array"},"fetchedAt":{"type":"string"},"mostExpensiveCountry":{"type":"string"},"prevFetchedAt":{"type":"string"},"upstreamUnavailable":{"type":"boolean"},"wowAvailable":{"type":"boolean"},"wowAvgPct":{"format":"double","type":"number"}},"type":"object"},"ListWorldBankIndicatorsRequest":{"description":"ListWorldBankIndicatorsRequest specifies filters for retrieving World Bank data.","properties":{"countryCode":{"description":"Optional country filter (ISO 3166-1 alpha-2).","type":"string"},"cursor":{"description":"Cursor for next page.","type":"string"},"indicatorCode":{"description":"World Bank indicator code (e.g., \"NY.GDP.MKTP.CD\").","minLength":1,"type":"string"},"pageSize":{"description":"Maximum items per page.","format":"int32","type":"integer"},"year":{"description":"Optional year filter. Defaults to latest available.","format":"int32","type":"integer"}},"required":["indicatorCode"],"type":"object"},"ListWorldBankIndicatorsResponse":{"description":"ListWorldBankIndicatorsResponse contains World Bank indicator data.","properties":{"data":{"items":{"$ref":"#/components/schemas/WorldBankCountryData"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationResponse"}},"type":"object"},"MacroMeta":{"description":"MacroMeta contains supplementary chart data.","properties":{"qqqSparkline":{"items":{"description":"Last 30 QQQ close prices for sparkline.","format":"double","type":"number"},"type":"array"}},"type":"object"},"MacroRegimeSignal":{"description":"MacroRegimeSignal compares QQQ vs XLP 20-day rate of change.","properties":{"qqqRoc20":{"description":"QQQ 20d ROC percentage.","format":"double","type":"number"},"status":{"description":"\"RISK-ON\", \"DEFENSIVE\", or \"UNKNOWN\".","type":"string"},"xlpRoc20":{"description":"XLP 20d ROC percentage.","format":"double","type":"number"}},"type":"object"},"MacroSignals":{"description":"MacroSignals contains all 7 individual signal computations.","properties":{"fearGreed":{"$ref":"#/components/schemas/FearGreedSignal"},"flowStructure":{"$ref":"#/components/schemas/FlowStructureSignal"},"hashRate":{"$ref":"#/components/schemas/HashRateSignal"},"liquidity":{"$ref":"#/components/schemas/LiquiditySignal"},"macroRegime":{"$ref":"#/components/schemas/MacroRegimeSignal"},"priceMomentum":{"$ref":"#/components/schemas/PriceMomentumSignal"},"technicalTrend":{"$ref":"#/components/schemas/TechnicalTrendSignal"}},"type":"object"},"NatGasStorageWeek":{"description":"NatGasStorageWeek represents one week of US natural gas working gas storage data from EIA.","properties":{"period":{"description":"ISO week period (YYYY-MM-DD, Monday of the EIA report week).","type":"string"},"storBcf":{"description":"Working gas in underground storage, Lower-48 States, in Bcf.","format":"double","type":"number"},"weeklyChangeBcf":{"description":"Week-over-week change in Bcf. Positive = build (bearish for gas prices), negative = draw (bullish).\n Absent for the oldest week when no prior week is available for comparison.","format":"double","type":"number"}},"type":"object"},"NationalDebtEntry":{"description":"NationalDebtEntry holds debt data for a single country.","properties":{"annualGrowth":{"description":"Year-over-year debt growth percent (2023-\u003e2024).","format":"double","type":"number"},"baselineTs":{"description":"UTC ms timestamp anchoring the debt_usd figure (2024-01-01T00:00:00Z).","format":"int64","type":"string"},"debtToGdp":{"description":"Debt as % of GDP.","format":"double","type":"number"},"debtUsd":{"description":"Total debt in USD at baseline_ts.","format":"double","type":"number"},"gdpUsd":{"description":"GDP in USD (nominal, latest year).","format":"double","type":"number"},"iso3":{"description":"ISO3 country code (e.g. \"USA\").","type":"string"},"perDayRate":{"description":"Deficit-derived accrual in USD per day.","format":"double","type":"number"},"perSecondRate":{"description":"Deficit-derived accrual in USD per second.","format":"double","type":"number"},"source":{"description":"Human-readable source string.","type":"string"}},"type":"object"},"PaginationResponse":{"description":"PaginationResponse contains pagination metadata returned alongside list results.","properties":{"nextCursor":{"description":"Cursor for fetching the next page. Empty string indicates no more pages.","type":"string"},"totalCount":{"description":"Total count of items matching the query, if known. Zero if the total is unknown.","format":"int32","type":"integer"}},"type":"object"},"PriceMomentumSignal":{"description":"PriceMomentumSignal uses the Mayer Multiple (price/SMA200) as a market-adaptive signal.","properties":{"status":{"description":"\"STRONG\", \"MODERATE\", \"WEAK\", or \"UNKNOWN\".","type":"string"}},"type":"object"},"RatesEntry":{"properties":{"key":{"type":"string"},"value":{"format":"double","type":"number"}},"type":"object"},"ResultsEntry":{"properties":{"key":{"type":"string"},"value":{"$ref":"#/components/schemas/FredSeries"}},"type":"object"},"TechnicalTrendSignal":{"description":"TechnicalTrendSignal evaluates BTC price vs moving averages and VWAP.","properties":{"btcPrice":{"description":"Current BTC price.","format":"double","type":"number"},"mayerMultiple":{"description":"Mayer multiple (BTC price / SMA200).","format":"double","type":"number"},"sma200":{"description":"200-day simple moving average.","format":"double","type":"number"},"sma50":{"description":"50-day simple moving average.","format":"double","type":"number"},"sparkline":{"items":{"description":"Last 30 BTC close prices.","format":"double","type":"number"},"type":"array"},"status":{"description":"\"BULLISH\", \"BEARISH\", \"NEUTRAL\", or \"UNKNOWN\".","type":"string"},"vwap30d":{"description":"30-day volume-weighted average price.","format":"double","type":"number"}},"type":"object"},"ValidationError":{"description":"ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.","properties":{"violations":{"description":"List of validation violations","items":{"$ref":"#/components/schemas/FieldViolation"},"type":"array"}},"required":["violations"],"type":"object"},"WorldBankCountryData":{"description":"WorldBankCountryData represents a World Bank indicator value for a country.","properties":{"countryCode":{"description":"ISO 3166-1 alpha-2 country code.","minLength":1,"type":"string"},"countryName":{"description":"Country name.","type":"string"},"indicatorCode":{"description":"World Bank indicator code (e.g., \"NY.GDP.MKTP.CD\").","minLength":1,"type":"string"},"indicatorName":{"description":"Indicator name.","type":"string"},"value":{"description":"Indicator value.","format":"double","type":"number"},"year":{"description":"Data year.","format":"int32","type":"integer"}},"required":["countryCode","indicatorCode"],"type":"object"}}},"info":{"title":"EconomicService API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/economic/v1/get-bis-credit":{"get":{"description":"GetBisCredit retrieves credit-to-GDP ratio data from BIS.","operationId":"GetBisCredit","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetBisCreditResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetBisCredit","tags":["EconomicService"]}},"/api/economic/v1/get-bis-exchange-rates":{"get":{"description":"GetBisExchangeRates retrieves effective exchange rates from BIS.","operationId":"GetBisExchangeRates","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetBisExchangeRatesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetBisExchangeRates","tags":["EconomicService"]}},"/api/economic/v1/get-bis-policy-rates":{"get":{"description":"GetBisPolicyRates retrieves central bank policy rates from BIS.","operationId":"GetBisPolicyRates","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetBisPolicyRatesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetBisPolicyRates","tags":["EconomicService"]}},"/api/economic/v1/get-bls-series":{"get":{"description":"GetBlsSeries retrieves BLS-only series not available on FRED (CES, LAUMT, CIU).","operationId":"GetBlsSeries","parameters":[{"description":"BLS series ID (e.g. \"CES0500000001\", \"CIU1010000000000A\").","in":"query","name":"series_id","required":false,"schema":{"type":"string"}},{"description":"Maximum number of observations to return. Defaults to 60.","in":"query","name":"limit","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetBlsSeriesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetBlsSeries","tags":["EconomicService"]}},"/api/economic/v1/get-crude-inventories":{"get":{"description":"GetCrudeInventories retrieves the 8 most recent weeks of US crude oil stockpile data from EIA (WCRSTUS1).","operationId":"GetCrudeInventories","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCrudeInventoriesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCrudeInventories","tags":["EconomicService"]}},"/api/economic/v1/get-ecb-fx-rates":{"get":{"description":"GetEcbFxRates retrieves daily ECB official reference rates for EUR/major currency pairs.","operationId":"GetEcbFxRates","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEcbFxRatesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEcbFxRates","tags":["EconomicService"]}},"/api/economic/v1/get-economic-calendar":{"get":{"description":"GetEconomicCalendar retrieves upcoming major economic events (FOMC, CPI, NFP, etc).","operationId":"GetEconomicCalendar","parameters":[{"in":"query","name":"fromDate","required":false,"schema":{"type":"string"}},{"in":"query","name":"toDate","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEconomicCalendarResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEconomicCalendar","tags":["EconomicService"]}},"/api/economic/v1/get-economic-stress":{"get":{"description":"GetEconomicStress retrieves the composite Economic Stress Index (0-100) from 6 FRED series.","operationId":"GetEconomicStress","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEconomicStressResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEconomicStress","tags":["EconomicService"]}},"/api/economic/v1/get-energy-capacity":{"get":{"description":"GetEnergyCapacity retrieves installed capacity data (solar, wind, coal) from EIA.","operationId":"GetEnergyCapacity","parameters":[{"description":"Energy source codes to query (e.g., \"SUN\", \"WND\", \"COL\").\n Empty returns all tracked sources (SUN, WND, COL).","in":"query","name":"energy_sources","required":false,"schema":{"type":"string"}},{"description":"Number of years of historical data. Default 20 if not set.","in":"query","name":"years","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEnergyCapacityResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEnergyCapacity","tags":["EconomicService"]}},"/api/economic/v1/get-energy-prices":{"get":{"description":"GetEnergyPrices retrieves current energy commodity prices from EIA.","operationId":"GetEnergyPrices","parameters":[{"description":"Optional commodity filter. Empty returns all tracked commodities.","in":"query","name":"commodities","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEnergyPricesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEnergyPrices","tags":["EconomicService"]}},"/api/economic/v1/get-eu-fsi":{"get":{"description":"GetEuFsi retrieves the ECB CISS (Composite Indicator of Systemic Stress) for the Euro area.","operationId":"GetEuFsi","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEuFsiResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEuFsi","tags":["EconomicService"]}},"/api/economic/v1/get-eu-gas-storage":{"get":{"description":"GetEuGasStorage retrieves EU aggregate natural gas storage fill % from GIE AGSI+.","operationId":"GetEuGasStorage","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEuGasStorageResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEuGasStorage","tags":["EconomicService"]}},"/api/economic/v1/get-eu-yield-curve":{"get":{"description":"GetEuYieldCurve retrieves the ECB Euro Area AAA sovereign yield curve (Svensson model spot rates).","operationId":"GetEuYieldCurve","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEuYieldCurveResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEuYieldCurve","tags":["EconomicService"]}},"/api/economic/v1/get-eurostat-country-data":{"get":{"description":"GetEurostatCountryData retrieves per-country CPI, unemployment, and GDP growth for 10 EU member states.","operationId":"GetEurostatCountryData","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEurostatCountryDataResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetEurostatCountryData","tags":["EconomicService"]}},"/api/economic/v1/get-fao-food-price-index":{"get":{"description":"GetFaoFoodPriceIndex retrieves the FAO Food Price Index for the past 12 months.","operationId":"GetFaoFoodPriceIndex","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetFaoFoodPriceIndexResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetFaoFoodPriceIndex","tags":["EconomicService"]}},"/api/economic/v1/get-fred-series":{"get":{"description":"GetFredSeries retrieves time series data from the Federal Reserve Economic Data.","operationId":"GetFredSeries","parameters":[{"description":"FRED series ID (e.g., \"GDP\", \"UNRATE\", \"CPIAUCSL\").","in":"query","name":"series_id","required":false,"schema":{"type":"string"}},{"description":"Maximum number of observations to return. Defaults to 120.","in":"query","name":"limit","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetFredSeriesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetFredSeries","tags":["EconomicService"]}},"/api/economic/v1/get-fred-series-batch":{"post":{"description":"GetFredSeriesBatch retrieves multiple FRED series in a single call.","operationId":"GetFredSeriesBatch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetFredSeriesBatchRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetFredSeriesBatchResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetFredSeriesBatch","tags":["EconomicService"]}},"/api/economic/v1/get-macro-signals":{"get":{"description":"GetMacroSignals computes 7 macro signals from 6 upstream sources with BUY/CASH verdict.","operationId":"GetMacroSignals","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetMacroSignalsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetMacroSignals","tags":["EconomicService"]}},"/api/economic/v1/get-nat-gas-storage":{"get":{"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":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetNatGasStorageResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetNatGasStorage","tags":["EconomicService"]}},"/api/economic/v1/get-national-debt":{"get":{"description":"GetNationalDebt retrieves national debt clock data for all countries.","operationId":"GetNationalDebt","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetNationalDebtResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetNationalDebt","tags":["EconomicService"]}},"/api/economic/v1/list-bigmac-prices":{"get":{"description":"ListBigMacPrices retrieves Big Mac Index prices across Middle East countries.","operationId":"ListBigMacPrices","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListBigMacPricesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListBigMacPrices","tags":["EconomicService"]}},"/api/economic/v1/list-fuel-prices":{"get":{"description":"ListFuelPrices retrieves retail gasoline and diesel prices across 30+ countries.","operationId":"ListFuelPrices","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListFuelPricesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListFuelPrices","tags":["EconomicService"]}},"/api/economic/v1/list-grocery-basket-prices":{"get":{"description":"ListGroceryBasketPrices retrieves grocery basket price comparison across 24 countries worldwide.","operationId":"ListGroceryBasketPrices","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListGroceryBasketPricesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListGroceryBasketPrices","tags":["EconomicService"]}},"/api/economic/v1/list-world-bank-indicators":{"get":{"description":"ListWorldBankIndicators retrieves development indicator data from the World Bank.","operationId":"ListWorldBankIndicators","parameters":[{"description":"World Bank indicator code (e.g., \"NY.GDP.MKTP.CD\").","in":"query","name":"indicator_code","required":false,"schema":{"type":"string"}},{"description":"Optional country filter (ISO 3166-1 alpha-2).","in":"query","name":"country_code","required":false,"schema":{"type":"string"}},{"description":"Optional year filter. Defaults to latest available.","in":"query","name":"year","required":false,"schema":{"format":"int32","type":"integer"}},{"description":"Maximum items per page.","in":"query","name":"page_size","required":false,"schema":{"format":"int32","type":"integer"}},{"description":"Cursor for next page.","in":"query","name":"cursor","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListWorldBankIndicatorsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListWorldBankIndicators","tags":["EconomicService"]}}}} \ No newline at end of file diff --git a/docs/api/EconomicService.openapi.yaml b/docs/api/EconomicService.openapi.yaml index 134fd1a0b..0f817dd5d 100644 --- a/docs/api/EconomicService.openapi.yaml +++ b/docs/api/EconomicService.openapi.yaml @@ -702,6 +702,32 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /api/economic/v1/get-fao-food-price-index: + get: + tags: + - EconomicService + summary: GetFaoFoodPriceIndex + description: GetFaoFoodPriceIndex retrieves the FAO Food Price Index for the past 12 months. + operationId: GetFaoFoodPriceIndex + responses: + "200": + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/GetFaoFoodPriceIndexResponse' + "400": + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + default: + description: Error response + content: + application/json: + schema: + $ref: '#/components/schemas/Error' components: schemas: Error: @@ -1919,3 +1945,46 @@ components: format: double missing: type: boolean + GetFaoFoodPriceIndexRequest: + type: object + GetFaoFoodPriceIndexResponse: + type: object + properties: + points: + type: array + items: + $ref: '#/components/schemas/FaoFoodPricePoint' + fetchedAt: + type: string + currentFfpi: + type: number + format: double + momPct: + type: number + format: double + yoyPct: + type: number + format: double + FaoFoodPricePoint: + type: object + properties: + date: + type: string + ffpi: + type: number + format: double + meat: + type: number + format: double + dairy: + type: number + format: double + cereals: + type: number + format: double + oils: + type: number + format: double + sugar: + type: number + format: double diff --git a/proto/worldmonitor/economic/v1/get_fao_food_price_index.proto b/proto/worldmonitor/economic/v1/get_fao_food_price_index.proto new file mode 100644 index 000000000..48b869888 --- /dev/null +++ b/proto/worldmonitor/economic/v1/get_fao_food_price_index.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package worldmonitor.economic.v1; + +import "sebuf/http/annotations.proto"; + +message FaoFoodPricePoint { + string date = 1; // YYYY-MM + double ffpi = 2; // FAO Food Price Index (composite) + double meat = 3; + double dairy = 4; + double cereals = 5; + double oils = 6; + double sugar = 7; +} + +message GetFaoFoodPriceIndexRequest {} + +message GetFaoFoodPriceIndexResponse { + repeated FaoFoodPricePoint points = 1; // last 12 months, ascending + string fetched_at = 2; + double current_ffpi = 3; + double mom_pct = 4; // month-over-month % change + double yoy_pct = 5; // year-over-year % change +} diff --git a/proto/worldmonitor/economic/v1/service.proto b/proto/worldmonitor/economic/v1/service.proto index 5e792982f..3839d3155 100644 --- a/proto/worldmonitor/economic/v1/service.proto +++ b/proto/worldmonitor/economic/v1/service.proto @@ -26,6 +26,7 @@ import "worldmonitor/economic/v1/get_eu_gas_storage.proto"; import "worldmonitor/economic/v1/get_eu_yield_curve.proto"; import "worldmonitor/economic/v1/get_eu_fsi.proto"; import "worldmonitor/economic/v1/get_economic_stress.proto"; +import "worldmonitor/economic/v1/get_fao_food_price_index.proto"; // EconomicService provides APIs for macroeconomic data from FRED, World Bank, and EIA. service EconomicService { @@ -145,4 +146,9 @@ service EconomicService { rpc GetEconomicStress(GetEconomicStressRequest) returns (GetEconomicStressResponse) { option (sebuf.http.config) = {path: "/get-economic-stress", method: HTTP_METHOD_GET}; } + + // GetFaoFoodPriceIndex retrieves the FAO Food Price Index for the past 12 months. + rpc GetFaoFoodPriceIndex(GetFaoFoodPriceIndexRequest) returns (GetFaoFoodPriceIndexResponse) { + option (sebuf.http.config) = {path: "/get-fao-food-price-index", method: HTTP_METHOD_GET}; + } } diff --git a/scripts/ais-relay.cjs b/scripts/ais-relay.cjs index 11fd0aec1..6c8e88855 100644 --- a/scripts/ais-relay.cjs +++ b/scripts/ais-relay.cjs @@ -9414,7 +9414,8 @@ Market & Crypto: Economic & Energy: macroSignals, bisPolicy, bisExchange, bisCredit, nationalDebt, bigmac, fuelPrices, euGasStorage, natGasStorage, crudeInventories, ecbFxRates, euFsi, groceryBasket, - eurostatCountryData, progressData, renewableEnergy, spending, correlationCards + eurostatCountryData, progressData, renewableEnergy, spending, correlationCards, + faoFoodPriceIndex Tech & Intelligence: techReadiness, techEvents, riskScores, crossSourceSignals, securityAdvisories, @@ -9994,7 +9995,8 @@ Market & Crypto: Economic & Energy: macroSignals, bisPolicy, bisExchange, bisCredit, nationalDebt, bigmac, fuelPrices, euGasStorage, natGasStorage, crudeInventories, ecbFxRates, euFsi, groceryBasket, - eurostatCountryData, progressData, renewableEnergy, spending, correlationCards + eurostatCountryData, progressData, renewableEnergy, spending, correlationCards, + faoFoodPriceIndex Tech & Intelligence: techReadiness, techEvents, riskScores, crossSourceSignals, securityAdvisories, diff --git a/scripts/seed-fao-food-price-index.mjs b/scripts/seed-fao-food-price-index.mjs new file mode 100644 index 000000000..0b639a482 --- /dev/null +++ b/scripts/seed-fao-food-price-index.mjs @@ -0,0 +1,100 @@ +#!/usr/bin/env node + +/** + * Seed script: fetches the FAO Food Price Index (FFPI) CSV and writes + * the past 12 months to Upstash Redis. + * + * Source: https://www.fao.org/media/docs/worldfoodsituationlibraries/default-document-library/food_price_indices_data.csv + * Released: first Friday of each month ~08:30 UTC + * + * Railway cron: 45 8 * * * (daily at 08:45 UTC — over-seeds safely; FAO releases ~first Friday of month) + */ + +import { loadEnvFile, CHROME_UA, runSeed } from './_seed-utils.mjs'; + +loadEnvFile(import.meta.url); + +const CANONICAL_KEY = 'economic:fao-ffpi:v1'; +const CACHE_TTL = 90 * 24 * 60 * 60; // 90 days — monthly seed, 3x interval per gold standard +const CSV_URL = 'https://www.fao.org/media/docs/worldfoodsituationlibraries/default-document-library/food_price_indices_data.csv'; +const MONTHS_TO_KEEP = 12; + +async function fetchFaoFfpi() { + const resp = await globalThis.fetch(CSV_URL, { + headers: { 'User-Agent': CHROME_UA, 'Accept': 'text/csv,text/plain,*/*' }, + signal: AbortSignal.timeout(20_000), + }); + if (!resp.ok) throw new Error(`FAO CSV HTTP ${resp.status}`); + + const raw = await resp.text(); + // Strip BOM if present + const text = raw.startsWith('\ufeff') ? raw.slice(1) : raw; + + // CSV structure: + // Row 0: "FAO Food Price Index" (title) + // Row 1: "2014-2016=100" (base note) + // Row 2: "Date,Food Price Index,Meat,Dairy,Cereals,Oils,Sugar" (header) + // Row 3: blank + // Row 4+: YYYY-MM,value,... + const lines = text.split('\n').map(l => l.trim()).filter(Boolean); + + const dataLines = lines.filter(l => /^\d{4}-\d{2},/.test(l)); + if (dataLines.length === 0) throw new Error('FAO CSV: no data rows found'); + + function parseVal(s) { + const v = parseFloat(s); + return Number.isFinite(v) ? v : 0; + } + + const allPoints = dataLines.map(line => { + const [date, ffpi, meat, dairy, cereals, oils, sugar] = line.split(',').map(s => s.trim()); + return { + date, + ffpi: parseVal(ffpi), + meat: parseVal(meat), + dairy: parseVal(dairy), + cereals: parseVal(cereals), + oils: parseVal(oils), + sugar: parseVal(sugar), + }; + }); + + // Need MONTHS_TO_KEEP + 1 points: 12 for display + 1 year-ago for YoY + const recentPoints = allPoints.slice(-(MONTHS_TO_KEEP + 1)); + + if (recentPoints.length < 2) throw new Error('FAO CSV: insufficient data rows'); + + const last = recentPoints[recentPoints.length - 1]; + const prev = recentPoints[recentPoints.length - 2]; + const yearAgo = recentPoints.length >= 13 ? recentPoints[recentPoints.length - 13] : null; + + const momPct = prev.ffpi > 0 + ? +((last.ffpi - prev.ffpi) / prev.ffpi * 100).toFixed(2) + : 0; + + const yoyPct = yearAgo && yearAgo.ffpi > 0 + ? +((last.ffpi - yearAgo.ffpi) / yearAgo.ffpi * 100).toFixed(2) + : 0; + + // Store only the last 12 months in the response + const points = recentPoints.slice(-MONTHS_TO_KEEP); + + console.log(` Latest: ${last.date} FFPI=${last.ffpi} MoM=${momPct}% YoY=${yoyPct}%`); + + return { + points, + fetchedAt: new Date().toISOString(), + currentFfpi: last.ffpi, + momPct, + yoyPct, + }; +} + +const isMain = process.argv[1]?.endsWith('seed-fao-food-price-index.mjs'); +if (isMain) { + await runSeed('economic', 'fao-ffpi', CANONICAL_KEY, fetchFaoFfpi, { + ttlSeconds: CACHE_TTL, + validateFn: (data) => data?.points?.length > 0, + recordCount: (data) => data?.points?.length || 0, + }); +} diff --git a/server/_shared/cache-keys.ts b/server/_shared/cache-keys.ts index 16bb2ca0e..ca89dd091 100644 --- a/server/_shared/cache-keys.ts +++ b/server/_shared/cache-keys.ts @@ -126,6 +126,7 @@ export const BOOTSTRAP_CACHE_KEYS: Record = { pizzint: 'intelligence:pizzint:seed:v1', diseaseOutbreaks: 'health:disease-outbreaks:v1', economicStress: 'economic:stress-index:v1', + faoFoodPriceIndex: 'economic:fao-ffpi:v1', }; export const BOOTSTRAP_TIERS: Record = { @@ -169,4 +170,5 @@ export const BOOTSTRAP_TIERS: Record = { pizzint: 'slow', diseaseOutbreaks: 'slow', economicStress: 'slow', + faoFoodPriceIndex: 'slow', }; diff --git a/server/gateway.ts b/server/gateway.ts index 1045fb806..0c5238724 100644 --- a/server/gateway.ts +++ b/server/gateway.ts @@ -139,6 +139,7 @@ const RPC_CACHE_TIER: Record = { '/api/economic/v1/list-grocery-basket-prices': 'static', '/api/economic/v1/list-bigmac-prices': 'static', '/api/economic/v1/list-fuel-prices': 'static', + '/api/economic/v1/get-fao-food-price-index': 'static', '/api/economic/v1/get-crude-inventories': 'static', '/api/economic/v1/get-nat-gas-storage': 'static', '/api/economic/v1/get-eu-yield-curve': 'daily', diff --git a/server/worldmonitor/economic/v1/get-fao-food-price-index.ts b/server/worldmonitor/economic/v1/get-fao-food-price-index.ts new file mode 100644 index 000000000..c0f16f946 --- /dev/null +++ b/server/worldmonitor/economic/v1/get-fao-food-price-index.ts @@ -0,0 +1,35 @@ +/** + * RPC: getFaoFoodPriceIndex -- reads seeded FAO FFPI data from Railway seed cache. + * All data fetching happens in seed-fao-food-price-index.mjs on Railway. + */ + +import type { + ServerContext, + GetFaoFoodPriceIndexRequest, + GetFaoFoodPriceIndexResponse, +} from '../../../../src/generated/server/worldmonitor/economic/v1/service_server'; + +import { getCachedJson } from '../../../_shared/redis'; + +const SEED_CACHE_KEY = 'economic:fao-ffpi:v1'; + +const EMPTY: GetFaoFoodPriceIndexResponse = { + points: [], + fetchedAt: '', + currentFfpi: 0, + momPct: 0, + yoyPct: 0, +}; + +export async function getFaoFoodPriceIndex( + _ctx: ServerContext, + _req: GetFaoFoodPriceIndexRequest, +): Promise { + try { + const result = await getCachedJson(SEED_CACHE_KEY, true) as GetFaoFoodPriceIndexResponse | null; + if (!result?.points?.length) return EMPTY; + return result; + } catch { + return EMPTY; + } +} diff --git a/server/worldmonitor/economic/v1/handler.ts b/server/worldmonitor/economic/v1/handler.ts index 70f0c53fa..f0322b776 100644 --- a/server/worldmonitor/economic/v1/handler.ts +++ b/server/worldmonitor/economic/v1/handler.ts @@ -23,6 +23,7 @@ import { getEuGasStorage } from './get-eu-gas-storage'; import { getEuYieldCurve } from './get-eu-yield-curve'; import { getEuFsi } from './get-eu-fsi'; import { getEconomicStress } from './get-economic-stress'; +import { getFaoFoodPriceIndex } from './get-fao-food-price-index'; export const economicHandler: EconomicServiceHandler = { getFredSeries, @@ -48,4 +49,5 @@ export const economicHandler: EconomicServiceHandler = { getEuYieldCurve, getEuFsi, getEconomicStress, + getFaoFoodPriceIndex, }; diff --git a/src/App.ts b/src/App.ts index b07d4af30..06aa99fe9 100644 --- a/src/App.ts +++ b/src/App.ts @@ -36,6 +36,7 @@ import type { GulfEconomiesPanel } from '@/components/GulfEconomiesPanel'; import type { GroceryBasketPanel } from '@/components/GroceryBasketPanel'; import type { BigMacPanel } from '@/components/BigMacPanel'; import type { FuelPricesPanel } from '@/components/FuelPricesPanel'; +import type { FaoFoodPriceIndexPanel } from '@/components/FaoFoodPriceIndexPanel'; import type { ConsumerPricesPanel } from '@/components/ConsumerPricesPanel'; import type { DefensePatentsPanel } from '@/components/DefensePatentsPanel'; import type { MacroTilesPanel } from '@/components/MacroTilesPanel'; @@ -286,6 +287,10 @@ export class App { const panel = this.state.panels['fuel-prices'] as FuelPricesPanel | undefined; if (panel) primeTask('fuel-prices', () => panel.fetchData()); } + if (shouldPrime('fao-food-price-index')) { + const panel = this.state.panels['fao-food-price-index'] as FaoFoodPriceIndexPanel | undefined; + if (panel) primeTask('fao-food-price-index', () => panel.fetchData()); + } if (shouldPrime('consumer-prices')) { const panel = this.state.panels['consumer-prices'] as ConsumerPricesPanel | undefined; if (panel) primeTask('consumer-prices', () => panel.fetchData()); @@ -1301,6 +1306,13 @@ export class App { () => this.isPanelNearViewport('fuel-prices') ); + this.refreshScheduler.scheduleRefresh( + 'fao-food-price-index', + () => (this.state.panels['fao-food-price-index'] as FaoFoodPriceIndexPanel).fetchData(), + REFRESH_INTERVALS.faoFoodPriceIndex, + () => this.isPanelNearViewport('fao-food-price-index') + ); + this.refreshScheduler.scheduleRefresh( 'macro-tiles', () => (this.state.panels['macro-tiles'] as MacroTilesPanel).fetchData(), diff --git a/src/app/panel-layout.ts b/src/app/panel-layout.ts index 12ddf84ac..46f1ba2a3 100644 --- a/src/app/panel-layout.ts +++ b/src/app/panel-layout.ts @@ -50,6 +50,7 @@ import { GroceryBasketPanel, BigMacPanel, FuelPricesPanel, + FaoFoodPriceIndexPanel, WorldClockPanel, AirlineIntelPanel, AviationCommandBar, @@ -991,6 +992,10 @@ export class PanelLayoutManager implements AppModule { this.ctx.panels['fuel-prices'] = new FuelPricesPanel(); } + if (this.shouldCreatePanel('fao-food-price-index') && !this.ctx.panels['fao-food-price-index']) { + this.ctx.panels['fao-food-price-index'] = new FaoFoodPriceIndexPanel(); + } + if (this.shouldCreatePanel('live-news') && (getDefaultLiveChannels().length > 0 || loadChannelsFromStorage().length > 0)) { this.ctx.panels['live-news'] = new LiveNewsPanel(); diff --git a/src/components/FaoFoodPriceIndexPanel.ts b/src/components/FaoFoodPriceIndexPanel.ts new file mode 100644 index 000000000..101133c1c --- /dev/null +++ b/src/components/FaoFoodPriceIndexPanel.ts @@ -0,0 +1,156 @@ +import { Panel } from './Panel'; +import { t } from '@/services/i18n'; +import { escapeHtml } from '@/utils/sanitize'; +import { getHydratedData } from '@/services/bootstrap'; +import { getRpcBaseUrl } from '@/services/rpc-client'; +import { EconomicServiceClient } from '@/generated/client/worldmonitor/economic/v1/service_client'; +import type { GetFaoFoodPriceIndexResponse, FaoFoodPricePoint } from '@/generated/client/worldmonitor/economic/v1/service_client'; + +const client = new EconomicServiceClient(getRpcBaseUrl(), { fetch: (...args: Parameters) => globalThis.fetch(...args) }); + +const SVG_W = 480; +const SVG_H = 140; +const ML = 36; +const MR = 12; +const MT = 8; +const MB = 20; +const CW = SVG_W - ML - MR; +const CH = SVG_H - MT - MB; + +const SERIES: { key: keyof FaoFoodPricePoint; color: string; label: string }[] = [ + { key: 'ffpi', color: '#f5a623', label: 'Food' }, + { key: 'cereals', color: '#7ed321', label: 'Cereals' }, + { key: 'meat', color: '#e86c6c', label: 'Meat' }, + { key: 'dairy', color: '#74c8e8', label: 'Dairy' }, + { key: 'oils', color: '#b57ce8', label: 'Oils' }, + { key: 'sugar', color: '#f0c36a', label: 'Sugar' }, +]; + +function xPos(i: number, total: number): number { + if (total <= 1) return ML + CW / 2; + return ML + (i / (total - 1)) * CW; +} + +function yPos(v: number, yMin: number, yMax: number): number { + const range = yMax - yMin || 1; + return MT + CH - ((v - yMin) / range) * CH; +} + +function buildLine(points: FaoFoodPricePoint[], key: keyof FaoFoodPricePoint, yMin: number, yMax: number): string { + const coords = points + .map((p, i) => { + const v = p[key] as number; + if (!Number.isFinite(v) || v <= 0) return null; + return `${xPos(i, points.length).toFixed(1)},${yPos(v, yMin, yMax).toFixed(1)}`; + }) + .filter(Boolean) + .join(' '); + return coords; +} + +function buildChart(points: FaoFoodPricePoint[]): string { + if (!points.length) return ''; + + // Collect all values to compute y range + const vals: number[] = []; + for (const p of points) { + for (const s of SERIES) { + const v = p[s.key] as number; + if (Number.isFinite(v) && v > 0) vals.push(v); + } + } + const yMin = Math.floor(Math.min(...vals) * 0.96); + const yMax = Math.ceil(Math.max(...vals) * 1.02); + + // Y-axis labels (4 ticks: bottom, 1/3, 2/3, top) + const yAxis = [0, 1, 2, 3].map(i => { + const v = yMin + ((yMax - yMin) / 3) * i; + const y = yPos(v, yMin, yMax); + return ` + + ${v.toFixed(0)}`; + }).join(''); + + // X-axis labels (show every 3rd month to avoid crowding) + const xAxis = points.map((p, i) => { + if (i % 3 !== 0 && i !== points.length - 1) return ''; + const x = xPos(i, points.length); + const label = p.date; + return `${escapeHtml(label)}`; + }).join(''); + + // Series lines + const lines = SERIES.map(s => { + const coords = buildLine(points, s.key, yMin, yMax); + if (!coords) return ''; + return ``; + }).join(''); + + return `${yAxis}${xAxis}${lines}`; +} + +function buildLegend(): string { + return SERIES.map(s => + `${escapeHtml(t(`components.faoFoodPriceIndex.${s.key}`))}` + ).join(''); +} + +export class FaoFoodPriceIndexPanel extends Panel { + constructor() { + super({ id: 'fao-food-price-index', title: t('panels.faoFoodPriceIndex'), infoTooltip: t('components.faoFoodPriceIndex.infoTooltip') }); + } + + public async fetchData(): Promise { + try { + const hydrated = getHydratedData('faoFoodPriceIndex') as GetFaoFoodPriceIndexResponse | undefined; + if (hydrated?.points?.length) { + if (!this.element?.isConnected) return; + this.renderChart(hydrated); + void client.getFaoFoodPriceIndex({}).then(data => { + if (!this.element?.isConnected || !data.points?.length) return; + this.renderChart(data); + }).catch(() => {}); + return; + } + const data = await client.getFaoFoodPriceIndex({}); + if (!this.element?.isConnected) return; + this.renderChart(data); + } catch (err) { + if (this.isAbortError(err)) return; + if (!this.element?.isConnected) return; + this.showError(t('common.failedMarketData'), () => void this.fetchData()); + } + } + + private renderChart(data: GetFaoFoodPriceIndexResponse): void { + if (!data.points?.length) { + this.showError(t('common.failedMarketData'), () => void this.fetchData()); + return; + } + + const momSign = data.momPct >= 0 ? '+' : ''; + const yoySign = data.yoyPct >= 0 ? '+' : ''; + const momCls = data.momPct >= 0 ? 'fao-up' : 'fao-down'; + const yoyCls = data.yoyPct >= 0 ? 'fao-up' : 'fao-down'; + const latest = data.points[data.points.length - 1]; + + const headline = ` +
+
+ ${data.currentFfpi.toFixed(1)} + ${escapeHtml(t('components.faoFoodPriceIndex.indexLabel'))} +
+
+ ${momSign}${data.momPct.toFixed(1)}% ${escapeHtml(t('components.faoFoodPriceIndex.mom'))} + ${yoySign}${data.yoyPct.toFixed(1)}% ${escapeHtml(t('components.faoFoodPriceIndex.yoy'))} +
+
${escapeHtml(t('components.faoFoodPriceIndex.asOf'))} ${escapeHtml(latest?.date ?? '')}
+
`; + + const chart = buildChart(data.points); + const legend = `
${buildLegend()}
`; + const base = `
${escapeHtml(t('components.faoFoodPriceIndex.baseNote'))}
`; + + this.setContent(`
${headline}${chart}${legend}${base}
`); + } +} diff --git a/src/components/index.ts b/src/components/index.ts index 672d138ff..6817ea17e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -61,6 +61,7 @@ export * from './GulfEconomiesPanel'; export * from './GroceryBasketPanel'; export * from './BigMacPanel'; export * from './FuelPricesPanel'; +export * from './FaoFoodPriceIndexPanel'; export * from './WorldClockPanel'; export { AirlineIntelPanel } from './AirlineIntelPanel'; export { AviationCommandBar } from './AviationCommandBar'; diff --git a/src/config/commands.ts b/src/config/commands.ts index 688c8bb0f..1a5927604 100644 --- a/src/config/commands.ts +++ b/src/config/commands.ts @@ -163,6 +163,7 @@ export const COMMANDS: Command[] = [ { id: 'panel:grocery-basket', keywords: ['grocery', 'grocery basket', 'grocery index', 'food prices', 'supermarket'], label: 'Panel: Grocery Index', icon: '\u{1F96C}', category: 'panels' }, { id: 'panel:bigmac', keywords: ['bigmac', 'big mac', 'big mac index', 'purchasing power parity', 'ppp'], label: 'Panel: Big Mac Index', icon: '\u{1F354}', category: 'panels' }, { id: 'panel:fuel-prices', keywords: ['fuel prices', 'gas prices', 'gasoline', 'diesel', 'petrol', 'fuel cost', 'pump prices'], label: 'Panel: Fuel Prices', icon: '\u26FD', category: 'panels' }, + { id: 'panel:fao-food-price-index', keywords: ['fao', 'food price index', 'ffpi', 'food prices', 'cereals', 'fao food', 'global food prices', 'food inflation'], label: 'Panel: FAO Food Price Index', icon: '\u{1F33E}', category: 'panels' }, { id: 'panel:national-debt', keywords: ['national debt', 'debt clock', 'government debt', 'deficit'], label: 'Panel: National Debt Clock', icon: '\u{1F4B8}', category: 'panels' }, { id: 'panel:fsi', keywords: ['fsi', 'financial stress', 'financial stress indicator', 'systemic risk'], label: 'Panel: Financial Stress Indicator', icon: '\u{1F4C9}', category: 'panels' }, { id: 'panel:yield-curve', keywords: ['yield curve', 'rates', 'treasury', 'ecb rates', 'bond yield', 'inversion'], label: 'Panel: Yield Curve & Rates', icon: '\u{1F4C8}', category: 'panels' }, diff --git a/src/config/panels.ts b/src/config/panels.ts index 66b8e8207..d914c8cd4 100644 --- a/src/config/panels.ts +++ b/src/config/panels.ts @@ -74,6 +74,7 @@ const FULL_PANELS: Record = { 'grocery-basket': { name: 'Grocery Index', enabled: false, priority: 2 }, 'bigmac': { name: 'Big Mac Index', enabled: false, priority: 2 }, 'fuel-prices': { name: 'Fuel Prices', enabled: false, priority: 2 }, + 'fao-food-price-index': { name: 'FAO Food Price Index', enabled: false, priority: 2 }, 'etf-flows': { name: 'BTC ETF Tracker', enabled: true, priority: 2 }, stablecoins: { name: 'Stablecoins', enabled: true, priority: 2 }, 'ucdp-events': { name: 'UCDP Conflict Events', enabled: true, priority: 2 }, diff --git a/src/config/variants/base.ts b/src/config/variants/base.ts index 4cc72f84b..97d444d92 100644 --- a/src/config/variants/base.ts +++ b/src/config/variants/base.ts @@ -47,6 +47,7 @@ export const REFRESH_INTERVALS = { gulfEconomies: 10 * 60 * 1000, groceryBasket: 6 * 60 * 60 * 1000, fuelPrices: 6 * 60 * 60 * 1000, + faoFoodPriceIndex: 24 * 60 * 60 * 1000, // monthly data; refresh daily is sufficient intelligence: 15 * 60 * 1000, correlationEngine: 5 * 60 * 1000, defensePatents: 24 * 60 * 60 * 1000, // 24h — data is weekly, daily poll is sufficient diff --git a/src/generated/client/worldmonitor/economic/v1/service_client.ts b/src/generated/client/worldmonitor/economic/v1/service_client.ts index f634cce7b..1b400296f 100644 --- a/src/generated/client/worldmonitor/economic/v1/service_client.ts +++ b/src/generated/client/worldmonitor/economic/v1/service_client.ts @@ -527,6 +527,27 @@ export interface EconomicStressComponent { missing: boolean; } +export interface GetFaoFoodPriceIndexRequest { +} + +export interface GetFaoFoodPriceIndexResponse { + points: FaoFoodPricePoint[]; + fetchedAt: string; + currentFfpi: number; + momPct: number; + yoyPct: number; +} + +export interface FaoFoodPricePoint { + date: string; + ffpi: number; + meat: number; + dairy: number; + cereals: number; + oils: number; + sugar: number; +} + export interface FieldViolation { field: string; description: string; @@ -1125,6 +1146,29 @@ export class EconomicServiceClient { return await resp.json() as GetEconomicStressResponse; } + async getFaoFoodPriceIndex(req: GetFaoFoodPriceIndexRequest, options?: EconomicServiceCallOptions): Promise { + let path = "/api/economic/v1/get-fao-food-price-index"; + const url = this.baseURL + path; + + const headers: Record = { + "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 GetFaoFoodPriceIndexResponse; + } + private async handleError(resp: Response): Promise { const body = await resp.text(); if (resp.status === 400) { diff --git a/src/generated/server/worldmonitor/economic/v1/service_server.ts b/src/generated/server/worldmonitor/economic/v1/service_server.ts index 37b48b2de..6e454e214 100644 --- a/src/generated/server/worldmonitor/economic/v1/service_server.ts +++ b/src/generated/server/worldmonitor/economic/v1/service_server.ts @@ -527,6 +527,27 @@ export interface EconomicStressComponent { missing: boolean; } +export interface GetFaoFoodPriceIndexRequest { +} + +export interface GetFaoFoodPriceIndexResponse { + points: FaoFoodPricePoint[]; + fetchedAt: string; + currentFfpi: number; + momPct: number; + yoyPct: number; +} + +export interface FaoFoodPricePoint { + date: string; + ffpi: number; + meat: number; + dairy: number; + cereals: number; + oils: number; + sugar: number; +} + export interface FieldViolation { field: string; description: string; @@ -595,6 +616,7 @@ export interface EconomicServiceHandler { getEuYieldCurve(ctx: ServerContext, req: GetEuYieldCurveRequest): Promise; getEuFsi(ctx: ServerContext, req: GetEuFsiRequest): Promise; getEconomicStress(ctx: ServerContext, req: GetEconomicStressRequest): Promise; + getFaoFoodPriceIndex(ctx: ServerContext, req: GetFaoFoodPriceIndexRequest): Promise; } export function createEconomicServiceRoutes( @@ -1527,6 +1549,43 @@ export function createEconomicServiceRoutes( } }, }, + { + method: "GET", + path: "/api/economic/v1/get-fao-food-price-index", + handler: async (req: Request): Promise => { + try { + const pathParams: Record = {}; + const body = {} as GetFaoFoodPriceIndexRequest; + + const ctx: ServerContext = { + request: req, + pathParams, + headers: Object.fromEntries(req.headers.entries()), + }; + + const result = await handler.getFaoFoodPriceIndex(ctx, body); + return new Response(JSON.stringify(result as GetFaoFoodPriceIndexResponse), { + 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" }, + }); + } + }, + }, ]; } diff --git a/src/locales/en.json b/src/locales/en.json index 2826696da..c52e0093b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -362,6 +362,7 @@ "bigmacDesc": "Big Mac prices across Middle East countries (Big Mac Index)", "bigmacWow": "WoW", "bigmacCountry": "Country", + "faoFoodPriceIndex": "FAO Food Price Index", "fuelPrices": "Fuel Prices", "fuelPricesDesc": "Retail gasoline and diesel prices across 30+ countries worldwide", "fuelPricesCountry": "Country", @@ -1937,6 +1938,20 @@ "infoTooltip": "Fuel Prices Retail pump prices for gasoline and diesel across 30+ countries, normalized to USD/liter for comparison. Prices are sourced from official government open-data programs and updated weekly. Cheapest and most expensive countries are highlighted. WoW change shown when prior week data is available.", "countries": "countries" }, + "faoFoodPriceIndex": { + "infoTooltip": "FAO Food Price Index The UN Food and Agriculture Organization's FFPI tracks international export prices for a basket of food commodities (Cereals, Dairy, Meat, Oils, Sugar), base 2014-2016=100. Updated monthly on the first Friday of each month.", + "indexLabel": "FFPI (2014-16=100)", + "mom": "MoM", + "yoy": "YoY", + "asOf": "As of", + "baseNote": "Base: 2014-2016 = 100 | Source: UN FAO", + "ffpi": "Food", + "cereals": "Cereals", + "meat": "Meat", + "dairy": "Dairy", + "oils": "Oils", + "sugar": "Sugar" + }, "panel": { "showMethodologyInfo": "Show methodology info", "dragToResize": "Drag to resize (double-click to reset)", diff --git a/src/styles/main.css b/src/styles/main.css index 8c631d8f0..b49404767 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -22096,6 +22096,22 @@ body.map-width-resizing { margin-top: 1px; } +/* FAO Food Price Index panel */ +.fao-food-price-index-panel { padding: 4px 0; } +.fao-headline { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 4px 12px 8px; } +.fao-headline-primary { display: flex; align-items: baseline; gap: 6px; } +.fao-index-value { font-size: 1.6rem; font-weight: 700; color: #f5a623; line-height: 1; } +.fao-index-label { font-size: 0.65rem; opacity: 0.55; } +.fao-headline-changes { display: flex; gap: 8px; } +.fao-change { font-size: 0.75rem; font-weight: 600; } +.fao-up { color: #f87171; } +.fao-down { color: #4ade80; } +.fao-as-of { font-size: 0.65rem; opacity: 0.4; margin-left: auto; } +.fao-legend { display: flex; flex-wrap: wrap; gap: 8px; padding: 6px 12px 2px; } +.fao-legend-item { display: flex; align-items: center; gap: 4px; font-size: 0.65rem; opacity: 0.7; } +.fao-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } +.fao-base-note { font-size: 0.6rem; opacity: 0.35; padding: 2px 12px 6px; text-align: right; } + /* ── Risk Score Badge on News Cards ──────────────────────────── */ .risk-score-badge {