Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/backtesting/performance/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions tests/backtesting/test_performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down