""" Data & Market Router. Endpoints for price data, company info, market metadata, news, features, and data ingestion triggers. """ from __future__ import annotations from typing import List, Optional from fastapi import APIRouter, Depends, Query from app.dependencies import get_current_user from app.schemas.market import ( AssetSearchResult, ExchangeOut, FeatureRequest, FeatureResponse, NewsArticleOut, NewsSearchRequest, PriceHistoryRequest, PriceHistoryResponse, ) from app.services.data_ingestion.market_metadata import market_metadata from app.services.data_ingestion.news import news_adapter from app.services.data_ingestion.yahoo import yahoo_adapter from app.services.feature_engineering.pipeline import feature_pipeline router = APIRouter(prefix="/data", tags=["Market Data"]) # ── Ticker Search ──────────────────────────────────────────────────────── @router.get("/search-tickers") async def search_tickers( q: str = Query("", description="Search query for tickers"), limit: int = Query(15, ge=1, le=50), ): """Search for tickers across all global markets (stocks, ETFs, futures, options, indices, crypto).""" if not q or len(q.strip()) < 1: return {"results": []} results = await yahoo_adapter.search_tickers(q.strip(), limit=limit) return {"results": results} # ── Exchanges ──────────────────────────────────────────────────────────── @router.get("/exchanges", response_model=List[ExchangeOut]) async def list_exchanges(): """Get all supported exchanges.""" return market_metadata.get_all_exchanges() @router.get("/exchanges/{code}") async def get_exchange(code: str): """Get exchange details by code.""" exc = market_metadata.get_exchange(code) if not exc: return {"error": f"Exchange '{code}' not found"} return exc @router.get("/markets") async def list_markets(): """Get all supported market regions with their popular tickers.""" markets = market_metadata.get_all_markets() result = {} for m in markets: result[m] = { "tickers": market_metadata.get_popular_tickers(m), "exchanges": [ e for e in market_metadata.get_all_exchanges() if any(m.lower() in e.get("country", "").lower() for _ in [1]) ], } return result @router.get("/sectors") async def list_sectors(): """Get GICS sector classification.""" return {"sectors": market_metadata.get_sectors()} @router.get("/popular-tickers") async def popular_tickers(market: str = "US"): """Get popular tickers for a market region.""" return {"market": market, "tickers": market_metadata.get_popular_tickers(market)} # ── Prices ─────────────────────────────────────────────────────────────── @router.get("/prices/{ticker}") async def get_price_history( ticker: str, period: str = Query("1y", description="1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max"), interval: str = Query("1d", description="1d, 1wk, 1mo"), ): """Fetch historical OHLCV price data for a ticker.""" data = await yahoo_adapter.fetch_price_history(ticker, period=period, interval=interval) return data @router.get("/company/{ticker}") async def get_company_info(ticker: str): """Fetch detailed company information.""" return await yahoo_adapter.fetch_company_info(ticker) # ── Features ───────────────────────────────────────────────────────────── @router.post("/features") async def compute_features(request: FeatureRequest): """Compute technical features for a ticker.""" df = await yahoo_adapter.get_price_dataframe(request.ticker, period=request.period) if df.empty: return {"ticker": request.ticker, "data": [], "error": "No price data available"} featured = feature_pipeline.compute_all_features(df, request.features) # Convert to response format feature_cols = [c for c in featured.columns if c not in ["Open", "High", "Low", "Close", "Volume", "Dividends", "Stock Splits"]] data = [] for idx, row in featured.iterrows(): date_str = idx.strftime("%Y-%m-%d") if hasattr(idx, "strftime") else str(idx) features_dict = {col: round(float(row[col]), 6) if not (row[col] != row[col]) else None for col in feature_cols} data.append({"date": date_str, "features": features_dict}) return { "ticker": request.ticker, "data": data[-60:], # Last 60 data points "available_features": feature_cols, } # ── News ───────────────────────────────────────────────────────────────── @router.post("/news") async def search_news(request: NewsSearchRequest): """Search for news articles.""" return await news_adapter.fetch_articles( query=request.query, from_date=request.from_date, to_date=request.to_date, sources=request.sources, page_size=request.page_size, ) @router.get("/news/headlines") async def get_headlines( country: str = "us", category: Optional[str] = None, ): """Get top news headlines.""" return await news_adapter.fetch_headlines(country=country, category=category)