diff --git a/backtesting/lib.py b/backtesting/lib.py index fd05026a..0d34c748 100644 --- a/backtesting/lib.py +++ b/backtesting/lib.py @@ -470,7 +470,7 @@ def set_atr_periods(self, periods: int = 100): """ hi, lo, c_prev = self.data.High, self.data.Low, pd.Series(self.data.Close).shift(1) tr = np.max([hi - lo, (c_prev - hi).abs(), (c_prev - lo).abs()], axis=0) - atr = pd.Series(tr).rolling(periods).mean().bfill().values + atr = pd.Series(tr).rolling(periods).mean().values self.__atr = atr def set_trailing_sl(self, n_atr: float = 6): @@ -490,20 +490,25 @@ def set_trailing_pct(self, pct: float = .05): with `mean(Close * pct / atr)` and set with `set_trailing_sl`. """ assert 0 < pct < 1, 'Need pct= as rate, i.e. 5% == 0.05' - pct_in_atr = np.mean(self.data.Close * pct / self.__atr) # type: ignore + ratios = self.data.Close * pct / self.__atr # type: ignore + ratios = ratios[np.isfinite(ratios)] + pct_in_atr = np.mean(ratios) if len(ratios) else np.nan self.set_trailing_sl(pct_in_atr) def next(self): super().next() # Can't use index=-1 because self.__atr is not an Indicator type index = len(self.data) - 1 + atr = self.__atr[index] + if not np.isfinite(atr): + return for trade in self.trades: if trade.is_long: trade.sl = max(trade.sl or -np.inf, - self.data.Close[index] - self.__atr[index] * self.__n_atr) + self.data.Close[index] - atr * self.__n_atr) else: trade.sl = min(trade.sl or np.inf, - self.data.Close[index] + self.__atr[index] * self.__n_atr) + self.data.Close[index] + atr * self.__n_atr) class FractionalBacktest(Backtest): diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index 63045ce1..c20e8356 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -955,6 +955,34 @@ def next(self): stats = Backtest(GOOG, S).run() self.assertEqual(stats['# Trades'], 56) + def test_TrailingStrategy_does_not_backfill_atr(self): + class S(TrailingStrategy): + sl_by_bar = {} + + def init(self): + super().init() + self.set_atr_periods(3) + self.set_trailing_sl(1) + + def next(self): + super().next() + if not self.position: + self.buy() + if self.trades: + S.sl_by_bar[len(self.data) - 1] = self.trades[0].sl + + data = pd.DataFrame({ + 'Open': [10] * 8, + 'High': [12] * 8, + 'Low': [8] * 8, + 'Close': [10] * 8, + }, index=pd.date_range('2020', periods=8)) + + Backtest(data, S, cash=10_000, trade_on_close=True).run() + + self.assertIsNone(S.sl_by_bar[2]) + self.assertEqual(S.sl_by_bar[3], 6) + def test_FractionalBacktest(self): ubtc_bt = FractionalBacktest(BTCUSD['2015':], SmaCross, fractional_unit=1 / 1e6, cash=100) stats = ubtc_bt.run(fast=2, slow=3)