Repository: freqtrade/freqtrade

Issue:
Backtest sells via ROI instead of sell-signal in case of simultaneous occurrence
<!-- 
Have you searched for similar issues before posting it?

If you have discovered a bug in the bot, please [search the issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue). 
If it hasn't been reported, please create a new issue.

Please do not use bug reports to request new features.
-->

## Describe your environment

  * Operating system: macOS 11.6.2
  * Python Version: 3.7.9
  * CCXT version: 1.61.92
  * Freqtrade Version: 2021.11
  
Note: All issues other than enhancement requests will be closed without further comment if the above template is deleted or not filled out.

## Describe the problem:

*Explain the problem you have encountered*

Backtest sells on the simultaneous occurrence of a sell-signal and a ROI signal via the ROI signal. 

### Steps to reproduce:

  1. Run an arbitrary backtest that produces a sell-signal and triggers the ROI at the same time.
  2. Plotting helps to visualise the issue.
  
### Observed Results:

  * What happened?
  Backtest trade sold by ROI instead of the sell signal.
![Ohne Titel 2](https://user-images.githubusercontent.com/96880297/147769843-fc743c0e-86ae-4359-b5ca-7a57d98b878a.jpg)
  * What did you expect to happen?
  Backtest trade should be sold at the 'open' price, as the process time is ideally very small and this value is closer to reality; the ROI execution takes place intra-candle.

### Relevant code

https://github.com/freqtrade/freqtrade/blob/bd98637ae934db16968d921f560e9a05a04b1549/freqtrade/strategy/interface.py#L708-L721

Thanks for your support!


Discussion:
This is actually intended behavior, and documented in the [backtesting assumptions](https://www.freqtrade.io/en/stable/backtesting/#assumptions-made-by-backtesting) (last point).

I don't see it as a bug for the moment (it's documented, therefore expected behavior).

Now it's obviously arguable which sell-reason should be preferred (you're probably right that changing the sequence might be better - there's also a TODO a few lines above the linked relevant code) - but the change is non-trivial (not from a code perspective) - and can potentially have quite an impact on strategy results (depending on how the strategy works).


Thank you very much for your quick reply. I have seen the post in the documentation and thought that was taken accordingly. However, since it is consistent, it is of course not a bug in the proper sense, you are right.

In live I have always seen significant deviations from the backtest, in my specific case the reverse order is much closer to live. What was the reason to define exactly this order at that time? And in which cases would the reverse implementation be disadvantageous for a strategies (long process times,...)?

Probably addressing the TODO would be the right approach, using an option flag would satisfy both cases.
I've added the TODO as part of a refactor that brought the whole logic to the form (or a similar format, anyway) than it is now - keeping the original logic for this.

I'm not entirely sure why the logic was the way it was - but doing a refactor AND reversing some logics at the same time is rarely a good idea.

As said - it's potentially the right approach (hence the TODO) - but it's not a change i'd want to take lightly - without considering potential downsides (if there are any).


Resolving patch:
diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -703,23 +703,21 @@ def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
                             custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH]
                     else:
                         custom_reason = None
-            # TODO: return here if sell-signal should be favored over ROI
+            if sell_signal in (SellType.CUSTOM_SELL, SellType.SELL_SIGNAL):
+                logger.debug(f"{trade.pair} - Sell signal received. "
+                             f"sell_type=SellType.{sell_signal.name}" +
+                             (f", custom_reason={custom_reason}" if custom_reason else ""))
+                return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason)
 
         # Start evaluations
         # Sequence:
-        # ROI (if not stoploss)
         # Sell-signal
+        # ROI (if not stoploss)
         # Stoploss
         if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS:
             logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI")
             return SellCheckTuple(sell_type=SellType.ROI)
 
-        if sell_signal != SellType.NONE:
-            logger.debug(f"{trade.pair} - Sell signal received. "
-                         f"sell_type=SellType.{sell_signal.name}" +
-                         (f", custom_reason={custom_reason}" if custom_reason else ""))
-            return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason)
-
         if stoplossflag.sell_flag:
 
             logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.sell_type}")


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
@@ -426,8 +426,6 @@
 
 # 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],
@@ -436,8 +434,8 @@
     [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, 4950, 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)]
+    stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.002, use_sell_signal=True,
+    trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)]
 )
 
 # Test 28: trailing_stop should raise so candle 3 causes a stoploss