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

140
routes_calendar.py Normal file
View File

@@ -0,0 +1,140 @@
"""Routes for calendar events, screening, ownership, and estimates."""
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 calendar_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]
# --- Calendar Events ---
@router.get("/calendar/earnings", response_model=ApiResponse)
@_safe
async def earnings_calendar(
start_date: str | None = Query(default=None, description="YYYY-MM-DD"),
end_date: str | None = Query(default=None, description="YYYY-MM-DD"),
):
"""Get upcoming earnings announcements."""
data = await calendar_service.get_earnings_calendar(start_date, end_date)
return ApiResponse(data=data)
@router.get("/calendar/dividends", response_model=ApiResponse)
@_safe
async def dividend_calendar(
start_date: str | None = Query(default=None, description="YYYY-MM-DD"),
end_date: str | None = Query(default=None, description="YYYY-MM-DD"),
):
"""Get upcoming dividend dates."""
data = await calendar_service.get_dividend_calendar(start_date, end_date)
return ApiResponse(data=data)
@router.get("/calendar/ipo", response_model=ApiResponse)
@_safe
async def ipo_calendar(
start_date: str | None = Query(default=None, description="YYYY-MM-DD"),
end_date: str | None = Query(default=None, description="YYYY-MM-DD"),
):
"""Get upcoming IPOs."""
data = await calendar_service.get_ipo_calendar(start_date, end_date)
return ApiResponse(data=data)
@router.get("/calendar/splits", response_model=ApiResponse)
@_safe
async def splits_calendar(
start_date: str | None = Query(default=None, description="YYYY-MM-DD"),
end_date: str | None = Query(default=None, description="YYYY-MM-DD"),
):
"""Get upcoming stock splits."""
data = await calendar_service.get_splits_calendar(start_date, end_date)
return ApiResponse(data=data)
# --- Analyst Estimates ---
@router.get("/stock/{symbol}/estimates", response_model=ApiResponse)
@_safe
async def stock_estimates(symbol: str = Path(..., min_length=1, max_length=20)):
"""Get analyst consensus estimates."""
symbol = _validate_symbol(symbol)
data = await calendar_service.get_analyst_estimates(symbol)
return ApiResponse(data=data)
@router.get("/stock/{symbol}/share-statistics", response_model=ApiResponse)
@_safe
async def stock_share_stats(symbol: str = Path(..., min_length=1, max_length=20)):
"""Get share statistics: float, outstanding, short interest."""
symbol = _validate_symbol(symbol)
data = await calendar_service.get_share_statistics(symbol)
return ApiResponse(data=data)
# --- Ownership (SEC, free) ---
@router.get("/stock/{symbol}/sec-insider", response_model=ApiResponse)
@_safe
async def stock_sec_insider(symbol: str = Path(..., min_length=1, max_length=20)):
"""Get insider trading data from SEC (Form 4)."""
symbol = _validate_symbol(symbol)
data = await calendar_service.get_insider_trading(symbol)
return ApiResponse(data=data)
@router.get("/stock/{symbol}/institutional", response_model=ApiResponse)
@_safe
async def stock_institutional(symbol: str = Path(..., min_length=1, max_length=20)):
"""Get institutional holders from SEC 13F filings."""
symbol = _validate_symbol(symbol)
data = await calendar_service.get_institutional_holders(symbol)
return ApiResponse(data=data)
# --- Screener ---
@router.get("/screener", response_model=ApiResponse)
@_safe
async def stock_screener():
"""Screen stocks using available filters."""
data = await calendar_service.screen_stocks()
return ApiResponse(data=data)