diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 0bd0e1549..54b73a3f3 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -758,7 +758,7 @@ For performance reasons, it's disabled by default and freqtrade will show a warn Additional orders also result in additional fees and those orders don't count towards `max_open_trades`. -This callback is **not** called when there is an open order (either buy or sell) waiting for execution. +This callback is also called when there is an open order (either buy or sell) waiting for execution - and will cancel the existing open order to place a new order if the amount, price or direction is different. `adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6212ad855..31c84f05c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -730,7 +730,7 @@ class FreqtradeBot(LoggingMixin): for trade in Trade.get_open_trades(): # If there is any open orders, wait for them to finish. # TODO Remove to allow mul open orders - if not trade.has_open_orders: + if trade.has_open_position or trade.has_open_orders: # Do a wallets update (will be ratelimited to once per hour) self.wallets.update(False) try: @@ -808,7 +808,10 @@ class FreqtradeBot(LoggingMixin): ) if amount == 0.0: - logger.info("Amount to exit is 0.0 due to exchange limits - not exiting.") + logger.info( + f"Wanted to exit of {stake_amount} amount, " + "but exit amount is now 0.0 due to exchange limits - not exiting." + ) return remaining = (trade.amount - amount) * current_exit_rate @@ -923,6 +926,10 @@ class FreqtradeBot(LoggingMixin): ): logger.info(f"User denied entry for {pair}.") return False + + if trade and self.handle_similar_open_order(trade, enter_limit_requested, amount, side): + return False + order = self.exchange.create_order( pair=pair, ordertype=order_type, @@ -1303,8 +1310,8 @@ class FreqtradeBot(LoggingMixin): logger.warning( f"Unable to handle stoploss on exchange for {trade.pair}: {exception}" ) - # Check if we can exit our current pair - if not trade.has_open_orders and trade.is_open and self.handle_trade(trade): + # Check if we can exit our current position for this trade + if trade.has_open_position and trade.is_open and self.handle_trade(trade): trades_closed += 1 except DependencyException as exception: @@ -1448,9 +1455,7 @@ class FreqtradeBot(LoggingMixin): self.handle_protections(trade.pair, trade.trade_direction) return True - if trade.has_open_orders or not trade.is_open: - # Trade has an open order, Stoploss-handling can't happen in this case - # as the Amount on the exchange is tied up in another trade. + if not trade.has_open_position or not trade.is_open: # The trade can be closed already (sell-order fill confirmation came in this iteration) return False @@ -1718,6 +1723,31 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Unable to replace order for {trade.pair}: {exception}") self.replace_order_failed(trade, f"Could not replace order for {trade}.") + def cancel_open_orders_of_trade( + self, trade: Trade, sides: list[str], reason: str, replacing: bool = False + ) -> None: + """ + Cancel trade orders of specified sides that are currently open + :param trade: Trade object of the trade we're analyzing + :param reason: The reason for that cancellation + :param sides: The sides where cancellation should take place + :return: None + """ + + for open_order in trade.open_orders: + try: + order = self.exchange.fetch_order(open_order.order_id, trade.pair) + except ExchangeError: + logger.info("Can't query order for %s due to %s", trade, traceback.format_exc()) + continue + + if order["side"] in sides: + if order["side"] == trade.entry_side: + self.handle_cancel_enter(trade, order, open_order, reason, replacing) + + elif order["side"] == trade.exit_side: + self.handle_cancel_exit(trade, order, open_order, reason) + def cancel_all_open_orders(self) -> None: """ Cancel all orders that are currently open @@ -1725,24 +1755,44 @@ class FreqtradeBot(LoggingMixin): """ for trade in Trade.get_open_trades(): - for open_order in trade.open_orders: - try: - order = self.exchange.fetch_order(open_order.order_id, trade.pair) - except ExchangeError: - logger.info("Can't query order for %s due to %s", trade, traceback.format_exc()) - continue + self.cancel_open_orders_of_trade( + trade, [trade.entry_side, trade.exit_side], constants.CANCEL_REASON["ALL_CANCELLED"] + ) - if order["side"] == trade.entry_side: - self.handle_cancel_enter( - trade, order, open_order, constants.CANCEL_REASON["ALL_CANCELLED"] - ) - - elif order["side"] == trade.exit_side: - self.handle_cancel_exit( - trade, order, open_order, constants.CANCEL_REASON["ALL_CANCELLED"] - ) Trade.commit() + def handle_similar_open_order( + self, trade: Trade, price: float, amount: float, side: str + ) -> bool: + """ + Keep existing open order if same amount and side otherwise cancel + :param trade: Trade object of the trade we're analyzing + :param price: Limit price of the potential new order + :param amount: Quantity of assets of the potential new order + :param side: Side of the potential new order + :return: True if an existing similar order was found + """ + if trade.has_open_orders: + oo = trade.select_order(side, True) + if oo is not None: + if (price == oo.price) and (side == oo.side) and (amount == oo.amount): + logger.info( + f"A similar open order was found for {trade.pair}. " + f"Keeping existing {trade.exit_side} order. {price=}, {amount=}" + ) + return True + # cancel open orders of this trade if order is different + self.cancel_open_orders_of_trade( + trade, + [trade.entry_side, trade.exit_side], + constants.CANCEL_REASON["REPLACE"], + True, + ) + Trade.commit() + return False + + return False + def handle_cancel_enter( self, trade: Trade, @@ -1924,7 +1974,11 @@ class FreqtradeBot(LoggingMixin): return amount trade_base_currency = self.exchange.get_pair_base_currency(pair) - wallet_amount = self.wallets.get_free(trade_base_currency) + # Free + Used - open orders will eventually still be canceled. + wallet_amount = self.wallets.get_free(trade_base_currency) + self.wallets.get_used( + trade_base_currency + ) + logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") if wallet_amount >= amount: return amount @@ -2017,6 +2071,10 @@ class FreqtradeBot(LoggingMixin): logger.info(f"User denied exit for {trade.pair}.") return False + if trade.has_open_orders: + if self.handle_similar_open_order(trade, limit, amount, trade.exit_side): + return False + try: # Execute sell and update trade record order = self.exchange.create_order( diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 471769c2d..eae9c91a6 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -832,6 +832,10 @@ class Backtesting: amount = amount_to_contract_precision( amount or trade.amount, trade.amount_precision, self.precision_mode, trade.contract_size ) + + if self.handle_similar_order(trade, close_rate, amount, trade.exit_side, exit_candle_time): + return None + order = Order( id=self.order_id_counter, ft_trade_id=trade.id, @@ -1117,6 +1121,10 @@ class Backtesting: orders=[], ) LocalTrade.add_bt_trade(trade) + elif self.handle_similar_order( + trade, propose_rate, amount, trade.entry_side, current_time + ): + return None trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) @@ -1158,9 +1166,13 @@ class Backtesting: """ for pair in open_trades.keys(): for trade in list(open_trades[pair]): - if trade.has_open_orders and trade.nr_of_successful_entries == 0: + if ( + trade.has_open_orders and trade.nr_of_successful_entries == 0 + ) or not trade.has_open_position: # Ignore trade if entry-order did not fill yet + LocalTrade.remove_bt_trade(trade) continue + exit_row = data[pair][-1] self._exit_trade( trade, exit_row, exit_row[OPEN_IDX], trade.amount, ExitType.FORCE_EXIT.value @@ -1215,6 +1227,37 @@ class Backtesting: # default maintain trade return False + def cancel_open_orders(self, trade: LocalTrade, current_time: datetime): + """ + Cancel all open orders for the given trade. + """ + for order in [o for o in trade.orders if o.ft_is_open]: + if order.side == trade.entry_side: + self.canceled_entry_orders += 1 + # elif order.side == trade.exit_side: + # self.canceled_exit_orders += 1 + # canceled orders are removed from the trade + del trade.orders[trade.orders.index(order)] + + def handle_similar_order( + self, trade: LocalTrade, price: float, amount: float, side: str, current_time: datetime + ) -> bool: + """ + Handle similar order for the given trade. + """ + if trade.has_open_orders: + oo = trade.select_order(side, True) + if oo: + if (price == oo.price) and (side == oo.side) and (amount == oo.amount): + # logger.info( + # f"A similar open order was found for {trade.pair}. " + # f"Keeping existing {trade.exit_side} order. {price=}, {amount=}" + # ) + return True + self.cancel_open_orders(trade, current_time) + + return False + def check_order_cancel( self, trade: LocalTrade, order: Order, current_time: datetime ) -> bool | None: @@ -1400,7 +1443,7 @@ class Backtesting: self.wallets.update() # 4. Create exit orders (if any) - if not trade.has_open_orders: + if trade.has_open_position: self._check_trade_exit(trade, row, current_time) # Place exit order if necessary # 5. Process exit orders. diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 6ecd44b4b..5f64e7c4a 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -191,6 +191,7 @@ class Order(ModelBase): return ( f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, " f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, " + f"amount={self.amount}, " f"status={self.status}, date={self.order_date_utc:{DATETIME_PRINT_FORMAT}})" ) @@ -599,6 +600,13 @@ class LocalTrade: ] return len(open_orders_wo_sl) > 0 + @property + def has_open_position(self) -> bool: + """ + True if there is an open position for this trade + """ + return self.amount > 0 + @property def open_sl_orders(self) -> list[Order]: """ diff --git a/tests/conftest.py b/tests/conftest.py index a66bd3430..142c8a443 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -988,6 +988,29 @@ def get_markets(): }, "info": {}, }, + "ETC/BTC": { + "id": "ETCBTC", + "symbol": "ETC/BTC", + "base": "ETC", + "quote": "BTC", + "active": True, + "spot": True, + "swap": False, + "linear": None, + "type": "spot", + "contractSize": None, + "precision": {"base": 8, "quote": 8, "amount": 2, "price": 7}, + "limits": { + "amount": {"min": 0.01, "max": 90000000.0}, + "price": {"min": 1e-07, "max": 1000.0}, + "cost": {"min": 0.0001, "max": 9000000.0}, + "leverage": { + "min": None, + "max": None, + }, + }, + "info": {}, + }, "ETH/USDT": { "id": "USDT-ETH", "symbol": "ETH/USDT", diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index 5f4ff4ef0..96b3349cf 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -1257,7 +1257,7 @@ def test_enter_positions( def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - mocker.patch("freqtrade.freqtradebot.FreqtradeBot.handle_trade", MagicMock(return_value=True)) + mocker.patch("freqtrade.freqtradebot.FreqtradeBot.handle_trade", MagicMock(return_value=False)) mocker.patch(f"{EXMS}.fetch_order", return_value=limit_order[entry_side(is_short)]) mocker.patch(f"{EXMS}.get_trades_for_order", return_value=[]) @@ -1329,6 +1329,7 @@ def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog ft_price=trade.open_rate, order_id=order_id, ft_is_open=False, + filled=11, ) ) Trade.session.add(trade) @@ -5957,13 +5958,13 @@ def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, ca freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(10, "aaaa")) freqtrade.process_open_trade_positions() assert log_has_re(r"Max adjustment entries for .* has been reached\.", caplog) - assert freqtrade.strategy.adjust_trade_position.call_count == 1 + assert freqtrade.strategy.adjust_trade_position.call_count == 4 caplog.clear() freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(-0.0005, "partial_exit_c")) freqtrade.process_open_trade_positions() assert log_has_re(r"LIMIT_SELL has been fulfilled.*", caplog) - assert freqtrade.strategy.adjust_trade_position.call_count == 1 + assert freqtrade.strategy.adjust_trade_position.call_count == 4 trade = Trade.get_trades(trade_filter=[Trade.id == 5]).first() assert trade.orders[-1].ft_order_tag == "partial_exit_c" assert trade.is_open diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index 75cc81fa1..9fc580753 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -436,7 +436,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) # Replace new order with diff. order at a lower price freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.95) - + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None) freqtrade.process() trade = Trade.get_trades().first() assert len(trade.orders) == 4 @@ -478,10 +478,14 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) assert pytest.approx(trade.amount) == 91.689215 * leverage assert pytest.approx(trade.orders[-1].amount) == 91.689215 * leverage assert freqtrade.strategy.adjust_entry_price.call_count == 0 + # Process again, should not adjust entry price freqtrade.process() trade = Trade.get_trades().first() + + assert trade.orders[-2].status == "closed" assert len(trade.orders) == 5 + assert trade.orders[-1].side == trade.exit_side assert trade.orders[-1].status == "open" assert trade.orders[-1].price == 2.02 # Adjust entry price cannot be called - this is an exit order @@ -532,7 +536,7 @@ def test_dca_order_adjust_entry_replace_fails( freqtrade.process() - assert freqtrade.strategy.adjust_trade_position.call_count == 1 + assert freqtrade.strategy.adjust_trade_position.call_count == 2 trades = Trade.session.scalars( select(Trade) .where(Order.ft_is_open.is_(True)) @@ -677,7 +681,11 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera assert trade.orders[-1].ft_order_side == "sell" assert pytest.approx(trade.stake_amount) == 40 assert trade.is_open is False - assert log_has_re("Amount to exit is 0.0 due to exchange limits - not exiting.", caplog) + assert log_has_re( + "Wanted to exit of -0.01 amount, but exit amount is now 0.0 due to exchange limits " + "- not exiting.", + caplog, + ) expected_profit = starting_amount - 60 + trade.realized_profit assert pytest.approx(freqtrade.wallets.get_free("USDT")) == expected_profit if spot: @@ -685,3 +693,133 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera else: # total won't change in futures mode, only free / used will. assert freqtrade.wallets.get_total("USDT") == starting_amount + trade.realized_profit + + +@pytest.mark.parametrize("leverage", [1, 2]) +@pytest.mark.parametrize("is_short", [False, True]) +def test_dca_handle_similar_open_order( + default_conf_usdt, ticker_usdt, is_short, leverage, fee, mocker, caplog +) -> None: + default_conf_usdt["position_adjustment_enable"] = True + default_conf_usdt["trading_mode"] = "futures" + default_conf_usdt["margin_mode"] = "isolated" + + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + mocker.patch.multiple( + EXMS, + fetch_ticker=ticker_usdt, + get_fee=fee, + amount_to_precision=lambda s, x, y: y, + price_to_precision=lambda s, x, y: y, + ) + mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False) + mocker.patch(f"{EXMS}.get_max_leverage", return_value=10) + mocker.patch(f"{EXMS}.get_funding_fees", return_value=0) + mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0, 0)) + + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + freqtrade.strategy.custom_entry_price = lambda **kwargs: ticker_usdt["ask"] * 0.96 + freqtrade.strategy.leverage = MagicMock(return_value=leverage) + freqtrade.strategy.custom_exit = MagicMock(return_value=False) + freqtrade.strategy.minimal_roi = {0: 0.2} + + # Create trade and initial entry order + freqtrade.enter_positions() + + assert len(Trade.get_trades().all()) == 1 + trade: Trade = Trade.get_trades().first() + assert len(trade.orders) == 1 + assert trade.orders[-1].side == trade.entry_side + assert trade.orders[-1].status == "open" + + assert trade.has_open_orders + # Process - shouldn't do anything + freqtrade.process() + # Doesn't try to exit, as we're not in a position yet + assert freqtrade.strategy.custom_exit.call_count == 0 + + # Adjust with new price, cancel initial entry order and place new one + freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.99) + freqtrade.strategy.ft_check_timed_out = MagicMock(return_value=False) + freqtrade.process() + trade = Trade.get_trades().first() + freqtrade.strategy.ft_check_timed_out = MagicMock(return_value=False) + + assert len(trade.orders) == 2 + assert len(trade.open_orders) == 1 + + # Adjust with new amount, should cancel and replace existing order + freqtrade.strategy.adjust_trade_position = MagicMock( + return_value=21 + ) # -(trade.stake_amount * 0.5) + freqtrade.process() + trade = Trade.get_trades().first() + + assert len(trade.orders) == 3 + assert len(trade.open_orders) == 1 + + # Fill entry order + assert freqtrade.strategy.custom_exit.call_count == 0 + + mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=True) + freqtrade.process() + + trade = Trade.get_trades().first() + assert trade.amount > 0 + + assert freqtrade.strategy.custom_exit.call_count == 1 + freqtrade.strategy.custom_exit.reset_mock() + + # Should Create a new exit order + freqtrade.exchange.amount_to_contract_precision = MagicMock(return_value=2) + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-2) + + mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False) + freqtrade.process() + trade = Trade.get_trades().first() + + assert trade.orders[-2].status == "closed" + assert trade.orders[-1].status == "open" + assert trade.orders[-1].side == trade.exit_side + assert len(trade.orders) == 5 + assert len(trade.open_orders) == 1 + assert freqtrade.strategy.custom_exit.call_count == 1 + freqtrade.strategy.custom_exit.reset_mock() + + # Adjust with new exit amount, should cancel and replace existing exit order + freqtrade.exchange.amount_to_contract_precision = MagicMock(return_value=3) + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-3) + freqtrade.process() + trade = Trade.get_trades().first() + # Even with open order, trying to exit... + assert freqtrade.strategy.custom_exit.call_count == 1 + freqtrade.strategy.custom_exit.reset_mock() + + assert trade.orders[-2].status == "canceled" + assert len(trade.orders) == 6 + assert len(trade.open_orders) == 1 + + # Adjust with new exit price, should cancel and replace existing exit order + freqtrade.strategy.custom_exit_price = MagicMock(return_value=1.95) + freqtrade.process() + # Even with open order, trying to exit... + assert freqtrade.strategy.custom_exit.call_count == 1 + freqtrade.strategy.custom_exit.reset_mock() + + trade = Trade.get_trades().first() + + assert trade.orders[-2].status == "canceled" + assert len(trade.orders) == 7 + assert len(trade.open_orders) == 1 + similar_msg = r"A similar open order was found for.*" + + assert not log_has_re(similar_msg, caplog) + + # Adjust with same params, should keep existing order as price and amount are similar + freqtrade.strategy.custom_exit_price = MagicMock(return_value=1.95) + freqtrade.process() + trade = Trade.get_trades().first() + assert log_has_re(similar_msg, caplog) + + assert len(trade.orders) == 7 + assert len(trade.open_orders) == 1 diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index dfce6d8c5..3299c6a53 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -45,6 +45,7 @@ class BTContainer(NamedTuple): leverage: float = 1.0 timeout: int | None = None adjust_entry_price: float | None = None + adjust_trade_position: list[float] | None = None def _get_frame_time_from_offset(offset): diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 63fa1bd96..7ba53a1b3 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -1185,6 +1185,39 @@ tc56 = BTContainer( ) +# Test 57: Custom-entry-price for position adjustment which won't fill +# Causing the negative adjustment to cancel the unfilled order and exit partially +tc57 = BTContainer( + data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5050, 4950, 5000, 6172, 1, 0, 0, 0], + [1, 4598, 5200, 4498, 5000, 6172, 0, 0, 0, 0], + [2, 4900, 5250, 4900, 5100, 6172, 0, 0, 0, 0], # Enhance position, but won't fill + [3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 0], + [4, 4750, 4950, 4650, 4750, 6172, 0, 0, 0, 0], + [5, 4750, 4950, 4650, 4750, 6172, 0, 1, 0, 0], + [6, 4750, 4950, 4650, 4750, 6172, 0, 0, 0, 0], + ], + stop_loss=-0.2, + roi={"0": 0.50}, + profit_perc=0.033, + use_exit_signal=True, + timeout=1000, + custom_entry_price=4600, + adjust_trade_position=[ + None, + 0.001, + None, + -0.0001, # Cancels the above unfilled order and exits partially + None, + None, + ], + trades=[ + BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=6, is_short=False), + ], +) + + TESTS = [ tc0, tc1, @@ -1243,6 +1276,7 @@ TESTS = [ tc54, tc55, tc56, + tc57, ] @@ -1289,7 +1323,13 @@ def test_backtest_results(default_conf, mocker, caplog, data: BTContainer) -> No backtesting.strategy.custom_entry_price = MagicMock(return_value=data.custom_entry_price) if data.custom_exit_price: backtesting.strategy.custom_exit_price = MagicMock(return_value=data.custom_exit_price) - backtesting.strategy.adjust_entry_price = MagicMock(return_value=data.adjust_entry_price) + if data.adjust_trade_position: + backtesting.strategy.position_adjustment_enable = True + backtesting.strategy.adjust_trade_position = MagicMock( + side_effect=data.adjust_trade_position + ) + if data.adjust_entry_price: + backtesting.strategy.adjust_entry_price = MagicMock(return_value=data.adjust_entry_price) backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss backtesting.strategy.leverage = lambda **kwargs: data.leverage @@ -1317,6 +1357,6 @@ def test_backtest_results(default_conf, mocker, caplog, data: BTContainer) -> No assert res.close_date == _get_frame_time_from_offset(trade.close_tick) assert res.is_short == trade.is_short assert len(LocalTrade.bt_trades) == len(data.trades) - assert len(LocalTrade.bt_trades_open) == 0 + assert len(LocalTrade.bt_trades_open) == 0, "Left open trade" backtesting.cleanup() del backtesting diff --git a/tests/persistence/test_trade_custom_data.py b/tests/persistence/test_trade_custom_data.py index b2971883d..e493276d7 100644 --- a/tests/persistence/test_trade_custom_data.py +++ b/tests/persistence/test_trade_custom_data.py @@ -60,6 +60,7 @@ def test_trade_custom_data(fee, use_db): def test_trade_custom_data_strategy_compat(mocker, default_conf_usdt, fee): mocker.patch(f"{EXMS}.get_rate", return_value=0.50) mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", return_value=None) + mocker.patch("freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit", return_value=True) default_conf_usdt["minimal_roi"] = {"0": 100} freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) @@ -85,8 +86,9 @@ def test_trade_custom_data_strategy_compat(mocker, default_conf_usdt, fee): trade_after = Trade.get_trades_proxy(pair="ADA/USDT")[0] assert trade_after.get_custom_data("test_str") == "test_value" assert trade_after.get_custom_data("test_int") == 1 - # 2 open pairs eligible for exit - assert ff_spy.call_count == 2 + # 2 trades filled entry, with open exit order + # 1 trade with filled entry order + assert ff_spy.call_count == 3 assert trade_after.exit_reason == "test_value_1"