"""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) class ClusterRequest(BaseModel): symbols: list[str] = Field(..., min_length=3, max_length=50) days: int = Field(default=180, ge=30, le=3650) n_clusters: int | None = Field(default=None, ge=2, le=20) class SimilarRequest(BaseModel): symbol: str = Field(..., min_length=1, max_length=20) universe: list[str] = Field(..., min_length=2, max_length=50) days: int = Field(default=180, ge=30, le=3650) top_n: int = Field(default=5, ge=1, le=20) @router.post("/cluster", response_model=ApiResponse) @safe async def portfolio_cluster(request: ClusterRequest): """Cluster stocks by return similarity using t-SNE + KMeans.""" try: result = await portfolio_service.cluster_stocks( request.symbols, days=request.days, n_clusters=request.n_clusters ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) return ApiResponse(data=result) @router.post("/similar", response_model=ApiResponse) @safe async def portfolio_similar(request: SimilarRequest): """Find stocks most/least similar to a target by return correlation.""" try: result = await portfolio_service.find_similar_stocks( request.symbol, request.universe, days=request.days, top_n=request.top_n, ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) return ApiResponse(data=result)