From a9e0730e4b9a52c7f3ba26529d4481f5bd25b2f5 Mon Sep 17 00:00:00 2001 From: nyxst4ck <289980115+nyxst4ck@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:59:10 -0300 Subject: [PATCH] BUG: Preserve stop-loss value in stats._trades when SL is gapped through When a stop-loss order triggers, its stop price is cleared as the stop becomes a market order. The value was only restored on stats._trades["SL"] when the fill price equalled the stop price exactly. If the bar gapped past the stop (fill price worse than the stop), the SL column was left empty (NaN). Restore the recorded SL whenever the executed order is the trade's own stop-loss order, regardless of the fill price. Adds a regression test. Fixes #1340 --- backtesting/backtesting.py | 5 ++++- backtesting/test/_test.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) 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)