"""Routes for quantitative analysis: risk metrics, CAPM, normality, unit root.""" import functools import logging from collections.abc import Awaitable, Callable from typing import ParamSpec, TypeVar from fastapi import APIRouter, HTTPException, Path, Query from models import SYMBOL_PATTERN, ApiResponse import quantitative_service logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1") P = ParamSpec("P") R = TypeVar("R") def _validate_symbol(symbol: str) -> str: if not SYMBOL_PATTERN.match(symbol): raise HTTPException(status_code=400, detail="Invalid symbol format") return symbol.upper() def _safe(fn: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]: @functools.wraps(fn) async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: try: return await fn(*args, **kwargs) except HTTPException: raise except Exception: logger.exception("Upstream data error") raise HTTPException( status_code=502, detail="Data provider error. Check server logs.", ) return wrapper # type: ignore[return-value] @router.get("/stock/{symbol}/performance", response_model=ApiResponse) @_safe async def stock_performance( symbol: str = Path(..., min_length=1, max_length=20), days: int = Query(default=365, ge=30, le=3650), ): """Performance metrics: Sharpe, Sortino, max drawdown, volatility.""" symbol = _validate_symbol(symbol) data = await quantitative_service.get_performance_metrics(symbol, days=days) return ApiResponse(data=data) @router.get("/stock/{symbol}/capm", response_model=ApiResponse) @_safe async def stock_capm(symbol: str = Path(..., min_length=1, max_length=20)): """CAPM: beta, alpha, systematic and idiosyncratic risk.""" symbol = _validate_symbol(symbol) data = await quantitative_service.get_capm(symbol) return ApiResponse(data=data) @router.get("/stock/{symbol}/normality", response_model=ApiResponse) @_safe async def stock_normality( symbol: str = Path(..., min_length=1, max_length=20), days: int = Query(default=365, ge=30, le=3650), ): """Normality tests: Jarque-Bera, Shapiro-Wilk on returns.""" symbol = _validate_symbol(symbol) data = await quantitative_service.get_normality_test(symbol, days=days) return ApiResponse(data=data) @router.get("/stock/{symbol}/unitroot", response_model=ApiResponse) @_safe async def stock_unitroot( symbol: str = Path(..., min_length=1, max_length=20), days: int = Query(default=365, ge=30, le=3650), ): """Unit root tests: ADF, KPSS for stationarity.""" symbol = _validate_symbol(symbol) data = await quantitative_service.get_unitroot_test(symbol, days=days) return ApiResponse(data=data)