diff --git a/backtesting/lib.py b/backtesting/lib.py index f7f61e74..6f73e894 100644 --- a/backtesting/lib.py +++ b/backtesting/lib.py @@ -452,6 +452,43 @@ def next(self): self.data.Close[index] + self.__atr[index] * self.__n_atr) +class PercentageTrailingStrategy(Strategy): + """ + A strategy with automatic trailing stop-loss, trailing the current + price at distance of some percentage. Call + `PercentageTrailingStrategy.set_trailing_sl()` to set said percentage + (`5` by default). See [tutorials] for usage examples. + + [tutorials]: index.html#tutorials + + Remember to call `super().init()` and `super().next()` in your + overridden methods. + """ + _sl_pct = 5. + + def init(self): + super().init() + + def set_trailing_sl(self, percentage: float = 5): + assert percentage > 0, "percentage must be greater than 0" + """ + Sets the future trailing stop-loss as some (`percentage`) + percentage away from the current price. + """ + self._sl_pct = percentage/100 + + def next(self): + super().next() + index = len(self.data)-1 + for trade in self.trades: + if trade.is_long: + trade.sl = max(trade.sl or -np.inf, + self.data.Close[index]*(1-self._sl_pct)) + else: + trade.sl = min(trade.sl or np.inf, + self.data.Close[index]*(1+self._sl_pct)) + + # Prevent pdoc3 documenting __init__ signature of Strategy subclasses for cls in list(globals().values()): if isinstance(cls, type) and issubclass(cls, Strategy): diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index 85ecea6a..e4cc8d05 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -24,6 +24,7 @@ quantile, SignalStrategy, TrailingStrategy, + PercentageTrailingStrategy, resample_apply, plot_heatmaps, random_ohlc_data, @@ -862,6 +863,21 @@ def next(self): stats = Backtest(GOOG, S).run() self.assertEqual(stats['# Trades'], 57) + def test_PercentageTrailingStrategy(self): + class S(PercentageTrailingStrategy): + def init(self): + super().init() + self.set_trailing_sl(5) + self.sma = self.I(lambda: self.data.Close.s.rolling(10).mean()) + + def next(self): + super().next() + if not self.position and self.data.Close > self.sma: + self.buy() + + stats = Backtest(GOOG, S).run() + self.assertEqual(stats['# Trades'], 91) + class TestUtil(TestCase): def test_as_str(self):