""" Data explorer API endpoints for viewing collected stock/ETF data. """ from datetime import date from typing import Optional, List from fastapi import APIRouter, Depends, Query from sqlalchemy import or_ from sqlalchemy.orm import Session from pydantic import BaseModel from app.core.database import get_db from app.api.deps import CurrentUser from app.models.stock import Stock, ETF, Price, ETFPrice, Sector, Valuation from app.schemas.portfolio import FloatDecimal router = APIRouter(prefix="/api/data", tags=["data-explorer"]) # --- Response schemas --- class PaginatedResponse(BaseModel): items: list total: int page: int size: int class StockItem(BaseModel): ticker: str name: str market: str close_price: FloatDecimal | None = None market_cap: int | None = None class Config: from_attributes = True class ETFItem(BaseModel): ticker: str name: str asset_class: str market: str expense_ratio: FloatDecimal | None = None class Config: from_attributes = True class PriceItem(BaseModel): date: date open: FloatDecimal | None = None high: FloatDecimal | None = None low: FloatDecimal | None = None close: FloatDecimal volume: int | None = None class Config: from_attributes = True class ETFPriceItem(BaseModel): date: date close: FloatDecimal nav: FloatDecimal | None = None volume: int | None = None class Config: from_attributes = True class SectorItem(BaseModel): ticker: str company_name: str sector_code: str sector_name: str class Config: from_attributes = True class ValuationItem(BaseModel): ticker: str base_date: date per: FloatDecimal | None = None pbr: FloatDecimal | None = None psr: FloatDecimal | None = None pcr: FloatDecimal | None = None dividend_yield: FloatDecimal | None = None class Config: from_attributes = True # --- Endpoints --- @router.get("/stocks") async def list_stocks( current_user: CurrentUser, db: Session = Depends(get_db), page: int = Query(1, ge=1), size: int = Query(50, ge=1, le=200), search: Optional[str] = None, market: Optional[str] = None, ): """List collected stocks with pagination and search.""" query = db.query(Stock) if search: query = query.filter( or_(Stock.ticker.contains(search), Stock.name.contains(search)) ) if market: query = query.filter(Stock.market == market) total = query.count() items = query.order_by(Stock.ticker).offset((page - 1) * size).limit(size).all() return { "items": [StockItem.model_validate(s) for s in items], "total": total, "page": page, "size": size, } @router.get("/stocks/{ticker}/prices") async def get_stock_prices( ticker: str, current_user: CurrentUser, db: Session = Depends(get_db), skip: int = Query(0, ge=0), limit: int = Query(365, ge=1, le=3000), ): """Get daily prices for a stock with pagination.""" base_query = db.query(Price).filter(Price.ticker == ticker) total = base_query.count() prices = ( base_query .order_by(Price.date.desc()) .offset(skip) .limit(limit) .all() ) return { "items": [PriceItem.model_validate(p) for p in prices], "total": total, "skip": skip, "limit": limit, } @router.get("/etfs") async def list_etfs( current_user: CurrentUser, db: Session = Depends(get_db), page: int = Query(1, ge=1), size: int = Query(50, ge=1, le=200), search: Optional[str] = None, ): """List collected ETFs with pagination and search.""" query = db.query(ETF) if search: query = query.filter( or_(ETF.ticker.contains(search), ETF.name.contains(search)) ) total = query.count() items = query.order_by(ETF.ticker).offset((page - 1) * size).limit(size).all() return { "items": [ETFItem.model_validate(e) for e in items], "total": total, "page": page, "size": size, } @router.get("/etfs/{ticker}/prices") async def get_etf_prices( ticker: str, current_user: CurrentUser, db: Session = Depends(get_db), skip: int = Query(0, ge=0), limit: int = Query(365, ge=1, le=3000), ): """Get daily prices for an ETF with pagination.""" base_query = db.query(ETFPrice).filter(ETFPrice.ticker == ticker) total = base_query.count() prices = ( base_query .order_by(ETFPrice.date.desc()) .offset(skip) .limit(limit) .all() ) return { "items": [ETFPriceItem.model_validate(p) for p in prices], "total": total, "skip": skip, "limit": limit, } @router.get("/sectors") async def list_sectors( current_user: CurrentUser, db: Session = Depends(get_db), page: int = Query(1, ge=1), size: int = Query(50, ge=1, le=200), search: Optional[str] = None, ): """List sector classification data.""" query = db.query(Sector) if search: query = query.filter( or_(Sector.ticker.contains(search), Sector.company_name.contains(search), Sector.sector_name.contains(search)) ) total = query.count() items = query.order_by(Sector.ticker).offset((page - 1) * size).limit(size).all() return { "items": [SectorItem.model_validate(s) for s in items], "total": total, "page": page, "size": size, } @router.get("/valuations") async def list_valuations( current_user: CurrentUser, db: Session = Depends(get_db), page: int = Query(1, ge=1), size: int = Query(50, ge=1, le=200), search: Optional[str] = None, ): """List valuation metrics data.""" query = db.query(Valuation) if search: query = query.filter(Valuation.ticker.contains(search)) total = query.count() items = ( query.order_by(Valuation.ticker, Valuation.base_date.desc()) .offset((page - 1) * size) .limit(size) .all() ) return { "items": [ValuationItem.model_validate(v) for v in items], "total": total, "page": page, "size": size, }