Files
openbb-invest-api/routes_backtest.py
Yaojia Wang 0f7341b158 refactor: address architect review findings (6 items)
R1: Extend @safe to catch ValueError->400, simplify routes_backtest
    (eliminated 4 copies of duplicated try/except)
R2: Consolidate PROVIDER constant into obb_utils.py (single source)
R3: Add days_ago() helper to obb_utils.py, replace 8+ duplications
R4: Extract Reddit/ApeWisdom into reddit_service.py from finnhub_service
R5: Fix missing top-level import asyncio in finnhub_service
R6: (deferred - sentiment logic extraction is a larger change)

All 561 tests passing.
2026-03-19 23:15:00 +01:00

107 lines
3.3 KiB
Python

"""Routes for backtesting strategies."""
from fastapi import APIRouter
from pydantic import BaseModel, Field
import backtest_service
from models import ApiResponse
from route_utils import safe
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)
@safe
async def sma_crossover(req: SMARequest) -> ApiResponse:
"""SMA crossover strategy: buy when short SMA crosses above long SMA."""
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)
@router.post("/rsi", response_model=ApiResponse)
@safe
async def rsi_strategy(req: RSIRequest) -> ApiResponse:
"""RSI strategy: buy when RSI < oversold, sell when RSI > overbought."""
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)
@router.post("/buy-and-hold", response_model=ApiResponse)
@safe
async def buy_and_hold(req: BuyAndHoldRequest) -> ApiResponse:
"""Buy-and-hold benchmark: buy on day 1, hold through end of period."""
result = await backtest_service.backtest_buy_and_hold(
req.symbol,
days=req.days,
initial_capital=req.initial_capital,
)
return ApiResponse(data=result)
@router.post("/momentum", response_model=ApiResponse)
@safe
async def momentum_strategy(req: MomentumRequest) -> ApiResponse:
"""Momentum strategy: every rebalance_days pick top_n by lookback return."""
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)