Add short-exit logic to backtesting

This commit is contained in:
Matthias 2021-08-24 19:55:00 +02:00
parent eb71ee847c
commit b40f985b13
4 changed files with 42 additions and 22 deletions

View File

@ -856,14 +856,14 @@ class FreqtradeBot(LoggingMixin):
""" """
Check and execute sell Check and execute sell
""" """
should_sell = self.strategy.should_sell( should_exit: SellCheckTuple = self.strategy.should_exit(
trade, sell_rate, datetime.now(timezone.utc), buy, sell, trade, sell_rate, datetime.now(timezone.utc), buy, sell,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
) )
if should_sell.sell_flag: if should_exit.sell_flag:
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') logger.info(f'Executing Sell for {trade.pair}. Reason: {should_exit.sell_type}')
self.execute_sell(trade, sell_rate, should_sell) self.execute_sell(trade, sell_rate, should_exit)
return True return True
return False return False

View File

@ -332,12 +332,13 @@ class Backtesting:
return sell_row[OPEN_IDX] return sell_row[OPEN_IDX]
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
# TODO: short exits
sell_candle_time = sell_row[DATE_IDX].to_pydatetime() sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell = self.strategy.should_exit(
sell_candle_time, buy=sell_row[LONG_IDX], trade, sell_row[OPEN_IDX], sell_candle_time, # type: ignore
sell=sell_row[ELONG_IDX], enter_long=sell_row[LONG_IDX], enter_short=sell_row[SHORT_IDX],
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) exit_long=sell_row[ELONG_IDX], exit_short=sell_row[ESHORT_IDX],
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]
)
if sell.sell_flag: if sell.sell_flag:
trade.close_date = sell_candle_time trade.close_date = sell_candle_time

View File

@ -614,8 +614,10 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
return False return False
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, def should_exit(self, trade: Trade, rate: float, date: datetime, *,
sell: bool, low: float = None, high: float = None, enter_long: bool, enter_short: bool,
exit_long: bool, exit_short: bool,
low: float = None, high: float = None,
force_stoploss: float = 0) -> SellCheckTuple: force_stoploss: float = 0) -> SellCheckTuple:
""" """
This function evaluates if one of the conditions required to trigger a sell/exit_short This function evaluates if one of the conditions required to trigger a sell/exit_short
@ -625,6 +627,10 @@ class IStrategy(ABC, HyperStrategyMixin):
:param force_stoploss: Externally provided stoploss :param force_stoploss: Externally provided stoploss
:return: True if trade should be exited, False otherwise :return: True if trade should be exited, False otherwise
""" """
enter = enter_short if trade.is_short else enter_long
exit_ = exit_short if trade.is_short else exit_long
current_rate = rate current_rate = rate
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)
@ -639,7 +645,7 @@ class IStrategy(ABC, HyperStrategyMixin):
current_profit = trade.calc_profit_ratio(current_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. # if enter signal and ignore_roi is set, we don't need to evaluate min_roi.
roi_reached = (not (buy and self.ignore_roi_if_buy_signal) roi_reached = (not (enter and self.ignore_roi_if_buy_signal)
and self.min_roi_reached(trade=trade, current_profit=current_profit, and self.min_roi_reached(trade=trade, current_profit=current_profit,
current_time=date)) current_time=date))
@ -652,8 +658,8 @@ class IStrategy(ABC, HyperStrategyMixin):
if (self.sell_profit_only and current_profit <= self.sell_profit_offset): if (self.sell_profit_only and current_profit <= self.sell_profit_offset):
# sell_profit_only and profit doesn't reach the offset - ignore sell signal # sell_profit_only and profit doesn't reach the offset - ignore sell signal
pass pass
elif self.use_sell_signal and not buy: elif self.use_sell_signal and not enter:
if sell: if exit_:
sell_signal = SellType.SELL_SIGNAL sell_signal = SellType.SELL_SIGNAL
else: else:
trade_type = "exit_short" if trade.is_short else "sell" trade_type = "exit_short" if trade.is_short else "sell"
@ -712,9 +718,9 @@ class IStrategy(ABC, HyperStrategyMixin):
# Initiate stoploss with open_rate. Does nothing if stoploss is already set. # Initiate stoploss with open_rate. Does nothing if stoploss is already set.
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
dir_correct = ( dir_correct = (trade.stop_loss < (low or current_rate)
trade.stop_loss < (low or current_rate) and not trade.is_short or if not trade.is_short else
trade.stop_loss > (low or current_rate) and trade.is_short trade.stop_loss > (high or current_rate)
) )
if self.use_custom_stoploss and dir_correct: if self.use_custom_stoploss and dir_correct:
@ -735,6 +741,7 @@ class IStrategy(ABC, HyperStrategyMixin):
sl_offset = self.trailing_stop_positive_offset sl_offset = self.trailing_stop_positive_offset
# Make sure current_profit is calculated using high for backtesting. # Make sure current_profit is calculated using high for backtesting.
# TODO-lev: Check this function - high / low usage must be inversed for short trades!
high_profit = current_profit if not high else trade.calc_profit_ratio(high) high_profit = current_profit if not high else trade.calc_profit_ratio(high)
# Don't update stoploss if trailing_only_offset_is_reached is true. # Don't update stoploss if trailing_only_offset_is_reached is true.

View File

@ -452,27 +452,39 @@ def test_custom_sell(default_conf, fee, caplog) -> None:
) )
now = arrow.utcnow().datetime now = arrow.utcnow().datetime
res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) res = strategy.should_exit(trade, 1, now,
enter_long=False, enter_short=False,
exit_long=False, exit_short=False,
low=None, high=None)
assert res.sell_flag is False assert res.sell_flag is False
assert res.sell_type == SellType.NONE assert res.sell_type == SellType.NONE
strategy.custom_sell = MagicMock(return_value=True) strategy.custom_sell = MagicMock(return_value=True)
res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) res = strategy.should_exit(trade, 1, now,
enter_long=False, enter_short=False,
exit_long=False, exit_short=False,
low=None, high=None)
assert res.sell_flag is True assert res.sell_flag is True
assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_type == SellType.CUSTOM_SELL
assert res.sell_reason == 'custom_sell' assert res.sell_reason == 'custom_sell'
strategy.custom_sell = MagicMock(return_value='hello world') strategy.custom_sell = MagicMock(return_value='hello world')
res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) res = strategy.should_exit(trade, 1, now,
enter_long=False, enter_short=False,
exit_long=False, exit_short=False,
low=None, high=None)
assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_type == SellType.CUSTOM_SELL
assert res.sell_flag is True assert res.sell_flag is True
assert res.sell_reason == 'hello world' assert res.sell_reason == 'hello world'
caplog.clear() caplog.clear()
strategy.custom_sell = MagicMock(return_value='h' * 100) strategy.custom_sell = MagicMock(return_value='h' * 100)
res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) res = strategy.should_exit(trade, 1, now,
enter_long=False, enter_short=False,
exit_long=False, exit_short=False,
low=None, high=None)
assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_type == SellType.CUSTOM_SELL
assert res.sell_flag is True assert res.sell_flag is True
assert res.sell_reason == 'h' * 64 assert res.sell_reason == 'h' * 64