diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bf70093..6bcfe46f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ These were the major changes contributing to each release: ### 0.x.x +* `Strategy.I()` gained a `warmup=True` parameter. Setting it `False` excludes + that indicator's leading NaN values from the automatic warm-up bar count that + delays the start of the backtest (#1339) + ### 0.6.6 (2026-07-22) diff --git a/backtesting/_util.py b/backtesting/_util.py index 408d764f..9812cd3d 100644 --- a/backtesting/_util.py +++ b/backtesting/_util.py @@ -89,7 +89,8 @@ def _indicator_warmup_nbars(strategy): return 0 nbars = max((np.isnan(indicator.astype(float)).argmin(axis=-1).max() for _, indicator in _strategy_indicators(strategy) - if not indicator._opts['scatter']), default=0) + if not indicator._opts['scatter'] and indicator._opts.get('warmup', True)), + default=0) return nbars diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index d356b211..4f36a242 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -77,6 +77,7 @@ def _check_params(self, params): def I(self, # noqa: E743 func: Callable, *args, name=None, plot=True, overlay=None, color=None, scatter=False, + warmup=True, **kwargs) -> np.ndarray: """ Declare an indicator. An indicator is just an array of values @@ -109,6 +110,13 @@ def I(self, # noqa: E743 If `scatter` is `True`, the plotted indicator marker will be a circle instead of a connected line segment (default). + If `warmup` is `False`, this indicator's leading NaN values are + _not_ counted towards the number of warm-up bars before which + the backtest doesn't begin trading (see warning below). Set this + for indicators that are informational/secondary only (e.g. computed + on a higher, resampled time frame with `backtesting.lib.resample_apply`) + and aren't essential to have available from the very first bar. + Additional `*args` and `**kwargs` are passed to `func` and can be used for parameters. @@ -175,7 +183,7 @@ def _format_name(name: str) -> str: overlay = ((x < 1.4) & (x > .6)).mean() > .6 value = _Indicator(value, name=name, plot=plot, overlay=overlay, - color=color, scatter=scatter, + color=color, scatter=scatter, warmup=warmup, # _Indicator.s Series accessor uses this: index=self.data.index) self._indicators.append(value) diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index d74fde9f..b788ec7d 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -591,6 +591,20 @@ def coroutine(self): stats = self._Backtest(coroutine).run() self.assertEqual(list(stats._trades.Tag), [1, 1, 2]) + def test_indicator_warmup_false_excluded_from_start_bar(self): + class S(Strategy): + def init(self): + self.sma_fast = self.I(SMA, self.data.Close, 5) + self.sma_slow = self.I(SMA, self.data.Close, 100, warmup=False) + self.first_next_bar = None + + def next(self): + if self.first_next_bar is None: + self.first_next_bar = len(self.data) - 1 + + stats = Backtest(GOOG, S).run() + self.assertEqual(stats._strategy.first_next_bar, 5) + class TestOptimize(TestCase): def test_optimize(self):