Portfolio optimization (3 endpoints): - POST /portfolio/optimize - HRP optimal weights via scipy clustering - POST /portfolio/correlation - pairwise correlation matrix - POST /portfolio/risk-parity - inverse-volatility risk parity weights Congress tracking (2 endpoints): - GET /regulators/congress/trades - congress member stock trades - GET /regulators/congress/bills?query= - search congress bills Implementation: - portfolio_service.py: HRP with scipy fallback to inverse-vol - congress_service.py: multi-provider fallback pattern - 51 new tests (14 portfolio unit, 20 portfolio route, 12 congress unit, 7 congress route) - All 312 tests passing
55 lines
1.8 KiB
Python
55 lines
1.8 KiB
Python
"""Routes for portfolio optimization (HRP, correlation, risk parity)."""
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
|
|
from models import ApiResponse
|
|
from route_utils import safe
|
|
import portfolio_service
|
|
|
|
router = APIRouter(prefix="/api/v1/portfolio")
|
|
|
|
|
|
class PortfolioOptimizeRequest(BaseModel):
|
|
symbols: list[str] = Field(..., min_length=1, max_length=50)
|
|
days: int = Field(default=365, ge=1, le=3650)
|
|
|
|
|
|
@router.post("/optimize", response_model=ApiResponse)
|
|
@safe
|
|
async def portfolio_optimize(request: PortfolioOptimizeRequest):
|
|
"""Compute HRP optimal weights for a list of symbols."""
|
|
try:
|
|
result = await portfolio_service.optimize_hrp(
|
|
request.symbols, days=request.days
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc))
|
|
return ApiResponse(data=result)
|
|
|
|
|
|
@router.post("/correlation", response_model=ApiResponse)
|
|
@safe
|
|
async def portfolio_correlation(request: PortfolioOptimizeRequest):
|
|
"""Compute correlation matrix for a list of symbols."""
|
|
try:
|
|
result = await portfolio_service.compute_correlation(
|
|
request.symbols, days=request.days
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc))
|
|
return ApiResponse(data=result)
|
|
|
|
|
|
@router.post("/risk-parity", response_model=ApiResponse)
|
|
@safe
|
|
async def portfolio_risk_parity(request: PortfolioOptimizeRequest):
|
|
"""Compute equal risk contribution weights for a list of symbols."""
|
|
try:
|
|
result = await portfolio_service.compute_risk_parity(
|
|
request.symbols, days=request.days
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc))
|
|
return ApiResponse(data=result)
|