Skip to content

Commit 2b89c4a

Browse files
committed
BUG: Fix annualized stats with weekly/monthly data
Fixes #537
1 parent a08eb0f commit 2b89c4a

File tree

2 files changed

+15
-5
lines changed

2 files changed

+15
-5
lines changed

backtesting/_stats.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, List, Union
3+
from typing import TYPE_CHECKING, List, Union, cast
44

55
import numpy as np
66
import pandas as pd
@@ -119,11 +119,16 @@ def _round_timedelta(value, _period=_data_period(index)):
119119
annual_trading_days = np.nan
120120
is_datetime_index = isinstance(index, pd.DatetimeIndex)
121121
if is_datetime_index:
122-
day_returns = equity_df['Equity'].resample('D').last().dropna().pct_change()
122+
freq_days = cast(pd.Timedelta, _data_period(index)).days
123+
have_weekends = index.dayofweek.to_series().between(5, 6).mean() > 2 / 7 * .6
124+
annual_trading_days = (
125+
52 if freq_days == 7 else
126+
12 if freq_days == 31 else
127+
1 if freq_days == 365 else
128+
(365 if have_weekends else 252))
129+
freq = {7: 'W', 31: 'ME', 365: 'YE'}.get(freq_days, 'D')
130+
day_returns = equity_df['Equity'].resample(freq).last().dropna().pct_change()
123131
gmean_day_return = geometric_mean(day_returns)
124-
annual_trading_days = float(
125-
365 if index.dayofweek.to_series().between(5, 6).mean() > 2/7 * .6 else
126-
252)
127132

128133
# Annualized return and risk metrics are computed based on the (mostly correct)
129134
# assumption that the returns are compounded. See: https://dx.doi.org/10.2139/ssrn.3054517

backtesting/test/_test.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,11 @@ def next(self):
10351035
bt = Backtest(df, S, cash=100, trade_on_close=True)
10361036
self.assertEqual(bt.run()._trades['ExitPrice'][0], 50)
10371037

1038+
def test_stats_annualized(self):
1039+
stats = Backtest(GOOG.resample('W').agg(OHLCV_AGG), SmaCross).run()
1040+
self.assertFalse(np.isnan(stats['Return (Ann.) [%]']))
1041+
self.assertEqual(round(stats['Return (Ann.) [%]']), -3)
1042+
10381043

10391044
if __name__ == '__main__':
10401045
warnings.filterwarnings('error')

0 commit comments

Comments
 (0)