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)
This commit is contained in:
@@ -141,6 +141,35 @@ async def get_central_bank_holdings() -> list[dict[str, Any]]:
|
|||||||
return []
|
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]]:
|
async def get_fomc_documents(year: int | None = None) -> list[dict[str, Any]]:
|
||||||
"""Get FOMC meeting documents (minutes, projections, etc.)."""
|
"""Get FOMC meeting documents (minutes, projections, etc.)."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -75,6 +75,25 @@ async def macro_hpi(country: str = Query(default="united_states", max_length=50,
|
|||||||
# --- Economy data endpoints ---
|
# --- 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)
|
@router.get("/economy/fred-search", response_model=ApiResponse)
|
||||||
@safe
|
@safe
|
||||||
async def economy_fred_search(query: str = Query(..., min_length=1, max_length=100)):
|
async def economy_fred_search(query: str = Query(..., min_length=1, max_length=100)):
|
||||||
|
|||||||
@@ -153,3 +153,25 @@ async def stock_cones(symbol: str = Path(..., min_length=1, max_length=20)):
|
|||||||
symbol = validate_symbol(symbol)
|
symbol = validate_symbol(symbol)
|
||||||
data = await technical_service.get_cones(symbol)
|
data = await technical_service.get_cones(symbol)
|
||||||
return ApiResponse(data=data)
|
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)
|
||||||
|
|||||||
@@ -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"}
|
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]:
|
async def get_cones(symbol: str, days: int = 365) -> dict[str, Any]:
|
||||||
"""Volatility Cones -- realized volatility quantiles for options analysis."""
|
"""Volatility Cones -- realized volatility quantiles for options analysis."""
|
||||||
hist = await fetch_historical(symbol, days)
|
hist = await fetch_historical(symbol, days)
|
||||||
|
|||||||
Reference in New Issue
Block a user