diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index 9ed77d6b..377657c5 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -916,8 +916,11 @@ def _process_orders(self): if trade in self.trades: self._reduce_trade(trade, price, size, time_index) assert order.size != -_prev_size or trade not in self.trades - if price == stop_price: + if order is trade._sl_order: # Set SL back on the order for stats._trades["SL"] + # (it was cleared above when the stop was hit). Restore it + # even when the SL was gapped through and the fill price is + # worse than the stop price (i.e. `price != stop_price`). trade._sl_order._replace(stop_price=stop_price) if order in (trade._sl_order, trade._tp_order): diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index 63045ce1..cc85bf50 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -1177,3 +1177,21 @@ def next(self): trades = Backtest(SHORT_DATA, S).run()._trades self.assertEqual(trades['SL'].fillna(0).tolist(), [0, 99]) self.assertEqual(trades['TP'].fillna(0).tolist(), [111, 0]) + + def test_sl_value_in_trades_df_when_gapped_through(self): + # An SL that is gapped through (the bar opens beyond the stop, so the + # fill price is worse than the stop price) must still be recorded in + # stats._trades["SL"]. See GH issue #1340. + class S(_S): + def next(self): + if len(self.data.index) == 9: + self.buy(size=1, sl=99.5) + + trades = Backtest(SHORT_DATA, S).run()._trades + self.assertEqual(len(trades), 1) + trade = trades.iloc[0] + # The long SL (99.5) is gapped through on the next bar's open (99.19), + # so the exit price is below the stop price ... + self.assertLess(trade['ExitPrice'], 99.5) + # ... yet the SL value must be preserved in the trades data frame. + self.assertEqual(trade['SL'], 99.5)