from __future__ import annotations from collections.abc import Sequence from app.schemas import BacktestMetricsOut, BacktestPointOut, BacktestTradeOut # Bars per year for annualising the Sharpe ratio. Unknown timeframes -> None. BARS_PER_YEAR: dict[str, float] = { "1m": 525_600, "3m": 175_200, "5m": 105_120, "15m": 35_040, "30m": 17_520, "1h": 8_760, "2h": 4_380, "4h": 2_190, "6h": 1_460, "12h": 730, "1d": 365, "3d": 121, "1w": 52, } def max_drawdown_pct(equities: Sequence[float]) -> float: peak = equities[0] if equities else 0.0 worst = 0.0 for value in equities: peak = max(peak, value) if peak > 0: worst = max(worst, (peak - value) / peak) return worst * 100 def sharpe_ratio(equities: Sequence[float], timeframe: str) -> float | None: if len(equities) < 3: return None returns = [ equities[i] / equities[i - 1] - 1 for i in range(1, len(equities)) if equities[i - 1] > 0 ] if len(returns) < 2: return None mean = sum(returns) / len(returns) variance = sum((r - mean) ** 2 for r in returns) / (len(returns) - 1) std = variance**0.5 periods = BARS_PER_YEAR.get(timeframe) if std == 0 or not periods: return None return mean / std * (periods**0.5) def compute_metrics( curve: Sequence[BacktestPointOut], trades: Sequence[BacktestTradeOut], starting_balance: float, timeframe: str, ) -> BacktestMetricsOut: equities = [point.equity for point in curve] or [starting_balance] final_equity = equities[-1] total_return = (final_equity / starting_balance - 1) * 100 if starting_balance else 0.0 buy_hold = ( (curve[-1].buy_hold / starting_balance - 1) * 100 if curve and starting_balance else 0.0 ) returns = [trade.return_pct for trade in trades] gross_profit = sum(trade.pnl for trade in trades if trade.pnl > 0) gross_loss = -sum(trade.pnl for trade in trades if trade.pnl < 0) wins = sum(1 for trade in trades if trade.pnl > 0) return BacktestMetricsOut( initial_equity=starting_balance, final_equity=final_equity, total_return_pct=total_return, buy_hold_return_pct=buy_hold, max_drawdown_pct=max_drawdown_pct(equities), num_trades=len(trades), win_rate_pct=(wins / len(trades) * 100) if trades else 0.0, profit_factor=(gross_profit / gross_loss) if gross_loss > 0 else None, avg_trade_pct=(sum(returns) / len(returns)) if returns else 0.0, best_trade_pct=max(returns) if returns else 0.0, worst_trade_pct=min(returns) if returns else 0.0, sharpe=sharpe_ratio(equities, timeframe), )