From 615f17a3bbaad12800ac5338a371499f8283d65f Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Thu, 19 Mar 2026 17:31:08 +0100 Subject: [PATCH] feat: add remaining 5 endpoints (VWAP, relative rotation, fred-regional, primary dealer) Complete all 67 planned endpoints: - VWAP and Relative Rotation technical indicators - FRED regional data (by state/county/MSA) - Primary dealer positioning (Fed data) --- economy_service.py | 29 +++++++++++++++++++++++++++++ routes_economy.py | 19 +++++++++++++++++++ routes_technical.py | 22 ++++++++++++++++++++++ technical_service.py | 41 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) diff --git a/economy_service.py b/economy_service.py index 39c84c9..e198c2a 100644 --- a/economy_service.py +++ b/economy_service.py @@ -141,6 +141,35 @@ async def get_central_bank_holdings() -> list[dict[str, Any]]: return [] +async def get_fred_regional( + series_id: str, region: str | None = None, +) -> list[dict[str, Any]]: + """Get geographically disaggregated FRED data (by state, county, MSA).""" + try: + kwargs: dict[str, Any] = {"symbol": series_id, "provider": "fred"} + if region: + kwargs["region_type"] = region + result = await asyncio.to_thread( + obb.economy.fred_regional, **kwargs + ) + return to_list(result) + except Exception: + logger.warning("FRED regional failed for %s", series_id, exc_info=True) + return [] + + +async def get_primary_dealer_positioning() -> list[dict[str, Any]]: + """Get primary dealer net positions in treasuries, MBS, corporate bonds.""" + try: + result = await asyncio.to_thread( + obb.economy.primary_dealer_positioning, provider="federal_reserve" + ) + return to_list(result) + except Exception: + logger.warning("Primary dealer positioning failed", exc_info=True) + return [] + + async def get_fomc_documents(year: int | None = None) -> list[dict[str, Any]]: """Get FOMC meeting documents (minutes, projections, etc.).""" try: diff --git a/routes_economy.py b/routes_economy.py index eb0ef3d..ad3fa31 100644 --- a/routes_economy.py +++ b/routes_economy.py @@ -75,6 +75,25 @@ async def macro_hpi(country: str = Query(default="united_states", max_length=50, # --- Economy data endpoints --- +@router.get("/economy/fred-regional", response_model=ApiResponse) +@safe +async def economy_fred_regional( + series_id: str = Query(..., min_length=1, max_length=30), + region: str = Query(default=None, max_length=20, pattern=r"^[a-z_]+$"), +): + """Regional FRED data by state, county, or MSA.""" + data = await economy_service.get_fred_regional(series_id=series_id, region=region) + return ApiResponse(data=data) + + +@router.get("/economy/primary-dealer-positioning", response_model=ApiResponse) +@safe +async def economy_primary_dealer(): + """Primary dealer net positions: treasuries, MBS, corporate bonds.""" + data = await economy_service.get_primary_dealer_positioning() + return ApiResponse(data=data) + + @router.get("/economy/fred-search", response_model=ApiResponse) @safe async def economy_fred_search(query: str = Query(..., min_length=1, max_length=100)): diff --git a/routes_technical.py b/routes_technical.py index 49d7522..0e0120c 100644 --- a/routes_technical.py +++ b/routes_technical.py @@ -153,3 +153,25 @@ async def stock_cones(symbol: str = Path(..., min_length=1, max_length=20)): symbol = validate_symbol(symbol) data = await technical_service.get_cones(symbol) return ApiResponse(data=data) + + +@router.get("/stock/{symbol}/technical/vwap", response_model=ApiResponse) +@safe +async def stock_vwap(symbol: str = Path(..., min_length=1, max_length=20)): + """Volume Weighted Average Price -- intraday fair value benchmark.""" + symbol = validate_symbol(symbol) + data = await technical_service.get_vwap(symbol) + return ApiResponse(data=data) + + +@router.get("/stock/{symbol}/technical/relative-rotation", response_model=ApiResponse) +@safe +async def stock_relative_rotation( + symbol: str = Path(..., min_length=1, max_length=20), + benchmark: str = Query(default="SPY", min_length=1, max_length=20), +): + """Relative Rotation -- strength vs benchmark (sector rotation analysis).""" + symbol = validate_symbol(symbol) + benchmark = validate_symbol(benchmark) + data = await technical_service.get_relative_rotation(symbol, benchmark=benchmark) + return ApiResponse(data=data) diff --git a/technical_service.py b/technical_service.py index 0bd8f02..ee15246 100644 --- a/technical_service.py +++ b/technical_service.py @@ -382,6 +382,47 @@ async def get_ad(symbol: str, days: int = 400) -> dict[str, Any]: return {"symbol": symbol, "error": "Failed to compute A/D Line"} +async def get_vwap(symbol: str, days: int = 5) -> dict[str, Any]: + """Volume Weighted Average Price -- intraday fair value benchmark.""" + hist = await fetch_historical(symbol, days) + if hist is None: + return {"symbol": symbol, "error": "No historical data"} + try: + result = await asyncio.to_thread(obb.technical.vwap, data=hist.results) + latest = _extract_latest(result) + return { + "symbol": symbol, + "vwap": latest.get("VWAP_D"), + } + except Exception: + logger.warning("VWAP failed for %s", symbol, exc_info=True) + return {"symbol": symbol, "error": "Failed to compute VWAP"} + + +async def get_relative_rotation( + symbols: str, benchmark: str = "SPY", days: int = 365, +) -> dict[str, Any]: + """Relative Rotation -- strength ratio and momentum vs benchmark.""" + hist = await fetch_historical(symbols, days) + bench_hist = await fetch_historical(benchmark, days) + if hist is None or bench_hist is None: + return {"symbols": symbols, "benchmark": benchmark, "error": "No historical data"} + try: + result = await asyncio.to_thread( + obb.technical.relative_rotation, + data=hist.results, benchmark=bench_hist.results, + ) + items = to_list(result) + return { + "symbols": symbols, + "benchmark": benchmark, + "data": items[-10:] if len(items) > 10 else items, + } + except Exception: + logger.warning("Relative rotation failed for %s", symbols, exc_info=True) + return {"symbols": symbols, "error": "Failed to compute relative rotation"} + + async def get_cones(symbol: str, days: int = 365) -> dict[str, Any]: """Volatility Cones -- realized volatility quantiles for options analysis.""" hist = await fetch_historical(symbol, days)