Add 3 new service layers and route modules: - quantitative_service: Sharpe ratio, CAPM, normality tests, unit root tests - calendar_service: earnings/dividends/IPO/splits calendars, estimates, SEC ownership - market_service: ETF, index, crypto, forex, options, futures data Total endpoints: 50. All use free providers (yfinance, SEC). Update README with comprehensive endpoint documentation.
167 lines
4.9 KiB
Python
167 lines
4.9 KiB
Python
"""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)
|