Files
openbb-invest-api/routes_calendar.py
Yaojia Wang 507194397e 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.
2026-03-09 10:28:33 +01:00

141 lines
4.4 KiB
Python

"""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)