feat: add portfolio optimization and congress tracking (TDD)

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
This commit is contained in:
Yaojia Wang
2026-03-19 22:27:03 +01:00
parent 27b131492f
commit 42ba359c48
9 changed files with 1140 additions and 1 deletions

54
routes_portfolio.py Normal file
View File

@@ -0,0 +1,54 @@
"""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)