From 8e03fee8685ccbb8098cabb81bc622498e8dbf53 Mon Sep 17 00:00:00 2001 From: radwayne <73605415+radwayne@users.noreply.github.com> Date: Fri, 6 Nov 2020 13:56:46 +0100 Subject: [PATCH 1/6] Update interface.py Changed The should_sell() method, to handle the case where both ROI and trailing stoploss are reached in backtest. --- freqtrade/strategy/interface.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1c6aa535d..44a281ebe 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -475,16 +475,27 @@ class IStrategy(ABC): stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, 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', {}) + + roi_reached = self.min_roi_reached(trade=trade, current_profit=current_profit, + current_time=date) + + if stoplossflag.sell_flag: + + # When backtesting, in the case of trailing_stop_loss, + # make sure we don't make a profit higher than ROI. + if stoplossflag.sell_type == SellType.TRAILING_STOP_LOSS and roi_reached: + 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) + + logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " + f"sell_type={stoplossflag.sell_type}") + return stoplossflag if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False): # This one is noisy, commented out @@ -492,7 +503,7 @@ class IStrategy(ABC): return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) # 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 roi_reached: 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) From 81d08c4deff25a9bd8b24e6d6847bdc04ccdace0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 08:16:11 +0100 Subject: [PATCH 2/6] Add detailed backtest test verifying the ROI / trailing stop collision --- freqtrade/strategy/interface.py | 12 +++--- tests/optimize/test_backtest_detail.py | 55 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 44a281ebe..172264b10 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -475,24 +475,24 @@ class IStrategy(ABC): stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, force_stoploss=force_stoploss, high=high) - + # 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', {}) - + roi_reached = self.min_roi_reached(trade=trade, current_profit=current_profit, - current_time=date) - + current_time=date) + if stoplossflag.sell_flag: - + # When backtesting, in the case of trailing_stop_loss, # make sure we don't make a profit higher than ROI. if stoplossflag.sell_type == SellType.TRAILING_STOP_LOSS and roi_reached: 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) - + logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " f"sell_type={stoplossflag.sell_type}") return stoplossflag diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index a5de64fe4..f3a2d8b96 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -328,6 +328,58 @@ tc20 = BTContainer(data=[ 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)] +) + TESTS = [ tc0, @@ -351,6 +403,9 @@ TESTS = [ tc18, tc19, tc20, + tc21, + tc22, + tc23, ] From 57461a59f3b416add4e2a5169d89b81cd4c12ea8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 08:30:17 +0100 Subject: [PATCH 3/6] Update backtesting documentation with new logic --- docs/backtesting.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backtesting.md b/docs/backtesting.md index 84911568b..2121e3126 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -278,6 +278,7 @@ Since backtesting lacks some detailed information about what happens within a ca - Trailing stoploss - High happens first - adjusting stoploss - Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly) + - ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies - Sell-reason does not explain if a trade was positive or negative, just what triggered the sell (this can look odd if negative ROI values are used) - Stoploss (and trailing stoploss) is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` and/or `trailing_stop` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes. From 4aa6ebee049a924b5fd5c931f14360af917eb3e4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 09:17:25 +0100 Subject: [PATCH 4/6] Add more tests for #2422 --- tests/optimize/test_backtest_detail.py | 64 ++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index f3a2d8b96..720ed8c13 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -380,6 +380,66 @@ tc23 = BTContainer(data=[ 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, @@ -406,6 +466,10 @@ TESTS = [ tc21, tc22, tc23, + tc24, + tc25, + tc26, + tc27, ] From fefb4b23d0603683800790de198f1894143449a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 09:18:03 +0100 Subject: [PATCH 5/6] revise logic in should_sell --- freqtrade/strategy/interface.py | 53 ++++++++++++++------------------- tests/test_freqtradebot.py | 2 +- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 172264b10..81f4e7651 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -481,46 +481,39 @@ class IStrategy(ABC): current_profit = trade.calc_profit_ratio(current_rate) config_ask_strategy = self.config.get('ask_strategy', {}) - roi_reached = self.min_roi_reached(trade=trade, current_profit=current_profit, - current_time=date) + # 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)) - if stoplossflag.sell_flag: + 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 - # When backtesting, in the case of trailing_stop_loss, - # make sure we don't make a profit higher than ROI. - if stoplossflag.sell_type == SellType.TRAILING_STOP_LOSS and roi_reached: - 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) - - logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " - f"sell_type={stoplossflag.sell_type}") - return stoplossflag - - 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) - - # Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee) - if roi_reached: + # 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) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 1f5b3ecaa..64dfb016e 100644 --- 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): From c69ce28b76449a1785cc41bc1491d66c766e8f9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 09:26:58 +0100 Subject: [PATCH 6/6] Update backtest assumption documentation --- docs/backtesting.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 2121e3126..953198ddd 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -268,19 +268,24 @@ It contains some useful key metrics about performance of your strategy on backte Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions: - Buys happen at open-price -- Sell signal sells happen at open-price of the following candle -- Low happens before high for stoploss, protecting capital first +- Sell-signal sells happen at open-price of the consecutive candle +- Sell-signal is favored over Stoploss, because sell-signals are assumed to trigger on candle's open - ROI - sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%) - sells are never "below the candle", so a ROI of 2% may result in a sell at 2.4% if low was at 2.4% profit - Forcesells caused by `=-1` ROI entries use low as sell value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) - Stoploss sells happen exactly at stoploss price, even if low was lower +- Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes +- Low happens before high for stoploss, protecting capital first - Trailing stoploss - High happens first - adjusting stoploss - Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly) - ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies - Sell-reason does not explain if a trade was positive or negative, just what triggered the sell (this can look odd if negative ROI values are used) -- Stoploss (and trailing stoploss) is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` and/or `trailing_stop` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes. +- Evaluation sequence (if multiple signals happen on the same candle) + - ROI (if not stoploss) + - Sell-signal + - Stoploss Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode. Also, keep in mind that past results don't guarantee future success.