Merge pull request #7506 from freqtrade/cancel_partial_sell
Support cancellation partially filled exit orders
This commit is contained in:
commit
4c83552f3b
@ -1292,7 +1292,14 @@ class Exchange:
|
|||||||
order = self.fetch_order(order_id, pair)
|
order = self.fetch_order(order_id, pair)
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.warning(f"Could not fetch cancelled order {order_id}.")
|
logger.warning(f"Could not fetch cancelled order {order_id}.")
|
||||||
order = {'id': order_id, 'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
|
order = {
|
||||||
|
'id': order_id,
|
||||||
|
'status': 'canceled',
|
||||||
|
'amount': amount,
|
||||||
|
'filled': 0.0,
|
||||||
|
'fee': {},
|
||||||
|
'info': {}
|
||||||
|
}
|
||||||
|
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
@ -1409,47 +1409,63 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:return: True if exit order was cancelled, false otherwise
|
:return: True if exit order was cancelled, false otherwise
|
||||||
"""
|
"""
|
||||||
cancelled = False
|
cancelled = False
|
||||||
# if trade is not partially completed, just cancel the order
|
# Cancelled orders may have the status of 'canceled' or 'closed'
|
||||||
if order['remaining'] == order['amount'] or order.get('filled') == 0.0:
|
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
|
||||||
if not self.exchange.check_order_canceled_empty(order):
|
filled_val: float = order.get('filled', 0.0) or 0.0
|
||||||
try:
|
filled_rem_stake = trade.stake_amount - filled_val * trade.open_rate
|
||||||
# if trade is not partially completed, just delete the order
|
minstake = self.exchange.get_min_pair_stake_amount(
|
||||||
co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
|
trade.pair, trade.open_rate, self.strategy.stoploss)
|
||||||
trade.amount)
|
# Double-check remaining amount
|
||||||
trade.update_order(co)
|
if filled_val > 0:
|
||||||
except InvalidOrderException:
|
reason = constants.CANCEL_REASON['PARTIALLY_FILLED']
|
||||||
logger.exception(
|
if minstake and filled_rem_stake < minstake:
|
||||||
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
|
logger.warning(
|
||||||
return False
|
f"Order {trade.open_order_id} for {trade.pair} not cancelled, as "
|
||||||
logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
|
f"the filled amount of {filled_val} would result in an unexitable trade.")
|
||||||
else:
|
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
|
||||||
logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
|
|
||||||
trade.update_order(order)
|
|
||||||
|
|
||||||
|
self._notify_exit_cancel(
|
||||||
|
trade,
|
||||||
|
order_type=self.strategy.order_types['exit'],
|
||||||
|
reason=reason, order_id=order['id'],
|
||||||
|
sub_trade=trade.amount != order['amount']
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
|
||||||
|
trade.amount)
|
||||||
|
except InvalidOrderException:
|
||||||
|
logger.exception(
|
||||||
|
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
|
||||||
|
return False
|
||||||
trade.close_rate = None
|
trade.close_rate = None
|
||||||
trade.close_rate_requested = None
|
trade.close_rate_requested = None
|
||||||
trade.close_profit = None
|
trade.close_profit = None
|
||||||
trade.close_profit_abs = None
|
trade.close_profit_abs = None
|
||||||
trade.open_order_id = None
|
# Set exit_reason for fill message
|
||||||
trade.exit_reason = None
|
exit_reason_prev = trade.exit_reason
|
||||||
|
trade.exit_reason = trade.exit_reason + f", {reason}" if trade.exit_reason else reason
|
||||||
|
self.update_trade_state(trade, trade.open_order_id, co)
|
||||||
|
# Order might be filled above in odd timing issues.
|
||||||
|
if co.get('status') in ('canceled', 'cancelled'):
|
||||||
|
trade.exit_reason = None
|
||||||
|
else:
|
||||||
|
trade.exit_reason = exit_reason_prev
|
||||||
|
|
||||||
|
logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
|
||||||
cancelled = True
|
cancelled = True
|
||||||
self.wallets.update()
|
|
||||||
else:
|
else:
|
||||||
# TODO: figure out how to handle partially complete sell orders
|
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
|
||||||
cancelled = False
|
self.update_trade_state(trade, trade.open_order_id, order)
|
||||||
|
|
||||||
order_obj = trade.select_order_by_order_id(order['id'])
|
self.wallets.update()
|
||||||
if not order_obj:
|
|
||||||
raise DependencyException(
|
|
||||||
f"Order_obj not found for {order['id']}. This should not have happened.")
|
|
||||||
|
|
||||||
sub_trade = order_obj.amount != trade.amount
|
|
||||||
self._notify_exit_cancel(
|
self._notify_exit_cancel(
|
||||||
trade,
|
trade,
|
||||||
order_type=self.strategy.order_types['exit'],
|
order_type=self.strategy.order_types['exit'],
|
||||||
reason=reason, order=order_obj, sub_trade=sub_trade
|
reason=reason, order_id=order['id'], sub_trade=trade.amount != order['amount']
|
||||||
)
|
)
|
||||||
return cancelled
|
return cancelled
|
||||||
|
|
||||||
@ -1646,7 +1662,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str,
|
def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str,
|
||||||
order: Order, sub_trade: bool = False) -> None:
|
order_id: str, sub_trade: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a sell cancel occurred.
|
Sends rpc notification when a sell cancel occurred.
|
||||||
"""
|
"""
|
||||||
@ -1655,6 +1671,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
else:
|
else:
|
||||||
trade.exit_order_status = reason
|
trade.exit_order_status = reason
|
||||||
|
|
||||||
|
order = trade.select_order_by_order_id(order_id)
|
||||||
|
if not order:
|
||||||
|
raise DependencyException(
|
||||||
|
f"Order_obj not found for {order_id}. This should not have happened.")
|
||||||
|
|
||||||
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||||
profit_trade = trade.calc_profit(rate=profit_rate)
|
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||||
current_rate = self.exchange.get_rate(
|
current_rate = self.exchange.get_rate(
|
||||||
|
@ -3095,6 +3095,9 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
|
|||||||
|
|
||||||
cancel_order_mock.reset_mock()
|
cancel_order_mock.reset_mock()
|
||||||
l_order['filled'] = 1.0
|
l_order['filled'] = 1.0
|
||||||
|
order = deepcopy(l_order)
|
||||||
|
order['status'] = 'canceled'
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
|
||||||
assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
|
assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
|
|
||||||
@ -3108,6 +3111,9 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
|
|||||||
cancel_order=cancel_order_mock,
|
cancel_order=cancel_order_mock,
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.245441)
|
mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.245441)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=0.2)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_order_fee')
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
|
|
||||||
@ -3175,7 +3181,9 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
|
|||||||
|
|
||||||
send_msg_mock.reset_mock()
|
send_msg_mock.reset_mock()
|
||||||
|
|
||||||
|
# Partial exit - below exit threshold
|
||||||
order['amount'] = 2
|
order['amount'] = 2
|
||||||
|
order['filled'] = 1.9
|
||||||
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||||
# Assert cancel_order was not called (callcount remains unchanged)
|
# Assert cancel_order was not called (callcount remains unchanged)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
@ -3185,12 +3193,21 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
|
|||||||
|
|
||||||
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
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']
|
assert (send_msg_mock.call_args_list[0][0][0]['reason']
|
||||||
|
== CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'])
|
||||||
|
|
||||||
# Message should not be iterated again
|
# Message should not be iterated again
|
||||||
assert trade.exit_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
assert trade.exit_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||||
assert send_msg_mock.call_count == 1
|
assert send_msg_mock.call_count == 1
|
||||||
|
|
||||||
|
send_msg_mock.reset_mock()
|
||||||
|
|
||||||
|
order['filled'] = 1
|
||||||
|
assert freqtrade.handle_cancel_exit(trade, order, reason)
|
||||||
|
assert send_msg_mock.call_count == 1
|
||||||
|
assert (send_msg_mock.call_args_list[0][0][0]['reason']
|
||||||
|
== CANCEL_REASON['PARTIALLY_FILLED'])
|
||||||
|
|
||||||
|
|
||||||
def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
|
def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
Loading…
Reference in New Issue
Block a user