From b4e732617e8b1b2ad483c846301254c78b4b35ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 21 Sep 2023 20:45:06 +0200 Subject: [PATCH] Add handling for order replacement cancel failing --- docs/strategy-callbacks.md | 2 ++ freqtrade/freqtradebot.py | 23 +++++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index e42aa39d2..2639d5521 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -920,6 +920,8 @@ Returning any other price will cancel the existing order, and replace it with a The trade open-date (`trade.open_date_utc`) will remain at the time of the very first order placed. Please make sure to be aware of this - and eventually adjust your logic in other callbacks to account for this, and use the date of the first filled order instead. +If the cancellation of the original order fails, then the order will not be replaced - though the order will most likely have been canceled on exchange. Having this happen on initial entries will result in the deletion of the order, while on position adjustment orders, it'll result in the trade size remaining as is. + !!! Warning "Regular timeout" Entry `unfilledtimeout` mechanism (as well as `check_entry_timeout()`) takes precedence over this. Entry Orders that are cancelled via the above methods will not have this callback called. Be sure to update timeout values to match your expectations. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0386771e1..02d43432d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -7,6 +7,7 @@ from copy import deepcopy from datetime import datetime, time, timedelta, timezone from math import isclose from threading import Lock +from time import sleep from typing import Any, Dict, List, Optional, Tuple from schedule import Scheduler @@ -1439,8 +1440,12 @@ class FreqtradeBot(LoggingMixin): cancel_reason = constants.CANCEL_REASON['USER_CANCEL'] if order_obj.price != adjusted_entry_price: # cancel existing order if new price is supplied or None - self.handle_cancel_enter(trade, order, order_obj.order_id, cancel_reason, - replacing=replacing) + res = self.handle_cancel_enter(trade, order, order_obj.order_id, cancel_reason, + replacing=replacing) + if not res: + self.replace_order_failed( + trade, f"Could not cancel order for {trade}, therefore not replacing.") + return if adjusted_entry_price: # place new order only if new price is supplied if not self.execute_entry( @@ -1494,7 +1499,6 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"No open order for {trade}.") return False - # Cancelled orders may have the status of 'canceled' or 'closed' if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES: filled_val: float = order.get('filled', 0.0) or 0.0 filled_stake = filled_val * trade.open_rate @@ -1508,6 +1512,17 @@ class FreqtradeBot(LoggingMixin): return False corder = self.exchange.cancel_order_with_result(order_id, trade.pair, trade.amount) + # if replacing, retry fetching the order 3 times if the status is not what we need + if replacing: + retry_count = 0 + while ( + corder.get('status') not in constants.NON_OPEN_EXCHANGE_STATES + and retry_count < 3 + ): + sleep(0.5) + corder = self.exchange.fetch_order(order_id, trade.pair) + retry_count += 1 + # Avoid race condition where the order could not be cancelled coz its already filled. # Simply bailing here is the only safe way - as this order will then be # handled in the next iteration. @@ -1524,6 +1539,7 @@ class FreqtradeBot(LoggingMixin): # Using filled to determine the filled amount filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): + was_trade_fully_canceled = True # if trade is not partially completed and it's the only order, just delete the trade open_order_count = len([ order for order in trade.orders if order.ft_is_open and order.order_id != order_id @@ -1531,7 +1547,6 @@ class FreqtradeBot(LoggingMixin): if open_order_count < 1 and trade.nr_of_successful_entries == 0 and not replacing: logger.info(f'{side} order fully cancelled. Removing {trade} from database.') trade.delete() - was_trade_fully_canceled = True reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" else: self.update_trade_state(trade, order_id, corder)