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.
This commit is contained in:
@@ -1,14 +1,11 @@
|
||||
"""Routes for backtesting strategies."""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
import backtest_service
|
||||
from models import ApiResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from route_utils import safe
|
||||
|
||||
router = APIRouter(prefix="/api/v1/backtest", tags=["backtest"])
|
||||
|
||||
@@ -54,88 +51,56 @@ class MomentumRequest(BaseModel):
|
||||
|
||||
|
||||
@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."""
|
||||
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
|
||||
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."""
|
||||
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
|
||||
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."""
|
||||
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
|
||||
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 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
|
||||
"""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)
|
||||
|
||||
Reference in New Issue
Block a user