from fastapi import APIRouter, Path, Query from mappers import ( discover_items_from_list, metrics_from_dict, profile_from_dict, quote_from_dict, ) from models import ( ApiResponse, FinancialsResponse, HistoricalBar, NewsItem, PortfolioRequest, PortfolioResponse, SummaryResponse, ) from route_utils import safe, validate_symbol import openbb_service import analysis_service router = APIRouter(prefix="/api/v1") # --- Stock Data --- @router.get("/stock/{symbol}/quote", response_model=ApiResponse) @safe async def stock_quote(symbol: str = Path(..., min_length=1, max_length=20)): """Get current quote for a stock.""" symbol = validate_symbol(symbol) data = await openbb_service.get_quote(symbol) return ApiResponse(data=quote_from_dict(symbol, data).model_dump()) @router.get("/stock/{symbol}/profile", response_model=ApiResponse) @safe async def stock_profile(symbol: str = Path(..., min_length=1, max_length=20)): """Get company profile.""" symbol = validate_symbol(symbol) data = await openbb_service.get_profile(symbol) return ApiResponse(data=profile_from_dict(symbol, data).model_dump()) @router.get("/stock/{symbol}/metrics", response_model=ApiResponse) @safe async def stock_metrics(symbol: str = Path(..., min_length=1, max_length=20)): """Get key financial metrics (PE, PB, ROE, etc.).""" symbol = validate_symbol(symbol) data = await openbb_service.get_metrics(symbol) return ApiResponse(data=metrics_from_dict(symbol, data).model_dump()) @router.get("/stock/{symbol}/financials", response_model=ApiResponse) @safe async def stock_financials(symbol: str = Path(..., min_length=1, max_length=20)): """Get income statement, balance sheet, and cash flow.""" symbol = validate_symbol(symbol) data = await openbb_service.get_financials(symbol) return ApiResponse(data=FinancialsResponse(**data).model_dump()) @router.get("/stock/{symbol}/historical", response_model=ApiResponse) @safe async def stock_historical( symbol: str = Path(..., min_length=1, max_length=20), days: int = Query(default=365, ge=1, le=3650), ): """Get historical price data.""" symbol = validate_symbol(symbol) data = await openbb_service.get_historical(symbol, days=days) bars = [ HistoricalBar( date=str(item.get("date", "")), open=item.get("open"), high=item.get("high"), low=item.get("low"), close=item.get("close"), volume=item.get("volume"), ).model_dump() for item in data ] return ApiResponse(data=bars) @router.get("/stock/{symbol}/news", response_model=ApiResponse) @safe async def stock_news(symbol: str = Path(..., min_length=1, max_length=20)): """Get recent company news.""" symbol = validate_symbol(symbol) data = await openbb_service.get_news(symbol) news = [ NewsItem( title=item.get("title"), url=item.get("url"), date=str(item.get("date", "")), source=item.get("source"), ).model_dump() for item in data ] return ApiResponse(data=news) @router.get("/stock/{symbol}/summary", response_model=ApiResponse) @safe async def stock_summary(symbol: str = Path(..., min_length=1, max_length=20)): """Get aggregated stock data: quote + profile + metrics + financials.""" symbol = validate_symbol(symbol) data = await openbb_service.get_summary(symbol) summary = SummaryResponse( quote=quote_from_dict(symbol, data.get("quote", {})), profile=profile_from_dict(symbol, data.get("profile", {})), metrics=metrics_from_dict(symbol, data.get("metrics", {})), financials=FinancialsResponse( **data.get("financials", {"symbol": symbol}) ), ) return ApiResponse(data=summary.model_dump()) # --- Portfolio Analysis --- @router.post("/portfolio/analyze", response_model=ApiResponse) @safe async def portfolio_analyze(request: PortfolioRequest): """Analyze portfolio holdings with rule-based engine.""" result: PortfolioResponse = await analysis_service.analyze_portfolio( request.holdings ) return ApiResponse(data=result.model_dump()) # --- Discovery --- @router.get("/discover/gainers", response_model=ApiResponse) @safe async def discover_gainers(): """Get top gainers (US market).""" data = await openbb_service.get_gainers() return ApiResponse(data=discover_items_from_list(data)) @router.get("/discover/losers", response_model=ApiResponse) @safe async def discover_losers(): """Get top losers (US market).""" data = await openbb_service.get_losers() return ApiResponse(data=discover_items_from_list(data)) @router.get("/discover/active", response_model=ApiResponse) @safe async def discover_active(): """Get most active stocks (US market).""" data = await openbb_service.get_active() return ApiResponse(data=discover_items_from_list(data)) @router.get("/discover/undervalued", response_model=ApiResponse) @safe async def discover_undervalued(): """Get undervalued large cap stocks.""" data = await openbb_service.get_undervalued() return ApiResponse(data=discover_items_from_list(data)) @router.get("/discover/growth", response_model=ApiResponse) @safe async def discover_growth(): """Get growth tech stocks.""" data = await openbb_service.get_growth() return ApiResponse(data=discover_items_from_list(data))