Repository: freqtrade/freqtrade

Issue:
backtesting and sell signal / stoploss priority
When there is a sell signal, and a stoploss trigger on the same candle, stoploss takes over sell signal. Is this intended?
- It makes sense for timeframes <15m
- not so much for timeframes >30m
There are edge cases where if you use order book, sell is obviously not guaranteed, so it is conservative to prioritize stoploss, but generally market selling with >30m timeframes should always favour sell signal over stoploss.


Discussion:
as explained [here](https://www.freqtrade.io/en/latest/backtesting/#assumptions-made-by-backtesting), backtesting has to make a few assumptions, since it does not see "intra-candle".

One of these assumptions is that stoploss is first, protecting your assets.
in real trading, sell-signal may be first, and stoploss can happen 15 minutes later (assuming 30m candles), but we don't know that for sure, and therefore have to make this assumption.
This obviously becomes worse for longer-time candles - as we have a longer period of "no data".

Now this will have backtesting results lower than what you could archive in live trading - but backtesting aims to be "more conservative" than real trading - so it does not promise profits you cannot reach in real trading.
I'm intentionally saying it aims to be - since this may not always be true, depending on the strategy - but the aim is still there.

hope this makes sense?
The list does not specify that sell signals happen after stoplosses I think. 
Also I agree that backtesting should be conservative. However stoplesses also make sell signals harder to debug.
well yes ... you're right, that's missing from the list.

Thinking about it - you may have a point ... (will need some further investigation...)

assuming 1h candles:
the sell-signal triggers at the candle 8:00 (goes from 8:00-9:00) - for backtesting, this means it'll sell at 9:00 (open - next candle) it'll mean the same for live trading, btw.

Now when does your stoploss trigger? 
at the 9:00 candle, or at the 8:00 candle?
If it's the 9:00 candle, then in live-trading, sell-signal would probably win (real trading would sell at 9:00:05 or so) - stoploss could occur a lot later.

will need further investigation i think



Resolving patch:
diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -476,40 +476,44 @@ def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
                                               current_time=date, current_profit=current_profit,
                                               force_stoploss=force_stoploss, high=high)
 
-        if stoplossflag.sell_flag:
-            logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, "
-                         f"sell_type={stoplossflag.sell_type}")
-            return stoplossflag
-
         # Set current rate to high for backtesting sell
         current_rate = high or rate
         current_profit = trade.calc_profit_ratio(current_rate)
         config_ask_strategy = self.config.get('ask_strategy', {})
 
-        if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False):
-            # This one is noisy, commented out
-            # logger.debug(f"{trade.pair} - Buy signal still active. sell_flag=False")
-            return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
+        # if buy signal and ignore_roi is set, we don't need to evaluate min_roi.
+        roi_reached = (not (buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False))
+                       and self.min_roi_reached(trade=trade, current_profit=current_profit,
+                                                current_time=date))
 
-        # Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
-        if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date):
+        if config_ask_strategy.get('sell_profit_only', False) and trade.calc_profit(rate=rate) <= 0:
+            # Negative profits and sell_profit_only - ignore sell signal
+            sell_signal = False
+        else:
+            sell_signal = sell and not buy and config_ask_strategy.get('use_sell_signal', True)
+            # TODO: return here if sell-signal should be favored over ROI
+
+        # Start evaluations
+        # Sequence:
+        # ROI (if not stoploss)
+        # Sell-signal
+        # Stoploss
+        if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS:
             logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, "
                          f"sell_type=SellType.ROI")
             return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)
 
-        if config_ask_strategy.get('sell_profit_only', False):
-            # This one is noisy, commented out
-            # logger.debug(f"{trade.pair} - Checking if trade is profitable...")
-            if trade.calc_profit(rate=rate) <= 0:
-                # This one is noisy, commented out
-                # logger.debug(f"{trade.pair} - Trade is not profitable. sell_flag=False")
-                return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
-
-        if sell and not buy and config_ask_strategy.get('use_sell_signal', True):
+        if sell_signal:
             logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, "
                          f"sell_type=SellType.SELL_SIGNAL")
             return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)
 
