""" 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