diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b79cac243..58fdebe85 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -11,9 +11,9 @@ from typing import Any, Dict, List, Optional, Tuple import arrow from numpy import NAN, mean -from freqtrade.exceptions import ExchangeError, PricingError - -from freqtrade.exchange import timeframe_to_msecs, timeframe_to_minutes +from freqtrade.exceptions import (ExchangeError, InvalidOrderException, + PricingError) +from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -541,24 +541,42 @@ class RPC: def _rpc_delete(self, trade_id: str) -> Dict[str, str]: """ Handler for delete . - Delete the given trade + Delete the given trade and close eventually existing open orders. """ - def _exec_delete(trade: Trade) -> None: - Trade.session.delete(trade) - Trade.session.flush() - with self._freqtrade._sell_lock: - trade = Trade.get_trades( - trade_filter=[Trade.id == trade_id, ] - ).first() + c_count = 0 + trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first() if not trade: - logger.warning('delete: Invalid argument received') + logger.warning('delete trade: Invalid argument received') raise RPCException('invalid argument') - _exec_delete(trade) + # Try cancelling regular order if that exists + if trade.open_order_id: + try: + self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair) + c_count += 1 + except (ExchangeError, InvalidOrderException): + pass + + # cancel stoploss on exchange ... + if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange') + and trade.stoploss_order_id): + try: + self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id, + trade.pair) + c_count += 1 + except (ExchangeError, InvalidOrderException): + pass + + Trade.session.delete(trade) Trade.session.flush() self._freqtrade.wallets.update() - return {'result_msg': f'Deleted trade {trade_id}.'} + return { + 'result': 'success', + 'trade_id': trade_id, + 'result_msg': f'Deleted trade {trade_id}. Closed {c_count} open orders.', + 'cancel_order_count': c_count, + } def _rpc_performance(self) -> List[Dict[str, Any]]: """ diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index e5859fcd9..b4a781459 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -8,7 +8,7 @@ import pytest from numpy import isnan from freqtrade.edge import PairInfo -from freqtrade.exceptions import ExchangeError, TemporaryError +from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -291,6 +291,61 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee): assert trades['trades'][0]['pair'] == 'XRP/BTC' +def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog): + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + stoploss_mock = MagicMock() + cancel_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + cancel_order=cancel_mock, + cancel_stoploss_order=stoploss_mock, + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + freqtradebot.strategy.order_types['stoploss_on_exchange'] = True + create_mock_trades(fee) + rpc = RPC(freqtradebot) + with pytest.raises(RPCException, match='invalid argument'): + rpc._rpc_delete('200') + + create_mock_trades(fee) + trades = Trade.query.all() + trades[1].stoploss_order_id = '1234' + trades[2].stoploss_order_id = '1234' + assert len(trades) > 2 + + res = rpc._rpc_delete('1') + assert isinstance(res, dict) + assert res['result'] == 'success' + assert res['trade_id'] == '1' + assert res['cancel_order_count'] == 1 + assert cancel_mock.call_count == 1 + assert stoploss_mock.call_count == 0 + cancel_mock.reset_mock() + stoploss_mock.reset_mock() + + res = rpc._rpc_delete('2') + assert isinstance(res, dict) + assert cancel_mock.call_count == 1 + assert stoploss_mock.call_count == 1 + assert res['cancel_order_count'] == 2 + + stoploss_mock = mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', + side_effect=InvalidOrderException) + + res = rpc._rpc_delete('3') + assert stoploss_mock.call_count == 1 + stoploss_mock.reset_mock() + + cancel_mock = mocker.patch('freqtrade.exchange.Exchange.cancel_order', + side_effect=InvalidOrderException) + + res = rpc._rpc_delete('4') + assert cancel_mock.call_count == 1 + assert stoploss_mock.call_count == 0 + + def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch.multiple(