+        if stoplossflag.sell_flag:
+
+            logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, "
+                         f"sell_type={stoplossflag.sell_type}")
+            return stoplossflag
+
         # This one is noisy, commented out...
         # logger.debug(f"{trade.pair} - No sell signal. sell_flag=False")
         return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)


Test patch:
diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py
--- a/tests/optimize/test_backtest_detail.py
+++ b/tests/optimize/test_backtest_detail.py
@@ -328,6 +328,118 @@
     trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)]
 )
 
+# Test 21: trailing_stop ROI collision.
+# Roi should trigger before Trailing stop - otherwise Trailing stop profits can be > ROI
+# which cannot happen in reality
+# stop-loss: 10%, ROI: 4%, Trailing stop adjusted at the sell candle
+tc21 = BTContainer(data=[
+    # D   O     H     L     C    V    B  S
+    [0, 5000, 5050, 4950, 5000, 6172, 1, 0],
+    [1, 5000, 5050, 4950, 5100, 6172, 0, 0],
+    [2, 5100, 5251, 4650, 5100, 6172, 0, 0],
+    [3, 4850, 5050, 4650, 4750, 6172, 0, 0],
+    [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
+    stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True,
+    trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
+    trailing_stop_positive=0.03,
+    trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)]
+)
+
+# Test 22: trailing_stop Raises in candle 2 - but ROI applies at the same time.
+# applying a positive trailing stop of 3% - ROI should apply before trailing stop.
+# stop-loss: 10%, ROI: 4%, stoploss adjusted candle 2
+tc22 = BTContainer(data=[
+    # D   O     H     L     C    V    B  S
+    [0, 5000, 5050, 4950, 5000, 6172, 1, 0],
+    [1, 5000, 5050, 4950, 5100, 6172, 0, 0],
+    [2, 5100, 5251, 5100, 5100, 6172, 0, 0],
+    [3, 4850, 5050, 4650, 4750, 6172, 0, 0],
+    [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
+    stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True,
+    trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
+    trailing_stop_positive=0.03,
+    trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)]
+)
+
+# Test 23: trailing_stop Raises in candle 2 (does not trigger)
+# applying a positive trailing stop of 3% since stop_positive_offset is reached.
+# ROI is changed after this to 4%, dropping ROI below trailing_stop_positive, causing a sell
+# in the candle after the raised stoploss candle with ROI reason.
+# Stoploss would trigger in this candle too, but it's no longer relevant.
+# stop-loss: 10%, ROI: 4%, stoploss adjusted candle 2, ROI adjusted in candle 3 (causing the sell)
+tc23 = BTContainer(data=[
+    # D   O     H     L     C    V    B  S
+    [0, 5000, 5050, 4950, 5000, 6172, 1, 0],
+    [1, 5000, 5050, 4950, 5100, 6172, 0, 0],
+    [2, 5100, 5251, 5100, 5100, 6172, 0, 0],
+    [3, 4850, 5251, 4650, 4750, 6172, 0, 0],
+    [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
+    stop_loss=-0.10, roi={"0": 0.1, "119": 0.03}, profit_perc=0.03, trailing_stop=True,
+    trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
+    trailing_stop_positive=0.03,
+    trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)]
+)
+
+# Test 24: Sell with signal sell in candle 3 (stoploss also triggers on this candle)
+# Stoploss at 1%.
+# Stoploss wins over Sell-signal (because sell-signal is acted on in the next candle)
+tc24 = BTContainer(data=[
+    # D  O     H     L     C     V    B  S
+    [0, 5000, 5025, 4975, 4987, 6172, 1, 0],
+    [1, 5000, 5025, 4975, 4987, 6172, 0, 0],  # enter trade (signal on last candle)
+    [2, 4987, 5012, 4986, 4600, 6172, 0, 0],
+    [3, 5010, 5000, 4855, 5010, 6172, 0, 1],  # Triggers stoploss + sellsignal
+    [4, 5010, 4987, 4977, 4995, 6172, 0, 0],
+    [5, 4995, 4995, 4995, 4950, 6172, 0, 0]],
+    stop_loss=-0.01, roi={"0": 1}, profit_perc=-0.01, use_sell_signal=True,
+    trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=3)]
+)
+
+# Test 25: Sell with signal sell in candle 3 (stoploss also triggers on this candle)
+# Stoploss at 1%.
+# Sell-signal wins over stoploss
+tc25 = BTContainer(data=[
+    # D  O     H     L     C     V    B  S
+    [0, 5000, 5025, 4975, 4987, 6172, 1, 0],
+    [1, 5000, 5025, 4975, 4987, 6172, 0, 0],  # enter trade (signal on last candle)
+    [2, 4987, 5012, 4986, 4600, 6172, 0, 0],
+    [3, 5010, 5000, 4986, 5010, 6172, 0, 1],
+    [4, 5010, 4987, 4855, 4995, 6172, 0, 0],  # Triggers stoploss + sellsignal acted on
+    [5, 4995, 4995, 4995, 4950, 6172, 0, 0]],
+    stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_sell_signal=True,
+    trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)]
+)
+
+# Test 26: Sell with signal sell in candle 3 (ROI at signal candle)
+# Stoploss at 10% (irrelevant), ROI at 5% (will trigger)
+# Sell-signal wins over stoploss
+tc26 = BTContainer(data=[
+    # D  O     H     L     C     V    B  S
+    [0, 5000, 5025, 4975, 4987, 6172, 1, 0],
+    [1, 5000, 5025, 4975, 4987, 6172, 0, 0],  # enter trade (signal on last candle)
+    [2, 4987, 5012, 4986, 4600, 6172, 0, 0],
+    [3, 5010, 5251, 4986, 5010, 6172, 0, 1],  # Triggers ROI, sell-signal
+    [4, 5010, 4987, 4855, 4995, 6172, 0, 0],
+    [5, 4995, 4995, 4995, 4950, 6172, 0, 0]],
+    stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_sell_signal=True,
+    trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)]
+)
+
+# Test 27: Sell with signal sell in candle 3 (ROI at signal candle)
+# Stoploss at 10% (irrelevant), ROI at 5% (will trigger) - Wins over Sell-signal
+# TODO: figure out if sell-signal should win over ROI
+# Sell-signal wins over stoploss
+tc27 = BTContainer(data=[
+    # D  O     H     L     C     V    B  S
+    [0, 5000, 5025, 4975, 4987, 6172, 1, 0],
+    [1, 5000, 5025, 4975, 4987, 6172, 0, 0],  # enter trade (signal on last candle)
+    [2, 4987, 5012, 4986, 4600, 6172, 0, 0],
+    [3, 5010, 5012, 4986, 5010, 6172, 0, 1],  # sell-signal
+    [4, 5010, 5251, 4855, 4995, 6172, 0, 0],  # Triggers ROI, sell-signal acted on
+    [5, 4995, 4995, 4995, 4950, 6172, 0, 0]],
+    stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_sell_signal=True,
+    trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=4)]
+)
 
 TESTS = [
     tc0,
@@ -351,6 +463,13 @@
     tc18,
     tc19,
     tc20,
+    tc21,
+    tc22,
+    tc23,
+    tc24,
+    tc25,
+    tc26,
+    tc27,
 ]
 
 
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -3556,7 +3556,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_b
     # Test if buy-signal is absent
     patch_get_signal(freqtrade, value=(False, True))
     assert freqtrade.handle_trade(trade) is True
-    assert trade.sell_reason == SellType.STOP_LOSS.value
+    assert trade.sell_reason == SellType.SELL_SIGNAL.value
 
 
 def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, fee, caplog, mocker):