Skip to content

Commit 8b88e81

Browse files
committed
BUG: Buy&Hold duration should match trading duration
Fixes #327
1 parent d323fa6 commit 8b88e81

File tree

4 files changed

+21
-9
lines changed

4 files changed

+21
-9
lines changed

backtesting/_stats.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import numpy as np
66
import pandas as pd
77

8-
from ._util import _data_period
8+
from ._util import _data_period, _indicator_warmup_nbars
99

1010
if TYPE_CHECKING:
1111
from .backtesting import Strategy, Trade
@@ -111,8 +111,9 @@ def _round_timedelta(value, _period=_data_period(index)):
111111
if commissions:
112112
s.loc['Commissions [$]'] = commissions
113113
s.loc['Return [%]'] = (equity[-1] - equity[0]) / equity[0] * 100
114+
first_trading_bar = _indicator_warmup_nbars(strategy_instance)
114115
c = ohlc_data.Close.values
115-
s.loc['Buy & Hold Return [%]'] = (c[-1] - c[0]) / c[0] * 100 # long-only return
116+
s.loc['Buy & Hold Return [%]'] = (c[-1] - c[first_trading_bar]) / c[first_trading_bar] * 100 # long-only return
116117

117118
gmean_day_return: float = 0
118119
day_returns = np.array(np.nan)

backtesting/_util.py

+14
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ def _data_period(index) -> Union[pd.Timedelta, Number]:
4242
return values.diff().dropna().median()
4343

4444

45+
def _strategy_indicators(strategy):
46+
return {attr: indicator
47+
for attr, indicator in strategy.__dict__.items()
48+
if isinstance(indicator, _Indicator)}.items()
49+
50+
51+
def _indicator_warmup_nbars(strategy):
52+
if strategy is None:
53+
return 0
54+
nbars = max((np.isnan(indicator.astype(float)).argmin(axis=-1).max()
55+
for _, indicator in _strategy_indicators(strategy)), default=0)
56+
return nbars
57+
58+
4559
class _Array(np.ndarray):
4660
"""
4761
ndarray extended to supply .name and other arbitrary properties

backtesting/backtesting.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def _tqdm(seq, **_):
3434

3535
from ._plotting import plot # noqa: I001
3636
from ._stats import compute_stats
37-
from ._util import _as_str, _Indicator, _Data, try_
37+
from ._util import _as_str, _Indicator, _Data, _indicator_warmup_nbars, _strategy_indicators, try_
3838

3939
__pdoc__ = {
4040
'Strategy.__init__': False,
@@ -1290,14 +1290,11 @@ def run(self, **kwargs) -> pd.Series:
12901290
data._update() # Strategy.init might have changed/added to data.df
12911291

12921292
# Indicators used in Strategy.next()
1293-
indicator_attrs = {attr: indicator
1294-
for attr, indicator in strategy.__dict__.items()
1295-
if isinstance(indicator, _Indicator)}.items()
1293+
indicator_attrs = _strategy_indicators(strategy)
12961294

12971295
# Skip first few candles where indicators are still "warming up"
12981296
# +1 to have at least two entries available
1299-
start = 1 + max((np.isnan(indicator.astype(float)).argmin(axis=-1).max()
1300-
for _, indicator in indicator_attrs), default=0)
1297+
start = 1 + _indicator_warmup_nbars(strategy)
13011298

13021299
# Disable "invalid value encountered in ..." warnings. Comparison
13031300
# np.nan >= 3 is not invalid; it's False.

backtesting/test/_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def test_compute_stats(self):
291291
'Avg. Trade Duration': pd.Timedelta('46 days 00:00:00'),
292292
'Avg. Trade [%]': 2.531715975158555,
293293
'Best Trade [%]': 53.59595229490424,
294-
'Buy & Hold Return [%]': 703.4582419772772,
294+
'Buy & Hold Return [%]': 522.0601851851852,
295295
'Calmar Ratio': 0.4414380935608377,
296296
'Duration': pd.Timedelta('3116 days 00:00:00'),
297297
'End': pd.Timestamp('2013-03-01 00:00:00'),

0 commit comments

Comments
 (0)