diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 99872ff0b..20fd833eb 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -979,10 +979,10 @@ class FreqtradeBot(LoggingMixin): or (order_obj and self.strategy.ft_check_timed_out( 'sell', trade, order_obj, datetime.now(timezone.utc)) ))): - self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) + canceled = self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) canceled_count = trade.get_exit_order_count() max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) - if max_timeouts > 0 and canceled_count >= max_timeouts: + if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: logger.warning(f'Emergencyselling trade {trade}, as the sell order ' f'timed out {max_timeouts} times.') try: @@ -1079,11 +1079,12 @@ class FreqtradeBot(LoggingMixin): reason=reason) return was_trade_fully_canceled - def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str: + def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> bool: """ Sell cancel - cancel order and update trade - :return: Reason for cancel + :return: True if exit order was cancelled, false otherwise """ + cancelled = False # if trade is not partially completed, just cancel the order if order['remaining'] == order['amount'] or order.get('filled') == 0.0: if not self.exchange.check_order_canceled_empty(order): @@ -1094,7 +1095,7 @@ class FreqtradeBot(LoggingMixin): trade.update_order(co) except InvalidOrderException: logger.exception(f"Could not cancel sell order {trade.open_order_id}") - return 'error cancelling order' + return False logger.info('Sell order %s for %s.', reason, trade) else: reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] @@ -1108,9 +1109,11 @@ class FreqtradeBot(LoggingMixin): trade.close_date = None trade.is_open = True trade.open_order_id = None + cancelled = True else: # TODO: figure out how to handle partially complete sell orders reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + cancelled = False self.wallets.update() self._notify_exit_cancel( @@ -1118,7 +1121,7 @@ class FreqtradeBot(LoggingMixin): order_type=self.strategy.order_types['sell'], reason=reason ) - return reason + return cancelled def _safe_exit_amount(self, pair: str, amount: float) -> float: """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d7b47174b..d433998a1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -6,7 +6,7 @@ import time from copy import deepcopy from math import isclose from typing import List -from unittest.mock import ANY, MagicMock, PropertyMock +from unittest.mock import ANY, MagicMock, PropertyMock, patch import arrow import pytest @@ -2220,9 +2220,14 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') caplog.clear() - # 2nd canceled trade ... open_trade.open_order_id = limit_sell_order_old['id'] + + # If cancelling fails - no emergency sell! + with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False): + freqtrade.check_handle_timedout() + assert et_mock.call_count == 0 + freqtrade.check_handle_timedout() assert log_has_re('Emergencyselling trade.*', caplog) assert et_mock.call_count == 1 @@ -2564,13 +2569,17 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: send_msg_mock.reset_mock() order['amount'] = 2 - assert freqtrade.handle_cancel_exit(trade, order, reason - ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + assert not freqtrade.handle_cancel_exit(trade, order, reason) # Assert cancel_order was not called (callcount remains unchanged) assert cancel_order_mock.call_count == 1 assert send_msg_mock.call_count == 1 - assert freqtrade.handle_cancel_exit(trade, order, reason - ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + assert (send_msg_mock.call_args_list[0][0][0]['reason'] + == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']) + + assert not freqtrade.handle_cancel_exit(trade, order, reason) + + send_msg_mock.call_args_list[0][0][0]['reason'] = CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + # Message should not be iterated again assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert send_msg_mock.call_count == 1 @@ -2589,7 +2598,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: order = {'remaining': 1, 'amount': 1, 'status': "open"} - assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order' + assert not freqtrade.handle_cancel_exit(trade, order, reason) def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, mocker