From 7013b2f9f15413689422425c8e09a4eb13960558 Mon Sep 17 00:00:00 2001 From: Elie Habib Date: Tue, 24 Mar 2026 09:45:59 +0400 Subject: [PATCH] =?UTF-8?q?feat(market):=20Fear=20&=20Greed=20Index=202.0?= =?UTF-8?q?=20=E2=80=94=2010-category=20composite=20sentiment=20panel=20(#?= =?UTF-8?q?2181)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Fear & Greed Index 2.0 reverse engineering brief Analyzes the 10-category weighted composite (Sentiment, Volatility, Positioning, Trend, Breadth, Momentum, Liquidity, Credit, Macro, Cross-Asset) with scoring formulas, data source audit, and implementation plan for building it as a worldmonitor panel. https://claude.ai/code/session_01HR69u6oF1VCMwsC2PHFL8i * Add seed script implementation plan to F&G brief Details exact endpoints, Yahoo symbols (17 calls), Redis key schema, computed metrics, FRED series to add (BAMLC0A0CM, SOFR), CNN/AAII sources, output JSON schema, and estimated runtime (~8s per seed run). https://claude.ai/code/session_01HR69u6oF1VCMwsC2PHFL8i * Update brief: all sources are free, zero paid APIs needed - CBOE CDN CSVs for put/call ratios (totalpc.csv, equitypc.csv) - CNN dataviz API for Fear & Greed (production.dataviz.cnn.io) - Yahoo Finance for VIX9D/VIX3M/SKEW/RSP/NYA (standard symbols) - FRED for IG spread (BAMLC0A0CM) and SOFR (add to existing array) - AAII scrape for bull/bear survey (only medium-effort source) - Breadth via RSP/SPY divergence + NYSE composite (no scraping) https://claude.ai/code/session_01HR69u6oF1VCMwsC2PHFL8i * Add verified Yahoo symbols for breadth + finalized source list New discoveries: - ^MMTH = % stocks above 200 DMA (direct Yahoo symbol!) - C:ISSU = NYSE advance/decline data - CNN endpoint accepts date param for historical data - CBOE CSVs have data back to 2003 - 33 total calls per seed run, ~6s runtime All 10 categories now have confirmed free sources. https://claude.ai/code/session_01HR69u6oF1VCMwsC2PHFL8i * Rewrite F&G brief as forward-looking design doc Remove all reverse-engineering language, screenshot references, and discovery notes. Clean structure: goal, scoring model, data sources, formulas, seed script plan, implementation phases, MVP path. https://claude.ai/code/session_01HR69u6oF1VCMwsC2PHFL8i * docs: apply gold standard corrections to fear-greed-index-2.0 brief * feat(market): add Fear & Greed Index 2.0 — 10-category composite sentiment panel Composite 0-100 index from 10 weighted categories: sentiment (CNN F&G, AAII, crypto F&G), volatility (VIX, term structure), positioning (P/C ratio, SKEW), trend (SPX vs MAs), breadth (% >200d, RSP/SPY divergence), momentum (sector RSI, ROC), liquidity (M2, Fed BS, SOFR), credit (HY/IG spreads), macro (Fed rate, yield curve, unemployment), cross-asset (gold/bonds/DXY vs equities). Data layer: - seed-fear-greed.mjs: 19 Yahoo symbols (150ms gaps), CBOE P/C CSVs, CNN F&G API, AAII scrape (degraded-safe), FRED Redis reads. TTL 64800s. - seed-economy.mjs: add BAMLC0A0CM (IG spread) and SOFR to FRED_SERIES. - Bootstrap 4-file checklist: cache-keys, bootstrap.js, health.js, handler. Proto + RPC: - get_fear_greed_index.proto with FearGreedCategory message. - get-fear-greed-index.ts handler reads seeded Redis data. Frontend: - FearGreedPanel with gauge, 9-metric header grid, 10-category breakdown. - Self-loading via bootstrap hydration + RPC fallback. - Registered in panel-layout, App.ts (prime + refresh), panel config, Cmd-K commands, finance variant, i18n (en/ar/zh/es). * fix(market): add RPC_CACHE_TIER entry for get-fear-greed-index * fix(docs): escape bare angle bracket in fear-greed brief for MDX * fix(docs): fix markdown lint errors in fear-greed brief (blank lines around headings/lists) * fix(market): fix seed-fear-greed bugs from code review - fredLatest/fredNMonthsAgo: guard parseFloat with Number.isFinite to handle FRED's "." missing-data sentinel (was returning NaN which propagated through scoring as a truthy non-null value) - Remove 3 unused Yahoo symbols (^NYA, HYG, LQD) that were fetched but not referenced in any scoring category (saves ~450ms per run) - fedRateStr: display effective rate directly instead of deriving target range via (fedRate - 0.25) which was incorrect * fix(market): address P2/P3 review findings in Fear & Greed - FearGreedPanel: add mapSeedPayload() to correctly map raw seed JSON to proto-shaped FearGreedData; bootstrap hydration was always falling through to RPC because seed shape (composite.score) differs from proto shape (compositeScore) - FearGreedPanel: fix fmt() — remove === 0 guard and add explicit > 0 checks on VIX and P/C Ratio display to handle proto default zeros without masking genuine zero values (e.g. pctAbove200d) - seed-fear-greed: remove broken history write — each run overwrote the key with a single-entry array (no read-then-append), making the 90-day TTL meaningless; no consumer exists yet so defer to later - seed-fear-greed: extract hySpreadVal const to avoid double fredLatest call - seed-fear-greed: fix stale comment (19 symbols → 16 after prior cleanup) --------- Co-authored-by: Claude --- api/bootstrap.js | 2 + api/health.js | 3 +- docs/api/MarketService.openapi.json | 2 +- docs/api/MarketService.openapi.yaml | 107 +++++ docs/fear-greed-index-2.0-brief.md | 376 ++++++++++++++++ .../market/v1/get_fear_greed_index.proto | 43 ++ proto/worldmonitor/market/v1/service.proto | 6 + scripts/seed-economy.mjs | 2 +- scripts/seed-fear-greed.mjs | 411 ++++++++++++++++++ server/_shared/cache-keys.ts | 2 + server/gateway.ts | 1 + .../market/v1/get-fear-greed-index.ts | 61 +++ server/worldmonitor/market/v1/handler.ts | 2 + src/App.ts | 11 + src/app/panel-layout.ts | 2 + src/components/FearGreedPanel.ts | 234 ++++++++++ src/components/index.ts | 1 + src/config/commands.ts | 1 + src/config/panels.ts | 1 + src/config/variants/base.ts | 1 + src/config/variants/finance.ts | 1 + .../worldmonitor/market/v1/service_client.ts | 62 +++ .../worldmonitor/market/v1/service_server.ts | 77 ++++ src/locales/ar.json | 4 +- src/locales/en.json | 3 +- src/locales/es.json | 4 +- src/locales/zh.json | 4 +- 27 files changed, 1417 insertions(+), 7 deletions(-) create mode 100644 docs/fear-greed-index-2.0-brief.md create mode 100644 proto/worldmonitor/market/v1/get_fear_greed_index.proto create mode 100644 scripts/seed-fear-greed.mjs create mode 100644 server/worldmonitor/market/v1/get-fear-greed-index.ts create mode 100644 src/components/FearGreedPanel.ts diff --git a/api/bootstrap.js b/api/bootstrap.js index 2f57e7fff..a241abe35 100644 --- a/api/bootstrap.js +++ b/api/bootstrap.js @@ -67,6 +67,7 @@ const BOOTSTRAP_CACHE_KEYS = { fuelPrices: 'economic:fuel-prices:v1', nationalDebt: 'economic:national-debt:v1', marketImplications: 'intelligence:market-implications:v1', + fearGreedIndex: 'market:fear-greed:v1', }; const SLOW_KEYS = new Set([ @@ -87,6 +88,7 @@ const SLOW_KEYS = new Set([ 'fuelPrices', 'nationalDebt', 'marketImplications', + 'fearGreedIndex', ]); const FAST_KEYS = new Set([ 'earthquakes', 'outages', 'serviceStatuses', 'ddosAttacks', 'trafficAnomalies', 'macroSignals', 'chokepoints', 'chokepointTransits', diff --git a/api/health.js b/api/health.js index 865fe1fda..60a628a60 100644 --- a/api/health.js +++ b/api/health.js @@ -53,6 +53,7 @@ const BOOTSTRAP_KEYS = { aiTokens: 'market:ai-tokens:v1', otherTokens: 'market:other-tokens:v1', fredBatch: 'economic:fred:v1:FEDFUNDS:0', + fearGreedIndex: 'market:fear-greed:v1', }; const STANDALONE_KEYS = { @@ -172,7 +173,7 @@ const SEED_META = { otherTokens: { key: 'seed-meta:market:token-panels', maxStaleMin: 90 }, fredBatch: { key: 'seed-meta:economic:fred:v1:FEDFUNDS:0', maxStaleMin: 1500 }, // daily cron gscpi: { key: 'seed-meta:economic:gscpi', maxStaleMin: 2880 }, // 24h interval; 2880min = 48h = 2x interval - marketImplications: { key: 'seed-meta:intelligence:market-implications', maxStaleMin: 150 }, // 75min TTL; 150min = 2x interval + fearGreedIndex: { key: 'seed-meta:market:fear-greed', maxStaleMin: 720 }, // 6h cron; 720min = 12h = 2x interval }; // Standalone keys that are populated on-demand by RPC handlers (not seeds). diff --git a/docs/api/MarketService.openapi.json b/docs/api/MarketService.openapi.json index 8aff74196..77f0c4138 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"},"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"},"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-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"},"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 diff --git a/docs/api/MarketService.openapi.yaml b/docs/api/MarketService.openapi.yaml index 002c327df..52d903f68 100644 --- a/docs/api/MarketService.openapi.yaml +++ b/docs/api/MarketService.openapi.yaml @@ -523,6 +523,32 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /api/market/v1/get-fear-greed-index: + get: + tags: + - MarketService + summary: GetFearGreedIndex + description: GetFearGreedIndex retrieves the composite Fear & Greed sentiment index. + operationId: GetFearGreedIndex + responses: + "200": + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/GetFearGreedIndexResponse' + "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: @@ -1354,3 +1380,84 @@ components: items: $ref: '#/components/schemas/CryptoQuote' description: ListOtherTokensResponse contains other token price data. + GetFearGreedIndexRequest: + type: object + GetFearGreedIndexResponse: + type: object + properties: + compositeScore: + type: number + format: double + compositeLabel: + type: string + previousScore: + type: number + format: double + seededAt: + type: string + sentiment: + $ref: '#/components/schemas/FearGreedCategory' + volatility: + $ref: '#/components/schemas/FearGreedCategory' + positioning: + $ref: '#/components/schemas/FearGreedCategory' + trend: + $ref: '#/components/schemas/FearGreedCategory' + breadth: + $ref: '#/components/schemas/FearGreedCategory' + momentum: + $ref: '#/components/schemas/FearGreedCategory' + liquidity: + $ref: '#/components/schemas/FearGreedCategory' + credit: + $ref: '#/components/schemas/FearGreedCategory' + macro: + $ref: '#/components/schemas/FearGreedCategory' + crossAsset: + $ref: '#/components/schemas/FearGreedCategory' + vix: + type: number + format: double + hySpread: + type: number + format: double + yield10y: + type: number + format: double + putCallRatio: + type: number + format: double + pctAbove200d: + type: number + format: double + cnnFearGreed: + type: number + format: double + cnnLabel: + type: string + aaiiBull: + type: number + format: double + aaiiBear: + type: number + format: double + fedRate: + type: string + unavailable: + type: boolean + FearGreedCategory: + type: object + properties: + score: + type: number + format: double + weight: + type: number + format: double + contribution: + type: number + format: double + degraded: + type: boolean + inputsJson: + type: string diff --git a/docs/fear-greed-index-2.0-brief.md b/docs/fear-greed-index-2.0-brief.md new file mode 100644 index 000000000..adbbd0082 --- /dev/null +++ b/docs/fear-greed-index-2.0-brief.md @@ -0,0 +1,376 @@ +# Fear & Greed Index 2.0 — Design Brief + +## Goal + +Build a composite market sentiment gauge (0–100) combining **10 weighted categories** into a single score. Unlike CNN's Fear & Greed Index (~7 inputs, widely criticized for lagging and oversimplifying), this uses 10 granular categories with more inputs per category to produce a nuanced, institutional-quality reading. + +--- + +## Composite Score + +``` +Final Score = Σ (Category_Score × Category_Weight) +``` + +Each category scores **0–100** (0 = Extreme Fear, 100 = Extreme Greed). The weighted sum produces the composite index. + +### 10 Categories + +| # | Category | Weight | What It Measures | +|---|----------------|--------|------------------| +| 1 | **Sentiment** | 10% | CNN F&G, AAII Bull/Bear surveys, crypto sentiment | +| 2 | **Volatility** | 10% | VIX level, VIX term structure (contango/backwardation) | +| 3 | **Positioning** | 15% | Put/Call ratios, options skew (CBOE SKEW) | +| 4 | **Trend** | 10% | SPX vs 20d/50d/200d MAs, price momentum | +| 5 | **Breadth** | 10% | % stocks > 200 DMA, advance/decline ratio, equal-weight divergence | +| 6 | **Momentum** | 10% | Sector RSI spread, rate of change | +| 7 | **Liquidity** | 15% | M2 growth, Fed balance sheet, SOFR rate | +| 8 | **Credit** | 10% | HY spreads, IG spreads, credit ETF trends | +| 9 | **Macro** | 5% | Fed rate, yield curve, unemployment | +| 10| **Cross-Asset** | 5% | Gold/USD correlation, bonds vs equities | + +### Score Labels + +| Range | Label | +|-------|-------| +| 0–20 | Extreme Fear | +| 20–40 | Fear | +| 40–60 | Neutral | +| 60–80 | Greed | +| 80–100| Extreme Greed | + +### Header Metrics (9 key stats) + +| Metric | Source | Context | +|--------|--------|---------| +| CNN F&G | CNN dataviz API | 0–100 score + label | +| AAII Bear % | AAII survey | vs historical average | +| AAII Bull % | AAII survey | vs historical average | +| Put/Call Ratio | CBOE CDN CSV | vs 1yr average | +| VIX | Yahoo / FRED | % change | +| HY Spread | FRED | vs long-term average | +| % > 200 DMA | Yahoo `^MMTH` | vs recent peak | +| 10Y Yield | FRED | level | +| Fed Rate | FRED | current range | + +--- + +## Data Sources + +All sources are free with no paid API keys required. + +### Already Available (read from Redis) + +| Data Point | FRED Series | Used In | +|-----------|------------|---------| +| VIX | VIXCLS | Volatility | +| HY Spread (OAS) | BAMLH0A0HYM2 | Credit | +| 10Y Yield | DGS10 | Macro | +| Fed Funds Rate | FEDFUNDS | Macro | +| 10Y-2Y Spread | T10Y2Y | Macro | +| M2 Money Supply | M2SL | Liquidity | +| Fed Balance Sheet | WALCL | Liquidity | +| Unemployment | UNRATE | Macro | +| Crypto Fear & Greed | Alternative.me (macro-signals) | Sentiment | + +### New FRED Series (add to `seed-economy.mjs`) + +| Series | Name | Category | +|--------|------|----------| +| `BAMLC0A0CM` | ICE BofA US IG OAS | Credit | +| `SOFR` | Secured Overnight Financing Rate | Liquidity | + +### New External Sources + +| Source | Endpoint | Format | Auth | Reliability | +|--------|----------|--------|------|-------------| +| **CNN Fear & Greed** | `production.dataviz.cnn.io/index/fearandgreed/graphdata/{date}` | JSON | User-Agent header | MEDIUM | +| **AAII Sentiment** | `aaii.com/sentimentsurvey` (HTML scrape) | HTML | User-Agent header | LOW (blocks bots) | +| **CBOE Total P/C** | `cdn.cboe.com/.../totalpc.csv` | CSV | None | HIGH | +| **CBOE Equity P/C** | `cdn.cboe.com/.../equitypc.csv` | CSV | None | HIGH | + +### Yahoo Finance Symbols (19 total) + +Uses `query1.finance.yahoo.com/v8/finance/chart` — no API key, User-Agent header only. + +| # | Symbol | Category | Purpose | +|---|--------|----------|---------| +| 1 | `^GSPC` | Trend, Momentum | SPX — compute 20/50/200 DMA, ROC | +| 2 | `^VIX` | Volatility | Real-time VIX | +| 3 | `^VIX9D` | Volatility | 9-day VIX for term structure | +| 4 | `^VIX3M` | Volatility | 3-month VIX for term structure | +| 5 | `^SKEW` | Positioning | CBOE SKEW index | +| 6 | `^MMTH` | Breadth | % of stocks above 200 DMA | +| 7 | `^NYA` | Breadth | NYSE Composite for breadth divergence | +| 8 | `C:ISSU` | Breadth | NYSE advances/declines/unchanged | +| 9 | `GLD` | Cross-Asset | Gold proxy | +| 10 | `TLT` | Cross-Asset | Bonds proxy | +| 11 | `SPY` | Cross-Asset, Breadth | Equity benchmark | +| 12 | `RSP` | Breadth | Equal-weight S&P 500 (vs SPY divergence) | +| 13 | `DX-Y.NYB` | Cross-Asset | USD Dollar Index | +| 14 | `HYG` | Credit | HY bond ETF trend | +| 15 | `LQD` | Credit | IG bond ETF trend | +| 16 | `XLK` | Momentum | Tech sector | +| 17 | `XLF` | Momentum | Financial sector | +| 18 | `XLE` | Momentum | Energy sector | +| 19 | `XLV` | Momentum | Healthcare sector | + +**Notes:** + +- `^MMTH` = % above **200-day** MA (not `^MMTW` which is 20-day) +- `C:ISSU` = NYSE advance/decline/unchanged data. **Unvalidated via `/v8/finance/chart` endpoint** — must confirm it returns advance/decline figures before relying on it. If unavailable, Breadth drops `ad_score` and reweights: `breadth_score * 0.57 + rsp_score * 0.43` +- Fallback: Finnhub candle API for ETF symbols; breadth symbols Yahoo-only + +--- + +## Scoring Formulas + +### 1. Sentiment (10%) + +``` +inputs: CNN_FG, AAII_Bull, AAII_Bear (AAII is LOW reliability — blocks bots) + +// Normal path (AAII available): +score = (CNN_FG * 0.4) + (AAII_Bull_Percentile * 0.3) + ((100 - AAII_Bear_Percentile) * 0.3) + +// Degraded path (AAII unavailable — store aaiBull/aaiBear as null, not 0): +score = CNN_FG // 100% weight on CNN F&G; crypto F&G from Redis as secondary signal if CNN also fails +// aaiBull and aaiBear fields: null (not 0 — zero skews score toward Extreme Fear) +``` + +**Reliability notes:** CNN F&G is MEDIUM reliability. If both CNN and AAII fail, use `cryptoFearGreed` from Redis (already seeded via macro-signals) as a proxy — it is directionally correlated. Mark `unavailable: true` only if all three sentiment sources are absent. + +### 2. Volatility (10%) + +``` +inputs: VIX, VIX_Term_Structure +vix_score = clamp(100 - ((VIX - 12) / 28) * 100, 0, 100) // VIX 12=100, VIX 40=0 +term_score = contango ? 70 : backwardation ? 30 : 50 +score = vix_score * 0.7 + term_score * 0.3 +``` + +### 3. Positioning (15%) + +``` +inputs: Put_Call_Ratio, Options_Skew +pc_score = clamp(100 - ((PC_Ratio - 0.7) / 0.6) * 100, 0, 100) // 0.7=greed, 1.3=fear +skew_score = clamp(100 - ((SKEW - 100) / 50) * 100, 0, 100) +score = pc_score * 0.6 + skew_score * 0.4 +``` + +### 4. Trend (10%) + +``` +inputs: SPX_Price, SMA20, SMA50, SMA200 +above_count = count(price > SMA20, price > SMA50, price > SMA200) +distance_200 = (price - SMA200) / SMA200 +score = (above_count / 3) * 50 + clamp(distance_200 * 500 + 50, 0, 100) * 0.5 +``` + +### 5. Breadth (10%) + +``` +inputs: Pct_Above_200DMA, Advance_Decline, RSP_SPY_Divergence +breadth_score = Pct_Above_200DMA // already 0-100 +ad_score = clamp((AD_Ratio - 0.5) / 1.5 * 100, 0, 100) +rsp_score = clamp(RSP_SPY_30d_diff * 10 + 50, 0, 100) +score = breadth_score * 0.4 + ad_score * 0.3 + rsp_score * 0.3 +``` + +### 6. Momentum (10%) + +``` +inputs: Sector_RSI_Spread, SPX_ROC_20d +rsi_score = clamp((avg_sector_rsi - 30) / 40 * 100, 0, 100) +roc_score = clamp(SPX_ROC_20d * 10 + 50, 0, 100) +score = rsi_score * 0.5 + roc_score * 0.5 +``` + +### 7. Liquidity (15%) + +``` +inputs: M2_YoY_Change, Fed_Balance_Sheet_Change, SOFR_Rate +m2_score = clamp(M2_YoY * 10 + 50, 0, 100) +fed_score = clamp(Fed_BS_MoM * 20 + 50, 0, 100) +sofr_score = clamp(100 - SOFR * 15, 0, 100) +score = m2_score * 0.4 + fed_score * 0.3 + sofr_score * 0.3 +``` + +### 8. Credit (10%) + +``` +inputs: HY_Spread, IG_Spread, HY_Spread_Change_30d +hy_score = clamp(100 - ((HY_Spread - 3.0) / 5.0) * 100, 0, 100) +ig_score = clamp(100 - ((IG_Spread - 0.8) / 2.0) * 100, 0, 100) +trend_score = HY_narrowing ? 70 : HY_widening ? 30 : 50 +score = hy_score * 0.4 + ig_score * 0.3 + trend_score * 0.3 +``` + +### 9. Macro (5%) + +``` +inputs: Fed_Rate, Yield_Curve_10Y2Y, Unemployment_Trend +rate_score = clamp(100 - Fed_Rate * 15, 0, 100) +curve_score = T10Y2Y > 0 ? 60 + T10Y2Y * 20 : 40 + T10Y2Y * 40 +unemp_score = clamp(100 - (UNRATE - 3.5) * 20, 0, 100) +score = rate_score * 0.3 + curve_score * 0.4 + unemp_score * 0.3 +``` + +### 10. Cross-Asset (5%) + +``` +inputs: Gold_vs_SPY_30d, TLT_vs_SPY_30d, DXY_30d_Change +gold_signal = Gold_30d > SPY_30d ? fear : greed +bond_signal = TLT_30d > SPY_30d ? fear : greed +dxy_signal = DXY_rising ? slight_fear : slight_greed +score = weighted combination with mean reversion +``` + +### Computed Metrics (derived from fetched data, no extra API calls) + +| Metric | Inputs | Formula | Category | +|--------|--------|---------|----------| +| SPX 20/50/200 DMA | ^GSPC closes | `smaCalc(prices, period)` | Trend | +| SPX ROC 20d | ^GSPC closes | `rateOfChange(prices, 20)` | Momentum | +| VIX Term Structure | ^VIX, ^VIX9D, ^VIX3M | `VIX/VIX3M` ratio (<1 = contango) | Volatility | +| Sector RSI (14d) | XLK/XLF/XLE/XLV | Standard RSI formula | Momentum | +| Cross-asset 30d returns | GLD, TLT, SPY, DXY | `rateOfChange(prices, 30)` | Cross-Asset | +| M2 YoY change | M2SL | `(latest - 12mo_ago) / 12mo_ago` | Liquidity | +| Fed BS MoM change | WALCL | `(latest - 4wk_ago) / 4wk_ago` | Liquidity | +| HY spread trend | BAMLH0A0HYM2 | `30d change direction` | Credit | +| RSP/SPY ratio | RSP, SPY | `RSP_return_30d - SPY_return_30d` | Breadth | + +--- + +## Seed Script: `seed-fear-greed.mjs` + +Follows the existing pattern: Railway cron → fetch external APIs → compute scores → atomic publish to Redis → server handler reads from Redis. + +### Redis Keys + +``` +market:fear-greed:v1 # Composite index + all category scores +market:fear-greed:history:v1 # Sorted set — daily snapshots for sparklines (score UNIX score, member ISO date string) +seed-meta:market:fear-greed # Metadata (fetchedAt, recordCount, sourceVersion) +seed-lock:market:fear-greed # Concurrency lock +``` + +**TTL**: 64800s (18h) — 3× the 6h cron interval. Required to survive 2 missed cron cycles (Railway downtime, deploy gaps). `runSeed()` extends this same TTL on both fetch-failure and empty-data paths. +**Cron**: `0 0,6,12,18 * * *` (every 6h) +**health.js `maxStaleMin`**: 720 (12h) — 2× interval. One missed cycle never fires a spurious WARN; the 20min self-heal from `runSeed()` retry covers transient failures. + +**`composite.previous` requires a pre-write Redis GET.** Before calling `runSeed()`, read `market:fear-greed:v1` from Redis, extract `composite.score`, pass it into `publishTransform` as `previous`. `runSeed()` then overwrites the key atomically. Do NOT compute `previous` after the write — the key is already overwritten. + +### API Call Budget + +| Source | Calls | Rate Limited? | Auth | +|--------|-------|--------------|------| +| Yahoo Finance | 19 symbols | 150ms gaps | User-Agent only | +| CBOE CDN | 2 CSVs | No | None | +| CNN dataviz | 1 | No | User-Agent only | +| AAII | 1 | Blocks bots | User-Agent + scrape | +| Redis reads | ~10 FRED series | No | Bearer token | +| **Total** | **~33** | — | — | + +**Estimated runtime**: ~3s (Yahoo sequential) + ~2s (CBOE/CNN/AAII parallel) + ~1s (Redis) = **~6s per run** + +**Timeouts**: Set `AbortSignal.timeout(8000)` on AAII scrape (frequently stalls). AAII failure must not block the entire seed run — wrap in `try/catch`, log warn, continue with degraded Sentiment scoring. + +### Output Schema (stored in Redis) + +```json +{ + "timestamp": "2026-03-24T12:00:00Z", + "composite": { + "score": 38.7, + "label": "Fear", + "previous": 41.2 + }, + "categories": { + "sentiment": { "score": 19, "weight": 0.10, "contribution": 1.9, "inputs": { "cnnFearGreed": 16, "cnnLabel": "Extreme Fear", "aaiBull": 30.4, "aaiBear": 52.0 }, "degraded": false }, + // degraded: true when AAII unavailable; aaiBull/aaiBear: null (not 0) when AAII fetch fails + "volatility": { "score": 47, "weight": 0.10, "contribution": 4.7, "inputs": { "vix": 26.78, "vixChange": 11.31, "vix9d": 28.1, "vix3m": 24.5, "termStructure": "backwardation" } }, + "positioning": { "score": 34, "weight": 0.15, "contribution": 5.1, "inputs": { "putCallRatio": 1.01, "putCallAvg": 0.87, "skew": 135 } }, + "trend": { "score": 52, "weight": 0.10, "contribution": 5.2, "inputs": { "spxPrice": 5667, "sma20": 5580, "sma50": 5520, "sma200": 5200, "aboveMaCount": 3 } }, + "breadth": { "score": 40, "weight": 0.10, "contribution": 4.0, "inputs": { "pctAbove200d": 43.93, "rspSpyRatio": -2.1, "advDecRatio": 0.85 } }, + "momentum": { "score": 13, "weight": 0.10, "contribution": 1.3, "inputs": { "spxRoc20d": -3.2, "sectorRsiAvg": 38, "leadersVsLaggards": -12.5 } }, + "liquidity": { "score": 26, "weight": 0.15, "contribution": 3.9, "inputs": { "m2Yoy": 1.2, "fedBsMom": -0.8, "sofr": 5.31 } }, + "credit": { "score": 68, "weight": 0.10, "contribution": 6.8, "inputs": { "hySpread": 3.27, "igSpread": 1.15, "hyTrend30d": "narrowing" } }, + "macro": { "score": 44, "weight": 0.05, "contribution": 2.2, "inputs": { "fedRate": 3.625, "t10y2y": 0.15, "unrate": 4.1 } }, + "crossAsset": { "score": 72, "weight": 0.05, "contribution": 3.6, "inputs": { "goldReturn30d": 4.2, "tltReturn30d": 1.8, "spyReturn30d": -2.1, "dxyChange30d": -1.5 } } + }, + "headerMetrics": { + "cnnFearGreed": { "value": 16, "label": "Extreme Fear" }, + "aaiBear": { "value": 52, "context": "6-wk high" }, + "aaiBull": { "value": 30.4, "context": "Below avg" }, + "putCall": { "value": 1.01, "context": "vs 0.87 yr avg" }, + "vix": { "value": 26.78, "context": "+11.31%" }, + "hySpread": { "value": 3.27, "context": "vs LT avg" }, + "pctAbove200d": { "value": 43.93, "context": "Down from 68.5%" }, + "yield10y": { "value": 4.25 }, + "fedRate": { "value": "3.50-3.75%" } + }, + "unavailable": false +} +``` + +--- + +## Implementation Plan + +### Phase 1: Data Layer + +1. Add `BAMLC0A0CM` and `SOFR` to `seed-economy.mjs` FRED_SERIES array + - Note: SOFR is weekly cadence from FRED, not daily — Liquidity formula is stable between releases +2. Validate `C:ISSU` symbol returns advance/decline data via Yahoo `/v8/finance/chart` — confirm before building Breadth formula around it +3. Create `seed-fear-greed.mjs`: + - TTL: **64800s** (18h = 3× interval) + - AAII fetch: `AbortSignal.timeout(8000)`, wrapped in `try/catch` — failure uses degraded Sentiment scoring + - Pre-write step: GET `market:fear-greed:v1` from Redis, extract `composite.score` as `previous`, pass via `publishTransform` + - `runSeed()` calls `process.exit(0)` — all extra key writes (e.g. history key) must use the `extraKeys` option, NOT code after the `runSeed()` call +4. Register with **bootstrap 4-file checklist**: + - `cache-keys.ts` — add `market:fear-greed:v1` + - `api/bootstrap.js` — register the key + - `health.js` — classify as `BOOTSTRAP_KEYS` (seeded, CRIT if empty); set `maxStaleMin: 720` (12h = 2× interval) + - `gateway.ts` — wire `GetFearGreedIndex` RPC + +### Phase 2: Proto + RPC + +5. New proto: `proto/worldmonitor/market/v1/fear_greed.proto` + - `GetFearGreedIndex` RPC + - Messages for composite score, category scores, and header metrics +6. New handler: `server/worldmonitor/market/v1/get-fear-greed-index.ts` + - Reads computed data from Redis, returns structured response + +### Phase 3: Frontend Panel + +7. New component: `src/components/FearGreedPanel.ts` + - Gauge — semicircular 0–100 dial with color gradient (red→yellow→green) + - Header grid — 9 key metrics with contextual annotations + - Category breakdown — expandable cards per category (score, weight, contribution, bar) + - Handle `degraded: true` on Sentiment card (show "AAII unavailable" note) +8. Register in finance variant panel config + +### Phase 4: Polish + +9. Historical sparklines — append daily snapshot to `market:fear-greed:history:v1` (sorted set, score = UNIX timestamp, member = ISO date + composite score JSON). Write via `extraKeys` in Phase 1 seeder. TTL: 90 days (7776000s). Frontend reads this key for trend sparkline. +10. Alerts on threshold crossings (e.g. score drops below 20) + +--- + +## MVP Path + +Build the initial version using only data we already have + easy additions: + +1. **Volatility** — VIX from FRED +2. **Credit** — HY + IG spread from FRED +3. **Macro** — Fed rate + yield curve + unemployment from FRED +4. **Trend** — SPX price vs computed MAs from Yahoo +5. **Liquidity** — M2 + Fed balance sheet from FRED + SOFR +6. **Sentiment** — CNN F&G endpoint + crypto F&G (already have) +7. **Momentum** — Sector ETF returns from Yahoo +8. **Cross-Asset** — GLD/TLT/SPY/DXY returns from Yahoo +9. **Positioning** — CBOE put/call CSVs + SKEW from Yahoo +10. **Breadth** — ^MMTH + RSP/SPY divergence + C:ISSU from Yahoo + +All 10 categories covered from day one. No paid sources needed. diff --git a/proto/worldmonitor/market/v1/get_fear_greed_index.proto b/proto/worldmonitor/market/v1/get_fear_greed_index.proto new file mode 100644 index 000000000..1c877d72d --- /dev/null +++ b/proto/worldmonitor/market/v1/get_fear_greed_index.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package worldmonitor.market.v1; + +import "sebuf/http/annotations.proto"; + +message GetFearGreedIndexRequest {} + +message FearGreedCategory { + double score = 1; + double weight = 2; + double contribution = 3; + bool degraded = 4; + string inputs_json = 5; +} + +message GetFearGreedIndexResponse { + double composite_score = 1; + string composite_label = 2; + double previous_score = 3; + string seeded_at = 4; + FearGreedCategory sentiment = 5; + FearGreedCategory volatility = 6; + FearGreedCategory positioning = 7; + FearGreedCategory trend = 8; + FearGreedCategory breadth = 9; + FearGreedCategory momentum = 10; + FearGreedCategory liquidity = 11; + FearGreedCategory credit = 12; + FearGreedCategory macro = 13; + FearGreedCategory cross_asset = 14; + double vix = 15; + double hy_spread = 16; + double yield_10y = 17; + double put_call_ratio = 18; + double pct_above_200d = 19; + double cnn_fear_greed = 20; + string cnn_label = 21; + double aaii_bull = 22; + double aaii_bear = 23; + string fed_rate = 24; + bool unavailable = 25; +} diff --git a/proto/worldmonitor/market/v1/service.proto b/proto/worldmonitor/market/v1/service.proto index a3881aff8..eb94c8a3c 100644 --- a/proto/worldmonitor/market/v1/service.proto +++ b/proto/worldmonitor/market/v1/service.proto @@ -19,6 +19,7 @@ import "worldmonitor/market/v1/list_crypto_sectors.proto"; 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"; // MarketService provides APIs for financial market data from Finnhub, Yahoo Finance, and CoinGecko. service MarketService { @@ -103,4 +104,9 @@ service MarketService { rpc ListOtherTokens(ListOtherTokensRequest) returns (ListOtherTokensResponse) { option (sebuf.http.config) = {path: "/list-other-tokens", method: HTTP_METHOD_GET}; } + + // GetFearGreedIndex retrieves the composite Fear & Greed sentiment index. + rpc GetFearGreedIndex(GetFearGreedIndexRequest) returns (GetFearGreedIndexResponse) { + option (sebuf.http.config) = {path: "/get-fear-greed-index", method: HTTP_METHOD_GET}; + } } diff --git a/scripts/seed-economy.mjs b/scripts/seed-economy.mjs index 3bd15a30b..86349fcfb 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']; +const FRED_SERIES = ['WALCL', 'FEDFUNDS', 'T10Y2Y', 'UNRATE', 'CPIAUCSL', 'DGS10', 'VIXCLS', 'GDP', 'M2SL', 'DCOILWTICO', 'BAMLH0A0HYM2', 'ICSA', 'MORTGAGE30US', 'BAMLC0A0CM', 'SOFR']; // ─── EIA Energy Prices (WTI + Brent) ─── diff --git a/scripts/seed-fear-greed.mjs b/scripts/seed-fear-greed.mjs new file mode 100644 index 000000000..3e838de5a --- /dev/null +++ b/scripts/seed-fear-greed.mjs @@ -0,0 +1,411 @@ +#!/usr/bin/env node + +import { loadEnvFile, CHROME_UA, runSeed, readSeedSnapshot, sleep } from './_seed-utils.mjs'; + +loadEnvFile(import.meta.url); + +const FEAR_GREED_KEY = 'market:fear-greed:v1'; +const FEAR_GREED_TTL = 64800; // 18h = 3x 6h interval + +const FRED_PREFIX = 'economic:fred:v1'; + +// --- Yahoo Finance fetching (16 symbols, 150ms gaps) --- +const YAHOO_SYMBOLS = ['^GSPC','^VIX','^VIX9D','^VIX3M','^SKEW','^MMTH','C:ISSU','GLD','TLT','SPY','RSP','DX-Y.NYB','XLK','XLF','XLE','XLV']; + +async function fetchYahooSymbol(symbol) { + const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(symbol)}?interval=1d&range=3mo`; + try { + const resp = await fetch(url, { + headers: { 'User-Agent': CHROME_UA, Accept: 'application/json' }, + signal: AbortSignal.timeout(10_000), + }); + if (!resp.ok) { console.warn(` Yahoo ${symbol}: HTTP ${resp.status}`); return null; } + const data = await resp.json(); + const result = data?.chart?.result?.[0]; + if (!result) return null; + const closes = result.indicators?.quote?.[0]?.close ?? []; + const validCloses = closes.filter(v => v != null); + const price = result.meta?.regularMarketPrice ?? validCloses.at(-1) ?? null; + return { symbol, price, closes: validCloses }; + } catch (e) { + console.warn(` Yahoo ${symbol}: ${e.message}`); + return null; + } +} + +async function fetchAllYahoo() { + const results = {}; + for (const sym of YAHOO_SYMBOLS) { + results[sym] = await fetchYahooSymbol(sym); + await sleep(150); + } + return results; +} + +// --- CBOE P/C ratios --- +async function fetchCBOE() { + const [totalResp, equityResp] = await Promise.allSettled([ + fetch('https://cdn.cboe.com/api/global/us_indices/daily_prices/totalpc.csv', { headers: { 'User-Agent': CHROME_UA }, signal: AbortSignal.timeout(10_000) }), + fetch('https://cdn.cboe.com/api/global/us_indices/daily_prices/equitypc.csv', { headers: { 'User-Agent': CHROME_UA }, signal: AbortSignal.timeout(10_000) }), + ]); + const parseLastValue = async (resp) => { + if (resp.status !== 'fulfilled' || !resp.value.ok) return null; + const text = await resp.value.text(); + const lines = text.trim().split('\n').filter(l => l.trim()); + const last = lines.at(-1)?.split(','); + return last?.length >= 2 ? parseFloat(last[1]) : null; + }; + const [totalPc, equityPc] = await Promise.all([parseLastValue(totalResp), parseLastValue(equityResp)]); + return { totalPc, equityPc }; +} + +// --- CNN Fear & Greed --- +async function fetchCNN() { + try { + const date = new Date().toISOString().slice(0,10).replace(/-/g,''); + const resp = await fetch(`https://production.dataviz.cnn.io/index/fearandgreed/graphdata/${date}`, { + headers: { 'User-Agent': CHROME_UA, Accept: 'application/json' }, + signal: AbortSignal.timeout(8_000), + }); + if (!resp.ok) return null; + const data = await resp.json(); + const score = data?.fear_and_greed?.score; + const rating = data?.fear_and_greed?.rating; + return score != null ? { score: Math.round(score), label: rating ?? labelFromScore(Math.round(score)) } : null; + } catch { return null; } +} + +// --- AAII Sentiment (LOW reliability, always wrapped, non-blocking) --- +async function fetchAAII() { + try { + const resp = await fetch('https://www.aaii.com/sentimentsurvey/sent_results', { + headers: { 'User-Agent': CHROME_UA, Accept: 'text/html,application/xhtml+xml' }, + signal: AbortSignal.timeout(8_000), + }); + if (!resp.ok) return null; + const html = await resp.text(); + const bullMatch = html.match(/Bullish[^%]*?([\d.]+)%/i); + const bearMatch = html.match(/Bearish[^%]*?([\d.]+)%/i); + if (!bullMatch || !bearMatch) return null; + return { bull: parseFloat(bullMatch[1]), bear: parseFloat(bearMatch[1]) }; + } catch (e) { + console.warn(' AAII: fetch failed:', e.message, '(using degraded Sentiment)'); + return null; + } +} + +// --- FRED Redis reads --- +async function readFred(seriesId) { + const url = process.env.UPSTASH_REDIS_REST_URL; + const token = process.env.UPSTASH_REDIS_REST_TOKEN; + if (!url || !token) return null; + try { + const resp = await fetch(`${url}/get/${encodeURIComponent(`${FRED_PREFIX}:${seriesId}:0`)}`, { + headers: { Authorization: `Bearer ${token}` }, + signal: AbortSignal.timeout(5_000), + }); + if (!resp.ok) return null; + const { result } = await resp.json(); + if (!result) return null; + const parsed = JSON.parse(result); + const obs = parsed?.series?.observations; + if (!obs?.length) return null; + return obs; + } catch { return null; } +} + +async function readMacroSignals() { + const url = process.env.UPSTASH_REDIS_REST_URL; + const token = process.env.UPSTASH_REDIS_REST_TOKEN; + if (!url || !token) return null; + try { + const resp = await fetch(`${url}/get/${encodeURIComponent('economic:macro-signals:v1')}`, { + headers: { Authorization: `Bearer ${token}` }, + signal: AbortSignal.timeout(5_000), + }); + if (!resp.ok) return null; + const { result } = await resp.json(); + return result ? JSON.parse(result) : null; + } catch { return null; } +} + +// --- Math helpers --- +function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); } +function sma(prices, period) { + if (prices.length < period) return null; + return prices.slice(-period).reduce((a,b) => a+b, 0) / period; +} +function roc(prices, period) { + if (prices.length < period+1) return null; + const prev = prices[prices.length - period - 1]; + const curr = prices[prices.length - 1]; + return prev ? ((curr - prev) / prev) * 100 : null; +} +function rsi(prices, period=14) { + if (prices.length < period+1) return 50; + let gains=0, losses=0; + for (let i=prices.length-period; i0) gains+=d; else losses+=Math.abs(d); + } + if (losses===0) return 100; + const rs = (gains/period)/(losses/period); + return 100 - (100/(1+rs)); +} +function fredLatest(obs) { + if (!obs) return null; + const v = parseFloat(obs.at(-1)?.value ?? 'NaN'); + return Number.isFinite(v) ? v : null; +} +function fredNMonthsAgo(obs, months) { + if (!obs) return null; + const idx = obs.length - 1 - months; + if (idx < 0) return null; + const v = parseFloat(obs[idx]?.value ?? 'NaN'); + return Number.isFinite(v) ? v : null; +} +function labelFromScore(s) { + if (s <= 20) return 'Extreme Fear'; + if (s <= 40) return 'Fear'; + if (s <= 60) return 'Neutral'; + if (s <= 80) return 'Greed'; + return 'Extreme Greed'; +} + +// --- Scoring --- +function scoreCategory(name, inputs) { + switch(name) { + case 'sentiment': { + const { cnnFg, aaiBull, aaiBear, cryptoFg } = inputs; + const degraded = aaiBull == null || aaiBear == null; + let score; + if (!degraded) { + const bullPct = clamp(aaiBull, 0, 100); + const bearPct = clamp(aaiBear, 0, 100); + const bullPercentile = clamp((bullPct / 60) * 100, 0, 100); + const bearPercentile = clamp((bearPct / 55) * 100, 0, 100); + if (cnnFg != null) { + score = (cnnFg * 0.4) + (bullPercentile * 0.3) + ((100 - bearPercentile) * 0.3); + } else { + score = (bullPercentile * 0.5) + ((100 - bearPercentile) * 0.5); + } + } else if (cnnFg != null) { + score = cnnFg; + } else if (cryptoFg != null) { + score = cryptoFg; + } else { + score = 50; + } + return { score: clamp(Math.round(score), 0, 100), degraded, inputs: { cnnFearGreed: cnnFg, aaiBull: aaiBull ?? null, aaiBear: aaiBear ?? null, cryptoFg } }; + } + case 'volatility': { + const { vix, vix9d, vix3m } = inputs; + if (vix == null) return { score: 50, inputs }; + const vixScore = clamp(100 - ((vix - 12) / 28) * 100, 0, 100); + const termScore = (vix9d != null && vix3m != null) ? (vix / vix3m < 1 ? 70 : 30) : 50; + const termStructure = (vix9d != null && vix3m != null) ? (vix / vix3m < 1 ? 'contango' : 'backwardation') : 'unknown'; + return { score: Math.round(vixScore * 0.7 + termScore * 0.3), inputs: { vix, vix9d, vix3m, termStructure } }; + } + case 'positioning': { + const { totalPc, equityPc, skew } = inputs; + const pc = totalPc ?? equityPc; + if (pc == null && skew == null) return { score: 50, inputs }; + const pcScore = pc != null ? clamp(100 - ((pc - 0.7) / 0.6) * 100, 0, 100) : 50; + const skewScore = skew != null ? clamp(100 - ((skew - 100) / 50) * 100, 0, 100) : 50; + const w = pc != null && skew != null ? [0.6, 0.4] : [1.0, 0.0]; + return { score: Math.round(pcScore * w[0] + skewScore * w[1]), inputs: { putCallRatio: pc, skew } }; + } + case 'trend': { + const { prices } = inputs; + if (!prices?.length) return { score: 50, inputs: {} }; + const price = prices.at(-1); + const s20 = sma(prices, 20), s50 = sma(prices, 50), s200 = sma(prices, 200); + const aboveCount = [s20, s50, s200].filter(s => s != null && price > s).length; + const dist200 = s200 ? (price - s200) / s200 : 0; + const score = (aboveCount / 3) * 50 + clamp(dist200 * 500 + 50, 0, 100) * 0.5; + return { score: Math.round(clamp(score, 0, 100)), inputs: { spxPrice: price, sma20: s20, sma50: s50, sma200: s200, aboveMaCount: aboveCount } }; + } + case 'breadth': { + const { mmthPrice, rspCloses, spyCloses, advDecRatio } = inputs; + const breadthScore = mmthPrice != null ? clamp(mmthPrice, 0, 100) : 50; + const rspRoc = (rspCloses?.length && spyCloses?.length) ? (roc(rspCloses, 30) ?? 0) - (roc(spyCloses, 30) ?? 0) : null; + const rspScore = rspRoc != null ? clamp(rspRoc * 10 + 50, 0, 100) : 50; + const adScore = advDecRatio != null ? clamp((advDecRatio - 0.5) / 1.5 * 100, 0, 100) : 50; + const hasAd = advDecRatio != null; + const w = hasAd ? [0.4, 0.3, 0.3] : [0.57, 0, 0.43]; + const score = breadthScore * w[0] + adScore * w[1] + rspScore * w[2]; + return { score: Math.round(clamp(score, 0, 100)), inputs: { pctAbove200d: mmthPrice, rspSpyRatio: rspRoc, advDecRatio: advDecRatio ?? null } }; + } + case 'momentum': { + const { spxCloses, sectorCloses } = inputs; + const spxRoc = spxCloses?.length ? roc(spxCloses, 20) : null; + const rocScore = spxRoc != null ? clamp(spxRoc * 10 + 50, 0, 100) : 50; + const sectorRsiValues = sectorCloses ? Object.values(sectorCloses).filter(Boolean).map(c => rsi(c)) : []; + const avgRsi = sectorRsiValues.length ? sectorRsiValues.reduce((a,b)=>a+b,0)/sectorRsiValues.length : 50; + const rsiScore = clamp((avgRsi - 30) / 40 * 100, 0, 100); + return { score: Math.round((rsiScore * 0.5 + rocScore * 0.5)), inputs: { spxRoc20d: spxRoc, sectorRsiAvg: Math.round(avgRsi) } }; + } + case 'liquidity': { + const { m2Obs, walclObs, sofr } = inputs; + const m2Latest = fredLatest(m2Obs), m2Ago = fredNMonthsAgo(m2Obs, 12); + const m2Yoy = (m2Latest && m2Ago && m2Ago !== 0) ? ((m2Latest - m2Ago) / m2Ago) * 100 : null; + const walclLatest = fredLatest(walclObs), walclAgo = fredNMonthsAgo(walclObs, 1); + const fedBsMom = (walclLatest && walclAgo && walclAgo !== 0) ? ((walclLatest - walclAgo) / walclAgo) * 100 : null; + const m2Score = m2Yoy != null ? clamp(m2Yoy * 10 + 50, 0, 100) : 50; + const fedScore = fedBsMom != null ? clamp(fedBsMom * 20 + 50, 0, 100) : 50; + const sofrScore = sofr != null ? clamp(100 - sofr * 15, 0, 100) : 50; + return { score: Math.round(m2Score * 0.4 + fedScore * 0.3 + sofrScore * 0.3), inputs: { m2Yoy, fedBsMom, sofr } }; + } + case 'credit': { + const { hyObs, igObs } = inputs; + const hySpread = fredLatest(hyObs), igSpread = fredLatest(igObs); + const hyScore = hySpread != null ? clamp(100 - ((hySpread - 3.0) / 5.0) * 100, 0, 100) : 50; + const igScore = igSpread != null ? clamp(100 - ((igSpread - 0.8) / 2.0) * 100, 0, 100) : 50; + const hyPrev = fredNMonthsAgo(hyObs, 1); + const hyTrend = (hySpread != null && hyPrev != null) ? (hySpread < hyPrev ? 'narrowing' : hySpread > hyPrev ? 'widening' : 'stable') : 'stable'; + const trendScore = hyTrend === 'narrowing' ? 70 : hyTrend === 'widening' ? 30 : 50; + return { score: Math.round(hyScore * 0.4 + igScore * 0.3 + trendScore * 0.3), inputs: { hySpread, igSpread, hyTrend30d: hyTrend } }; + } + case 'macro': { + const { fedObs, curveObs, unrateObs } = inputs; + const fedRate = fredLatest(fedObs), t10y2y = fredLatest(curveObs), unrate = fredLatest(unrateObs); + const rateScore = fedRate != null ? clamp(100 - fedRate * 15, 0, 100) : 50; + const curveScore = t10y2y != null ? (t10y2y > 0 ? clamp(60 + t10y2y * 20, 0, 100) : clamp(40 + t10y2y * 40, 0, 100)) : 50; + const unempScore = unrate != null ? clamp(100 - (unrate - 3.5) * 20, 0, 100) : 50; + return { score: Math.round(rateScore * 0.3 + curveScore * 0.4 + unempScore * 0.3), inputs: { fedRate, t10y2y, unrate } }; + } + case 'crossAsset': { + const { gldCloses, tltCloses, spyCloses, dxyCloses } = inputs; + const goldRoc = gldCloses?.length ? roc(gldCloses, 30) : null; + const tltRoc = tltCloses?.length ? roc(tltCloses, 30) : null; + const spyRoc = spyCloses?.length ? roc(spyCloses, 30) : null; + const dxyRoc = dxyCloses?.length ? roc(dxyCloses, 30) : null; + const goldSignal = (goldRoc != null && spyRoc != null) ? (goldRoc > spyRoc ? 30 : 70) : 50; + const bondSignal = (tltRoc != null && spyRoc != null) ? (tltRoc > spyRoc ? 30 : 70) : 50; + const dxySignal = dxyRoc != null ? (dxyRoc > 0 ? 40 : 60) : 50; + return { score: Math.round((goldSignal + bondSignal + dxySignal) / 3), inputs: { goldReturn30d: goldRoc, tltReturn30d: tltRoc, spyReturn30d: spyRoc, dxyChange30d: dxyRoc } }; + } + default: return { score: 50, inputs }; + } +} + +const WEIGHTS = { sentiment: 0.10, volatility: 0.10, positioning: 0.15, trend: 0.10, breadth: 0.10, momentum: 0.10, liquidity: 0.15, credit: 0.10, macro: 0.05, crossAsset: 0.05 }; + +async function fetchAll() { + const prevSnapshot = await readSeedSnapshot(FEAR_GREED_KEY).catch(() => null); + const previousScore = prevSnapshot?.composite?.score ?? null; + + const [yahooResults, cboeResult, cnnResult, aaiiResult, macroSignals] = await Promise.allSettled([ + fetchAllYahoo(), + fetchCBOE(), + fetchCNN(), + fetchAAII(), + readMacroSignals(), + ]); + + const yahoo = yahooResults.status === 'fulfilled' ? yahooResults.value : {}; + const cboe = cboeResult.status === 'fulfilled' ? cboeResult.value : {}; + const cnn = cnnResult.status === 'fulfilled' ? cnnResult.value : null; + const aaii = aaiiResult.status === 'fulfilled' ? aaiiResult.value : null; + const macro = macroSignals.status === 'fulfilled' ? macroSignals.value : null; + + if (yahooResults.status === 'rejected') console.warn(' Yahoo batch failed:', yahooResults.reason?.message); + if (cboeResult.status === 'rejected') console.warn(' CBOE failed:', cboeResult.reason?.message); + if (cnnResult.status === 'rejected') console.warn(' CNN failed:', cnnResult.reason?.message); + if (aaiiResult.status === 'rejected') console.warn(' AAII failed:', aaiiResult.reason?.message); + + const [hyObs, igObs, m2Obs, walclObs, sofrObs, fedObs, curveObs, unrateObs, vixObs, dgs10Obs] = await Promise.all([ + readFred('BAMLH0A0HYM2'), readFred('BAMLC0A0CM'), readFred('M2SL'), readFred('WALCL'), + readFred('SOFR'), readFred('FEDFUNDS'), readFred('T10Y2Y'), readFred('UNRATE'), readFred('VIXCLS'), readFred('DGS10'), + ]); + + const gspc = yahoo['^GSPC']; + const vixData = yahoo['^VIX']; + const vix9d = yahoo['^VIX9D']; + const vix3m = yahoo['^VIX3M']; + const skew = yahoo['^SKEW']; + const mmth = yahoo['^MMTH']; + const cissu = yahoo['C:ISSU']; + const gld = yahoo['GLD'], tlt = yahoo['TLT'], 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 vixLive = vixData?.price ?? fredLatest(vixObs); + const vix9dPrice = vix9d?.price ?? null; + const vix3mPrice = vix3m?.price ?? null; + const skewPrice = skew?.price ?? null; + const mmthPrice = mmth?.price ?? null; + const sofrRate = fredLatest(sofrObs); + const cryptoFg = macro?.fearGreed?.score ?? macro?.signals?.fearGreed?.value ?? null; + + let advDecRatio = null; + if (cissu?.price != null) { + advDecRatio = cissu.price > 0 ? Math.min(cissu.price / 100, 2.0) : null; + } + + const cats = { + sentiment: scoreCategory('sentiment', { cnnFg: cnn?.score ?? null, aaiBull: aaii?.bull ?? null, aaiBear: aaii?.bear ?? null, cryptoFg }), + volatility: scoreCategory('volatility', { vix: vixLive, vix9d: vix9dPrice, vix3m: vix3mPrice }), + positioning: scoreCategory('positioning', { totalPc: cboe.totalPc, equityPc: cboe.equityPc, skew: skewPrice }), + trend: scoreCategory('trend', { prices: gspc?.closes ?? [] }), + breadth: scoreCategory('breadth', { mmthPrice, rspCloses: rsp?.closes, spyCloses: spy?.closes, advDecRatio }), + momentum: scoreCategory('momentum', { spxCloses: gspc?.closes, sectorCloses: { XLK: xlk?.closes, XLF: xlf?.closes, XLE: xle?.closes, XLV: xlv?.closes } }), + liquidity: scoreCategory('liquidity', { m2Obs, walclObs, sofr: sofrRate }), + credit: scoreCategory('credit', { hyObs, igObs }), + macro: scoreCategory('macro', { fedObs, curveObs, unrateObs }), + crossAsset: scoreCategory('crossAsset', { gldCloses: gld?.closes, tltCloses: tlt?.closes, spyCloses: spy?.closes, dxyCloses: dxy?.closes }), + }; + + const compositeScore = Math.round( + Object.entries(cats).reduce((sum, [name, cat]) => sum + cat.score * WEIGHTS[name], 0) * 10 + ) / 10; + const compositeLabel = labelFromScore(compositeScore); + + const fedRate = fredLatest(fedObs); + const fedRateStr = fedRate != null ? `${fedRate.toFixed(2)}%` : null; + const hySpreadVal = fredLatest(hyObs); + + const payload = { + timestamp: new Date().toISOString(), + composite: { score: compositeScore, label: compositeLabel, previous: previousScore }, + categories: { + sentiment: { score: cats.sentiment.score, weight: WEIGHTS.sentiment, contribution: Math.round(cats.sentiment.score * WEIGHTS.sentiment * 10)/10, inputs: cats.sentiment.inputs, degraded: cats.sentiment.degraded ?? false }, + volatility: { score: cats.volatility.score, weight: WEIGHTS.volatility, contribution: Math.round(cats.volatility.score * WEIGHTS.volatility * 10)/10, inputs: cats.volatility.inputs }, + positioning: { score: cats.positioning.score, weight: WEIGHTS.positioning, contribution: Math.round(cats.positioning.score * WEIGHTS.positioning * 10)/10, inputs: cats.positioning.inputs }, + trend: { score: cats.trend.score, weight: WEIGHTS.trend, contribution: Math.round(cats.trend.score * WEIGHTS.trend * 10)/10, inputs: cats.trend.inputs }, + breadth: { score: cats.breadth.score, weight: WEIGHTS.breadth, contribution: Math.round(cats.breadth.score * WEIGHTS.breadth * 10)/10, inputs: cats.breadth.inputs }, + momentum: { score: cats.momentum.score, weight: WEIGHTS.momentum, contribution: Math.round(cats.momentum.score * WEIGHTS.momentum * 10)/10, inputs: cats.momentum.inputs }, + liquidity: { score: cats.liquidity.score, weight: WEIGHTS.liquidity, contribution: Math.round(cats.liquidity.score * WEIGHTS.liquidity * 10)/10, inputs: cats.liquidity.inputs }, + credit: { score: cats.credit.score, weight: WEIGHTS.credit, contribution: Math.round(cats.credit.score * WEIGHTS.credit * 10)/10, inputs: cats.credit.inputs }, + macro: { score: cats.macro.score, weight: WEIGHTS.macro, contribution: Math.round(cats.macro.score * WEIGHTS.macro * 10)/10, inputs: cats.macro.inputs }, + crossAsset: { score: cats.crossAsset.score, weight: WEIGHTS.crossAsset, contribution: Math.round(cats.crossAsset.score * WEIGHTS.crossAsset * 10)/10, inputs: cats.crossAsset.inputs }, + }, + headerMetrics: { + cnnFearGreed: cnn ? { value: cnn.score, label: cnn.label } : null, + aaiBear: aaii ? { value: Math.round(aaii.bear), context: `${aaii.bear.toFixed(1)}%` } : null, + aaiBull: aaii ? { value: Math.round(aaii.bull), context: `${aaii.bull.toFixed(1)}%` } : null, + putCall: cboe.totalPc != null ? { value: cboe.totalPc } : null, + vix: vixLive != null ? { value: vixLive } : null, + hySpread: hySpreadVal != null ? { value: hySpreadVal } : null, + pctAbove200d: mmthPrice != null ? { value: mmthPrice } : null, + yield10y: fredLatest(dgs10Obs) != null ? { value: fredLatest(dgs10Obs) } : null, + fedRate: fedRateStr ? { value: fedRateStr } : null, + }, + unavailable: false, + }; + + return payload; +} + +function validate(data) { + return data?.composite?.score != null && data.timestamp != null; +} + +runSeed('market', 'fear-greed', FEAR_GREED_KEY, fetchAll, { + validateFn: validate, + ttlSeconds: FEAR_GREED_TTL, + sourceVersion: 'yahoo-cboe-cnn-fred-v1', +}).catch((err) => { + console.error('FATAL:', err.message || err); + process.exit(1); +}); diff --git a/server/_shared/cache-keys.ts b/server/_shared/cache-keys.ts index 075e09e1c..9810d130c 100644 --- a/server/_shared/cache-keys.ts +++ b/server/_shared/cache-keys.ts @@ -61,6 +61,7 @@ export const BOOTSTRAP_CACHE_KEYS: Record = { otherTokens: 'market:other-tokens:v1', nationalDebt: 'economic:national-debt:v1', marketImplications: 'intelligence:market-implications:v1', + fearGreedIndex: 'market:fear-greed:v1', }; export const BOOTSTRAP_TIERS: Record = { @@ -92,4 +93,5 @@ export const BOOTSTRAP_TIERS: Record = { otherTokens: 'slow', nationalDebt: 'slow', marketImplications: 'slow', + fearGreedIndex: 'slow', }; diff --git a/server/gateway.ts b/server/gateway.ts index abe6e293c..b1eb4fc6c 100644 --- a/server/gateway.ts +++ b/server/gateway.ts @@ -65,6 +65,7 @@ const RPC_CACHE_TIER: Record = { '/api/market/v1/list-commodity-quotes': 'medium', '/api/market/v1/list-stablecoin-markets': 'medium', '/api/market/v1/get-sector-summary': 'medium', + '/api/market/v1/get-fear-greed-index': 'slow', '/api/market/v1/list-gulf-quotes': 'medium', '/api/market/v1/analyze-stock': 'slow', '/api/market/v1/get-stock-analysis-history': 'medium', diff --git a/server/worldmonitor/market/v1/get-fear-greed-index.ts b/server/worldmonitor/market/v1/get-fear-greed-index.ts new file mode 100644 index 000000000..6315969d9 --- /dev/null +++ b/server/worldmonitor/market/v1/get-fear-greed-index.ts @@ -0,0 +1,61 @@ +import type { + ServerContext, + GetFearGreedIndexRequest, + GetFearGreedIndexResponse, + FearGreedCategory, +} from '../../../../src/generated/server/worldmonitor/market/v1/service_server'; +import { getCachedJson } from '../../../_shared/redis'; + +const SEED_CACHE_KEY = 'market:fear-greed:v1'; + +export async function getFearGreedIndex( + _ctx: ServerContext, + _req: GetFearGreedIndexRequest, +): Promise { + try { + const raw = await getCachedJson(SEED_CACHE_KEY, true) as Record | null; + if (!raw?.composite) return { compositeScore: 0, compositeLabel: '', unavailable: true } as GetFearGreedIndexResponse; + + const comp = raw.composite as Record; + const cats = (raw.categories ?? {}) as Record>; + const hdr = (raw.headerMetrics ?? {}) as Record | null>; + + const mapCat = (c: Record | undefined): FearGreedCategory => ({ + score: Number(c?.score ?? 50), + weight: Number(c?.weight ?? 0), + contribution: Number(c?.contribution ?? 0), + degraded: Boolean(c?.degraded), + inputsJson: JSON.stringify(c?.inputs ?? {}), + }); + + return { + compositeScore: Number(comp.score ?? 0), + compositeLabel: String(comp.label ?? ''), + previousScore: Number(comp.previous ?? 0), + seededAt: String(raw.timestamp ?? ''), + sentiment: mapCat(cats.sentiment), + volatility: mapCat(cats.volatility), + positioning: mapCat(cats.positioning), + trend: mapCat(cats.trend), + breadth: mapCat(cats.breadth), + momentum: mapCat(cats.momentum), + liquidity: mapCat(cats.liquidity), + credit: mapCat(cats.credit), + macro: mapCat(cats.macro), + crossAsset: mapCat(cats.crossAsset), + vix: Number(hdr?.vix?.value ?? 0), + hySpread: Number(hdr?.hySpread?.value ?? 0), + yield10y: Number(hdr?.yield10y?.value ?? 0), + putCallRatio: Number(hdr?.putCall?.value ?? 0), + pctAbove200d: Number(hdr?.pctAbove200d?.value ?? 0), + cnnFearGreed: Number(hdr?.cnnFearGreed?.value ?? 0), + cnnLabel: String(hdr?.cnnFearGreed?.label ?? ''), + aaiiBull: Number(hdr?.aaiBull?.value ?? 0), + aaiiBear: Number(hdr?.aaiBear?.value ?? 0), + fedRate: String(hdr?.fedRate?.value ?? ''), + unavailable: false, + }; + } catch { + return { compositeScore: 0, compositeLabel: '', unavailable: true } as GetFearGreedIndexResponse; + } +} diff --git a/server/worldmonitor/market/v1/handler.ts b/server/worldmonitor/market/v1/handler.ts index 02be5e56f..cfdf30657 100644 --- a/server/worldmonitor/market/v1/handler.ts +++ b/server/worldmonitor/market/v1/handler.ts @@ -29,6 +29,7 @@ import { listCryptoSectors } from './list-crypto-sectors'; 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'; export const marketHandler: MarketServiceHandler = { listMarketQuotes, @@ -47,4 +48,5 @@ export const marketHandler: MarketServiceHandler = { listDefiTokens, listAiTokens, listOtherTokens, + getFearGreedIndex, }; diff --git a/src/App.ts b/src/App.ts index 5830e336e..f190ccb73 100644 --- a/src/App.ts +++ b/src/App.ts @@ -27,6 +27,7 @@ import type { ServiceStatusPanel } from '@/components/ServiceStatusPanel'; import type { StablecoinPanel } from '@/components/StablecoinPanel'; import type { ETFFlowsPanel } from '@/components/ETFFlowsPanel'; import type { MacroSignalsPanel } from '@/components/MacroSignalsPanel'; +import type { FearGreedPanel } from '@/components/FearGreedPanel'; import type { StrategicPosturePanel } from '@/components/StrategicPosturePanel'; import type { StrategicRiskPanel } from '@/components/StrategicRiskPanel'; import type { GulfEconomiesPanel } from '@/components/GulfEconomiesPanel'; @@ -235,6 +236,10 @@ export class App { const panel = this.state.panels['macro-signals'] as MacroSignalsPanel | undefined; if (panel) primeTask('macro-signals', () => panel.fetchData()); } + if (shouldPrime('fear-greed')) { + const panel = this.state.panels['fear-greed'] as FearGreedPanel | undefined; + if (panel) primeTask('fear-greed', () => panel.fetchData()); + } if (shouldPrime('etf-flows')) { const panel = this.state.panels['etf-flows'] as ETFFlowsPanel | undefined; if (panel) primeTask('etf-flows', () => panel.fetchData()); @@ -1068,6 +1073,12 @@ export class App { REFRESH_INTERVALS.macroSignals, () => this.isPanelNearViewport('macro-signals') ); + this.refreshScheduler.scheduleRefresh( + 'fear-greed', + () => (this.state.panels['fear-greed'] as FearGreedPanel).fetchData(), + REFRESH_INTERVALS.fearGreed, + () => this.isPanelNearViewport('fear-greed') + ); this.refreshScheduler.scheduleRefresh( 'strategic-posture', () => (this.state.panels['strategic-posture'] as StrategicPosturePanel).refresh(), diff --git a/src/app/panel-layout.ts b/src/app/panel-layout.ts index fec071427..ef8455153 100644 --- a/src/app/panel-layout.ts +++ b/src/app/panel-layout.ts @@ -36,6 +36,7 @@ import { RuntimeConfigPanel, InsightsPanel, MacroSignalsPanel, + FearGreedPanel, ETFFlowsPanel, StablecoinPanel, UcdpEventsPanel, @@ -866,6 +867,7 @@ export class PanelLayoutManager implements AppModule { ); this.createPanel('macro-signals', () => new MacroSignalsPanel()); + this.createPanel('fear-greed', () => new FearGreedPanel()); this.createPanel('etf-flows', () => new ETFFlowsPanel()); this.createPanel('stablecoins', () => new StablecoinPanel()); diff --git a/src/components/FearGreedPanel.ts b/src/components/FearGreedPanel.ts new file mode 100644 index 000000000..34b963702 --- /dev/null +++ b/src/components/FearGreedPanel.ts @@ -0,0 +1,234 @@ +import { Panel } from './Panel'; +import { t } from '@/services/i18n'; +import { escapeHtml } from '@/utils/sanitize'; +import { getHydratedData } from '@/services/bootstrap'; + +interface FearGreedData { + compositeScore: number; + compositeLabel: string; + previousScore: number; + seededAt: string; + sentiment?: CategoryData; + volatility?: CategoryData; + positioning?: CategoryData; + trend?: CategoryData; + breadth?: CategoryData; + momentum?: CategoryData; + liquidity?: CategoryData; + credit?: CategoryData; + macro?: CategoryData; + crossAsset?: CategoryData; + vix: number; + hySpread: number; + yield10y: number; + putCallRatio: number; + pctAbove200d: number; + cnnFearGreed: number; + cnnLabel: string; + aaiiBull: number; + aaiiBear: number; + fedRate: string; + unavailable?: boolean; +} + +interface CategoryData { + score: number; + weight: number; + contribution: number; + degraded?: boolean; + inputsJson?: string; +} + +function scoreColor(score: number): string { + if (score <= 20) return '#e74c3c'; + if (score <= 40) return '#e67e22'; + if (score <= 60) return '#f1c40f'; + if (score <= 80) return '#2ecc71'; + return '#27ae60'; +} + +function fmt(v: number | null | undefined, digits = 2): string { + if (v == null) return 'N/A'; + return v.toFixed(digits); +} + +function mapSeedPayload(raw: Record): FearGreedData | null { + const comp = raw.composite as Record | undefined; + if (!comp?.score) return null; + const cats = (raw.categories ?? {}) as Record>; + const hdr = (raw.headerMetrics ?? {}) as Record | null>; + const mapCat = (c: Record | undefined): CategoryData | undefined => c ? { + score: Number(c.score ?? 50), + weight: Number(c.weight ?? 0), + contribution: Number(c.contribution ?? 0), + degraded: Boolean(c.degraded), + inputsJson: JSON.stringify(c.inputs ?? {}), + } : undefined; + return { + compositeScore: Number(comp.score), + compositeLabel: String(comp.label ?? ''), + previousScore: Number(comp.previous ?? 0), + seededAt: String(raw.timestamp ?? ''), + sentiment: mapCat(cats.sentiment), + volatility: mapCat(cats.volatility), + positioning: mapCat(cats.positioning), + trend: mapCat(cats.trend), + breadth: mapCat(cats.breadth), + momentum: mapCat(cats.momentum), + liquidity: mapCat(cats.liquidity), + credit: mapCat(cats.credit), + macro: mapCat(cats.macro), + crossAsset: mapCat(cats.crossAsset), + vix: Number(hdr?.vix?.value ?? 0), + hySpread: Number(hdr?.hySpread?.value ?? 0), + yield10y: Number(hdr?.yield10y?.value ?? 0), + putCallRatio: Number(hdr?.putCall?.value ?? 0), + pctAbove200d: Number(hdr?.pctAbove200d?.value ?? 0), + cnnFearGreed: Number(hdr?.cnnFearGreed?.value ?? 0), + cnnLabel: String(hdr?.cnnFearGreed?.label ?? ''), + aaiiBull: Number(hdr?.aaiBull?.value ?? 0), + aaiiBear: Number(hdr?.aaiBear?.value ?? 0), + fedRate: String(hdr?.fedRate?.value ?? ''), + unavailable: false, + }; +} + +const CAT_NAMES = ['sentiment','volatility','positioning','trend','breadth','momentum','liquidity','credit','macro','crossAsset'] as const; + +const CAT_DISPLAY: Record = { + sentiment: 'Sentiment', + volatility: 'Volatility', + positioning: 'Positioning', + trend: 'Trend', + breadth: 'Breadth', + momentum: 'Momentum', + liquidity: 'Liquidity', + credit: 'Credit', + macro: 'Macro', + crossAsset: 'Cross-Asset', +}; + +export class FearGreedPanel extends Panel { + private data: FearGreedData | null = null; + private loading = true; + private error: string | null = null; + + constructor() { + super({ id: 'fear-greed', title: t('panels.fearGreed'), showCount: false, infoTooltip: 'Composite sentiment index: 10 weighted categories (volatility, positioning, breadth, momentum, liquidity, credit, macro, cross-asset, sentiment, trend).' }); + } + + public async fetchData(): Promise { + const hydrated = getHydratedData('fearGreedIndex') as Record | undefined; + if (hydrated && !hydrated.unavailable) { + const mapped = mapSeedPayload(hydrated); + if (mapped && mapped.compositeScore > 0) { + this.data = mapped; + this.loading = false; + this.error = null; + this.renderPanel(); + return true; + } + } + + try { + 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) { + this.error = 'Fear & Greed index unavailable'; + this.loading = false; + this.renderPanel(); + return false; + } + this.data = resp as FearGreedData; + this.loading = false; + this.error = null; + this.renderPanel(); + return true; + } catch (e) { + this.error = e instanceof Error ? e.message : 'Failed to load'; + this.loading = false; + this.renderPanel(); + return false; + } + } + + private renderPanel(): void { + if (this.loading) { + this.setContent('
Loading...
'); + return; + } + if (this.error || !this.data) { + this.setContent(`
${escapeHtml(this.error ?? 'Fear & Greed index unavailable')}
`); + return; + } + + const d = this.data; + const score = d.compositeScore; + const label = escapeHtml(d.compositeLabel); + const prev = d.previousScore; + const delta = prev > 0 ? score - prev : null; + const color = scoreColor(score); + + const catRows = CAT_NAMES.map(name => { + const c = d[name] as CategoryData | undefined; + if (!c) return ''; + const s = Math.round(c.score ?? 50); + const w = Math.round((c.weight ?? 0) * 100); + const contrib = (c.contribution ?? 0).toFixed(1); + const deg = c.degraded ? ' degraded' : ''; + const barColor = scoreColor(s); + const displayName = CAT_DISPLAY[name] ?? name; + return ` +
+
+ ${escapeHtml(displayName)}${deg} + ${s} +
+
+
+
+
${w}% weight · +${contrib} pts
+
`; + }).join(''); + + const deltaHtml = delta != null + ? `${delta >= 0 ? '+' : ''}${delta.toFixed(1)} vs prev` + : ''; + + const hdrMetric = (lbl: string, val: string) => + `
+
${escapeHtml(val)}
+
${escapeHtml(lbl)}
+
`; + + const hdr = [ + hdrMetric('VIX', d.vix > 0 ? fmt(d.vix, 2) : 'N/A'), + hdrMetric('HY Spread', d.hySpread > 0 ? `${fmt(d.hySpread, 2)}%` : 'N/A'), + hdrMetric('10Y Yield', d.yield10y > 0 ? `${fmt(d.yield10y, 2)}%` : 'N/A'), + hdrMetric('P/C Ratio', d.putCallRatio > 0 ? fmt(d.putCallRatio, 2) : 'N/A'), + hdrMetric('% > 200d', d.pctAbove200d ? `${fmt(d.pctAbove200d, 1)}%` : 'N/A'), + hdrMetric('CNN F&G', d.cnnFearGreed ? `${Math.round(d.cnnFearGreed)}` : 'N/A'), + hdrMetric('AAII Bull', d.aaiiBull ? `${fmt(d.aaiiBull, 1)}%` : 'N/A'), + hdrMetric('AAII Bear', d.aaiiBear ? `${fmt(d.aaiiBear, 1)}%` : 'N/A'), + hdrMetric('Fed Rate', d.fedRate || 'N/A'), + ].join(''); + + const html = ` +
+
+
${score}
+
${label}
+ ${deltaHtml} +
+
+ ${hdr} +
+
Category Breakdown
+ ${catRows} +
`; + + this.setContent(html); + } +} diff --git a/src/components/index.ts b/src/components/index.ts index fbabf1513..7441c95e0 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -70,3 +70,4 @@ export * from './EconomicCorrelationPanel'; export * from './DisasterCorrelationPanel'; export * from './ConsumerPricesPanel'; export { NationalDebtPanel } from './NationalDebtPanel'; +export * from './FearGreedPanel'; diff --git a/src/config/commands.ts b/src/config/commands.ts index 81f913134..0369d9845 100644 --- a/src/config/commands.ts +++ b/src/config/commands.ts @@ -121,6 +121,7 @@ export const COMMANDS: Command[] = [ { id: 'panel:heatmap', keywords: ['heatmap', 'sector heatmap'], label: 'Panel: Sector Heatmap', icon: '\u{1F5FA}\uFE0F', category: 'panels' }, { id: 'panel:ai', keywords: ['ai', 'ml', 'artificial intelligence'], label: 'Panel: AI/ML', icon: '\u{1F916}', category: 'panels' }, { id: 'panel:macro-signals', keywords: ['macro', 'macro signals', 'liquidity'], label: 'Panel: Market Radar', icon: '\u{1F4C9}', category: 'panels' }, + { id: 'panel:fear-greed', keywords: ['fear', 'greed', 'fear and greed', 'sentiment', 'fear greed index'], label: 'Panel: Fear & Greed', icon: '\u{1F4CA}', category: 'panels' }, { id: 'panel:etf-flows', keywords: ['etf', 'etf flows', 'fund flows'], label: 'Panel: BTC ETF Tracker', icon: '\u{1F4B9}', category: 'panels' }, { id: 'panel:stablecoins', keywords: ['stablecoins', 'usdt', 'usdc'], label: 'Panel: Stablecoins', icon: '\u{1FA99}', category: 'panels' }, { id: 'panel:monitors', keywords: ['monitors', 'my monitors', 'watchlist'], label: 'Panel: My Monitors', icon: '\u{1F4CB}', category: 'panels' }, diff --git a/src/config/panels.ts b/src/config/panels.ts index 7f8a99a95..cf572e900 100644 --- a/src/config/panels.ts +++ b/src/config/panels.ts @@ -55,6 +55,7 @@ const FULL_PANELS: Record = { monitors: { name: 'My Monitors', enabled: true, priority: 2 }, '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 }, 'gulf-economies': { name: 'Gulf Economies', enabled: false, priority: 2 }, 'consumer-prices': { name: 'Consumer Prices', enabled: false, priority: 2 }, 'grocery-basket': { name: 'Grocery Index', enabled: false, priority: 2 }, diff --git a/src/config/variants/base.ts b/src/config/variants/base.ts index f51293cd9..555203edc 100644 --- a/src/config/variants/base.ts +++ b/src/config/variants/base.ts @@ -36,6 +36,7 @@ export const REFRESH_INTERVALS = { stablecoins: 15 * 60 * 1000, etfFlows: 15 * 60 * 1000, macroSignals: 15 * 60 * 1000, + fearGreed: 30 * 60 * 1000, strategicPosture: 15 * 60 * 1000, strategicRisk: 5 * 60 * 1000, temporalBaseline: 10 * 60 * 1000, diff --git a/src/config/variants/finance.ts b/src/config/variants/finance.ts index 5c2a3802f..2ce61e85e 100644 --- a/src/config/variants/finance.ts +++ b/src/config/variants/finance.ts @@ -158,6 +158,7 @@ export const DEFAULT_PANELS: Record = { ipo: { name: 'IPOs, Earnings & M&A', enabled: true, priority: 1 }, heatmap: { name: 'Sector Heatmap', enabled: true, priority: 1 }, 'macro-signals': { name: 'Market Radar', enabled: true, priority: 1 }, + 'fear-greed': { name: 'Fear & Greed', enabled: true, priority: 1 }, 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/market/v1/service_client.ts b/src/generated/client/worldmonitor/market/v1/service_client.ts index 11b26ee96..211b412fa 100644 --- a/src/generated/client/worldmonitor/market/v1/service_client.ts +++ b/src/generated/client/worldmonitor/market/v1/service_client.ts @@ -328,6 +328,45 @@ export interface ListOtherTokensResponse { tokens: CryptoQuote[]; } +export interface GetFearGreedIndexRequest { +} + +export interface GetFearGreedIndexResponse { + compositeScore: number; + compositeLabel: string; + previousScore: number; + seededAt: string; + sentiment?: FearGreedCategory; + volatility?: FearGreedCategory; + positioning?: FearGreedCategory; + trend?: FearGreedCategory; + breadth?: FearGreedCategory; + momentum?: FearGreedCategory; + liquidity?: FearGreedCategory; + credit?: FearGreedCategory; + macro?: FearGreedCategory; + crossAsset?: FearGreedCategory; + vix: number; + hySpread: number; + yield10y: number; + putCallRatio: number; + pctAbove200d: number; + cnnFearGreed: number; + cnnLabel: string; + aaiiBull: number; + aaiiBear: number; + fedRate: string; + unavailable: boolean; +} + +export interface FearGreedCategory { + score: number; + weight: number; + contribution: number; + degraded: boolean; + inputsJson: string; +} + export interface FieldViolation { field: string; description: string; @@ -771,6 +810,29 @@ export class MarketServiceClient { return await resp.json() as ListOtherTokensResponse; } + async getFearGreedIndex(req: GetFearGreedIndexRequest, options?: MarketServiceCallOptions): Promise { + let path = "/api/market/v1/get-fear-greed-index"; + const url = this.baseURL + path; + + const headers: Record = { + "Content-Type": "application/json", + ...this.defaultHeaders, + ...options?.headers, + }; + + const resp = await this.fetchFn(url, { + method: "GET", + headers, + signal: options?.signal, + }); + + if (!resp.ok) { + return this.handleError(resp); + } + + return await resp.json() as GetFearGreedIndexResponse; + } + private async handleError(resp: Response): Promise { const body = await resp.text(); if (resp.status === 400) { diff --git a/src/generated/server/worldmonitor/market/v1/service_server.ts b/src/generated/server/worldmonitor/market/v1/service_server.ts index fad59a9af..2fda00ff9 100644 --- a/src/generated/server/worldmonitor/market/v1/service_server.ts +++ b/src/generated/server/worldmonitor/market/v1/service_server.ts @@ -328,6 +328,45 @@ export interface ListOtherTokensResponse { tokens: CryptoQuote[]; } +export interface GetFearGreedIndexRequest { +} + +export interface GetFearGreedIndexResponse { + compositeScore: number; + compositeLabel: string; + previousScore: number; + seededAt: string; + sentiment?: FearGreedCategory; + volatility?: FearGreedCategory; + positioning?: FearGreedCategory; + trend?: FearGreedCategory; + breadth?: FearGreedCategory; + momentum?: FearGreedCategory; + liquidity?: FearGreedCategory; + credit?: FearGreedCategory; + macro?: FearGreedCategory; + crossAsset?: FearGreedCategory; + vix: number; + hySpread: number; + yield10y: number; + putCallRatio: number; + pctAbove200d: number; + cnnFearGreed: number; + cnnLabel: string; + aaiiBull: number; + aaiiBear: number; + fedRate: string; + unavailable: boolean; +} + +export interface FearGreedCategory { + score: number; + weight: number; + contribution: number; + degraded: boolean; + inputsJson: string; +} + export interface FieldViolation { field: string; description: string; @@ -389,6 +428,7 @@ export interface MarketServiceHandler { listDefiTokens(ctx: ServerContext, req: ListDefiTokensRequest): Promise; listAiTokens(ctx: ServerContext, req: ListAiTokensRequest): Promise; listOtherTokens(ctx: ServerContext, req: ListOtherTokensRequest): Promise; + getFearGreedIndex(ctx: ServerContext, req: GetFearGreedIndexRequest): Promise; } export function createMarketServiceRoutes( @@ -1095,6 +1135,43 @@ export function createMarketServiceRoutes( } }, }, + { + method: "GET", + path: "/api/market/v1/get-fear-greed-index", + handler: async (req: Request): Promise => { + try { + const pathParams: Record = {}; + const body = {} as GetFearGreedIndexRequest; + + const ctx: ServerContext = { + request: req, + pathParams, + headers: Object.fromEntries(req.headers.entries()), + }; + + const result = await handler.getFearGreedIndex(ctx, body); + return new Response(JSON.stringify(result as GetFearGreedIndexResponse), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (err: unknown) { + if (err instanceof ValidationError) { + return new Response(JSON.stringify({ violations: err.violations }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + if (options?.onError) { + return options.onError(err, req); + } + const message = err instanceof Error ? err.message : String(err); + return new Response(JSON.stringify({ message }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } + }, + }, ]; } diff --git a/src/locales/ar.json b/src/locales/ar.json index 34fa922af..e251b6213 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -281,7 +281,9 @@ "gulfIndices": "مؤشرات الخليج", "gulfCurrencies": "عملات الخليج", "gulfOil": "نفط الخليج", - "bigmac": "Real Big Mac Index" + + "bigmac": "Real Big Mac Index", + "fearGreed": "Fear & Greed" }, "commands": { "prefixes": { diff --git a/src/locales/en.json b/src/locales/en.json index 8551fb97a..2be809dc5 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -373,7 +373,8 @@ "gulfCurrencies": "Gulf Currencies", "gulfOil": "Gulf Oil", "airlineIntel": "✈️ Airline Intelligence", - "consumerPrices": "Consumer Prices" + "consumerPrices": "Consumer Prices", + "fearGreed": "Fear & Greed" }, "commands": { "prefixes": { diff --git a/src/locales/es.json b/src/locales/es.json index 8d65672ec..70994b8cf 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -281,7 +281,9 @@ "gulfIndices": "Índices del Golfo", "gulfCurrencies": "Monedas del Golfo", "gulfOil": "Petróleo del Golfo", - "bigmac": "Real Big Mac Index" + + "bigmac": "Real Big Mac Index", + "fearGreed": "Fear & Greed" }, "commands": { "prefixes": { diff --git a/src/locales/zh.json b/src/locales/zh.json index c9c2718fb..5de295255 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -281,7 +281,9 @@ "gulfIndices": "海湾指数", "gulfCurrencies": "海湾货币", "gulfOil": "海湾石油", - "bigmac": "Real Big Mac Index" + + "bigmac": "Real Big Mac Index", + "fearGreed": "Fear & Greed" }, "commands": { "prefixes": {