diff --git a/api/health.js b/api/health.js index 84e3edc3d..4f22a4333 100644 --- a/api/health.js +++ b/api/health.js @@ -55,6 +55,9 @@ const BOOTSTRAP_KEYS = { otherTokens: 'market:other-tokens:v1', fredBatch: 'economic:fred:v1:FEDFUNDS:0', fearGreedIndex: 'market:fear-greed:v1', + earningsCalendar: 'market:earnings-calendar:v1', + econCalendar: 'economic:econ-calendar:v1', + cotPositioning: 'market:cot:v1', }; const STANDALONE_KEYS = { @@ -180,6 +183,9 @@ const SEED_META = { gscpi: { key: 'seed-meta:economic:gscpi', maxStaleMin: 2880 }, // 24h interval; 2880min = 48h = 2x interval fearGreedIndex: { key: 'seed-meta:market:fear-greed', maxStaleMin: 720 }, // 6h cron; 720min = 12h = 2x interval hormuzTracker: { key: 'seed-meta:supply_chain:hormuz_tracker', maxStaleMin: 2880 }, // daily cron; 2880min = 48h = 2x interval + earningsCalendar: { key: 'seed-meta:market:earnings-calendar', maxStaleMin: 1440 }, // 12h cron; 1440min = 24h = 2x interval + econCalendar: { key: 'seed-meta:economic:econ-calendar', maxStaleMin: 1440 }, // 12h cron; 1440min = 24h = 2x interval + cotPositioning: { key: 'seed-meta:market:cot', maxStaleMin: 14400 }, // weekly CFTC release; 14400min = 10d = 1.4x interval (weekend + delay buffer) }; // Standalone keys that are populated on-demand by RPC handlers (not seeds). @@ -246,6 +252,7 @@ function dataSize(parsed) { 'airports', 'closedIcaos', 'categories', 'regions', 'entries', 'satellites', 'sectors', 'statuses', 'scores', 'topics', 'advisories', 'months', 'observations', 'datapoints', 'clusters', + 'earnings', 'instruments', 'charts']) { if (Array.isArray(parsed[k])) return parsed[k].length; } diff --git a/compound-engineering.local.md b/compound-engineering.local.md new file mode 100644 index 000000000..f75d3c9c7 --- /dev/null +++ b/compound-engineering.local.md @@ -0,0 +1,22 @@ +--- +review_agents: + - compound-engineering:review:kieran-typescript-reviewer + - compound-engineering:review:security-sentinel + - compound-engineering:review:performance-oracle + - compound-engineering:review:architecture-strategist + - compound-engineering:review:code-simplicity-reviewer +--- + +# WorldMonitor Review Context + +TypeScript monorepo: Vanilla TS panels (no React), sebuf proto RPCs, Redis-cached seed data, +Vercel edge functions, Railway cron seeds. + +Key patterns: + +- Panels extend `Panel` base class with `fetchData()` returning boolean, `setContent(html)`, `showError(msg, retry)` +- Private `_hasData` guard prevents overwriting good data with error on retry +- Seed scripts use `runSeed(domain, name, key, fetchFn, options)` with TTL β‰₯ 3Γ— seed interval +- RPC handlers read from Redis via `getCachedJson(key, true)`, return typed proto response +- `cachedFetchJson` coalesces concurrent cache misses β€” use it for on-demand fetches +- All panels registered in `src/config/panels.ts` (FINANCE_PANELS + FULL_PANELS) and `src/app/panel-layout.ts` diff --git a/docs/api/EconomicService.openapi.json b/docs/api/EconomicService.openapi.json index 390abbd84..fea68601f 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"},"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"},"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"},"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"},"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"},"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"},"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"},"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"},"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-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-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-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"},"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"},"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"},"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"},"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"},"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"},"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"},"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"},"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"},"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"},"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-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-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-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-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 59fd4c971..e67fd3fdd 100644 --- a/docs/api/EconomicService.openapi.yaml +++ b/docs/api/EconomicService.openapi.yaml @@ -457,6 +457,43 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /api/economic/v1/get-economic-calendar: + get: + tags: + - EconomicService + summary: GetEconomicCalendar + description: GetEconomicCalendar retrieves upcoming major economic events (FOMC, CPI, NFP, etc). + operationId: GetEconomicCalendar + parameters: + - name: fromDate + in: query + required: false + schema: + type: string + - name: toDate + in: query + required: false + schema: + type: string + responses: + "200": + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/GetEconomicCalendarResponse' + "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: @@ -1320,3 +1357,45 @@ components: type: string description: Observed value. description: BlsObservation is a single BLS data point. + GetEconomicCalendarRequest: + type: object + properties: + fromDate: + type: string + toDate: + type: string + GetEconomicCalendarResponse: + type: object + properties: + events: + type: array + items: + $ref: '#/components/schemas/EconomicEvent' + fromDate: + type: string + toDate: + type: string + total: + type: integer + format: int32 + unavailable: + type: boolean + EconomicEvent: + type: object + properties: + event: + type: string + country: + type: string + date: + type: string + impact: + type: string + actual: + type: string + estimate: + type: string + previous: + type: string + unit: + type: string diff --git a/docs/api/MarketService.openapi.json b/docs/api/MarketService.openapi.json index 77f0c4138..c5bd676e7 100644 --- a/docs/api/MarketService.openapi.json +++ b/docs/api/MarketService.openapi.json @@ -1 +1 @@ -{"components":{"schemas":{"AnalyzeStockRequest":{"properties":{"includeNews":{"type":"boolean"},"name":{"maxLength":120,"type":"string"},"symbol":{"maxLength":32,"minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"AnalyzeStockResponse":{"properties":{"action":{"type":"string"},"analysisAt":{"description":"Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"analysisId":{"type":"string"},"available":{"type":"boolean"},"biasMa10":{"format":"double","type":"number"},"biasMa20":{"format":"double","type":"number"},"biasMa5":{"format":"double","type":"number"},"bullishFactors":{"items":{"type":"string"},"type":"array"},"changePercent":{"format":"double","type":"number"},"confidence":{"type":"string"},"currency":{"type":"string"},"currentPrice":{"format":"double","type":"number"},"display":{"type":"string"},"engineVersion":{"type":"string"},"fallback":{"type":"boolean"},"generatedAt":{"type":"string"},"headlines":{"items":{"$ref":"#/components/schemas/StockAnalysisHeadline"},"type":"array"},"ma10":{"format":"double","type":"number"},"ma20":{"format":"double","type":"number"},"ma5":{"format":"double","type":"number"},"ma60":{"format":"double","type":"number"},"macdBar":{"format":"double","type":"number"},"macdDea":{"format":"double","type":"number"},"macdDif":{"format":"double","type":"number"},"macdStatus":{"type":"string"},"model":{"type":"string"},"name":{"type":"string"},"newsSearched":{"type":"boolean"},"newsSummary":{"type":"string"},"provider":{"type":"string"},"resistanceLevels":{"items":{"format":"double","type":"number"},"type":"array"},"riskFactors":{"items":{"type":"string"},"type":"array"},"rsi12":{"format":"double","type":"number"},"rsiStatus":{"type":"string"},"signal":{"type":"string"},"signalScore":{"format":"double","type":"number"},"stopLoss":{"format":"double","type":"number"},"summary":{"type":"string"},"supportLevels":{"items":{"format":"double","type":"number"},"type":"array"},"symbol":{"type":"string"},"takeProfit":{"format":"double","type":"number"},"technicalSummary":{"type":"string"},"trendStatus":{"type":"string"},"volumeRatio5d":{"format":"double","type":"number"},"volumeStatus":{"type":"string"},"whyNow":{"type":"string"}},"type":"object"},"BacktestStockEvaluation":{"properties":{"analysisAt":{"description":"Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"analysisId":{"type":"string"},"directionCorrect":{"type":"boolean"},"entryPrice":{"format":"double","type":"number"},"exitPrice":{"format":"double","type":"number"},"outcome":{"type":"string"},"signal":{"type":"string"},"signalScore":{"format":"double","type":"number"},"simulatedReturnPct":{"format":"double","type":"number"},"stopLoss":{"format":"double","type":"number"},"takeProfit":{"format":"double","type":"number"}},"type":"object"},"BacktestStockRequest":{"properties":{"evalWindowDays":{"format":"int32","maximum":30,"minimum":3,"type":"integer"},"name":{"maxLength":120,"type":"string"},"symbol":{"maxLength":32,"minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"BacktestStockResponse":{"properties":{"actionableEvaluations":{"format":"int32","type":"integer"},"available":{"type":"boolean"},"avgSimulatedReturnPct":{"format":"double","type":"number"},"cumulativeSimulatedReturnPct":{"format":"double","type":"number"},"currency":{"type":"string"},"directionAccuracy":{"format":"double","type":"number"},"display":{"type":"string"},"engineVersion":{"type":"string"},"evalWindowDays":{"format":"int32","type":"integer"},"evaluations":{"items":{"$ref":"#/components/schemas/BacktestStockEvaluation"},"type":"array"},"evaluationsRun":{"format":"int32","type":"integer"},"generatedAt":{"type":"string"},"latestSignal":{"type":"string"},"latestSignalScore":{"format":"double","type":"number"},"name":{"type":"string"},"summary":{"type":"string"},"symbol":{"type":"string"},"winRate":{"format":"double","type":"number"}},"type":"object"},"CommodityQuote":{"description":"CommodityQuote represents a commodity price quote from Yahoo Finance.","properties":{"change":{"description":"Percentage change from previous close.","format":"double","type":"number"},"display":{"description":"Display label.","type":"string"},"name":{"description":"Human-readable name.","type":"string"},"price":{"description":"Current price.","format":"double","type":"number"},"sparkline":{"items":{"description":"Sparkline data points.","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Commodity symbol (e.g., \"CL=F\" for crude oil).","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"CryptoQuote":{"description":"CryptoQuote represents a cryptocurrency quote from CoinGecko.","properties":{"change":{"description":"24-hour percentage change.","format":"double","type":"number"},"change7d":{"description":"7-day percentage change.","format":"double","type":"number"},"name":{"description":"Cryptocurrency name (e.g., \"Bitcoin\").","type":"string"},"price":{"description":"Current price in USD.","format":"double","type":"number"},"sparkline":{"items":{"description":"Sparkline data points (recent price history).","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Ticker symbol (e.g., \"BTC\").","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"CryptoSector":{"description":"CryptoSector represents performance data for a crypto market sector.","properties":{"change":{"description":"Average 24h percentage change across sector tokens.","format":"double","type":"number"},"id":{"description":"Sector identifier.","type":"string"},"name":{"description":"Sector display name.","type":"string"}},"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"},"EtfFlow":{"description":"EtfFlow represents a single ETF with estimated flow data.","properties":{"avgVolume":{"description":"Average volume over prior days.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"direction":{"description":"Flow direction: \"inflow\", \"outflow\", or \"neutral\".","type":"string"},"estFlow":{"description":"Estimated dollar flow magnitude.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"issuer":{"description":"Fund issuer (e.g. \"BlackRock\").","type":"string"},"price":{"description":"Latest closing price.","format":"double","type":"number"},"priceChange":{"description":"Day-over-day price change percentage.","format":"double","type":"number"},"ticker":{"description":"Ticker symbol (e.g. \"IBIT\").","minLength":1,"type":"string"},"volume":{"description":"Latest daily volume.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"volumeRatio":{"description":"Volume ratio (latest / average).","format":"double","type":"number"}},"required":["ticker"],"type":"object"},"EtfFlowsSummary":{"description":"EtfFlowsSummary contains aggregate ETF flow stats.","properties":{"etfCount":{"description":"Number of ETFs with data.","format":"int32","type":"integer"},"inflowCount":{"description":"Number of ETFs with inflow.","format":"int32","type":"integer"},"netDirection":{"description":"Net direction: \"NET INFLOW\", \"NET OUTFLOW\", or \"NEUTRAL\".","type":"string"},"outflowCount":{"description":"Number of ETFs with outflow.","format":"int32","type":"integer"},"totalEstFlow":{"description":"Total estimated flow across all ETFs.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"totalVolume":{"description":"Total volume across all ETFs.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"}},"type":"object"},"FearGreedCategory":{"properties":{"contribution":{"format":"double","type":"number"},"degraded":{"type":"boolean"},"inputsJson":{"type":"string"},"score":{"format":"double","type":"number"},"weight":{"format":"double","type":"number"}},"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"},"GetCountryStockIndexRequest":{"description":"GetCountryStockIndexRequest specifies which country's stock index to retrieve.","properties":{"countryCode":{"description":"ISO 3166-1 alpha-2 country code (e.g., \"US\", \"GB\", \"JP\").","pattern":"^[A-Z]{2}$","type":"string"}},"required":["countryCode"],"type":"object"},"GetCountryStockIndexResponse":{"description":"GetCountryStockIndexResponse contains the country's primary stock index data.","properties":{"available":{"description":"Whether stock index data is available for this country.","type":"boolean"},"code":{"description":"ISO 3166-1 alpha-2 country code.","type":"string"},"currency":{"description":"Currency of the index.","type":"string"},"fetchedAt":{"description":"When the data was fetched (ISO 8601).","type":"string"},"indexName":{"description":"Index name (e.g., \"S\u0026P 500\").","type":"string"},"price":{"description":"Latest closing price.","format":"double","type":"number"},"symbol":{"description":"Ticker symbol (e.g., \"^GSPC\").","type":"string"},"weekChangePercent":{"description":"Weekly change percentage.","format":"double","type":"number"}},"type":"object"},"GetFearGreedIndexRequest":{"type":"object"},"GetFearGreedIndexResponse":{"properties":{"aaiiBear":{"format":"double","type":"number"},"aaiiBull":{"format":"double","type":"number"},"breadth":{"$ref":"#/components/schemas/FearGreedCategory"},"cnnFearGreed":{"format":"double","type":"number"},"cnnLabel":{"type":"string"},"compositeLabel":{"type":"string"},"compositeScore":{"format":"double","type":"number"},"credit":{"$ref":"#/components/schemas/FearGreedCategory"},"crossAsset":{"$ref":"#/components/schemas/FearGreedCategory"},"fedRate":{"type":"string"},"hySpread":{"format":"double","type":"number"},"liquidity":{"$ref":"#/components/schemas/FearGreedCategory"},"macro":{"$ref":"#/components/schemas/FearGreedCategory"},"momentum":{"$ref":"#/components/schemas/FearGreedCategory"},"pctAbove200d":{"format":"double","type":"number"},"positioning":{"$ref":"#/components/schemas/FearGreedCategory"},"previousScore":{"format":"double","type":"number"},"putCallRatio":{"format":"double","type":"number"},"seededAt":{"type":"string"},"sentiment":{"$ref":"#/components/schemas/FearGreedCategory"},"trend":{"$ref":"#/components/schemas/FearGreedCategory"},"unavailable":{"type":"boolean"},"vix":{"format":"double","type":"number"},"volatility":{"$ref":"#/components/schemas/FearGreedCategory"},"yield10y":{"format":"double","type":"number"}},"type":"object"},"GetSectorSummaryRequest":{"description":"GetSectorSummaryRequest specifies parameters for retrieving sector performance.","properties":{"period":{"description":"Time period for performance calculation (e.g., \"1d\", \"1w\", \"1m\"). Defaults to \"1d\".","type":"string"}},"type":"object"},"GetSectorSummaryResponse":{"description":"GetSectorSummaryResponse contains sector performance data.","properties":{"sectors":{"items":{"$ref":"#/components/schemas/SectorPerformance"},"type":"array"}},"type":"object"},"GetStockAnalysisHistoryRequest":{"properties":{"includeNews":{"type":"boolean"},"limitPerSymbol":{"format":"int32","maximum":32,"minimum":1,"type":"integer"},"symbols":{"items":{"type":"string"},"type":"array"}},"type":"object"},"GetStockAnalysisHistoryResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/StockAnalysisHistoryItem"},"type":"array"}},"type":"object"},"GulfQuote":{"description":"GulfQuote represents a Gulf region market quote (index, currency, or oil).","properties":{"change":{"format":"double","type":"number"},"country":{"type":"string"},"flag":{"type":"string"},"name":{"type":"string"},"price":{"format":"double","type":"number"},"sparkline":{"items":{"format":"double","type":"number"},"type":"array"},"symbol":{"type":"string"},"type":{"type":"string"}},"type":"object"},"ListAiTokensRequest":{"description":"ListAiTokensRequest retrieves AI crypto token prices.","type":"object"},"ListAiTokensResponse":{"description":"ListAiTokensResponse contains AI token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListCommodityQuotesRequest":{"description":"ListCommodityQuotesRequest specifies which commodities to retrieve.","properties":{"symbols":{"items":{"description":"Commodity symbols to retrieve (Yahoo symbols). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListCommodityQuotesResponse":{"description":"ListCommodityQuotesResponse contains commodity quotes.","properties":{"quotes":{"items":{"$ref":"#/components/schemas/CommodityQuote"},"type":"array"}},"type":"object"},"ListCryptoQuotesRequest":{"description":"ListCryptoQuotesRequest specifies which cryptocurrencies to retrieve.","properties":{"ids":{"items":{"description":"Cryptocurrency IDs to retrieve (CoinGecko IDs). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListCryptoQuotesResponse":{"description":"ListCryptoQuotesResponse contains cryptocurrency quotes.","properties":{"quotes":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListCryptoSectorsRequest":{"description":"ListCryptoSectorsRequest retrieves crypto sector performance.","type":"object"},"ListCryptoSectorsResponse":{"description":"ListCryptoSectorsResponse contains crypto sector performance data.","properties":{"sectors":{"items":{"$ref":"#/components/schemas/CryptoSector"},"type":"array"}},"type":"object"},"ListDefiTokensRequest":{"description":"ListDefiTokensRequest retrieves DeFi token prices.","type":"object"},"ListDefiTokensResponse":{"description":"ListDefiTokensResponse contains DeFi token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListEtfFlowsRequest":{"description":"ListEtfFlowsRequest is empty; the handler uses a fixed list of BTC spot ETFs.","type":"object"},"ListEtfFlowsResponse":{"description":"ListEtfFlowsResponse contains BTC spot ETF flow data.","properties":{"etfs":{"items":{"$ref":"#/components/schemas/EtfFlow"},"type":"array"},"rateLimited":{"description":"True when the upstream API rate-limited the request.","type":"boolean"},"summary":{"$ref":"#/components/schemas/EtfFlowsSummary"},"timestamp":{"description":"Timestamp of the data fetch (ISO 8601).","type":"string"}},"type":"object"},"ListGulfQuotesRequest":{"type":"object"},"ListGulfQuotesResponse":{"properties":{"quotes":{"items":{"$ref":"#/components/schemas/GulfQuote"},"type":"array"},"rateLimited":{"type":"boolean"}},"type":"object"},"ListMarketQuotesRequest":{"description":"ListMarketQuotesRequest specifies which stock/index symbols to retrieve.","properties":{"symbols":{"items":{"description":"Ticker symbols to retrieve (e.g., [\"AAPL\", \"^GSPC\"]). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListMarketQuotesResponse":{"description":"ListMarketQuotesResponse contains stock and index quotes.","properties":{"finnhubSkipped":{"description":"True when the Finnhub API key is not configured and stock quotes were skipped.","type":"boolean"},"quotes":{"items":{"$ref":"#/components/schemas/MarketQuote"},"type":"array"},"rateLimited":{"description":"True when the upstream API rate-limited the request.","type":"boolean"},"skipReason":{"description":"Human-readable reason when Finnhub was skipped (e.g., \"FINNHUB_API_KEY not configured\").","type":"string"}},"type":"object"},"ListOtherTokensRequest":{"description":"ListOtherTokensRequest retrieves other/trending crypto token prices.","type":"object"},"ListOtherTokensResponse":{"description":"ListOtherTokensResponse contains other token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListStablecoinMarketsRequest":{"description":"ListStablecoinMarketsRequest specifies which stablecoins to retrieve.","properties":{"coins":{"items":{"description":"CoinGecko IDs to retrieve (e.g. \"tether,usd-coin\"). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListStablecoinMarketsResponse":{"description":"ListStablecoinMarketsResponse contains stablecoin market data.","properties":{"stablecoins":{"items":{"$ref":"#/components/schemas/Stablecoin"},"type":"array"},"summary":{"$ref":"#/components/schemas/StablecoinSummary"},"timestamp":{"description":"Timestamp of the data fetch (ISO 8601).","type":"string"}},"type":"object"},"ListStoredStockBacktestsRequest":{"properties":{"evalWindowDays":{"format":"int32","maximum":30,"minimum":3,"type":"integer"},"symbols":{"items":{"type":"string"},"type":"array"}},"type":"object"},"ListStoredStockBacktestsResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/BacktestStockResponse"},"type":"array"}},"type":"object"},"MarketQuote":{"description":"MarketQuote represents a stock or index quote from Finnhub or Yahoo Finance.","properties":{"change":{"description":"Percentage change from previous close.","format":"double","type":"number"},"display":{"description":"Display label.","type":"string"},"name":{"description":"Human-readable name.","type":"string"},"price":{"description":"Current price.","format":"double","type":"number"},"sparkline":{"items":{"description":"Sparkline data points (recent price history).","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Ticker symbol (e.g., \"AAPL\", \"^GSPC\").","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"SectorPerformance":{"description":"SectorPerformance represents performance data for a market sector.","properties":{"change":{"description":"Percentage change over the measured period.","format":"double","type":"number"},"name":{"description":"Sector name.","type":"string"},"symbol":{"description":"Sector symbol.","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"Stablecoin":{"description":"Stablecoin represents a single stablecoin with peg health data.","properties":{"change24h":{"description":"24-hour price change percentage.","format":"double","type":"number"},"change7d":{"description":"7-day price change percentage.","format":"double","type":"number"},"deviation":{"description":"Deviation from $1.00 peg, as a percentage.","format":"double","type":"number"},"id":{"description":"CoinGecko ID.","minLength":1,"type":"string"},"image":{"description":"Coin image URL.","type":"string"},"marketCap":{"description":"Market capitalization in USD.","format":"double","type":"number"},"name":{"description":"Human-readable name.","type":"string"},"pegStatus":{"description":"Peg status: \"ON PEG\", \"SLIGHT DEPEG\", or \"DEPEGGED\".","type":"string"},"price":{"description":"Current price in USD.","format":"double","minimum":0,"type":"number"},"symbol":{"description":"Ticker symbol (e.g. \"USDT\").","minLength":1,"type":"string"},"volume24h":{"description":"24-hour trading volume in USD.","format":"double","type":"number"}},"required":["id","symbol"],"type":"object"},"StablecoinSummary":{"description":"StablecoinSummary contains aggregate stablecoin market stats.","properties":{"coinCount":{"description":"Number of stablecoins returned.","format":"int32","type":"integer"},"depeggedCount":{"description":"Number of stablecoins in DEPEGGED state.","format":"int32","type":"integer"},"healthStatus":{"description":"Overall health: \"HEALTHY\", \"CAUTION\", or \"WARNING\".","type":"string"},"totalMarketCap":{"description":"Total market cap across all queried stablecoins.","format":"double","type":"number"},"totalVolume24h":{"description":"Total 24h volume across all queried stablecoins.","format":"double","type":"number"}},"type":"object"},"StockAnalysisHeadline":{"properties":{"link":{"type":"string"},"publishedAt":{"description":"Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"source":{"type":"string"},"title":{"type":"string"}},"type":"object"},"StockAnalysisHistoryItem":{"properties":{"snapshots":{"items":{"$ref":"#/components/schemas/AnalyzeStockResponse"},"type":"array"},"symbol":{"type":"string"}},"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"}}},"info":{"title":"MarketService API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/market/v1/analyze-stock":{"get":{"description":"AnalyzeStock retrieves a premium stock analysis report with technicals, news, and AI synthesis.","operationId":"AnalyzeStock","parameters":[{"in":"query","name":"symbol","required":false,"schema":{"type":"string"}},{"in":"query","name":"name","required":false,"schema":{"type":"string"}},{"in":"query","name":"include_news","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyzeStockResponse"}}},"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":"AnalyzeStock","tags":["MarketService"]}},"/api/market/v1/backtest-stock":{"get":{"description":"BacktestStock replays premium stock-analysis signals over recent price history.","operationId":"BacktestStock","parameters":[{"in":"query","name":"symbol","required":false,"schema":{"type":"string"}},{"in":"query","name":"name","required":false,"schema":{"type":"string"}},{"in":"query","name":"eval_window_days","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BacktestStockResponse"}}},"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":"BacktestStock","tags":["MarketService"]}},"/api/market/v1/get-country-stock-index":{"get":{"description":"GetCountryStockIndex retrieves the primary stock index for a country from Yahoo Finance.","operationId":"GetCountryStockIndex","parameters":[{"description":"ISO 3166-1 alpha-2 country code (e.g., \"US\", \"GB\", \"JP\").","in":"query","name":"country_code","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCountryStockIndexResponse"}}},"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":"GetCountryStockIndex","tags":["MarketService"]}},"/api/market/v1/get-fear-greed-index":{"get":{"description":"GetFearGreedIndex retrieves the composite Fear \u0026 Greed sentiment index.","operationId":"GetFearGreedIndex","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetFearGreedIndexResponse"}}},"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":"GetFearGreedIndex","tags":["MarketService"]}},"/api/market/v1/get-sector-summary":{"get":{"description":"GetSectorSummary retrieves market sector performance data from Finnhub.","operationId":"GetSectorSummary","parameters":[{"description":"Time period for performance calculation (e.g., \"1d\", \"1w\", \"1m\"). Defaults to \"1d\".","in":"query","name":"period","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSectorSummaryResponse"}}},"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":"GetSectorSummary","tags":["MarketService"]}},"/api/market/v1/get-stock-analysis-history":{"get":{"description":"GetStockAnalysisHistory retrieves shared premium stock analysis history from the backend store.","operationId":"GetStockAnalysisHistory","parameters":[{"in":"query","name":"symbols","required":false,"schema":{"type":"string"}},{"in":"query","name":"limit_per_symbol","required":false,"schema":{"format":"int32","type":"integer"}},{"in":"query","name":"include_news","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetStockAnalysisHistoryResponse"}}},"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":"GetStockAnalysisHistory","tags":["MarketService"]}},"/api/market/v1/list-ai-tokens":{"get":{"description":"ListAiTokens retrieves AI-focused crypto token prices and changes.","operationId":"ListAiTokens","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListAiTokensResponse"}}},"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":"ListAiTokens","tags":["MarketService"]}},"/api/market/v1/list-commodity-quotes":{"get":{"description":"ListCommodityQuotes retrieves commodity price quotes from Yahoo Finance.","operationId":"ListCommodityQuotes","parameters":[{"description":"Commodity symbols to retrieve (Yahoo symbols). Empty returns defaults.","in":"query","name":"symbols","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCommodityQuotesResponse"}}},"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":"ListCommodityQuotes","tags":["MarketService"]}},"/api/market/v1/list-crypto-quotes":{"get":{"description":"ListCryptoQuotes retrieves cryptocurrency quotes from CoinGecko.","operationId":"ListCryptoQuotes","parameters":[{"description":"Cryptocurrency IDs to retrieve (CoinGecko IDs). Empty returns defaults.","in":"query","name":"ids","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCryptoQuotesResponse"}}},"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":"ListCryptoQuotes","tags":["MarketService"]}},"/api/market/v1/list-crypto-sectors":{"get":{"description":"ListCryptoSectors retrieves crypto sector performance averages.","operationId":"ListCryptoSectors","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCryptoSectorsResponse"}}},"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":"ListCryptoSectors","tags":["MarketService"]}},"/api/market/v1/list-defi-tokens":{"get":{"description":"ListDefiTokens retrieves DeFi token prices and changes.","operationId":"ListDefiTokens","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListDefiTokensResponse"}}},"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":"ListDefiTokens","tags":["MarketService"]}},"/api/market/v1/list-etf-flows":{"get":{"description":"ListEtfFlows retrieves BTC spot ETF flow estimates from Yahoo Finance.","operationId":"ListEtfFlows","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEtfFlowsResponse"}}},"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":"ListEtfFlows","tags":["MarketService"]}},"/api/market/v1/list-gulf-quotes":{"get":{"description":"ListGulfQuotes retrieves Gulf region market quotes (indices, currencies, oil).","operationId":"ListGulfQuotes","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListGulfQuotesResponse"}}},"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":"ListGulfQuotes","tags":["MarketService"]}},"/api/market/v1/list-market-quotes":{"get":{"description":"ListMarketQuotes retrieves stock and index quotes.","operationId":"ListMarketQuotes","parameters":[{"description":"Ticker symbols to retrieve (e.g., [\"AAPL\", \"^GSPC\"]). Empty returns defaults.","in":"query","name":"symbols","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListMarketQuotesResponse"}}},"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":"ListMarketQuotes","tags":["MarketService"]}},"/api/market/v1/list-other-tokens":{"get":{"description":"ListOtherTokens retrieves other/trending crypto token prices and changes.","operationId":"ListOtherTokens","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListOtherTokensResponse"}}},"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":"ListOtherTokens","tags":["MarketService"]}},"/api/market/v1/list-stablecoin-markets":{"get":{"description":"ListStablecoinMarkets retrieves stablecoin peg health and market data from CoinGecko.","operationId":"ListStablecoinMarkets","parameters":[{"description":"CoinGecko IDs to retrieve (e.g. \"tether,usd-coin\"). Empty returns defaults.","in":"query","name":"coins","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListStablecoinMarketsResponse"}}},"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":"ListStablecoinMarkets","tags":["MarketService"]}},"/api/market/v1/list-stored-stock-backtests":{"get":{"description":"ListStoredStockBacktests retrieves stored premium backtest snapshots from the backend store.","operationId":"ListStoredStockBacktests","parameters":[{"in":"query","name":"symbols","required":false,"schema":{"type":"string"}},{"in":"query","name":"eval_window_days","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListStoredStockBacktestsResponse"}}},"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":"ListStoredStockBacktests","tags":["MarketService"]}}}} \ No newline at end of file +{"components":{"schemas":{"AnalyzeStockRequest":{"properties":{"includeNews":{"type":"boolean"},"name":{"maxLength":120,"type":"string"},"symbol":{"maxLength":32,"minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"AnalyzeStockResponse":{"properties":{"action":{"type":"string"},"analysisAt":{"description":"Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"analysisId":{"type":"string"},"available":{"type":"boolean"},"biasMa10":{"format":"double","type":"number"},"biasMa20":{"format":"double","type":"number"},"biasMa5":{"format":"double","type":"number"},"bullishFactors":{"items":{"type":"string"},"type":"array"},"changePercent":{"format":"double","type":"number"},"confidence":{"type":"string"},"currency":{"type":"string"},"currentPrice":{"format":"double","type":"number"},"display":{"type":"string"},"engineVersion":{"type":"string"},"fallback":{"type":"boolean"},"generatedAt":{"type":"string"},"headlines":{"items":{"$ref":"#/components/schemas/StockAnalysisHeadline"},"type":"array"},"ma10":{"format":"double","type":"number"},"ma20":{"format":"double","type":"number"},"ma5":{"format":"double","type":"number"},"ma60":{"format":"double","type":"number"},"macdBar":{"format":"double","type":"number"},"macdDea":{"format":"double","type":"number"},"macdDif":{"format":"double","type":"number"},"macdStatus":{"type":"string"},"model":{"type":"string"},"name":{"type":"string"},"newsSearched":{"type":"boolean"},"newsSummary":{"type":"string"},"provider":{"type":"string"},"resistanceLevels":{"items":{"format":"double","type":"number"},"type":"array"},"riskFactors":{"items":{"type":"string"},"type":"array"},"rsi12":{"format":"double","type":"number"},"rsiStatus":{"type":"string"},"signal":{"type":"string"},"signalScore":{"format":"double","type":"number"},"stopLoss":{"format":"double","type":"number"},"summary":{"type":"string"},"supportLevels":{"items":{"format":"double","type":"number"},"type":"array"},"symbol":{"type":"string"},"takeProfit":{"format":"double","type":"number"},"technicalSummary":{"type":"string"},"trendStatus":{"type":"string"},"volumeRatio5d":{"format":"double","type":"number"},"volumeStatus":{"type":"string"},"whyNow":{"type":"string"}},"type":"object"},"BacktestStockEvaluation":{"properties":{"analysisAt":{"description":"Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"analysisId":{"type":"string"},"directionCorrect":{"type":"boolean"},"entryPrice":{"format":"double","type":"number"},"exitPrice":{"format":"double","type":"number"},"outcome":{"type":"string"},"signal":{"type":"string"},"signalScore":{"format":"double","type":"number"},"simulatedReturnPct":{"format":"double","type":"number"},"stopLoss":{"format":"double","type":"number"},"takeProfit":{"format":"double","type":"number"}},"type":"object"},"BacktestStockRequest":{"properties":{"evalWindowDays":{"format":"int32","maximum":30,"minimum":3,"type":"integer"},"name":{"maxLength":120,"type":"string"},"symbol":{"maxLength":32,"minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"BacktestStockResponse":{"properties":{"actionableEvaluations":{"format":"int32","type":"integer"},"available":{"type":"boolean"},"avgSimulatedReturnPct":{"format":"double","type":"number"},"cumulativeSimulatedReturnPct":{"format":"double","type":"number"},"currency":{"type":"string"},"directionAccuracy":{"format":"double","type":"number"},"display":{"type":"string"},"engineVersion":{"type":"string"},"evalWindowDays":{"format":"int32","type":"integer"},"evaluations":{"items":{"$ref":"#/components/schemas/BacktestStockEvaluation"},"type":"array"},"evaluationsRun":{"format":"int32","type":"integer"},"generatedAt":{"type":"string"},"latestSignal":{"type":"string"},"latestSignalScore":{"format":"double","type":"number"},"name":{"type":"string"},"summary":{"type":"string"},"symbol":{"type":"string"},"winRate":{"format":"double","type":"number"}},"type":"object"},"CommodityQuote":{"description":"CommodityQuote represents a commodity price quote from Yahoo Finance.","properties":{"change":{"description":"Percentage change from previous close.","format":"double","type":"number"},"display":{"description":"Display label.","type":"string"},"name":{"description":"Human-readable name.","type":"string"},"price":{"description":"Current price.","format":"double","type":"number"},"sparkline":{"items":{"description":"Sparkline data points.","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Commodity symbol (e.g., \"CL=F\" for crude oil).","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"CotInstrument":{"properties":{"assetManagerLong":{"format":"int64","type":"string"},"assetManagerShort":{"format":"int64","type":"string"},"code":{"type":"string"},"dealerLong":{"format":"int64","type":"string"},"dealerShort":{"format":"int64","type":"string"},"leveragedFundsLong":{"format":"int64","type":"string"},"leveragedFundsShort":{"format":"int64","type":"string"},"name":{"type":"string"},"netPct":{"format":"double","type":"number"},"reportDate":{"type":"string"}},"type":"object"},"CryptoQuote":{"description":"CryptoQuote represents a cryptocurrency quote from CoinGecko.","properties":{"change":{"description":"24-hour percentage change.","format":"double","type":"number"},"change7d":{"description":"7-day percentage change.","format":"double","type":"number"},"name":{"description":"Cryptocurrency name (e.g., \"Bitcoin\").","type":"string"},"price":{"description":"Current price in USD.","format":"double","type":"number"},"sparkline":{"items":{"description":"Sparkline data points (recent price history).","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Ticker symbol (e.g., \"BTC\").","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"CryptoSector":{"description":"CryptoSector represents performance data for a crypto market sector.","properties":{"change":{"description":"Average 24h percentage change across sector tokens.","format":"double","type":"number"},"id":{"description":"Sector identifier.","type":"string"},"name":{"description":"Sector display name.","type":"string"}},"type":"object"},"EarningsEntry":{"properties":{"company":{"type":"string"},"date":{"type":"string"},"epsActual":{"format":"double","type":"number"},"epsEstimate":{"format":"double","type":"number"},"hasActuals":{"type":"boolean"},"hour":{"type":"string"},"revenueActual":{"format":"double","type":"number"},"revenueEstimate":{"format":"double","type":"number"},"surpriseDirection":{"type":"string"},"symbol":{"type":"string"}},"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"},"EtfFlow":{"description":"EtfFlow represents a single ETF with estimated flow data.","properties":{"avgVolume":{"description":"Average volume over prior days.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"direction":{"description":"Flow direction: \"inflow\", \"outflow\", or \"neutral\".","type":"string"},"estFlow":{"description":"Estimated dollar flow magnitude.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"issuer":{"description":"Fund issuer (e.g. \"BlackRock\").","type":"string"},"price":{"description":"Latest closing price.","format":"double","type":"number"},"priceChange":{"description":"Day-over-day price change percentage.","format":"double","type":"number"},"ticker":{"description":"Ticker symbol (e.g. \"IBIT\").","minLength":1,"type":"string"},"volume":{"description":"Latest daily volume.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"volumeRatio":{"description":"Volume ratio (latest / average).","format":"double","type":"number"}},"required":["ticker"],"type":"object"},"EtfFlowsSummary":{"description":"EtfFlowsSummary contains aggregate ETF flow stats.","properties":{"etfCount":{"description":"Number of ETFs with data.","format":"int32","type":"integer"},"inflowCount":{"description":"Number of ETFs with inflow.","format":"int32","type":"integer"},"netDirection":{"description":"Net direction: \"NET INFLOW\", \"NET OUTFLOW\", or \"NEUTRAL\".","type":"string"},"outflowCount":{"description":"Number of ETFs with outflow.","format":"int32","type":"integer"},"totalEstFlow":{"description":"Total estimated flow across all ETFs.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"totalVolume":{"description":"Total volume across all ETFs.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"}},"type":"object"},"FearGreedCategory":{"properties":{"contribution":{"format":"double","type":"number"},"degraded":{"type":"boolean"},"inputsJson":{"type":"string"},"score":{"format":"double","type":"number"},"weight":{"format":"double","type":"number"}},"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"},"GetCotPositioningRequest":{"type":"object"},"GetCotPositioningResponse":{"properties":{"instruments":{"items":{"$ref":"#/components/schemas/CotInstrument"},"type":"array"},"reportDate":{"type":"string"},"unavailable":{"type":"boolean"}},"type":"object"},"GetCountryStockIndexRequest":{"description":"GetCountryStockIndexRequest specifies which country's stock index to retrieve.","properties":{"countryCode":{"description":"ISO 3166-1 alpha-2 country code (e.g., \"US\", \"GB\", \"JP\").","pattern":"^[A-Z]{2}$","type":"string"}},"required":["countryCode"],"type":"object"},"GetCountryStockIndexResponse":{"description":"GetCountryStockIndexResponse contains the country's primary stock index data.","properties":{"available":{"description":"Whether stock index data is available for this country.","type":"boolean"},"code":{"description":"ISO 3166-1 alpha-2 country code.","type":"string"},"currency":{"description":"Currency of the index.","type":"string"},"fetchedAt":{"description":"When the data was fetched (ISO 8601).","type":"string"},"indexName":{"description":"Index name (e.g., \"S\u0026P 500\").","type":"string"},"price":{"description":"Latest closing price.","format":"double","type":"number"},"symbol":{"description":"Ticker symbol (e.g., \"^GSPC\").","type":"string"},"weekChangePercent":{"description":"Weekly change percentage.","format":"double","type":"number"}},"type":"object"},"GetFearGreedIndexRequest":{"type":"object"},"GetFearGreedIndexResponse":{"properties":{"aaiiBear":{"format":"double","type":"number"},"aaiiBull":{"format":"double","type":"number"},"breadth":{"$ref":"#/components/schemas/FearGreedCategory"},"cnnFearGreed":{"format":"double","type":"number"},"cnnLabel":{"type":"string"},"compositeLabel":{"type":"string"},"compositeScore":{"format":"double","type":"number"},"credit":{"$ref":"#/components/schemas/FearGreedCategory"},"crossAsset":{"$ref":"#/components/schemas/FearGreedCategory"},"fedRate":{"type":"string"},"fsiLabel":{"type":"string"},"fsiValue":{"format":"double","type":"number"},"hySpread":{"format":"double","type":"number"},"hygPrice":{"format":"double","type":"number"},"liquidity":{"$ref":"#/components/schemas/FearGreedCategory"},"macro":{"$ref":"#/components/schemas/FearGreedCategory"},"momentum":{"$ref":"#/components/schemas/FearGreedCategory"},"pctAbove200d":{"format":"double","type":"number"},"positioning":{"$ref":"#/components/schemas/FearGreedCategory"},"previousScore":{"format":"double","type":"number"},"putCallRatio":{"format":"double","type":"number"},"seededAt":{"type":"string"},"sentiment":{"$ref":"#/components/schemas/FearGreedCategory"},"tltPrice":{"format":"double","type":"number"},"trend":{"$ref":"#/components/schemas/FearGreedCategory"},"unavailable":{"type":"boolean"},"vix":{"format":"double","type":"number"},"volatility":{"$ref":"#/components/schemas/FearGreedCategory"},"yield10y":{"format":"double","type":"number"}},"type":"object"},"GetSectorSummaryRequest":{"description":"GetSectorSummaryRequest specifies parameters for retrieving sector performance.","properties":{"period":{"description":"Time period for performance calculation (e.g., \"1d\", \"1w\", \"1m\"). Defaults to \"1d\".","type":"string"}},"type":"object"},"GetSectorSummaryResponse":{"description":"GetSectorSummaryResponse contains sector performance data.","properties":{"sectors":{"items":{"$ref":"#/components/schemas/SectorPerformance"},"type":"array"}},"type":"object"},"GetStockAnalysisHistoryRequest":{"properties":{"includeNews":{"type":"boolean"},"limitPerSymbol":{"format":"int32","maximum":32,"minimum":1,"type":"integer"},"symbols":{"items":{"type":"string"},"type":"array"}},"type":"object"},"GetStockAnalysisHistoryResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/StockAnalysisHistoryItem"},"type":"array"}},"type":"object"},"GulfQuote":{"description":"GulfQuote represents a Gulf region market quote (index, currency, or oil).","properties":{"change":{"format":"double","type":"number"},"country":{"type":"string"},"flag":{"type":"string"},"name":{"type":"string"},"price":{"format":"double","type":"number"},"sparkline":{"items":{"format":"double","type":"number"},"type":"array"},"symbol":{"type":"string"},"type":{"type":"string"}},"type":"object"},"ListAiTokensRequest":{"description":"ListAiTokensRequest retrieves AI crypto token prices.","type":"object"},"ListAiTokensResponse":{"description":"ListAiTokensResponse contains AI token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListCommodityQuotesRequest":{"description":"ListCommodityQuotesRequest specifies which commodities to retrieve.","properties":{"symbols":{"items":{"description":"Commodity symbols to retrieve (Yahoo symbols). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListCommodityQuotesResponse":{"description":"ListCommodityQuotesResponse contains commodity quotes.","properties":{"quotes":{"items":{"$ref":"#/components/schemas/CommodityQuote"},"type":"array"}},"type":"object"},"ListCryptoQuotesRequest":{"description":"ListCryptoQuotesRequest specifies which cryptocurrencies to retrieve.","properties":{"ids":{"items":{"description":"Cryptocurrency IDs to retrieve (CoinGecko IDs). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListCryptoQuotesResponse":{"description":"ListCryptoQuotesResponse contains cryptocurrency quotes.","properties":{"quotes":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListCryptoSectorsRequest":{"description":"ListCryptoSectorsRequest retrieves crypto sector performance.","type":"object"},"ListCryptoSectorsResponse":{"description":"ListCryptoSectorsResponse contains crypto sector performance data.","properties":{"sectors":{"items":{"$ref":"#/components/schemas/CryptoSector"},"type":"array"}},"type":"object"},"ListDefiTokensRequest":{"description":"ListDefiTokensRequest retrieves DeFi token prices.","type":"object"},"ListDefiTokensResponse":{"description":"ListDefiTokensResponse contains DeFi token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListEarningsCalendarRequest":{"properties":{"fromDate":{"type":"string"},"toDate":{"type":"string"}},"type":"object"},"ListEarningsCalendarResponse":{"properties":{"earnings":{"items":{"$ref":"#/components/schemas/EarningsEntry"},"type":"array"},"fromDate":{"type":"string"},"toDate":{"type":"string"},"total":{"format":"int32","type":"integer"},"unavailable":{"type":"boolean"}},"type":"object"},"ListEtfFlowsRequest":{"description":"ListEtfFlowsRequest is empty; the handler uses a fixed list of BTC spot ETFs.","type":"object"},"ListEtfFlowsResponse":{"description":"ListEtfFlowsResponse contains BTC spot ETF flow data.","properties":{"etfs":{"items":{"$ref":"#/components/schemas/EtfFlow"},"type":"array"},"rateLimited":{"description":"True when the upstream API rate-limited the request.","type":"boolean"},"summary":{"$ref":"#/components/schemas/EtfFlowsSummary"},"timestamp":{"description":"Timestamp of the data fetch (ISO 8601).","type":"string"}},"type":"object"},"ListGulfQuotesRequest":{"type":"object"},"ListGulfQuotesResponse":{"properties":{"quotes":{"items":{"$ref":"#/components/schemas/GulfQuote"},"type":"array"},"rateLimited":{"type":"boolean"}},"type":"object"},"ListMarketQuotesRequest":{"description":"ListMarketQuotesRequest specifies which stock/index symbols to retrieve.","properties":{"symbols":{"items":{"description":"Ticker symbols to retrieve (e.g., [\"AAPL\", \"^GSPC\"]). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListMarketQuotesResponse":{"description":"ListMarketQuotesResponse contains stock and index quotes.","properties":{"finnhubSkipped":{"description":"True when the Finnhub API key is not configured and stock quotes were skipped.","type":"boolean"},"quotes":{"items":{"$ref":"#/components/schemas/MarketQuote"},"type":"array"},"rateLimited":{"description":"True when the upstream API rate-limited the request.","type":"boolean"},"skipReason":{"description":"Human-readable reason when Finnhub was skipped (e.g., \"FINNHUB_API_KEY not configured\").","type":"string"}},"type":"object"},"ListOtherTokensRequest":{"description":"ListOtherTokensRequest retrieves other/trending crypto token prices.","type":"object"},"ListOtherTokensResponse":{"description":"ListOtherTokensResponse contains other token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListStablecoinMarketsRequest":{"description":"ListStablecoinMarketsRequest specifies which stablecoins to retrieve.","properties":{"coins":{"items":{"description":"CoinGecko IDs to retrieve (e.g. \"tether,usd-coin\"). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListStablecoinMarketsResponse":{"description":"ListStablecoinMarketsResponse contains stablecoin market data.","properties":{"stablecoins":{"items":{"$ref":"#/components/schemas/Stablecoin"},"type":"array"},"summary":{"$ref":"#/components/schemas/StablecoinSummary"},"timestamp":{"description":"Timestamp of the data fetch (ISO 8601).","type":"string"}},"type":"object"},"ListStoredStockBacktestsRequest":{"properties":{"evalWindowDays":{"format":"int32","maximum":30,"minimum":3,"type":"integer"},"symbols":{"items":{"type":"string"},"type":"array"}},"type":"object"},"ListStoredStockBacktestsResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/BacktestStockResponse"},"type":"array"}},"type":"object"},"MarketQuote":{"description":"MarketQuote represents a stock or index quote from Finnhub or Yahoo Finance.","properties":{"change":{"description":"Percentage change from previous close.","format":"double","type":"number"},"display":{"description":"Display label.","type":"string"},"name":{"description":"Human-readable name.","type":"string"},"price":{"description":"Current price.","format":"double","type":"number"},"sparkline":{"items":{"description":"Sparkline data points (recent price history).","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Ticker symbol (e.g., \"AAPL\", \"^GSPC\").","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"SectorPerformance":{"description":"SectorPerformance represents performance data for a market sector.","properties":{"change":{"description":"Percentage change over the measured period.","format":"double","type":"number"},"name":{"description":"Sector name.","type":"string"},"symbol":{"description":"Sector symbol.","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"Stablecoin":{"description":"Stablecoin represents a single stablecoin with peg health data.","properties":{"change24h":{"description":"24-hour price change percentage.","format":"double","type":"number"},"change7d":{"description":"7-day price change percentage.","format":"double","type":"number"},"deviation":{"description":"Deviation from $1.00 peg, as a percentage.","format":"double","type":"number"},"id":{"description":"CoinGecko ID.","minLength":1,"type":"string"},"image":{"description":"Coin image URL.","type":"string"},"marketCap":{"description":"Market capitalization in USD.","format":"double","type":"number"},"name":{"description":"Human-readable name.","type":"string"},"pegStatus":{"description":"Peg status: \"ON PEG\", \"SLIGHT DEPEG\", or \"DEPEGGED\".","type":"string"},"price":{"description":"Current price in USD.","format":"double","minimum":0,"type":"number"},"symbol":{"description":"Ticker symbol (e.g. \"USDT\").","minLength":1,"type":"string"},"volume24h":{"description":"24-hour trading volume in USD.","format":"double","type":"number"}},"required":["id","symbol"],"type":"object"},"StablecoinSummary":{"description":"StablecoinSummary contains aggregate stablecoin market stats.","properties":{"coinCount":{"description":"Number of stablecoins returned.","format":"int32","type":"integer"},"depeggedCount":{"description":"Number of stablecoins in DEPEGGED state.","format":"int32","type":"integer"},"healthStatus":{"description":"Overall health: \"HEALTHY\", \"CAUTION\", or \"WARNING\".","type":"string"},"totalMarketCap":{"description":"Total market cap across all queried stablecoins.","format":"double","type":"number"},"totalVolume24h":{"description":"Total 24h volume across all queried stablecoins.","format":"double","type":"number"}},"type":"object"},"StockAnalysisHeadline":{"properties":{"link":{"type":"string"},"publishedAt":{"description":"Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"source":{"type":"string"},"title":{"type":"string"}},"type":"object"},"StockAnalysisHistoryItem":{"properties":{"snapshots":{"items":{"$ref":"#/components/schemas/AnalyzeStockResponse"},"type":"array"},"symbol":{"type":"string"}},"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"}}},"info":{"title":"MarketService API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/market/v1/analyze-stock":{"get":{"description":"AnalyzeStock retrieves a premium stock analysis report with technicals, news, and AI synthesis.","operationId":"AnalyzeStock","parameters":[{"in":"query","name":"symbol","required":false,"schema":{"type":"string"}},{"in":"query","name":"name","required":false,"schema":{"type":"string"}},{"in":"query","name":"include_news","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyzeStockResponse"}}},"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":"AnalyzeStock","tags":["MarketService"]}},"/api/market/v1/backtest-stock":{"get":{"description":"BacktestStock replays premium stock-analysis signals over recent price history.","operationId":"BacktestStock","parameters":[{"in":"query","name":"symbol","required":false,"schema":{"type":"string"}},{"in":"query","name":"name","required":false,"schema":{"type":"string"}},{"in":"query","name":"eval_window_days","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BacktestStockResponse"}}},"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":"BacktestStock","tags":["MarketService"]}},"/api/market/v1/get-cot-positioning":{"get":{"description":"GetCotPositioning retrieves CFTC COT institutional positioning data.","operationId":"GetCotPositioning","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCotPositioningResponse"}}},"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":"GetCotPositioning","tags":["MarketService"]}},"/api/market/v1/get-country-stock-index":{"get":{"description":"GetCountryStockIndex retrieves the primary stock index for a country from Yahoo Finance.","operationId":"GetCountryStockIndex","parameters":[{"description":"ISO 3166-1 alpha-2 country code (e.g., \"US\", \"GB\", \"JP\").","in":"query","name":"country_code","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCountryStockIndexResponse"}}},"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":"GetCountryStockIndex","tags":["MarketService"]}},"/api/market/v1/get-fear-greed-index":{"get":{"description":"GetFearGreedIndex retrieves the composite Fear \u0026 Greed sentiment index.","operationId":"GetFearGreedIndex","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetFearGreedIndexResponse"}}},"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":"GetFearGreedIndex","tags":["MarketService"]}},"/api/market/v1/get-sector-summary":{"get":{"description":"GetSectorSummary retrieves market sector performance data from Finnhub.","operationId":"GetSectorSummary","parameters":[{"description":"Time period for performance calculation (e.g., \"1d\", \"1w\", \"1m\"). Defaults to \"1d\".","in":"query","name":"period","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSectorSummaryResponse"}}},"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":"GetSectorSummary","tags":["MarketService"]}},"/api/market/v1/get-stock-analysis-history":{"get":{"description":"GetStockAnalysisHistory retrieves shared premium stock analysis history from the backend store.","operationId":"GetStockAnalysisHistory","parameters":[{"in":"query","name":"symbols","required":false,"schema":{"type":"string"}},{"in":"query","name":"limit_per_symbol","required":false,"schema":{"format":"int32","type":"integer"}},{"in":"query","name":"include_news","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetStockAnalysisHistoryResponse"}}},"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":"GetStockAnalysisHistory","tags":["MarketService"]}},"/api/market/v1/list-ai-tokens":{"get":{"description":"ListAiTokens retrieves AI-focused crypto token prices and changes.","operationId":"ListAiTokens","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListAiTokensResponse"}}},"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":"ListAiTokens","tags":["MarketService"]}},"/api/market/v1/list-commodity-quotes":{"get":{"description":"ListCommodityQuotes retrieves commodity price quotes from Yahoo Finance.","operationId":"ListCommodityQuotes","parameters":[{"description":"Commodity symbols to retrieve (Yahoo symbols). Empty returns defaults.","in":"query","name":"symbols","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCommodityQuotesResponse"}}},"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":"ListCommodityQuotes","tags":["MarketService"]}},"/api/market/v1/list-crypto-quotes":{"get":{"description":"ListCryptoQuotes retrieves cryptocurrency quotes from CoinGecko.","operationId":"ListCryptoQuotes","parameters":[{"description":"Cryptocurrency IDs to retrieve (CoinGecko IDs). Empty returns defaults.","in":"query","name":"ids","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCryptoQuotesResponse"}}},"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":"ListCryptoQuotes","tags":["MarketService"]}},"/api/market/v1/list-crypto-sectors":{"get":{"description":"ListCryptoSectors retrieves crypto sector performance averages.","operationId":"ListCryptoSectors","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCryptoSectorsResponse"}}},"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":"ListCryptoSectors","tags":["MarketService"]}},"/api/market/v1/list-defi-tokens":{"get":{"description":"ListDefiTokens retrieves DeFi token prices and changes.","operationId":"ListDefiTokens","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListDefiTokensResponse"}}},"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":"ListDefiTokens","tags":["MarketService"]}},"/api/market/v1/list-earnings-calendar":{"get":{"description":"ListEarningsCalendar retrieves upcoming and recent earnings releases.","operationId":"ListEarningsCalendar","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/ListEarningsCalendarResponse"}}},"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":"ListEarningsCalendar","tags":["MarketService"]}},"/api/market/v1/list-etf-flows":{"get":{"description":"ListEtfFlows retrieves BTC spot ETF flow estimates from Yahoo Finance.","operationId":"ListEtfFlows","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEtfFlowsResponse"}}},"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":"ListEtfFlows","tags":["MarketService"]}},"/api/market/v1/list-gulf-quotes":{"get":{"description":"ListGulfQuotes retrieves Gulf region market quotes (indices, currencies, oil).","operationId":"ListGulfQuotes","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListGulfQuotesResponse"}}},"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":"ListGulfQuotes","tags":["MarketService"]}},"/api/market/v1/list-market-quotes":{"get":{"description":"ListMarketQuotes retrieves stock and index quotes.","operationId":"ListMarketQuotes","parameters":[{"description":"Ticker symbols to retrieve (e.g., [\"AAPL\", \"^GSPC\"]). Empty returns defaults.","in":"query","name":"symbols","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListMarketQuotesResponse"}}},"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":"ListMarketQuotes","tags":["MarketService"]}},"/api/market/v1/list-other-tokens":{"get":{"description":"ListOtherTokens retrieves other/trending crypto token prices and changes.","operationId":"ListOtherTokens","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListOtherTokensResponse"}}},"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":"ListOtherTokens","tags":["MarketService"]}},"/api/market/v1/list-stablecoin-markets":{"get":{"description":"ListStablecoinMarkets retrieves stablecoin peg health and market data from CoinGecko.","operationId":"ListStablecoinMarkets","parameters":[{"description":"CoinGecko IDs to retrieve (e.g. \"tether,usd-coin\"). Empty returns defaults.","in":"query","name":"coins","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListStablecoinMarketsResponse"}}},"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":"ListStablecoinMarkets","tags":["MarketService"]}},"/api/market/v1/list-stored-stock-backtests":{"get":{"description":"ListStoredStockBacktests retrieves stored premium backtest snapshots from the backend store.","operationId":"ListStoredStockBacktests","parameters":[{"in":"query","name":"symbols","required":false,"schema":{"type":"string"}},{"in":"query","name":"eval_window_days","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListStoredStockBacktestsResponse"}}},"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":"ListStoredStockBacktests","tags":["MarketService"]}}}} \ No newline at end of file diff --git a/docs/api/MarketService.openapi.yaml b/docs/api/MarketService.openapi.yaml index 52d903f68..0c9f417ad 100644 --- a/docs/api/MarketService.openapi.yaml +++ b/docs/api/MarketService.openapi.yaml @@ -549,6 +549,69 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /api/market/v1/list-earnings-calendar: + get: + tags: + - MarketService + summary: ListEarningsCalendar + description: ListEarningsCalendar retrieves upcoming and recent earnings releases. + operationId: ListEarningsCalendar + parameters: + - name: fromDate + in: query + required: false + schema: + type: string + - name: toDate + in: query + required: false + schema: + type: string + responses: + "200": + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/ListEarningsCalendarResponse' + "400": + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + default: + description: Error response + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /api/market/v1/get-cot-positioning: + get: + tags: + - MarketService + summary: GetCotPositioning + description: GetCotPositioning retrieves CFTC COT institutional positioning data. + operationId: GetCotPositioning + responses: + "200": + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/GetCotPositioningResponse' + "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: @@ -1445,6 +1508,17 @@ components: type: string unavailable: type: boolean + fsiValue: + type: number + format: double + fsiLabel: + type: string + hygPrice: + type: number + format: double + tltPrice: + type: number + format: double FearGreedCategory: type: object properties: @@ -1461,3 +1535,96 @@ components: type: boolean inputsJson: type: string + ListEarningsCalendarRequest: + type: object + properties: + fromDate: + type: string + toDate: + type: string + ListEarningsCalendarResponse: + type: object + properties: + earnings: + type: array + items: + $ref: '#/components/schemas/EarningsEntry' + fromDate: + type: string + toDate: + type: string + total: + type: integer + format: int32 + unavailable: + type: boolean + EarningsEntry: + type: object + properties: + symbol: + type: string + company: + type: string + date: + type: string + hour: + type: string + epsEstimate: + type: number + format: double + revenueEstimate: + type: number + format: double + epsActual: + type: number + format: double + revenueActual: + type: number + format: double + hasActuals: + type: boolean + surpriseDirection: + type: string + GetCotPositioningRequest: + type: object + GetCotPositioningResponse: + type: object + properties: + instruments: + type: array + items: + $ref: '#/components/schemas/CotInstrument' + reportDate: + type: string + unavailable: + type: boolean + CotInstrument: + type: object + properties: + name: + type: string + code: + type: string + reportDate: + type: string + assetManagerLong: + type: string + format: int64 + assetManagerShort: + type: string + format: int64 + leveragedFundsLong: + type: string + format: int64 + leveragedFundsShort: + type: string + format: int64 + dealerLong: + type: string + format: int64 + dealerShort: + type: string + format: int64 + netPct: + type: number + format: double diff --git a/proto/worldmonitor/economic/v1/get_economic_calendar.proto b/proto/worldmonitor/economic/v1/get_economic_calendar.proto new file mode 100644 index 000000000..a12459e54 --- /dev/null +++ b/proto/worldmonitor/economic/v1/get_economic_calendar.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package worldmonitor.economic.v1; + +import "sebuf/http/annotations.proto"; + +message GetEconomicCalendarRequest { + string from_date = 1 [(sebuf.http.query) = {name: "fromDate"}]; + string to_date = 2 [(sebuf.http.query) = {name: "toDate"}]; +} + +message EconomicEvent { + string event = 1; + string country = 2; + string date = 3; + string impact = 4; + string actual = 5; + string estimate = 6; + string previous = 7; + string unit = 8; +} + +message GetEconomicCalendarResponse { + repeated EconomicEvent events = 1; + string from_date = 2; + string to_date = 3; + int32 total = 4; + bool unavailable = 5; +} diff --git a/proto/worldmonitor/economic/v1/service.proto b/proto/worldmonitor/economic/v1/service.proto index d12d0c6c8..66416c6cf 100644 --- a/proto/worldmonitor/economic/v1/service.proto +++ b/proto/worldmonitor/economic/v1/service.proto @@ -17,6 +17,7 @@ import "worldmonitor/economic/v1/list_bigmac_prices.proto"; import "worldmonitor/economic/v1/get_national_debt.proto"; import "worldmonitor/economic/v1/list_fuel_prices.proto"; import "worldmonitor/economic/v1/get_bls_series.proto"; +import "worldmonitor/economic/v1/get_economic_calendar.proto"; // EconomicService provides APIs for macroeconomic data from FRED, World Bank, and EIA. service EconomicService { @@ -91,4 +92,9 @@ service EconomicService { rpc GetBlsSeries(GetBlsSeriesRequest) returns (GetBlsSeriesResponse) { option (sebuf.http.config) = {path: "/get-bls-series", method: HTTP_METHOD_GET}; } + + // GetEconomicCalendar retrieves upcoming major economic events (FOMC, CPI, NFP, etc). + rpc GetEconomicCalendar(GetEconomicCalendarRequest) returns (GetEconomicCalendarResponse) { + option (sebuf.http.config) = {path: "/get-economic-calendar", method: HTTP_METHOD_GET}; + } } diff --git a/proto/worldmonitor/market/v1/get_cot_positioning.proto b/proto/worldmonitor/market/v1/get_cot_positioning.proto new file mode 100644 index 000000000..ebca5ad8d --- /dev/null +++ b/proto/worldmonitor/market/v1/get_cot_positioning.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package worldmonitor.market.v1; + +import "sebuf/http/annotations.proto"; + +message GetCotPositioningRequest {} + +message CotInstrument { + string name = 1; + string code = 2; + string report_date = 3; + int64 asset_manager_long = 4; + int64 asset_manager_short = 5; + int64 leveraged_funds_long = 6; + int64 leveraged_funds_short = 7; + int64 dealer_long = 8; + int64 dealer_short = 9; + double net_pct = 10; +} + +message GetCotPositioningResponse { + repeated CotInstrument instruments = 1; + string report_date = 2; + bool unavailable = 3; +} diff --git a/proto/worldmonitor/market/v1/get_fear_greed_index.proto b/proto/worldmonitor/market/v1/get_fear_greed_index.proto index 1c877d72d..18fb8ba53 100644 --- a/proto/worldmonitor/market/v1/get_fear_greed_index.proto +++ b/proto/worldmonitor/market/v1/get_fear_greed_index.proto @@ -40,4 +40,8 @@ message GetFearGreedIndexResponse { double aaii_bear = 23; string fed_rate = 24; bool unavailable = 25; + double fsi_value = 26; + string fsi_label = 27; + double hyg_price = 28; + double tlt_price = 29; } diff --git a/proto/worldmonitor/market/v1/list_earnings_calendar.proto b/proto/worldmonitor/market/v1/list_earnings_calendar.proto new file mode 100644 index 000000000..b695552c3 --- /dev/null +++ b/proto/worldmonitor/market/v1/list_earnings_calendar.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package worldmonitor.market.v1; + +import "sebuf/http/annotations.proto"; + +message ListEarningsCalendarRequest { + string from_date = 1 [(sebuf.http.query) = {name: "fromDate"}]; + string to_date = 2 [(sebuf.http.query) = {name: "toDate"}]; +} + +message EarningsEntry { + string symbol = 1; + string company = 2; + string date = 3; + string hour = 4; + double eps_estimate = 5; + double revenue_estimate = 6; + double eps_actual = 7; + double revenue_actual = 8; + bool has_actuals = 9; + string surprise_direction = 10; +} + +message ListEarningsCalendarResponse { + repeated EarningsEntry earnings = 1; + string from_date = 2; + string to_date = 3; + int32 total = 4; + bool unavailable = 5; +} diff --git a/proto/worldmonitor/market/v1/service.proto b/proto/worldmonitor/market/v1/service.proto index eb94c8a3c..5ac9d2aab 100644 --- a/proto/worldmonitor/market/v1/service.proto +++ b/proto/worldmonitor/market/v1/service.proto @@ -20,6 +20,8 @@ import "worldmonitor/market/v1/list_defi_tokens.proto"; import "worldmonitor/market/v1/list_ai_tokens.proto"; import "worldmonitor/market/v1/list_other_tokens.proto"; import "worldmonitor/market/v1/get_fear_greed_index.proto"; +import "worldmonitor/market/v1/list_earnings_calendar.proto"; +import "worldmonitor/market/v1/get_cot_positioning.proto"; // MarketService provides APIs for financial market data from Finnhub, Yahoo Finance, and CoinGecko. service MarketService { @@ -109,4 +111,14 @@ service MarketService { rpc GetFearGreedIndex(GetFearGreedIndexRequest) returns (GetFearGreedIndexResponse) { option (sebuf.http.config) = {path: "/get-fear-greed-index", method: HTTP_METHOD_GET}; } + + // ListEarningsCalendar retrieves upcoming and recent earnings releases. + rpc ListEarningsCalendar(ListEarningsCalendarRequest) returns (ListEarningsCalendarResponse) { + option (sebuf.http.config) = {path: "/list-earnings-calendar", method: HTTP_METHOD_GET}; + } + + // GetCotPositioning retrieves CFTC COT institutional positioning data. + rpc GetCotPositioning(GetCotPositioningRequest) returns (GetCotPositioningResponse) { + option (sebuf.http.config) = {path: "/get-cot-positioning", method: HTTP_METHOD_GET}; + } } diff --git a/scripts/seed-cot.mjs b/scripts/seed-cot.mjs new file mode 100644 index 000000000..0a84d3239 --- /dev/null +++ b/scripts/seed-cot.mjs @@ -0,0 +1,148 @@ +#!/usr/bin/env node + +import { loadEnvFile, CHROME_UA, runSeed } from './_seed-utils.mjs'; +loadEnvFile(import.meta.url); + +const COT_KEY = 'market:cot:v1'; +const COT_TTL = 604800; + +const TARGET_INSTRUMENTS = [ + { name: 'S&P 500 E-Mini', code: 'ES', pattern: /E-MINI S&P 500/i }, + { name: 'Nasdaq 100 E-Mini', code: 'NQ', pattern: /E-MINI NASDAQ-100/i }, + { name: '10-Year T-Note', code: 'ZN', pattern: /10-YEAR U.S. TREASURY NOTE/i }, + { name: '2-Year T-Note', code: 'ZT', pattern: /2-YEAR U.S. TREASURY NOTE/i }, + { name: 'Gold', code: 'GC', pattern: /GOLD - COMMODITY EXCHANGE/i }, + { name: 'Crude Oil (WTI)', code: 'CL', pattern: /CRUDE OIL, LIGHT SWEET/i }, + { name: 'EUR/USD', code: 'EC', pattern: /EURO FX/i }, + { name: 'USD/JPY', code: 'JY', pattern: /JAPANESE YEN/i }, +]; + +function parseDate(raw) { + if (!raw) return ''; + const s = String(raw).trim(); + if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s; + if (/^\d{6}$/.test(s)) { + const yy = s.slice(0, 2); + const mm = s.slice(2, 4); + const dd = s.slice(4, 6); + const year = parseInt(yy, 10) >= 50 ? `19${yy}` : `20${yy}`; + return `${year}-${mm}-${dd}`; + } + return s; +} + +async function fetchCotData() { + const url = 'https://www.cftc.gov/dea/newcot/c_disaggrt.txt'; + let text; + try { + const resp = await fetch(url, { + headers: { 'User-Agent': CHROME_UA }, + signal: AbortSignal.timeout(30_000), + }); + if (!resp.ok) { + console.warn(` CFTC fetch failed: HTTP ${resp.status}`); + return { instruments: [], reportDate: '' }; + } + text = await resp.text(); + } catch (e) { + console.warn(` CFTC fetch error: ${e.message}`); + return { instruments: [], reportDate: '' }; + } + + const lines = text.split('\n').map(l => l.trimEnd()); + if (lines.length < 2) { + console.warn(' CFTC: empty file'); + return { instruments: [], reportDate: '' }; + } + + const headerLine = lines[0]; + const headers = headerLine.split('|').map(h => h.trim()); + + const colIdx = name => { + const idx = headers.indexOf(name); + return idx; + }; + + const nameCol = colIdx('Market_and_Exchange_Names'); + const dateCol1 = colIdx('Report_Date_as_YYYY-MM-DD'); + const dateCol2 = colIdx('As_of_Date_In_Form_YYMMDD'); + const dealerLongCol = colIdx('Dealer_Positions_Long_All'); + const dealerShortCol = colIdx('Dealer_Positions_Short_All'); + const amLongCol = colIdx('Asset_Mgr_Positions_Long_All'); + const amShortCol = colIdx('Asset_Mgr_Positions_Short_All'); + const levLongCol = colIdx('Lev_Money_Positions_Long_All'); + const levShortCol = colIdx('Lev_Money_Positions_Short_All'); + + if (nameCol === -1) { + console.warn(' CFTC: Market_and_Exchange_Names column not found'); + return { instruments: [], reportDate: '' }; + } + + const dataLines = lines.slice(1).filter(l => l.trim().length > 0); + + const instruments = []; + let latestReportDate = ''; + + for (const target of TARGET_INSTRUMENTS) { + const matchingLines = dataLines.filter(line => { + const fields = line.split('|'); + const marketName = fields[nameCol] ?? ''; + return target.pattern.test(marketName); + }); + + if (matchingLines.length === 0) { + console.warn(` CFTC: no rows found for ${target.name}`); + continue; + } + + const row = matchingLines[0]; + const fields = row.split('|'); + + const rawDate = (dateCol1 !== -1 && fields[dateCol1]?.trim()) + ? fields[dateCol1].trim() + : (dateCol2 !== -1 ? fields[dateCol2]?.trim() ?? '' : ''); + const reportDate = parseDate(rawDate); + + if (reportDate && !latestReportDate) latestReportDate = reportDate; + + const toNum = idx => { + if (idx === -1) return 0; + const v = parseInt((fields[idx] ?? '').replace(/,/g, '').trim(), 10); + return isNaN(v) ? 0 : v; + }; + + const dealerLong = toNum(dealerLongCol); + const dealerShort = toNum(dealerShortCol); + const amLong = toNum(amLongCol); + const amShort = toNum(amShortCol); + const levLong = toNum(levLongCol); + const levShort = toNum(levShortCol); + + const netPct = ((amLong - amShort) / Math.max(amLong + amShort, 1)) * 100; + + instruments.push({ + name: target.name, + code: target.code, + reportDate, + assetManagerLong: amLong, + assetManagerShort: amShort, + leveragedFundsLong: levLong, + leveragedFundsShort: levShort, + dealerLong, + dealerShort, + netPct: parseFloat(netPct.toFixed(2)), + }); + + console.log(` ${target.code}: AM net ${netPct.toFixed(1)}% (${amLong}L / ${amShort}S), date=${reportDate}`); + } + + return { instruments, reportDate: latestReportDate }; +} + +if (process.argv[1] && process.argv[1].endsWith('seed-cot.mjs')) { + runSeed('market', 'cot', COT_KEY, fetchCotData, { + ttlSeconds: COT_TTL, + validateFn: data => Array.isArray(data?.instruments) && data.instruments.length > 0, + recordCount: data => data?.instruments?.length ?? 0, + }).catch(err => { console.error('FATAL:', err.message || err); process.exit(1); }); +} diff --git a/scripts/seed-earnings-calendar.mjs b/scripts/seed-earnings-calendar.mjs new file mode 100644 index 000000000..6a40c98bc --- /dev/null +++ b/scripts/seed-earnings-calendar.mjs @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +import { loadEnvFile, CHROME_UA, runSeed } from './_seed-utils.mjs'; + +loadEnvFile(import.meta.url); + +const KEY = 'market:earnings-calendar:v1'; +const TTL = 129600; // 36h β€” 3Γ— a 12h cron interval + +function toDateStr(d) { + return d.toISOString().slice(0, 10); +} + +async function fetchAll() { + const apiKey = process.env.FINNHUB_API_KEY; + if (!apiKey) { + console.warn(' FINNHUB_API_KEY not set β€” skipping'); + return { earnings: [], unavailable: true }; + } + + const from = new Date(); + const to = new Date(); + to.setDate(to.getDate() + 14); + + const url = `https://finnhub.io/api/v1/calendar/earnings?from=${toDateStr(from)}&to=${toDateStr(to)}`; + + const resp = await fetch(url, { + headers: { 'User-Agent': CHROME_UA, 'X-Finnhub-Token': apiKey }, + signal: AbortSignal.timeout(15_000), + }); + + if (!resp.ok) { + throw new Error(`Finnhub earnings calendar HTTP ${resp.status}`); + } + + const data = await resp.json(); + const raw = Array.isArray(data?.earningsCalendar) ? data.earningsCalendar : []; + + const earnings = raw + .filter(e => e.symbol) + .map(e => { + const epsEst = e.epsEstimate != null ? Number(e.epsEstimate) : null; + const epsAct = e.epsActual != null ? Number(e.epsActual) : null; + const revEst = e.revenueEstimate != null ? Number(e.revenueEstimate) : null; + const revAct = e.revenueActual != null ? Number(e.revenueActual) : null; + const hasActuals = epsAct != null; + let surpriseDirection = ''; + if (hasActuals && epsEst != null) { + if (epsAct > epsEst) surpriseDirection = 'beat'; + else if (epsAct < epsEst) surpriseDirection = 'miss'; + } + return { + symbol: String(e.symbol), + company: e.name ? String(e.name) : String(e.symbol), + date: e.date ? String(e.date) : '', + hour: e.hour ? String(e.hour) : '', + epsEstimate: epsEst, + revenueEstimate: revEst, + epsActual: epsAct, + revenueActual: revAct, + hasActuals, + surpriseDirection, + }; + }) + .sort((a, b) => a.date.localeCompare(b.date)) + .slice(0, 100); + + console.log(` Fetched ${earnings.length} earnings entries`); + return { earnings, unavailable: false }; +} + +function validate(data) { + return Array.isArray(data?.earnings) && data.earnings.length > 0; +} + +if (process.argv[1]?.endsWith('seed-earnings-calendar.mjs')) { + runSeed('market', 'earnings-calendar', KEY, fetchAll, { + validateFn: validate, + ttlSeconds: TTL, + sourceVersion: 'finnhub-v1', + }).catch(err => { console.error('FATAL:', err.message || err); process.exit(1); }); +} diff --git a/scripts/seed-economic-calendar.mjs b/scripts/seed-economic-calendar.mjs new file mode 100644 index 000000000..635148bf1 --- /dev/null +++ b/scripts/seed-economic-calendar.mjs @@ -0,0 +1,135 @@ +#!/usr/bin/env node + +import { loadEnvFile, CHROME_UA, runSeed } from './_seed-utils.mjs'; + +loadEnvFile(import.meta.url); + +const CANONICAL_KEY = 'economic:econ-calendar:v1'; +const CACHE_TTL = 129600; // 36h β€” 3Γ— a 12h cron interval + +const HIGH_PRIORITY_TERMS = [ + 'fomc', 'fed funds', 'federal funds', 'nonfarm', 'non-farm', + 'cpi', 'pce', 'gdp', 'unemployment', 'payroll', 'retail sales', 'pmi', 'ism', +]; + +const ALLOWED_COUNTRIES = new Set(['US', 'UK', 'EUR', 'EU', 'DE', 'FR', 'JP', 'CN']); + +function isHighPriority(eventName) { + const lower = (eventName || '').toLowerCase(); + return HIGH_PRIORITY_TERMS.some((term) => lower.includes(term)); +} + +function normalizeImpact(raw) { + if (raw === null || raw === undefined) return 'low'; + const s = String(raw).toLowerCase(); + if (s === '3' || s === 'high') return 'high'; + if (s === '2' || s === 'medium' || s === 'moderate') return 'medium'; + return 'low'; +} + +function toDateString(timeStr) { + if (!timeStr) return ''; + const d = new Date(timeStr); + if (!Number.isNaN(d.getTime())) { + return d.toISOString().slice(0, 10); + } + if (/^\d{4}-\d{2}-\d{2}/.test(timeStr)) return timeStr.slice(0, 10); + return ''; +} + +function formatValue(v) { + if (v === null || v === undefined) return ''; + return String(v); +} + +function buildFallbackEvents() { + const year = new Date().getFullYear(); + return [ + { event: 'FOMC Rate Decision', country: 'US', date: `${year}-01-29`, impact: 'high', actual: '', estimate: '', previous: '', unit: '' }, + { event: 'FOMC Rate Decision', country: 'US', date: `${year}-03-19`, impact: 'high', actual: '', estimate: '', previous: '', unit: '' }, + { event: 'FOMC Rate Decision', country: 'US', date: `${year}-05-07`, impact: 'high', actual: '', estimate: '', previous: '', unit: '' }, + { event: 'FOMC Rate Decision', country: 'US', date: `${year}-06-18`, impact: 'high', actual: '', estimate: '', previous: '', unit: '' }, + { event: 'FOMC Rate Decision', country: 'US', date: `${year}-07-30`, impact: 'high', actual: '', estimate: '', previous: '', unit: '' }, + { event: 'FOMC Rate Decision', country: 'US', date: `${year}-09-17`, impact: 'high', actual: '', estimate: '', previous: '', unit: '' }, + { event: 'FOMC Rate Decision', country: 'US', date: `${year}-11-05`, impact: 'high', actual: '', estimate: '', previous: '', unit: '' }, + { event: 'FOMC Rate Decision', country: 'US', date: `${year}-12-17`, impact: 'high', actual: '', estimate: '', previous: '', unit: '' }, + ].filter((e) => e.date >= new Date().toISOString().slice(0, 10)); +} + +async function fetchEconomicCalendar() { + const apiKey = process.env.FINNHUB_API_KEY; + + if (!apiKey) { + console.warn(' FINNHUB_API_KEY missing β€” returning hardcoded FOMC dates'); + const events = buildFallbackEvents(); + const today = new Date().toISOString().slice(0, 10); + const future = new Date(Date.now() + 30 * 86400_000).toISOString().slice(0, 10); + return { events, fromDate: today, toDate: future, total: events.length }; + } + + const today = new Date(); + const from = today.toISOString().slice(0, 10); + const to = new Date(today.getTime() + 30 * 86400_000).toISOString().slice(0, 10); + + const url = `https://finnhub.io/api/v1/calendar/economic?from=${from}&to=${to}`; + + console.log(` Fetching Finnhub economic calendar ${from} β†’ ${to}`); + + const resp = await fetch(url, { + headers: { 'User-Agent': CHROME_UA, 'X-Finnhub-Token': apiKey }, + signal: AbortSignal.timeout(20_000), + }); + + if (!resp.ok) { + throw new Error(`Finnhub HTTP ${resp.status}`); + } + + const data = await resp.json(); + const raw = data?.economicCalendar ?? []; + + console.log(` Raw events from Finnhub: ${raw.length}`); + + const filtered = raw.filter((item) => { + const country = (item.country || '').toUpperCase(); + if (!ALLOWED_COUNTRIES.has(country)) return false; + const impact = normalizeImpact(item.impact); + if (impact === 'high') return true; + if (isHighPriority(item.event)) return true; + return false; + }); + + const transformed = filtered.map((item) => ({ + event: item.event || '', + country: (item.country || '').toUpperCase(), + date: toDateString(item.time || item.date || ''), + impact: normalizeImpact(item.impact), + actual: formatValue(item.actual), + estimate: formatValue(item.estimate), + previous: formatValue(item.prev), + unit: formatValue(item.unit), + })); + + transformed.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0)); + + const events = transformed.slice(0, 60); + + console.log(` Filtered to ${events.length} events`); + + return { events, fromDate: from, toDate: to, total: events.length }; +} + +function validate(data) { + return Array.isArray(data?.events) && data.events.length > 0; +} + +if (process.argv[1]?.endsWith('seed-economic-calendar.mjs')) { + runSeed('economic', 'econ-calendar', CANONICAL_KEY, fetchEconomicCalendar, { + validateFn: validate, + ttlSeconds: CACHE_TTL, + sourceVersion: 'finnhub-v1', + }).catch((err) => { + const _cause = err.cause ? ` (cause: ${err.cause.message || err.cause.code || err.cause})` : ''; + console.error('FATAL:', (err.message || err) + _cause); + process.exit(1); + }); +} diff --git a/scripts/seed-economy.mjs b/scripts/seed-economy.mjs index 86349fcfb..8a2a229c0 100755 --- a/scripts/seed-economy.mjs +++ b/scripts/seed-economy.mjs @@ -17,7 +17,7 @@ const ENERGY_TTL = 3600; const CAPACITY_TTL = 86400; const MACRO_TTL = 21600; // 6h β€” survive extended Yahoo outages -const FRED_SERIES = ['WALCL', 'FEDFUNDS', 'T10Y2Y', 'UNRATE', 'CPIAUCSL', 'DGS10', 'VIXCLS', 'GDP', 'M2SL', 'DCOILWTICO', 'BAMLH0A0HYM2', 'ICSA', 'MORTGAGE30US', 'BAMLC0A0CM', 'SOFR']; +const FRED_SERIES = ['WALCL', 'FEDFUNDS', 'T10Y2Y', 'UNRATE', 'CPIAUCSL', 'DGS10', 'VIXCLS', 'GDP', 'M2SL', 'DCOILWTICO', 'BAMLH0A0HYM2', 'ICSA', 'MORTGAGE30US', 'BAMLC0A0CM', 'SOFR', 'DGS1MO', 'DGS3MO', 'DGS6MO', 'DGS1', 'DGS2', 'DGS5', 'DGS30']; // ─── EIA Energy Prices (WTI + Brent) ─── diff --git a/scripts/seed-fear-greed.mjs b/scripts/seed-fear-greed.mjs index 08e409054..8228943c7 100644 --- a/scripts/seed-fear-greed.mjs +++ b/scripts/seed-fear-greed.mjs @@ -41,7 +41,7 @@ const FEAR_GREED_TTL = 64800; // 18h = 3x 6h interval const FRED_PREFIX = 'economic:fred:v1'; // --- Yahoo Finance fetching (15 symbols, 150ms gaps) --- -const YAHOO_SYMBOLS = ['^GSPC','^VIX','^VIX9D','^VIX3M','^SKEW','GLD','TLT','SPY','RSP','DX-Y.NYB','XLK','XLF','XLE','XLV','XLY','XLP','XLI','XLB','XLU','XLRE','XLC']; +const YAHOO_SYMBOLS = ['^GSPC','^VIX','^VIX9D','^VIX3M','^SKEW','GLD','TLT','HYG','SPY','RSP','DX-Y.NYB','XLK','XLF','XLE','XLV','XLY','XLP','XLI','XLB','XLU','XLRE','XLC']; async function fetchYahooSymbol(symbol) { const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(symbol)}?interval=1d&range=1y`; @@ -126,7 +126,9 @@ async function fetchCNN() { if (!resp.ok) { console.warn(` CNN F&G: HTTP ${resp.status}`); return null; } const data = await resp.json(); const score = data?.score ?? data?.fear_and_greed?.score; - const rating = data?.rating ?? data?.fear_and_greed?.rating; + const rawRating = data?.rating ?? data?.fear_and_greed?.rating; + const VALID_CNN_LABELS = new Set(['Extreme Fear', 'Fear', 'Neutral', 'Greed', 'Extreme Greed']); + const rating = (typeof rawRating === 'string' && VALID_CNN_LABELS.has(rawRating)) ? rawRating : null; return score != null ? { score: Math.round(score), label: rating ?? labelFromScore(Math.round(score)) } : null; } catch (e) { console.warn(` CNN F&G: ${e.message}`); return null; } } @@ -410,7 +412,7 @@ async function fetchAll() { const vix9d = yahoo['^VIX9D']; const vix3m = yahoo['^VIX3M']; const skew = yahoo['^SKEW']; - const gld = yahoo['GLD'], tlt = yahoo['TLT'], spy = yahoo['SPY'], rsp = yahoo['RSP']; + const gld = yahoo['GLD'], tlt = yahoo['TLT'], hyg = yahoo['HYG'], spy = yahoo['SPY'], rsp = yahoo['RSP']; const dxy = yahoo['DX-Y.NYB']; const xlk = yahoo['XLK'], xlf = yahoo['XLF'], xle = yahoo['XLE'], xlv = yahoo['XLV']; const xly = yahoo['XLY'], xlp = yahoo['XLP'], xli = yahoo['XLI'], xlb = yahoo['XLB']; @@ -450,6 +452,27 @@ async function fetchAll() { const fedRateStr = fedRate != null ? `${fedRate.toFixed(2)}%` : null; const hySpreadVal = fredLatest(hyObs); + const hygPrice = hyg?.price ?? null; + const tltPrice = tlt?.price ?? null; + let fsiValue = null; + let fsiLabel = 'Unknown'; + if (hygPrice != null && tltPrice != null && tltPrice > 0 && vixLive != null && vixLive > 0 && hySpreadVal != null && hySpreadVal > 0) { + fsiValue = Math.round(((hygPrice / tltPrice) / (vixLive * hySpreadVal / 100)) * 10000) / 10000; + if (fsiValue >= 1.5) fsiLabel = 'Low Stress'; + else if (fsiValue >= 0.8) fsiLabel = 'Moderate Stress'; + else if (fsiValue >= 0.3) fsiLabel = 'Elevated Stress'; + else fsiLabel = 'High Stress'; + } + + const SECTOR_ETF_NAMES = { XLK: 'Technology', XLF: 'Financials', XLE: 'Energy', XLV: 'Health Care', XLY: 'Consumer Discr.', XLP: 'Consumer Staples', XLI: 'Industrials', XLB: 'Materials', XLU: 'Utilities', XLRE: 'Real Estate', XLC: 'Comm. Services' }; + const sectorPerformance = Object.entries(SECTOR_ETF_NAMES).map(([sym, name]) => { + const d = yahoo[sym]; + if (!d?.closes || d.closes.length < 2) return null; + const prev = d.closes.at(-2), curr = d.closes.at(-1); + const change1d = (prev && prev > 0) ? Math.round(((curr - prev) / prev) * 10000) / 100 : null; + return change1d != null ? { symbol: sym, name, change1d } : null; + }).filter(Boolean); + const payload = { timestamp: new Date().toISOString(), composite: { score: compositeScore, label: compositeLabel, previous: previousScore }, @@ -475,7 +498,9 @@ async function fetchAll() { pctAbove200d: pctAbove200d != null ? { value: pctAbove200d } : null, yield10y: fredLatest(dgs10Obs) != null ? { value: fredLatest(dgs10Obs) } : null, fedRate: fedRateStr ? { value: fedRateStr } : null, + fsi: fsiValue != null ? { value: fsiValue, label: fsiLabel, hygPrice, tltPrice } : null, }, + sectorPerformance, unavailable: false, }; diff --git a/server/gateway.ts b/server/gateway.ts index 2246625f6..127fe17cb 100644 --- a/server/gateway.ts +++ b/server/gateway.ts @@ -183,6 +183,10 @@ const RPC_CACHE_TIER: Record = { '/api/consumer-prices/v1/get-consumer-price-freshness': 'slow', '/api/aviation/v1/get-youtube-live-stream-info': 'fast', + + '/api/market/v1/list-earnings-calendar': 'slow', + '/api/market/v1/get-cot-positioning': 'slow', + '/api/economic/v1/get-economic-calendar': 'slow', }; // TODO(payment-pr): PREMIUM_RPC_PATHS is intentionally empty until the payment/pro-user diff --git a/server/worldmonitor/economic/v1/get-economic-calendar.ts b/server/worldmonitor/economic/v1/get-economic-calendar.ts new file mode 100644 index 000000000..0d6bf5d78 --- /dev/null +++ b/server/worldmonitor/economic/v1/get-economic-calendar.ts @@ -0,0 +1,40 @@ +import type { + ServerContext, + GetEconomicCalendarRequest, + GetEconomicCalendarResponse, + EconomicEvent, +} from '../../../../src/generated/server/worldmonitor/economic/v1/service_server'; +import { getCachedJson } from '../../../_shared/redis'; + +const SEED_CACHE_KEY = 'economic:econ-calendar:v1'; + +function buildFallbackResult(): GetEconomicCalendarResponse { + return { + events: [], + fromDate: '', + toDate: '', + total: 0, + unavailable: true, + }; +} + +export async function getEconomicCalendar( + _ctx: ServerContext, + _req: GetEconomicCalendarRequest, +): Promise { + try { + const result = await getCachedJson(SEED_CACHE_KEY, true) as GetEconomicCalendarResponse | null; + if (result && !result.unavailable && Array.isArray(result.events) && result.events.length > 0) { + return { + events: result.events as EconomicEvent[], + fromDate: result.fromDate ?? '', + toDate: result.toDate ?? '', + total: result.total ?? result.events.length, + unavailable: false, + }; + } + return buildFallbackResult(); + } catch { + return buildFallbackResult(); + } +} diff --git a/server/worldmonitor/economic/v1/get-fred-series-batch.ts b/server/worldmonitor/economic/v1/get-fred-series-batch.ts index 7ade53c09..105f5dc42 100644 --- a/server/worldmonitor/economic/v1/get-fred-series-batch.ts +++ b/server/worldmonitor/economic/v1/get-fred-series-batch.ts @@ -18,6 +18,8 @@ const ALLOWED_SERIES = new Set([ 'WALCL', 'FEDFUNDS', 'T10Y2Y', 'UNRATE', 'CPIAUCSL', 'DGS10', 'VIXCLS', 'GDP', 'M2SL', 'DCOILWTICO', 'BAMLH0A0HYM2', 'ICSA', 'MORTGAGE30US', 'GSCPI', // NY Fed Global Supply Chain Pressure Index (seeded by ais-relay, not FRED API) + 'DGS1MO', 'DGS3MO', 'DGS6MO', 'DGS1', 'DGS2', 'DGS5', 'DGS30', // yield curve tenors + 'BAMLC0A0CM', 'SOFR', // IG OAS spread + Secured Overnight Financing Rate (seeded by seed-economy.mjs) ]); export async function getFredSeriesBatch( diff --git a/server/worldmonitor/economic/v1/handler.ts b/server/worldmonitor/economic/v1/handler.ts index c571ac89d..52e368bd1 100644 --- a/server/worldmonitor/economic/v1/handler.ts +++ b/server/worldmonitor/economic/v1/handler.ts @@ -14,6 +14,7 @@ import { listBigMacPrices } from './list-bigmac-prices'; import { getNationalDebt } from './get-national-debt'; import { listFuelPrices } from './list-fuel-prices'; import { getBlsSeries } from './get-bls-series'; +import { getEconomicCalendar } from './get-economic-calendar'; export const economicHandler: EconomicServiceHandler = { getFredSeries, @@ -30,4 +31,5 @@ export const economicHandler: EconomicServiceHandler = { getNationalDebt, listFuelPrices, getBlsSeries, + getEconomicCalendar, }; diff --git a/server/worldmonitor/market/v1/get-cot-positioning.ts b/server/worldmonitor/market/v1/get-cot-positioning.ts new file mode 100644 index 000000000..be60aa66d --- /dev/null +++ b/server/worldmonitor/market/v1/get-cot-positioning.ts @@ -0,0 +1,55 @@ +import type { + ServerContext, + GetCotPositioningRequest, + GetCotPositioningResponse, + CotInstrument, +} from '../../../../src/generated/server/worldmonitor/market/v1/service_server'; +import { getCachedJson } from '../../../_shared/redis'; + +const SEED_CACHE_KEY = 'market:cot:v1'; + +interface RawInstrument { + name: string; + code: string; + reportDate: string; + assetManagerLong: number; + assetManagerShort: number; + leveragedFundsLong: number; + leveragedFundsShort: number; + dealerLong: number; + dealerShort: number; + netPct: number; +} + +export async function getCotPositioning( + _ctx: ServerContext, + _req: GetCotPositioningRequest, +): Promise { + try { + const raw = await getCachedJson(SEED_CACHE_KEY, true) as { instruments?: RawInstrument[]; reportDate?: string } | null; + if (!raw?.instruments || raw.instruments.length === 0) { + return { instruments: [], reportDate: '', unavailable: true }; + } + + const instruments: CotInstrument[] = raw.instruments.map(item => ({ + name: String(item.name ?? ''), + code: String(item.code ?? ''), + reportDate: String(item.reportDate ?? ''), + assetManagerLong: String(item.assetManagerLong ?? 0), + assetManagerShort: String(item.assetManagerShort ?? 0), + leveragedFundsLong: String(item.leveragedFundsLong ?? 0), + leveragedFundsShort: String(item.leveragedFundsShort ?? 0), + dealerLong: String(item.dealerLong ?? 0), + dealerShort: String(item.dealerShort ?? 0), + netPct: Number(item.netPct ?? 0), + })); + + return { + instruments, + reportDate: String(raw.reportDate ?? ''), + unavailable: false, + }; + } catch { + return { instruments: [], reportDate: '', unavailable: true }; + } +} diff --git a/server/worldmonitor/market/v1/get-fear-greed-index.ts b/server/worldmonitor/market/v1/get-fear-greed-index.ts index 6315969d9..29d4b286c 100644 --- a/server/worldmonitor/market/v1/get-fear-greed-index.ts +++ b/server/worldmonitor/market/v1/get-fear-greed-index.ts @@ -53,6 +53,10 @@ export async function getFearGreedIndex( aaiiBull: Number(hdr?.aaiBull?.value ?? 0), aaiiBear: Number(hdr?.aaiBear?.value ?? 0), fedRate: String(hdr?.fedRate?.value ?? ''), + fsiValue: Number(hdr?.fsi?.value ?? 0), + fsiLabel: String(hdr?.fsi?.label ?? ''), + hygPrice: Number(hdr?.fsi?.hygPrice ?? 0), + tltPrice: Number(hdr?.fsi?.tltPrice ?? 0), unavailable: false, }; } catch { diff --git a/server/worldmonitor/market/v1/handler.ts b/server/worldmonitor/market/v1/handler.ts index cfdf30657..5d0488712 100644 --- a/server/worldmonitor/market/v1/handler.ts +++ b/server/worldmonitor/market/v1/handler.ts @@ -30,6 +30,8 @@ import { listDefiTokens } from './list-defi-tokens'; import { listAiTokens } from './list-ai-tokens'; import { listOtherTokens } from './list-other-tokens'; import { getFearGreedIndex } from './get-fear-greed-index'; +import { listEarningsCalendar } from './list-earnings-calendar'; +import { getCotPositioning } from './get-cot-positioning'; export const marketHandler: MarketServiceHandler = { listMarketQuotes, @@ -49,4 +51,6 @@ export const marketHandler: MarketServiceHandler = { listAiTokens, listOtherTokens, getFearGreedIndex, + listEarningsCalendar, + getCotPositioning, }; diff --git a/server/worldmonitor/market/v1/list-earnings-calendar.ts b/server/worldmonitor/market/v1/list-earnings-calendar.ts new file mode 100644 index 000000000..54655a33f --- /dev/null +++ b/server/worldmonitor/market/v1/list-earnings-calendar.ts @@ -0,0 +1,42 @@ +import type { + ServerContext, + ListEarningsCalendarRequest, + ListEarningsCalendarResponse, + EarningsEntry, +} from '../../../../src/generated/server/worldmonitor/market/v1/service_server'; +import { getCachedJson } from '../../../_shared/redis'; + +const SEED_CACHE_KEY = 'market:earnings-calendar:v1'; + +export async function listEarningsCalendar( + _ctx: ServerContext, + _req: ListEarningsCalendarRequest, +): Promise { + try { + const cached = await getCachedJson(SEED_CACHE_KEY, true) as { earnings?: EarningsEntry[]; unavailable?: boolean } | null; + if (!cached?.earnings?.length) { + return { earnings: [], fromDate: '', toDate: '', total: 0, unavailable: true }; + } + + const entries: EarningsEntry[] = cached.earnings.map(e => ({ + symbol: e.symbol ?? '', + company: e.company ?? '', + date: e.date ?? '', + hour: e.hour ?? '', + epsEstimate: e.epsEstimate ?? 0, + revenueEstimate: e.revenueEstimate ?? 0, + epsActual: e.epsActual ?? 0, + revenueActual: e.revenueActual ?? 0, + hasActuals: e.hasActuals ?? false, + surpriseDirection: e.surpriseDirection ?? '', + })); + + const dates = entries.map(e => e.date).filter(Boolean).sort(); + const fromDate = dates[0] ?? ''; + const toDate = dates[dates.length - 1] ?? ''; + + return { earnings: entries, fromDate, toDate, total: entries.length, unavailable: false }; + } catch { + return { earnings: [], fromDate: '', toDate: '', total: 0, unavailable: true }; + } +} diff --git a/src/app/data-loader.ts b/src/app/data-loader.ts index 8c704a49b..54ab16bb3 100644 --- a/src/app/data-loader.ts +++ b/src/app/data-loader.ts @@ -168,6 +168,9 @@ import { cacheDailyMarketBrief, getCachedDailyMarketBrief, shouldRefreshDailyBrief, + type RegimeMacroContext, + type YieldCurveContext, + type SectorBriefContext, } from '@/services/daily-market-brief'; import { fetchCachedRiskScores } from '@/services/cached-risk-scores'; import type { ThreatLevel as ClientThreatLevel } from '@/types'; @@ -1404,10 +1407,22 @@ export class DataLoaderManager implements AppModule { this.callPanel('daily-market-brief', 'showLoading', 'Building daily market brief...'); } + const [r0, r1, r2] = await Promise.allSettled([ + this._collectRegimeContext(), + this._collectYieldCurveContext(), + this._collectSectorContext(), + ]); + const regimeContext = r0.status === 'fulfilled' ? r0.value : undefined; + const yieldCurveContext = r1.status === 'fulfilled' ? r1.value : undefined; + const sectorContext = r2.status === 'fulfilled' ? r2.value : undefined; + const brief = await buildDailyMarketBrief({ markets: this.ctx.latestMarkets, newsByCategory: this.ctx.newsByCategory, timezone, + regimeContext, + yieldCurveContext, + sectorContext, }); if (!brief.available) { @@ -1433,6 +1448,93 @@ export class DataLoaderManager implements AppModule { } } + private async _collectRegimeContext(): Promise { + try { + const hydrated = getHydratedData('fearGreedIndex') as Record | undefined; + if (hydrated && !hydrated.unavailable && Number(hydrated.compositeScore) > 0) { + const comp = hydrated.composite as Record | undefined; + const cats = (hydrated.categories ?? {}) as Record>; + const hdr = (hydrated.headerMetrics ?? {}) as Record | null>; + return { + compositeScore: Number(comp?.score ?? hydrated.compositeScore ?? 0), + compositeLabel: String(comp?.label ?? hydrated.compositeLabel ?? ''), + fsiValue: Number(hdr?.fsi?.value ?? 0), + fsiLabel: String(hdr?.fsi?.label ?? ''), + vix: Number(hdr?.vix?.value ?? 0), + hySpread: Number(hdr?.hySpread?.value ?? 0), + cnnFearGreed: Number(hdr?.cnnFearGreed?.value ?? 0), + cnnLabel: String(hdr?.cnnFearGreed?.label ?? ''), + momentum: cats.momentum ? { score: Number(cats.momentum.score ?? 0) } : undefined, + sentiment: cats.sentiment ? { score: Number(cats.sentiment.score ?? 0) } : undefined, + }; + } + const { MarketServiceClient } = await import('@/generated/client/worldmonitor/market/v1/service_client'); + const { getRpcBaseUrl } = await import('@/services/rpc-client'); + const client = new MarketServiceClient(getRpcBaseUrl(), { fetch: (...args: Parameters) => globalThis.fetch(...args) }); + const resp = await client.getFearGreedIndex({}); + if (resp.unavailable || resp.compositeScore <= 0) return undefined; + return { + compositeScore: resp.compositeScore, + compositeLabel: resp.compositeLabel, + fsiValue: resp.fsiValue ?? 0, + fsiLabel: resp.fsiLabel ?? '', + vix: resp.vix ?? 0, + hySpread: resp.hySpread ?? 0, + cnnFearGreed: resp.cnnFearGreed ?? 0, + cnnLabel: resp.cnnLabel ?? '', + momentum: resp.momentum ? { score: resp.momentum.score } : undefined, + sentiment: resp.sentiment ? { score: resp.sentiment.score } : undefined, + }; + } catch { + return undefined; + } + } + + private async _collectYieldCurveContext(): Promise { + try { + const { EconomicServiceClient } = await import('@/generated/client/worldmonitor/economic/v1/service_client'); + const { getRpcBaseUrl } = await import('@/services/rpc-client'); + const client = new EconomicServiceClient(getRpcBaseUrl(), { fetch: (...args: Parameters) => globalThis.fetch(...args) }); + const resp = await client.getFredSeriesBatch({ seriesIds: ['DGS2', 'DGS10', 'DGS30'], limit: 1 }); + const lastVal = (id: string): number => { + const obs = resp.results[id]?.observations; + if (!obs?.length) return 0; + return obs[obs.length - 1]?.value ?? 0; + }; + const rate2y = lastVal('DGS2'); + const rate10y = lastVal('DGS10'); + const rate30y = lastVal('DGS30'); + if (!rate10y) return undefined; + const spread2s10s = rate2y > 0 ? Math.round((rate10y - rate2y) * 100) : 0; + return { inverted: spread2s10s < 0, spread2s10s, rate2y, rate10y, rate30y }; + } catch { + return undefined; + } + } + + private _collectSectorContext(): SectorBriefContext | undefined { + try { + const hydratedSectors = getHydratedData('sectors') as GetSectorSummaryResponse | undefined; + const sectors = hydratedSectors?.sectors; + if (!sectors?.length) return undefined; + const sorted = [...sectors].sort((a, b) => b.change - a.change); + const countPositive = sorted.filter(s => s.change > 0).length; + const top = sorted[0]; + const worst = sorted[sorted.length - 1]; + if (!top || !worst) return undefined; + return { + topName: top.name, + topChange: top.change, + worstName: worst.name, + worstChange: worst.change, + countPositive, + total: sorted.length, + }; + } catch { + return undefined; + } + } + async loadMarketImplications(): Promise { if (!getSecretState('WORLDMONITOR_API_KEY').present && !isProUser()) return; if (this.ctx.isDestroyed || this.ctx.inFlight.has('marketImplications')) return; diff --git a/src/app/panel-layout.ts b/src/app/panel-layout.ts index 43454ed51..1cd05bb0b 100644 --- a/src/app/panel-layout.ts +++ b/src/app/panel-layout.ts @@ -56,6 +56,12 @@ import { EconomicCorrelationPanel, DisasterCorrelationPanel, HormuzPanel, + MacroTilesPanel, + FSIPanel, + YieldCurvePanel, + EarningsCalendarPanel, + EconomicCalendarPanel, + CotPositioningPanel, } from '@/components'; import { SatelliteFiresPanel } from '@/components/SatelliteFiresPanel'; import { focusInvestmentOnMap } from '@/services/investments-focus'; @@ -891,6 +897,12 @@ export class PanelLayoutManager implements AppModule { this.createPanel('macro-signals', () => new MacroSignalsPanel()); this.createPanel('fear-greed', () => new FearGreedPanel()); + this.createPanel('macro-tiles', () => new MacroTilesPanel()); + this.createPanel('fsi', () => new FSIPanel()); + this.createPanel('yield-curve', () => new YieldCurvePanel()); + this.createPanel('earnings-calendar', () => new EarningsCalendarPanel()); + this.createPanel('economic-calendar', () => new EconomicCalendarPanel()); + this.createPanel('cot-positioning', () => new CotPositioningPanel()); this.createPanel('hormuz-tracker', () => new HormuzPanel()); this.createPanel('etf-flows', () => new ETFFlowsPanel()); this.createPanel('stablecoins', () => new StablecoinPanel()); diff --git a/src/components/CotPositioningPanel.ts b/src/components/CotPositioningPanel.ts new file mode 100644 index 000000000..e2999f8e7 --- /dev/null +++ b/src/components/CotPositioningPanel.ts @@ -0,0 +1,105 @@ +import type { MarketServiceClient } from '@/generated/client/worldmonitor/market/v1/service_client'; +import { Panel } from './Panel'; +import { escapeHtml } from '@/utils/sanitize'; + +let _client: MarketServiceClient | null = null; +async function getMarketClient(): Promise { + if (!_client) { + const { MarketServiceClient } = await import('@/generated/client/worldmonitor/market/v1/service_client'); + const { getRpcBaseUrl } = await import('@/services/rpc-client'); + _client = new MarketServiceClient(getRpcBaseUrl(), { fetch: (...args: Parameters) => globalThis.fetch(...args) }); + } + return _client; +} + +interface CotInstrumentData { + name: string; + code: string; + reportDate: string; + assetManagerLong: string; + assetManagerShort: string; + leveragedFundsLong: string; + leveragedFundsShort: string; + dealerLong: string; + dealerShort: string; + netPct: number; +} + +function toNum(v: string | number): number { + return typeof v === 'number' ? v : parseInt(String(v), 10) || 0; +} + +function renderPositionBar(netPct: number, label: string): string { + const clamped = Math.max(-100, Math.min(100, netPct)); + const halfWidth = Math.abs(clamped) / 100 * 50; + const color = clamped >= 0 ? '#2ecc71' : '#e74c3c'; + const leftPct = clamped >= 0 ? 50 : 50 - halfWidth; + const sign = clamped >= 0 ? '+' : ''; + return ` +
+
+ ${escapeHtml(label)} + ${sign}${clamped.toFixed(1)}% +
+
+
+
+
+
`; +} + +function renderInstrument(item: CotInstrumentData): string { + const levLong = toNum(item.leveragedFundsLong); + const levShort = toNum(item.leveragedFundsShort); + const amNetPct = item.netPct; + const levNetPct = ((levLong - levShort) / Math.max(levLong + levShort, 1)) * 100; + + return ` +
+
+ ${escapeHtml(item.name)} + ${escapeHtml(item.code)} +
+ ${renderPositionBar(amNetPct, 'Asset Managers')} + ${renderPositionBar(levNetPct, 'Leveraged Funds')} +
`; +} + +export class CotPositioningPanel extends Panel { + private _hasData = false; + + constructor() { + super({ id: 'cot-positioning', title: 'CFTC COT Positioning', showCount: false }); + } + + public async fetchData(): Promise { + this.showLoading(); + try { + const client = await getMarketClient(); + const resp = await client.getCotPositioning({}); + if (resp.unavailable || !resp.instruments || resp.instruments.length === 0) { + if (!this._hasData) this.showError('COT data unavailable', () => void this.fetchData()); + return false; + } + this._hasData = true; + this.render(resp.instruments as CotInstrumentData[], resp.reportDate ?? ''); + return true; + } catch (e) { + if (!this._hasData) this.showError(e instanceof Error ? e.message : 'Failed to load', () => void this.fetchData()); + return false; + } + } + + private render(instruments: CotInstrumentData[], reportDate: string): void { + const rows = instruments.map(renderInstrument).join(''); + const dateFooter = reportDate + ? `
Report date: ${escapeHtml(reportDate)}
` + : ''; + const html = ` +
+ ${rows} + ${dateFooter} +
`; + this.setContent(html); + } +} diff --git a/src/components/EarningsCalendarPanel.ts b/src/components/EarningsCalendarPanel.ts new file mode 100644 index 000000000..f6429dfb5 --- /dev/null +++ b/src/components/EarningsCalendarPanel.ts @@ -0,0 +1,138 @@ +import type { MarketServiceClient } from '@/generated/client/worldmonitor/market/v1/service_client'; +import { Panel } from './Panel'; +import { escapeHtml } from '@/utils/sanitize'; + +let _client: MarketServiceClient | null = null; +async function getMarketClient(): Promise { + if (!_client) { + const { MarketServiceClient } = await import('@/generated/client/worldmonitor/market/v1/service_client'); + const { getRpcBaseUrl } = await import('@/services/rpc-client'); + _client = new MarketServiceClient(getRpcBaseUrl(), { fetch: (...args: Parameters) => globalThis.fetch(...args) }); + } + return _client; +} + +interface EarningsEntry { + symbol: string; + company: string; + date: string; + hour: string; + epsEstimate: number | null; + revenueEstimate: number | null; + epsActual: number | null; + revenueActual: number | null; + hasActuals: boolean; + surpriseDirection: string; +} + +function fmtDate(dateStr: string): string { + try { + const d = new Date(dateStr + 'T12:00:00Z'); + return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', timeZone: 'UTC' }); + } catch { + return dateStr; + } +} + +function fmtEps(v: number | null): string { + if (v == null) return ''; + return v.toFixed(2); +} + +function renderEntry(e: EarningsEntry): string { + const hourLabel = e.hour === 'bmo' ? 'BMO' : e.hour === 'amc' ? 'AMC' : e.hour.toUpperCase(); + const hourColor = e.hour === 'bmo' + ? 'background:rgba(46,204,113,0.15);color:#2ecc71' + : 'background:rgba(52,152,219,0.15);color:#3498db'; + + const epsEst = fmtEps(e.epsEstimate); + const epsAct = fmtEps(e.epsActual); + + let rightSection = ''; + if (e.hasActuals && epsAct) { + const badgeColor = e.surpriseDirection === 'beat' + ? 'background:rgba(46,204,113,0.2);color:#2ecc71' + : e.surpriseDirection === 'miss' + ? 'background:rgba(231,76,60,0.2);color:#e74c3c' + : 'background:rgba(255,255,255,0.08);color:var(--text-dim)'; + const badgeLabel = e.surpriseDirection === 'beat' ? 'BEAT' : e.surpriseDirection === 'miss' ? 'MISS' : ''; + rightSection = ` + EPS ${escapeHtml(epsAct)} + ${badgeLabel ? `${escapeHtml(badgeLabel)}` : ''}`; + } else if (epsEst) { + rightSection = `est ${escapeHtml(epsEst)}`; + } + + return ` +
+ ${escapeHtml(hourLabel)} +
+
${escapeHtml(e.symbol)}
+
${escapeHtml(e.company)}
+
+
${rightSection}
+
`; +} + +function renderGroup(date: string, entries: EarningsEntry[]): string { + return ` +
${escapeHtml(fmtDate(date))}
+ ${entries.map(renderEntry).join('')}`; +} + +export class EarningsCalendarPanel extends Panel { + private _hasData = false; + + constructor() { + super({ id: 'earnings-calendar', title: 'Earnings Calendar', showCount: false }); + } + + public async fetchData(): Promise { + this.showLoading(); + return this.refreshFromRpc(); + } + + private async refreshFromRpc(): Promise { + try { + const client = await getMarketClient(); + const today = new Date(); + const future = new Date(); + future.setDate(future.getDate() + 14); + const fromDate = today.toISOString().slice(0, 10); + const toDate = future.toISOString().slice(0, 10); + const resp = await client.listEarningsCalendar({ fromDate, toDate }); + + if (resp.unavailable || !resp.earnings?.length) { + if (!this._hasData) this.showError('No earnings data', () => void this.fetchData()); + return false; + } + + this.render(resp.earnings as EarningsEntry[]); + return true; + } catch (e) { + if (!this._hasData) this.showError(e instanceof Error ? e.message : 'Failed to load', () => void this.fetchData()); + return false; + } + } + + private render(earnings: EarningsEntry[]): void { + this._hasData = true; + + const grouped = new Map(); + for (const e of earnings) { + const key = e.date || 'Unknown'; + const arr = grouped.get(key); + if (arr) arr.push(e); + else grouped.set(key, [e]); + } + + const sortedDates = [...grouped.keys()].sort(); + + const html = ` +
+ ${sortedDates.map(d => renderGroup(d, grouped.get(d)!)).join('')} +
`; + + this.setContent(html); + } +} diff --git a/src/components/EconomicCalendarPanel.ts b/src/components/EconomicCalendarPanel.ts new file mode 100644 index 000000000..ceca02844 --- /dev/null +++ b/src/components/EconomicCalendarPanel.ts @@ -0,0 +1,142 @@ +import type { EconomicServiceClient } from '@/generated/client/worldmonitor/economic/v1/service_client'; +import { Panel } from './Panel'; +import { escapeHtml } from '@/utils/sanitize'; + +let _client: EconomicServiceClient | null = null; +async function getEconomicClient(): Promise { + if (!_client) { + const { EconomicServiceClient } = await import('@/generated/client/worldmonitor/economic/v1/service_client'); + const { getRpcBaseUrl } = await import('@/services/rpc-client'); + _client = new EconomicServiceClient(getRpcBaseUrl(), { fetch: (...args: Parameters) => globalThis.fetch(...args) }); + } + return _client; +} + +const COUNTRY_FLAGS: Record = { + US: 'πŸ‡ΊπŸ‡Έ', + GB: 'πŸ‡¬πŸ‡§', + UK: 'πŸ‡¬πŸ‡§', + EU: 'πŸ‡ͺπŸ‡Ί', + EUR: 'πŸ‡ͺπŸ‡Ί', + DE: 'πŸ‡©πŸ‡ͺ', + FR: 'πŸ‡«πŸ‡·', + JP: 'πŸ‡―πŸ‡΅', + CN: 'πŸ‡¨πŸ‡³', + CA: 'πŸ‡¨πŸ‡¦', + AU: 'πŸ‡¦πŸ‡Ί', +}; + +const IMPACT_COLORS: Record = { + high: '#e74c3c', + medium: '#f39c12', + low: 'rgba(255,255,255,0.3)', +}; + +interface EconomicEvent { + event: string; + country: string; + date: string; + impact: string; + actual: string; + estimate: string; + previous: string; + unit: string; +} + +function groupByDate(events: EconomicEvent[]): Map { + const map = new Map(); + for (const ev of events) { + const key = ev.date || 'Unknown'; + if (!map.has(key)) map.set(key, []); + map.get(key)!.push(ev); + } + return map; +} + +function formatDate(dateStr: string): string { + if (!dateStr || dateStr === 'Unknown') return 'Unknown Date'; + const d = new Date(`${dateStr}T00:00:00`); + if (Number.isNaN(d.getTime())) return dateStr; + return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' }); +} + +function formatMetaValue(val: string, unit: string): string { + if (!val) return 'β€”'; + return unit ? `${val} ${unit}` : val; +} + +export class EconomicCalendarPanel extends Panel { + private _hasData = false; + private _events: EconomicEvent[] = []; + + constructor() { + super({ id: 'economic-calendar', title: 'Economic Calendar', showCount: false }); + } + + public async fetchData(): Promise { + this.showLoading('Loading economic calendar...'); + try { + const client = await getEconomicClient(); + const today = new Date(); + const fromDate = today.toISOString().slice(0, 10); + const toDate = new Date(today.getTime() + 30 * 86400_000).toISOString().slice(0, 10); + const resp = await client.getEconomicCalendar({ fromDate, toDate }); + + if (resp.unavailable || !resp.events || resp.events.length === 0) { + if (!this._hasData) this.showError('Economic calendar data unavailable.', () => void this.fetchData()); + return false; + } + + this._events = resp.events as EconomicEvent[]; + this._hasData = true; + this._render(); + return true; + } catch (err) { + if (this.isAbortError(err)) return false; + if (!this._hasData) this.showError('Failed to load economic calendar.', () => void this.fetchData()); + return false; + } + } + + private _render(): void { + if (!this._hasData || this._events.length === 0) { + if (!this._hasData) this.showError('No upcoming economic events.', () => void this.fetchData()); + return; + } + + const grouped = groupByDate(this._events); + const sections: string[] = []; + + for (const [date, events] of grouped) { + const dateHeader = `
${escapeHtml(formatDate(date))}
`; + const rows = events.map((ev) => { + const impact = (ev.impact || 'low').toLowerCase(); + const color = IMPACT_COLORS[impact] ?? IMPACT_COLORS.low; + const flag = COUNTRY_FLAGS[ev.country] ?? escapeHtml(ev.country); + const isHigh = impact === 'high'; + const badge = `${escapeHtml(impact)}`; + const name = isHigh + ? `${escapeHtml(ev.event)}` + : escapeHtml(ev.event); + const meta = [ + ev.actual ? `Actual: ${escapeHtml(formatMetaValue(ev.actual, ev.unit))}` : '', + ev.estimate ? `Est: ${escapeHtml(formatMetaValue(ev.estimate, ev.unit))}` : '', + ev.previous ? `Prev: ${escapeHtml(formatMetaValue(ev.previous, ev.unit))}` : '', + ].filter(Boolean).join('  '); + + return `
+
+ ${flag} + ${name} + ${badge} +
+ ${meta ? `
${meta}
` : ''} +
`; + }).join(''); + + sections.push(`
${dateHeader}${rows}
`); + } + + this.setContent(`
${sections.join('')}
`); + } +} diff --git a/src/components/FSIPanel.ts b/src/components/FSIPanel.ts new file mode 100644 index 000000000..9a5e7e96a --- /dev/null +++ b/src/components/FSIPanel.ts @@ -0,0 +1,113 @@ +import type { MarketServiceClient } from '@/generated/client/worldmonitor/market/v1/service_client'; +import { Panel } from './Panel'; +import { escapeHtml } from '@/utils/sanitize'; +import { getHydratedData } from '@/services/bootstrap'; + +let _client: MarketServiceClient | null = null; +async function getMarketClient(): Promise { + if (!_client) { + const { MarketServiceClient } = await import('@/generated/client/worldmonitor/market/v1/service_client'); + const { getRpcBaseUrl } = await import('@/services/rpc-client'); + _client = new MarketServiceClient(getRpcBaseUrl(), { fetch: (...args: Parameters) => globalThis.fetch(...args) }); + } + return _client; +} + +function fsiLabelColor(label: string): string { + if (label === 'Low Stress') return '#27ae60'; + if (label === 'Moderate Stress') return '#f39c12'; + if (label === 'Elevated Stress') return '#e67e22'; + return '#c0392b'; +} + +function fsiInterpretation(label: string): string { + if (label === 'Low Stress') return 'Credit markets functioning normally, equity/bond ratio healthy.'; + if (label === 'Moderate Stress') return 'Some deterioration in credit conditions, monitor closely.'; + if (label === 'Elevated Stress') return 'Significant credit market stress, defensive positioning warranted.'; + return 'Severe financial stress, systemic risk elevated.'; +} + +function metricCard(label: string, value: string): string { + return `
+
${escapeHtml(label)}
+
${escapeHtml(value)}
+
`; +} + +export class FSIPanel extends Panel { + private _hasData = false; + + constructor() { + super({ id: 'fsi', title: 'Financial Stress Indicator', showCount: false }); + } + + public async fetchData(): Promise { + this.showLoading(); + try { + const hydrated = getHydratedData('fearGreedIndex') as Record | undefined; + if (hydrated && !hydrated.unavailable) { + const hdr = (hydrated.headerMetrics ?? {}) as Record | null>; + const fsiValue = Number(hdr?.fsi?.value ?? 0); + const fsiLabel = String(hdr?.fsi?.label ?? ''); + if (fsiValue > 0) { + this._hasData = true; + this.render({ + fsiValue, + fsiLabel, + hygPrice: 0, + tltPrice: 0, + vix: Number(hdr?.vix?.value ?? 0), + hySpread: Number(hdr?.hySpread?.value ?? 0), + }); + return true; + } + } + const client = await getMarketClient(); + const resp = await client.getFearGreedIndex({}); + if (resp.unavailable || resp.fsiValue <= 0) { + if (!this._hasData) this.showError('FSI data unavailable', () => void this.fetchData()); + return false; + } + this._hasData = true; + this.render(resp); + return true; + } catch (e) { + if (!this._hasData) this.showError(e instanceof Error ? e.message : 'Failed to load', () => void this.fetchData()); + return false; + } + } + + private render(resp: { fsiValue: number; fsiLabel: string; hygPrice: number; tltPrice: number; vix: number; hySpread: number }): void { + const { fsiValue, fsiLabel, hygPrice, tltPrice, vix, hySpread } = resp; + const labelColor = fsiLabelColor(fsiLabel); + const fillPct = Math.min(Math.max((fsiValue / 2.5) * 100, 0), 100); + const interpretation = fsiInterpretation(fsiLabel); + + const html = `
+
+
FSI VALUE
+
${fsiValue.toFixed(4)}
+
${escapeHtml(fsiLabel)}
+
+
+
+ High StressLow Stress +
+
+
+
+
+
+ ${metricCard('VIX', vix > 0 ? vix.toFixed(2) : 'N/A')} + ${metricCard('HY Spread', hySpread > 0 ? hySpread.toFixed(2) + '%' : 'N/A')} + ${metricCard('HYG Price', hygPrice > 0 ? '$' + hygPrice.toFixed(2) : 'N/A')} + ${metricCard('TLT Price', tltPrice > 0 ? '$' + tltPrice.toFixed(2) : 'N/A')} +
+
+ ${escapeHtml(interpretation)} +
+
`; + + this.setContent(html); + } +} diff --git a/src/components/FearGreedPanel.ts b/src/components/FearGreedPanel.ts index bec4b41f6..d76b2bcea 100644 --- a/src/components/FearGreedPanel.ts +++ b/src/components/FearGreedPanel.ts @@ -52,6 +52,28 @@ function fmt(v: number | null | undefined, digits = 2): string { return v.toFixed(digits); } +function getRegimeState(score: number): { state: string; stance: string; color: string } { + if (score <= 20) return { state: 'Crisis / Risk-Off', stance: 'CASH', color: '#c0392b' }; + if (score <= 35) return { state: 'Stressed / Defensive', stance: 'DEFENSIVE', color: '#e67e22' }; + if (score <= 50) return { state: 'Fragile / Hedged', stance: 'HEDGED', color: '#f1c40f' }; + if (score <= 65) return { state: 'Stable / Normal', stance: 'NORMAL', color: '#2ecc71' }; + return { state: 'Strong / Risk-On', stance: 'AGGRESSIVE', color: '#27ae60' }; +} + +function getDivergenceWarnings(d: FearGreedData): string[] { + const warnings: string[] = []; + const mom = d.momentum?.score ?? 50; + const sent = d.sentiment?.score ?? 50; + const cnn = d.cnnFearGreed; + const comp = d.compositeScore; + const trend = d.trend?.score ?? 50; + if (mom < 10) warnings.push('Momentum at extreme low β€” broad equity selling pressure'); + if (sent < 15) warnings.push('Sentiment in extreme fear zone'); + if (cnn > 0 && Math.abs(comp - cnn) > 20) warnings.push(`CNN F&G ${Math.round(cnn)} diverges ${Math.abs(Math.round(comp - cnn))}pts from composite β€” sentiment/structural disconnect`); + if (trend < 20) warnings.push('Trend in breakdown β€” price structure deteriorating'); + return warnings; +} + function renderGauge(score: number, label: string, delta: number | null, color: string): string { const cx = 100, cy = 100, R = 88, r = 60; @@ -205,6 +227,8 @@ export class FearGreedPanel extends Panel { const prev = d.previousScore; const delta = prev > 0 ? score - prev : null; const color = scoreColor(score); + const regime = getRegimeState(score); + const warnings = getDivergenceWarnings(d); const catRows = CAT_NAMES.map(name => { const c = d[name] as CategoryData | undefined; @@ -246,11 +270,22 @@ export class FearGreedPanel extends Panel { hdrMetric('Fed Rate', d.fedRate || 'N/A'), ].join(''); + const warningsHtml = warnings.length > 0 + ? `
+ ${warnings.map(w => `
⚠ ${escapeHtml(w)}
`).join('')} +
` + : ''; + const html = `
+
${escapeHtml(regime.state)}
${renderGauge(score, label, delta, color)} +
+ ${escapeHtml(regime.stance)} +
+ ${warningsHtml}
${hdr}
diff --git a/src/components/MacroTilesPanel.ts b/src/components/MacroTilesPanel.ts new file mode 100644 index 000000000..9750eae36 --- /dev/null +++ b/src/components/MacroTilesPanel.ts @@ -0,0 +1,133 @@ +import type { EconomicServiceClient } from '@/generated/client/worldmonitor/economic/v1/service_client'; +import { Panel } from './Panel'; +import { escapeHtml } from '@/utils/sanitize'; + +let _client: EconomicServiceClient | null = null; +async function getEconomicClient(): Promise { + if (!_client) { + const { EconomicServiceClient } = await import('@/generated/client/worldmonitor/economic/v1/service_client'); + const { getRpcBaseUrl } = await import('@/services/rpc-client'); + _client = new EconomicServiceClient(getRpcBaseUrl(), { fetch: (...args: Parameters) => globalThis.fetch(...args) }); + } + return _client; +} + +interface MacroTile { + id: string; + label: string; + unit: string; + value: number | null; + prior: number | null; + date: string; + lowerIsBetter: boolean; + neutral?: boolean; + format: (v: number) => string; + deltaFormat?: (v: number) => string; +} + +function pctFmt(v: number): string { + return `${v.toFixed(1)}%`; +} + +function gdpFmt(v: number): string { + return `$${v.toLocaleString(undefined, { maximumFractionDigits: 0 })}B`; +} + +function cpiYoY(obs: { date: string; value: number }[]): { value: number | null; prior: number | null; date: string } { + if (obs.length < 13) return { value: null, prior: null, date: '' }; + const latest = obs[obs.length - 1]; + const yearAgo = obs[obs.length - 13]; + const priorMonth = obs[obs.length - 2]; + const priorYearAgo = obs[obs.length - 14] ?? obs[obs.length - 13]; + if (!latest || !yearAgo) return { value: null, prior: null, date: '' }; + const yoy = yearAgo.value > 0 ? ((latest.value - yearAgo.value) / yearAgo.value) * 100 : null; + const priorYoy = (priorYearAgo && priorMonth && priorYearAgo.value > 0) + ? ((priorMonth.value - priorYearAgo.value) / priorYearAgo.value) * 100 + : null; + return { value: yoy, prior: priorYoy, date: latest.date }; +} + +function lastTwo(obs: { date: string; value: number }[]): { value: number | null; prior: number | null; date: string } { + const last = obs[obs.length - 1]; + if (!obs.length || !last) return { value: null, prior: null, date: '' }; + const prev = obs[obs.length - 2]; + return { + value: last.value, + prior: prev?.value ?? null, + date: last.date, + }; +} + +function deltaColor(delta: number, lowerIsBetter: boolean, neutral: boolean): string { + if (neutral) return 'var(--text-dim)'; + if (delta === 0) return 'var(--text-dim)'; + const improved = lowerIsBetter ? delta < 0 : delta > 0; + return improved ? '#27ae60' : '#e74c3c'; +} + +function tileHtml(tile: MacroTile): string { + const val = tile.value !== null ? escapeHtml(tile.format(tile.value)) : 'N/A'; + const delta = tile.value !== null && tile.prior !== null ? tile.value - tile.prior : null; + const fmt = tile.deltaFormat ?? tile.format; + const deltaStr = delta !== null + ? `${delta >= 0 ? '+' : ''}${fmt(delta)} vs prior` + : ''; + const deltaColor_ = delta !== null ? deltaColor(delta, tile.lowerIsBetter, tile.neutral ?? false) : 'var(--text-dim)'; + + return `
+
${escapeHtml(tile.label)}
+
${val}
+ ${deltaStr ? `
${escapeHtml(deltaStr)}
` : ''} +
${escapeHtml(tile.date)}
+
`; +} + +export class MacroTilesPanel extends Panel { + private _hasData = false; + + constructor() { + super({ id: 'macro-tiles', title: 'Macro Indicators', showCount: false }); + } + + public async fetchData(): Promise { + this.showLoading(); + try { + const client = await getEconomicClient(); + const resp = await client.getFredSeriesBatch({ + seriesIds: ['CPIAUCSL', 'UNRATE', 'GDP', 'FEDFUNDS'], + limit: 14, + }); + + const cpiObs = resp.results['CPIAUCSL']?.observations ?? []; + const unrateObs = resp.results['UNRATE']?.observations ?? []; + const gdpObs = resp.results['GDP']?.observations ?? []; + const fedObs = resp.results['FEDFUNDS']?.observations ?? []; + + const cpi = cpiYoY(cpiObs); + const unrate = lastTwo(unrateObs); + const gdp = lastTwo(gdpObs); + const fed = lastTwo(fedObs); + + const tiles: MacroTile[] = [ + { id: 'cpi', label: 'CPI (YoY)', unit: '%', ...cpi, lowerIsBetter: true, format: pctFmt, deltaFormat: (v) => v.toFixed(2) }, + { id: 'unrate', label: 'Unemployment', unit: '%', ...unrate, lowerIsBetter: true, format: pctFmt }, + { id: 'gdp', label: 'GDP (Billions)', unit: '$B', ...gdp, lowerIsBetter: false, format: gdpFmt, deltaFormat: (v) => `${v.toLocaleString(undefined, { maximumFractionDigits: 0 })}B` }, + { id: 'fed', label: 'Fed Funds Rate', unit: '%', ...fed, lowerIsBetter: false, neutral: true, format: pctFmt }, + ]; + + const hasAny = tiles.some(t => t.value !== null); + if (!hasAny) { + if (!this._hasData) this.showError('Macro data unavailable', () => void this.fetchData()); + return false; + } + + this._hasData = true; + const html = `
${tiles.map(tileHtml).join('')}
`; + this.setContent(html); + return true; + } catch (e) { + if (!this._hasData) this.showError(e instanceof Error ? e.message : 'Failed to load', () => void this.fetchData()); + return false; + } + } +} diff --git a/src/components/YieldCurvePanel.ts b/src/components/YieldCurvePanel.ts new file mode 100644 index 000000000..bca59a7b4 --- /dev/null +++ b/src/components/YieldCurvePanel.ts @@ -0,0 +1,198 @@ +import type { EconomicServiceClient } from '@/generated/client/worldmonitor/economic/v1/service_client'; +import { Panel } from './Panel'; +import { escapeHtml } from '@/utils/sanitize'; + +let _client: EconomicServiceClient | null = null; +async function getEconomicClient(): Promise { + if (!_client) { + const { EconomicServiceClient } = await import('@/generated/client/worldmonitor/economic/v1/service_client'); + const { getRpcBaseUrl } = await import('@/services/rpc-client'); + _client = new EconomicServiceClient(getRpcBaseUrl(), { fetch: (...args: Parameters) => globalThis.fetch(...args) }); + } + return _client; +} + +const SERIES_IDS = ['DGS1MO', 'DGS3MO', 'DGS6MO', 'DGS1', 'DGS2', 'DGS5', 'DGS10', 'DGS30'] as const; +const TENOR_LABELS = ['1M', '3M', '6M', '1Y', '2Y', '5Y', '10Y', '30Y']; + +const SVG_W = 480; +const SVG_H = 180; +const MARGIN_L = 40; +const MARGIN_R = 20; +const MARGIN_T = 16; +const MARGIN_B = 24; + +const CHART_W = SVG_W - MARGIN_L - MARGIN_R; +const CHART_H = SVG_H - MARGIN_T - MARGIN_B; + +interface YieldPoint { + tenor: string; + value: number | null; +} + +function xPos(index: number, count: number): number { + if (count <= 1) return MARGIN_L + CHART_W / 2; + return MARGIN_L + (index / (count - 1)) * CHART_W; +} + +function yPos(value: number, yMin: number, yMax: number): number { + const range = yMax - yMin || 1; + const scale = (value - yMin) / range; + return MARGIN_T + CHART_H - scale * CHART_H; +} + +function buildPolylinePoints(points: YieldPoint[], yMin: number, yMax: number): string { + return points + .map((p, i) => { + if (p.value === null) return null; + const x = xPos(i, points.length); + const y = yPos(p.value, yMin, yMax); + return `${x.toFixed(2)},${y.toFixed(2)}`; + }) + .filter(Boolean) + .join(' '); +} + +function buildYAxisLabels(yMin: number, yMax: number): string { + const step = (yMax - yMin) / 3; + const labels: string[] = []; + for (let i = 0; i <= 3; i++) { + const val = yMin + step * i; + const y = yPos(val, yMin, yMax); + labels.push( + `${val.toFixed(1)}%` + ); + labels.push( + `` + ); + } + return labels.join(''); +} + +function buildXAxisLabels(count: number): string { + return TENOR_LABELS.slice(0, count).map((label, i) => { + const x = xPos(i, count); + const y = SVG_H - MARGIN_B + 12; + return `${escapeHtml(label)}`; + }).join(''); +} + +function buildCircles(points: YieldPoint[], yMin: number, yMax: number): string { + return points.map((p, i) => { + if (p.value === null) return ''; + const x = xPos(i, points.length); + const y = yPos(p.value, yMin, yMax); + return ``; + }).join(''); +} + +function renderChart(current: YieldPoint[], prior: YieldPoint[]): string { + const validValues = current.map(p => p.value).filter((v): v is number => v !== null); + if (validValues.length === 0) return '
No yield data available.
'; + + const yMin = Math.max(0, Math.min(...validValues) - 0.25); + const yMax = Math.max(...validValues) + 0.5; + + const curPoints = buildPolylinePoints(current, yMin, yMax); + const priorPoints = buildPolylinePoints(prior, yMin, yMax); + + const priorLine = priorPoints.length > 0 + ? `` + : ''; + + return ` + + ${buildYAxisLabels(yMin, yMax)} + ${buildXAxisLabels(current.length)} + ${priorLine} + + ${buildCircles(current, yMin, yMax)} + `; +} + +function renderTable(points: YieldPoint[]): string { + const headers = points.map(p => `${escapeHtml(p.tenor)}`).join(''); + const cells = points.map(p => { + const val = p.value !== null ? `${p.value.toFixed(2)}%` : 'N/A'; + return `${escapeHtml(val)}`; + }).join(''); + return ` +
+ + ${headers} + ${cells} +
+
`; +} + +export class YieldCurvePanel extends Panel { + private _hasData = false; + + constructor() { + super({ id: 'yield-curve', title: 'US Treasury Yield Curve', showCount: false }); + } + + public async fetchData(): Promise { + this.showLoading(); + try { + const client = await getEconomicClient(); + const resp = await client.getFredSeriesBatch({ seriesIds: [...SERIES_IDS], limit: 2 }); + + const results = resp.results ?? {}; + const current: YieldPoint[] = SERIES_IDS.map((id, i) => { + const obs = results[id]?.observations ?? []; + return { tenor: TENOR_LABELS[i] ?? id, value: obs.length > 0 ? (obs[obs.length - 1]?.value ?? null) : null }; + }); + const prior: YieldPoint[] = SERIES_IDS.map((id, i) => { + const obs = results[id]?.observations ?? []; + return { tenor: TENOR_LABELS[i] ?? id, value: obs.length > 1 ? (obs[obs.length - 2]?.value ?? null) : null }; + }); + + const validCount = current.filter(p => p.value !== null).length; + if (validCount === 0) { + if (!this._hasData) this.showError('No yield data available', () => void this.fetchData()); + return false; + } + + this.render(current, prior); + return true; + } catch (e) { + if (!this._hasData) this.showError(e instanceof Error ? e.message : 'Failed to load yield curve', () => void this.fetchData()); + return false; + } + } + + private render(current: YieldPoint[], prior: YieldPoint[]): void { + this._hasData = true; + + const y2 = current.find(p => p.tenor === '2Y')?.value ?? null; + const y10 = current.find(p => p.tenor === '10Y')?.value ?? null; + const isInverted = y2 !== null && y10 !== null && y2 > y10; + const spreadBps = y2 !== null && y10 !== null ? ((y10 - y2) * 100).toFixed(0) : null; + const spreadSign = spreadBps !== null ? (Number(spreadBps) >= 0 ? '+' : '') : ''; + + const statusBadge = isInverted + ? `INVERTED` + : `NORMAL`; + + const spreadHtml = spreadBps !== null + ? `2Y-10Y Spread: ${escapeHtml(spreadSign + spreadBps)}bps` + : ''; + + const html = ` +
+
+ ${statusBadge}${spreadHtml} +
+
${renderChart(current, prior)}
+ ${renderTable(current)} +
+ Current + Prior + Source: FRED +
+
`; + + this.setContent(html); + } +} diff --git a/src/components/index.ts b/src/components/index.ts index 03c85e7ec..21a6b6618 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -71,4 +71,10 @@ export * from './DisasterCorrelationPanel'; export * from './ConsumerPricesPanel'; export { NationalDebtPanel } from './NationalDebtPanel'; export * from './FearGreedPanel'; +export * from './MacroTilesPanel'; +export * from './FSIPanel'; +export * from './YieldCurvePanel'; +export * from './EarningsCalendarPanel'; +export * from './EconomicCalendarPanel'; +export * from './CotPositioningPanel'; export { HormuzPanel } from './HormuzPanel'; diff --git a/src/config/panels.ts b/src/config/panels.ts index bd0e062d7..25a7072d0 100644 --- a/src/config/panels.ts +++ b/src/config/panels.ts @@ -56,6 +56,12 @@ const FULL_PANELS: Record = { 'satellite-fires': { name: 'Fires', enabled: true, priority: 2 }, 'macro-signals': { name: 'Market Regime', enabled: true, priority: 2 }, 'fear-greed': { name: 'Fear & Greed', enabled: true, priority: 2 }, + 'macro-tiles': { name: 'Macro Indicators', enabled: false, priority: 2 }, + 'fsi': { name: 'Financial Stress', enabled: false, priority: 2 }, + 'yield-curve': { name: 'Yield Curve', enabled: false, priority: 2 }, + 'earnings-calendar': { name: 'Earnings Calendar', enabled: false, priority: 2 }, + 'economic-calendar': { name: 'Economic Calendar', enabled: false, priority: 2 }, + 'cot-positioning': { name: 'COT Positioning', enabled: false, priority: 2 }, 'hormuz-tracker': { name: 'Hormuz Trade Tracker', enabled: true, priority: 2 }, 'gulf-economies': { name: 'Gulf Economies', enabled: false, priority: 2 }, 'consumer-prices': { name: 'Consumer Prices', enabled: false, priority: 2 }, @@ -407,6 +413,13 @@ const FINANCE_PANELS: Record = { ipo: { name: 'IPOs, Earnings & M&A', enabled: true, priority: 1 }, heatmap: { name: 'Sector Heatmap', enabled: true, priority: 1 }, 'macro-signals': { name: 'Market Regime', enabled: true, priority: 1 }, + 'macro-tiles': { name: 'Macro Indicators', enabled: true, priority: 1 }, + 'fear-greed': { name: 'Fear & Greed', enabled: true, priority: 1 }, + 'fsi': { name: 'Financial Stress', enabled: true, priority: 1 }, + 'yield-curve': { name: 'Yield Curve', enabled: true, priority: 1 }, + 'earnings-calendar': { name: 'Earnings Calendar', enabled: true, priority: 1 }, + 'economic-calendar': { name: 'Economic Calendar', enabled: true, priority: 1 }, + 'cot-positioning': { name: 'COT Positioning', enabled: true, priority: 2 }, derivatives: { name: 'Derivatives & Options', enabled: true, priority: 2 }, fintech: { name: 'Fintech & Trading Tech', enabled: true, priority: 2 }, regulation: { name: 'Financial Regulation', enabled: true, priority: 2 }, diff --git a/src/generated/client/worldmonitor/economic/v1/service_client.ts b/src/generated/client/worldmonitor/economic/v1/service_client.ts index 5580ccf37..17ea84150 100644 --- a/src/generated/client/worldmonitor/economic/v1/service_client.ts +++ b/src/generated/client/worldmonitor/economic/v1/service_client.ts @@ -363,6 +363,30 @@ export interface BlsObservation { value: string; } +export interface GetEconomicCalendarRequest { + fromDate: string; + toDate: string; +} + +export interface GetEconomicCalendarResponse { + events: EconomicEvent[]; + fromDate: string; + toDate: string; + total: number; + unavailable: boolean; +} + +export interface EconomicEvent { + event: string; + country: string; + date: string; + impact: string; + actual: string; + estimate: string; + previous: string; + unit: string; +} + export interface FieldViolation { field: string; description: string; @@ -751,6 +775,32 @@ export class EconomicServiceClient { return await resp.json() as GetBlsSeriesResponse; } + async getEconomicCalendar(req: GetEconomicCalendarRequest, options?: EconomicServiceCallOptions): Promise { + let path = "/api/economic/v1/get-economic-calendar"; + const params = new URLSearchParams(); + if (req.fromDate != null && req.fromDate !== "") params.set("fromDate", String(req.fromDate)); + if (req.toDate != null && req.toDate !== "") params.set("toDate", String(req.toDate)); + const url = this.baseURL + path + (params.toString() ? "?" + params.toString() : ""); + + 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 GetEconomicCalendarResponse; + } + private async handleError(resp: Response): Promise { const body = await resp.text(); if (resp.status === 400) { diff --git a/src/generated/client/worldmonitor/market/v1/service_client.ts b/src/generated/client/worldmonitor/market/v1/service_client.ts index 211b412fa..03862dee9 100644 --- a/src/generated/client/worldmonitor/market/v1/service_client.ts +++ b/src/generated/client/worldmonitor/market/v1/service_client.ts @@ -357,6 +357,10 @@ export interface GetFearGreedIndexResponse { aaiiBear: number; fedRate: string; unavailable: boolean; + fsiValue: number; + fsiLabel: string; + hygPrice: number; + tltPrice: number; } export interface FearGreedCategory { @@ -367,6 +371,54 @@ export interface FearGreedCategory { inputsJson: string; } +export interface ListEarningsCalendarRequest { + fromDate: string; + toDate: string; +} + +export interface ListEarningsCalendarResponse { + earnings: EarningsEntry[]; + fromDate: string; + toDate: string; + total: number; + unavailable: boolean; +} + +export interface EarningsEntry { + symbol: string; + company: string; + date: string; + hour: string; + epsEstimate: number; + revenueEstimate: number; + epsActual: number; + revenueActual: number; + hasActuals: boolean; + surpriseDirection: string; +} + +export interface GetCotPositioningRequest { +} + +export interface GetCotPositioningResponse { + instruments: CotInstrument[]; + reportDate: string; + unavailable: boolean; +} + +export interface CotInstrument { + name: string; + code: string; + reportDate: string; + assetManagerLong: string; + assetManagerShort: string; + leveragedFundsLong: string; + leveragedFundsShort: string; + dealerLong: string; + dealerShort: string; + netPct: number; +} + export interface FieldViolation { field: string; description: string; @@ -833,6 +885,55 @@ export class MarketServiceClient { return await resp.json() as GetFearGreedIndexResponse; } + async listEarningsCalendar(req: ListEarningsCalendarRequest, options?: MarketServiceCallOptions): Promise { + let path = "/api/market/v1/list-earnings-calendar"; + const params = new URLSearchParams(); + if (req.fromDate != null && req.fromDate !== "") params.set("fromDate", String(req.fromDate)); + if (req.toDate != null && req.toDate !== "") params.set("toDate", String(req.toDate)); + const url = this.baseURL + path + (params.toString() ? "?" + params.toString() : ""); + + 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 ListEarningsCalendarResponse; + } + + async getCotPositioning(req: GetCotPositioningRequest, options?: MarketServiceCallOptions): Promise { + let path = "/api/market/v1/get-cot-positioning"; + 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 GetCotPositioningResponse; + } + 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 c65bab2ed..3963002b9 100644 --- a/src/generated/server/worldmonitor/economic/v1/service_server.ts +++ b/src/generated/server/worldmonitor/economic/v1/service_server.ts @@ -363,6 +363,30 @@ export interface BlsObservation { value: string; } +export interface GetEconomicCalendarRequest { + fromDate: string; + toDate: string; +} + +export interface GetEconomicCalendarResponse { + events: EconomicEvent[]; + fromDate: string; + toDate: string; + total: number; + unavailable: boolean; +} + +export interface EconomicEvent { + event: string; + country: string; + date: string; + impact: string; + actual: string; + estimate: string; + previous: string; + unit: string; +} + export interface FieldViolation { field: string; description: string; @@ -422,6 +446,7 @@ export interface EconomicServiceHandler { getNationalDebt(ctx: ServerContext, req: GetNationalDebtRequest): Promise; listFuelPrices(ctx: ServerContext, req: ListFuelPricesRequest): Promise; getBlsSeries(ctx: ServerContext, req: GetBlsSeriesRequest): Promise; + getEconomicCalendar(ctx: ServerContext, req: GetEconomicCalendarRequest): Promise; } export function createEconomicServiceRoutes( @@ -1010,6 +1035,54 @@ export function createEconomicServiceRoutes( } }, }, + { + method: "GET", + path: "/api/economic/v1/get-economic-calendar", + handler: async (req: Request): Promise => { + try { + const pathParams: Record = {}; + const url = new URL(req.url, "http://localhost"); + const params = url.searchParams; + const body: GetEconomicCalendarRequest = { + fromDate: params.get("fromDate") ?? "", + toDate: params.get("toDate") ?? "", + }; + if (options?.validateRequest) { + const bodyViolations = options.validateRequest("getEconomicCalendar", body); + if (bodyViolations) { + throw new ValidationError(bodyViolations); + } + } + + const ctx: ServerContext = { + request: req, + pathParams, + headers: Object.fromEntries(req.headers.entries()), + }; + + const result = await handler.getEconomicCalendar(ctx, body); + return new Response(JSON.stringify(result as GetEconomicCalendarResponse), { + 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/generated/server/worldmonitor/market/v1/service_server.ts b/src/generated/server/worldmonitor/market/v1/service_server.ts index 2fda00ff9..1b2def3e6 100644 --- a/src/generated/server/worldmonitor/market/v1/service_server.ts +++ b/src/generated/server/worldmonitor/market/v1/service_server.ts @@ -357,6 +357,10 @@ export interface GetFearGreedIndexResponse { aaiiBear: number; fedRate: string; unavailable: boolean; + fsiValue: number; + fsiLabel: string; + hygPrice: number; + tltPrice: number; } export interface FearGreedCategory { @@ -367,6 +371,54 @@ export interface FearGreedCategory { inputsJson: string; } +export interface ListEarningsCalendarRequest { + fromDate: string; + toDate: string; +} + +export interface ListEarningsCalendarResponse { + earnings: EarningsEntry[]; + fromDate: string; + toDate: string; + total: number; + unavailable: boolean; +} + +export interface EarningsEntry { + symbol: string; + company: string; + date: string; + hour: string; + epsEstimate: number; + revenueEstimate: number; + epsActual: number; + revenueActual: number; + hasActuals: boolean; + surpriseDirection: string; +} + +export interface GetCotPositioningRequest { +} + +export interface GetCotPositioningResponse { + instruments: CotInstrument[]; + reportDate: string; + unavailable: boolean; +} + +export interface CotInstrument { + name: string; + code: string; + reportDate: string; + assetManagerLong: string; + assetManagerShort: string; + leveragedFundsLong: string; + leveragedFundsShort: string; + dealerLong: string; + dealerShort: string; + netPct: number; +} + export interface FieldViolation { field: string; description: string; @@ -429,6 +481,8 @@ export interface MarketServiceHandler { listAiTokens(ctx: ServerContext, req: ListAiTokensRequest): Promise; listOtherTokens(ctx: ServerContext, req: ListOtherTokensRequest): Promise; getFearGreedIndex(ctx: ServerContext, req: GetFearGreedIndexRequest): Promise; + listEarningsCalendar(ctx: ServerContext, req: ListEarningsCalendarRequest): Promise; + getCotPositioning(ctx: ServerContext, req: GetCotPositioningRequest): Promise; } export function createMarketServiceRoutes( @@ -1172,6 +1226,91 @@ export function createMarketServiceRoutes( } }, }, + { + method: "GET", + path: "/api/market/v1/list-earnings-calendar", + handler: async (req: Request): Promise => { + try { + const pathParams: Record = {}; + const url = new URL(req.url, "http://localhost"); + const params = url.searchParams; + const body: ListEarningsCalendarRequest = { + fromDate: params.get("fromDate") ?? "", + toDate: params.get("toDate") ?? "", + }; + if (options?.validateRequest) { + const bodyViolations = options.validateRequest("listEarningsCalendar", body); + if (bodyViolations) { + throw new ValidationError(bodyViolations); + } + } + + const ctx: ServerContext = { + request: req, + pathParams, + headers: Object.fromEntries(req.headers.entries()), + }; + + const result = await handler.listEarningsCalendar(ctx, body); + return new Response(JSON.stringify(result as ListEarningsCalendarResponse), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (err: unknown) { + if (err instanceof ValidationError) { + return new Response(JSON.stringify({ violations: err.violations }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + if (options?.onError) { + return options.onError(err, req); + } + const message = err instanceof Error ? err.message : String(err); + return new Response(JSON.stringify({ message }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } + }, + }, + { + method: "GET", + path: "/api/market/v1/get-cot-positioning", + handler: async (req: Request): Promise => { + try { + const pathParams: Record = {}; + const body = {} as GetCotPositioningRequest; + + const ctx: ServerContext = { + request: req, + pathParams, + headers: Object.fromEntries(req.headers.entries()), + }; + + const result = await handler.getCotPositioning(ctx, body); + return new Response(JSON.stringify(result as GetCotPositioningResponse), { + 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/services/daily-market-brief.ts b/src/services/daily-market-brief.ts index a8ff776b3..3f3d03a58 100644 --- a/src/services/daily-market-brief.ts +++ b/src/services/daily-market-brief.ts @@ -30,12 +30,45 @@ export interface DailyMarketBrief { headlineCount: number; } +export interface RegimeMacroContext { + compositeScore: number; + compositeLabel: string; + fsiValue: number; + fsiLabel: string; + vix: number; + hySpread: number; + cnnFearGreed: number; + cnnLabel: string; + momentum?: { score: number }; + sentiment?: { score: number }; +} + +export interface YieldCurveContext { + inverted: boolean; + spread2s10s: number; + rate2y: number; + rate10y: number; + rate30y: number; +} + +export interface SectorBriefContext { + topName: string; + topChange: number; + worstName: string; + worstChange: number; + countPositive: number; + total: number; +} + export interface BuildDailyMarketBriefOptions { markets: MarketData[]; newsByCategory: Record; timezone?: string; now?: Date; targets?: MarketWatchlistEntry[]; + regimeContext?: RegimeMacroContext; + yieldCurveContext?: YieldCurveContext; + sectorContext?: SectorBriefContext; summarize?: ( headlines: string[], onProgress?: undefined, @@ -273,6 +306,47 @@ function buildSummaryInputs(items: DailyMarketBriefItem[], headlines: NewsItem[] return { headlines: headlineLines, marketContext }; } +function buildExtendedMarketContext( + baseContext: string, + regime?: RegimeMacroContext, + yieldCurve?: YieldCurveContext, + sector?: SectorBriefContext, +): string { + const parts: string[] = [`Markets: ${baseContext}`]; + + if (regime && regime.compositeScore > 0) { + const lines = [ + `Fear & Greed: ${regime.compositeScore.toFixed(0)} (${regime.compositeLabel})`, + ]; + if (regime.fsiValue > 0) lines.push(`FSI: ${regime.fsiValue.toFixed(2)} (${regime.fsiLabel})`); + if (regime.vix > 0) lines.push(`VIX: ${regime.vix.toFixed(1)}`); + if (regime.hySpread > 0) lines.push(`HY Spread: ${regime.hySpread.toFixed(0)}bps`); + if (regime.cnnFearGreed > 0) lines.push(`CNN F&G: ${regime.cnnFearGreed.toFixed(0)} (${regime.cnnLabel})`); + if (regime.momentum) lines.push(`Momentum: ${regime.momentum.score.toFixed(0)}/100`); + if (regime.sentiment) lines.push(`Sentiment: ${regime.sentiment.score.toFixed(0)}/100`); + parts.push(`Market Stress Indicators:\n${lines.join('\n')}`); + } + + if (yieldCurve && yieldCurve.rate10y > 0) { + const spreadStr = (yieldCurve.spread2s10s >= 0 ? '+' : '') + yieldCurve.spread2s10s.toFixed(0); + parts.push([ + `Yield Curve: ${yieldCurve.inverted ? 'INVERTED' : 'NORMAL'} (2s/10s ${spreadStr}bps)`, + `2Y: ${yieldCurve.rate2y.toFixed(2)}% 10Y: ${yieldCurve.rate10y.toFixed(2)}% 30Y: ${yieldCurve.rate30y.toFixed(2)}%`, + ].join('\n')); + } + + if (sector && sector.total > 0) { + const topSign = sector.topChange >= 0 ? '+' : ''; + const worstSign = sector.worstChange >= 0 ? '+' : ''; + parts.push([ + `Sectors: ${sector.countPositive}/${sector.total} positive`, + `Top: ${sector.topName} ${topSign}${sector.topChange.toFixed(1)}% Worst: ${sector.worstName} ${worstSign}${sector.worstChange.toFixed(1)}%`, + ].join('\n')); + } + + return parts.join('\n\n'); +} + export function shouldRefreshDailyBrief( brief: DailyMarketBrief | null | undefined, timezone = 'UTC', @@ -337,6 +411,7 @@ export async function buildDailyMarketBrief(options: BuildDailyMarketBriefOption } const { headlines: summaryHeadlines, marketContext } = buildSummaryInputs(items, relevantHeadlines); + const extendedContext = buildExtendedMarketContext(marketContext, options.regimeContext, options.yieldCurveContext, options.sectorContext); let summary = buildRuleSummary(items, relevantHeadlines.length); let provider = 'rules'; let model = ''; @@ -348,7 +423,7 @@ export async function buildDailyMarketBrief(options: BuildDailyMarketBriefOption const generated = await summaryProvider( summaryHeadlines, undefined, - `Market context: ${marketContext}`, + extendedContext, 'en', ); if (generated?.summary) { diff --git a/todos/030-pending-p3-macro-tiles-fragile-delta-formatter.md b/todos/030-pending-p3-macro-tiles-fragile-delta-formatter.md new file mode 100644 index 000000000..c1e53919f --- /dev/null +++ b/todos/030-pending-p3-macro-tiles-fragile-delta-formatter.md @@ -0,0 +1,51 @@ +--- +status: complete +priority: p3 +issue_id: "030" +tags: [code-review, quality, finance-panels] +dependencies: [] +--- + +# MacroTilesPanel: Fragile Delta Formatter Switching on tile.id + +## Problem Statement + +`MacroTilesPanel.ts:59-61` uses a single expression that switches formatter behavior by `tile.id` string comparison and strips characters from formatted output. Breaks silently if a tile id or format function changes. + +## Findings + +```typescript +const deltaStr = delta !== null + ? `${delta >= 0 ? '+' : ''}${tile.id === 'cpi' ? delta.toFixed(2) : tile.format(delta).replace('$', '').replace('B', '')}${tile.id === 'cpi' ? '' : tile.id === 'gdp' ? 'B' : ''} vs prior` + : ''; +``` + +- Switches on `tile.id` string comparison +- Strips `$` and `B` from formatted output then re-appends `B` for GDP +- Adding a new tile with a different format will silently produce wrong output +- The `MacroTile` interface already has a `format` field β€” a `deltaFormat?: (v: number) => string` field would be the correct extension point + +## Proposed Solutions + +### Option A: Add deltaFormat field to MacroTile interface + +Add `deltaFormat?: (v: number) => string` to `MacroTile`, define it per-tile in the tiles array. Clean, self-contained, extensible. + +- **Effort**: Small +- **Risk**: Low + +### Option B: Keep as-is with a comment + +Add an explanatory comment documenting the intent. Low effort but keeps fragility. + +- **Effort**: Minimal +- **Risk**: Low + +## Acceptance Criteria + +- [ ] Adding a new tile type to MacroTilesPanel does not require updating a switch expression +- [ ] Delta formatting logic is co-located with tile definition + +## Work Log + +- 2026-03-26: Identified by code review of PR #2258 diff --git a/todos/031-pending-p3-fsi-panel-no-hydrated-data-fast-path.md b/todos/031-pending-p3-fsi-panel-no-hydrated-data-fast-path.md new file mode 100644 index 000000000..1b6853448 --- /dev/null +++ b/todos/031-pending-p3-fsi-panel-no-hydrated-data-fast-path.md @@ -0,0 +1,47 @@ +--- +status: complete +priority: p3 +issue_id: "031" +tags: [code-review, performance, finance-panels] +dependencies: [] +--- + +# FSIPanel: Missing getHydratedData Fast Path + +## Problem Statement + +`FSIPanel.fetchData()` always fires a live `getFearGreedIndex` RPC call, even though `getHydratedData('fearGreedIndex')` already contains the FSI fields from bootstrap. This causes a redundant Redis round-trip on every panel open. + +## Findings + +The `fearGreedIndex` bootstrap key contains `hdr.fsi.value`, `hdr.fsi.label`, `hdr.vix.value`, `hdr.hySpread.value` β€” all the fields FSIPanel needs. The `_collectRegimeContext()` method in `data-loader.ts` already demonstrates the correct pattern: check `getHydratedData('fearGreedIndex')` first, fall back to RPC if absent. + +FSIPanel skips this optimization entirely. On sessions with a bootstrap payload, every FSI panel open costs an extra RPC call to Redis. + +## Proposed Solutions + +### Option A: Mirror _collectRegimeContext pattern + +Check `getHydratedData('fearGreedIndex')` at the top of `fetchData()`. Extract FSI fields from `hdr.fsi`. Fall back to RPC only if hydrated data is absent or `unavailable`. + +```typescript +const hydrated = getHydratedData('fearGreedIndex') as Record | undefined; +if (hydrated && !hydrated.unavailable) { + // extract hdr.fsi fields and render + return true; +} +// fall back to RPC +``` + +- **Effort**: Small +- **Risk**: Low β€” hydrated data read is synchronous and always stale by definition + +## Acceptance Criteria + +- [ ] FSIPanel reads from bootstrap hydration on sessions where fearGreedIndex is loaded +- [ ] Falls back to RPC when hydrated data is absent +- [ ] No visible change in rendered output + +## Work Log + +- 2026-03-26: Identified by performance review of PR #2258 diff --git a/todos/032-pending-p3-finance-panels-rpc-client-singletons.md b/todos/032-pending-p3-finance-panels-rpc-client-singletons.md new file mode 100644 index 000000000..4ac05e505 --- /dev/null +++ b/todos/032-pending-p3-finance-panels-rpc-client-singletons.md @@ -0,0 +1,58 @@ +--- +status: complete +priority: p3 +issue_id: "032" +tags: [code-review, performance, finance-panels] +dependencies: [] +--- + +# Finance Panels: RPC Client Constructed on Every fetchData() Call + +## Problem Statement + +All 6 new finance panels (MacroTilesPanel, YieldCurvePanel, FSIPanel, EarningsCalendarPanel, EconomicCalendarPanel, CotPositioningPanel) plus `_collectRegimeContext` and `_collectYieldCurveContext` in data-loader.ts construct a new `EconomicServiceClient` or `MarketServiceClient` on every `fetchData()` call via dynamic imports. + +## Findings + +```typescript +// Re-runs on every fetchData() call: +const { EconomicServiceClient } = await import('@/generated/client/...'); +const { getRpcBaseUrl } = await import('@/services/rpc-client'); +const client = new EconomicServiceClient(getRpcBaseUrl(), { fetch: ... }); +``` + +While Vite caches module resolution, a new client object is constructed each call. The fetch lambda `(...args) => globalThis.fetch(...args)` is also recreated each time. On retry or multiple panel opens in the same session, this is unnecessary work. + +## Proposed Solutions + +### Option A: Module-level lazy singleton per panel + +```typescript +let _client: EconomicServiceClient | null = null; +function getClient(): EconomicServiceClient { + if (!_client) { + const { getRpcBaseUrl } = require('@/services/rpc-client'); // sync after module load + _client = new EconomicServiceClient(getRpcBaseUrl(), { fetch: (...args) => globalThis.fetch(...args) }); + } + return _client; +} +``` + +- **Effort**: Small per panel +- **Risk**: Low β€” clients are stateless + +### Option B: Shared RPC client factory in a separate module + +A `getRpcClients()` helper that lazily initializes and caches both service clients. + +- **Effort**: Medium +- **Risk**: Low + +## Acceptance Criteria + +- [ ] Each panel's service client is created at most once per panel instance +- [ ] fetch lambda still uses deferred `globalThis.fetch` (not bound at construction) + +## Work Log + +- 2026-03-26: Identified by performance review of PR #2258 diff --git a/todos/033-pending-p3-allowed-series-missing-bamlc-sofr.md b/todos/033-pending-p3-allowed-series-missing-bamlc-sofr.md new file mode 100644 index 000000000..814ab1cff --- /dev/null +++ b/todos/033-pending-p3-allowed-series-missing-bamlc-sofr.md @@ -0,0 +1,43 @@ +--- +status: complete +priority: p3 +issue_id: "033" +tags: [code-review, bug, finance-panels] +dependencies: [] +--- + +# ALLOWED_SERIES Missing BAMLC0A0CM and SOFR (Pre-existing) + +## Problem Statement + +`scripts/seed-economy.mjs` seeds `BAMLC0A0CM` (IG OAS) and `SOFR` into Redis, but neither series appears in `ALLOWED_SERIES` in `server/worldmonitor/economic/v1/get-fred-series-batch.ts`. Any RPC request for these series silently returns empty data. + +## Findings + +- `seed-economy.mjs` line 20: includes `'BAMLC0A0CM', 'SOFR'` in FRED_SERIES +- `get-fred-series-batch.ts` ALLOWED_SERIES: does NOT include these two series +- Result: data is written to Redis but unreachable via the public RPC +- Pre-existing bug, not introduced by PR #2258 but visible because the file was touched + +## Proposed Solutions + +### Option A: Add to ALLOWED_SERIES + +```typescript +'BAMLC0A0CM', // IG OAS spread +'SOFR', // Secured Overnight Financing Rate +``` + +One-line fix. No other changes needed. + +- **Effort**: Minimal +- **Risk**: None β€” just allowlisting existing seeded data + +## Acceptance Criteria + +- [ ] `getFredSeriesBatch({ seriesIds: ['BAMLC0A0CM', 'SOFR'] })` returns data +- [ ] No changes to seed scripts needed + +## Work Log + +- 2026-03-26: Identified by architecture review of PR #2258 (pre-existing gap)