"""Performance analysis and reporting utilities.""" from __future__ import annotations from typing import List import numpy as np import pandas as pd from backtest.backtester import BacktestResult, BacktestTrade def compute_sharpe_ratio( equity_curve: List[float], risk_free_rate: float = 0.0, periods: int = 252 ) -> float: """Compute annualised Sharpe ratio from an equity curve.""" if len(equity_curve) < 2: return 0.0 returns = pd.Series(equity_curve).pct_change().dropna() excess = returns - risk_free_rate / periods if excess.std() == 0: return 0.0 return float((excess.mean() / excess.std()) * np.sqrt(periods)) def compute_sortino_ratio( equity_curve: List[float], risk_free_rate: float = 0.0, periods: int = 252 ) -> float: """Compute annualised Sortino ratio (downside deviation only).""" if len(equity_curve) < 2: return 0.0 returns = pd.Series(equity_curve).pct_change().dropna() excess = returns - risk_free_rate / periods downside = excess[excess < 0] if len(downside) == 0 or downside.std() == 0: return float("inf") if excess.mean() > 0 else 0.0 return float((excess.mean() / downside.std()) * np.sqrt(periods)) def compute_max_consecutive_losses(trades: List[BacktestTrade]) -> int: """Return the longest streak of consecutive losing trades.""" max_streak = 0 current = 0 for t in trades: if t.pnl <= 0: current += 1 max_streak = max(max_streak, current) else: current = 0 return max_streak def compute_calmar_ratio( total_return: float, max_drawdown: float, years: float = 1.0 ) -> float: """Calmar ratio = annualised return / max drawdown.""" if max_drawdown == 0: return float("inf") if total_return > 0 else 0.0 annual_return = total_return / years return annual_return / max_drawdown def trade_analysis(trades: List[BacktestTrade]) -> dict: """Detailed trade-level statistics.""" if not trades: return {} pnls = [t.pnl for t in trades] wins = [p for p in pnls if p > 0] losses = [p for p in pnls if p <= 0] durations = [t.exit_index - t.entry_index for t in trades] return { "total_trades": len(trades), "avg_pnl": np.mean(pnls), "median_pnl": np.median(pnls), "std_pnl": np.std(pnls), "best_trade": max(pnls), "worst_trade": min(pnls), "avg_win": np.mean(wins) if wins else 0, "avg_loss": np.mean(losses) if losses else 0, "max_consecutive_losses": compute_max_consecutive_losses(trades), "avg_duration_candles": np.mean(durations), "total_fees": sum(t.fee for t in trades), }