Merge pull request #1954 from freqtrade/fix/stoploss_cancel_error
Trailing stoploss cancel orders should be handled gracefully
This commit is contained in:
		| @@ -690,13 +690,22 @@ class FreqtradeBot(object): | |||||||
|                 # cancelling the current stoploss on exchange first |                 # cancelling the current stoploss on exchange first | ||||||
|                 logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})' |                 logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})' | ||||||
|                             'in order to add another one ...', order['id']) |                             'in order to add another one ...', order['id']) | ||||||
|                 if self.exchange.cancel_order(order['id'], trade.pair): |                 try: | ||||||
|  |                     self.exchange.cancel_order(order['id'], trade.pair) | ||||||
|  |                 except InvalidOrderException: | ||||||
|  |                     logger.exception(f"Could not cancel stoploss order {order['id']} " | ||||||
|  |                                      f"for pair {trade.pair}") | ||||||
|  |  | ||||||
|  |                 try: | ||||||
|                     # creating the new one |                     # creating the new one | ||||||
|                     stoploss_order_id = self.exchange.stoploss_limit( |                     stoploss_order_id = self.exchange.stoploss_limit( | ||||||
|                         pair=trade.pair, amount=trade.amount, |                         pair=trade.pair, amount=trade.amount, | ||||||
|                         stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99 |                         stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99 | ||||||
|                     )['id'] |                     )['id'] | ||||||
|                     trade.stoploss_order_id = str(stoploss_order_id) |                     trade.stoploss_order_id = str(stoploss_order_id) | ||||||
|  |                 except DependencyException: | ||||||
|  |                     logger.exception(f"Could create trailing stoploss order " | ||||||
|  |                                      f"for pair {trade.pair}.") | ||||||
|  |  | ||||||
|     def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: |     def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: | ||||||
|         if self.edge: |         if self.edge: | ||||||
| @@ -842,7 +851,10 @@ class FreqtradeBot(object): | |||||||
|  |  | ||||||
|         # First cancelling stoploss on exchange ... |         # First cancelling stoploss on exchange ... | ||||||
|         if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: |         if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: | ||||||
|             self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) |             try: | ||||||
|  |                 self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) | ||||||
|  |             except InvalidOrderException: | ||||||
|  |                 logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") | ||||||
|  |  | ||||||
|         # Execute sell and update trade record |         # Execute sell and update trade record | ||||||
|         order_id = self.exchange.sell(pair=str(trade.pair), |         order_id = self.exchange.sell(pair=str(trade.pair), | ||||||
|   | |||||||
| @@ -110,11 +110,23 @@ def patch_freqtradebot(mocker, config) -> None: | |||||||
|  |  | ||||||
|  |  | ||||||
| def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: | def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: | ||||||
|  |     """ | ||||||
|  |     This function patches _init_modules() to not call dependencies | ||||||
|  |     :param mocker: a Mocker object to apply patches | ||||||
|  |     :param config: Config to pass to the bot | ||||||
|  |     :return: FreqtradeBot | ||||||
|  |     """ | ||||||
|     patch_freqtradebot(mocker, config) |     patch_freqtradebot(mocker, config) | ||||||
|     return FreqtradeBot(config) |     return FreqtradeBot(config) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_patched_worker(mocker, config) -> Worker: | def get_patched_worker(mocker, config) -> Worker: | ||||||
|  |     """ | ||||||
|  |     This function patches _init_modules() to not call dependencies | ||||||
|  |     :param mocker: a Mocker object to apply patches | ||||||
|  |     :param config: Config to pass to the bot | ||||||
|  |     :return: Worker | ||||||
|  |     """ | ||||||
|     patch_freqtradebot(mocker, config) |     patch_freqtradebot(mocker, config) | ||||||
|     return Worker(args=None, config=config) |     return Worker(args=None, config=config) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -19,47 +19,13 @@ from freqtrade.persistence import Trade | |||||||
| from freqtrade.rpc import RPCMessageType | from freqtrade.rpc import RPCMessageType | ||||||
| from freqtrade.state import State | from freqtrade.state import State | ||||||
| from freqtrade.strategy.interface import SellCheckTuple, SellType | from freqtrade.strategy.interface import SellCheckTuple, SellType | ||||||
| from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge, | from freqtrade.tests.conftest import (get_patched_freqtradebot, | ||||||
|                                       patch_exchange, patch_get_signal, |                                       get_patched_worker, log_has, log_has_re, | ||||||
|                                       patch_wallet) |                                       patch_edge, patch_exchange, | ||||||
|  |                                       patch_get_signal, patch_wallet) | ||||||
| from freqtrade.worker import Worker | from freqtrade.worker import Worker | ||||||
|  |  | ||||||
|  |  | ||||||
| # Functions for recurrent object patching |  | ||||||
| def patch_freqtradebot(mocker, config) -> None: |  | ||||||
|     """ |  | ||||||
|     This function patches _init_modules() to not call dependencies |  | ||||||
|     :param mocker: a Mocker object to apply patches |  | ||||||
|     :param config: Config to pass to the bot |  | ||||||
|     :return: None |  | ||||||
|     """ |  | ||||||
|     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) |  | ||||||
|     mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) |  | ||||||
|     patch_exchange(mocker) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: |  | ||||||
|     """ |  | ||||||
|     This function patches _init_modules() to not call dependencies |  | ||||||
|     :param mocker: a Mocker object to apply patches |  | ||||||
|     :param config: Config to pass to the bot |  | ||||||
|     :return: FreqtradeBot |  | ||||||
|     """ |  | ||||||
|     patch_freqtradebot(mocker, config) |  | ||||||
|     return FreqtradeBot(config) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_patched_worker(mocker, config) -> Worker: |  | ||||||
|     """ |  | ||||||
|     This function patches _init_modules() to not call dependencies |  | ||||||
|     :param mocker: a Mocker object to apply patches |  | ||||||
|     :param config: Config to pass to the bot |  | ||||||
|     :return: Worker |  | ||||||
|     """ |  | ||||||
|     patch_freqtradebot(mocker, config) |  | ||||||
|     return Worker(args=None, config=config) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def patch_RPCManager(mocker) -> MagicMock: | def patch_RPCManager(mocker) -> MagicMock: | ||||||
|     """ |     """ | ||||||
|     This function mock RPC manager to avoid repeating this code in almost every tests |     This function mock RPC manager to avoid repeating this code in almost every tests | ||||||
| @@ -1176,6 +1142,77 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, | |||||||
|                                                 stop_price=0.00002344 * 0.95) |                                                 stop_price=0.00002344 * 0.95) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog, | ||||||
|  |                                                     markets, limit_buy_order, | ||||||
|  |                                                     limit_sell_order) -> None: | ||||||
|  |     # When trailing stoploss is set | ||||||
|  |     stoploss_limit = MagicMock(return_value={'id': 13434334}) | ||||||
|  |     patch_exchange(mocker) | ||||||
|  |  | ||||||
|  |     mocker.patch.multiple( | ||||||
|  |         'freqtrade.exchange.Exchange', | ||||||
|  |         get_ticker=MagicMock(return_value={ | ||||||
|  |             'bid': 0.00001172, | ||||||
|  |             'ask': 0.00001173, | ||||||
|  |             'last': 0.00001172 | ||||||
|  |         }), | ||||||
|  |         buy=MagicMock(return_value={'id': limit_buy_order['id']}), | ||||||
|  |         sell=MagicMock(return_value={'id': limit_sell_order['id']}), | ||||||
|  |         get_fee=fee, | ||||||
|  |         markets=PropertyMock(return_value=markets), | ||||||
|  |         stoploss_limit=stoploss_limit | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # enabling TSL | ||||||
|  |     default_conf['trailing_stop'] = True | ||||||
|  |  | ||||||
|  |     freqtrade = get_patched_freqtradebot(mocker, default_conf) | ||||||
|  |     # enabling stoploss on exchange | ||||||
|  |     freqtrade.strategy.order_types['stoploss_on_exchange'] = True | ||||||
|  |  | ||||||
|  |     # setting stoploss | ||||||
|  |     freqtrade.strategy.stoploss = -0.05 | ||||||
|  |  | ||||||
|  |     # setting stoploss_on_exchange_interval to 60 seconds | ||||||
|  |     freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 | ||||||
|  |     patch_get_signal(freqtrade) | ||||||
|  |     freqtrade.create_trade() | ||||||
|  |     trade = Trade.query.first() | ||||||
|  |     trade.is_open = True | ||||||
|  |     trade.open_order_id = None | ||||||
|  |     trade.stoploss_order_id = "abcd" | ||||||
|  |     trade.stop_loss = 0.2 | ||||||
|  |     trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime.replace(tzinfo=None) | ||||||
|  |  | ||||||
|  |     stoploss_order_hanging = { | ||||||
|  |         'id': "abcd", | ||||||
|  |         'status': 'open', | ||||||
|  |         'type': 'stop_loss_limit', | ||||||
|  |         'price': 3, | ||||||
|  |         'average': 2, | ||||||
|  |         'info': { | ||||||
|  |             'stopPrice': '0.1' | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) | ||||||
|  |     mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging) | ||||||
|  |     freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) | ||||||
|  |     assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", | ||||||
|  |                       caplog.record_tuples) | ||||||
|  |  | ||||||
|  |     # Still try to create order | ||||||
|  |     assert stoploss_limit.call_count == 1 | ||||||
|  |  | ||||||
|  |     # Fail creating stoploss order | ||||||
|  |     caplog.clear() | ||||||
|  |     cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock()) | ||||||
|  |     mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException()) | ||||||
|  |     freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) | ||||||
|  |     assert cancel_mock.call_count == 1 | ||||||
|  |     assert log_has_re(r"Could create trailing stoploss order for pair ETH/BTC\..*", | ||||||
|  |                       caplog.record_tuples) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, | def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, | ||||||
|                                               markets, limit_buy_order, limit_sell_order) -> None: |                                               markets, limit_buy_order, limit_sell_order) -> None: | ||||||
|  |  | ||||||
| @@ -2108,6 +2145,36 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe | |||||||
|     } == last_msg |     } == last_msg | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, | ||||||
|  |                                             markets, caplog) -> None: | ||||||
|  |     freqtrade = get_patched_freqtradebot(mocker, default_conf) | ||||||
|  |     mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) | ||||||
|  |     sellmock = MagicMock() | ||||||
|  |     mocker.patch.multiple( | ||||||
|  |         'freqtrade.exchange.Exchange', | ||||||
|  |         _load_markets=MagicMock(return_value={}), | ||||||
|  |         get_ticker=ticker, | ||||||
|  |         get_fee=fee, | ||||||
|  |         markets=PropertyMock(return_value=markets), | ||||||
|  |         sell=sellmock | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     freqtrade.strategy.order_types['stoploss_on_exchange'] = True | ||||||
|  |     patch_get_signal(freqtrade) | ||||||
|  |     freqtrade.create_trade() | ||||||
|  |  | ||||||
|  |     trade = Trade.query.first() | ||||||
|  |     Trade.session = MagicMock() | ||||||
|  |  | ||||||
|  |     freqtrade.config['dry_run'] = False | ||||||
|  |     trade.stoploss_order_id = "abcd" | ||||||
|  |  | ||||||
|  |     freqtrade.execute_sell(trade=trade, limit=1234, | ||||||
|  |                            sell_reason=SellType.STOP_LOSS) | ||||||
|  |     assert sellmock.call_count == 1 | ||||||
|  |     assert log_has('Could not cancel stoploss order abcd', caplog.record_tuples) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_execute_sell_with_stoploss_on_exchange(default_conf, | def test_execute_sell_with_stoploss_on_exchange(default_conf, | ||||||
|                                                 ticker, fee, ticker_sell_up, |                                                 ticker, fee, ticker_sell_up, | ||||||
|                                                 markets, mocker) -> None: |                                                 markets, mocker) -> None: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user