"""Routes for ETF, index, crypto, currency, and derivatives data.""" 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 market_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] # --- ETF --- @router.get("/etf/{symbol}/info", response_model=ApiResponse) @_safe async def etf_info(symbol: str = Path(..., min_length=1, max_length=20)): """Get ETF profile and info.""" symbol = _validate_symbol(symbol) data = await market_service.get_etf_info(symbol) return ApiResponse(data=data) @router.get("/etf/{symbol}/historical", response_model=ApiResponse) @_safe async def etf_historical( symbol: str = Path(..., min_length=1, max_length=20), days: int = Query(default=365, ge=1, le=3650), ): """Get ETF price history.""" symbol = _validate_symbol(symbol) data = await market_service.get_etf_historical(symbol, days=days) return ApiResponse(data=data) @router.get("/etf/search", response_model=ApiResponse) @_safe async def etf_search(query: str = Query(..., min_length=1, max_length=100)): """Search for ETFs by name or keyword.""" data = await market_service.search_etf(query) return ApiResponse(data=data) # --- Index --- @router.get("/index/available", response_model=ApiResponse) @_safe async def index_available(): """List available market indices.""" data = await market_service.get_available_indices() return ApiResponse(data=data) @router.get("/index/{symbol}/historical", response_model=ApiResponse) @_safe async def index_historical( symbol: str = Path(..., min_length=1, max_length=20), days: int = Query(default=365, ge=1, le=3650), ): """Get index price history (e.g., ^GSPC, ^DJI, ^IXIC).""" symbol = _validate_symbol(symbol) data = await market_service.get_index_historical(symbol, days=days) return ApiResponse(data=data) # --- Crypto --- @router.get("/crypto/{symbol}/historical", response_model=ApiResponse) @_safe async def crypto_historical( symbol: str = Path(..., min_length=1, max_length=20), days: int = Query(default=365, ge=1, le=3650), ): """Get cryptocurrency price history (e.g., BTC-USD).""" symbol = _validate_symbol(symbol) data = await market_service.get_crypto_historical(symbol, days=days) return ApiResponse(data=data) @router.get("/crypto/search", response_model=ApiResponse) @_safe async def crypto_search(query: str = Query(..., min_length=1, max_length=100)): """Search for cryptocurrencies.""" data = await market_service.search_crypto(query) return ApiResponse(data=data) # --- Currency --- @router.get("/currency/{symbol}/historical", response_model=ApiResponse) @_safe async def currency_historical( symbol: str = Path(..., min_length=1, max_length=20), days: int = Query(default=365, ge=1, le=3650), ): """Get forex price history (e.g., EURUSD, USDSEK).""" symbol = _validate_symbol(symbol) data = await market_service.get_currency_historical(symbol, days=days) return ApiResponse(data=data) # --- Derivatives --- @router.get("/options/{symbol}/chains", response_model=ApiResponse) @_safe async def options_chains(symbol: str = Path(..., min_length=1, max_length=20)): """Get options chain data.""" symbol = _validate_symbol(symbol) data = await market_service.get_options_chains(symbol) return ApiResponse(data=data) @router.get("/futures/{symbol}/historical", response_model=ApiResponse) @_safe async def futures_historical( symbol: str = Path(..., min_length=1, max_length=20), days: int = Query(default=365, ge=1, le=3650), ): """Get futures price history.""" symbol = _validate_symbol(symbol) data = await market_service.get_futures_historical(symbol, days=days) return ApiResponse(data=data) @router.get("/futures/{symbol}/curve", response_model=ApiResponse) @_safe async def futures_curve(symbol: str = Path(..., min_length=1, max_length=20)): """Get futures term structure/curve.""" symbol = _validate_symbol(symbol) data = await market_service.get_futures_curve(symbol) return ApiResponse(data=data)