diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index cf499d4e2..6c5d098dc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -429,7 +429,7 @@ class Backtesting: # - (Expected abs profit - open_rate - open_fee) / (fee_close -1) roi_rate = trade.open_rate * roi / leverage open_fee_rate = side_1 * trade.open_rate * (1 + side_1 * trade.fee_open) - close_rate = -side_1 * (roi_rate + open_fee_rate) / (trade.fee_close - side_1 * 1) + close_rate = -(roi_rate + open_fee_rate) / (trade.fee_close - side_1 * 1) if is_short: is_new_roi = sell_row[OPEN_IDX] < close_rate else: @@ -542,7 +542,10 @@ class Backtesting: proposed_rate=closerate, current_profit=current_profit) # We can't place orders lower than current low. # freqtrade does not support this in live, and the order would fill immediately - closerate = max(closerate, sell_row[LOW_IDX]) + if trade.is_short: + closerate = min(closerate, sell_row[HIGH_IDX]) + else: + closerate = max(closerate, sell_row[LOW_IDX]) # Confirm trade exit: time_in_force = self.strategy.order_time_in_force['exit'] diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 975e9d41f..3fddc98df 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -869,7 +869,7 @@ class IStrategy(ABC, HyperStrategyMixin): force_stoploss=force_stoploss, low=low, high=high) # Set current rate to high for backtesting sell - current_rate = high or rate + current_rate = (low if trade.is_short else high) or rate current_profit = trade.calc_profit_ratio(current_rate) # if enter signal and ignore_roi is set, we don't need to evaluate min_roi. @@ -961,9 +961,9 @@ class IStrategy(ABC, HyperStrategyMixin): else: logger.warning("CustomStoploss function did not return valid stoploss") - sl_lower_long = (trade.stop_loss < (low or current_rate) and not trade.is_short) - sl_higher_short = (trade.stop_loss > (high or current_rate) and trade.is_short) - if self.trailing_stop and (sl_lower_long or sl_higher_short): + sl_lower_short = (trade.stop_loss < (low or current_rate) and not trade.is_short) + sl_higher_long = (trade.stop_loss > (high or current_rate) and trade.is_short) + if self.trailing_stop and (sl_lower_short or sl_higher_long): # trailing stoploss handling sl_offset = self.trailing_stop_positive_offset @@ -981,12 +981,12 @@ class IStrategy(ABC, HyperStrategyMixin): trade.adjust_stop_loss(bound or current_rate, stop_loss_value) - sl_higher_short = (trade.stop_loss >= (low or current_rate) and not trade.is_short) - sl_lower_long = ((trade.stop_loss <= (high or current_rate) and trade.is_short)) + sl_higher_long = (trade.stop_loss >= (low or current_rate) and not trade.is_short) + sl_lower_short = (trade.stop_loss <= (high or current_rate) and trade.is_short) # evaluate if the stoploss was hit if stoploss is not on exchange # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to # regular stoploss handling. - if ((sl_higher_short or sl_lower_long) and + if ((sl_higher_long or sl_lower_short) and (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): sell_type = SellType.STOP_LOSS diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index 29b7fd210..43ad6ecd8 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -19,6 +19,7 @@ class BTrade(NamedTuple): open_tick: int close_tick: int enter_tag: Optional[str] = None + is_short: bool = False class BTContainer(NamedTuple): diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index a8260af49..7ede1adc3 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -361,6 +361,23 @@ tc22 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] ) + +# Test 22s: 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 +tc22s = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0], + [1, 5000, 5050, 4900, 4900, 6172, 0, 0, 0, 0], + [2, 4900, 4900, 4749, 4900, 6172, 0, 0, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0, 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, is_short=True)] +) + # 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 @@ -410,6 +427,39 @@ tc25 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] ) +# Test 25l: (copy of test25 with leverage) +# Sell with signal sell in candle 3 (stoploss also triggers on this candle) +# Stoploss at 1%. +# Sell-signal wins over stoploss +tc25l = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [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, 4986, 6172, 0, 0], + [3, 5010, 5010, 4986, 5010, 6172, 0, 1], + [4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on + [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], + stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True, + leverage=5.0, + trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] +) + +# Test 25s: (copy of test25 with leverage and as short) +# Sell with signal sell in candle 3 (stoploss also triggers on this candle) +# Stoploss at 1%. +# Sell-signal wins over stoploss +tc25s = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5025, 4975, 4987, 6172, 0, 0, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0, 0, 0], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4986, 6172, 0, 0, 0, 0], + [3, 5010, 5010, 4986, 5010, 6172, 0, 0, 0, 1], + [4, 4990, 5010, 4855, 4995, 6172, 0, 0, 0, 0], # Triggers stoploss + sellsignal acted on + [5, 4995, 4995, 4950, 4950, 6172, 0, 0, 0, 0]], + stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True, + leverage=5.0, + trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4, is_short=True)] +) # 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 @@ -456,6 +506,25 @@ tc28 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) +# Test 28s: trailing_stop should raise so candle 3 causes a stoploss +# Same case than tc11 - but candle 3 "gaps down" - the stoploss will be above the candle, +# therefore "open" will be used +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 +tc28s = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0], + [1, 5000, 5050, 4890, 4890, 6172, 0, 0, 0, 0], + [2, 4890, 4890, 4749, 4890, 6172, 0, 0, 0, 0], + [3, 5150, 5350, 4950, 4950, 6172, 0, 0, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, 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.TRAILING_STOP_LOSS, open_tick=1, close_tick=3, is_short=True) + ] +) + # Test 29: trailing_stop should be triggered by low of next candle, without adjusting stoploss using # high of stoploss candle. # stop-loss: 10%, ROI: 10% (should not apply) @@ -534,6 +603,27 @@ tc33 = BTContainer(data=[ enter_tag='buy_signal_01' )] ) +# Test 33s: trailing_stop should be triggered immediately on trade open candle. +# copy of Test33 using shorts. +# stop-loss: 1%, ROI: 10% (should not apply) +tc33s = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0, 'short_signal_01'], + [1, 5000, 5049, 4500, 5000, 6172, 0, 0, 0, 0, None], # enter trade and stop + [2, 4900, 5250, 4500, 5100, 6172, 0, 0, 0, 0, None], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 0, None], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0, None]], + stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, + trailing_stop_positive=0.01, use_custom_stoploss=True, + trades=[BTrade( + sell_reason=SellType.TRAILING_STOP_LOSS, + open_tick=1, + close_tick=1, + enter_tag='short_signal_01', + is_short=True, + )] +) # Test 34: Custom-entry-price below all candles should timeout - so no trade happens. tc34 = BTContainer(data=[ @@ -558,7 +648,21 @@ tc35 = BTContainer(data=[ stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, custom_entry_price=7200, trades=[ BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1) -] +]) + +# Test 35s: Custom-entry-price above all candles should have rate adjusted to "entry candle high" +tc35s = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0], + [1, 5000, 5500, 4951, 5000, 6172, 0, 0, 0, 0], # Timeout + [2, 4900, 5250, 4500, 5100, 6172, 0, 0, 0, 0], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0]], + stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, + custom_entry_price=4000, + trades=[ + BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1, is_short=True) + ] ) # Test 36: Custom-entry-price around candle low @@ -613,7 +717,7 @@ tc39 = BTContainer(data=[ # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5500, 4951, 5000, 6172, 0, 0], - [2, 4900, 5250, 4900, 5100, 6172, 0, 1], # exit - but timeout + [2, 4950, 5250, 4900, 5100, 6172, 0, 1], # exit - entry timeout [3, 5100, 5100, 4950, 4950, 6172, 0, 0], [4, 5000, 5100, 4950, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, @@ -622,21 +726,32 @@ tc39 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4)] ) -# Test 40: (copy of test25 with leverage) -# Sell with signal sell in candle 3 (stoploss also triggers on this candle) -# Stoploss at 1%. -# Sell-signal wins over stoploss +# Test 39: Custom short exit price above below candles +# causes sell signal timeout +tc39a = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0], + [1, 5000, 5000, 4951, 5000, 6172, 0, 0, 0, 0], + [2, 4910, 5150, 4910, 5100, 6172, 0, 0, 0, 1], # exit - entry timeout + [3, 5100, 5100, 4950, 4950, 6172, 0, 0, 0, 0], + [4, 5000, 5100, 4950, 4950, 6172, 0, 0, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, + use_sell_signal=True, + custom_exit_price=4700, + trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4, is_short=True)] +) + +# Test 40: Colliding long and short signal tc40 = BTContainer(data=[ # D O H L C V EL XL ES Xs BT - [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, 4986, 6172, 0, 0], - [3, 5010, 5010, 4986, 5010, 6172, 0, 1], - [4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on - [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], - stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True, - leverage=5.0, - trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] + [0, 5000, 5050, 4950, 5000, 6172, 1, 0, 1, 0], + [1, 5000, 5500, 4951, 5000, 6172, 0, 0, 0, 0], + [2, 4900, 5250, 4900, 5100, 6172, 0, 0, 0, 0], + [3, 5100, 5100, 4950, 4950, 6172, 0, 0, 0, 0], + [4, 5000, 5100, 4950, 4950, 6172, 0, 0, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, + use_sell_signal=True, + trades=[] ) @@ -664,25 +779,31 @@ TESTS = [ tc20, tc21, tc22, + tc22s, tc23, tc24, tc25, + tc25l, + tc25s, tc26, tc27, tc28, + tc28s, tc29, tc30, tc31, tc32, tc33, + tc33s, tc34, tc35, + tc35s, tc36, tc37, tc38, tc39, + tc39a, tc40, - # TODO-lev: Add tests for short here ] @@ -709,11 +830,10 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) + # TODO: Should we initialize this properly?? + backtesting._can_short = True backtesting._set_strategy(backtesting.strategylist[0]) backtesting.required_startup = 0 - if data.leverage > 1.0: - # TODO: Should we initialize this properly?? - backtesting._can_short = True backtesting.strategy.advise_entry = lambda a, m: frame backtesting.strategy.advise_exit = lambda a, m: frame if data.custom_entry_price: @@ -740,8 +860,9 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: assert round(results["profit_ratio"].sum(), 3) == round(data.profit_perc, 3) for c, trade in enumerate(data.trades): - res = results.iloc[c] + res: BTrade = results.iloc[c] assert res.sell_reason == trade.sell_reason.value assert res.enter_tag == trade.enter_tag assert res.open_date == _get_frame_time_from_offset(trade.open_tick) assert res.close_date == _get_frame_time_from_offset(trade.close_tick) + assert res.is_short == trade.is_short