diff --git a/docs/api/MarketService.openapi.json b/docs/api/MarketService.openapi.json index 74b009e9e..2ccd98833 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"},"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"},"GetSectorSummaryRequest":{"description":"GetSectorSummaryRequest specifies parameters for retrieving sector performance.","properties":{"period":{"description":"Time period for performance calculation (e.g., \"1d\", \"1w\", \"1m\"). Defaults to \"1d\".","type":"string"}},"type":"object"},"GetSectorSummaryResponse":{"description":"GetSectorSummaryResponse contains sector performance data.","properties":{"sectors":{"items":{"$ref":"#/components/schemas/SectorPerformance"},"type":"array"}},"type":"object"},"GetStockAnalysisHistoryRequest":{"properties":{"includeNews":{"type":"boolean"},"limitPerSymbol":{"format":"int32","maximum":32,"minimum":1,"type":"integer"},"symbols":{"items":{"type":"string"},"type":"array"}},"type":"object"},"GetStockAnalysisHistoryResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/StockAnalysisHistoryItem"},"type":"array"}},"type":"object"},"GulfQuote":{"description":"GulfQuote represents a Gulf region market quote (index, currency, or oil).","properties":{"change":{"format":"double","type":"number"},"country":{"type":"string"},"flag":{"type":"string"},"name":{"type":"string"},"price":{"format":"double","type":"number"},"sparkline":{"items":{"format":"double","type":"number"},"type":"array"},"symbol":{"type":"string"},"type":{"type":"string"}},"type":"object"},"ListAiTokensRequest":{"description":"ListAiTokensRequest retrieves AI crypto token prices.","type":"object"},"ListAiTokensResponse":{"description":"ListAiTokensResponse contains AI token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListCommodityQuotesRequest":{"description":"ListCommodityQuotesRequest specifies which commodities to retrieve.","properties":{"symbols":{"items":{"description":"Commodity symbols to retrieve (Yahoo symbols). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListCommodityQuotesResponse":{"description":"ListCommodityQuotesResponse contains commodity quotes.","properties":{"quotes":{"items":{"$ref":"#/components/schemas/CommodityQuote"},"type":"array"}},"type":"object"},"ListCryptoQuotesRequest":{"description":"ListCryptoQuotesRequest specifies which cryptocurrencies to retrieve.","properties":{"ids":{"items":{"description":"Cryptocurrency IDs to retrieve (CoinGecko IDs). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListCryptoQuotesResponse":{"description":"ListCryptoQuotesResponse contains cryptocurrency quotes.","properties":{"quotes":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListCryptoSectorsRequest":{"description":"ListCryptoSectorsRequest retrieves crypto sector performance.","type":"object"},"ListCryptoSectorsResponse":{"description":"ListCryptoSectorsResponse contains crypto sector performance data.","properties":{"sectors":{"items":{"$ref":"#/components/schemas/CryptoSector"},"type":"array"}},"type":"object"},"ListDefiTokensRequest":{"description":"ListDefiTokensRequest retrieves DeFi token prices.","type":"object"},"ListDefiTokensResponse":{"description":"ListDefiTokensResponse contains DeFi token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListEarningsCalendarRequest":{"properties":{"fromDate":{"type":"string"},"toDate":{"type":"string"}},"type":"object"},"ListEarningsCalendarResponse":{"properties":{"earnings":{"items":{"$ref":"#/components/schemas/EarningsEntry"},"type":"array"},"fromDate":{"type":"string"},"toDate":{"type":"string"},"total":{"format":"int32","type":"integer"},"unavailable":{"type":"boolean"}},"type":"object"},"ListEtfFlowsRequest":{"description":"ListEtfFlowsRequest is empty; the handler uses a fixed list of BTC spot ETFs.","type":"object"},"ListEtfFlowsResponse":{"description":"ListEtfFlowsResponse contains BTC spot ETF flow data.","properties":{"etfs":{"items":{"$ref":"#/components/schemas/EtfFlow"},"type":"array"},"rateLimited":{"description":"True when the upstream API rate-limited the request.","type":"boolean"},"summary":{"$ref":"#/components/schemas/EtfFlowsSummary"},"timestamp":{"description":"Timestamp of the data fetch (ISO 8601).","type":"string"}},"type":"object"},"ListGulfQuotesRequest":{"type":"object"},"ListGulfQuotesResponse":{"properties":{"quotes":{"items":{"$ref":"#/components/schemas/GulfQuote"},"type":"array"},"rateLimited":{"type":"boolean"}},"type":"object"},"ListMarketQuotesRequest":{"description":"ListMarketQuotesRequest specifies which stock/index symbols to retrieve.","properties":{"symbols":{"items":{"description":"Ticker symbols to retrieve (e.g., [\"AAPL\", \"^GSPC\"]). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListMarketQuotesResponse":{"description":"ListMarketQuotesResponse contains stock and index quotes.","properties":{"finnhubSkipped":{"description":"True when the Finnhub API key is not configured and stock quotes were skipped.","type":"boolean"},"quotes":{"items":{"$ref":"#/components/schemas/MarketQuote"},"type":"array"},"rateLimited":{"description":"True when the upstream API rate-limited the request.","type":"boolean"},"skipReason":{"description":"Human-readable reason when Finnhub was skipped (e.g., \"FINNHUB_API_KEY not configured\").","type":"string"}},"type":"object"},"ListOtherTokensRequest":{"description":"ListOtherTokensRequest retrieves other/trending crypto token prices.","type":"object"},"ListOtherTokensResponse":{"description":"ListOtherTokensResponse contains other token price data.","properties":{"tokens":{"items":{"$ref":"#/components/schemas/CryptoQuote"},"type":"array"}},"type":"object"},"ListStablecoinMarketsRequest":{"description":"ListStablecoinMarketsRequest specifies which stablecoins to retrieve.","properties":{"coins":{"items":{"description":"CoinGecko IDs to retrieve (e.g. \"tether,usd-coin\"). Empty returns defaults.","type":"string"},"type":"array"}},"type":"object"},"ListStablecoinMarketsResponse":{"description":"ListStablecoinMarketsResponse contains stablecoin market data.","properties":{"stablecoins":{"items":{"$ref":"#/components/schemas/Stablecoin"},"type":"array"},"summary":{"$ref":"#/components/schemas/StablecoinSummary"},"timestamp":{"description":"Timestamp of the data fetch (ISO 8601).","type":"string"}},"type":"object"},"ListStoredStockBacktestsRequest":{"properties":{"evalWindowDays":{"format":"int32","maximum":30,"minimum":3,"type":"integer"},"symbols":{"items":{"type":"string"},"type":"array"}},"type":"object"},"ListStoredStockBacktestsResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/BacktestStockResponse"},"type":"array"}},"type":"object"},"MarketQuote":{"description":"MarketQuote represents a stock or index quote from Finnhub or Yahoo Finance.","properties":{"change":{"description":"Percentage change from previous close.","format":"double","type":"number"},"display":{"description":"Display label.","type":"string"},"name":{"description":"Human-readable name.","type":"string"},"price":{"description":"Current price.","format":"double","type":"number"},"sparkline":{"items":{"description":"Sparkline data points (recent price history).","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Ticker symbol (e.g., \"AAPL\", \"^GSPC\").","minLength":1,"type":"string"}},"required":["symbol"],"type":"object"},"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-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"},"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"},"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"},"GetSectorSummaryRequest":{"description":"GetSectorSummaryRequest specifies parameters for retrieving sector performance.","properties":{"period":{"description":"Time period for performance calculation (e.g., \"1d\", \"1w\", \"1m\"). Defaults to \"1d\".","type":"string"}},"type":"object"},"GetSectorSummaryResponse":{"description":"GetSectorSummaryResponse contains sector performance data.","properties":{"sectors":{"items":{"$ref":"#/components/schemas/SectorPerformance"},"type":"array"}},"type":"object"},"GetStockAnalysisHistoryRequest":{"properties":{"includeNews":{"type":"boolean"},"limitPerSymbol":{"format":"int32","maximum":32,"minimum":1,"type":"integer"},"symbols":{"items":{"type":"string"},"type":"array"}},"type":"object"},"GetStockAnalysisHistoryResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/StockAnalysisHistoryItem"},"type":"array"}},"type":"object"},"GulfQuote":{"description":"GulfQuote represents a Gulf region market quote (index, currency, or oil).","properties":{"change":{"format":"double","type":"number"},"country":{"type":"string"},"flag":{"type":"string"},"name":{"type":"string"},"price":{"format":"double","type":"number"},"sparkline":{"items":{"format":"double","type":"number"},"type":"array"},"symbol":{"type":"string"},"type":{"type":"string"}},"type":"object"},"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-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-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 fc9b3cb1d..a91b3c9c6 100644 --- a/docs/api/MarketService.openapi.yaml +++ b/docs/api/MarketService.openapi.yaml @@ -612,6 +612,38 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /api/market/v1/get-insider-transactions: + get: + tags: + - MarketService + summary: GetInsiderTransactions + description: GetInsiderTransactions retrieves SEC insider buy/sell activity from Finnhub. + operationId: GetInsiderTransactions + parameters: + - name: symbol + in: query + required: false + schema: + type: string + responses: + "200": + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/GetInsiderTransactionsResponse' + "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: @@ -1727,3 +1759,50 @@ components: netPct: type: number format: double + GetInsiderTransactionsRequest: + type: object + properties: + symbol: + type: string + maxLength: 32 + minLength: 1 + required: + - symbol + GetInsiderTransactionsResponse: + type: object + properties: + unavailable: + type: boolean + symbol: + type: string + totalBuys: + type: number + format: double + totalSells: + type: number + format: double + netValue: + type: number + format: double + transactions: + type: array + items: + $ref: '#/components/schemas/InsiderTransaction' + fetchedAt: + type: string + InsiderTransaction: + type: object + properties: + name: + type: string + shares: + type: integer + format: int64 + description: 'Warning: Values > 2^53 may lose precision in JavaScript' + value: + type: number + format: double + transactionCode: + type: string + transactionDate: + type: string diff --git a/proto/worldmonitor/market/v1/get_insider_transactions.proto b/proto/worldmonitor/market/v1/get_insider_transactions.proto new file mode 100644 index 000000000..e869b9019 --- /dev/null +++ b/proto/worldmonitor/market/v1/get_insider_transactions.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +package worldmonitor.market.v1; + +import "buf/validate/validate.proto"; +import "sebuf/http/annotations.proto"; + +message InsiderTransaction { + string name = 1; + int64 shares = 2 [(sebuf.http.int64_encoding) = INT64_ENCODING_NUMBER]; + double value = 3; + string transaction_code = 4; + string transaction_date = 5; +} + +message GetInsiderTransactionsRequest { + string symbol = 1 [(sebuf.http.query) = { name: "symbol" }, + (buf.validate.field).required = true, + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 32 + ]; +} + +message GetInsiderTransactionsResponse { + bool unavailable = 1; + string symbol = 2; + double total_buys = 3; + double total_sells = 4; + double net_value = 5; + repeated InsiderTransaction transactions = 6; + string fetched_at = 7; +} diff --git a/proto/worldmonitor/market/v1/service.proto b/proto/worldmonitor/market/v1/service.proto index 5ac9d2aab..47ae853f1 100644 --- a/proto/worldmonitor/market/v1/service.proto +++ b/proto/worldmonitor/market/v1/service.proto @@ -22,6 +22,7 @@ import "worldmonitor/market/v1/list_other_tokens.proto"; import "worldmonitor/market/v1/get_fear_greed_index.proto"; import "worldmonitor/market/v1/list_earnings_calendar.proto"; import "worldmonitor/market/v1/get_cot_positioning.proto"; +import "worldmonitor/market/v1/get_insider_transactions.proto"; // MarketService provides APIs for financial market data from Finnhub, Yahoo Finance, and CoinGecko. service MarketService { @@ -121,4 +122,9 @@ service MarketService { rpc GetCotPositioning(GetCotPositioningRequest) returns (GetCotPositioningResponse) { option (sebuf.http.config) = {path: "/get-cot-positioning", method: HTTP_METHOD_GET}; } + + // GetInsiderTransactions retrieves SEC insider buy/sell activity from Finnhub. + rpc GetInsiderTransactions(GetInsiderTransactionsRequest) returns (GetInsiderTransactionsResponse) { + option (sebuf.http.config) = {path: "/get-insider-transactions", method: HTTP_METHOD_GET}; + } } diff --git a/server/gateway.ts b/server/gateway.ts index 639251de8..c123f4d4b 100644 --- a/server/gateway.ts +++ b/server/gateway.ts @@ -199,6 +199,7 @@ const RPC_CACHE_TIER: Record = { '/api/market/v1/list-earnings-calendar': 'slow', '/api/market/v1/get-cot-positioning': 'slow', + '/api/market/v1/get-insider-transactions': 'slow', '/api/economic/v1/get-economic-calendar': 'slow', '/api/intelligence/v1/list-market-implications': 'slow', '/api/economic/v1/get-ecb-fx-rates': 'slow', diff --git a/server/worldmonitor/market/v1/get-insider-transactions.ts b/server/worldmonitor/market/v1/get-insider-transactions.ts new file mode 100644 index 000000000..ce977eee5 --- /dev/null +++ b/server/worldmonitor/market/v1/get-insider-transactions.ts @@ -0,0 +1,145 @@ +import type { + ServerContext, + GetInsiderTransactionsRequest, + GetInsiderTransactionsResponse, + InsiderTransaction, +} from '../../../../src/generated/server/worldmonitor/market/v1/service_server'; +import { cachedFetchJson } from '../../../_shared/redis'; +import { CHROME_UA, finnhubGate } from '../../../_shared/constants'; +import { UPSTREAM_TIMEOUT_MS, sanitizeSymbol } from './_shared'; + +const CACHE_TTL_SECONDS = 86_400; +const SIX_MONTHS_MS = 6 * 30 * 24 * 60 * 60 * 1_000; + +// Only genuine open-market Form 4 codes count toward buy/sell conviction: +// P = open-market or private purchase +// S = open-market or private sale +const PURCHASE_CODES = new Set(['P']); +const SALE_CODES = new Set(['S']); +// Non-market Form 4 codes we still surface in the transactions list so the +// panel does not look empty, but which do NOT contribute to buy/sell totals +// because their transactionPrice is not a market execution price: +// M = exercise/conversion of derivative (price = strike) +// A = grant/award (compensation, not a purchase) +// D = disposition to issuer (e.g. buyback redemption) +// F = payment of exercise price or tax withholding (mechanical) +const NEUTRAL_CODES = new Set(['M', 'A', 'D', 'F']); + +interface FinnhubTransaction { + name: string; + share: number; + change: number; + transactionPrice: number; + transactionCode: string; + transactionDate: string; + filingDate: string; +} + +interface FinnhubInsiderResponse { + data?: FinnhubTransaction[]; + symbol?: string; +} + +export async function getInsiderTransactions( + _ctx: ServerContext, + req: GetInsiderTransactionsRequest, +): Promise { + const symbol = sanitizeSymbol(req.symbol); + if (!symbol) { + return { unavailable: true, symbol: '', totalBuys: 0, totalSells: 0, netValue: 0, transactions: [], fetchedAt: '' }; + } + + const apiKey = process.env.FINNHUB_API_KEY; + if (!apiKey) { + return { unavailable: true, symbol, totalBuys: 0, totalSells: 0, netValue: 0, transactions: [], fetchedAt: '' }; + } + + const cacheKey = `insider:${symbol}:v1`; + + try { + const result = await cachedFetchJson<{ + totalBuys: number; + totalSells: number; + netValue: number; + transactions: InsiderTransaction[]; + fetchedAt: string; + }>(cacheKey, CACHE_TTL_SECONDS, async () => { + await finnhubGate(); + const url = `https://finnhub.io/api/v1/stock/insider-transactions?symbol=${encodeURIComponent(symbol)}&token=${apiKey}`; + const resp = await fetch(url, { + headers: { 'User-Agent': CHROME_UA }, + signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS), + }); + if (!resp.ok) return null; + const raw = (await resp.json()) as FinnhubInsiderResponse; + if (!raw.data || raw.data.length === 0) return { + totalBuys: 0, + totalSells: 0, + netValue: 0, + transactions: [] as InsiderTransaction[], + fetchedAt: new Date().toISOString(), + }; + + const cutoff = Date.now() - SIX_MONTHS_MS; + const recent = raw.data.filter(tx => { + const txDate = new Date(tx.transactionDate).getTime(); + return Number.isFinite(txDate) && txDate >= cutoff; + }); + + let totalBuys = 0; + let totalSells = 0; + for (const tx of recent) { + const val = Math.abs((tx.change ?? 0) * (tx.transactionPrice ?? 0)); + if (PURCHASE_CODES.has(tx.transactionCode)) totalBuys += val; + else if (SALE_CODES.has(tx.transactionCode)) totalSells += val; + } + + const mapped: InsiderTransaction[] = recent + .filter(tx => + PURCHASE_CODES.has(tx.transactionCode) + || SALE_CODES.has(tx.transactionCode) + || NEUTRAL_CODES.has(tx.transactionCode), + ) + .sort((a, b) => new Date(b.transactionDate).getTime() - new Date(a.transactionDate).getTime()) + .slice(0, 20) + .map(tx => ({ + name: String(tx.name ?? ''), + shares: Math.abs(tx.change ?? 0), + // For exercise/conversion (code M), transactionPrice is the option + // strike price, not a market execution price, so the derived + // dollar amount would be misleading. Zero it out and let the UI + // render a placeholder. The buy/sell totals above already + // exclude M rows. + value: NEUTRAL_CODES.has(tx.transactionCode) + ? 0 + : Math.abs((tx.change ?? 0) * (tx.transactionPrice ?? 0)), + transactionCode: tx.transactionCode, + transactionDate: tx.transactionDate, + })); + + return { + totalBuys: Math.round(totalBuys), + totalSells: Math.round(totalSells), + netValue: Math.round(totalBuys - totalSells), + transactions: mapped, + fetchedAt: new Date().toISOString(), + }; + }); + + if (!result) { + return { unavailable: true, symbol, totalBuys: 0, totalSells: 0, netValue: 0, transactions: [], fetchedAt: '' }; + } + + return { + unavailable: false, + symbol, + totalBuys: result.totalBuys, + totalSells: result.totalSells, + netValue: result.netValue, + transactions: result.transactions, + fetchedAt: result.fetchedAt, + }; + } catch { + return { unavailable: true, symbol, totalBuys: 0, totalSells: 0, netValue: 0, transactions: [], fetchedAt: '' }; + } +} diff --git a/server/worldmonitor/market/v1/handler.ts b/server/worldmonitor/market/v1/handler.ts index 5d0488712..26b95a5df 100644 --- a/server/worldmonitor/market/v1/handler.ts +++ b/server/worldmonitor/market/v1/handler.ts @@ -32,6 +32,7 @@ import { listOtherTokens } from './list-other-tokens'; import { getFearGreedIndex } from './get-fear-greed-index'; import { listEarningsCalendar } from './list-earnings-calendar'; import { getCotPositioning } from './get-cot-positioning'; +import { getInsiderTransactions } from './get-insider-transactions'; export const marketHandler: MarketServiceHandler = { listMarketQuotes, @@ -53,4 +54,5 @@ export const marketHandler: MarketServiceHandler = { getFearGreedIndex, listEarningsCalendar, getCotPositioning, + getInsiderTransactions, }; diff --git a/src/app/data-loader.ts b/src/app/data-loader.ts index 8528d4bc5..aa79365d9 100644 --- a/src/app/data-loader.ts +++ b/src/app/data-loader.ts @@ -82,6 +82,7 @@ import { } from '@/services'; import { getMarketWatchlistEntries } from '@/services/market-watchlist'; import { fetchStockAnalysesForTargets, getStockAnalysisTargets, type StockAnalysisResult } from '@/services/stock-analysis'; +import { fetchInsiderTransactions } from '@/services/insider-transactions'; import { fetchStockBacktestsForTargets, fetchStoredStockBacktests, @@ -94,6 +95,7 @@ import { hasFreshStockAnalysisHistory, getLatestStockAnalysisSnapshots, mergeStockAnalysisHistory, + type StockAnalysisHistory, } from '@/services/stock-analysis-history'; import { checkBatchForBreakingAlerts, dispatchOrefBreakingAlert } from '@/services/breaking-news-alerts'; import { mlWorker } from '@/services/ml-worker'; @@ -273,6 +275,7 @@ export class DataLoaderManager implements AppModule { private boundMarketWatchlistHandler: (() => void) | null = null; private satellitePropagationCleanup: (() => void) | null = null; private dailyBriefGeneration = 0; + private _stockAnalysisGeneration = 0; private dailyBriefFrameworkUnsubscribe: (() => void) | null = null; private marketImplicationsFrameworkUnsubscribe: (() => void) | null = null; private cachedSatRecs: SatRecEntry[] | null = null; @@ -1204,16 +1207,32 @@ export class DataLoaderManager implements AppModule { const panel = this.ctx.panels['stock-analysis'] as StockAnalysisPanel | undefined; if (!panel) return; + // Bump generation so any in-flight insider fetch from a prior invocation + // of loadStockAnalysis no-ops instead of re-rendering stale snapshots on + // top of the current render. + const generation = ++this._stockAnalysisGeneration; + try { const targets = getStockAnalysisTargets(); const targetSymbols = targets.map((target) => target.symbol); const storedHistory = await fetchStockAnalysisHistory(targets.length); const cachedSnapshots = getLatestStockAnalysisSnapshots(storedHistory, targets.length); + const historyIsFresh = hasFreshStockAnalysisHistory(storedHistory, targetSymbols); + if (cachedSnapshots.length > 0) { panel.renderAnalyses(cachedSnapshots, storedHistory, 'cached'); } - if (hasFreshStockAnalysisHistory(storedHistory, targetSymbols)) { + if (historyIsFresh) { + // No live fetch coming — safe to enrich the cached render with + // insiders now. This is the only cached-path insider fetch; when a + // live fetch is about to run we defer insider enrichment until after + // the live render so we never re-render stale cached snapshots over + // fresh live data. + if (cachedSnapshots.length > 0) { + void this.loadInsiderDataForPanel(panel, targetSymbols, cachedSnapshots, storedHistory, 'cached', generation) + .catch((error) => console.error('[StockAnalysis] insider fetch failed:', error)); + } return; } @@ -1223,7 +1242,13 @@ export class DataLoaderManager implements AppModule { if (results.length === 0) { if (cachedSnapshots.length === 0) { panel.showRetrying('Stock analysis is waiting for eligible watchlist symbols.'); + return; } + // Live fetch returned nothing but we already rendered cachedSnapshots + // above. Enrich the displayed cached snapshots with insider data so + // the user still sees the insider section. + void this.loadInsiderDataForPanel(panel, targetSymbols, cachedSnapshots, storedHistory, 'cached', generation) + .catch((error) => console.error('[StockAnalysis] insider fetch failed:', error)); return; } const nextHistory = mergeStockAnalysisHistory(storedHistory, results); @@ -1241,11 +1266,10 @@ export class DataLoaderManager implements AppModule { const cached = storedHistory[target.symbol]?.[0]; if (cached?.available) combined.push(cached); } - if (combined.length > 0) { - panel.renderAnalyses(combined, nextHistory, 'live'); - } else { - panel.renderAnalyses(results, nextHistory, 'live'); - } + const snapshotsToRender = combined.length > 0 ? combined : results; + panel.renderAnalyses(snapshotsToRender, nextHistory, 'live'); + void this.loadInsiderDataForPanel(panel, targetSymbols, snapshotsToRender, nextHistory, 'live', generation) + .catch((error) => console.error('[StockAnalysis] insider fetch failed:', error)); } catch (error) { console.error('[StockAnalysis] failed:', error); const cachedHistory = await fetchStockAnalysisHistory().catch(() => ({})); @@ -1258,6 +1282,34 @@ export class DataLoaderManager implements AppModule { } } + private async loadInsiderDataForPanel( + panel: StockAnalysisPanel, + symbols: string[], + snapshotsToReRender: StockAnalysisResult[], + historyForReRender: StockAnalysisHistory, + source: 'live' | 'cached', + generation: number, + ): Promise { + const results = await Promise.allSettled(symbols.map(s => fetchInsiderTransactions(s))); + // If another loadStockAnalysis invocation has started while this fetch + // was in flight, drop the result entirely — both setInsiderData and the + // re-render would clobber the current state. + if (generation !== this._stockAnalysisGeneration) return; + for (let i = 0; i < symbols.length; i++) { + const r = results[i]; + if (r && r.status === 'fulfilled') { + panel.setInsiderData(symbols[i]!, r.value); + } else { + panel.setInsiderData(symbols[i]!, { unavailable: true, symbol: symbols[i]!, totalBuys: 0, totalSells: 0, netValue: 0, transactions: [], fetchedAt: '' }); + } + } + // Re-render the panel so the insider section becomes visible now that + // setInsiderData has populated insiderBySymbol. Guard once more in case + // something awaited between the setInsiderData calls above. + if (generation !== this._stockAnalysisGeneration) return; + panel.renderAnalyses(snapshotsToReRender, historyForReRender, source); + } + async loadStockBacktest(): Promise { const panel = this.ctx.panels['stock-backtest'] as StockBacktestPanel | undefined; if (!panel) return; diff --git a/src/components/StockAnalysisPanel.ts b/src/components/StockAnalysisPanel.ts index 7119dd791..b140d1d25 100644 --- a/src/components/StockAnalysisPanel.ts +++ b/src/components/StockAnalysisPanel.ts @@ -2,6 +2,7 @@ import { Panel } from './Panel'; import { t } from '@/services/i18n'; import type { StockAnalysisResult } from '@/services/stock-analysis'; import type { AnalystConsensus, PriceTarget, UpgradeDowngrade } from '@/generated/client/worldmonitor/market/v1/service_client'; +import type { InsiderTransactionsResult } from '@/services/insider-transactions'; import { escapeHtml, sanitizeUrl } from '@/utils/sanitize'; import type { StockAnalysisHistory } from '@/services/stock-analysis-history'; import { sparkline } from '@/utils/sparkline'; @@ -28,11 +29,35 @@ function list(items: string[], cssClass: string): string { return `
    ${items.map((item) => `
  • ${escapeHtml(item)}
  • `).join('')}
`; } +function formatDollarCompact(value: number): string { + const abs = Math.abs(value); + if (abs >= 1e9) return `$${(value / 1e9).toFixed(1)}B`; + if (abs >= 1e6) return `$${(value / 1e6).toFixed(1)}M`; + if (abs >= 1e3) return `$${(value / 1e3).toFixed(0)}K`; + return `$${value.toFixed(0)}`; +} + +function txCodeLabel(code: string): string { + if (code === 'P') return 'Buy'; + if (code === 'S') return 'Sell'; + if (code === 'M') return 'Exercise'; + if (code === 'A') return 'Award'; + if (code === 'D') return 'Disposition'; + if (code === 'F') return 'Tax/Fee'; + return code; +} + export class StockAnalysisPanel extends Panel { + private insiderBySymbol: Record = {}; + constructor() { super({ id: 'stock-analysis', title: 'Premium Stock Analysis', infoTooltip: t('components.stockAnalysis.infoTooltip'), premium: 'locked' }); } + public setInsiderData(symbol: string, data: InsiderTransactionsResult): void { + this.insiderBySymbol[symbol] = data; + } + public renderAnalyses(items: StockAnalysisResult[], historyBySymbol: StockAnalysisHistory = {}, source: 'live' | 'cached' = 'live'): void { if (items.length === 0) { this.setDataBadge('unavailable'); @@ -180,6 +205,7 @@ export class StockAnalysisPanel extends Panel { `).join('')} ` : ''} + ${this.renderInsiderSection(item.symbol)} ${headlines ? `
${headlines}
` : ''} ${this.renderAnalystConsensus(item)} @@ -291,4 +317,81 @@ export class StockAnalysisPanel extends Panel { `; } + + private renderInsiderSection(symbol: string): string { + const data = this.insiderBySymbol[symbol]; + // Unknown / not yet fetched: omit the section entirely. A later + // re-render from loadInsiderDataForPanel fills it in, so we avoid a + // transient "Insider data unavailable" flash on initial render before + // the RPC completes. + if (data === undefined) { + return ''; + } + if (data.unavailable) { + return ` +
+ Insider data unavailable +
`; + } + if (data.transactions.length === 0 && data.totalBuys === 0 && data.totalSells === 0) { + return ` +
+ No insider transactions in the last 6 months +
`; + } + + const buysStr = formatDollarCompact(data.totalBuys); + const sellsStr = formatDollarCompact(data.totalSells); + const netStr = `${data.netValue >= 0 ? '+' : ''}${formatDollarCompact(data.netValue)}`; + const netColor = data.netValue >= 0 ? 'var(--semantic-normal)' : 'var(--semantic-critical)'; + + const summary = ` +
+ Buys: ${escapeHtml(buysStr)} + Sells: ${escapeHtml(sellsStr)} + Net: ${escapeHtml(netStr)} +
`; + + const rows = data.transactions.slice(0, 5); + const table = rows.length > 0 ? ` + + + + + + + + + + + + ${rows.map(tx => { + const isBuy = tx.transactionCode === 'P'; + const isSell = tx.transactionCode === 'S'; + const typeColor = isBuy ? 'var(--semantic-normal)' : isSell ? 'var(--semantic-critical)' : 'var(--text-dim)'; + // Non-market rows (M/A/D/F) carry value: 0 from the server because + // their transactionPrice is not a market execution price (strike, + // grant price, buyback redemption, or tax withholding). Render a + // dash so users do not read a misleading dollar figure that + // contradicts the buy/sell totals (which only count P and S). + const valueCell = tx.value === 0 ? '—' : formatDollarCompact(tx.value); + return ` + + + + + + + `; + }).join('')} + +
NameTypeSharesValueDate
${escapeHtml(tx.name)}${escapeHtml(txCodeLabel(tx.transactionCode))}${Number.isFinite(tx.shares) ? tx.shares.toLocaleString() : '0'}${valueCell}${escapeHtml(tx.transactionDate)}
` : ''; + + return ` +
+
Insider Activity (6 months)
+ ${summary} + ${table} +
`; + } } diff --git a/src/generated/client/worldmonitor/market/v1/service_client.ts b/src/generated/client/worldmonitor/market/v1/service_client.ts index c4b05d9f9..359741838 100644 --- a/src/generated/client/worldmonitor/market/v1/service_client.ts +++ b/src/generated/client/worldmonitor/market/v1/service_client.ts @@ -462,6 +462,28 @@ export interface CotInstrument { netPct: number; } +export interface GetInsiderTransactionsRequest { + symbol: string; +} + +export interface GetInsiderTransactionsResponse { + unavailable: boolean; + symbol: string; + totalBuys: number; + totalSells: number; + netValue: number; + transactions: InsiderTransaction[]; + fetchedAt: string; +} + +export interface InsiderTransaction { + name: string; + shares: number; + value: number; + transactionCode: string; + transactionDate: string; +} + export interface FieldViolation { field: string; description: string; @@ -977,6 +999,31 @@ export class MarketServiceClient { return await resp.json() as GetCotPositioningResponse; } + async getInsiderTransactions(req: GetInsiderTransactionsRequest, options?: MarketServiceCallOptions): Promise { + let path = "/api/market/v1/get-insider-transactions"; + const params = new URLSearchParams(); + if (req.symbol != null && req.symbol !== "") params.set("symbol", String(req.symbol)); + const url = this.baseURL + path + (params.toString() ? "?" + params.toString() : ""); + + const headers: Record = { + "Content-Type": "application/json", + ...this.defaultHeaders, + ...options?.headers, + }; + + const resp = await this.fetchFn(url, { + method: "GET", + headers, + signal: options?.signal, + }); + + if (!resp.ok) { + return this.handleError(resp); + } + + return await resp.json() as GetInsiderTransactionsResponse; + } + 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 347ecb4fc..3ec5eb2e1 100644 --- a/src/generated/server/worldmonitor/market/v1/service_server.ts +++ b/src/generated/server/worldmonitor/market/v1/service_server.ts @@ -462,6 +462,28 @@ export interface CotInstrument { netPct: number; } +export interface GetInsiderTransactionsRequest { + symbol: string; +} + +export interface GetInsiderTransactionsResponse { + unavailable: boolean; + symbol: string; + totalBuys: number; + totalSells: number; + netValue: number; + transactions: InsiderTransaction[]; + fetchedAt: string; +} + +export interface InsiderTransaction { + name: string; + shares: number; + value: number; + transactionCode: string; + transactionDate: string; +} + export interface FieldViolation { field: string; description: string; @@ -526,6 +548,7 @@ export interface MarketServiceHandler { getFearGreedIndex(ctx: ServerContext, req: GetFearGreedIndexRequest): Promise; listEarningsCalendar(ctx: ServerContext, req: ListEarningsCalendarRequest): Promise; getCotPositioning(ctx: ServerContext, req: GetCotPositioningRequest): Promise; + getInsiderTransactions(ctx: ServerContext, req: GetInsiderTransactionsRequest): Promise; } export function createMarketServiceRoutes( @@ -1354,6 +1377,53 @@ export function createMarketServiceRoutes( } }, }, + { + method: "GET", + path: "/api/market/v1/get-insider-transactions", + handler: async (req: Request): Promise => { + try { + const pathParams: Record = {}; + const url = new URL(req.url, "http://localhost"); + const params = url.searchParams; + const body: GetInsiderTransactionsRequest = { + symbol: params.get("symbol") ?? "", + }; + if (options?.validateRequest) { + const bodyViolations = options.validateRequest("getInsiderTransactions", body); + if (bodyViolations) { + throw new ValidationError(bodyViolations); + } + } + + const ctx: ServerContext = { + request: req, + pathParams, + headers: Object.fromEntries(req.headers.entries()), + }; + + const result = await handler.getInsiderTransactions(ctx, body); + return new Response(JSON.stringify(result as GetInsiderTransactionsResponse), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (err: unknown) { + if (err instanceof ValidationError) { + return new Response(JSON.stringify({ violations: err.violations }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + if (options?.onError) { + return options.onError(err, req); + } + const message = err instanceof Error ? err.message : String(err); + return new Response(JSON.stringify({ message }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } + }, + }, ]; } diff --git a/src/services/insider-transactions.ts b/src/services/insider-transactions.ts new file mode 100644 index 000000000..a158f804f --- /dev/null +++ b/src/services/insider-transactions.ts @@ -0,0 +1,14 @@ +import { getRpcBaseUrl } from '@/services/rpc-client'; +import { + MarketServiceClient, + type GetInsiderTransactionsResponse, +} from '@/generated/client/worldmonitor/market/v1/service_client'; +import { premiumFetch } from '@/services/premium-fetch'; + +const client = new MarketServiceClient(getRpcBaseUrl(), { fetch: premiumFetch }); + +export type InsiderTransactionsResult = GetInsiderTransactionsResponse; + +export async function fetchInsiderTransactions(symbol: string): Promise { + return client.getInsiderTransactions({ symbol }); +} diff --git a/src/shared/premium-paths.ts b/src/shared/premium-paths.ts index f71147b9b..9ccd0748a 100644 --- a/src/shared/premium-paths.ts +++ b/src/shared/premium-paths.ts @@ -8,6 +8,7 @@ export const PREMIUM_RPC_PATHS = new Set([ '/api/market/v1/analyze-stock', '/api/market/v1/get-stock-analysis-history', '/api/market/v1/backtest-stock', + '/api/market/v1/get-insider-transactions', '/api/market/v1/list-stored-stock-backtests', '/api/intelligence/v1/deduct-situation', '/api/intelligence/v1/list-market-implications', diff --git a/tests/insider-transactions.test.mts b/tests/insider-transactions.test.mts new file mode 100644 index 000000000..abbfbca20 --- /dev/null +++ b/tests/insider-transactions.test.mts @@ -0,0 +1,235 @@ +import assert from 'node:assert/strict'; +import { afterEach, describe, it } from 'node:test'; + +import { getInsiderTransactions } from '../server/worldmonitor/market/v1/get-insider-transactions.ts'; + +const originalFetch = globalThis.fetch; +const originalEnv = { ...process.env }; + +function mockFinnhubResponse(data: unknown[]) { + return new Response(JSON.stringify({ data, symbol: 'AAPL' }), { status: 200 }); +} + +function recentDate(daysAgo: number): string { + const d = new Date(Date.now() - daysAgo * 86_400_000); + return d.toISOString().split('T')[0]!; +} + +afterEach(() => { + globalThis.fetch = originalFetch; + process.env.FINNHUB_API_KEY = originalEnv.FINNHUB_API_KEY; +}); + +describe('getInsiderTransactions handler', () => { + it('returns unavailable when FINNHUB_API_KEY is missing', async () => { + delete process.env.FINNHUB_API_KEY; + const resp = await getInsiderTransactions({} as never, { symbol: 'AAPL' }); + assert.equal(resp.unavailable, true); + assert.equal(resp.symbol, 'AAPL'); + }); + + it('returns unavailable when symbol is empty', async () => { + process.env.FINNHUB_API_KEY = 'test-key'; + const resp = await getInsiderTransactions({} as never, { symbol: '' }); + assert.equal(resp.unavailable, true); + }); + + it('aggregates purchase and sale totals for recent transactions', async () => { + process.env.FINNHUB_API_KEY = 'test-key'; + globalThis.fetch = (async () => { + return mockFinnhubResponse([ + { name: 'Tim Cook', share: 10000, change: 10000, transactionPrice: 150, transactionCode: 'P', transactionDate: recentDate(10), filingDate: recentDate(8) }, + { name: 'Jeff Williams', share: 5000, change: -5000, transactionPrice: 155, transactionCode: 'S', transactionDate: recentDate(20), filingDate: recentDate(18) }, + { name: 'Luca Maestri', share: 2000, change: 2000, transactionPrice: 148, transactionCode: 'P', transactionDate: recentDate(30), filingDate: recentDate(28) }, + ]); + }) as typeof fetch; + + const resp = await getInsiderTransactions({} as never, { symbol: 'AAPL' }); + assert.equal(resp.unavailable, false); + assert.equal(resp.symbol, 'AAPL'); + assert.equal(resp.totalBuys, 10000 * 150 + 2000 * 148); + assert.equal(resp.totalSells, 5000 * 155); + assert.equal(resp.netValue, resp.totalBuys - resp.totalSells); + assert.equal(resp.transactions.length, 3); + assert.equal(resp.transactions[0]!.name, 'Tim Cook'); + }); + + it('filters out transactions older than 6 months', async () => { + process.env.FINNHUB_API_KEY = 'test-key'; + globalThis.fetch = (async () => { + return mockFinnhubResponse([ + { name: 'Recent Exec', share: 1000, change: 1000, transactionPrice: 100, transactionCode: 'P', transactionDate: recentDate(30), filingDate: recentDate(28) }, + { name: 'Old Exec', share: 5000, change: 5000, transactionPrice: 100, transactionCode: 'P', transactionDate: recentDate(200), filingDate: recentDate(198) }, + ]); + }) as typeof fetch; + + const resp = await getInsiderTransactions({} as never, { symbol: 'AAPL' }); + assert.equal(resp.unavailable, false); + assert.equal(resp.transactions.length, 1); + assert.equal(resp.transactions[0]!.name, 'Recent Exec'); + assert.equal(resp.totalBuys, 100000); + }); + + it('returns unavailable on upstream failure', async () => { + process.env.FINNHUB_API_KEY = 'test-key'; + globalThis.fetch = (async () => { + return new Response('error', { status: 500 }); + }) as typeof fetch; + + const resp = await getInsiderTransactions({} as never, { symbol: 'AAPL' }); + assert.equal(resp.unavailable, true); + }); + + it('returns no-activity when Finnhub returns empty data', async () => { + process.env.FINNHUB_API_KEY = 'test-key'; + globalThis.fetch = (async () => { + return mockFinnhubResponse([]); + }) as typeof fetch; + + const resp = await getInsiderTransactions({} as never, { symbol: 'AAPL' }); + assert.equal(resp.unavailable, false); + assert.equal(resp.transactions.length, 0); + }); + + it('passes the symbol in the Finnhub URL', async () => { + process.env.FINNHUB_API_KEY = 'test-key'; + let requestedUrl = ''; + globalThis.fetch = (async (input: RequestInfo | URL) => { + requestedUrl = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url; + return mockFinnhubResponse([ + { name: 'Exec', share: 100, change: 100, transactionPrice: 50, transactionCode: 'P', transactionDate: recentDate(5), filingDate: recentDate(3) }, + ]); + }) as typeof fetch; + + await getInsiderTransactions({} as never, { symbol: 'MSFT' }); + assert.match(requestedUrl, /symbol=MSFT/); + assert.match(requestedUrl, /token=test-key/); + }); + + it('sorts transactions by date descending', async () => { + process.env.FINNHUB_API_KEY = 'test-key'; + globalThis.fetch = (async () => { + return mockFinnhubResponse([ + { name: 'Older', share: 100, change: 100, transactionPrice: 50, transactionCode: 'P', transactionDate: recentDate(60), filingDate: recentDate(58) }, + { name: 'Newer', share: 200, change: 200, transactionPrice: 50, transactionCode: 'S', transactionDate: recentDate(10), filingDate: recentDate(8) }, + { name: 'Middle', share: 150, change: 150, transactionPrice: 50, transactionCode: 'P', transactionDate: recentDate(30), filingDate: recentDate(28) }, + ]); + }) as typeof fetch; + + const resp = await getInsiderTransactions({} as never, { symbol: 'AAPL' }); + assert.equal(resp.transactions[0]!.name, 'Newer'); + assert.equal(resp.transactions[1]!.name, 'Middle'); + assert.equal(resp.transactions[2]!.name, 'Older'); + }); + + it('surfaces exercise-only (code M) activity so panels do not show empty', async () => { + process.env.FINNHUB_API_KEY = 'test-key'; + globalThis.fetch = (async () => { + return mockFinnhubResponse([ + { name: 'CFO Exercise', share: 5000, change: 5000, transactionPrice: 10, transactionCode: 'M', transactionDate: recentDate(15), filingDate: recentDate(13) }, + { name: 'CTO Exercise', share: 3000, change: 3000, transactionPrice: 8, transactionCode: 'M', transactionDate: recentDate(25), filingDate: recentDate(23) }, + ]); + }) as typeof fetch; + + const resp = await getInsiderTransactions({} as never, { symbol: 'AAPL' }); + assert.equal(resp.unavailable, false); + assert.equal(resp.transactions.length, 2, 'exercise-only activity must reach the client so panels render the table'); + assert.equal(resp.transactions[0]!.transactionCode, 'M'); + // Exercise activity does not contribute to buys/sells dollar totals because + // transactionPrice is the option strike, not a market purchase/sale price. + assert.equal(resp.totalBuys, 0); + assert.equal(resp.totalSells, 0); + assert.equal(resp.netValue, 0); + }); + + it('zeros out per-row value for exercise (code M) rows so UI can render a dash placeholder', async () => { + process.env.FINNHUB_API_KEY = 'test-key'; + globalThis.fetch = (async () => { + return mockFinnhubResponse([ + { name: 'CFO Exercise', share: 5000, change: 5000, transactionPrice: 10, transactionCode: 'M', transactionDate: recentDate(15), filingDate: recentDate(13) }, + { name: 'Buyer', share: 1000, change: 1000, transactionPrice: 100, transactionCode: 'P', transactionDate: recentDate(5), filingDate: recentDate(3) }, + ]); + }) as typeof fetch; + + const resp = await getInsiderTransactions({} as never, { symbol: 'AAPL' }); + const mRow = resp.transactions.find(t => t.transactionCode === 'M'); + const pRow = resp.transactions.find(t => t.transactionCode === 'P'); + assert.ok(mRow, 'M row should be present'); + assert.ok(pRow, 'P row should be present'); + // Shares should still be populated for exercise rows. + assert.equal(mRow!.shares, 5000); + // But the dollar value must be zero because transactionPrice is the + // strike price, not a market execution price. Rendering the naive + // product would be misleading and contradict the buy/sell totals. + assert.equal(mRow!.value, 0, 'exercise row must carry value: 0'); + // Regular buys still carry a real dollar value. + assert.equal(pRow!.value, 100_000); + }); + + it('excludes non-market Form 4 codes (A/D/F) from buy/sell totals', async () => { + process.env.FINNHUB_API_KEY = 'test-key'; + globalThis.fetch = (async () => { + return mockFinnhubResponse([ + // Grant/award — compensation, not a market purchase. + { name: 'Awardee', share: 10000, change: 10000, transactionPrice: 150, transactionCode: 'A', transactionDate: recentDate(5), filingDate: recentDate(3) }, + // Disposition to issuer — e.g. buyback redemption. + { name: 'Dispositioner', share: 5000, change: -5000, transactionPrice: 160, transactionCode: 'D', transactionDate: recentDate(10), filingDate: recentDate(8) }, + // Payment of exercise price / tax withholding — mechanical, not discretionary. + { name: 'TaxPayer', share: 2000, change: -2000, transactionPrice: 155, transactionCode: 'F', transactionDate: recentDate(15), filingDate: recentDate(13) }, + // One real buy so we can assert only P counts toward totalBuys. + { name: 'Buyer', share: 1000, change: 1000, transactionPrice: 100, transactionCode: 'P', transactionDate: recentDate(20), filingDate: recentDate(18) }, + ]); + }) as typeof fetch; + + const resp = await getInsiderTransactions({} as never, { symbol: 'AAPL' }); + assert.equal(resp.unavailable, false); + // Only the P row contributes to totalBuys; A/D/F contribute nothing. + assert.equal(resp.totalBuys, 100_000); + assert.equal(resp.totalSells, 0); + assert.equal(resp.netValue, 100_000); + // A/D/F rows still reach the client so the panel does not look empty, + // but their per-row dollar value is zeroed out (rendered as a dash). + assert.equal(resp.transactions.length, 4); + const aRow = resp.transactions.find(t => t.transactionCode === 'A'); + const dRow = resp.transactions.find(t => t.transactionCode === 'D'); + const fRow = resp.transactions.find(t => t.transactionCode === 'F'); + assert.ok(aRow && dRow && fRow, 'A/D/F rows should be surfaced'); + assert.equal(aRow!.value, 0); + assert.equal(dRow!.value, 0); + assert.equal(fRow!.value, 0); + }); + + it('blends exercise codes with buys and sells', async () => { + process.env.FINNHUB_API_KEY = 'test-key'; + globalThis.fetch = (async () => { + return mockFinnhubResponse([ + { name: 'Buyer', share: 1000, change: 1000, transactionPrice: 100, transactionCode: 'P', transactionDate: recentDate(5), filingDate: recentDate(3) }, + { name: 'Exerciser', share: 500, change: 500, transactionPrice: 10, transactionCode: 'M', transactionDate: recentDate(10), filingDate: recentDate(8) }, + { name: 'Seller', share: 2000, change: -2000, transactionPrice: 105, transactionCode: 'S', transactionDate: recentDate(15), filingDate: recentDate(13) }, + ]); + }) as typeof fetch; + + const resp = await getInsiderTransactions({} as never, { symbol: 'AAPL' }); + assert.equal(resp.transactions.length, 3); + assert.equal(resp.totalBuys, 100000); + assert.equal(resp.totalSells, 210000); + const codes = resp.transactions.map(t => t.transactionCode).sort(); + assert.deepEqual(codes, ['M', 'P', 'S']); + }); +}); + +describe('MarketServiceClient getInsiderTransactions', () => { + it('serializes the query parameters using generated names', async () => { + const { MarketServiceClient } = await import('../src/generated/client/worldmonitor/market/v1/service_client.ts'); + let requestedUrl = ''; + globalThis.fetch = (async (input: RequestInfo | URL) => { + requestedUrl = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url; + return new Response(JSON.stringify({ unavailable: true }), { status: 200 }); + }) as typeof fetch; + + const client = new MarketServiceClient(''); + await client.getInsiderTransactions({ symbol: 'TSLA' }); + assert.match(requestedUrl, /\/api\/market\/v1\/get-insider-transactions\?/); + assert.match(requestedUrl, /symbol=TSLA/); + }); +});