diff --git a/api/health.js b/api/health.js index 8e5a8bfea..e7b9a5ea4 100644 --- a/api/health.js +++ b/api/health.js @@ -169,6 +169,7 @@ const STANDALONE_KEYS = { recoveryExternalDebt: 'resilience:recovery:external-debt:v1', recoveryImportHhi: 'resilience:recovery:import-hhi:v1', recoveryFuelStocks: 'resilience:recovery:fuel-stocks:v1', + goldExtended: 'market:gold-extended:v1', }; const SEED_META = { @@ -195,6 +196,7 @@ const SEED_META = { newsInsights: { key: 'seed-meta:news:insights', maxStaleMin: 30 }, marketQuotes: { key: 'seed-meta:market:stocks', maxStaleMin: 30 }, commodityQuotes: { key: 'seed-meta:market:commodities', maxStaleMin: 30 }, + goldExtended: { key: 'seed-meta:market:gold-extended', maxStaleMin: 30 }, // RPC/warm-ping keys — seed-meta written by relay loops or handlers // serviceStatuses: moved to ON_DEMAND — RPC-populated, no dedicated seed, goes stale when no users visit cableHealth: { key: 'seed-meta:cable-health', maxStaleMin: 90 }, // ais-relay warm-ping runs every 30min; 90min = 3× interval catches missed pings without false positives diff --git a/api/seed-health.js b/api/seed-health.js index cf8e98bea..f1169df64 100644 --- a/api/seed-health.js +++ b/api/seed-health.js @@ -30,6 +30,7 @@ const SEED_DOMAINS = { // Aligned with health.js SEED_META (intervalMin = maxStaleMin / 2) 'market:stocks': { key: 'seed-meta:market:stocks', intervalMin: 15 }, 'market:commodities': { key: 'seed-meta:market:commodities', intervalMin: 15 }, + 'market:gold-extended': { key: 'seed-meta:market:gold-extended', intervalMin: 15 }, 'market:sectors': { key: 'seed-meta:market:sectors', intervalMin: 15 }, 'aviation:faa': { key: 'seed-meta:aviation:faa', intervalMin: 45 }, 'news:insights': { key: 'seed-meta:news:insights', intervalMin: 15 }, diff --git a/docs/api/MarketService.openapi.json b/docs/api/MarketService.openapi.json index c41a81532..af4c960a1 100644 --- a/docs/api/MarketService.openapi.json +++ b/docs/api/MarketService.openapi.json @@ -1 +1 @@ -{"components":{"schemas":{"AnalystConsensus":{"properties":{"buy":{"format":"int32","type":"integer"},"hold":{"format":"int32","type":"integer"},"period":{"type":"string"},"sell":{"format":"int32","type":"integer"},"strongBuy":{"format":"int32","type":"integer"},"strongSell":{"format":"int32","type":"integer"},"total":{"format":"int32","type":"integer"}},"type":"object"},"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"},"analystConsensus":{"$ref":"#/components/schemas/AnalystConsensus"},"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"},"dividendCagr":{"format":"double","type":"number"},"dividendFrequency":{"type":"string"},"dividendYield":{"format":"double","type":"number"},"engineVersion":{"type":"string"},"exDividendDate":{"description":"Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"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"},"payoutRatio":{"format":"double","type":"number"},"priceTarget":{"$ref":"#/components/schemas/PriceTarget"},"provider":{"type":"string"},"recentUpgrades":{"items":{"$ref":"#/components/schemas/UpgradeDowngrade"},"type":"array"},"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"},"trailingAnnualDividendRate":{"format":"double","type":"number"},"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"},"BreadthSnapshot":{"properties":{"date":{"type":"string"},"pctAbove200d":{"format":"double","type":"number"},"pctAbove20d":{"description":"Optional so a missing/failed Barchart reading serializes as JSON null\n instead of collapsing to 0, which would render identically to a real 0%\n reading (severe market dislocation with no S\u0026P stocks above SMA).","format":"double","type":"number"},"pctAbove50d":{"format":"double","type":"number"}},"type":"object"},"CommodityQuote":{"description":"CommodityQuote represents a commodity price quote from Yahoo Finance.","properties":{"change":{"description":"Percentage change from previous close.","format":"double","type":"number"},"display":{"description":"Display label.","type":"string"},"name":{"description":"Human-readable name.","type":"string"},"price":{"description":"Current price.","format":"double","type":"number"},"sparkline":{"items":{"description":"Sparkline data points.","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Commodity symbol (e.g., \"CL=F\" for crude oil).","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"CotInstrument":{"properties":{"assetManagerLong":{"format":"int64","type":"string"},"assetManagerShort":{"format":"int64","type":"string"},"code":{"type":"string"},"dealerLong":{"format":"int64","type":"string"},"dealerShort":{"format":"int64","type":"string"},"leveragedFundsLong":{"format":"int64","type":"string"},"leveragedFundsShort":{"format":"int64","type":"string"},"name":{"type":"string"},"netPct":{"format":"double","type":"number"},"reportDate":{"type":"string"}},"type":"object"},"CryptoQuote":{"description":"CryptoQuote represents a cryptocurrency quote from CoinGecko.","properties":{"change":{"description":"24-hour percentage change.","format":"double","type":"number"},"change7d":{"description":"7-day percentage change.","format":"double","type":"number"},"name":{"description":"Cryptocurrency name (e.g., \"Bitcoin\").","type":"string"},"price":{"description":"Current price in USD.","format":"double","type":"number"},"sparkline":{"items":{"description":"Sparkline data points (recent price history).","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Ticker symbol (e.g., \"BTC\").","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"CryptoSector":{"description":"CryptoSector represents performance data for a crypto market sector.","properties":{"change":{"description":"Average 24h percentage change across sector tokens.","format":"double","type":"number"},"id":{"description":"Sector identifier.","type":"string"},"name":{"description":"Sector display name.","type":"string"}},"type":"object"},"EarningsEntry":{"properties":{"company":{"type":"string"},"date":{"type":"string"},"epsActual":{"format":"double","type":"number"},"epsEstimate":{"format":"double","type":"number"},"hasActuals":{"type":"boolean"},"hour":{"type":"string"},"revenueActual":{"format":"double","type":"number"},"revenueEstimate":{"format":"double","type":"number"},"surpriseDirection":{"type":"string"},"symbol":{"type":"string"}},"type":"object"},"Error":{"description":"Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.","properties":{"message":{"description":"Error message (e.g., 'user not found', 'database connection failed')","type":"string"}},"type":"object"},"EtfFlow":{"description":"EtfFlow represents a single ETF with estimated flow data.","properties":{"avgVolume":{"description":"Average volume over prior days.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"direction":{"description":"Flow direction: \"inflow\", \"outflow\", or \"neutral\".","type":"string"},"estFlow":{"description":"Estimated dollar flow magnitude.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"issuer":{"description":"Fund issuer (e.g. \"BlackRock\").","type":"string"},"price":{"description":"Latest closing price.","format":"double","type":"number"},"priceChange":{"description":"Day-over-day price change percentage.","format":"double","type":"number"},"ticker":{"description":"Ticker symbol (e.g. \"IBIT\").","minLength":1,"type":"string"},"volume":{"description":"Latest daily volume.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"volumeRatio":{"description":"Volume ratio (latest / average).","format":"double","type":"number"}},"required":["ticker"],"type":"object"},"EtfFlowsSummary":{"description":"EtfFlowsSummary contains aggregate ETF flow stats.","properties":{"etfCount":{"description":"Number of ETFs with data.","format":"int32","type":"integer"},"inflowCount":{"description":"Number of ETFs with inflow.","format":"int32","type":"integer"},"netDirection":{"description":"Net direction: \"NET INFLOW\", \"NET OUTFLOW\", or \"NEUTRAL\".","type":"string"},"outflowCount":{"description":"Number of ETFs with outflow.","format":"int32","type":"integer"},"totalEstFlow":{"description":"Total estimated flow across all ETFs.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"totalVolume":{"description":"Total volume across all ETFs.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"}},"type":"object"},"FearGreedCategory":{"properties":{"contribution":{"format":"double","type":"number"},"degraded":{"type":"boolean"},"inputsJson":{"type":"string"},"score":{"format":"double","type":"number"},"weight":{"format":"double","type":"number"}},"type":"object"},"FearGreedSectorPerformance":{"properties":{"change1d":{"format":"double","type":"number"},"name":{"type":"string"},"symbol":{"type":"string"}},"type":"object"},"FieldViolation":{"description":"FieldViolation describes a single validation error for a specific field.","properties":{"description":{"description":"Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')","type":"string"},"field":{"description":"The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')","type":"string"}},"required":["field","description"],"type":"object"},"GetCotPositioningRequest":{"type":"object"},"GetCotPositioningResponse":{"properties":{"instruments":{"items":{"$ref":"#/components/schemas/CotInstrument"},"type":"array"},"reportDate":{"type":"string"},"unavailable":{"type":"boolean"}},"type":"object"},"GetCountryStockIndexRequest":{"description":"GetCountryStockIndexRequest specifies which country's stock index to retrieve.","properties":{"countryCode":{"description":"ISO 3166-1 alpha-2 country code (e.g., \"US\", \"GB\", \"JP\").","pattern":"^[A-Z]{2}$","type":"string"}},"required":["countryCode"],"type":"object"},"GetCountryStockIndexResponse":{"description":"GetCountryStockIndexResponse contains the country's primary stock index data.","properties":{"available":{"description":"Whether stock index data is available for this country.","type":"boolean"},"code":{"description":"ISO 3166-1 alpha-2 country code.","type":"string"},"currency":{"description":"Currency of the index.","type":"string"},"fetchedAt":{"description":"When the data was fetched (ISO 8601).","type":"string"},"indexName":{"description":"Index name (e.g., \"S\u0026P 500\").","type":"string"},"price":{"description":"Latest closing price.","format":"double","type":"number"},"symbol":{"description":"Ticker symbol (e.g., \"^GSPC\").","type":"string"},"weekChangePercent":{"description":"Weekly change percentage.","format":"double","type":"number"}},"type":"object"},"GetFearGreedIndexRequest":{"type":"object"},"GetFearGreedIndexResponse":{"properties":{"aaiiBear":{"format":"double","type":"number"},"aaiiBull":{"format":"double","type":"number"},"breadth":{"$ref":"#/components/schemas/FearGreedCategory"},"cnnFearGreed":{"format":"double","type":"number"},"cnnLabel":{"type":"string"},"compositeLabel":{"type":"string"},"compositeScore":{"format":"double","type":"number"},"credit":{"$ref":"#/components/schemas/FearGreedCategory"},"crossAsset":{"$ref":"#/components/schemas/FearGreedCategory"},"fedRate":{"type":"string"},"fsiLabel":{"type":"string"},"fsiValue":{"format":"double","type":"number"},"hySpread":{"format":"double","type":"number"},"hygPrice":{"format":"double","type":"number"},"liquidity":{"$ref":"#/components/schemas/FearGreedCategory"},"macro":{"$ref":"#/components/schemas/FearGreedCategory"},"momentum":{"$ref":"#/components/schemas/FearGreedCategory"},"pctAbove200d":{"format":"double","type":"number"},"positioning":{"$ref":"#/components/schemas/FearGreedCategory"},"previousScore":{"format":"double","type":"number"},"putCallRatio":{"format":"double","type":"number"},"sectorPerformance":{"items":{"$ref":"#/components/schemas/FearGreedSectorPerformance"},"type":"array"},"seededAt":{"type":"string"},"sentiment":{"$ref":"#/components/schemas/FearGreedCategory"},"tltPrice":{"format":"double","type":"number"},"trend":{"$ref":"#/components/schemas/FearGreedCategory"},"unavailable":{"type":"boolean"},"vix":{"format":"double","type":"number"},"volatility":{"$ref":"#/components/schemas/FearGreedCategory"},"yield10y":{"format":"double","type":"number"}},"type":"object"},"GetGoldIntelligenceRequest":{"type":"object"},"GetGoldIntelligenceResponse":{"properties":{"cot":{"$ref":"#/components/schemas/GoldCotPositioning"},"crossCurrencyPrices":{"items":{"$ref":"#/components/schemas/GoldCrossCurrencyPrice"},"type":"array"},"goldChangePct":{"format":"double","type":"number"},"goldPlatinumPremiumPct":{"format":"double","type":"number"},"goldPrice":{"format":"double","type":"number"},"goldSilverRatio":{"format":"double","type":"number"},"goldSparkline":{"items":{"format":"double","type":"number"},"type":"array"},"palladiumPrice":{"format":"double","type":"number"},"platinumPrice":{"format":"double","type":"number"},"silverPrice":{"format":"double","type":"number"},"unavailable":{"type":"boolean"},"updatedAt":{"type":"string"}},"type":"object"},"GetInsiderTransactionsRequest":{"properties":{"symbol":{"maxLength":32,"minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"GetInsiderTransactionsResponse":{"properties":{"fetchedAt":{"type":"string"},"netValue":{"format":"double","type":"number"},"symbol":{"type":"string"},"totalBuys":{"format":"double","type":"number"},"totalSells":{"format":"double","type":"number"},"transactions":{"items":{"$ref":"#/components/schemas/InsiderTransaction"},"type":"array"},"unavailable":{"type":"boolean"}},"type":"object"},"GetMarketBreadthHistoryRequest":{"type":"object"},"GetMarketBreadthHistoryResponse":{"properties":{"currentPctAbove200d":{"format":"double","type":"number"},"currentPctAbove20d":{"format":"double","type":"number"},"currentPctAbove50d":{"format":"double","type":"number"},"history":{"items":{"$ref":"#/components/schemas/BreadthSnapshot"},"type":"array"},"unavailable":{"type":"boolean"},"updatedAt":{"type":"string"}},"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"},"GoldCotPositioning":{"properties":{"dealerLong":{"format":"double","type":"number"},"dealerShort":{"format":"double","type":"number"},"managedMoneyLong":{"format":"double","type":"number"},"managedMoneyShort":{"format":"double","type":"number"},"netPct":{"format":"double","type":"number"},"reportDate":{"type":"string"}},"type":"object"},"GoldCrossCurrencyPrice":{"properties":{"currency":{"type":"string"},"flag":{"type":"string"},"price":{"format":"double","type":"number"}},"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"},"InsiderTransaction":{"properties":{"name":{"type":"string"},"shares":{"description":"Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"transactionCode":{"type":"string"},"transactionDate":{"type":"string"},"value":{"format":"double","type":"number"}},"type":"object"},"ListAiTokensRequest":{"description":"ListAiTokensRequest retrieves AI crypto token prices.","type":"object"},"ListAiTokensResponse":{"description":"ListAiTokensResponse contains AI token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListCommodityQuotesRequest":{"description":"ListCommodityQuotesRequest specifies which commodities to retrieve.","properties":{"symbols":{"items":{"description":"Commodity symbols to retrieve (Yahoo symbols). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListCommodityQuotesResponse":{"description":"ListCommodityQuotesResponse contains commodity quotes.","properties":{"quotes":{"items":{"$ref":"#/components/schemas/CommodityQuote"},"type":"array"}},"type":"object"},"ListCryptoQuotesRequest":{"description":"ListCryptoQuotesRequest specifies which cryptocurrencies to retrieve.","properties":{"ids":{"items":{"description":"Cryptocurrency IDs to retrieve (CoinGecko IDs). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListCryptoQuotesResponse":{"description":"ListCryptoQuotesResponse contains cryptocurrency quotes.","properties":{"quotes":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListCryptoSectorsRequest":{"description":"ListCryptoSectorsRequest retrieves crypto sector performance.","type":"object"},"ListCryptoSectorsResponse":{"description":"ListCryptoSectorsResponse contains crypto sector performance data.","properties":{"sectors":{"items":{"$ref":"#/components/schemas/CryptoSector"},"type":"array"}},"type":"object"},"ListDefiTokensRequest":{"description":"ListDefiTokensRequest retrieves DeFi token prices.","type":"object"},"ListDefiTokensResponse":{"description":"ListDefiTokensResponse contains DeFi token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListEarningsCalendarRequest":{"properties":{"fromDate":{"type":"string"},"toDate":{"type":"string"}},"type":"object"},"ListEarningsCalendarResponse":{"properties":{"earnings":{"items":{"$ref":"#/components/schemas/EarningsEntry"},"type":"array"},"fromDate":{"type":"string"},"toDate":{"type":"string"},"total":{"format":"int32","type":"integer"},"unavailable":{"type":"boolean"}},"type":"object"},"ListEtfFlowsRequest":{"description":"ListEtfFlowsRequest is empty; the handler uses a fixed list of BTC spot ETFs.","type":"object"},"ListEtfFlowsResponse":{"description":"ListEtfFlowsResponse contains BTC spot ETF flow data.","properties":{"etfs":{"items":{"$ref":"#/components/schemas/EtfFlow"},"type":"array"},"rateLimited":{"description":"True when the upstream API rate-limited the request.","type":"boolean"},"summary":{"$ref":"#/components/schemas/EtfFlowsSummary"},"timestamp":{"description":"Timestamp of the data fetch (ISO 8601).","type":"string"}},"type":"object"},"ListGulfQuotesRequest":{"type":"object"},"ListGulfQuotesResponse":{"properties":{"quotes":{"items":{"$ref":"#/components/schemas/GulfQuote"},"type":"array"},"rateLimited":{"type":"boolean"}},"type":"object"},"ListMarketQuotesRequest":{"description":"ListMarketQuotesRequest specifies which stock/index symbols to retrieve.","properties":{"symbols":{"items":{"description":"Ticker symbols to retrieve (e.g., [\"AAPL\", \"^GSPC\"]). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListMarketQuotesResponse":{"description":"ListMarketQuotesResponse contains stock and index quotes.","properties":{"finnhubSkipped":{"description":"True when the Finnhub API key is not configured and stock quotes were skipped.","type":"boolean"},"quotes":{"items":{"$ref":"#/components/schemas/MarketQuote"},"type":"array"},"rateLimited":{"description":"True when the upstream API rate-limited the request.","type":"boolean"},"skipReason":{"description":"Human-readable reason when Finnhub was skipped (e.g., \"FINNHUB_API_KEY not configured\").","type":"string"}},"type":"object"},"ListOtherTokensRequest":{"description":"ListOtherTokensRequest retrieves other/trending crypto token prices.","type":"object"},"ListOtherTokensResponse":{"description":"ListOtherTokensResponse contains other token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListStablecoinMarketsRequest":{"description":"ListStablecoinMarketsRequest specifies which stablecoins to retrieve.","properties":{"coins":{"items":{"description":"CoinGecko IDs to retrieve (e.g. \"tether,usd-coin\"). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListStablecoinMarketsResponse":{"description":"ListStablecoinMarketsResponse contains stablecoin market data.","properties":{"stablecoins":{"items":{"$ref":"#/components/schemas/Stablecoin"},"type":"array"},"summary":{"$ref":"#/components/schemas/StablecoinSummary"},"timestamp":{"description":"Timestamp of the data fetch (ISO 8601).","type":"string"}},"type":"object"},"ListStoredStockBacktestsRequest":{"properties":{"evalWindowDays":{"format":"int32","maximum":30,"minimum":3,"type":"integer"},"symbols":{"items":{"type":"string"},"type":"array"}},"type":"object"},"ListStoredStockBacktestsResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/BacktestStockResponse"},"type":"array"}},"type":"object"},"MarketQuote":{"description":"MarketQuote represents a stock or index quote from Finnhub or Yahoo Finance.","properties":{"change":{"description":"Percentage change from previous close.","format":"double","type":"number"},"display":{"description":"Display label.","type":"string"},"name":{"description":"Human-readable name.","type":"string"},"price":{"description":"Current price.","format":"double","type":"number"},"sparkline":{"items":{"description":"Sparkline data points (recent price history).","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Ticker symbol (e.g., \"AAPL\", \"^GSPC\").","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"PriceTarget":{"properties":{"current":{"format":"double","type":"number"},"high":{"format":"double","type":"number"},"low":{"format":"double","type":"number"},"mean":{"format":"double","type":"number"},"median":{"format":"double","type":"number"},"numberOfAnalysts":{"format":"int32","type":"integer"}},"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"},"UpgradeDowngrade":{"properties":{"action":{"type":"string"},"epochGradeDate":{"description":"Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"firm":{"type":"string"},"fromGrade":{"type":"string"},"toGrade":{"type":"string"}},"type":"object"},"ValidationError":{"description":"ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.","properties":{"violations":{"description":"List of validation violations","items":{"$ref":"#/components/schemas/FieldViolation"},"type":"array"}},"required":["violations"],"type":"object"}}},"info":{"title":"MarketService API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/market/v1/analyze-stock":{"get":{"description":"AnalyzeStock retrieves a premium stock analysis report with technicals, news, and AI synthesis.","operationId":"AnalyzeStock","parameters":[{"in":"query","name":"symbol","required":false,"schema":{"type":"string"}},{"in":"query","name":"name","required":false,"schema":{"type":"string"}},{"in":"query","name":"include_news","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyzeStockResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"AnalyzeStock","tags":["MarketService"]}},"/api/market/v1/backtest-stock":{"get":{"description":"BacktestStock replays premium stock-analysis signals over recent price history.","operationId":"BacktestStock","parameters":[{"in":"query","name":"symbol","required":false,"schema":{"type":"string"}},{"in":"query","name":"name","required":false,"schema":{"type":"string"}},{"in":"query","name":"eval_window_days","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BacktestStockResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"BacktestStock","tags":["MarketService"]}},"/api/market/v1/get-cot-positioning":{"get":{"description":"GetCotPositioning retrieves CFTC COT institutional positioning data.","operationId":"GetCotPositioning","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCotPositioningResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCotPositioning","tags":["MarketService"]}},"/api/market/v1/get-country-stock-index":{"get":{"description":"GetCountryStockIndex retrieves the primary stock index for a country from Yahoo Finance.","operationId":"GetCountryStockIndex","parameters":[{"description":"ISO 3166-1 alpha-2 country code (e.g., \"US\", \"GB\", \"JP\").","in":"query","name":"country_code","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCountryStockIndexResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCountryStockIndex","tags":["MarketService"]}},"/api/market/v1/get-fear-greed-index":{"get":{"description":"GetFearGreedIndex retrieves the composite Fear \u0026 Greed sentiment index.","operationId":"GetFearGreedIndex","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetFearGreedIndexResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetFearGreedIndex","tags":["MarketService"]}},"/api/market/v1/get-gold-intelligence":{"get":{"description":"GetGoldIntelligence retrieves gold pricing, cross-currency XAU, ratios, and CFTC positioning.","operationId":"GetGoldIntelligence","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetGoldIntelligenceResponse"}}},"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":"GetGoldIntelligence","tags":["MarketService"]}},"/api/market/v1/get-insider-transactions":{"get":{"description":"GetInsiderTransactions retrieves SEC insider buy/sell activity from Finnhub.","operationId":"GetInsiderTransactions","parameters":[{"in":"query","name":"symbol","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetInsiderTransactionsResponse"}}},"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":"GetInsiderTransactions","tags":["MarketService"]}},"/api/market/v1/get-market-breadth-history":{"get":{"description":"GetMarketBreadthHistory retrieves historical % of S\u0026P 500 stocks above 20/50/200-day SMAs.","operationId":"GetMarketBreadthHistory","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetMarketBreadthHistoryResponse"}}},"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":"GetMarketBreadthHistory","tags":["MarketService"]}},"/api/market/v1/get-sector-summary":{"get":{"description":"GetSectorSummary retrieves market sector performance data from Finnhub.","operationId":"GetSectorSummary","parameters":[{"description":"Time period for performance calculation (e.g., \"1d\", \"1w\", \"1m\"). Defaults to \"1d\".","in":"query","name":"period","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSectorSummaryResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetSectorSummary","tags":["MarketService"]}},"/api/market/v1/get-stock-analysis-history":{"get":{"description":"GetStockAnalysisHistory retrieves shared premium stock analysis history from the backend store.","operationId":"GetStockAnalysisHistory","parameters":[{"in":"query","name":"symbols","required":false,"schema":{"type":"string"}},{"in":"query","name":"limit_per_symbol","required":false,"schema":{"format":"int32","type":"integer"}},{"in":"query","name":"include_news","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetStockAnalysisHistoryResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetStockAnalysisHistory","tags":["MarketService"]}},"/api/market/v1/list-ai-tokens":{"get":{"description":"ListAiTokens retrieves AI-focused crypto token prices and changes.","operationId":"ListAiTokens","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListAiTokensResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListAiTokens","tags":["MarketService"]}},"/api/market/v1/list-commodity-quotes":{"get":{"description":"ListCommodityQuotes retrieves commodity price quotes from Yahoo Finance.","operationId":"ListCommodityQuotes","parameters":[{"description":"Commodity symbols to retrieve (Yahoo symbols). Empty returns defaults.","in":"query","name":"symbols","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCommodityQuotesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListCommodityQuotes","tags":["MarketService"]}},"/api/market/v1/list-crypto-quotes":{"get":{"description":"ListCryptoQuotes retrieves cryptocurrency quotes from CoinGecko.","operationId":"ListCryptoQuotes","parameters":[{"description":"Cryptocurrency IDs to retrieve (CoinGecko IDs). Empty returns defaults.","in":"query","name":"ids","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCryptoQuotesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListCryptoQuotes","tags":["MarketService"]}},"/api/market/v1/list-crypto-sectors":{"get":{"description":"ListCryptoSectors retrieves crypto sector performance averages.","operationId":"ListCryptoSectors","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCryptoSectorsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListCryptoSectors","tags":["MarketService"]}},"/api/market/v1/list-defi-tokens":{"get":{"description":"ListDefiTokens retrieves DeFi token prices and changes.","operationId":"ListDefiTokens","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListDefiTokensResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListDefiTokens","tags":["MarketService"]}},"/api/market/v1/list-earnings-calendar":{"get":{"description":"ListEarningsCalendar retrieves upcoming and recent earnings releases.","operationId":"ListEarningsCalendar","parameters":[{"in":"query","name":"fromDate","required":false,"schema":{"type":"string"}},{"in":"query","name":"toDate","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEarningsCalendarResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListEarningsCalendar","tags":["MarketService"]}},"/api/market/v1/list-etf-flows":{"get":{"description":"ListEtfFlows retrieves BTC spot ETF flow estimates from Yahoo Finance.","operationId":"ListEtfFlows","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEtfFlowsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListEtfFlows","tags":["MarketService"]}},"/api/market/v1/list-gulf-quotes":{"get":{"description":"ListGulfQuotes retrieves Gulf region market quotes (indices, currencies, oil).","operationId":"ListGulfQuotes","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListGulfQuotesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListGulfQuotes","tags":["MarketService"]}},"/api/market/v1/list-market-quotes":{"get":{"description":"ListMarketQuotes retrieves stock and index quotes.","operationId":"ListMarketQuotes","parameters":[{"description":"Ticker symbols to retrieve (e.g., [\"AAPL\", \"^GSPC\"]). Empty returns defaults.","in":"query","name":"symbols","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListMarketQuotesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListMarketQuotes","tags":["MarketService"]}},"/api/market/v1/list-other-tokens":{"get":{"description":"ListOtherTokens retrieves other/trending crypto token prices and changes.","operationId":"ListOtherTokens","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListOtherTokensResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListOtherTokens","tags":["MarketService"]}},"/api/market/v1/list-stablecoin-markets":{"get":{"description":"ListStablecoinMarkets retrieves stablecoin peg health and market data from CoinGecko.","operationId":"ListStablecoinMarkets","parameters":[{"description":"CoinGecko IDs to retrieve (e.g. \"tether,usd-coin\"). Empty returns defaults.","in":"query","name":"coins","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListStablecoinMarketsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListStablecoinMarkets","tags":["MarketService"]}},"/api/market/v1/list-stored-stock-backtests":{"get":{"description":"ListStoredStockBacktests retrieves stored premium backtest snapshots from the backend store.","operationId":"ListStoredStockBacktests","parameters":[{"in":"query","name":"symbols","required":false,"schema":{"type":"string"}},{"in":"query","name":"eval_window_days","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListStoredStockBacktestsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListStoredStockBacktests","tags":["MarketService"]}}}} \ No newline at end of file +{"components":{"schemas":{"AnalystConsensus":{"properties":{"buy":{"format":"int32","type":"integer"},"hold":{"format":"int32","type":"integer"},"period":{"type":"string"},"sell":{"format":"int32","type":"integer"},"strongBuy":{"format":"int32","type":"integer"},"strongSell":{"format":"int32","type":"integer"},"total":{"format":"int32","type":"integer"}},"type":"object"},"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"},"analystConsensus":{"$ref":"#/components/schemas/AnalystConsensus"},"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"},"dividendCagr":{"format":"double","type":"number"},"dividendFrequency":{"type":"string"},"dividendYield":{"format":"double","type":"number"},"engineVersion":{"type":"string"},"exDividendDate":{"description":"Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"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"},"payoutRatio":{"format":"double","type":"number"},"priceTarget":{"$ref":"#/components/schemas/PriceTarget"},"provider":{"type":"string"},"recentUpgrades":{"items":{"$ref":"#/components/schemas/UpgradeDowngrade"},"type":"array"},"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"},"trailingAnnualDividendRate":{"format":"double","type":"number"},"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"},"BreadthSnapshot":{"properties":{"date":{"type":"string"},"pctAbove200d":{"format":"double","type":"number"},"pctAbove20d":{"description":"Optional so a missing/failed Barchart reading serializes as JSON null\n instead of collapsing to 0, which would render identically to a real 0%\n reading (severe market dislocation with no S\u0026P stocks above SMA).","format":"double","type":"number"},"pctAbove50d":{"format":"double","type":"number"}},"type":"object"},"CommodityQuote":{"description":"CommodityQuote represents a commodity price quote from Yahoo Finance.","properties":{"change":{"description":"Percentage change from previous close.","format":"double","type":"number"},"display":{"description":"Display label.","type":"string"},"name":{"description":"Human-readable name.","type":"string"},"price":{"description":"Current price.","format":"double","type":"number"},"sparkline":{"items":{"description":"Sparkline data points.","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Commodity symbol (e.g., \"CL=F\" for crude oil).","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"CotInstrument":{"properties":{"assetManagerLong":{"format":"int64","type":"string"},"assetManagerShort":{"format":"int64","type":"string"},"code":{"type":"string"},"dealerLong":{"format":"int64","type":"string"},"dealerShort":{"format":"int64","type":"string"},"leveragedFundsLong":{"format":"int64","type":"string"},"leveragedFundsShort":{"format":"int64","type":"string"},"name":{"type":"string"},"netPct":{"format":"double","type":"number"},"reportDate":{"type":"string"}},"type":"object"},"CryptoQuote":{"description":"CryptoQuote represents a cryptocurrency quote from CoinGecko.","properties":{"change":{"description":"24-hour percentage change.","format":"double","type":"number"},"change7d":{"description":"7-day percentage change.","format":"double","type":"number"},"name":{"description":"Cryptocurrency name (e.g., \"Bitcoin\").","type":"string"},"price":{"description":"Current price in USD.","format":"double","type":"number"},"sparkline":{"items":{"description":"Sparkline data points (recent price history).","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Ticker symbol (e.g., \"BTC\").","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"CryptoSector":{"description":"CryptoSector represents performance data for a crypto market sector.","properties":{"change":{"description":"Average 24h percentage change across sector tokens.","format":"double","type":"number"},"id":{"description":"Sector identifier.","type":"string"},"name":{"description":"Sector display name.","type":"string"}},"type":"object"},"EarningsEntry":{"properties":{"company":{"type":"string"},"date":{"type":"string"},"epsActual":{"format":"double","type":"number"},"epsEstimate":{"format":"double","type":"number"},"hasActuals":{"type":"boolean"},"hour":{"type":"string"},"revenueActual":{"format":"double","type":"number"},"revenueEstimate":{"format":"double","type":"number"},"surpriseDirection":{"type":"string"},"symbol":{"type":"string"}},"type":"object"},"Error":{"description":"Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.","properties":{"message":{"description":"Error message (e.g., 'user not found', 'database connection failed')","type":"string"}},"type":"object"},"EtfFlow":{"description":"EtfFlow represents a single ETF with estimated flow data.","properties":{"avgVolume":{"description":"Average volume over prior days.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"direction":{"description":"Flow direction: \"inflow\", \"outflow\", or \"neutral\".","type":"string"},"estFlow":{"description":"Estimated dollar flow magnitude.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"issuer":{"description":"Fund issuer (e.g. \"BlackRock\").","type":"string"},"price":{"description":"Latest closing price.","format":"double","type":"number"},"priceChange":{"description":"Day-over-day price change percentage.","format":"double","type":"number"},"ticker":{"description":"Ticker symbol (e.g. \"IBIT\").","minLength":1,"type":"string"},"volume":{"description":"Latest daily volume.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"volumeRatio":{"description":"Volume ratio (latest / average).","format":"double","type":"number"}},"required":["ticker"],"type":"object"},"EtfFlowsSummary":{"description":"EtfFlowsSummary contains aggregate ETF flow stats.","properties":{"etfCount":{"description":"Number of ETFs with data.","format":"int32","type":"integer"},"inflowCount":{"description":"Number of ETFs with inflow.","format":"int32","type":"integer"},"netDirection":{"description":"Net direction: \"NET INFLOW\", \"NET OUTFLOW\", or \"NEUTRAL\".","type":"string"},"outflowCount":{"description":"Number of ETFs with outflow.","format":"int32","type":"integer"},"totalEstFlow":{"description":"Total estimated flow across all ETFs.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"totalVolume":{"description":"Total volume across all ETFs.. Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"}},"type":"object"},"FearGreedCategory":{"properties":{"contribution":{"format":"double","type":"number"},"degraded":{"type":"boolean"},"inputsJson":{"type":"string"},"score":{"format":"double","type":"number"},"weight":{"format":"double","type":"number"}},"type":"object"},"FearGreedSectorPerformance":{"properties":{"change1d":{"format":"double","type":"number"},"name":{"type":"string"},"symbol":{"type":"string"}},"type":"object"},"FieldViolation":{"description":"FieldViolation describes a single validation error for a specific field.","properties":{"description":{"description":"Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')","type":"string"},"field":{"description":"The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')","type":"string"}},"required":["field","description"],"type":"object"},"GetCotPositioningRequest":{"type":"object"},"GetCotPositioningResponse":{"properties":{"instruments":{"items":{"$ref":"#/components/schemas/CotInstrument"},"type":"array"},"reportDate":{"type":"string"},"unavailable":{"type":"boolean"}},"type":"object"},"GetCountryStockIndexRequest":{"description":"GetCountryStockIndexRequest specifies which country's stock index to retrieve.","properties":{"countryCode":{"description":"ISO 3166-1 alpha-2 country code (e.g., \"US\", \"GB\", \"JP\").","pattern":"^[A-Z]{2}$","type":"string"}},"required":["countryCode"],"type":"object"},"GetCountryStockIndexResponse":{"description":"GetCountryStockIndexResponse contains the country's primary stock index data.","properties":{"available":{"description":"Whether stock index data is available for this country.","type":"boolean"},"code":{"description":"ISO 3166-1 alpha-2 country code.","type":"string"},"currency":{"description":"Currency of the index.","type":"string"},"fetchedAt":{"description":"When the data was fetched (ISO 8601).","type":"string"},"indexName":{"description":"Index name (e.g., \"S\u0026P 500\").","type":"string"},"price":{"description":"Latest closing price.","format":"double","type":"number"},"symbol":{"description":"Ticker symbol (e.g., \"^GSPC\").","type":"string"},"weekChangePercent":{"description":"Weekly change percentage.","format":"double","type":"number"}},"type":"object"},"GetFearGreedIndexRequest":{"type":"object"},"GetFearGreedIndexResponse":{"properties":{"aaiiBear":{"format":"double","type":"number"},"aaiiBull":{"format":"double","type":"number"},"breadth":{"$ref":"#/components/schemas/FearGreedCategory"},"cnnFearGreed":{"format":"double","type":"number"},"cnnLabel":{"type":"string"},"compositeLabel":{"type":"string"},"compositeScore":{"format":"double","type":"number"},"credit":{"$ref":"#/components/schemas/FearGreedCategory"},"crossAsset":{"$ref":"#/components/schemas/FearGreedCategory"},"fedRate":{"type":"string"},"fsiLabel":{"type":"string"},"fsiValue":{"format":"double","type":"number"},"hySpread":{"format":"double","type":"number"},"hygPrice":{"format":"double","type":"number"},"liquidity":{"$ref":"#/components/schemas/FearGreedCategory"},"macro":{"$ref":"#/components/schemas/FearGreedCategory"},"momentum":{"$ref":"#/components/schemas/FearGreedCategory"},"pctAbove200d":{"format":"double","type":"number"},"positioning":{"$ref":"#/components/schemas/FearGreedCategory"},"previousScore":{"format":"double","type":"number"},"putCallRatio":{"format":"double","type":"number"},"sectorPerformance":{"items":{"$ref":"#/components/schemas/FearGreedSectorPerformance"},"type":"array"},"seededAt":{"type":"string"},"sentiment":{"$ref":"#/components/schemas/FearGreedCategory"},"tltPrice":{"format":"double","type":"number"},"trend":{"$ref":"#/components/schemas/FearGreedCategory"},"unavailable":{"type":"boolean"},"vix":{"format":"double","type":"number"},"volatility":{"$ref":"#/components/schemas/FearGreedCategory"},"yield10y":{"format":"double","type":"number"}},"type":"object"},"GetGoldIntelligenceRequest":{"type":"object"},"GetGoldIntelligenceResponse":{"properties":{"cot":{"$ref":"#/components/schemas/GoldCotPositioning"},"crossCurrencyPrices":{"items":{"$ref":"#/components/schemas/GoldCrossCurrencyPrice"},"type":"array"},"drivers":{"items":{"$ref":"#/components/schemas/GoldDriver"},"type":"array"},"goldChangePct":{"format":"double","type":"number"},"goldPlatinumPremiumPct":{"format":"double","type":"number"},"goldPrice":{"format":"double","type":"number"},"goldSilverRatio":{"format":"double","type":"number"},"goldSparkline":{"items":{"format":"double","type":"number"},"type":"array"},"palladiumPrice":{"format":"double","type":"number"},"platinumPrice":{"format":"double","type":"number"},"range52w":{"$ref":"#/components/schemas/GoldRange52w"},"returns":{"$ref":"#/components/schemas/GoldReturns"},"session":{"$ref":"#/components/schemas/GoldSessionRange"},"silverPrice":{"format":"double","type":"number"},"unavailable":{"type":"boolean"},"updatedAt":{"type":"string"}},"type":"object"},"GetInsiderTransactionsRequest":{"properties":{"symbol":{"maxLength":32,"minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"GetInsiderTransactionsResponse":{"properties":{"fetchedAt":{"type":"string"},"netValue":{"format":"double","type":"number"},"symbol":{"type":"string"},"totalBuys":{"format":"double","type":"number"},"totalSells":{"format":"double","type":"number"},"transactions":{"items":{"$ref":"#/components/schemas/InsiderTransaction"},"type":"array"},"unavailable":{"type":"boolean"}},"type":"object"},"GetMarketBreadthHistoryRequest":{"type":"object"},"GetMarketBreadthHistoryResponse":{"properties":{"currentPctAbove200d":{"format":"double","type":"number"},"currentPctAbove20d":{"format":"double","type":"number"},"currentPctAbove50d":{"format":"double","type":"number"},"history":{"items":{"$ref":"#/components/schemas/BreadthSnapshot"},"type":"array"},"unavailable":{"type":"boolean"},"updatedAt":{"type":"string"}},"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"},"GoldCotCategory":{"properties":{"longPositions":{"format":"int64","type":"string"},"netPct":{"format":"double","type":"number"},"oiSharePct":{"format":"double","type":"number"},"shortPositions":{"format":"int64","type":"string"},"wowNetDelta":{"format":"int64","type":"string"}},"type":"object"},"GoldCotPositioning":{"properties":{"managedMoney":{"$ref":"#/components/schemas/GoldCotCategory"},"nextReleaseDate":{"type":"string"},"openInterest":{"format":"int64","type":"string"},"producerSwap":{"$ref":"#/components/schemas/GoldCotCategory"},"reportDate":{"type":"string"}},"type":"object"},"GoldCrossCurrencyPrice":{"properties":{"currency":{"type":"string"},"flag":{"type":"string"},"price":{"format":"double","type":"number"}},"type":"object"},"GoldDriver":{"properties":{"changePct":{"format":"double","type":"number"},"correlation30d":{"format":"double","type":"number"},"label":{"type":"string"},"symbol":{"type":"string"},"value":{"format":"double","type":"number"}},"type":"object"},"GoldRange52w":{"properties":{"hi":{"format":"double","type":"number"},"lo":{"format":"double","type":"number"},"positionPct":{"format":"double","type":"number"}},"type":"object"},"GoldReturns":{"properties":{"m1":{"format":"double","type":"number"},"w1":{"format":"double","type":"number"},"y1":{"format":"double","type":"number"},"ytd":{"format":"double","type":"number"}},"type":"object"},"GoldSessionRange":{"properties":{"dayHigh":{"format":"double","type":"number"},"dayLow":{"format":"double","type":"number"},"prevClose":{"format":"double","type":"number"}},"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"},"InsiderTransaction":{"properties":{"name":{"type":"string"},"shares":{"description":"Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"transactionCode":{"type":"string"},"transactionDate":{"type":"string"},"value":{"format":"double","type":"number"}},"type":"object"},"ListAiTokensRequest":{"description":"ListAiTokensRequest retrieves AI crypto token prices.","type":"object"},"ListAiTokensResponse":{"description":"ListAiTokensResponse contains AI token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListCommodityQuotesRequest":{"description":"ListCommodityQuotesRequest specifies which commodities to retrieve.","properties":{"symbols":{"items":{"description":"Commodity symbols to retrieve (Yahoo symbols). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListCommodityQuotesResponse":{"description":"ListCommodityQuotesResponse contains commodity quotes.","properties":{"quotes":{"items":{"$ref":"#/components/schemas/CommodityQuote"},"type":"array"}},"type":"object"},"ListCryptoQuotesRequest":{"description":"ListCryptoQuotesRequest specifies which cryptocurrencies to retrieve.","properties":{"ids":{"items":{"description":"Cryptocurrency IDs to retrieve (CoinGecko IDs). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListCryptoQuotesResponse":{"description":"ListCryptoQuotesResponse contains cryptocurrency quotes.","properties":{"quotes":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListCryptoSectorsRequest":{"description":"ListCryptoSectorsRequest retrieves crypto sector performance.","type":"object"},"ListCryptoSectorsResponse":{"description":"ListCryptoSectorsResponse contains crypto sector performance data.","properties":{"sectors":{"items":{"$ref":"#/components/schemas/CryptoSector"},"type":"array"}},"type":"object"},"ListDefiTokensRequest":{"description":"ListDefiTokensRequest retrieves DeFi token prices.","type":"object"},"ListDefiTokensResponse":{"description":"ListDefiTokensResponse contains DeFi token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListEarningsCalendarRequest":{"properties":{"fromDate":{"type":"string"},"toDate":{"type":"string"}},"type":"object"},"ListEarningsCalendarResponse":{"properties":{"earnings":{"items":{"$ref":"#/components/schemas/EarningsEntry"},"type":"array"},"fromDate":{"type":"string"},"toDate":{"type":"string"},"total":{"format":"int32","type":"integer"},"unavailable":{"type":"boolean"}},"type":"object"},"ListEtfFlowsRequest":{"description":"ListEtfFlowsRequest is empty; the handler uses a fixed list of BTC spot ETFs.","type":"object"},"ListEtfFlowsResponse":{"description":"ListEtfFlowsResponse contains BTC spot ETF flow data.","properties":{"etfs":{"items":{"$ref":"#/components/schemas/EtfFlow"},"type":"array"},"rateLimited":{"description":"True when the upstream API rate-limited the request.","type":"boolean"},"summary":{"$ref":"#/components/schemas/EtfFlowsSummary"},"timestamp":{"description":"Timestamp of the data fetch (ISO 8601).","type":"string"}},"type":"object"},"ListGulfQuotesRequest":{"type":"object"},"ListGulfQuotesResponse":{"properties":{"quotes":{"items":{"$ref":"#/components/schemas/GulfQuote"},"type":"array"},"rateLimited":{"type":"boolean"}},"type":"object"},"ListMarketQuotesRequest":{"description":"ListMarketQuotesRequest specifies which stock/index symbols to retrieve.","properties":{"symbols":{"items":{"description":"Ticker symbols to retrieve (e.g., [\"AAPL\", \"^GSPC\"]). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListMarketQuotesResponse":{"description":"ListMarketQuotesResponse contains stock and index quotes.","properties":{"finnhubSkipped":{"description":"True when the Finnhub API key is not configured and stock quotes were skipped.","type":"boolean"},"quotes":{"items":{"$ref":"#/components/schemas/MarketQuote"},"type":"array"},"rateLimited":{"description":"True when the upstream API rate-limited the request.","type":"boolean"},"skipReason":{"description":"Human-readable reason when Finnhub was skipped (e.g., \"FINNHUB_API_KEY not configured\").","type":"string"}},"type":"object"},"ListOtherTokensRequest":{"description":"ListOtherTokensRequest retrieves other/trending crypto token prices.","type":"object"},"ListOtherTokensResponse":{"description":"ListOtherTokensResponse contains other token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListStablecoinMarketsRequest":{"description":"ListStablecoinMarketsRequest specifies which stablecoins to retrieve.","properties":{"coins":{"items":{"description":"CoinGecko IDs to retrieve (e.g. \"tether,usd-coin\"). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListStablecoinMarketsResponse":{"description":"ListStablecoinMarketsResponse contains stablecoin market data.","properties":{"stablecoins":{"items":{"$ref":"#/components/schemas/Stablecoin"},"type":"array"},"summary":{"$ref":"#/components/schemas/StablecoinSummary"},"timestamp":{"description":"Timestamp of the data fetch (ISO 8601).","type":"string"}},"type":"object"},"ListStoredStockBacktestsRequest":{"properties":{"evalWindowDays":{"format":"int32","maximum":30,"minimum":3,"type":"integer"},"symbols":{"items":{"type":"string"},"type":"array"}},"type":"object"},"ListStoredStockBacktestsResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/BacktestStockResponse"},"type":"array"}},"type":"object"},"MarketQuote":{"description":"MarketQuote represents a stock or index quote from Finnhub or Yahoo Finance.","properties":{"change":{"description":"Percentage change from previous close.","format":"double","type":"number"},"display":{"description":"Display label.","type":"string"},"name":{"description":"Human-readable name.","type":"string"},"price":{"description":"Current price.","format":"double","type":"number"},"sparkline":{"items":{"description":"Sparkline data points (recent price history).","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Ticker symbol (e.g., \"AAPL\", \"^GSPC\").","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"PriceTarget":{"properties":{"current":{"format":"double","type":"number"},"high":{"format":"double","type":"number"},"low":{"format":"double","type":"number"},"mean":{"format":"double","type":"number"},"median":{"format":"double","type":"number"},"numberOfAnalysts":{"format":"int32","type":"integer"}},"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"},"UpgradeDowngrade":{"properties":{"action":{"type":"string"},"epochGradeDate":{"description":"Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"firm":{"type":"string"},"fromGrade":{"type":"string"},"toGrade":{"type":"string"}},"type":"object"},"ValidationError":{"description":"ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.","properties":{"violations":{"description":"List of validation violations","items":{"$ref":"#/components/schemas/FieldViolation"},"type":"array"}},"required":["violations"],"type":"object"}}},"info":{"title":"MarketService API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/market/v1/analyze-stock":{"get":{"description":"AnalyzeStock retrieves a premium stock analysis report with technicals, news, and AI synthesis.","operationId":"AnalyzeStock","parameters":[{"in":"query","name":"symbol","required":false,"schema":{"type":"string"}},{"in":"query","name":"name","required":false,"schema":{"type":"string"}},{"in":"query","name":"include_news","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyzeStockResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"AnalyzeStock","tags":["MarketService"]}},"/api/market/v1/backtest-stock":{"get":{"description":"BacktestStock replays premium stock-analysis signals over recent price history.","operationId":"BacktestStock","parameters":[{"in":"query","name":"symbol","required":false,"schema":{"type":"string"}},{"in":"query","name":"name","required":false,"schema":{"type":"string"}},{"in":"query","name":"eval_window_days","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BacktestStockResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"BacktestStock","tags":["MarketService"]}},"/api/market/v1/get-cot-positioning":{"get":{"description":"GetCotPositioning retrieves CFTC COT institutional positioning data.","operationId":"GetCotPositioning","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCotPositioningResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCotPositioning","tags":["MarketService"]}},"/api/market/v1/get-country-stock-index":{"get":{"description":"GetCountryStockIndex retrieves the primary stock index for a country from Yahoo Finance.","operationId":"GetCountryStockIndex","parameters":[{"description":"ISO 3166-1 alpha-2 country code (e.g., \"US\", \"GB\", \"JP\").","in":"query","name":"country_code","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCountryStockIndexResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCountryStockIndex","tags":["MarketService"]}},"/api/market/v1/get-fear-greed-index":{"get":{"description":"GetFearGreedIndex retrieves the composite Fear \u0026 Greed sentiment index.","operationId":"GetFearGreedIndex","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetFearGreedIndexResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetFearGreedIndex","tags":["MarketService"]}},"/api/market/v1/get-gold-intelligence":{"get":{"description":"GetGoldIntelligence retrieves gold pricing, cross-currency XAU, ratios, and CFTC positioning.","operationId":"GetGoldIntelligence","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetGoldIntelligenceResponse"}}},"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":"GetGoldIntelligence","tags":["MarketService"]}},"/api/market/v1/get-insider-transactions":{"get":{"description":"GetInsiderTransactions retrieves SEC insider buy/sell activity from Finnhub.","operationId":"GetInsiderTransactions","parameters":[{"in":"query","name":"symbol","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetInsiderTransactionsResponse"}}},"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":"GetInsiderTransactions","tags":["MarketService"]}},"/api/market/v1/get-market-breadth-history":{"get":{"description":"GetMarketBreadthHistory retrieves historical % of S\u0026P 500 stocks above 20/50/200-day SMAs.","operationId":"GetMarketBreadthHistory","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetMarketBreadthHistoryResponse"}}},"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":"GetMarketBreadthHistory","tags":["MarketService"]}},"/api/market/v1/get-sector-summary":{"get":{"description":"GetSectorSummary retrieves market sector performance data from Finnhub.","operationId":"GetSectorSummary","parameters":[{"description":"Time period for performance calculation (e.g., \"1d\", \"1w\", \"1m\"). Defaults to \"1d\".","in":"query","name":"period","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSectorSummaryResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetSectorSummary","tags":["MarketService"]}},"/api/market/v1/get-stock-analysis-history":{"get":{"description":"GetStockAnalysisHistory retrieves shared premium stock analysis history from the backend store.","operationId":"GetStockAnalysisHistory","parameters":[{"in":"query","name":"symbols","required":false,"schema":{"type":"string"}},{"in":"query","name":"limit_per_symbol","required":false,"schema":{"format":"int32","type":"integer"}},{"in":"query","name":"include_news","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetStockAnalysisHistoryResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetStockAnalysisHistory","tags":["MarketService"]}},"/api/market/v1/list-ai-tokens":{"get":{"description":"ListAiTokens retrieves AI-focused crypto token prices and changes.","operationId":"ListAiTokens","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListAiTokensResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListAiTokens","tags":["MarketService"]}},"/api/market/v1/list-commodity-quotes":{"get":{"description":"ListCommodityQuotes retrieves commodity price quotes from Yahoo Finance.","operationId":"ListCommodityQuotes","parameters":[{"description":"Commodity symbols to retrieve (Yahoo symbols). Empty returns defaults.","in":"query","name":"symbols","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCommodityQuotesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListCommodityQuotes","tags":["MarketService"]}},"/api/market/v1/list-crypto-quotes":{"get":{"description":"ListCryptoQuotes retrieves cryptocurrency quotes from CoinGecko.","operationId":"ListCryptoQuotes","parameters":[{"description":"Cryptocurrency IDs to retrieve (CoinGecko IDs). Empty returns defaults.","in":"query","name":"ids","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCryptoQuotesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListCryptoQuotes","tags":["MarketService"]}},"/api/market/v1/list-crypto-sectors":{"get":{"description":"ListCryptoSectors retrieves crypto sector performance averages.","operationId":"ListCryptoSectors","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCryptoSectorsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListCryptoSectors","tags":["MarketService"]}},"/api/market/v1/list-defi-tokens":{"get":{"description":"ListDefiTokens retrieves DeFi token prices and changes.","operationId":"ListDefiTokens","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListDefiTokensResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListDefiTokens","tags":["MarketService"]}},"/api/market/v1/list-earnings-calendar":{"get":{"description":"ListEarningsCalendar retrieves upcoming and recent earnings releases.","operationId":"ListEarningsCalendar","parameters":[{"in":"query","name":"fromDate","required":false,"schema":{"type":"string"}},{"in":"query","name":"toDate","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEarningsCalendarResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListEarningsCalendar","tags":["MarketService"]}},"/api/market/v1/list-etf-flows":{"get":{"description":"ListEtfFlows retrieves BTC spot ETF flow estimates from Yahoo Finance.","operationId":"ListEtfFlows","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEtfFlowsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListEtfFlows","tags":["MarketService"]}},"/api/market/v1/list-gulf-quotes":{"get":{"description":"ListGulfQuotes retrieves Gulf region market quotes (indices, currencies, oil).","operationId":"ListGulfQuotes","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListGulfQuotesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListGulfQuotes","tags":["MarketService"]}},"/api/market/v1/list-market-quotes":{"get":{"description":"ListMarketQuotes retrieves stock and index quotes.","operationId":"ListMarketQuotes","parameters":[{"description":"Ticker symbols to retrieve (e.g., [\"AAPL\", \"^GSPC\"]). Empty returns defaults.","in":"query","name":"symbols","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListMarketQuotesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListMarketQuotes","tags":["MarketService"]}},"/api/market/v1/list-other-tokens":{"get":{"description":"ListOtherTokens retrieves other/trending crypto token prices and changes.","operationId":"ListOtherTokens","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListOtherTokensResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListOtherTokens","tags":["MarketService"]}},"/api/market/v1/list-stablecoin-markets":{"get":{"description":"ListStablecoinMarkets retrieves stablecoin peg health and market data from CoinGecko.","operationId":"ListStablecoinMarkets","parameters":[{"description":"CoinGecko IDs to retrieve (e.g. \"tether,usd-coin\"). Empty returns defaults.","in":"query","name":"coins","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListStablecoinMarketsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListStablecoinMarkets","tags":["MarketService"]}},"/api/market/v1/list-stored-stock-backtests":{"get":{"description":"ListStoredStockBacktests retrieves stored premium backtest snapshots from the backend store.","operationId":"ListStoredStockBacktests","parameters":[{"in":"query","name":"symbols","required":false,"schema":{"type":"string"}},{"in":"query","name":"eval_window_days","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListStoredStockBacktestsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListStoredStockBacktests","tags":["MarketService"]}}}} \ No newline at end of file diff --git a/docs/api/MarketService.openapi.yaml b/docs/api/MarketService.openapi.yaml index 425f2b9e9..96baacbcc 100644 --- a/docs/api/MarketService.openapi.yaml +++ b/docs/api/MarketService.openapi.yaml @@ -1939,6 +1939,16 @@ components: type: string unavailable: type: boolean + session: + $ref: '#/components/schemas/GoldSessionRange' + returns: + $ref: '#/components/schemas/GoldReturns' + range52w: + $ref: '#/components/schemas/GoldRange52w' + drivers: + type: array + items: + $ref: '#/components/schemas/GoldDriver' GoldCrossCurrencyPrice: type: object properties: @@ -1954,18 +1964,85 @@ components: properties: reportDate: type: string - managedMoneyLong: - type: number - format: double - managedMoneyShort: - type: number - format: double + nextReleaseDate: + type: string + openInterest: + type: string + format: int64 + managedMoney: + $ref: '#/components/schemas/GoldCotCategory' + producerSwap: + $ref: '#/components/schemas/GoldCotCategory' + GoldCotCategory: + type: object + properties: + longPositions: + type: string + format: int64 + shortPositions: + type: string + format: int64 netPct: type: number format: double - dealerLong: + oiSharePct: type: number format: double - dealerShort: + wowNetDelta: + type: string + format: int64 + GoldSessionRange: + type: object + properties: + dayHigh: + type: number + format: double + dayLow: + type: number + format: double + prevClose: + type: number + format: double + GoldReturns: + type: object + properties: + w1: + type: number + format: double + m1: + type: number + format: double + ytd: + type: number + format: double + y1: + type: number + format: double + GoldRange52w: + type: object + properties: + hi: + type: number + format: double + lo: + type: number + format: double + positionPct: + type: number + format: double + GoldDriver: + type: object + properties: + symbol: + type: string + label: + type: string + value: + type: number + format: double + changePct: + type: number + format: double + correlation30d: type: number format: double diff --git a/proto/worldmonitor/market/v1/get_gold_intelligence.proto b/proto/worldmonitor/market/v1/get_gold_intelligence.proto index fd1dd3552..36de04d3f 100644 --- a/proto/worldmonitor/market/v1/get_gold_intelligence.proto +++ b/proto/worldmonitor/market/v1/get_gold_intelligence.proto @@ -10,13 +10,47 @@ message GoldCrossCurrencyPrice { double price = 3; } +message GoldSessionRange { + double day_high = 1; + double day_low = 2; + double prev_close = 3; +} + +message GoldReturns { + double w1 = 1; + double m1 = 2; + double ytd = 3; + double y1 = 4; +} + +message GoldRange52w { + double hi = 1; + double lo = 2; + double position_pct = 3; +} + +message GoldCotCategory { + int64 long_positions = 1; + int64 short_positions = 2; + double net_pct = 3; + double oi_share_pct = 4; + int64 wow_net_delta = 5; +} + message GoldCotPositioning { string report_date = 1; - double managed_money_long = 2; - double managed_money_short = 3; - double net_pct = 4; - double dealer_long = 5; - double dealer_short = 6; + string next_release_date = 2; + int64 open_interest = 3; + GoldCotCategory managed_money = 4; + GoldCotCategory producer_swap = 5; +} + +message GoldDriver { + string symbol = 1; + string label = 2; + double value = 3; + double change_pct = 4; + double correlation_30d = 5; } message GetGoldIntelligenceRequest {} @@ -34,4 +68,8 @@ message GetGoldIntelligenceResponse { GoldCotPositioning cot = 10; string updated_at = 11; bool unavailable = 12; + GoldSessionRange session = 13; + GoldReturns returns = 14; + GoldRange52w range_52w = 15; + repeated GoldDriver drivers = 16; } diff --git a/scripts/seed-commodity-quotes.mjs b/scripts/seed-commodity-quotes.mjs index 1436a0313..ff573cee1 100644 --- a/scripts/seed-commodity-quotes.mjs +++ b/scripts/seed-commodity-quotes.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { loadEnvFile, loadSharedConfig, sleep, runSeed, parseYahooChart, writeExtraKey } from './_seed-utils.mjs'; +import { loadEnvFile, loadSharedConfig, sleep, runSeed, parseYahooChart, writeExtraKey, writeExtraKeyWithMeta, CHROME_UA } from './_seed-utils.mjs'; import { AV_PHYSICAL_MAP, fetchAvPhysicalCommodity, fetchAvBulkQuotes } from './_shared-av.mjs'; const commodityConfig = loadSharedConfig('commodities.json'); @@ -8,9 +8,151 @@ const commodityConfig = loadSharedConfig('commodities.json'); loadEnvFile(import.meta.url); const CANONICAL_KEY = 'market:commodities-bootstrap:v1'; +const GOLD_EXTENDED_KEY = 'market:gold-extended:v1'; const CACHE_TTL = 1800; const YAHOO_DELAY_MS = 200; +const GOLD_HISTORY_SYMBOLS = ['GC=F', 'SI=F']; +const GOLD_DRIVER_SYMBOLS = [ + { symbol: '^TNX', label: 'US 10Y Yield' }, + { symbol: 'DX-Y.NYB', label: 'DXY' }, +]; + +async function fetchYahooChart1y(symbol) { + const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(symbol)}?range=1y&interval=1d`; + try { + const resp = await fetch(url, { headers: { 'User-Agent': CHROME_UA }, signal: AbortSignal.timeout(15_000) }); + if (!resp.ok) return null; + const json = await resp.json(); + const r = json?.chart?.result?.[0]; + if (!r) return null; + const meta = r.meta; + const ts = r.timestamp || []; + const closes = r.indicators?.quote?.[0]?.close || []; + const history = ts.map((t, i) => ({ d: new Date(t * 1000).toISOString().slice(0, 10), c: closes[i] })) + .filter(p => p.c != null && Number.isFinite(p.c)); + return { + symbol, + price: meta?.regularMarketPrice ?? null, + dayHigh: meta?.regularMarketDayHigh ?? null, + dayLow: meta?.regularMarketDayLow ?? null, + prevClose: meta?.chartPreviousClose ?? meta?.previousClose ?? null, + fiftyTwoWeekHigh: meta?.fiftyTwoWeekHigh ?? null, + fiftyTwoWeekLow: meta?.fiftyTwoWeekLow ?? null, + history, + }; + } catch { + return null; + } +} + +function computeReturns(history, currentPrice) { + if (!history.length || !Number.isFinite(currentPrice)) return { w1: 0, m1: 0, ytd: 0, y1: 0 }; + const byAgo = (days) => { + const target = history[Math.max(0, history.length - 1 - days)]; + return target?.c; + }; + const firstOfYear = history.find(p => p.d.startsWith(new Date().getUTCFullYear().toString()))?.c + ?? history[0].c; + const pct = (from) => from ? ((currentPrice - from) / from) * 100 : 0; + return { + w1: +pct(byAgo(5)).toFixed(2), + m1: +pct(byAgo(21)).toFixed(2), + ytd: +pct(firstOfYear).toFixed(2), + y1: +pct(history[0].c).toFixed(2), + }; +} + +function computeRange52w(history, currentPrice) { + if (!history.length) return { hi: 0, lo: 0, positionPct: 0 }; + const closes = history.map(p => p.c); + const hi = Math.max(...closes); + const lo = Math.min(...closes); + const span = hi - lo; + const positionPct = span > 0 ? ((currentPrice - lo) / span) * 100 : 50; + return { hi: +hi.toFixed(2), lo: +lo.toFixed(2), positionPct: +positionPct.toFixed(1) }; +} + +// Pearson correlation over the last N aligned daily returns +function pearsonCorrelation(aReturns, bReturns) { + const n = Math.min(aReturns.length, bReturns.length); + if (n < 5) return 0; + const a = aReturns.slice(-n); + const b = bReturns.slice(-n); + const meanA = a.reduce((s, v) => s + v, 0) / n; + const meanB = b.reduce((s, v) => s + v, 0) / n; + let num = 0, denA = 0, denB = 0; + for (let i = 0; i < n; i++) { + const da = a[i] - meanA; + const db = b[i] - meanB; + num += da * db; + denA += da * da; + denB += db * db; + } + const denom = Math.sqrt(denA * denB); + return denom > 0 ? +(num / denom).toFixed(3) : 0; +} + +function dailyReturns(history) { + const out = []; + for (let i = 1; i < history.length; i++) { + const prev = history[i - 1].c; + if (prev > 0) out.push((history[i].c - prev) / prev); + } + return out; +} + +async function fetchGoldExtended() { + const goldHistory = {}; + for (const sym of GOLD_HISTORY_SYMBOLS) { + await sleep(YAHOO_DELAY_MS); + const chart = await fetchYahooChart1y(sym); + if (chart) goldHistory[sym] = chart; + } + + const drivers = []; + const goldReturns = goldHistory['GC=F'] ? dailyReturns(goldHistory['GC=F'].history) : []; + + for (const cfg of GOLD_DRIVER_SYMBOLS) { + await sleep(YAHOO_DELAY_MS); + const chart = await fetchYahooChart1y(cfg.symbol); + if (!chart || chart.price == null) continue; + const changePct = chart.prevClose ? ((chart.price - chart.prevClose) / chart.prevClose) * 100 : 0; + const driverReturns = dailyReturns(chart.history).slice(-30); + const goldLast30 = goldReturns.slice(-30); + const correlation = pearsonCorrelation(goldLast30, driverReturns); + drivers.push({ + symbol: cfg.symbol, + label: cfg.label, + value: +chart.price.toFixed(2), + changePct: +changePct.toFixed(2), + correlation30d: correlation, + }); + } + + const gold = goldHistory['GC=F']; + const silver = goldHistory['SI=F']; + + const build = (chart) => { + if (!chart || chart.price == null) return null; + return { + price: chart.price, + dayHigh: chart.dayHigh ?? 0, + dayLow: chart.dayLow ?? 0, + prevClose: chart.prevClose ?? 0, + returns: computeReturns(chart.history, chart.price), + range52w: computeRange52w(chart.history, chart.price), + }; + }; + + return { + updatedAt: new Date().toISOString(), + gold: build(gold), + silver: build(silver), + drivers, + }; +} + async function fetchYahooWithRetry(url, label, maxAttempts = 4) { for (let i = 0; i < maxAttempts; i++) { const resp = await fetch(url, { @@ -120,6 +262,25 @@ runSeed('market', 'commodities', CANONICAL_KEY, fetchAndStash, { const quotesPayload = { ...seedData, finnhubSkipped: false, skipReason: '', rateLimited: false }; await writeExtraKey(commodityKey, seedData, CACHE_TTL); await writeExtraKey(quotesKey, quotesPayload, CACHE_TTL); + + try { + const extended = await fetchGoldExtended(); + // Require gold (the core metal) AND at least one driver or silver. Writing a + // partial payload would overwrite a healthy prior key with degraded data and + // stamp seed-meta as fresh, masking a broken Yahoo fetch in health checks. + const hasCore = extended.gold != null; + const hasContext = extended.silver != null || extended.drivers.length > 0; + if (hasCore && hasContext) { + const recordCount = (extended.gold ? 1 : 0) + (extended.silver ? 1 : 0) + extended.drivers.length; + await writeExtraKeyWithMeta(GOLD_EXTENDED_KEY, extended, CACHE_TTL, recordCount, 'seed-meta:market:gold-extended'); + console.log(` [Gold] extended: gold=${!!extended.gold} silver=${!!extended.silver} drivers=${extended.drivers.length}`); + } else { + // Preserve prior key (if any) and do NOT bump seed-meta — health will flag stale. + console.warn(` [Gold] extended: incomplete (gold=${!!extended.gold} silver=${!!extended.silver} drivers=${extended.drivers.length}) — skipping write, letting seed-meta go stale`); + } + } catch (e) { + console.warn(` [Gold] extended fetch error: ${e?.message || e} — skipping write, letting seed-meta go stale`); + } }).catch((err) => { const _cause = err.cause ? ` (cause: ${err.cause.message || err.cause.code || err.cause})` : ''; console.error('FATAL:', (err.message || err) + _cause); process.exit(1); diff --git a/scripts/seed-cot.mjs b/scripts/seed-cot.mjs index 423bf8841..329fac208 100644 --- a/scripts/seed-cot.mjs +++ b/scripts/seed-cot.mjs @@ -6,8 +6,6 @@ loadEnvFile(import.meta.url); const COT_KEY = 'market:cot:v1'; const COT_TTL = 604800; -// Financial futures: TFF Combined report (Socrata yw9f-hn96) -// Fields: dealer_positions_long_all, asset_mgr_positions_long, lev_money_positions_long const FINANCIAL_INSTRUMENTS = [ { name: 'S&P 500 E-Mini', code: 'ES', pattern: /E-MINI S&P 500 - CHICAGO/i }, { name: 'Nasdaq 100 E-Mini', code: 'NQ', pattern: /^NASDAQ MINI - CHICAGO/i }, @@ -17,12 +15,9 @@ const FINANCIAL_INSTRUMENTS = [ { name: 'USD/JPY', code: 'JY', pattern: /JAPANESE YEN - CHICAGO/i }, ]; -// Physical commodities: Disaggregated Combined report (Socrata rxbv-e226) -// Fields: swap_positions_long_all, m_money_positions_long_all (no lev_money equivalent) -// cftc_contract_market_code used for precise filtering — avoids fragile name matching const COMMODITY_INSTRUMENTS = [ { name: 'Gold', code: 'GC', contractCode: '088691' }, - { name: 'Crude Oil (WTI)', code: 'CL', contractCode: '067651' }, // WTI-PHYSICAL NYMEX + { name: 'Crude Oil (WTI)', code: 'CL', contractCode: '067651' }, ]; function parseDate(raw) { @@ -39,6 +34,19 @@ function parseDate(raw) { return s.slice(0, 10); } +// CFTC releases COT every Friday ~3:30pm ET for Tuesday data. Given a reportDate +// (Tuesday), the NEXT release is the Friday of the same week (reportDate + 3 days). +// If today is already past that Friday, the next Tuesday's data releases the +// following Friday — but we only call this with the *latest* stored row, so the +// next release is always reportDate + 3 days. +export function computeNextCotRelease(reportDate) { + if (!reportDate) return ''; + const d = new Date(`${reportDate}T00:00:00Z`); + if (Number.isNaN(d.getTime())) return ''; + d.setUTCDate(d.getUTCDate() + 3); + return d.toISOString().slice(0, 10); +} + async function fetchSocrata(datasetId, extraParams = '') { const url = `https://publicreporting.cftc.gov/resource/${datasetId}.json` + @@ -51,30 +59,93 @@ async function fetchSocrata(datasetId, extraParams = '') { return resp.json(); } -async function fetchCotData() { +export function buildInstrument(target, currentRow, priorRow, kind) { const toNum = v => { const n = parseInt(String(v ?? '').replace(/,/g, '').trim(), 10); - return isNaN(n) ? 0 : n; + return Number.isNaN(n) ? 0 : n; }; - let financialRows, commodityRows; + const reportDate = parseDate(currentRow.report_date_as_yyyy_mm_dd ?? ''); + const openInterest = toNum(currentRow.open_interest_all); + + let mmLong, mmShort, psLong, psShort, priorMmNet, priorPsNet; + let leveragedFundsLong = 0; + let leveragedFundsShort = 0; + + if (kind === 'financial') { + mmLong = toNum(currentRow.asset_mgr_positions_long); + mmShort = toNum(currentRow.asset_mgr_positions_short); + psLong = toNum(currentRow.dealer_positions_long_all); + psShort = toNum(currentRow.dealer_positions_short_all); + // TFF report also exposes leveraged-funds positions — consumed by CotPositioningPanel. + leveragedFundsLong = toNum(currentRow.lev_money_positions_long); + leveragedFundsShort = toNum(currentRow.lev_money_positions_short); + if (priorRow) { + priorMmNet = toNum(priorRow.asset_mgr_positions_long) - toNum(priorRow.asset_mgr_positions_short); + priorPsNet = toNum(priorRow.dealer_positions_long_all) - toNum(priorRow.dealer_positions_short_all); + } + } else { + mmLong = toNum(currentRow.m_money_positions_long_all); + mmShort = toNum(currentRow.m_money_positions_short_all); + psLong = toNum(currentRow.swap_positions_long_all); + psShort = toNum(currentRow.swap__positions_short_all); + if (priorRow) { + priorMmNet = toNum(priorRow.m_money_positions_long_all) - toNum(priorRow.m_money_positions_short_all); + priorPsNet = toNum(priorRow.swap_positions_long_all) - toNum(priorRow.swap__positions_short_all); + } + } + + const mkCategory = (long, short, priorNet) => { + const gross = Math.max(long + short, 1); + const netPct = ((long - short) / gross) * 100; + const oiSharePct = openInterest > 0 ? ((long + short) / openInterest) * 100 : 0; + const wowNetDelta = priorNet != null ? (long - short) - priorNet : 0; + return { + longPositions: long, + shortPositions: short, + netPct: parseFloat(netPct.toFixed(2)), + oiSharePct: parseFloat(oiSharePct.toFixed(2)), + wowNetDelta, + }; + }; + + const managedMoney = mkCategory(mmLong, mmShort, priorMmNet); + const producerSwap = mkCategory(psLong, psShort, priorPsNet); + + return { + name: target.name, + code: target.code, + reportDate, + nextReleaseDate: computeNextCotRelease(reportDate), + openInterest, + managedMoney, + producerSwap, + // legacy flat fields consumed by get-cot-positioning.ts / CotPositioningPanel + assetManagerLong: mmLong, + assetManagerShort: mmShort, + leveragedFundsLong, + leveragedFundsShort, + dealerLong: psLong, + dealerShort: psShort, + netPct: managedMoney.netPct, + }; +} + +async function fetchCotData() { + let financialRows = []; + let commodityRows = []; + try { - // yw9f-hn96: TFF Combined — financial futures (ES, NQ, ZN, ZT, EC, JY) - // Fields: dealer_positions_long_all, asset_mgr_positions_long, lev_money_positions_long financialRows = await fetchSocrata('yw9f-hn96'); } catch (e) { console.warn(` CFTC TFF fetch failed: ${e.message}`); - financialRows = []; } + try { - // rxbv-e226: Disaggregated All Combined — physical commodities (GC, CL) - // Fields: swap_positions_long_all, m_money_positions_long_all - // Filter by contract code — more reliable than name pattern matching const codeList = COMMODITY_INSTRUMENTS.map(i => `%27${i.contractCode}%27`).join('%2C'); commodityRows = await fetchSocrata('rxbv-e226', `%20AND%20cftc_contract_market_code%20IN%28${codeList}%29`); } catch (e) { console.warn(` CFTC Disaggregated fetch failed: ${e.message}`); - commodityRows = []; } if (!financialRows.length && !commodityRows.length) { @@ -85,45 +156,34 @@ async function fetchCotData() { const instruments = []; let latestReportDate = ''; - const pushInstrument = (target, row, amLong, amShort, levLong, levShort, dealerLong, dealerShort) => { - const reportDate = parseDate(row.report_date_as_yyyy_mm_dd ?? ''); - if (reportDate && !latestReportDate) latestReportDate = reportDate; - const netPct = ((amLong - amShort) / Math.max(amLong + amShort, 1)) * 100; - instruments.push({ - name: target.name, code: target.code, reportDate, - assetManagerLong: amLong, assetManagerShort: amShort, - leveragedFundsLong: levLong, leveragedFundsShort: levShort, - dealerLong, dealerShort, - netPct: parseFloat(netPct.toFixed(2)), - }); - console.log(` ${target.code}: AM net ${netPct.toFixed(1)}% (${amLong}L / ${amShort}S), date=${reportDate}`); + const findPair = (rows, predicate) => { + const matches = rows.filter(predicate); + // Sorted DESC already; index 0 = current, index 1 = prior week + return [matches[0], matches[1]]; }; for (const target of FINANCIAL_INSTRUMENTS) { - const row = financialRows.find(r => target.pattern.test(r.market_and_exchange_names ?? '')); - if (!row) { console.warn(` CFTC: no row for ${target.name}`); continue; } - pushInstrument(target, row, - toNum(row.asset_mgr_positions_long), toNum(row.asset_mgr_positions_short), - toNum(row.lev_money_positions_long), toNum(row.lev_money_positions_short), - toNum(row.dealer_positions_long_all), toNum(row.dealer_positions_short_all), - ); + const [current, prior] = findPair(financialRows, r => target.pattern.test(r.market_and_exchange_names ?? '')); + if (!current) { console.warn(` CFTC: no row for ${target.name}`); continue; } + const inst = buildInstrument(target, current, prior, 'financial'); + if (inst.reportDate && !latestReportDate) latestReportDate = inst.reportDate; + instruments.push(inst); + console.log(` ${inst.code}: MM net ${inst.managedMoney.netPct}% Δ${inst.managedMoney.wowNetDelta}, OI ${inst.openInterest}, date=${inst.reportDate}`); } for (const target of COMMODITY_INSTRUMENTS) { - const row = commodityRows.find(r => r.cftc_contract_market_code === target.contractCode); - if (!row) { console.warn(` CFTC: no row for ${target.name}`); continue; } - // Physical commodity disaggregated: managed money → assetManager, swap dealers → dealer - pushInstrument(target, row, - toNum(row.m_money_positions_long_all), toNum(row.m_money_positions_short_all), - 0, 0, - toNum(row.swap_positions_long_all), toNum(row.swap__positions_short_all), - ); + const [current, prior] = findPair(commodityRows, r => r.cftc_contract_market_code === target.contractCode); + if (!current) { console.warn(` CFTC: no row for ${target.name}`); continue; } + const inst = buildInstrument(target, current, prior, 'commodity'); + if (inst.reportDate && !latestReportDate) latestReportDate = inst.reportDate; + instruments.push(inst); + console.log(` ${inst.code}: MM net ${inst.managedMoney.netPct}% Δ${inst.managedMoney.wowNetDelta}, OI ${inst.openInterest}, date=${inst.reportDate}`); } return { instruments, reportDate: latestReportDate }; } -if (process.argv[1] && process.argv[1].endsWith('seed-cot.mjs')) { +if (process.argv[1]?.endsWith('seed-cot.mjs')) { runSeed('market', 'cot', COT_KEY, fetchCotData, { ttlSeconds: COT_TTL, validateFn: data => Array.isArray(data?.instruments) && data.instruments.length > 0, diff --git a/server/worldmonitor/market/v1/get-gold-intelligence.ts b/server/worldmonitor/market/v1/get-gold-intelligence.ts index 64121436f..a3cfa9ade 100644 --- a/server/worldmonitor/market/v1/get-gold-intelligence.ts +++ b/server/worldmonitor/market/v1/get-gold-intelligence.ts @@ -4,11 +4,17 @@ import type { GetGoldIntelligenceResponse, GoldCrossCurrencyPrice, GoldCotPositioning, + GoldCotCategory, + GoldSessionRange, + GoldReturns, + GoldRange52w, + GoldDriver, } from '../../../../src/generated/server/worldmonitor/market/v1/service_server'; import { getCachedJson } from '../../../_shared/redis'; const COMMODITY_KEY = 'market:commodities-bootstrap:v1'; const COT_KEY = 'market:cot:v1'; +const GOLD_EXTENDED_KEY = 'market:gold-extended:v1'; interface RawQuote { symbol: string; @@ -19,15 +25,52 @@ interface RawQuote { sparkline?: number[]; } +interface RawCotCategory { + longPositions: number; + shortPositions: number; + netPct: number; + oiSharePct: number; + wowNetDelta: number; +} + interface RawCotInstrument { name: string; code: string; reportDate: string; - assetManagerLong: number; - assetManagerShort: number; - dealerLong: number; - dealerShort: number; - netPct: number; + nextReleaseDate?: string; + openInterest?: number; + managedMoney?: RawCotCategory; + producerSwap?: RawCotCategory; + // legacy + assetManagerLong?: number; + assetManagerShort?: number; + dealerLong?: number; + dealerShort?: number; + netPct?: number; +} + +interface GoldExtendedMetal { + price: number; + dayHigh: number; + dayLow: number; + prevClose: number; + returns: { w1: number; m1: number; ytd: number; y1: number }; + range52w: { hi: number; lo: number; positionPct: number }; +} + +interface GoldExtendedDriver { + symbol: string; + label: string; + value: number; + changePct: number; + correlation30d: number; +} + +interface GoldExtendedPayload { + updatedAt: string; + gold?: GoldExtendedMetal | null; + silver?: GoldExtendedMetal | null; + drivers?: GoldExtendedDriver[]; } const XAU_FX = [ @@ -39,27 +82,91 @@ const XAU_FX = [ { symbol: 'USDCHF=X', label: 'CHF', flag: '\u{1F1E8}\u{1F1ED}', multiply: false }, ]; +function emptyResponse(): GetGoldIntelligenceResponse { + return { + goldPrice: 0, + goldChangePct: 0, + goldSparkline: [], + silverPrice: 0, + platinumPrice: 0, + palladiumPrice: 0, + crossCurrencyPrices: [], + drivers: [], + updatedAt: '', + unavailable: true, + }; +} + +function mapCategory(c: RawCotCategory | undefined): GoldCotCategory | undefined { + if (!c) return undefined; + return { + longPositions: String(Math.round(c.longPositions ?? 0)), + shortPositions: String(Math.round(c.shortPositions ?? 0)), + netPct: Number(c.netPct ?? 0), + oiSharePct: Number(c.oiSharePct ?? 0), + wowNetDelta: String(Math.round(c.wowNetDelta ?? 0)), + }; +} + +function mapCot(raw: RawCotInstrument | undefined): GoldCotPositioning | undefined { + if (!raw) return undefined; + // Legacy fallback: derive v2 category fields from flat long/short so a + // pre-migration seed payload still renders the new panel correctly. OI share + // stays 0 because old payloads don't carry open_interest; WoW delta stays 0 + // because the prior-week row wasn't captured before this migration. + const netPctFrom = (long: number, short: number) => { + const gross = Math.max(long + short, 1); + return ((long - short) / gross) * 100; + }; + const mmLong = raw.assetManagerLong ?? 0; + const mmShort = raw.assetManagerShort ?? 0; + const psLong = raw.dealerLong ?? 0; + const psShort = raw.dealerShort ?? 0; + const managedMoney = raw.managedMoney + ? mapCategory(raw.managedMoney) + : mapCategory({ + longPositions: mmLong, + shortPositions: mmShort, + netPct: raw.netPct ?? netPctFrom(mmLong, mmShort), + oiSharePct: 0, + wowNetDelta: 0, + }); + const producerSwap = raw.producerSwap + ? mapCategory(raw.producerSwap) + : mapCategory({ + longPositions: psLong, + shortPositions: psShort, + netPct: netPctFrom(psLong, psShort), + oiSharePct: 0, + wowNetDelta: 0, + }); + return { + reportDate: String(raw.reportDate ?? ''), + nextReleaseDate: String(raw.nextReleaseDate ?? ''), + openInterest: String(Math.round(raw.openInterest ?? 0)), + managedMoney, + producerSwap, + }; +} + export async function getGoldIntelligence( _ctx: ServerContext, _req: GetGoldIntelligenceRequest, ): Promise { try { - const [rawPayload, rawCot] = await Promise.all([ + const [rawPayload, rawCot, rawExtended] = await Promise.all([ getCachedJson(COMMODITY_KEY, true) as Promise<{ quotes?: RawQuote[] } | null>, getCachedJson(COT_KEY, true) as Promise<{ instruments?: RawCotInstrument[]; reportDate?: string } | null>, + getCachedJson(GOLD_EXTENDED_KEY, true) as Promise, ]); const rawQuotes = rawPayload?.quotes; - if (!rawQuotes || !Array.isArray(rawQuotes) || rawQuotes.length === 0) { - return { goldPrice: 0, goldChangePct: 0, goldSparkline: [], silverPrice: 0, platinumPrice: 0, palladiumPrice: 0, crossCurrencyPrices: [], updatedAt: '', unavailable: true }; - } + if (!rawQuotes || !Array.isArray(rawQuotes) || rawQuotes.length === 0) return emptyResponse(); const quoteMap = new Map(rawQuotes.map(q => [q.symbol, q])); - const gold = quoteMap.get('GC=F'); - if (!gold) { - return { goldPrice: 0, goldChangePct: 0, goldSparkline: [], silverPrice: 0, platinumPrice: 0, palladiumPrice: 0, crossCurrencyPrices: [], updatedAt: '', unavailable: true }; - } + if (!gold) return emptyResponse(); + const silver = quoteMap.get('SI=F'); const platinum = quoteMap.get('PL=F'); const palladium = quoteMap.get('PA=F'); @@ -70,7 +177,9 @@ export async function getGoldIntelligence( const palladiumPrice = palladium?.price ?? 0; const goldSilverRatio = (goldPrice > 0 && silverPrice > 0) ? goldPrice / silverPrice : undefined; - const goldPlatinumPremiumPct = (goldPrice > 0 && platinumPrice > 0) ? ((goldPrice - platinumPrice) / platinumPrice) * 100 : undefined; + const goldPlatinumPremiumPct = (goldPrice > 0 && platinumPrice > 0) + ? ((goldPrice - platinumPrice) / platinumPrice) * 100 + : undefined; const crossCurrencyPrices: GoldCrossCurrencyPrice[] = []; if (goldPrice > 0) { @@ -83,20 +192,21 @@ export async function getGoldIntelligence( } } - let cot: GoldCotPositioning | undefined; - if (rawCot?.instruments) { - const gc = rawCot.instruments.find(i => i.code === 'GC'); - if (gc) { - cot = { - reportDate: String(gc.reportDate ?? rawCot.reportDate ?? ''), - managedMoneyLong: Number(gc.assetManagerLong ?? 0), - managedMoneyShort: Number(gc.assetManagerShort ?? 0), - netPct: Number(gc.netPct ?? 0), - dealerLong: Number(gc.dealerLong ?? 0), - dealerShort: Number(gc.dealerShort ?? 0), - }; - } - } + const cot = mapCot(rawCot?.instruments?.find(i => i.code === 'GC')); + + const goldExt = rawExtended?.gold; + const session: GoldSessionRange | undefined = goldExt + ? { dayHigh: goldExt.dayHigh, dayLow: goldExt.dayLow, prevClose: goldExt.prevClose } + : undefined; + const returns: GoldReturns | undefined = goldExt ? { ...goldExt.returns } : undefined; + const range52w: GoldRange52w | undefined = goldExt ? { ...goldExt.range52w } : undefined; + const drivers: GoldDriver[] = (rawExtended?.drivers ?? []).map(d => ({ + symbol: d.symbol, + label: d.label, + value: d.value, + changePct: d.changePct, + correlation30d: d.correlation30d, + })); return { goldPrice, @@ -109,10 +219,18 @@ export async function getGoldIntelligence( goldPlatinumPremiumPct, crossCurrencyPrices, cot, - updatedAt: new Date().toISOString(), + session, + returns, + range52w, + drivers, + // updatedAt reflects the *enrichment* layer's freshness. If the extended + // key is missing we deliberately emit empty so the panel renders "Updated —" + // rather than a misleading "just now" stamp while session/returns/drivers + // are all absent. + updatedAt: rawExtended?.updatedAt ?? '', unavailable: false, }; } catch { - return { goldPrice: 0, goldChangePct: 0, goldSparkline: [], silverPrice: 0, platinumPrice: 0, palladiumPrice: 0, crossCurrencyPrices: [], updatedAt: '', unavailable: true }; + return emptyResponse(); } } diff --git a/src/components/GoldIntelligencePanel.ts b/src/components/GoldIntelligencePanel.ts index c8e7eac2d..2d5ddd2a3 100644 --- a/src/components/GoldIntelligencePanel.ts +++ b/src/components/GoldIntelligencePanel.ts @@ -4,20 +4,19 @@ import { escapeHtml } from '@/utils/sanitize'; import { toApiUrl } from '@/services/runtime'; import { miniSparkline } from '@/utils/sparkline'; -interface CrossCurrencyPrice { - currency: string; - flag: string; - price: number; -} - +interface CrossCurrencyPrice { currency: string; flag: string; price: number } +interface CotCategory { longPositions: string; shortPositions: string; netPct: number; oiSharePct: number; wowNetDelta: string } interface CotData { reportDate: string; - managedMoneyLong: number; - managedMoneyShort: number; - netPct: number; - dealerLong: number; - dealerShort: number; + nextReleaseDate: string; + openInterest: string; + managedMoney?: CotCategory; + producerSwap?: CotCategory; } +interface SessionRange { dayHigh: number; dayLow: number; prevClose: number } +interface Returns { w1: number; m1: number; ytd: number; y1: number } +interface Range52w { hi: number; lo: number; positionPct: number } +interface Driver { symbol: string; label: string; value: number; changePct: number; correlation30d: number } interface GoldIntelligenceData { goldPrice: number; @@ -30,6 +29,10 @@ interface GoldIntelligenceData { goldPlatinumPremiumPct?: number; crossCurrencyPrices: CrossCurrencyPrice[]; cot?: CotData; + session?: SessionRange; + returns?: Returns; + range52w?: Range52w; + drivers: Driver[]; updatedAt: string; unavailable?: boolean; } @@ -39,16 +42,62 @@ function fmtPrice(v: number, decimals = 2): string { return v >= 10000 ? Math.round(v).toLocaleString() : v.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); } -function renderPositionBar(netPct: number, label: string): string { +function fmtInt(raw: string | number): string { + const n = typeof raw === 'string' ? parseInt(raw, 10) : raw; + if (!Number.isFinite(n)) return '--'; + return Math.round(n).toLocaleString(); +} + +function fmtPct(v: number, decimals = 2): string { + if (!Number.isFinite(v)) return '--'; + const sign = v >= 0 ? '+' : ''; + return `${sign}${v.toFixed(decimals)}%`; +} + +function fmtSignedInt(raw: string | number): string { + const n = typeof raw === 'string' ? parseInt(raw, 10) : raw; + if (!Number.isFinite(n)) return '--'; + const sign = n >= 0 ? '+' : ''; + return `${sign}${Math.round(n).toLocaleString()}`; +} + +function freshnessLabel(iso: string): { text: string; dot: string } { + if (!iso) return { text: 'Updated —', dot: 'var(--text-dim)' }; + const diffMs = Date.now() - new Date(iso).getTime(); + if (!Number.isFinite(diffMs) || diffMs < 0) return { text: 'Updated now', dot: '#2ecc71' }; + const mins = Math.floor(diffMs / 60000); + const dot = mins < 10 ? '#2ecc71' : mins < 30 ? '#f5a623' : '#e74c3c'; + if (mins < 1) return { text: 'Updated just now', dot }; + if (mins < 60) return { text: `Updated ${mins}m ago`, dot }; + const hrs = Math.floor(mins / 60); + return { text: `Updated ${hrs}h ago`, dot }; +} + +function renderRangeBar(lo: number, hi: number, current: number, positionPct: number): string { + const clamped = Math.max(0, Math.min(100, positionPct)); + return ` +
+
+
+
+ Low $${escapeHtml(fmtPrice(lo))} + $${escapeHtml(fmtPrice(current))} • ${clamped.toFixed(0)}% of range + High $${escapeHtml(fmtPrice(hi))} +
`; +} + +function renderPositionBar(netPct: number, label: string, wow: string): string { const clamped = Math.max(-100, Math.min(100, netPct)); const halfWidth = Math.abs(clamped) / 100 * 50; const color = clamped >= 0 ? '#2ecc71' : '#e74c3c'; const leftPct = clamped >= 0 ? 50 : 50 - halfWidth; const sign = clamped >= 0 ? '+' : ''; + const wowN = parseInt(wow, 10); + const wowStr = Number.isFinite(wowN) && wowN !== 0 ? ` Δ ${fmtSignedInt(wow)}` : ''; return ` -
-
- ${escapeHtml(label)} +
+
+ ${escapeHtml(label)}${wowStr} ${sign}${clamped.toFixed(1)}%
@@ -64,6 +113,14 @@ function ratioLabel(ratio: number): { text: string; color: string } { return { text: 'Neutral', color: 'var(--text-dim)' }; } +function returnChip(label: string, pct: number): string { + const color = pct >= 0 ? '#2ecc71' : '#e74c3c'; + return `
+
${escapeHtml(label)}
+
${escapeHtml(fmtPct(pct, 1))}
+
`; +} + export class GoldIntelligencePanel extends Panel { private _hasData = false; @@ -96,16 +153,62 @@ export class GoldIntelligencePanel extends Panel { } } - private render(d: GoldIntelligenceData): void { + private renderHeader(d: GoldIntelligenceData): string { const changePct = d.goldChangePct; const changeColor = changePct >= 0 ? '#2ecc71' : '#e74c3c'; - const changeSign = changePct >= 0 ? '+' : ''; const spark = miniSparkline(d.goldSparkline, changePct, 80, 20); + const fresh = freshnessLabel(d.updatedAt); + const sessionLine = d.session && d.session.dayHigh > 0 + ? `
+ Session H $${escapeHtml(fmtPrice(d.session.dayHigh))} • L $${escapeHtml(fmtPrice(d.session.dayLow))} • Prev $${escapeHtml(fmtPrice(d.session.prevClose))} +
` + : ''; + + return ` +
+
Price & Performance
+
+ $${escapeHtml(fmtPrice(d.goldPrice))} + ${fmtPct(changePct)} + ${spark} +
+
+ + ${escapeHtml(fresh.text)} • GC=F front-month +
+ ${sessionLine} +
`; + } + + private renderReturns(d: GoldIntelligenceData): string { + if (!d.returns && !d.range52w) return ''; + const chips = d.returns + ? `
+ ${returnChip('1W', d.returns.w1)} + ${returnChip('1M', d.returns.m1)} + ${returnChip('YTD', d.returns.ytd)} + ${returnChip('1Y', d.returns.y1)} +
` + : ''; + const range = d.range52w && d.range52w.hi > 0 + ? `
+
52-week range
+ ${renderRangeBar(d.range52w.lo, d.range52w.hi, d.goldPrice, d.range52w.positionPct)} +
` + : ''; + return `
+
Returns
+ ${chips} + ${range} +
`; + } + + private renderMetals(d: GoldIntelligenceData): string { const ratioHtml = d.goldSilverRatio != null && Number.isFinite(d.goldSilverRatio) ? (() => { const rl = ratioLabel(d.goldSilverRatio!); - return `
+ return `
Gold/Silver Ratio ${escapeHtml(d.goldSilverRatio!.toFixed(1))} ${escapeHtml(rl.text)}
`; @@ -115,65 +218,102 @@ export class GoldIntelligencePanel extends Panel { const premiumHtml = d.goldPlatinumPremiumPct != null && Number.isFinite(d.goldPlatinumPremiumPct) ? `
Gold vs Platinum - ${d.goldPlatinumPremiumPct >= 0 ? '+' : ''}${escapeHtml(d.goldPlatinumPremiumPct.toFixed(1))}% premium + ${escapeHtml(fmtPct(d.goldPlatinumPremiumPct, 1))} premium
` : ''; - const metalCards = [ - { label: 'Silver', price: d.silverPrice, sym: 'SI=F' }, - { label: 'Platinum', price: d.platinumPrice, sym: 'PL=F' }, - { label: 'Palladium', price: d.palladiumPrice, sym: 'PA=F' }, + const tiles = [ + { label: 'Silver', price: d.silverPrice }, + { label: 'Platinum', price: d.platinumPrice }, + { label: 'Palladium', price: d.palladiumPrice }, ].map(m => `
${escapeHtml(m.label)}
$${escapeHtml(fmtPrice(m.price))}
-
` - ).join(''); +
`).join(''); - const section1 = ` -
-
Price & Performance
-
- $${escapeHtml(fmtPrice(d.goldPrice))} - ${changeSign}${escapeHtml(changePct.toFixed(2))}% - ${spark} -
- ${ratioHtml} - ${premiumHtml} -
${metalCards}
-
`; + return `
+
Metals Complex
+ ${ratioHtml} + ${premiumHtml} +
${tiles}
+
`; + } - const fxRows = d.crossCurrencyPrices.map(c => + private renderFx(d: GoldIntelligenceData): string { + if (!d.crossCurrencyPrices.length) return ''; + const rows = d.crossCurrencyPrices.map(c => `
${escapeHtml(c.flag)} XAU/${escapeHtml(c.currency)}
${escapeHtml(fmtPrice(c.price, 0))}
-
` - ).join(''); +
`).join(''); + return `
+
Gold in Major Currencies
+
${rows}
+
`; + } - const section2 = d.crossCurrencyPrices.length > 0 - ? `
-
Gold in Major Currencies
-
${fxRows}
+ private renderPositioning(d: GoldIntelligenceData): string { + const c = d.cot; + if (!c) return ''; + const mm = c.managedMoney; + const ps = c.producerSwap; + const mmBar = mm ? renderPositionBar(mm.netPct, 'Managed Money (speculators)', mm.wowNetDelta) : ''; + const psBar = ps ? renderPositionBar(ps.netPct, 'Producer/Swap (commercials)', ps.wowNetDelta) : ''; + + const detail = (cat: CotCategory | undefined, label: string) => cat + ? `
+ ${escapeHtml(label)} + L ${escapeHtml(fmtInt(cat.longPositions))} / S ${escapeHtml(fmtInt(cat.shortPositions))} • ${cat.oiSharePct.toFixed(1)}% OI
` : ''; - let section3 = ''; - if (d.cot) { - const c = d.cot; - const longStr = Math.round(c.managedMoneyLong).toLocaleString(); - const shortStr = Math.round(c.managedMoneyShort).toLocaleString(); - section3 = ` -
-
CFTC Positioning (Managed Money)
- ${renderPositionBar(c.netPct, 'Net Position')} -
- Long: ${escapeHtml(longStr)} - Short: ${escapeHtml(shortStr)} -
- ${c.reportDate ? `
Report: ${escapeHtml(c.reportDate)}
` : ''} -
`; - } + const releaseLine = c.reportDate + ? `
+ As of ${escapeHtml(c.reportDate)}${c.nextReleaseDate ? ` • next release ${escapeHtml(c.nextReleaseDate)}` : ''} + OI ${escapeHtml(fmtInt(c.openInterest))} +
` + : ''; - this.setContent(`
${section1}${section2}${section3}
`); + return `
+
CFTC Positioning
+ ${mmBar} + ${detail(mm, 'MM breakdown')} + ${psBar} + ${detail(ps, 'P/S breakdown')} + ${releaseLine} +
`; + } + + private renderDrivers(d: GoldIntelligenceData): string { + if (!d.drivers?.length) return ''; + const rows = d.drivers.map(dr => { + const color = dr.changePct >= 0 ? '#2ecc71' : '#e74c3c'; + const corrColor = dr.correlation30d <= -0.3 ? '#2ecc71' : dr.correlation30d >= 0.3 ? '#e74c3c' : 'var(--text-dim)'; + return `
+ ${escapeHtml(dr.label)} + + ${escapeHtml(dr.value.toFixed(2))} + ${escapeHtml(fmtPct(dr.changePct, 2))} + corr 30d ${dr.correlation30d >= 0 ? '+' : ''}${dr.correlation30d.toFixed(2)} + +
`; + }).join(''); + return `
+
Drivers
+ ${rows} +
`; + } + + private render(d: GoldIntelligenceData): void { + const html = [ + this.renderHeader(d), + this.renderReturns(d), + this.renderMetals(d), + this.renderFx(d), + this.renderPositioning(d), + this.renderDrivers(d), + ].join(''); + this.setContent(`
${html}
`); } } diff --git a/src/generated/client/worldmonitor/market/v1/service_client.ts b/src/generated/client/worldmonitor/market/v1/service_client.ts index ab0fa1168..fc7934d1f 100644 --- a/src/generated/client/worldmonitor/market/v1/service_client.ts +++ b/src/generated/client/worldmonitor/market/v1/service_client.ts @@ -519,6 +519,10 @@ export interface GetGoldIntelligenceResponse { cot?: GoldCotPositioning; updatedAt: string; unavailable: boolean; + session?: GoldSessionRange; + returns?: GoldReturns; + range52w?: GoldRange52w; + drivers: GoldDriver[]; } export interface GoldCrossCurrencyPrice { @@ -529,11 +533,45 @@ export interface GoldCrossCurrencyPrice { export interface GoldCotPositioning { reportDate: string; - managedMoneyLong: number; - managedMoneyShort: number; + nextReleaseDate: string; + openInterest: string; + managedMoney?: GoldCotCategory; + producerSwap?: GoldCotCategory; +} + +export interface GoldCotCategory { + longPositions: string; + shortPositions: string; netPct: number; - dealerLong: number; - dealerShort: number; + oiSharePct: number; + wowNetDelta: string; +} + +export interface GoldSessionRange { + dayHigh: number; + dayLow: number; + prevClose: number; +} + +export interface GoldReturns { + w1: number; + m1: number; + ytd: number; + y1: number; +} + +export interface GoldRange52w { + hi: number; + lo: number; + positionPct: number; +} + +export interface GoldDriver { + symbol: string; + label: string; + value: number; + changePct: number; + correlation30d: number; } export interface FieldViolation { diff --git a/src/generated/server/worldmonitor/market/v1/service_server.ts b/src/generated/server/worldmonitor/market/v1/service_server.ts index ea1dd9c36..c0caf78cb 100644 --- a/src/generated/server/worldmonitor/market/v1/service_server.ts +++ b/src/generated/server/worldmonitor/market/v1/service_server.ts @@ -519,6 +519,10 @@ export interface GetGoldIntelligenceResponse { cot?: GoldCotPositioning; updatedAt: string; unavailable: boolean; + session?: GoldSessionRange; + returns?: GoldReturns; + range52w?: GoldRange52w; + drivers: GoldDriver[]; } export interface GoldCrossCurrencyPrice { @@ -529,11 +533,45 @@ export interface GoldCrossCurrencyPrice { export interface GoldCotPositioning { reportDate: string; - managedMoneyLong: number; - managedMoneyShort: number; + nextReleaseDate: string; + openInterest: string; + managedMoney?: GoldCotCategory; + producerSwap?: GoldCotCategory; +} + +export interface GoldCotCategory { + longPositions: string; + shortPositions: string; netPct: number; - dealerLong: number; - dealerShort: number; + oiSharePct: number; + wowNetDelta: string; +} + +export interface GoldSessionRange { + dayHigh: number; + dayLow: number; + prevClose: number; +} + +export interface GoldReturns { + w1: number; + m1: number; + ytd: number; + y1: number; +} + +export interface GoldRange52w { + hi: number; + lo: number; + positionPct: number; +} + +export interface GoldDriver { + symbol: string; + label: string; + value: number; + changePct: number; + correlation30d: number; } export interface FieldViolation { diff --git a/tests/gold-intelligence-seed.test.mjs b/tests/gold-intelligence-seed.test.mjs new file mode 100644 index 000000000..e8bc323d3 --- /dev/null +++ b/tests/gold-intelligence-seed.test.mjs @@ -0,0 +1,135 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +import { buildInstrument, computeNextCotRelease } from '../scripts/seed-cot.mjs'; + +describe('seed-cot: computeNextCotRelease', () => { + it('returns report date + 3 days for a Tuesday report', () => { + // 2026-04-07 is a Tuesday; next Friday release is 2026-04-10 + assert.equal(computeNextCotRelease('2026-04-07'), '2026-04-10'); + }); + + it('handles month rollover', () => { + assert.equal(computeNextCotRelease('2026-03-31'), '2026-04-03'); + }); + + it('returns empty for empty input', () => { + assert.equal(computeNextCotRelease(''), ''); + }); + + it('returns empty for invalid date', () => { + assert.equal(computeNextCotRelease('not-a-date'), ''); + }); +}); + +describe('seed-cot: buildInstrument (commodity kind)', () => { + const gcTarget = { name: 'Gold', code: 'GC' }; + + it('computes managed money net % and OI share', () => { + const current = { + report_date_as_yyyy_mm_dd: '2026-04-07', + open_interest_all: '600000', + m_money_positions_long_all: '200000', + m_money_positions_short_all: '50000', + swap_positions_long_all: '30000', + swap__positions_short_all: '180000', + }; + const inst = buildInstrument(gcTarget, current, null, 'commodity'); + assert.equal(inst.code, 'GC'); + assert.equal(inst.openInterest, 600000); + assert.equal(inst.nextReleaseDate, '2026-04-10'); + // MM: (200000-50000)/(250000) = 60% + assert.equal(inst.managedMoney.netPct, 60); + // MM OI share: 250000/600000 = 41.67% + assert.ok(Math.abs(inst.managedMoney.oiSharePct - 41.67) < 0.05); + // Producer/Swap: (30000-180000)/(210000) ≈ -71.43% + assert.ok(Math.abs(inst.producerSwap.netPct - -71.43) < 0.05); + assert.equal(inst.managedMoney.wowNetDelta, 0); + }); + + it('computes WoW net delta from prior row', () => { + const current = { + report_date_as_yyyy_mm_dd: '2026-04-07', + open_interest_all: '600000', + m_money_positions_long_all: '200000', + m_money_positions_short_all: '50000', + swap_positions_long_all: '30000', + swap__positions_short_all: '180000', + }; + const prior = { + report_date_as_yyyy_mm_dd: '2026-03-31', + m_money_positions_long_all: '180000', + m_money_positions_short_all: '60000', + swap_positions_long_all: '40000', + swap__positions_short_all: '170000', + }; + const inst = buildInstrument(gcTarget, current, prior, 'commodity'); + // Prior MM net = 180000-60000 = 120000; current = 200000-50000 = 150000; delta = +30000 + assert.equal(inst.managedMoney.wowNetDelta, 30000); + // Prior P/S net = 40000-170000 = -130000; current = 30000-180000 = -150000; delta = -20000 + assert.equal(inst.producerSwap.wowNetDelta, -20000); + }); + + it('builds financial instrument from TFF fields', () => { + const target = { name: '10-Year T-Note', code: 'ZN' }; + const current = { + report_date_as_yyyy_mm_dd: '2026-04-07', + open_interest_all: '5000000', + asset_mgr_positions_long: '1500000', + asset_mgr_positions_short: '500000', + dealer_positions_long_all: '400000', + dealer_positions_short_all: '1600000', + }; + const inst = buildInstrument(target, current, null, 'financial'); + assert.equal(inst.managedMoney.longPositions, 1500000); + assert.equal(inst.producerSwap.longPositions, 400000); + assert.equal(inst.managedMoney.netPct, 50); // (1.5M-0.5M)/2M + }); + + it('preserves leveragedFunds fields for financial TFF consumers', () => { + const target = { name: '10-Year T-Note', code: 'ZN' }; + const current = { + report_date_as_yyyy_mm_dd: '2026-04-07', + open_interest_all: '5000000', + asset_mgr_positions_long: '1500000', + asset_mgr_positions_short: '500000', + lev_money_positions_long: '750000', + lev_money_positions_short: '250000', + dealer_positions_long_all: '400000', + dealer_positions_short_all: '1600000', + }; + const inst = buildInstrument(target, current, null, 'financial'); + // Regression guard: CotPositioningPanel reads these for the Leveraged Funds bar. + assert.equal(inst.leveragedFundsLong, 750000); + assert.equal(inst.leveragedFundsShort, 250000); + }); + + it('commodity instruments emit leveragedFunds as 0 (no equivalent field in disaggregated report)', () => { + const current = { + report_date_as_yyyy_mm_dd: '2026-04-07', + open_interest_all: '100000', + m_money_positions_long_all: '10000', + m_money_positions_short_all: '5000', + swap_positions_long_all: '2000', + swap__positions_short_all: '8000', + }; + const inst = buildInstrument(gcTarget, current, null, 'commodity'); + assert.equal(inst.leveragedFundsLong, 0); + assert.equal(inst.leveragedFundsShort, 0); + }); + + it('preserves legacy flat fields for backward compat', () => { + const current = { + report_date_as_yyyy_mm_dd: '2026-04-07', + open_interest_all: '100000', + m_money_positions_long_all: '10000', + m_money_positions_short_all: '5000', + swap_positions_long_all: '2000', + swap__positions_short_all: '8000', + }; + const inst = buildInstrument(gcTarget, current, null, 'commodity'); + assert.equal(inst.assetManagerLong, 10000); + assert.equal(inst.dealerShort, 8000); + assert.equal(inst.netPct, inst.managedMoney.netPct); + }); +});