diff --git a/api/bootstrap.js b/api/bootstrap.js index c39197e87..c07e5daba 100644 --- a/api/bootstrap.js +++ b/api/bootstrap.js @@ -87,6 +87,7 @@ const BOOTSTRAP_CACHE_KEYS = { eurostatIndProd: 'economic:eurostat:industrial-production:v1', marketImplications: 'intelligence:market-implications:v1', fearGreedIndex: 'market:fear-greed:v1', + hyperliquidFlow: 'market:hyperliquid:flow:v1', crudeInventories: 'economic:crude-inventories:v1', natGasStorage: 'economic:nat-gas-storage:v1', ecbFxRates: 'economic:ecb-fx-rates:v1', @@ -137,6 +138,7 @@ const SLOW_KEYS = new Set([ 'eurostatIndProd', 'marketImplications', 'fearGreedIndex', + 'hyperliquidFlow', 'crudeInventories', 'natGasStorage', 'ecbFxRates', diff --git a/api/health.js b/api/health.js index c42ace295..73c789b12 100644 --- a/api/health.js +++ b/api/health.js @@ -71,6 +71,7 @@ const BOOTSTRAP_KEYS = { earningsCalendar: 'market:earnings-calendar:v1', econCalendar: 'economic:econ-calendar:v1', cotPositioning: 'market:cot:v1', + hyperliquidFlow: 'market:hyperliquid:flow:v1', crudeInventories: 'economic:crude-inventories:v1', natGasStorage: 'economic:nat-gas-storage:v1', spr: 'economic:spr:v1', @@ -299,6 +300,7 @@ const SEED_META = { earningsCalendar: { key: 'seed-meta:market:earnings-calendar', maxStaleMin: 1440 }, // 12h cron; 1440min = 24h = 2x interval econCalendar: { key: 'seed-meta:economic:econ-calendar', maxStaleMin: 1440 }, // 12h cron; 1440min = 24h = 2x interval cotPositioning: { key: 'seed-meta:market:cot', maxStaleMin: 14400 }, // weekly CFTC release; 14400min = 10d = 1.4x interval (weekend + delay buffer) + hyperliquidFlow: { key: 'seed-meta:market:hyperliquid-flow', maxStaleMin: 15 }, // Railway cron 5min; 15min = 3x interval crudeInventories: { key: 'seed-meta:economic:crude-inventories', maxStaleMin: 20160 }, // weekly EIA data; 20160min = 14 days = 2x weekly cadence natGasStorage: { key: 'seed-meta:economic:nat-gas-storage', maxStaleMin: 20160 }, // weekly EIA data; 20160min = 14 days = 2x weekly cadence spr: { key: 'seed-meta:economic:spr', maxStaleMin: 20160 }, // weekly EIA data; 20160min = 14 days = 2x weekly cadence @@ -382,6 +384,9 @@ const ON_DEMAND_KEYS = new Set([ // gate as on-demand so a deploy-order race or first-cron-run failure doesn't // fire a CRIT health alarm. Remove from this set after ~7 days of clean // production cron runs (verify via `seed-meta:economic:fx-yoy.fetchedAt`). + 'hyperliquidFlow', // TRANSITIONAL: seed-hyperliquid-flow runs inside seed-bundle-market-backup on + // Railway; gate as on-demand so initial deploy-order race or first cold-start + // snapshot doesn't CRIT. Remove after ~7 days of clean production cron runs. ]); // Keys where 0 records is a valid healthy state (e.g. no airports closed, diff --git a/api/seed-health.js b/api/seed-health.js index 5900a68de..5a935e5b5 100644 --- a/api/seed-health.js +++ b/api/seed-health.js @@ -21,6 +21,7 @@ const SEED_DOMAINS = { 'unrest:events': { key: 'seed-meta:unrest:events', intervalMin: 15 }, 'cyber:threats': { key: 'seed-meta:cyber:threats', intervalMin: 240 }, 'market:crypto': { key: 'seed-meta:market:crypto', intervalMin: 15 }, + 'market:hyperliquid-flow': { key: 'seed-meta:market:hyperliquid-flow', intervalMin: 5 }, // Railway cron 5min via seed-bundle-market-backup 'market:etf-flows': { key: 'seed-meta:market:etf-flows', intervalMin: 30 }, 'market:gulf-quotes': { key: 'seed-meta:market:gulf-quotes', intervalMin: 15 }, 'market:stablecoins': { key: 'seed-meta:market:stablecoins', intervalMin: 30 }, diff --git a/docs/api/MarketService.openapi.json b/docs/api/MarketService.openapi.json index bf59e4b6f..b8722e750 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":{"cbReserves":{"$ref":"#/components/schemas/GoldCbReserves"},"cot":{"$ref":"#/components/schemas/GoldCotPositioning"},"crossCurrencyPrices":{"items":{"$ref":"#/components/schemas/GoldCrossCurrencyPrice"},"type":"array"},"drivers":{"items":{"$ref":"#/components/schemas/GoldDriver"},"type":"array"},"etfFlows":{"$ref":"#/components/schemas/GoldEtfFlows"},"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"},"GoldCbHolder":{"properties":{"iso3":{"type":"string"},"name":{"type":"string"},"pctOfReserves":{"format":"double","type":"number"},"tonnes":{"format":"double","type":"number"}},"type":"object"},"GoldCbMover":{"properties":{"deltaTonnes12m":{"format":"double","type":"number"},"iso3":{"type":"string"},"name":{"type":"string"}},"type":"object"},"GoldCbReserves":{"properties":{"asOfMonth":{"type":"string"},"topBuyers12m":{"items":{"$ref":"#/components/schemas/GoldCbMover"},"type":"array"},"topHolders":{"items":{"$ref":"#/components/schemas/GoldCbHolder"},"type":"array"},"topSellers12m":{"items":{"$ref":"#/components/schemas/GoldCbMover"},"type":"array"},"totalTonnes":{"format":"double","type":"number"}},"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"},"GoldEtfFlows":{"properties":{"asOfDate":{"type":"string"},"aumUsd":{"format":"double","type":"number"},"changeM1Pct":{"format":"double","type":"number"},"changeM1Tonnes":{"format":"double","type":"number"},"changeW1Pct":{"format":"double","type":"number"},"changeW1Tonnes":{"format":"double","type":"number"},"changeY1Pct":{"format":"double","type":"number"},"changeY1Tonnes":{"format":"double","type":"number"},"nav":{"format":"double","type":"number"},"sparkline90d":{"items":{"format":"double","type":"number"},"type":"array"},"tonnes":{"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 +{"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":{"cbReserves":{"$ref":"#/components/schemas/GoldCbReserves"},"cot":{"$ref":"#/components/schemas/GoldCotPositioning"},"crossCurrencyPrices":{"items":{"$ref":"#/components/schemas/GoldCrossCurrencyPrice"},"type":"array"},"drivers":{"items":{"$ref":"#/components/schemas/GoldDriver"},"type":"array"},"etfFlows":{"$ref":"#/components/schemas/GoldEtfFlows"},"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"},"GetHyperliquidFlowRequest":{"type":"object"},"GetHyperliquidFlowResponse":{"properties":{"assetCount":{"format":"int32","type":"integer"},"assets":{"items":{"$ref":"#/components/schemas/HyperliquidAssetFlow"},"type":"array"},"fetchedAt":{"type":"string"},"ts":{"format":"int64","type":"string"},"unavailable":{"type":"boolean"},"warmup":{"type":"boolean"}},"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"},"GoldCbHolder":{"properties":{"iso3":{"type":"string"},"name":{"type":"string"},"pctOfReserves":{"format":"double","type":"number"},"tonnes":{"format":"double","type":"number"}},"type":"object"},"GoldCbMover":{"properties":{"deltaTonnes12m":{"format":"double","type":"number"},"iso3":{"type":"string"},"name":{"type":"string"}},"type":"object"},"GoldCbReserves":{"properties":{"asOfMonth":{"type":"string"},"topBuyers12m":{"items":{"$ref":"#/components/schemas/GoldCbMover"},"type":"array"},"topHolders":{"items":{"$ref":"#/components/schemas/GoldCbHolder"},"type":"array"},"topSellers12m":{"items":{"$ref":"#/components/schemas/GoldCbMover"},"type":"array"},"totalTonnes":{"format":"double","type":"number"}},"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"},"GoldEtfFlows":{"properties":{"asOfDate":{"type":"string"},"aumUsd":{"format":"double","type":"number"},"changeM1Pct":{"format":"double","type":"number"},"changeM1Tonnes":{"format":"double","type":"number"},"changeW1Pct":{"format":"double","type":"number"},"changeW1Tonnes":{"format":"double","type":"number"},"changeY1Pct":{"format":"double","type":"number"},"changeY1Tonnes":{"format":"double","type":"number"},"nav":{"format":"double","type":"number"},"sparkline90d":{"items":{"format":"double","type":"number"},"type":"array"},"tonnes":{"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"},"HyperliquidAssetFlow":{"properties":{"alerts":{"items":{"type":"string"},"type":"array"},"assetClass":{"type":"string"},"basisScore":{"format":"double","type":"number"},"composite":{"format":"double","type":"number"},"dayNotional":{"type":"string"},"display":{"type":"string"},"funding":{"description":"Raw metrics (nullable as strings to preserve precision; \"\" = unavailable)","type":"string"},"fundingScore":{"description":"Component scores 0-100","format":"double","type":"number"},"group":{"type":"string"},"markPx":{"type":"string"},"missingPolls":{"format":"int32","type":"integer"},"oiScore":{"format":"double","type":"number"},"openInterest":{"type":"string"},"oraclePx":{"type":"string"},"sparkFunding":{"items":{"description":"Sparkline arrays (most recent last); up to 60 samples (5h @ 5min)","format":"double","type":"number"},"type":"array"},"sparkOi":{"items":{"format":"double","type":"number"},"type":"array"},"sparkScore":{"items":{"format":"double","type":"number"},"type":"array"},"stale":{"type":"boolean"},"staleSince":{"format":"int64","type":"string"},"symbol":{"type":"string"},"volumeScore":{"format":"double","type":"number"},"warmup":{"description":"State flags","type":"boolean"}},"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-hyperliquid-flow":{"get":{"description":"GetHyperliquidFlow retrieves Hyperliquid perp positioning flow (funding/OI/basis composite scores).","operationId":"GetHyperliquidFlow","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetHyperliquidFlowResponse"}}},"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":"GetHyperliquidFlow","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 1228906fa..4b6372e08 100644 --- a/docs/api/MarketService.openapi.yaml +++ b/docs/api/MarketService.openapi.yaml @@ -696,6 +696,32 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /api/market/v1/get-hyperliquid-flow: + get: + tags: + - MarketService + summary: GetHyperliquidFlow + description: GetHyperliquidFlow retrieves Hyperliquid perp positioning flow (funding/OI/basis composite scores). + operationId: GetHyperliquidFlow + responses: + "200": + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/GetHyperliquidFlowResponse' + "400": + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + default: + description: Error response + content: + application/json: + schema: + $ref: '#/components/schemas/Error' components: schemas: Error: @@ -2130,3 +2156,93 @@ components: deltaTonnes12m: type: number format: double + GetHyperliquidFlowRequest: + type: object + GetHyperliquidFlowResponse: + type: object + properties: + ts: + type: string + format: int64 + fetchedAt: + type: string + warmup: + type: boolean + assetCount: + type: integer + format: int32 + assets: + type: array + items: + $ref: '#/components/schemas/HyperliquidAssetFlow' + unavailable: + type: boolean + HyperliquidAssetFlow: + type: object + properties: + symbol: + type: string + display: + type: string + assetClass: + type: string + group: + type: string + funding: + type: string + description: Raw metrics (nullable as strings to preserve precision; "" = unavailable) + openInterest: + type: string + markPx: + type: string + oraclePx: + type: string + dayNotional: + type: string + fundingScore: + type: number + format: double + description: Component scores 0-100 + volumeScore: + type: number + format: double + oiScore: + type: number + format: double + basisScore: + type: number + format: double + composite: + type: number + format: double + sparkFunding: + type: array + items: + type: number + format: double + description: Sparkline arrays (most recent last); up to 60 samples (5h @ 5min) + sparkOi: + type: array + items: + type: number + format: double + sparkScore: + type: array + items: + type: number + format: double + warmup: + type: boolean + description: State flags + stale: + type: boolean + staleSince: + type: string + format: int64 + missingPolls: + type: integer + format: int32 + alerts: + type: array + items: + type: string diff --git a/proto/worldmonitor/market/v1/get_hyperliquid_flow.proto b/proto/worldmonitor/market/v1/get_hyperliquid_flow.proto new file mode 100644 index 000000000..3cfc648b7 --- /dev/null +++ b/proto/worldmonitor/market/v1/get_hyperliquid_flow.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +package worldmonitor.market.v1; + +import "sebuf/http/annotations.proto"; + +message GetHyperliquidFlowRequest {} + +message HyperliquidAssetFlow { + string symbol = 1; + string display = 2; + string asset_class = 3; // "crypto" or "commodity" + string group = 4; // "crypto" / "oil" / "metals" / "industrial" / "gas" / "fx" + // Raw metrics (nullable as strings to preserve precision; "" = unavailable) + string funding = 5; // hourly funding rate + string open_interest = 6; + string mark_px = 7; + string oracle_px = 8; + string day_notional = 9; // 24h notional volume in USD + // Component scores 0-100 + double funding_score = 10; + double volume_score = 11; + double oi_score = 12; + double basis_score = 13; + double composite = 14; // weighted composite 0-100 + // Sparkline arrays (most recent last); up to 60 samples (5h @ 5min) + repeated double spark_funding = 15; + repeated double spark_oi = 16; + repeated double spark_score = 17; + // State flags + bool warmup = 18; // true while baselines are being built (cold start) + bool stale = 19; // upstream omitted this symbol; carrying forward + int64 stale_since = 20; // epoch ms when stale flag first set; 0 if never stale + int32 missing_polls = 21; + repeated string alerts = 22; +} + +message GetHyperliquidFlowResponse { + int64 ts = 1; // snapshot timestamp (epoch ms) + string fetched_at = 2; // ISO-8601 of `ts` + bool warmup = 3; // true while any asset is in warmup state + int32 asset_count = 4; + repeated HyperliquidAssetFlow assets = 5; + bool unavailable = 6; // true when seed key is empty/missing +} diff --git a/proto/worldmonitor/market/v1/service.proto b/proto/worldmonitor/market/v1/service.proto index 5e2139f8c..d1f713c7c 100644 --- a/proto/worldmonitor/market/v1/service.proto +++ b/proto/worldmonitor/market/v1/service.proto @@ -25,6 +25,7 @@ import "worldmonitor/market/v1/get_cot_positioning.proto"; import "worldmonitor/market/v1/get_insider_transactions.proto"; import "worldmonitor/market/v1/get_market_breadth_history.proto"; import "worldmonitor/market/v1/get_gold_intelligence.proto"; +import "worldmonitor/market/v1/get_hyperliquid_flow.proto"; // MarketService provides APIs for financial market data from Finnhub, Yahoo Finance, and CoinGecko. service MarketService { @@ -139,4 +140,9 @@ service MarketService { rpc GetGoldIntelligence(GetGoldIntelligenceRequest) returns (GetGoldIntelligenceResponse) { option (sebuf.http.config) = {path: "/get-gold-intelligence", method: HTTP_METHOD_GET}; } + + // GetHyperliquidFlow retrieves Hyperliquid perp positioning flow (funding/OI/basis composite scores). + rpc GetHyperliquidFlow(GetHyperliquidFlowRequest) returns (GetHyperliquidFlowResponse) { + option (sebuf.http.config) = {path: "/get-hyperliquid-flow", method: HTTP_METHOD_GET}; + } } diff --git a/scripts/seed-bundle-market-backup.mjs b/scripts/seed-bundle-market-backup.mjs index ca19533af..53b00ca67 100644 --- a/scripts/seed-bundle-market-backup.mjs +++ b/scripts/seed-bundle-market-backup.mjs @@ -3,6 +3,7 @@ import { runBundle, MIN } from './_bundle-runner.mjs'; await runBundle('market-backup', [ { label: 'Crypto-Quotes', script: 'seed-crypto-quotes.mjs', seedMetaKey: 'market:crypto', intervalMs: 5 * MIN, timeoutMs: 120_000 }, + { label: 'Hyperliquid-Flow', script: 'seed-hyperliquid-flow.mjs', seedMetaKey: 'market:hyperliquid-flow', intervalMs: 5 * MIN, timeoutMs: 60_000 }, { label: 'Stablecoin-Markets', script: 'seed-stablecoin-markets.mjs', seedMetaKey: 'market:stablecoins', intervalMs: 10 * MIN, timeoutMs: 120_000 }, { label: 'ETF-Flows', script: 'seed-etf-flows.mjs', seedMetaKey: 'market:etf-flows', intervalMs: 15 * MIN, timeoutMs: 120_000 }, { label: 'Gulf-Quotes', script: 'seed-gulf-quotes.mjs', seedMetaKey: 'market:gulf-quotes', intervalMs: 10 * MIN, timeoutMs: 120_000 }, diff --git a/scripts/seed-hyperliquid-flow.mjs b/scripts/seed-hyperliquid-flow.mjs new file mode 100644 index 000000000..d56a6b8ad --- /dev/null +++ b/scripts/seed-hyperliquid-flow.mjs @@ -0,0 +1,333 @@ +#!/usr/bin/env node +// @ts-check +/** + * Hyperliquid perp positioning flow seeder. + * + * Polls the public Hyperliquid /info endpoint every 5 minutes, computes a + * 4-component composite "positioning stress" score (funding / volume / OI / + * basis) per asset, and publishes a self-contained snapshot — current metrics + * plus short per-asset sparkline arrays for funding, OI and score. + * + * Used as a leading indicator for commodities / crypto / FX in CommoditiesPanel. + */ + +import { loadEnvFile, runSeed, readSeedSnapshot } from './_seed-utils.mjs'; + +loadEnvFile(import.meta.url); + +export const CANONICAL_KEY = 'market:hyperliquid:flow:v1'; +export const CACHE_TTL_SECONDS = 2700; // 9× cron cadence (5 min); honest grace window +export const SPARK_MAX = 60; // 5h @ 5min +export const HYPERLIQUID_URL = 'https://api.hyperliquid.xyz/info'; +export const REQUEST_TIMEOUT_MS = 15_000; +export const MIN_NOTIONAL_USD_24H = 500_000; +export const STALE_SYMBOL_DROP_AFTER_POLLS = 3; +export const VOLUME_BASELINE_MIN_SAMPLES = 12; // 1h @ 5min cadence — minimum history to score volume spike +export const MAX_UPSTREAM_UNIVERSE = 2000; // defensive cap; Hyperliquid has ~200 perps today + +// Hardcoded symbol whitelist — never iterate the full universe. +// `class`: scoring threshold class. `display`: UI label. `group`: panel section. +export const ASSETS = [ + { symbol: 'BTC', class: 'crypto', display: 'BTC', group: 'crypto' }, + { symbol: 'ETH', class: 'crypto', display: 'ETH', group: 'crypto' }, + { symbol: 'SOL', class: 'crypto', display: 'SOL', group: 'crypto' }, + { symbol: 'PAXG', class: 'commodity', display: 'PAXG (gold)', group: 'metals' }, + { symbol: 'xyz:CL', class: 'commodity', display: 'WTI Crude', group: 'oil' }, + { symbol: 'xyz:BRENTOIL', class: 'commodity', display: 'Brent Crude', group: 'oil' }, + { symbol: 'xyz:GOLD', class: 'commodity', display: 'Gold', group: 'metals' }, + { symbol: 'xyz:SILVER', class: 'commodity', display: 'Silver', group: 'metals' }, + { symbol: 'xyz:PLATINUM', class: 'commodity', display: 'Platinum', group: 'metals' }, + { symbol: 'xyz:PALLADIUM', class: 'commodity', display: 'Palladium', group: 'metals' }, + { symbol: 'xyz:COPPER', class: 'commodity', display: 'Copper', group: 'industrial' }, + { symbol: 'xyz:NATGAS', class: 'commodity', display: 'Natural Gas', group: 'gas' }, + { symbol: 'xyz:EUR', class: 'commodity', display: 'EUR', group: 'fx' }, + { symbol: 'xyz:JPY', class: 'commodity', display: 'JPY', group: 'fx' }, +]; + +// Risk weights — must sum to 1.0 +export const WEIGHTS = { funding: 0.30, volume: 0.25, oi: 0.25, basis: 0.20 }; + +export const THRESHOLDS = { + crypto: { funding: 0.001, volume: 5.0, oi: 0.20, basis: 0.05 }, + commodity: { funding: 0.0005, volume: 3.0, oi: 0.15, basis: 0.03 }, +}; + +export const ALERT_THRESHOLD = 60; + +// ── Pure scoring helpers ────────────────────────────────────────────────────── + +export function clamp(x, lo = 0, hi = 100) { + if (!Number.isFinite(x)) return 0; + return Math.max(lo, Math.min(hi, x)); +} + +export function scoreFunding(rate, threshold) { + if (!Number.isFinite(rate) || threshold <= 0) return 0; + return clamp((Math.abs(rate) / threshold) * 100); +} + +export function scoreVolume(currentVol, avgVol, threshold) { + if (!Number.isFinite(currentVol) || !(avgVol > 0) || threshold <= 0) return 0; + return clamp(((currentVol / avgVol) / threshold) * 100); +} + +export function scoreOi(currentOi, prevOi, threshold) { + if (!Number.isFinite(currentOi) || !(prevOi > 0) || threshold <= 0) return 0; + return clamp((Math.abs(currentOi - prevOi) / prevOi / threshold) * 100); +} + +export function scoreBasis(mark, oracle, threshold) { + if (!Number.isFinite(mark) || !(oracle > 0) || threshold <= 0) return 0; + return clamp((Math.abs(mark - oracle) / oracle / threshold) * 100); +} + +/** + * Compute composite score and alerts for one asset. + * + * `prevAsset` may be null/undefined for cold start; in that case OI delta and + * volume spike are scored as 0 (we lack baselines). + * + * Per-asset `warmup` is TRUE until the volume baseline has VOLUME_BASELINE_MIN_SAMPLES + * and there is a prior OI to compute delta against — NOT just on the first poll after + * cold start. Without this, the "warming up" badge flips to false on poll 2 while the + * score is still missing most of its baseline. + * + * @param {{ symbol: string; display: string; class: 'crypto'|'commodity'; group: string }} meta + * @param {Record} ctx + * @param {any} prevAsset + * @param {{ coldStart?: boolean }} [opts] + */ +export function computeAsset(meta, ctx, prevAsset, opts = {}) { + const t = THRESHOLDS[meta.class]; + const fundingRate = Number(ctx.funding); + const currentOi = Number(ctx.openInterest); + const markPx = Number(ctx.markPx); + const oraclePx = Number(ctx.oraclePx); + const dayNotional = Number(ctx.dayNtlVlm); + const prevOi = prevAsset?.openInterest ?? null; + const prevVolSamples = /** @type {number[]} */ ((prevAsset?.sparkVol || []).filter( + /** @param {unknown} v */ (v) => Number.isFinite(v) + )); + + const fundingScore = scoreFunding(fundingRate, t.funding); + + // Volume spike scored against the MOST RECENT 12 samples in sparkVol. + // sparkVol is newest-at-tail (see shiftAndAppend), so we must slice(-N) — NOT + // slice(0, N), which would anchor the baseline to the oldest window and never + // update after the first hour. + let volumeScore = 0; + const volumeBaselineReady = prevVolSamples.length >= VOLUME_BASELINE_MIN_SAMPLES; + if (dayNotional >= MIN_NOTIONAL_USD_24H && volumeBaselineReady) { + const recent = prevVolSamples.slice(-VOLUME_BASELINE_MIN_SAMPLES); + const avg = recent.reduce((a, b) => a + b, 0) / recent.length; + volumeScore = scoreVolume(dayNotional, avg, t.volume); + } + + const oiScore = prevOi != null ? scoreOi(currentOi, prevOi, t.oi) : 0; + const basisScore = scoreBasis(markPx, oraclePx, t.basis); + + const composite = clamp( + fundingScore * WEIGHTS.funding + + volumeScore * WEIGHTS.volume + + oiScore * WEIGHTS.oi + + basisScore * WEIGHTS.basis, + ); + + const sparkFunding = shiftAndAppend(prevAsset?.sparkFunding, Number.isFinite(fundingRate) ? fundingRate : 0); + const sparkOi = shiftAndAppend(prevAsset?.sparkOi, Number.isFinite(currentOi) ? currentOi : 0); + const sparkScore = shiftAndAppend(prevAsset?.sparkScore, composite); + const sparkVol = shiftAndAppend(prevAsset?.sparkVol, Number.isFinite(dayNotional) ? dayNotional : 0); + + // Warmup stays TRUE until both baselines are usable — cold-start OR insufficient + // volume history OR missing prior OI. Clears only when the asset can produce all + // four component scores. + const warmup = opts.coldStart === true || !volumeBaselineReady || prevOi == null; + + const alerts = []; + if (composite >= ALERT_THRESHOLD) { + alerts.push(`HIGH RISK ${composite.toFixed(0)}/100`); + } + + return { + symbol: meta.symbol, + display: meta.display, + class: meta.class, + group: meta.group, + funding: Number.isFinite(fundingRate) ? fundingRate : null, + openInterest: Number.isFinite(currentOi) ? currentOi : null, + markPx: Number.isFinite(markPx) ? markPx : null, + oraclePx: Number.isFinite(oraclePx) ? oraclePx : null, + dayNotional: Number.isFinite(dayNotional) ? dayNotional : null, + fundingScore, + volumeScore, + oiScore, + basisScore, + composite, + sparkFunding, + sparkOi, + sparkScore, + sparkVol, + stale: false, + staleSince: null, + missingPolls: 0, + alerts, + warmup, + }; +} + +function shiftAndAppend(prev, value) { + const arr = Array.isArray(prev) ? prev.slice(-(SPARK_MAX - 1)) : []; + arr.push(value); + return arr; +} + +// ── Hyperliquid client ──────────────────────────────────────────────────────── + +export async function fetchHyperliquidMetaAndCtxs(fetchImpl = fetch) { + const resp = await fetchImpl(HYPERLIQUID_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': 'WorldMonitor/1.0 (+https://worldmonitor.app)', + }, + body: JSON.stringify({ type: 'metaAndAssetCtxs' }), + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!resp.ok) throw new Error(`Hyperliquid HTTP ${resp.status}`); + const ct = resp.headers?.get?.('content-type') || ''; + if (!ct.toLowerCase().includes('application/json')) { + throw new Error(`Hyperliquid wrong content-type: ${ct || ''}`); + } + const json = await resp.json(); + return json; +} + +/** + * Strict shape validation. Hyperliquid returns `[meta, assetCtxs]` where + * meta = { universe: [{ name, ... }, ...] } + * assetCtxs = [{ funding, openInterest, markPx, oraclePx, dayNtlVlm, ... }, ...] + * with assetCtxs[i] aligned to universe[i]. + * + * Throws on any mismatch — never persist a partial / malformed payload. + */ +export function validateUpstream(raw) { + if (!Array.isArray(raw) || raw.length < 2) { + throw new Error('Hyperliquid payload not a [meta, assetCtxs] tuple'); + } + const [meta, assetCtxs] = raw; + if (!meta || !Array.isArray(meta.universe)) { + throw new Error('Hyperliquid meta.universe missing or not array'); + } + if (meta.universe.length < 50) { + throw new Error(`Hyperliquid universe suspiciously small: ${meta.universe.length}`); + } + if (meta.universe.length > MAX_UPSTREAM_UNIVERSE) { + throw new Error(`Hyperliquid universe over cap: ${meta.universe.length} > ${MAX_UPSTREAM_UNIVERSE}`); + } + if (!Array.isArray(assetCtxs) || assetCtxs.length !== meta.universe.length) { + throw new Error('Hyperliquid assetCtxs length does not match universe'); + } + for (const m of meta.universe) { + if (typeof m?.name !== 'string') throw new Error('Hyperliquid universe entry missing name'); + } + return { universe: meta.universe, assetCtxs }; +} + +export function indexBySymbol({ universe, assetCtxs }) { + const out = new Map(); + for (let i = 0; i < universe.length; i++) { + out.set(universe[i].name, assetCtxs[i] || {}); + } + return out; +} + +// ── Main build path ────────────────────────────────────────────────────────── + +/** + * Build a fresh snapshot from the upstream payload + the previous Redis snapshot. + * Pure function — caller passes both inputs. + */ +export function buildSnapshot(upstream, prevSnapshot, opts = {}) { + const validated = validateUpstream(upstream); + const ctxBySymbol = indexBySymbol(validated); + const now = opts.now || Date.now(); + const prevByName = new Map(); + if (prevSnapshot?.assets && Array.isArray(prevSnapshot.assets)) { + for (const a of prevSnapshot.assets) prevByName.set(a.symbol, a); + } + const prevAgeMs = prevSnapshot?.ts ? now - prevSnapshot.ts : Infinity; + // Treat stale prior snapshot (>3× cadence = 900s) as cold start. + const coldStart = !prevSnapshot || prevAgeMs > 900_000; + + // Info-log unseen xyz: perps once per run so ops sees when Hyperliquid adds + // commodity/FX markets we could add to the whitelist. + const whitelisted = new Set(ASSETS.map((a) => a.symbol)); + const unknownXyz = validated.universe + .map((/** @type {{ name: string }} */ u) => u.name) + .filter((name) => typeof name === 'string' && name.startsWith('xyz:') && !whitelisted.has(name)); + if (unknownXyz.length > 0) { + console.log(` Unknown xyz: perps upstream (not whitelisted): ${unknownXyz.slice(0, 20).join(', ')}${unknownXyz.length > 20 ? ` (+${unknownXyz.length - 20} more)` : ''}`); + } + + const assets = []; + for (const meta of ASSETS) { + const ctx = ctxBySymbol.get(meta.symbol); + if (!ctx) { + // Whitelisted symbol absent from upstream — carry forward prior with stale flag. + const prev = prevByName.get(meta.symbol); + if (!prev) continue; // never seen, skip silently (don't synthesize) + const missing = (prev.missingPolls || 0) + 1; + if (missing >= STALE_SYMBOL_DROP_AFTER_POLLS) { + console.warn(` Dropping ${meta.symbol} — missing for ${missing} consecutive polls`); + continue; + } + assets.push({ + ...prev, + stale: true, + staleSince: prev.staleSince || now, + missingPolls: missing, + }); + continue; + } + const prev = coldStart ? null : prevByName.get(meta.symbol); + const asset = computeAsset(meta, ctx, prev, { coldStart }); + assets.push(asset); + } + + // Snapshot warmup = any asset still building a baseline. Reflects real + // component-score readiness, not just the first poll after cold start. + const warmup = assets.some((a) => a.warmup === true); + + return { + ts: now, + fetchedAt: new Date(now).toISOString(), + warmup, + assetCount: assets.length, + assets, + }; +} + +export function validateFn(snapshot) { + return !!snapshot && Array.isArray(snapshot.assets) && snapshot.assets.length >= 12; +} + +// ── Entry point ────────────────────────────────────────────────────────────── + +const isMain = process.argv[1]?.endsWith('seed-hyperliquid-flow.mjs'); +if (isMain) { + const prevSnapshot = await readSeedSnapshot(CANONICAL_KEY); + await runSeed('market', 'hyperliquid-flow', CANONICAL_KEY, async () => { + const upstream = await fetchHyperliquidMetaAndCtxs(); + return buildSnapshot(upstream, prevSnapshot); + }, { + ttlSeconds: CACHE_TTL_SECONDS, + validateFn, + sourceVersion: 'hyperliquid-info-metaAndAssetCtxs-v1', + recordCount: (snap) => snap?.assets?.length || 0, + }).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/server/_shared/cache-keys.ts b/server/_shared/cache-keys.ts index 7278707f8..ed73efdaf 100644 --- a/server/_shared/cache-keys.ts +++ b/server/_shared/cache-keys.ts @@ -189,6 +189,7 @@ export const BOOTSTRAP_CACHE_KEYS: Record = { nationalDebt: 'economic:national-debt:v1', marketImplications: 'intelligence:market-implications:v1', fearGreedIndex: 'market:fear-greed:v1', + hyperliquidFlow: 'market:hyperliquid:flow:v1', crudeInventories: 'economic:crude-inventories:v1', natGasStorage: 'economic:nat-gas-storage:v1', ecbFxRates: 'economic:ecb-fx-rates:v1', @@ -257,6 +258,7 @@ export const BOOTSTRAP_TIERS: Record = { nationalDebt: 'slow', marketImplications: 'slow', fearGreedIndex: 'slow', + hyperliquidFlow: 'slow', crudeInventories: 'slow', natGasStorage: 'slow', ecbFxRates: 'slow', diff --git a/server/gateway.ts b/server/gateway.ts index 279db64d5..86390119f 100644 --- a/server/gateway.ts +++ b/server/gateway.ts @@ -201,6 +201,7 @@ const RPC_CACHE_TIER: Record = { '/api/market/v1/list-earnings-calendar': 'slow', '/api/market/v1/get-cot-positioning': 'slow', '/api/market/v1/get-gold-intelligence': 'slow', + '/api/market/v1/get-hyperliquid-flow': 'medium', '/api/market/v1/get-insider-transactions': 'slow', '/api/economic/v1/get-economic-calendar': 'slow', '/api/intelligence/v1/list-market-implications': 'slow', diff --git a/server/worldmonitor/market/v1/get-hyperliquid-flow.ts b/server/worldmonitor/market/v1/get-hyperliquid-flow.ts new file mode 100644 index 000000000..e31cce594 --- /dev/null +++ b/server/worldmonitor/market/v1/get-hyperliquid-flow.ts @@ -0,0 +1,113 @@ +import type { + ServerContext, + GetHyperliquidFlowRequest, + GetHyperliquidFlowResponse, + HyperliquidAssetFlow, +} from '../../../../src/generated/server/worldmonitor/market/v1/service_server'; +import { getCachedJson } from '../../../_shared/redis'; + +const SEED_CACHE_KEY = 'market:hyperliquid:flow:v1'; + +interface SeededAsset { + symbol?: string; + display?: string; + class?: string; + group?: string; + funding?: number | null; + openInterest?: number | null; + markPx?: number | null; + oraclePx?: number | null; + dayNotional?: number | null; + fundingScore?: number; + volumeScore?: number; + oiScore?: number; + basisScore?: number; + composite?: number; + sparkFunding?: number[]; + sparkOi?: number[]; + sparkScore?: number[]; + warmup?: boolean; + stale?: boolean; + staleSince?: number | null; + missingPolls?: number; + alerts?: string[]; +} + +interface SeededSnapshot { + ts?: number; + fetchedAt?: string; + warmup?: boolean; + assetCount?: number; + assets?: SeededAsset[]; +} + +function numToStr(v: number | null | undefined): string { + return v == null || !Number.isFinite(v) ? '' : String(v); +} + +function arr(a: number[] | undefined): number[] { + return Array.isArray(a) ? a.filter((v) => Number.isFinite(v)) : []; +} + +export async function getHyperliquidFlow( + _ctx: ServerContext, + _req: GetHyperliquidFlowRequest, +): Promise { + try { + const raw = await getCachedJson(SEED_CACHE_KEY, true) as SeededSnapshot | null; + if (!raw?.assets || raw.assets.length === 0) { + // No error — seeder hasn't run yet, or empty snapshot. Distinguish from + // parse/Redis failures below (those hit the catch and log). + return { + ts: '0', + fetchedAt: '', + warmup: true, + assetCount: 0, + assets: [], + unavailable: true, + }; + } + const assets: HyperliquidAssetFlow[] = raw.assets.map((a) => ({ + symbol: String(a.symbol ?? ''), + display: String(a.display ?? ''), + assetClass: String(a.class ?? ''), + group: String(a.group ?? ''), + funding: numToStr(a.funding ?? null), + openInterest: numToStr(a.openInterest ?? null), + markPx: numToStr(a.markPx ?? null), + oraclePx: numToStr(a.oraclePx ?? null), + dayNotional: numToStr(a.dayNotional ?? null), + fundingScore: Number(a.fundingScore ?? 0), + volumeScore: Number(a.volumeScore ?? 0), + oiScore: Number(a.oiScore ?? 0), + basisScore: Number(a.basisScore ?? 0), + composite: Number(a.composite ?? 0), + sparkFunding: arr(a.sparkFunding), + sparkOi: arr(a.sparkOi), + sparkScore: arr(a.sparkScore), + warmup: Boolean(a.warmup), + stale: Boolean(a.stale), + staleSince: String(a.staleSince ?? 0), + missingPolls: Number(a.missingPolls ?? 0), + alerts: Array.isArray(a.alerts) ? a.alerts.map((x) => String(x)) : [], + })); + return { + ts: String(raw.ts ?? 0), + fetchedAt: String(raw.fetchedAt ?? ''), + warmup: Boolean(raw.warmup), + assetCount: assets.length, + assets, + unavailable: false, + }; + } catch (err) { + console.error('[getHyperliquidFlow] Redis read or parse failed:', err instanceof Error ? err.message : err); + return { + ts: '0', + fetchedAt: '', + warmup: true, + assetCount: 0, + assets: [], + unavailable: true, + }; + } +} diff --git a/server/worldmonitor/market/v1/handler.ts b/server/worldmonitor/market/v1/handler.ts index fc13dbca0..e9335b79c 100644 --- a/server/worldmonitor/market/v1/handler.ts +++ b/server/worldmonitor/market/v1/handler.ts @@ -35,6 +35,7 @@ import { getCotPositioning } from './get-cot-positioning'; import { getInsiderTransactions } from './get-insider-transactions'; import { getMarketBreadthHistory } from './get-market-breadth-history'; import { getGoldIntelligence } from './get-gold-intelligence'; +import { getHyperliquidFlow } from './get-hyperliquid-flow'; export const marketHandler: MarketServiceHandler = { listMarketQuotes, @@ -59,4 +60,5 @@ export const marketHandler: MarketServiceHandler = { getInsiderTransactions, getMarketBreadthHistory, getGoldIntelligence, + getHyperliquidFlow, }; diff --git a/src/App.ts b/src/App.ts index 9025782ae..2f6f57baf 100644 --- a/src/App.ts +++ b/src/App.ts @@ -29,6 +29,7 @@ import type { ServiceStatusPanel } from '@/components/ServiceStatusPanel'; import type { StablecoinPanel } from '@/components/StablecoinPanel'; import type { EnergyCrisisPanel } from '@/components/EnergyCrisisPanel'; import type { ETFFlowsPanel } from '@/components/ETFFlowsPanel'; +import type { CommoditiesPanel } from '@/components/MarketPanel'; import type { MacroSignalsPanel } from '@/components/MacroSignalsPanel'; import type { FearGreedPanel } from '@/components/FearGreedPanel'; import type { HormuzPanel } from '@/components/HormuzPanel'; @@ -265,6 +266,10 @@ export class App { const panel = this.state.panels['hormuz-tracker'] as HormuzPanel | undefined; if (panel) primeTask('hormuz-tracker', () => panel.fetchData()); } + if (shouldPrime('commodities')) { + const panel = this.state.panels['commodities'] as CommoditiesPanel | undefined; + if (panel) primeTask('commodities-hyperliquid-flow', () => panel.fetchHyperliquidFlow()); + } if (shouldPrime('etf-flows')) { const panel = this.state.panels['etf-flows'] as ETFFlowsPanel | undefined; if (panel) primeTask('etf-flows', () => panel.fetchData()); @@ -1297,6 +1302,12 @@ export class App { REFRESH_INTERVALS.hormuzTracker, () => this.isPanelNearViewport('hormuz-tracker') ); + this.refreshScheduler.scheduleRefresh( + 'commodities-hyperliquid-flow', + () => (this.state.panels['commodities'] as CommoditiesPanel).fetchHyperliquidFlow(), + REFRESH_INTERVALS.hyperliquidFlow, + () => this.isPanelNearViewport('commodities') + ); this.refreshScheduler.scheduleRefresh( 'strategic-posture', () => (this.state.panels['strategic-posture'] as StrategicPosturePanel).refresh(), diff --git a/src/components/MarketPanel.ts b/src/components/MarketPanel.ts index 11580a35b..ec0a4db54 100644 --- a/src/components/MarketPanel.ts +++ b/src/components/MarketPanel.ts @@ -5,6 +5,7 @@ import { formatPrice, formatChange, getChangeClass, getHeatmapClass } from '@/ut import { escapeHtml } from '@/utils/sanitize'; import { miniSparkline } from '@/utils/sparkline'; import { SITE_VARIANT } from '@/config'; +import { getHydratedData } from '@/services/bootstrap'; import { getMarketWatchlistEntries, parseMarketWatchlistInput, @@ -343,7 +344,117 @@ interface EcbFxRateItem { change1d?: number | null; } -type CommoditiesTab = 'commodities' | 'fx' | 'xau'; +type CommoditiesTab = 'commodities' | 'fx' | 'xau' | 'flow'; + +// Use the generated types directly — never hand-roll a subset, which silently +// drifts when the proto gains fields. +import type { + GetHyperliquidFlowResponse, + HyperliquidAssetFlow, +} from '@/generated/client/worldmonitor/market/v1/service_client'; + +function parseFiniteNumber(s: string): number | null { + if (typeof s !== 'string' || s === '') return null; + const n = Number(s); + return Number.isFinite(n) ? n : null; +} + +/** + * OI Δ1h derived from sparkOi tail: (last - lookback) / lookback. + * 12 samples back = 1h at 5min cadence. Returns null if too few samples. + */ +function oiDelta1h(sparkOi: number[]): number | null { + if (!Array.isArray(sparkOi) || sparkOi.length < 13) return null; + const last = sparkOi[sparkOi.length - 1]; + const lookback = sparkOi[sparkOi.length - 13]; + if (last == null || lookback == null) return null; + if (!(lookback > 0) || !Number.isFinite(last)) return null; + return (last - lookback) / lookback; +} + +/** + * Map the raw bootstrap-hydrated seed snapshot (seeder JSON shape) into the + * same view model the RPC mapper produces. Bootstrap returns the raw Redis + * blob (numeric fields), not the proto response (string-encoded numbers). + */ +export function mapHyperliquidFlowSeed(raw: Record): HyperliquidFlowView | null { + const assets = Array.isArray(raw.assets) ? (raw.assets as Array>) : null; + if (!assets || assets.length === 0) return null; + const fxAssets: HyperliquidAssetView[] = []; + const commodityAssets: HyperliquidAssetView[] = []; + for (const a of assets) { + const funding = typeof a.funding === 'number' && Number.isFinite(a.funding) ? a.funding : null; + const sparkOi = Array.isArray(a.sparkOi) ? (a.sparkOi as number[]).filter((v) => Number.isFinite(v)) : []; + const sparkScore = Array.isArray(a.sparkScore) ? (a.sparkScore as number[]).filter((v) => Number.isFinite(v)) : []; + const view: HyperliquidAssetView = { + symbol: String(a.symbol ?? ''), + display: String(a.display ?? ''), + group: String(a.group ?? ''), + funding, + oiDelta1h: oiDelta1h(sparkOi), + composite: typeof a.composite === 'number' ? a.composite : 0, + warmup: Boolean(a.warmup), + stale: Boolean(a.stale), + sparkScore, + }; + if (view.group === 'fx') fxAssets.push(view); + else commodityAssets.push(view); + } + return { + ts: typeof raw.ts === 'number' ? raw.ts : 0, + warmup: Boolean(raw.warmup), + fxAssets, + commodityAssets, + unavailable: false, + }; +} + +export function mapHyperliquidFlowResponse(resp: GetHyperliquidFlowResponse): HyperliquidFlowView { + const fxAssets: HyperliquidAssetView[] = []; + const commodityAssets: HyperliquidAssetView[] = []; + for (const a of resp.assets as HyperliquidAssetFlow[]) { + const view: HyperliquidAssetView = { + symbol: a.symbol, + display: a.display, + group: a.group, + funding: parseFiniteNumber(a.funding), + oiDelta1h: oiDelta1h(a.sparkOi), + composite: Number(a.composite || 0), + warmup: Boolean(a.warmup), + stale: Boolean(a.stale), + sparkScore: Array.isArray(a.sparkScore) ? a.sparkScore : [], + }; + if (a.group === 'fx') fxAssets.push(view); + else commodityAssets.push(view); + } + return { + ts: Number(resp.ts || 0), + warmup: Boolean(resp.warmup), + fxAssets, + commodityAssets, + unavailable: false, + }; +} + +interface HyperliquidAssetView { + symbol: string; + display: string; + group: string; + funding: number | null; + oiDelta1h: number | null; + composite: number; + warmup: boolean; + stale: boolean; + sparkScore: number[]; +} + +interface HyperliquidFlowView { + ts: number; + warmup: boolean; + fxAssets: HyperliquidAssetView[]; + commodityAssets: HyperliquidAssetView[]; + unavailable: boolean; +} // CCYUSD=X (e.g. EURUSD): USD is quote, rate = USD/FC → XAU_FC = XAU_USD / rate // USDCCY=X (e.g. USDJPY, USDCHF): USD is base, rate = FC/USD → XAU_FC = XAU_USD * rate @@ -363,6 +474,8 @@ export class CommoditiesPanel extends Panel { private _tab: CommoditiesTab = 'commodities'; private _commodityData: Array<{ display: string; price: number | null; change: number | null; sparkline?: number[]; symbol?: string }> = []; private _fxRates: EcbFxRateItem[] = []; + private _flow: HyperliquidFlowView | null = null; + private _flowLoading = false; constructor() { super({ id: 'commodities', title: t('panels.commodities'), infoTooltip: t('components.commodities.infoTooltip') }); @@ -370,13 +483,63 @@ export class CommoditiesPanel extends Panel { this.content.addEventListener('click', (e) => { const btn = (e.target as HTMLElement).closest('[data-tab]'); const tab = btn?.dataset.tab; - if (tab === 'commodities' || tab === 'fx' || (tab === 'xau' && SITE_VARIANT === 'commodity')) { + if ( + tab === 'commodities' || + tab === 'fx' || + tab === 'flow' || + (tab === 'xau' && SITE_VARIANT === 'commodity') + ) { this._tab = tab as CommoditiesTab; this._render(); } }); } + /** + * Fetch Hyperliquid perp positioning flow snapshot. + * Called from App.ts primeVisiblePanelData() and refreshScheduler. + * Uses bootstrap-hydrated data on first render if available (AGENTS.md mandates + * bootstrap hydration for new data sources), then refreshes from RPC. + */ + public async fetchHyperliquidFlow(): Promise { + if (this._flowLoading) return false; + this._flowLoading = true; + try { + if (!this._flow) { + const hydrated = getHydratedData('hyperliquidFlow') as Record | undefined; + if (hydrated && !hydrated.unavailable) { + const mapped = mapHyperliquidFlowSeed(hydrated); + if (mapped) { + this._flow = mapped; + if (this._tab === 'flow') this._render(); + } + } + } + + const { MarketServiceClient } = await import('@/generated/client/worldmonitor/market/v1/service_client'); + const { getRpcBaseUrl } = await import('@/services/rpc-client'); + const client = new MarketServiceClient(getRpcBaseUrl(), { + fetch: (...args: Parameters) => globalThis.fetch(...args), + }); + const resp = await client.getHyperliquidFlow({}); + if (resp.unavailable || !resp.assets || resp.assets.length === 0) { + if (!this._flow) this._flow = { ts: 0, warmup: true, fxAssets: [], commodityAssets: [], unavailable: true }; + } else { + this._flow = mapHyperliquidFlowResponse(resp); + } + if (this._tab === 'flow') this._render(); + return true; + } catch (err) { + console.error('[CommoditiesPanel.fetchHyperliquidFlow] RPC failed:', err instanceof Error ? err.message : err); + // Don't blow away an existing flow snapshot on transient fetch errors. + if (!this._flow) this._flow = { ts: 0, warmup: true, fxAssets: [], commodityAssets: [], unavailable: true }; + if (this._tab === 'flow') this._render(); + return false; + } finally { + this._flowLoading = false; + } + } + public renderCommodities(data: Array<{ symbol?: string; display: string; price: number | null; change: number | null; sparkline?: number[] }>): void { this._commodityData = data; this._render(); @@ -387,16 +550,63 @@ export class CommoditiesPanel extends Panel { this._render(); } - private _buildTabBar(hasFx: boolean, hasXau: boolean): string { + private _buildTabBar(hasFx: boolean, hasXau: boolean, hasFlow: boolean): string { const firstTabLabel = 'Commodities'; const tabs: string[] = [ ``, ]; if (hasFx) tabs.push(``); if (hasXau) tabs.push(``); + if (hasFlow) tabs.push(``); return tabs.length > 1 ? `
${tabs.join('')}
` : ''; } + private _renderFlow(): string { + if (!this._flow) { + return `
Loading perp flow…
`; + } + if (this._flow.unavailable) { + return `
Perp flow snapshot unavailable. Warming up — first samples populate within 5–10 minutes.
`; + } + const sections: string[] = []; + if (this._flow.warmup) { + sections.push(`
Warming up — volume/OI baselines build over the next ~12 polls (1h).
`); + } + if (this._flow.commodityAssets.length > 0) { + sections.push(`
Commodities
`); + sections.push(this._renderFlowGrid(this._flow.commodityAssets)); + } + if (this._flow.fxAssets.length > 0) { + sections.push(`
FX (Crypto-Native)
`); + sections.push(this._renderFlowGrid(this._flow.fxAssets)); + } + sections.push(`
Composite 0–100 from funding × volume × OI × basis · Hyperliquid /info · 5min cron
`); + return sections.join(''); + } + + private _renderFlowGrid(assets: HyperliquidAssetView[]): string { + return '
' + assets.map((a) => { + const score = Math.round(a.composite); + const scoreColor = score >= 60 ? '#e74c3c' : score >= 40 ? '#e67e22' : score >= 20 ? '#f1c40f' : 'var(--text-dim)'; + const fundingStr = a.funding != null ? `${(a.funding * 100).toFixed(3)}%` : '—'; + const fundingColor = a.funding != null && a.funding < 0 ? 'change-negative' : 'change-positive'; + const oiStr = a.oiDelta1h != null ? `${a.oiDelta1h >= 0 ? '+' : ''}${(a.oiDelta1h * 100).toFixed(1)}%` : '—'; + const oiColor = a.oiDelta1h != null && a.oiDelta1h < 0 ? 'change-negative' : 'change-positive'; + const staleBadge = a.stale ? ` stale` : ''; + const warmupBadge = a.warmup ? ` warm` : ''; + return ` +
+
${escapeHtml(a.display)}${staleBadge}${warmupBadge}
+ ${miniSparkline(a.sparkScore, score - 50, 60, 18)} +
${score}
+
+ ${escapeHtml(fundingStr)} + ${escapeHtml(oiStr)} +
+
`; + }).join('') + '
'; + } + private _renderXau(): string { const gcf = this._commodityData.find(d => d.symbol === 'GC=F' && d.price !== null); if (!gcf?.price) return `
Gold price unavailable
`; @@ -431,8 +641,15 @@ export class CommoditiesPanel extends Panel { private _render(): void { const hasFx = this._fxRates.length > 0; const hasXau = SITE_VARIANT === 'commodity' && this._commodityData.some(d => d.symbol === 'GC=F' && d.price !== null); + const hasFlow = !!this._flow && (!this._flow.unavailable || this._flow.warmup); if (this._tab === 'xau' && !hasXau) this._tab = 'commodities'; - const tabBar = this._buildTabBar(hasFx, hasXau); + if (this._tab === 'flow' && !hasFlow) this._tab = 'commodities'; + const tabBar = this._buildTabBar(hasFx, hasXau, hasFlow); + + if (this._tab === 'flow') { + this.setContent(tabBar + this._renderFlow()); + return; + } if (this._tab === 'fx' && hasFx) { const items = this._fxRates.map(r => { diff --git a/src/config/variants/base.ts b/src/config/variants/base.ts index 4b72c5929..1bd3c5cc7 100644 --- a/src/config/variants/base.ts +++ b/src/config/variants/base.ts @@ -56,6 +56,7 @@ export const REFRESH_INTERVALS = { wsbTickers: 10 * 60 * 1000, crossSourceSignals: 15 * 60 * 1000, hormuzTracker: 60 * 60 * 1000, // 1h — data updates daily + hyperliquidFlow: 5 * 60 * 1000, // 5min — matches Railway seed cadence energyCrisis: 6 * 60 * 60 * 1000, // 6h — policy data updates infrequently macroTiles: 30 * 60 * 1000, fsi: 30 * 60 * 1000, diff --git a/src/generated/client/worldmonitor/market/v1/service_client.ts b/src/generated/client/worldmonitor/market/v1/service_client.ts index 39bed179d..f9714a88c 100644 --- a/src/generated/client/worldmonitor/market/v1/service_client.ts +++ b/src/generated/client/worldmonitor/market/v1/service_client.ts @@ -611,6 +611,43 @@ export interface GoldCbMover { deltaTonnes12m: number; } +export interface GetHyperliquidFlowRequest { +} + +export interface GetHyperliquidFlowResponse { + ts: string; + fetchedAt: string; + warmup: boolean; + assetCount: number; + assets: HyperliquidAssetFlow[]; + unavailable: boolean; +} + +export interface HyperliquidAssetFlow { + symbol: string; + display: string; + assetClass: string; + group: string; + funding: string; + openInterest: string; + markPx: string; + oraclePx: string; + dayNotional: string; + fundingScore: number; + volumeScore: number; + oiScore: number; + basisScore: number; + composite: number; + sparkFunding: number[]; + sparkOi: number[]; + sparkScore: number[]; + warmup: boolean; + stale: boolean; + staleSince: string; + missingPolls: number; + alerts: string[]; +} + export interface FieldViolation { field: string; description: string; @@ -1197,6 +1234,29 @@ export class MarketServiceClient { return await resp.json() as GetGoldIntelligenceResponse; } + async getHyperliquidFlow(req: GetHyperliquidFlowRequest, options?: MarketServiceCallOptions): Promise { + let path = "/api/market/v1/get-hyperliquid-flow"; + const url = this.baseURL + path; + + const headers: Record = { + "Content-Type": "application/json", + ...this.defaultHeaders, + ...options?.headers, + }; + + const resp = await this.fetchFn(url, { + method: "GET", + headers, + signal: options?.signal, + }); + + if (!resp.ok) { + return this.handleError(resp); + } + + return await resp.json() as GetHyperliquidFlowResponse; + } + private async handleError(resp: Response): Promise { const body = await resp.text(); if (resp.status === 400) { diff --git a/src/generated/server/worldmonitor/market/v1/service_server.ts b/src/generated/server/worldmonitor/market/v1/service_server.ts index 50cc91511..930c43c02 100644 --- a/src/generated/server/worldmonitor/market/v1/service_server.ts +++ b/src/generated/server/worldmonitor/market/v1/service_server.ts @@ -611,6 +611,43 @@ export interface GoldCbMover { deltaTonnes12m: number; } +export interface GetHyperliquidFlowRequest { +} + +export interface GetHyperliquidFlowResponse { + ts: string; + fetchedAt: string; + warmup: boolean; + assetCount: number; + assets: HyperliquidAssetFlow[]; + unavailable: boolean; +} + +export interface HyperliquidAssetFlow { + symbol: string; + display: string; + assetClass: string; + group: string; + funding: string; + openInterest: string; + markPx: string; + oraclePx: string; + dayNotional: string; + fundingScore: number; + volumeScore: number; + oiScore: number; + basisScore: number; + composite: number; + sparkFunding: number[]; + sparkOi: number[]; + sparkScore: number[]; + warmup: boolean; + stale: boolean; + staleSince: string; + missingPolls: number; + alerts: string[]; +} + export interface FieldViolation { field: string; description: string; @@ -678,6 +715,7 @@ export interface MarketServiceHandler { getInsiderTransactions(ctx: ServerContext, req: GetInsiderTransactionsRequest): Promise; getMarketBreadthHistory(ctx: ServerContext, req: GetMarketBreadthHistoryRequest): Promise; getGoldIntelligence(ctx: ServerContext, req: GetGoldIntelligenceRequest): Promise; + getHyperliquidFlow(ctx: ServerContext, req: GetHyperliquidFlowRequest): Promise; } export function createMarketServiceRoutes( @@ -1627,6 +1665,43 @@ export function createMarketServiceRoutes( } }, }, + { + method: "GET", + path: "/api/market/v1/get-hyperliquid-flow", + handler: async (req: Request): Promise => { + try { + const pathParams: Record = {}; + const body = {} as GetHyperliquidFlowRequest; + + const ctx: ServerContext = { + request: req, + pathParams, + headers: Object.fromEntries(req.headers.entries()), + }; + + const result = await handler.getHyperliquidFlow(ctx, body); + return new Response(JSON.stringify(result as GetHyperliquidFlowResponse), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (err: unknown) { + if (err instanceof ValidationError) { + return new Response(JSON.stringify({ violations: err.violations }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + if (options?.onError) { + return options.onError(err, req); + } + const message = err instanceof Error ? err.message : String(err); + return new Response(JSON.stringify({ message }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } + }, + }, ]; } diff --git a/tests/hyperliquid-flow-seed.test.mjs b/tests/hyperliquid-flow-seed.test.mjs new file mode 100644 index 000000000..ad8483df3 --- /dev/null +++ b/tests/hyperliquid-flow-seed.test.mjs @@ -0,0 +1,358 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { + ASSETS, + CANONICAL_KEY, + CACHE_TTL_SECONDS, + SPARK_MAX, + MIN_NOTIONAL_USD_24H, + STALE_SYMBOL_DROP_AFTER_POLLS, + WEIGHTS, + THRESHOLDS, + ALERT_THRESHOLD, + clamp, + scoreFunding, + scoreVolume, + scoreOi, + scoreBasis, + computeAsset, + validateUpstream, + indexBySymbol, + buildSnapshot, + validateFn, +} from '../scripts/seed-hyperliquid-flow.mjs'; + +const META_BTC = { symbol: 'BTC', class: 'crypto', display: 'BTC', group: 'crypto' }; +const META_OIL = { symbol: 'xyz:CL', class: 'commodity', display: 'WTI Crude', group: 'oil' }; + +function makeUniverse(extra = []) { + // Build a universe with at least 50 entries so validateUpstream passes. + const filler = Array.from({ length: 50 }, (_, i) => ({ name: `FILL${i}` })); + return [...ASSETS.map((a) => ({ name: a.symbol })), ...filler, ...extra]; +} + +function makeAssetCtxs(universe, overrides = {}) { + return universe.map((u) => overrides[u.name] || { + funding: '0', + openInterest: '0', + markPx: '0', + oraclePx: '0', + dayNtlVlm: '0', + }); +} + +describe('TTL constants', () => { + it('CACHE_TTL_SECONDS is at least 9× cron cadence (5 min)', () => { + assert.ok(CACHE_TTL_SECONDS >= 9 * 5 * 60, `expected >= 2700, got ${CACHE_TTL_SECONDS}`); + }); + it('CANONICAL_KEY is the documented v1 key', () => { + assert.equal(CANONICAL_KEY, 'market:hyperliquid:flow:v1'); + }); +}); + +describe('weights', () => { + it('sum to 1.0', () => { + const sum = WEIGHTS.funding + WEIGHTS.volume + WEIGHTS.oi + WEIGHTS.basis; + assert.ok(Math.abs(sum - 1.0) < 1e-9, `weights sum=${sum}`); + }); +}); + +describe('clamp', () => { + it('bounds to [0,100] by default', () => { + assert.equal(clamp(150), 100); + assert.equal(clamp(-5), 0); + assert.equal(clamp(50), 50); + }); + it('returns 0 for non-finite', () => { + assert.equal(clamp(NaN), 0); + assert.equal(clamp(Infinity), 0); + }); +}); + +describe('scoreFunding (parity with risk.py)', () => { + it('|rate|/threshold * 100 clamped', () => { + assert.equal(scoreFunding(0.0005, 0.001), 50); + assert.equal(scoreFunding(-0.0005, 0.001), 50); + assert.equal(scoreFunding(0.002, 0.001), 100); + assert.equal(scoreFunding(0, 0.001), 0); + }); + it('returns 0 on zero/negative threshold', () => { + assert.equal(scoreFunding(0.001, 0), 0); + }); +}); + +describe('scoreVolume', () => { + it('ratio / threshold * 100', () => { + assert.equal(scoreVolume(2_000_000, 1_000_000, 5), 40); + assert.equal(scoreVolume(10_000_000, 1_000_000, 5), 100); + }); + it('returns 0 if avg is 0', () => { + assert.equal(scoreVolume(1_000_000, 0, 5), 0); + }); +}); + +describe('scoreOi', () => { + it('|delta|/prev / threshold * 100', () => { + assert.equal(scoreOi(120, 100, 0.20), 100); // 20% change vs 20% threshold → score 100 + assert.equal(scoreOi(110, 100, 0.20), 50); // 10% change → half of threshold + }); + it('returns 0 if prevOi <= 0', () => { + assert.equal(scoreOi(100, 0, 0.20), 0); + }); +}); + +describe('scoreBasis', () => { + it('|mark-oracle|/oracle / threshold * 100', () => { + assert.equal(scoreBasis(105, 100, 0.05), 100); // exactly threshold + assert.equal(Math.round(scoreBasis(102.5, 100, 0.05)), 50); + }); +}); + +describe('computeAsset min-notional guard', () => { + it('volumeScore = 0 when dayNotional below MIN_NOTIONAL_USD_24H, even with prior history', () => { + const prev = { + symbol: 'xyz:CL', + sparkVol: Array(12).fill(100_000), + sparkFunding: [], + sparkOi: [], + sparkScore: [], + openInterest: 1_000, + }; + const ctx = { funding: '0', openInterest: '1000', markPx: '0', oraclePx: '0', dayNtlVlm: String(MIN_NOTIONAL_USD_24H - 1) }; + const out = computeAsset(META_OIL, ctx, prev); + assert.equal(out.volumeScore, 0); + }); + it('volumeScore > 0 when dayNotional above MIN_NOTIONAL with sufficient prior samples', () => { + const prev = { + symbol: 'xyz:CL', + sparkVol: Array(12).fill(MIN_NOTIONAL_USD_24H), + sparkFunding: [], + sparkOi: [], + sparkScore: [], + openInterest: 1_000, + }; + const ctx = { funding: '0', openInterest: '1000', markPx: '0', oraclePx: '0', dayNtlVlm: String(MIN_NOTIONAL_USD_24H * 4) }; + const out = computeAsset(META_OIL, ctx, prev); + assert.ok(out.volumeScore > 0, `expected >0, got ${out.volumeScore}`); + }); +}); + +describe('computeAsset cold-start (no prev)', () => { + it('zeros volumeScore and oiScore on first run', () => { + const ctx = { funding: '0.0005', openInterest: '1000', markPx: '100', oraclePx: '100', dayNtlVlm: '5000000' }; + const out = computeAsset(META_BTC, ctx, null, { coldStart: true }); + assert.equal(out.oiScore, 0); + assert.equal(out.volumeScore, 0); + assert.ok(out.fundingScore > 0); // funding still computable + assert.equal(out.warmup, true); + }); +}); + +describe('warmup persists until baseline is usable (not just first poll)', () => { + it('stays warmup=true after coldStart clears if volume baseline has <12 samples', () => { + // Second poll: coldStart=false, but only 1 prior vol sample. + const prev = { + symbol: 'BTC', openInterest: 1000, + sparkVol: [1_000_000], + sparkFunding: [], sparkOi: [1000], sparkScore: [], + }; + const ctx = { funding: '0.0005', openInterest: '1010', markPx: '100', oraclePx: '100', dayNtlVlm: '5000000' }; + const out = computeAsset(META_BTC, ctx, prev, { coldStart: false }); + assert.equal(out.warmup, true, 'should stay warmup while baseline < 12 samples'); + assert.equal(out.volumeScore, 0, 'volume scoring must wait for baseline'); + }); + + it('clears warmup=false once baseline has >=12 samples AND prior OI exists', () => { + const prev = { + symbol: 'BTC', openInterest: 1000, + sparkVol: Array(12).fill(1_000_000), + sparkFunding: [], sparkOi: Array(12).fill(1000), sparkScore: [], + }; + const ctx = { funding: '0.0001', openInterest: '1010', markPx: '100', oraclePx: '100', dayNtlVlm: '1000000' }; + const out = computeAsset(META_BTC, ctx, prev, { coldStart: false }); + assert.equal(out.warmup, false); + }); + + it('stays warmup=true when prior OI is missing even with full vol baseline', () => { + const prev = { + symbol: 'BTC', openInterest: null, + sparkVol: Array(12).fill(1_000_000), + sparkFunding: [], sparkOi: [], sparkScore: [], + }; + const ctx = { funding: '0', openInterest: '1000', markPx: '100', oraclePx: '100', dayNtlVlm: '1000000' }; + const out = computeAsset(META_BTC, ctx, prev, { coldStart: false }); + assert.equal(out.warmup, true); + assert.equal(out.oiScore, 0); + }); +}); + +describe('volume baseline uses the MOST RECENT window (slice(-12), not slice(0,12))', () => { + // Regression: sparkVol is newest-at-tail via shiftAndAppend. Using slice(0,12) + // anchors the baseline to the OLDEST window forever once len >= 12 + new samples + // keep appending. Verify the baseline tracks the newest 12 samples. + it('reflects recent-volume regime, not stale oldest-window baseline', () => { + // Tail = last 12 samples (recent baseline ~200k). + // Head = old samples (~1M). If we regress to slice(0,12), avg=1M and dayNotional=2M + // would score volume=~2/5=40. With correct slice(-12), avg=200k so 2M/200k=10x → score=100. + const sparkVol = [ + ...Array(20).fill(1_000_000), // oldest + ...Array(12).fill(200_000), // newest (baseline) + ]; + const prev = { + symbol: 'BTC', openInterest: 1000, + sparkVol, + sparkFunding: [], sparkOi: Array(12).fill(1000), sparkScore: [], + }; + const ctx = { funding: '0', openInterest: '1010', markPx: '100', oraclePx: '100', dayNtlVlm: '2000000' }; + const out = computeAsset(META_BTC, ctx, prev, { coldStart: false }); + // Recent-window baseline: 2M / 200k / 5 * 100 = 200 → clamp 100. + assert.equal(out.volumeScore, 100, `expected volume baseline to track recent window, got score=${out.volumeScore}`); + }); +}); + +describe('validateUpstream', () => { + it('rejects non-tuple', () => { + assert.throws(() => validateUpstream({}), /tuple/); + }); + it('rejects missing universe', () => { + assert.throws(() => validateUpstream([{}, []]), /universe/); + }); + it('rejects too-small universe', () => { + const small = Array.from({ length: 10 }, (_, i) => ({ name: `X${i}` })); + assert.throws(() => validateUpstream([{ universe: small }, makeAssetCtxs(small)]), /suspiciously small/); + }); + it('rejects mismatched assetCtxs length', () => { + const u = makeUniverse(); + assert.throws(() => validateUpstream([{ universe: u }, []]), /length does not match/); + }); + it('accepts well-formed tuple', () => { + const u = makeUniverse(); + const ctxs = makeAssetCtxs(u); + const out = validateUpstream([{ universe: u }, ctxs]); + assert.equal(out.universe.length, u.length); + }); +}); + +describe('buildSnapshot — first run', () => { + it('flags warmup and emits all whitelisted assets present in upstream', () => { + const u = makeUniverse(); + const ctxs = makeAssetCtxs(u); + const snap = buildSnapshot([{ universe: u }, ctxs], null, { now: 1_700_000_000_000 }); + assert.equal(snap.warmup, true); + assert.equal(snap.assets.length, ASSETS.length); + assert.ok(snap.assets.every((a) => a.warmup === true)); + }); +}); + +describe('buildSnapshot — missing-symbol carry-forward', () => { + it('carries forward a stale entry when whitelisted symbol absent from upstream', () => { + const u = makeUniverse().filter((m) => m.name !== 'BTC'); + const ctxs = makeAssetCtxs(u); + const prevSnap = { + ts: 1_700_000_000_000 - 5 * 60_000, // 5min ago + assets: [{ + symbol: 'BTC', display: 'BTC', class: 'crypto', group: 'crypto', + funding: 0.0001, openInterest: 1000, markPx: 65000, oraclePx: 65000, dayNotional: 1e9, + fundingScore: 10, volumeScore: 0, oiScore: 0, basisScore: 0, composite: 3, + sparkFunding: [0.0001], sparkOi: [1000], sparkScore: [3], sparkVol: [1e9], + stale: false, staleSince: null, missingPolls: 0, alerts: [], warmup: false, + }], + }; + const snap = buildSnapshot([{ universe: u }, ctxs], prevSnap, { now: 1_700_000_000_000 }); + const btc = snap.assets.find((a) => a.symbol === 'BTC'); + assert.ok(btc, 'BTC should still appear'); + assert.equal(btc.stale, true); + assert.equal(btc.missingPolls, 1); + }); + + it('drops a symbol after STALE_SYMBOL_DROP_AFTER_POLLS consecutive misses', () => { + const u = makeUniverse().filter((m) => m.name !== 'BTC'); + const ctxs = makeAssetCtxs(u); + const prevSnap = { + ts: 1_700_000_000_000 - 5 * 60_000, + assets: [{ + symbol: 'BTC', display: 'BTC', class: 'crypto', group: 'crypto', + funding: 0, openInterest: 1000, markPx: 0, oraclePx: 0, dayNotional: 0, + fundingScore: 0, volumeScore: 0, oiScore: 0, basisScore: 0, composite: 0, + sparkFunding: [], sparkOi: [], sparkScore: [], sparkVol: [], + stale: true, staleSince: 1_700_000_000_000 - 30 * 60_000, + missingPolls: STALE_SYMBOL_DROP_AFTER_POLLS - 1, + alerts: [], warmup: false, + }], + }; + const snap = buildSnapshot([{ universe: u }, ctxs], prevSnap, { now: 1_700_000_000_000 }); + assert.equal(snap.assets.find((a) => a.symbol === 'BTC'), undefined); + }); +}); + +describe('buildSnapshot — post-outage cold start', () => { + it('zeroes deltas when prior snapshot is older than 900s', () => { + const u = makeUniverse(); + const ctxs = makeAssetCtxs(u, { + BTC: { funding: '0.0005', openInterest: '2000', markPx: '65000', oraclePx: '65000', dayNtlVlm: '5000000' }, + }); + const prevSnap = { + ts: 1_700_000_000_000 - 60 * 60_000, // 1h ago — way past 900s threshold + assets: [{ symbol: 'BTC', openInterest: 1000, sparkVol: Array(12).fill(1e6) }], + }; + const snap = buildSnapshot([{ universe: u }, ctxs], prevSnap, { now: 1_700_000_000_000 }); + const btc = snap.assets.find((a) => a.symbol === 'BTC'); + assert.equal(btc.warmup, true); + assert.equal(btc.oiScore, 0); // would be ~50 if prev OI was used + assert.equal(btc.volumeScore, 0); // would be >0 if prev vol samples were used + }); +}); + +describe('sparkline arrays', () => { + it('cap at SPARK_MAX samples', () => { + const u = makeUniverse(); + const ctxs = makeAssetCtxs(u, { + BTC: { funding: '0.0001', openInterest: '1000', markPx: '0', oraclePx: '0', dayNtlVlm: '0' }, + }); + const longArr = Array.from({ length: SPARK_MAX + 30 }, (_, i) => i); + const prevSnap = { + ts: 1_700_000_000_000 - 5 * 60_000, + assets: [{ + symbol: 'BTC', sparkFunding: longArr, sparkOi: longArr, sparkScore: longArr, sparkVol: longArr, + openInterest: 1000, + }], + }; + const snap = buildSnapshot([{ universe: u }, ctxs], prevSnap, { now: 1_700_000_000_000 }); + const btc = snap.assets.find((a) => a.symbol === 'BTC'); + assert.ok(btc.sparkFunding.length <= SPARK_MAX); + assert.ok(btc.sparkOi.length <= SPARK_MAX); + assert.ok(btc.sparkScore.length <= SPARK_MAX); + }); +}); + +describe('validateFn (runSeed gate)', () => { + it('rejects empty / fewer than 12 assets', () => { + assert.equal(validateFn(null), false); + assert.equal(validateFn({ assets: [] }), false); + assert.equal(validateFn({ assets: Array(11).fill({}) }), false); + }); + it('accepts >=12 assets', () => { + assert.equal(validateFn({ assets: Array(12).fill({}) }), true); + }); +}); + +describe('alert threshold', () => { + it('emits HIGH RISK alert at composite >= 60', () => { + // Funding=100% × 0.30 + Basis=100% × 0.20 = 50; bump volume to push >60 + const prev = { + symbol: 'BTC', + sparkVol: Array(12).fill(1_000_000), + sparkFunding: [], sparkOi: [], sparkScore: [], + openInterest: 1000, + }; + const ctx = { + funding: '0.002', // 2× threshold → score 100 + openInterest: '1500', // 50% delta vs 1000 → 250 → clamped to 100 + markPx: '105', oraclePx: '100', // basis 5% = threshold → 100 + dayNtlVlm: '10000000', // 10× avg → 200/5 → clamped 100 + }; + const out = computeAsset(META_BTC, ctx, prev); + assert.ok(out.composite >= ALERT_THRESHOLD, `composite=${out.composite}`); + assert.ok(out.alerts.some((a) => a.includes('HIGH RISK'))); + }); +});