feat: integrate quantitative, calendar, market data endpoints

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.
This commit is contained in:
Yaojia Wang
2026-03-09 10:28:33 +01:00
parent 00f2cb5e74
commit 507194397e
8 changed files with 980 additions and 18 deletions

166
routes_market.py Normal file
View File

@@ -0,0 +1,166 @@
"""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)