Spaces:
Sleeping
Sleeping
| """ | |
| 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"]) | |
| 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, | |
| } | |
| 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), | |
| } | |
| 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 ββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 | |