quanthedge / backend /app /routers /backtests.py
jashdoshi77's picture
QuantHedge: Full deployment with Docker + nginx + uvicorn
9d29748
Raw
History Blame Contribute Delete
6.93 kB
"""
Backtesting & Analytics Router.
Run backtests, list results, and cross-strategy analytics.
"""
from __future__ import annotations
import json
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.dependencies import get_current_user
from app.models.strategy import BacktestResult, Strategy
from app.models.user import User
from app.schemas.backtest import BacktestRequest, StrategyAnalyticsResponse
from app.services.analytics.engine import analytics_engine
from app.services.backtest.engine import backtest_engine
router = APIRouter(tags=["Backtesting & Analytics"])
@router.post("/backtests/run")
async def run_backtest(
request: BacktestRequest,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Run a backtest for a strategy."""
# Fetch strategy
result = await db.execute(
select(Strategy).where(Strategy.id == request.strategy_id, Strategy.user_id == user.id)
)
strategy = result.scalar_one_or_none()
if not strategy:
raise HTTPException(status_code=404, detail="Strategy not found")
config = json.loads(strategy.config_json)
# Run backtest
bt_result = await backtest_engine.run_backtest(
strategy_config=config,
start_date=request.start_date,
end_date=request.end_date,
initial_capital=request.initial_capital,
commission_pct=request.commission_pct,
slippage_pct=request.slippage_pct,
benchmark_ticker=request.benchmark_ticker,
rebalance_frequency=config.get("rebalance_frequency", "monthly"),
)
if bt_result["status"] == "failed":
raise HTTPException(status_code=400, detail=bt_result.get("error", "Backtest failed"))
# Save result to DB
metrics = bt_result.get("metrics", {})
db_result = BacktestResult(
strategy_id=strategy.id,
user_id=user.id,
name=f"{strategy.name} Backtest",
start_date=request.start_date,
end_date=request.end_date,
initial_capital=request.initial_capital,
final_value=bt_result.get("final_value"),
total_return=metrics.get("total_return"),
annualized_return=metrics.get("annualized_return"),
sharpe_ratio=metrics.get("sharpe_ratio"),
sortino_ratio=metrics.get("sortino_ratio"),
max_drawdown=metrics.get("max_drawdown"),
volatility=metrics.get("volatility"),
calmar_ratio=metrics.get("calmar_ratio"),
win_rate=metrics.get("win_rate"),
profit_factor=metrics.get("profit_factor"),
total_trades=metrics.get("total_trades"),
avg_trade_return=metrics.get("avg_trade_return"),
total_commission=metrics.get("total_commission"),
total_slippage=metrics.get("total_slippage"),
equity_curve_json=json.dumps(bt_result.get("equity_curve", [])),
trades_json=json.dumps(bt_result.get("trades", [])),
metrics_json=json.dumps(metrics),
monthly_returns_json=json.dumps(bt_result.get("monthly_returns", {})),
status="completed",
)
db.add(db_result)
await db.flush()
await db.refresh(db_result)
return {
"id": db_result.id,
"strategy_id": strategy.id,
"strategy_name": strategy.name,
**bt_result,
}
@router.get("/backtests/")
async def list_backtests(
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""List all backtest results for the current user."""
result = await db.execute(
select(BacktestResult)
.where(BacktestResult.user_id == user.id)
.order_by(desc(BacktestResult.created_at))
)
results = result.scalars().all()
return {
"backtests": [
{
"id": r.id,
"strategy_id": r.strategy_id,
"name": r.name,
"start_date": r.start_date.isoformat(),
"end_date": r.end_date.isoformat(),
"total_return": r.total_return,
"sharpe_ratio": r.sharpe_ratio,
"max_drawdown": r.max_drawdown,
"status": r.status,
"created_at": r.created_at.isoformat() if r.created_at else None,
}
for r in results
],
"total": len(results),
}
@router.get("/backtests/{backtest_id}")
async def get_backtest(
backtest_id: int,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get detailed backtest result."""
result = await db.execute(
select(BacktestResult).where(
BacktestResult.id == backtest_id, BacktestResult.user_id == user.id
)
)
bt = result.scalar_one_or_none()
if not bt:
raise HTTPException(status_code=404, detail="Backtest not found")
return {
"id": bt.id,
"strategy_id": bt.strategy_id,
"name": bt.name,
"start_date": bt.start_date.isoformat(),
"end_date": bt.end_date.isoformat(),
"initial_capital": bt.initial_capital,
"final_value": bt.final_value,
"status": bt.status,
"metrics": json.loads(bt.metrics_json) if bt.metrics_json else {},
"equity_curve": json.loads(bt.equity_curve_json) if bt.equity_curve_json else [],
"trades": json.loads(bt.trades_json) if bt.trades_json else [],
"monthly_returns": json.loads(bt.monthly_returns_json) if bt.monthly_returns_json else {},
}
# ── Cross-Strategy Analytics ────────────────────────────────────────────
@router.post("/analytics/compare")
async def compare_strategies(
strategy_ids: list[int],
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Compare multiple strategies based on their backtest results."""
backtest_results = []
for sid in strategy_ids:
result = await db.execute(
select(BacktestResult)
.where(BacktestResult.strategy_id == sid, BacktestResult.user_id == user.id)
.order_by(desc(BacktestResult.created_at))
)
bt = result.scalar_one_or_none()
if bt:
strategy_result = await db.execute(select(Strategy).where(Strategy.id == sid))
strategy = strategy_result.scalar_one_or_none()
backtest_results.append({
"strategy_id": sid,
"strategy_name": strategy.name if strategy else f"Strategy {sid}",
"metrics": json.loads(bt.metrics_json) if bt.metrics_json else {},
"equity_curve": json.loads(bt.equity_curve_json) if bt.equity_curve_json else [],
})
analytics = analytics_engine.compare_strategies(backtest_results)
return analytics