"""Routes for backtesting strategies.""" import logging from fastapi import APIRouter, HTTPException from pydantic import BaseModel, Field import backtest_service from models import ApiResponse logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1/backtest", tags=["backtest"]) # --------------------------------------------------------------------------- # Request models # --------------------------------------------------------------------------- class BacktestRequest(BaseModel): symbol: str = Field(..., min_length=1, max_length=20) days: int = Field(default=365, ge=30, le=3650) initial_capital: float = Field(default=10000.0, gt=0, le=1_000_000_000) class SMARequest(BacktestRequest): short_window: int = Field(default=20, ge=5, le=100) long_window: int = Field(default=50, ge=10, le=400) class RSIRequest(BacktestRequest): period: int = Field(default=14, ge=2, le=50) oversold: float = Field(default=30.0, ge=1, le=49) overbought: float = Field(default=70.0, ge=51, le=99) class BuyAndHoldRequest(BacktestRequest): pass class MomentumRequest(BaseModel): symbols: list[str] = Field(..., min_length=2, max_length=20) lookback: int = Field(default=60, ge=5, le=252) top_n: int = Field(default=2, ge=1) rebalance_days: int = Field(default=30, ge=5, le=252) days: int = Field(default=365, ge=60, le=3650) initial_capital: float = Field(default=10000.0, gt=0) # --------------------------------------------------------------------------- # Route handlers # --------------------------------------------------------------------------- @router.post("/sma-crossover", response_model=ApiResponse) async def sma_crossover(req: SMARequest) -> ApiResponse: """SMA crossover strategy: buy when short SMA crosses above long SMA.""" try: result = await backtest_service.backtest_sma_crossover( req.symbol, short_window=req.short_window, long_window=req.long_window, days=req.days, initial_capital=req.initial_capital, ) return ApiResponse(data=result) except ValueError as exc: logger.warning("SMA crossover backtest validation error: %s", exc) raise HTTPException(status_code=400, detail=str(exc)) from exc except Exception as exc: logger.exception("SMA crossover backtest failed") raise HTTPException( status_code=502, detail="Data provider error. Check server logs." ) from exc @router.post("/rsi", response_model=ApiResponse) async def rsi_strategy(req: RSIRequest) -> ApiResponse: """RSI strategy: buy when RSI < oversold, sell when RSI > overbought.""" try: result = await backtest_service.backtest_rsi( req.symbol, period=req.period, oversold=req.oversold, overbought=req.overbought, days=req.days, initial_capital=req.initial_capital, ) return ApiResponse(data=result) except ValueError as exc: logger.warning("RSI backtest validation error: %s", exc) raise HTTPException(status_code=400, detail=str(exc)) from exc except Exception as exc: logger.exception("RSI backtest failed") raise HTTPException( status_code=502, detail="Data provider error. Check server logs." ) from exc @router.post("/buy-and-hold", response_model=ApiResponse) async def buy_and_hold(req: BuyAndHoldRequest) -> ApiResponse: """Buy-and-hold benchmark: buy on day 1, hold through end of period.""" try: result = await backtest_service.backtest_buy_and_hold( req.symbol, days=req.days, initial_capital=req.initial_capital, ) return ApiResponse(data=result) except ValueError as exc: logger.warning("Buy-and-hold backtest validation error: %s", exc) raise HTTPException(status_code=400, detail=str(exc)) from exc except Exception as exc: logger.exception("Buy-and-hold backtest failed") raise HTTPException( status_code=502, detail="Data provider error. Check server logs." ) from exc @router.post("/momentum", response_model=ApiResponse) async def momentum_strategy(req: MomentumRequest) -> ApiResponse: """Momentum strategy: every rebalance_days pick top_n symbols by lookback return.""" try: result = await backtest_service.backtest_momentum( symbols=req.symbols, lookback=req.lookback, top_n=req.top_n, rebalance_days=req.rebalance_days, days=req.days, initial_capital=req.initial_capital, ) return ApiResponse(data=result) except ValueError as exc: logger.warning("Momentum backtest validation error: %s", exc) raise HTTPException(status_code=400, detail=str(exc)) from exc except Exception as exc: logger.exception("Momentum backtest failed") raise HTTPException( status_code=502, detail="Data provider error. Check server logs." ) from exc