diff --git a/src/backtesting/performance/metrics.py b/src/backtesting/performance/metrics.py index 3b0cbcd..e8e047e 100644 --- a/src/backtesting/performance/metrics.py +++ b/src/backtesting/performance/metrics.py @@ -54,11 +54,16 @@ def calculate_all_metrics(self, # Calculate period length for annualization if len(equity_curve) > 1: if 'datetime' in equity_curve.columns: + # Real timestamps: elapsed calendar time over calendar days/yr. days = (equity_curve['datetime'].iloc[-1] - equity_curve['datetime'].iloc[0]).days + years = max(days / 365.25, 1 / self.trading_days) else: - days = len(equity_curve) # Assume daily data + # No timestamps: each row is a trading bar, so annualize by + # trading days per year, not calendar days. Dividing a + # trading-bar count by 365.25 over-annualizes the return. + bars = len(equity_curve) + years = max(bars / self.trading_days, 1 / self.trading_days) - years = max(days / 365.25, 1/self.trading_days) # Minimum one trading day metrics['annualized_return'] = (1 + metrics['total_return']) ** (1/years) - 1 else: metrics['annualized_return'] = 0.0 diff --git a/tests/backtesting/test_performance.py b/tests/backtesting/test_performance.py index fd108f7..e0ee171 100644 --- a/tests/backtesting/test_performance.py +++ b/tests/backtesting/test_performance.py @@ -72,6 +72,17 @@ def test_sharpe_ratio_calculation(self): assert metrics['sharpe_ratio'] > 10, "Sharpe ratio should be high for consistent returns" assert metrics['annualized_return'] > 10, "Annualized return should be very high" + def test_annualized_return_without_datetime_uses_trading_days(self): + """Equity curves without a datetime column are trading bars and must be + annualized by trading_days, not calendar days (issue #12).""" + # 252 trading bars (one trading year), +20% total -> ~20% annualized. + equity_curve = pd.DataFrame({'total': np.linspace(100000, 120000, 252)}) + + metrics = self.calculator.calculate_all_metrics(equity_curve, pd.DataFrame()) + + # Old code divided 252 bars by 365.25, over-annualizing to ~30%. + assert abs(metrics['annualized_return'] - 0.20) < 0.01 + def test_max_drawdown_calculation(self): """Test maximum drawdown calculation""" # Create equity curve with known drawdown