From 3d67e0893ad21fd9845c6aae930e176c60676dbb Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 14 Apr 2024 00:16:55 -0400 Subject: [PATCH 01/65] edit backtest_loop to check exit if trade has open position --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/persistence/trade_model.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fea7a56ef..c9c128af5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1181,7 +1181,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 defeb0e3f..e49eea974 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -544,6 +544,25 @@ 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 + """ + entry_orders = [ + o for o in self.orders + if o.ft_order_side == self.entry_side + ] + entry_orders_filled_qty = sum(eo.filled for eo in entry_orders) + + exit_orders = [ + o for o in self.orders + if o.ft_order_side == self.exit_side + ] + exit_orders_filled_qty = sum(eo.filled for eo in exit_orders) + + return (entry_orders_filled_qty - exit_orders_filled_qty) > 0 + @property def open_sl_orders(self) -> List[Order]: """ From faeda2a1660460430ee6fb34f4c148caf145198b Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 14 Apr 2024 00:40:12 -0400 Subject: [PATCH 02/65] fix mypy error on has_open_position function --- freqtrade/persistence/trade_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index e49eea974..f092176da 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -553,13 +553,13 @@ class LocalTrade: o for o in self.orders if o.ft_order_side == self.entry_side ] - entry_orders_filled_qty = sum(eo.filled for eo in entry_orders) + entry_orders_filled_qty = sum(eno.safe_filled for eno in entry_orders) exit_orders = [ o for o in self.orders if o.ft_order_side == self.exit_side ] - exit_orders_filled_qty = sum(eo.filled for eo in exit_orders) + exit_orders_filled_qty = sum(exo.safe_filled for exo in exit_orders) return (entry_orders_filled_qty - exit_orders_filled_qty) > 0 From 6752c3e288dd5611549b5ddd7495e60cb4bfbde7 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 15 Apr 2024 14:43:40 -0400 Subject: [PATCH 03/65] add has_untied_assets, replace one has_open_orders condition by has_untied_assets in exit_positions --- freqtrade/freqtradebot.py | 2 +- freqtrade/persistence/trade_model.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0184741ff..e4835034d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1093,7 +1093,7 @@ class FreqtradeBot(LoggingMixin): logger.warning( f'Unable to handle stoploss on exchange for {trade.pair}: {exception}') # Check if we can sell our current pair - if not trade.has_open_orders and trade.is_open and self.handle_trade(trade): + if trade.has_untied_assets and trade.is_open and self.handle_trade(trade): trades_closed += 1 except DependencyException as exception: diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index f092176da..1ee350b5b 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -563,6 +563,26 @@ class LocalTrade: return (entry_orders_filled_qty - exit_orders_filled_qty) > 0 + @property + def has_untied_assets(self) -> bool: + """ + True if there is still remaining position not yet tied up to exit order + """ + entry_orders = [ + o for o in self.orders + if o.ft_order_side == self.entry_side + ] + entry_orders_filled_qty = sum(eno.safe_filled for eno in entry_orders) + + exit_orders = [ + o for o in self.orders + if o.ft_order_side == self.exit_side + ] + exit_orders_remaining_qty = sum(exo.safe_remaining for exo in exit_orders) + untied_remaining = entry_orders_filled_qty - exit_orders_remaining_qty + + return untied_remaining > 0 + @property def open_sl_orders(self) -> List[Order]: """ From 05cf4cab8e5c2c832bb4a910a1eb6d6d0fd5281e Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 15 Apr 2024 15:18:39 -0400 Subject: [PATCH 04/65] add has_open_entry_orders property to trade --- freqtrade/persistence/trade_model.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 1ee350b5b..11b97f8cb 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -544,6 +544,17 @@ class LocalTrade: ] return len(open_orders_wo_sl) > 0 + @property + def has_open_entry_orders(self) -> bool: + """ + True if there are open entry orders for this trade + """ + open_entry_orders = [ + o for o in self.orders + if o.ft_order_side == self.entry_side and o.ft_is_open + ] + return len(open_entry_orders) > 0 + @property def has_open_position(self) -> bool: """ From 910b3ad536ab94de19182ca461eec6c32b6da36d Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 15 Apr 2024 15:40:57 -0400 Subject: [PATCH 05/65] allow adjust trade position, even if there is an open order --- freqtrade/freqtradebot.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e4835034d..f61a9c3b0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -608,16 +608,13 @@ class FreqtradeBot(LoggingMixin): """ # Walk through each pair and check if it needs changes 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: - # Do a wallets update (will be ratelimited to once per hour) - self.wallets.update(False) - try: - self.check_and_call_adjust_trade_position(trade) - except DependencyException as exception: - logger.warning( - f"Unable to adjust position of trade for {trade.pair}: {exception}") + # Do a wallets update (will be ratelimited to once per hour) + self.wallets.update(False) + try: + self.check_and_call_adjust_trade_position(trade) + except DependencyException as exception: + logger.warning( + f"Unable to adjust position of trade for {trade.pair}: {exception}") def check_and_call_adjust_trade_position(self, trade: Trade): """ From fb3787173f344a38a7bf53d9e0b4de8fbe4011c8 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 9 Sep 2024 12:07:50 -0400 Subject: [PATCH 06/65] fix test_exit_positions_exception --- tests/freqtradebot/test_freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index 7c45928a2..ec9d27a4d 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -1305,6 +1305,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) From 714822c93c9615765bf8985157f645cc04bbb2a6 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 9 Sep 2024 13:15:17 -0400 Subject: [PATCH 07/65] add ETC/BTC pair to conftest get_markets --- tests/conftest.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 99c42de5f..66f9e195f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -964,6 +964,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", From 79ce1ddaef490550b725954330000bc68317960d Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 9 Sep 2024 13:30:07 -0400 Subject: [PATCH 08/65] rollback on process_open_trade_positions. Adjust position only if there is no open order, change will be made on other PR --- freqtrade/freqtradebot.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 05cab0e5f..64139f0ae 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -713,13 +713,14 @@ class FreqtradeBot(LoggingMixin): """ # Walk through each pair and check if it needs changes for trade in Trade.get_open_trades(): - # Do a wallets update (will be ratelimited to once per hour) - self.wallets.update(False) - try: - self.check_and_call_adjust_trade_position(trade) - except DependencyException as exception: - logger.warning( - f"Unable to adjust position of trade for {trade.pair}: {exception}") + if not trade.has_open_orders: + # Do a wallets update (will be ratelimited to once per hour) + self.wallets.update(False) + try: + self.check_and_call_adjust_trade_position(trade) + except DependencyException as exception: + logger.warning( + f"Unable to adjust position of trade for {trade.pair}: {exception}") def check_and_call_adjust_trade_position(self, trade: Trade): """ From 730bef292097ffa5fc5f2323421fe8cbcc519115 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 9 Sep 2024 19:28:04 -0400 Subject: [PATCH 09/65] add comments and logs for failing test, create untied_assets propertyto Trade --- freqtrade/persistence/trade_model.py | 22 +++++++++++++++++----- tests/freqtradebot/test_integration.py | 20 ++++++++++++++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 2df78796f..76f69793f 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -178,6 +178,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}})" ) @@ -616,10 +617,7 @@ class LocalTrade: return (entry_orders_filled_qty - exit_orders_filled_qty) > 0 @property - def has_untied_assets(self) -> bool: - """ - True if there is still remaining position not yet tied up to exit order - """ + def untied_assets(self) -> float: entry_orders = [ o for o in self.orders if o.ft_order_side == self.entry_side @@ -633,7 +631,21 @@ class LocalTrade: exit_orders_remaining_qty = sum(exo.safe_remaining for exo in exit_orders) untied_remaining = entry_orders_filled_qty - exit_orders_remaining_qty - return untied_remaining > 0 + logger.info(f"entry_orders: {entry_orders}") + logger.info(f"exit_orders: {exit_orders}") + logger.info(f"entry_orders_filled_qty: {entry_orders_filled_qty}") + logger.info(f"exit_orders_remaining_qty: {exit_orders_remaining_qty}") + + logger.info(f"untied_remaining: {untied_remaining}") + + return untied_remaining + + @property + def has_untied_assets(self) -> bool: + """ + True if there is still remaining position not yet tied up to exit order + """ + return self.untied_assets > 0 @property def open_sl_orders(self) -> List[Order]: diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index 75cc81fa1..649bacddf 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -1,3 +1,4 @@ +import logging import time from unittest.mock import MagicMock @@ -347,8 +348,8 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.nr_of_successful_exits == 1 -@pytest.mark.parametrize("leverage", [1, 2]) -def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) -> None: +@pytest.mark.parametrize("leverage", [1]) +def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker, caplog) -> None: default_conf_usdt["position_adjustment_enable"] = True default_conf_usdt["trading_mode"] = "futures" default_conf_usdt["margin_mode"] = "isolated" @@ -478,9 +479,24 @@ 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 + + print("BEFORE Process trade.orders") + print(trade.orders) + + # adding this will prevent the second exit order creation, but this test case must be handled properly + #freqtrade.strategy.custom_exit = MagicMock(return_value=None) + + caplog.clear() + caplog.set_level(logging.DEBUG) # Process again, should not adjust entry price freqtrade.process() trade = Trade.get_trades().first() + + print(f"DEBUG TEST") + print(caplog.text) + print("AFTER Process trade.orders") + print(trade.orders) + assert len(trade.orders) == 5 assert trade.orders[-1].status == "open" assert trade.orders[-1].price == 2.02 From 33b421014d97f3460efb383d3da3cd8032752e4b Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 11 Sep 2024 18:42:51 -0400 Subject: [PATCH 10/65] remove irrelevant trade.has_open_orders conditions --- freqtrade/freqtradebot.py | 7 +++---- freqtrade/optimize/backtesting.py | 3 +-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 64139f0ae..e23d07af9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1246,8 +1246,7 @@ class FreqtradeBot(LoggingMixin): trades_closed = 0 for trade in trades: if ( - not trade.has_open_orders - and not trade.has_open_sl_orders + not trade.has_open_sl_orders and not self.wallets.check_exit_amount(trade) ): logger.warning( @@ -1272,7 +1271,7 @@ class FreqtradeBot(LoggingMixin): f"Unable to handle stoploss on exchange for {trade.pair}: {exception}" ) # Check if we can sell our current pair - if trade.has_untied_assets and trade.is_open and self.handle_trade(trade): + if trade.is_open and self.handle_trade(trade): trades_closed += 1 except DependencyException as exception: @@ -1416,7 +1415,7 @@ class FreqtradeBot(LoggingMixin): self.handle_protections(trade.pair, trade.trade_direction) return True - if trade.has_open_orders or not trade.is_open: + if 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. # The trade can be closed already (sell-order fill confirmation came in this iteration) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 080001dfc..a56e259e3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1377,8 +1377,7 @@ class Backtesting: self.wallets.update() # 4. Create exit orders (if any) - if trade.has_open_position: - self._check_trade_exit(trade, row, current_time) # Place exit order if necessary + self._check_trade_exit(trade, row, current_time) # Place exit order if necessary # 5. Process exit orders. order = trade.select_order(trade.exit_side, is_open=True) From 1fccdd8cda15eb2ada54643ceb4b16057b85ce44 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 11 Sep 2024 18:59:29 -0400 Subject: [PATCH 11/65] fix test_exit_positions --- tests/freqtradebot/test_freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index ec9d27a4d..ec934fe53 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -1265,14 +1265,14 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog trades = [trade] freqtrade.wallets.update() n = freqtrade.exit_positions(trades) - assert n == 0 + assert n == 1 # Test amount not modified by fee-logic assert not log_has_re(r"Applying fee to amount for Trade .*", caplog) gra = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", return_value=0.0) # test amount modified by fee-logic n = freqtrade.exit_positions(trades) - assert n == 0 + assert n == 1 assert gra.call_count == 0 From 2811a470aa2ca1467713bc7caebef44f1c98b7e6 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 11 Sep 2024 21:11:20 -0400 Subject: [PATCH 12/65] handle pre existing open order cancelation on trade exit --- freqtrade/freqtradebot.py | 55 +++++++++++++++++++------- tests/freqtradebot/test_integration.py | 4 +- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e23d07af9..e1d56d581 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1683,6 +1683,34 @@ 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, reason: str, sides: List[str]) -> 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 cancelation + :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 + + for side in sides: + if (order["side"] == side): + if order["side"] == trade.entry_side: + self.handle_cancel_enter( + trade, order, open_order, reason + ) + + 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 @@ -1690,22 +1718,11 @@ 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, constants.CANCEL_REASON["ALL_CANCELLED"], + [trade.entry_side, trade.exit_side] + ) - 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_cancel_enter( @@ -1951,6 +1968,14 @@ class FreqtradeBot(LoggingMixin): limit = self.get_valid_price(custom_exit_price, proposed_limit_rate) + if trade.has_open_orders: + # cancel any open order of this trade + self.cancel_open_orders_of_trade( + trade, constants.CANCEL_REASON["REPLACE"], + [trade.exit_side] + ) + Trade.commit() + # First cancelling stoploss on exchange ... trade = self.cancel_stoploss_on_exchange(trade) diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index 649bacddf..418d38d07 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -497,7 +497,9 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker, print("AFTER Process trade.orders") print(trade.orders) - assert len(trade.orders) == 5 + assert trade.orders[-2].status == "canceled" + assert len(trade.orders) == 6 + 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 From 6b8ca7217bfe5039773993f47a1249819a478a5e Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 11 Sep 2024 21:48:48 -0400 Subject: [PATCH 13/65] flake8 fix and cleanup --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/persistence/trade_model.py | 2 +- tests/freqtradebot/test_integration.py | 11 ----------- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e1d56d581..dd39eb395 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1710,7 +1710,7 @@ class FreqtradeBot(LoggingMixin): self.handle_cancel_exit( trade, order, open_order, reason ) - + def cancel_all_open_orders(self) -> None: """ Cancel all orders that are currently open @@ -1975,7 +1975,7 @@ class FreqtradeBot(LoggingMixin): [trade.exit_side] ) Trade.commit() - + # First cancelling stoploss on exchange ... trade = self.cancel_stoploss_on_exchange(trade) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 76f69793f..2746806c5 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -639,7 +639,7 @@ class LocalTrade: logger.info(f"untied_remaining: {untied_remaining}") return untied_remaining - + @property def has_untied_assets(self) -> bool: """ diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index 418d38d07..e65225e4e 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -480,23 +480,12 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker, assert pytest.approx(trade.orders[-1].amount) == 91.689215 * leverage assert freqtrade.strategy.adjust_entry_price.call_count == 0 - print("BEFORE Process trade.orders") - print(trade.orders) - - # adding this will prevent the second exit order creation, but this test case must be handled properly - #freqtrade.strategy.custom_exit = MagicMock(return_value=None) - caplog.clear() caplog.set_level(logging.DEBUG) # Process again, should not adjust entry price freqtrade.process() trade = Trade.get_trades().first() - print(f"DEBUG TEST") - print(caplog.text) - print("AFTER Process trade.orders") - print(trade.orders) - assert trade.orders[-2].status == "canceled" assert len(trade.orders) == 6 assert trade.orders[-1].side == trade.exit_side From 0d3ab37eb7366b50696858a1c2108eb453a10834 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 20 Sep 2024 18:44:58 -0400 Subject: [PATCH 14/65] fix test_trade_custom_data and test_backtest_results --- freqtrade/optimize/backtesting.py | 3 ++- tests/persistence/test_trade_custom_data.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a56e259e3..080001dfc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1377,7 +1377,8 @@ class Backtesting: self.wallets.update() # 4. Create exit orders (if any) - self._check_trade_exit(trade, row, current_time) # Place exit order if necessary + if trade.has_open_position: + self._check_trade_exit(trade, row, current_time) # Place exit order if necessary # 5. Process exit orders. order = trade.select_order(trade.exit_side, is_open=True) diff --git a/tests/persistence/test_trade_custom_data.py b/tests/persistence/test_trade_custom_data.py index b2971883d..9dbd4d220 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,10 @@ 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 unfilled open entry order + # 1 trade with filled entry order + assert ff_spy.call_count == 4 assert trade_after.exit_reason == "test_value_1" From c6d7ae9d27fe06e35ec57800b2f1e7b28898b1e1 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 20 Sep 2024 19:10:52 -0400 Subject: [PATCH 15/65] fix ruff format --- freqtrade/freqtradebot.py | 24 ++++++++---------------- freqtrade/persistence/trade_model.py | 23 +++++------------------ tests/freqtradebot/test_freqtradebot.py | 2 +- 3 files changed, 14 insertions(+), 35 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dd39eb395..fbad3596f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -720,7 +720,8 @@ class FreqtradeBot(LoggingMixin): self.check_and_call_adjust_trade_position(trade) except DependencyException as exception: logger.warning( - f"Unable to adjust position of trade for {trade.pair}: {exception}") + f"Unable to adjust position of trade for {trade.pair}: {exception}" + ) def check_and_call_adjust_trade_position(self, trade: Trade): """ @@ -1245,10 +1246,7 @@ class FreqtradeBot(LoggingMixin): """ trades_closed = 0 for trade in trades: - if ( - not trade.has_open_sl_orders - and not self.wallets.check_exit_amount(trade) - ): + if not trade.has_open_sl_orders and not self.wallets.check_exit_amount(trade): logger.warning( f"Not enough {trade.safe_base_currency} in wallet to exit {trade}. " "Trying to recover." @@ -1700,16 +1698,12 @@ class FreqtradeBot(LoggingMixin): continue for side in sides: - if (order["side"] == side): + if order["side"] == side: if order["side"] == trade.entry_side: - self.handle_cancel_enter( - trade, order, open_order, reason - ) + self.handle_cancel_enter(trade, order, open_order, reason) elif order["side"] == trade.exit_side: - self.handle_cancel_exit( - trade, order, open_order, reason - ) + self.handle_cancel_exit(trade, order, open_order, reason) def cancel_all_open_orders(self) -> None: """ @@ -1719,8 +1713,7 @@ class FreqtradeBot(LoggingMixin): for trade in Trade.get_open_trades(): self.cancel_open_orders_of_trade( - trade, constants.CANCEL_REASON["ALL_CANCELLED"], - [trade.entry_side, trade.exit_side] + trade, constants.CANCEL_REASON["ALL_CANCELLED"], [trade.entry_side, trade.exit_side] ) Trade.commit() @@ -1971,8 +1964,7 @@ class FreqtradeBot(LoggingMixin): if trade.has_open_orders: # cancel any open order of this trade self.cancel_open_orders_of_trade( - trade, constants.CANCEL_REASON["REPLACE"], - [trade.exit_side] + trade, constants.CANCEL_REASON["REPLACE"], [trade.exit_side] ) Trade.commit() diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 2746806c5..da3d1e570 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -592,8 +592,7 @@ class LocalTrade: True if there are open entry orders for this trade """ open_entry_orders = [ - o for o in self.orders - if o.ft_order_side == self.entry_side and o.ft_is_open + o for o in self.orders if o.ft_order_side == self.entry_side and o.ft_is_open ] return len(open_entry_orders) > 0 @@ -602,32 +601,20 @@ class LocalTrade: """ True if there is an open position for this trade """ - entry_orders = [ - o for o in self.orders - if o.ft_order_side == self.entry_side - ] + entry_orders = [o for o in self.orders if o.ft_order_side == self.entry_side] entry_orders_filled_qty = sum(eno.safe_filled for eno in entry_orders) - exit_orders = [ - o for o in self.orders - if o.ft_order_side == self.exit_side - ] + exit_orders = [o for o in self.orders if o.ft_order_side == self.exit_side] exit_orders_filled_qty = sum(exo.safe_filled for exo in exit_orders) return (entry_orders_filled_qty - exit_orders_filled_qty) > 0 @property def untied_assets(self) -> float: - entry_orders = [ - o for o in self.orders - if o.ft_order_side == self.entry_side - ] + entry_orders = [o for o in self.orders if o.ft_order_side == self.entry_side] entry_orders_filled_qty = sum(eno.safe_filled for eno in entry_orders) - exit_orders = [ - o for o in self.orders - if o.ft_order_side == self.exit_side - ] + exit_orders = [o for o in self.orders if o.ft_order_side == self.exit_side] exit_orders_remaining_qty = sum(exo.safe_remaining for exo in exit_orders) untied_remaining = entry_orders_filled_qty - exit_orders_remaining_qty diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index ec934fe53..2ef03ed88 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -1305,7 +1305,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 + filled=11, ) ) Trade.session.add(trade) From 418356f3836974583b36bf0e2b3fbbbe177bde1a Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 20 Sep 2024 19:23:30 -0400 Subject: [PATCH 16/65] fix codespell --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fbad3596f..bb9f594d7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1685,7 +1685,7 @@ class FreqtradeBot(LoggingMixin): """ 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 cancelation + :param reason: The reason for that cancellation :param sides: The sides where cancellation should take place :return: None """ From bb4db022f0c9995aea13a0d039b814068f68ad0a Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 24 Sep 2024 11:11:42 -0400 Subject: [PATCH 17/65] remove unused properties --- freqtrade/persistence/trade_model.py | 35 ---------------------------- 1 file changed, 35 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index da3d1e570..19f823a96 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -586,16 +586,6 @@ class LocalTrade: ] return len(open_orders_wo_sl) > 0 - @property - def has_open_entry_orders(self) -> bool: - """ - True if there are open entry orders for this trade - """ - open_entry_orders = [ - o for o in self.orders if o.ft_order_side == self.entry_side and o.ft_is_open - ] - return len(open_entry_orders) > 0 - @property def has_open_position(self) -> bool: """ @@ -609,31 +599,6 @@ class LocalTrade: return (entry_orders_filled_qty - exit_orders_filled_qty) > 0 - @property - def untied_assets(self) -> float: - entry_orders = [o for o in self.orders if o.ft_order_side == self.entry_side] - entry_orders_filled_qty = sum(eno.safe_filled for eno in entry_orders) - - exit_orders = [o for o in self.orders if o.ft_order_side == self.exit_side] - exit_orders_remaining_qty = sum(exo.safe_remaining for exo in exit_orders) - untied_remaining = entry_orders_filled_qty - exit_orders_remaining_qty - - logger.info(f"entry_orders: {entry_orders}") - logger.info(f"exit_orders: {exit_orders}") - logger.info(f"entry_orders_filled_qty: {entry_orders_filled_qty}") - logger.info(f"exit_orders_remaining_qty: {exit_orders_remaining_qty}") - - logger.info(f"untied_remaining: {untied_remaining}") - - return untied_remaining - - @property - def has_untied_assets(self) -> bool: - """ - True if there is still remaining position not yet tied up to exit order - """ - return self.untied_assets > 0 - @property def open_sl_orders(self) -> List[Order]: """ From beebc1743a5248eb0d148821c0382a430c53bd13 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 24 Sep 2024 12:17:11 -0400 Subject: [PATCH 18/65] simplify has_open_position property, restore process_open_trades_positions comment --- freqtrade/freqtradebot.py | 2 ++ freqtrade/persistence/trade_model.py | 8 +------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 511788272..d7f895f56 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -715,6 +715,8 @@ class FreqtradeBot(LoggingMixin): """ # Walk through each pair and check if it needs changes 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: # Do a wallets update (will be ratelimited to once per hour) self.wallets.update(False) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index aef34d95e..8d54458d4 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -591,13 +591,7 @@ class LocalTrade: """ True if there is an open position for this trade """ - entry_orders = [o for o in self.orders if o.ft_order_side == self.entry_side] - entry_orders_filled_qty = sum(eno.safe_filled for eno in entry_orders) - - exit_orders = [o for o in self.orders if o.ft_order_side == self.exit_side] - exit_orders_filled_qty = sum(exo.safe_filled for exo in exit_orders) - - return (entry_orders_filled_qty - exit_orders_filled_qty) > 0 + return self.amount > 0 @property def open_sl_orders(self) -> List[Order]: From bfaf9e690d655040c788b5761d016db33e6d0904 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 24 Sep 2024 12:26:36 -0400 Subject: [PATCH 19/65] add trade.has_open_position check to stoploss on exchange handling, remove deprecated comment --- freqtrade/freqtradebot.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d7f895f56..1ee826845 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1424,9 +1424,7 @@ class FreqtradeBot(LoggingMixin): self.handle_protections(trade.pair, trade.trade_direction) return True - if 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 From 9dd887f2ff4ea723b5ca4172140bfb7dcb41bbbf Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 24 Sep 2024 12:39:16 -0400 Subject: [PATCH 20/65] update exit_positions, add trade.has_open_position check before trade handling, update comment --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1ee826845..1025cd6ee 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1279,8 +1279,8 @@ class FreqtradeBot(LoggingMixin): logger.warning( f"Unable to handle stoploss on exchange for {trade.pair}: {exception}" ) - # Check if we can sell our current pair - if 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: From 5efc2a09e5f8ecfd312eae16e3dee05da8fde011 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 24 Sep 2024 13:00:59 -0400 Subject: [PATCH 21/65] restore not having open order check before trying to retreive an exchange order --- freqtrade/freqtradebot.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1025cd6ee..6370e1c23 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1257,7 +1257,11 @@ class FreqtradeBot(LoggingMixin): """ trades_closed = 0 for trade in trades: - if not trade.has_open_sl_orders and not self.wallets.check_exit_amount(trade): + if ( + not trade.has_open_orders + and not trade.has_open_sl_orders + and not self.wallets.check_exit_amount(trade) + ): logger.warning( f"Not enough {trade.safe_base_currency} in wallet to exit {trade}. " "Trying to recover." From 3fea958faf930e28606189551f38a74cf6bf51ff Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 24 Sep 2024 13:47:28 -0400 Subject: [PATCH 22/65] test cleanup --- tests/freqtradebot/test_integration.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index e65225e4e..4f4a79864 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -480,8 +480,6 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker, assert pytest.approx(trade.orders[-1].amount) == 91.689215 * leverage assert freqtrade.strategy.adjust_entry_price.call_count == 0 - caplog.clear() - caplog.set_level(logging.DEBUG) # Process again, should not adjust entry price freqtrade.process() trade = Trade.get_trades().first() From 69f0ea5199ca2e582d179816e5b62e3416f0b015 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 24 Sep 2024 13:53:16 -0400 Subject: [PATCH 23/65] remove unused import --- tests/freqtradebot/test_integration.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index 4f4a79864..2ffd9eada 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -1,4 +1,3 @@ -import logging import time from unittest.mock import MagicMock From 476960b5b7ab64a4ec1f33e110a33587bd7744a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Oct 2024 20:01:49 +0200 Subject: [PATCH 24/65] tests: have apply different fix to exit_positions test --- tests/freqtradebot/test_freqtradebot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index d1c4a9ab8..691e48a13 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -1249,7 +1249,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=[]) @@ -1281,14 +1281,14 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog trades = [trade] freqtrade.wallets.update() n = freqtrade.exit_positions(trades) - assert n == 1 + assert n == 0 # Test amount not modified by fee-logic assert not log_has_re(r"Applying fee to amount for Trade .*", caplog) gra = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", return_value=0.0) # test amount modified by fee-logic n = freqtrade.exit_positions(trades) - assert n == 1 + assert n == 0 assert gra.call_count == 0 From a194ef0b78189499675e3158c8db776c8648db89 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Oct 2024 20:04:22 +0200 Subject: [PATCH 25/65] tests: revert accidental removal of leverage test case --- tests/freqtradebot/test_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index 2ffd9eada..cf30c679b 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -347,8 +347,8 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.nr_of_successful_exits == 1 -@pytest.mark.parametrize("leverage", [1]) -def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker, caplog) -> None: +@pytest.mark.parametrize("leverage", [1, 2]) +def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) -> None: default_conf_usdt["position_adjustment_enable"] = True default_conf_usdt["trading_mode"] = "futures" default_conf_usdt["margin_mode"] = "isolated" From 584bbd93e0bf5a38b9f3c931d653d4be5a53c2cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Oct 2024 20:55:58 +0200 Subject: [PATCH 26/65] tests: fix test to expected call count --- tests/persistence/test_trade_custom_data.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/persistence/test_trade_custom_data.py b/tests/persistence/test_trade_custom_data.py index 9dbd4d220..e493276d7 100644 --- a/tests/persistence/test_trade_custom_data.py +++ b/tests/persistence/test_trade_custom_data.py @@ -87,9 +87,8 @@ def test_trade_custom_data_strategy_compat(mocker, default_conf_usdt, fee): assert trade_after.get_custom_data("test_str") == "test_value" assert trade_after.get_custom_data("test_int") == 1 # 2 trades filled entry, with open exit order - # 1 trade with unfilled open entry order # 1 trade with filled entry order - assert ff_spy.call_count == 4 + assert ff_spy.call_count == 3 assert trade_after.exit_reason == "test_value_1" From 3d6e186b300bcb8f27c2541e22560e06e113779a Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 7 Oct 2024 12:35:09 -0400 Subject: [PATCH 27/65] allow position ajustment if position open --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6370e1c23..a63286d81 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -717,7 +717,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: # Do a wallets update (will be ratelimited to once per hour) self.wallets.update(False) try: From c01996d1d11e2a6dbe1efadfe09616c401ee7c1f Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 7 Oct 2024 15:31:26 -0400 Subject: [PATCH 28/65] fix tests related to position adjustment --- tests/freqtradebot/test_freqtradebot.py | 4 ++-- tests/freqtradebot/test_integration.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index 06dbe7790..c3414897c 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -5942,13 +5942,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 == 3 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 == 3 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 cf30c679b..9003b103d 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 From acb3941776c934a2f76730f501efd68665658962 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 8 Oct 2024 00:57:06 -0400 Subject: [PATCH 29/65] fix typo for list type --- freqtrade/freqtradebot.py | 2 +- freqtrade/persistence/trade_model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 393051bc4..af7c6a9f7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1706,7 +1706,7 @@ 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, reason: str, sides: List[str]) -> None: + def cancel_open_orders_of_trade(self, trade: Trade, reason: str, sides: list[str]) -> None: """ Cancel trade orders of specified sides that are currently open :param trade: Trade object of the trade we're analyzing diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 91fd76f9a..5970e63b3 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -595,7 +595,7 @@ class LocalTrade: return self.amount > 0 @property - def open_sl_orders(self) -> List[Order]: + def open_sl_orders(self) -> list[Order]: """ All open stoploss orders for this trade """ From 43b2ec1e3f6a1f44a5365b511b45d7049eca8a7a Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 8 Oct 2024 01:31:47 -0400 Subject: [PATCH 30/65] replace existing entry order before placing new one in execute_entry --- freqtrade/freqtradebot.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index af7c6a9f7..094c93557 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -922,6 +922,14 @@ class FreqtradeBot(LoggingMixin): ): logger.info(f"User denied entry for {pair}.") return False + + if trade and trade.has_open_orders: + # cancel any open order of this trade + self.cancel_open_orders_of_trade( + trade, [trade.entry_side], constants.CANCEL_REASON["REPLACE"], True + ) + Trade.commit() + order = self.exchange.create_order( pair=pair, ordertype=order_type, @@ -1706,7 +1714,7 @@ 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, reason: str, sides: list[str]) -> None: + def cancel_open_orders_of_trade(self, trade: Trade, sides: list[str], reason: str, replacing: Optional[bool] = False) -> None: """ Cancel trade orders of specified sides that are currently open :param trade: Trade object of the trade we're analyzing @@ -1725,7 +1733,7 @@ class FreqtradeBot(LoggingMixin): for side in sides: if order["side"] == side: if order["side"] == trade.entry_side: - self.handle_cancel_enter(trade, order, open_order, reason) + 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) @@ -1738,7 +1746,7 @@ class FreqtradeBot(LoggingMixin): for trade in Trade.get_open_trades(): self.cancel_open_orders_of_trade( - trade, constants.CANCEL_REASON["ALL_CANCELLED"], [trade.entry_side, trade.exit_side] + trade, [trade.entry_side, trade.exit_side], constants.CANCEL_REASON["ALL_CANCELLED"] ) Trade.commit() @@ -1989,7 +1997,7 @@ class FreqtradeBot(LoggingMixin): if trade.has_open_orders: # cancel any open order of this trade self.cancel_open_orders_of_trade( - trade, constants.CANCEL_REASON["REPLACE"], [trade.exit_side] + trade, [trade.exit_side], constants.CANCEL_REASON["REPLACE"], True ) Trade.commit() From 95f709ae14f9211753a925ca3da63141b39122e4 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 8 Oct 2024 01:40:56 -0400 Subject: [PATCH 31/65] fix formating --- freqtrade/freqtradebot.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 094c93557..bb120923b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -922,7 +922,7 @@ class FreqtradeBot(LoggingMixin): ): logger.info(f"User denied entry for {pair}.") return False - + if trade and trade.has_open_orders: # cancel any open order of this trade self.cancel_open_orders_of_trade( @@ -1714,7 +1714,9 @@ 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: Optional[bool] = False) -> None: + def cancel_open_orders_of_trade( + self, trade: Trade, sides: list[str], reason: str, replacing: Optional[bool] = False + ) -> None: """ Cancel trade orders of specified sides that are currently open :param trade: Trade object of the trade we're analyzing From 7d5d84763fe68f2d67fe6d8ce0d264852421fde5 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 14 Oct 2024 05:12:03 -0400 Subject: [PATCH 32/65] add handling of simital open order --- freqtrade/freqtradebot.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index bb120923b..e408d439b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -923,12 +923,8 @@ class FreqtradeBot(LoggingMixin): logger.info(f"User denied entry for {pair}.") return False - if trade and trade.has_open_orders: - # cancel any open order of this trade - self.cancel_open_orders_of_trade( - trade, [trade.entry_side], constants.CANCEL_REASON["REPLACE"], True - ) - Trade.commit() + if trade: + self.handle_similar_open_order(trade, side, amount) order = self.exchange.create_order( pair=pair, @@ -1753,6 +1749,26 @@ class FreqtradeBot(LoggingMixin): Trade.commit() + def handle_similar_open_order(self, trade: Trade, side: str, amount: float) -> bool: + """ + Keep existing open order if same amount and side, otherwise cancel + :return: True if an existing similar order was cancelled + """ + if trade.has_open_orders: + oo = trade.select_order(side, True) + open_order_side = oo.side + open_order_amount = oo.amount + + if (side != open_order_side) & (amount != open_order_amount): + # cancel open order of this trade if order is diferent + self.cancel_open_orders_of_trade( + trade, [trade.entry_side], constants.CANCEL_REASON["REPLACE"], True + ) + Trade.commit() + return True + + return False + def handle_cancel_enter( self, trade: Trade, From 092fbe25b86c0748943e08dab8f1f078ca5b4104 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 14 Oct 2024 05:23:05 -0400 Subject: [PATCH 33/65] enhancement on cancel_open_orders_of_trade --- freqtrade/freqtradebot.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e408d439b..9d5b91a30 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1728,13 +1728,12 @@ class FreqtradeBot(LoggingMixin): logger.info("Can't query order for %s due to %s", trade, traceback.format_exc()) continue - for side in sides: - if order["side"] == side: - if order["side"] == trade.entry_side: - self.handle_cancel_enter(trade, order, open_order, reason, replacing) + 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) + elif order["side"] == trade.exit_side: + self.handle_cancel_exit(trade, order, open_order, reason) def cancel_all_open_orders(self) -> None: """ From 084d951ffd80b83780e0858d746c771cf17aec43 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 14 Oct 2024 05:34:01 -0400 Subject: [PATCH 34/65] mypy fix for handle_similar_open_order --- freqtrade/freqtradebot.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9d5b91a30..b92bad8fc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1755,16 +1755,17 @@ class FreqtradeBot(LoggingMixin): """ if trade.has_open_orders: oo = trade.select_order(side, True) - open_order_side = oo.side - open_order_amount = oo.amount + if oo is not None: + open_order_side = oo.side + open_order_amount = oo.amount - if (side != open_order_side) & (amount != open_order_amount): - # cancel open order of this trade if order is diferent - self.cancel_open_orders_of_trade( - trade, [trade.entry_side], constants.CANCEL_REASON["REPLACE"], True - ) - Trade.commit() - return True + if (side != open_order_side) & (amount != open_order_amount): + # cancel open order of this trade if order is diferent + self.cancel_open_orders_of_trade( + trade, [trade.entry_side], constants.CANCEL_REASON["REPLACE"], True + ) + Trade.commit() + return True return False From 4dc20effb8b9f3ba61402ad830e69a7638e549ef Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 14 Oct 2024 05:40:21 -0400 Subject: [PATCH 35/65] fix codespell --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b92bad8fc..27ff6f801 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1760,7 +1760,7 @@ class FreqtradeBot(LoggingMixin): open_order_amount = oo.amount if (side != open_order_side) & (amount != open_order_amount): - # cancel open order of this trade if order is diferent + # cancel open order of this trade if order is different self.cancel_open_orders_of_trade( trade, [trade.entry_side], constants.CANCEL_REASON["REPLACE"], True ) From 5c9ec376094c695688e353575a6c11f8294f5997 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 14 Oct 2024 05:52:38 -0400 Subject: [PATCH 36/65] abort execute_entry if similar_open_order found --- freqtrade/freqtradebot.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 27ff6f801..547d53d55 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -923,8 +923,9 @@ class FreqtradeBot(LoggingMixin): logger.info(f"User denied entry for {pair}.") return False - if trade: - self.handle_similar_open_order(trade, side, amount) + if trade and self.handle_similar_open_order(trade, side, amount): + logger.info(f"A similar open order was found for {pair}.") + return False order = self.exchange.create_order( pair=pair, @@ -1751,7 +1752,7 @@ class FreqtradeBot(LoggingMixin): def handle_similar_open_order(self, trade: Trade, side: str, amount: float) -> bool: """ Keep existing open order if same amount and side, otherwise cancel - :return: True if an existing similar order was cancelled + :return: True if an existing similar order was found """ if trade.has_open_orders: oo = trade.select_order(side, True) @@ -1759,13 +1760,15 @@ class FreqtradeBot(LoggingMixin): open_order_side = oo.side open_order_amount = oo.amount - if (side != open_order_side) & (amount != open_order_amount): + if (side == open_order_side) & (amount == open_order_amount): + return True + else: # cancel open order of this trade if order is different self.cancel_open_orders_of_trade( trade, [trade.entry_side], constants.CANCEL_REASON["REPLACE"], True ) Trade.commit() - return True + return False return False From 9b8e485f005480766b0c2981d480402feb567702 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 29 Oct 2024 21:20:44 -0400 Subject: [PATCH 37/65] fix handle_similar_open_order operator --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 547d53d55..67520af01 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1760,7 +1760,7 @@ class FreqtradeBot(LoggingMixin): open_order_side = oo.side open_order_amount = oo.amount - if (side == open_order_side) & (amount == open_order_amount): + if (side == open_order_side) and (amount == open_order_amount): return True else: # cancel open order of this trade if order is different From 7ecf1b18c157ef0e861b85636bbfcb4b61490d2f Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 29 Oct 2024 22:05:42 -0400 Subject: [PATCH 38/65] consider price in handle_similar_open_order --- freqtrade/freqtradebot.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 67520af01..2a2f9e0f7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -923,7 +923,7 @@ class FreqtradeBot(LoggingMixin): logger.info(f"User denied entry for {pair}.") return False - if trade and self.handle_similar_open_order(trade, side, amount): + if trade and self.handle_similar_open_order(trade, enter_limit_requested, amount, side): logger.info(f"A similar open order was found for {pair}.") return False @@ -1749,7 +1749,9 @@ class FreqtradeBot(LoggingMixin): Trade.commit() - def handle_similar_open_order(self, trade: Trade, side: str, amount: float) -> bool: + 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 :return: True if an existing similar order was found @@ -1757,10 +1759,7 @@ class FreqtradeBot(LoggingMixin): if trade.has_open_orders: oo = trade.select_order(side, True) if oo is not None: - open_order_side = oo.side - open_order_amount = oo.amount - - if (side == open_order_side) and (amount == open_order_amount): + if (price == oo.price) and (side == oo.side) and (amount == oo.amount): return True else: # cancel open order of this trade if order is different From c0754587bfa755bdede7ba8dae5503a3aa1957ff Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 29 Oct 2024 23:30:36 -0400 Subject: [PATCH 39/65] add handle_similar_open_order for execute_trade_exit --- freqtrade/freqtradebot.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2a2f9e0f7..9db4bfd13 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -2014,13 +2014,6 @@ class FreqtradeBot(LoggingMixin): limit = self.get_valid_price(custom_exit_price, proposed_limit_rate) - if trade.has_open_orders: - # cancel any open order of this trade - self.cancel_open_orders_of_trade( - trade, [trade.exit_side], constants.CANCEL_REASON["REPLACE"], True - ) - Trade.commit() - # First cancelling stoploss on exchange ... trade = self.cancel_stoploss_on_exchange(trade) @@ -2050,6 +2043,17 @@ 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): + logger.info(f"A similar open order was found for {trade.pair}.") + return False + + # cancel any open order of this trade + self.cancel_open_orders_of_trade( + trade, [trade.exit_side], constants.CANCEL_REASON["REPLACE"], True + ) + Trade.commit() + try: # Execute sell and update trade record order = self.exchange.create_order( From 9432865bcdae930b4881b4391eed2ad01cdb4bde Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 30 Oct 2024 14:27:33 -0400 Subject: [PATCH 40/65] updated test_dca_order_adjust: if an exit order is open, a new one with same price, amount and side can't be created. --- tests/freqtradebot/test_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index 9003b103d..3545dde02 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -483,8 +483,8 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) freqtrade.process() trade = Trade.get_trades().first() - assert trade.orders[-2].status == "canceled" - assert len(trade.orders) == 6 + 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 From 091fe6be8828de0f508c9017eb52a8cf5a1c7b7d Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 30 Oct 2024 14:38:54 -0400 Subject: [PATCH 41/65] improve handle_similar_open_order log message --- freqtrade/freqtradebot.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9db4bfd13..6532ea9dc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -924,7 +924,10 @@ class FreqtradeBot(LoggingMixin): return False if trade and self.handle_similar_open_order(trade, enter_limit_requested, amount, side): - logger.info(f"A similar open order was found for {pair}.") + logger.info( + f"A similar open order was found for {trade.pair}. \ + Keeping existing {side} order. price: {enter_limit_requested}, amount: {amount}" + ) return False order = self.exchange.create_order( @@ -2045,7 +2048,10 @@ class FreqtradeBot(LoggingMixin): if trade.has_open_orders: if self.handle_similar_open_order(trade, limit, amount, trade.exit_side): - logger.info(f"A similar open order was found for {trade.pair}.") + logger.info( + f"A similar open order was found for {trade.pair}. \ + Keeping existing {trade.exit_side} order. price: {limit}, amount: {amount}" + ) return False # cancel any open order of this trade From 12424aef69fb2817527f3b253e24b19d42cf5688 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 30 Oct 2024 15:19:29 -0400 Subject: [PATCH 42/65] improve handle_similar_open_order description --- freqtrade/freqtradebot.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6532ea9dc..b1c4dc5c7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1756,7 +1756,11 @@ class FreqtradeBot(LoggingMixin): self, trade: Trade, price: float, amount: float, side: str ) -> bool: """ - Keep existing open order if same amount and side, otherwise cancel + 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: From 0c79c32b0ddc33bb8392ee0e9a932338c88bc9f1 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 25 Nov 2024 19:25:54 -0400 Subject: [PATCH 43/65] add test_dca_handle_similar_open_order (wip) --- tests/freqtradebot/test_integration.py | 74 ++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index 3545dde02..d203b3b1d 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -689,3 +689,77 @@ 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", [2]) +def test_dca_handle_similar_open_order(default_conf_usdt, ticker_usdt, leverage, fee, mocker) -> 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) + freqtrade.strategy.custom_entry_price = lambda **kwargs: ticker_usdt["ask"] * 0.96 + freqtrade.strategy.leverage = MagicMock(return_value=leverage) + freqtrade.strategy.minimal_roi = {0: 0.2} + + + # Create trade + # entry + freqtrade.enter_positions() + + assert len(Trade.get_trades().all()) == 1 + trade: Trade = Trade.get_trades().first() + assert len(trade.orders) == 1 + assert trade.has_open_orders + + print("Debug test_dca_handle_similar_open_order") + print("P0: Original entry order created") + print(trade.orders) + # Adjust with new price + # Cancel 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 + + print("P1: Cancel order and place new one") + print(trade.orders) + + # Adjust with new amount, should cancel and replace existing order + #freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.90) + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=0.1) # -(trade.stake_amount * 0.5) + freqtrade.process() + trade = Trade.get_trades().first() + + + print("P2: Cancel order and place new one") + print(trade.orders) + + assert len(trade.orders) == 3 + + # Adjust with new side, should cancel and replace existing order + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-120) + freqtrade.process() + trade = Trade.get_trades().first() + + + # Adjust with same params, should kepp existing order as price and amount are similar + freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.99) + freqtrade.process() + trade = Trade.get_trades().first() \ No newline at end of file From a49b0a922a8123a018176724e13edcbc0c0158e8 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Thu, 28 Nov 2024 22:17:53 -0400 Subject: [PATCH 44/65] enhance check_and_call_adjust_trade_position logging --- freqtrade/freqtradebot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b1c4dc5c7..ace21fe8c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -807,7 +807,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 From 4c365f6afc71654ebe63b347b1298c7756e970cc Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Thu, 28 Nov 2024 22:25:58 -0400 Subject: [PATCH 45/65] update test_dca_handle_similar_open_order --- tests/freqtradebot/test_integration.py | 72 +++++++++++++++++--------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index d203b3b1d..1612af8ce 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -691,8 +691,11 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera assert freqtrade.wallets.get_total("USDT") == starting_amount + trade.realized_profit -@pytest.mark.parametrize("leverage", [2]) -def test_dca_handle_similar_open_order(default_conf_usdt, ticker_usdt, leverage, fee, mocker) -> None: +@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 +) -> None: default_conf_usdt["position_adjustment_enable"] = True default_conf_usdt["trading_mode"] = "futures" default_conf_usdt["margin_mode"] = "isolated" @@ -710,26 +713,21 @@ def test_dca_handle_similar_open_order(default_conf_usdt, ticker_usdt, leverage, 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) + 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.minimal_roi = {0: 0.2} - - # Create trade - # entry + # 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.has_open_orders - print("Debug test_dca_handle_similar_open_order") - print("P0: Original entry order created") - print(trade.orders) - # Adjust with new price - # Cancel order and place new one + # 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() @@ -738,28 +736,52 @@ def test_dca_handle_similar_open_order(default_conf_usdt, ticker_usdt, leverage, assert len(trade.orders) == 2 - print("P1: Cancel order and place new one") - print(trade.orders) - # Adjust with new amount, should cancel and replace existing order - #freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.90) - freqtrade.strategy.adjust_trade_position = MagicMock(return_value=0.1) # -(trade.stake_amount * 0.5) + freqtrade.strategy.adjust_trade_position = MagicMock( + return_value=21 + ) # -(trade.stake_amount * 0.5) freqtrade.process() trade = Trade.get_trades().first() - - print("P2: Cancel order and place new one") - print(trade.orders) - assert len(trade.orders) == 3 - # Adjust with new side, should cancel and replace existing order - freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-120) + # Fill entry order + mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=True) + freqtrade.process() + + # 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 - # Adjust with same params, should kepp existing order as price and amount are similar - freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.99) + # 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() \ No newline at end of file + trade = Trade.get_trades().first() + + assert trade.orders[-2].status == "canceled" + assert len(trade.orders) == 6 + + # Adjust with new exit price, should cancel and replace existing exit order + freqtrade.strategy.custom_exit_price = MagicMock(return_value=1.95) + freqtrade.process() + trade = Trade.get_trades().first() + + assert trade.orders[-2].status == "canceled" + assert len(trade.orders) == 7 + + # 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 len(trade.orders) == 7 From 0a51006da28dff56d68bc3a126128a1c8ba98033 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Thu, 28 Nov 2024 22:27:07 -0400 Subject: [PATCH 46/65] update process_open_trade_positions to handle position adjustment for initial entry order (before filling) --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ace21fe8c..a27f003c4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -729,7 +729,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 trade.has_open_position: + 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: From 8f15bce680b2ac8d3cb9a9dcfbfac6a3f3fc9a77 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 1 Dec 2024 10:32:04 -0400 Subject: [PATCH 47/65] fix test_check_and_call_adjust_trade_position --- tests/freqtradebot/test_freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index 426333287..5f79037bf 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -5941,13 +5941,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 == 3 + 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 == 3 + 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 From 02a089cb64af9ea204066552bb0344063c95cac4 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 1 Dec 2024 10:36:49 -0400 Subject: [PATCH 48/65] fix test_dca_order_adjust_entry_replace_fails --- tests/freqtradebot/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index 1612af8ce..9cf82de75 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -536,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)) From 4717f716de0814fbebcf43b84e0b038ef7503b99 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 1 Dec 2024 10:41:41 -0400 Subject: [PATCH 49/65] fix test_dca_exiting --- tests/freqtradebot/test_integration.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index 9cf82de75..1cb6238d7 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -681,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: From 73d53579d99d042fe25d812e4259405d424c7abf Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 1 Dec 2024 11:05:38 -0400 Subject: [PATCH 50/65] remove Optinal property of replacing param in cancel_open_orders_of_trade --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a3157f305..64f6b2d80 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1728,7 +1728,7 @@ class FreqtradeBot(LoggingMixin): 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: Optional[bool] = False + self, trade: Trade, sides: list[str], reason: str, replacing: bool = False ) -> None: """ Cancel trade orders of specified sides that are currently open From 16d2dbc9d78eb88afc1c4f664f040f6376136aa3 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 1 Dec 2024 11:13:59 -0400 Subject: [PATCH 51/65] on backtesting try to create exit orders also if there is open order without position yet, to allow entry order adjustment --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 06e6590eb..bfb6af4e6 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1399,7 +1399,7 @@ class Backtesting: self.wallets.update() # 4. Create exit orders (if any) - if trade.has_open_position: + if trade.has_open_position or trade.has_open_orders: self._check_trade_exit(trade, row, current_time) # Place exit order if necessary # 5. Process exit orders. From 41a548d625421120ebc444e134c1d32a7a71976f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Dec 2024 15:28:22 +0100 Subject: [PATCH 52/65] chore: move logging to "handle_similar_open_order" --- freqtrade/freqtradebot.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 64f6b2d80..f40eabffb 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -928,10 +928,6 @@ class FreqtradeBot(LoggingMixin): return False if trade and self.handle_similar_open_order(trade, enter_limit_requested, amount, side): - logger.info( - f"A similar open order was found for {trade.pair}. \ - Keeping existing {side} order. price: {enter_limit_requested}, amount: {amount}" - ) return False order = self.exchange.create_order( @@ -1780,6 +1776,10 @@ class FreqtradeBot(LoggingMixin): 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 else: # cancel open order of this trade if order is different @@ -2067,10 +2067,6 @@ class FreqtradeBot(LoggingMixin): if trade.has_open_orders: if self.handle_similar_open_order(trade, limit, amount, trade.exit_side): - logger.info( - f"A similar open order was found for {trade.pair}. \ - Keeping existing {trade.exit_side} order. price: {limit}, amount: {amount}" - ) return False # cancel any open order of this trade From 23d6c362c43d0918c82e5af3c708ea3ec44bc4e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Dec 2024 15:51:01 +0100 Subject: [PATCH 53/65] feat: cancel orders of both sides in one go --- freqtrade/freqtradebot.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f40eabffb..067210b24 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1782,9 +1782,12 @@ class FreqtradeBot(LoggingMixin): ) return True else: - # cancel open order of this trade if order is different + # cancel open orders of this trade if order is different self.cancel_open_orders_of_trade( - trade, [trade.entry_side], constants.CANCEL_REASON["REPLACE"], True + trade, + [trade.entry_side, trade.exit_side], + constants.CANCEL_REASON["REPLACE"], + True, ) Trade.commit() return False @@ -2069,12 +2072,6 @@ class FreqtradeBot(LoggingMixin): if self.handle_similar_open_order(trade, limit, amount, trade.exit_side): return False - # cancel any open order of this trade - self.cancel_open_orders_of_trade( - trade, [trade.exit_side], constants.CANCEL_REASON["REPLACE"], True - ) - Trade.commit() - try: # Execute sell and update trade record order = self.exchange.create_order( From 48506f29456282746552cf1d6a026d9bde983fd5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2024 18:03:04 +0100 Subject: [PATCH 54/65] fix: always cancel al open orders if the order ain't identical --- freqtrade/freqtradebot.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 067210b24..fd1f586b4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1781,16 +1781,15 @@ class FreqtradeBot(LoggingMixin): f"Keeping existing {trade.exit_side} order. {price=}, {amount=}" ) return True - else: - # 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 + # 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 From 99539031934970cf3c45f9427dadb76e3179e5d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2024 18:05:57 +0100 Subject: [PATCH 55/65] test: close mock order for funding-fee test --- tests/freqtradebot/test_freqtradebot.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index 937bec8e5..0af302ec5 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -5190,6 +5190,13 @@ def test_update_funding_fees( open_exit_order = limit_order_open[exit_side(is_short)] bid = 0.11 enter_rate_mock = MagicMock(return_value=bid) + open_order.update( + { + "status": "closed", + "filled": open_order["amount"], + "remaining": 0, + } + ) enter_mm = MagicMock(return_value=open_order) patch_RPCManager(mocker) patch_exchange(mocker) From d37e23f68e45993a21cf63fe144a2acdd4909780 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2024 18:07:07 +0100 Subject: [PATCH 56/65] feat: implement order replacement for backtesting --- freqtrade/optimize/backtesting.py | 41 ++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bfb6af4e6..8cccae02d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -831,6 +831,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, @@ -1116,6 +1120,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) @@ -1214,6 +1222,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: @@ -1399,7 +1438,7 @@ class Backtesting: self.wallets.update() # 4. Create exit orders (if any) - if trade.has_open_position or 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. From 48b0ffac0667f6cd6e718087e6e1c6b149927738 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2024 18:15:59 +0100 Subject: [PATCH 57/65] chore: fix using non-existing attribute --- freqtrade/optimize/backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e77e69fc8..d3b8cfce3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1230,8 +1230,8 @@ class Backtesting: 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 + # 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)] From 84e5a9521de6a4841d64267e71c01c65dcd23763 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2024 20:11:32 +0100 Subject: [PATCH 58/65] chore: improve left open trades handling --- freqtrade/optimize/backtesting.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d3b8cfce3..ba9408697 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1166,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 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 From c9aa202be8451c24bb3bb9e3373c5615ca6e41f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2024 12:37:21 +0100 Subject: [PATCH 59/65] test: add additional Test case --- tests/optimize/__init__.py | 1 + tests/optimize/test_backtest_detail.py | 44 ++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) 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 From cefacf4a6d1c63df04dd0a17a11b3dd487393e13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2024 12:51:41 +0100 Subject: [PATCH 60/65] chore: fix typo causing double negation --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ba9408697..6489aac4b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1168,7 +1168,7 @@ class Backtesting: for trade in list(open_trades[pair]): if ( trade.has_open_orders and trade.nr_of_successful_entries == 0 - ) or not not trade.has_open_position: + ) or not trade.has_open_position: # Ignore trade if entry-order did not fill yet LocalTrade.remove_bt_trade(trade) continue From f4c004347e45bcad816ffcb398baa157f96ddcbb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Dec 2024 13:47:36 +0100 Subject: [PATCH 61/65] chore: backtesting should avoid unnecessary logging --- freqtrade/optimize/backtesting.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6489aac4b..47b41d6ca 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1249,10 +1249,10 @@ class Backtesting: 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=}" - ) + # 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) From 0919d4c274292d2bfd08a4ba58e472d74d2d38e4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Dec 2024 13:56:02 +0100 Subject: [PATCH 62/65] test: improve integration test case --- tests/freqtradebot/test_integration.py | 30 +++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index 1cb6238d7..cd4158734 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -698,7 +698,7 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera @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 + 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" @@ -720,6 +720,7 @@ def test_dca_handle_similar_open_order( 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 @@ -729,7 +730,13 @@ def test_dca_handle_similar_open_order( 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) @@ -750,9 +757,17 @@ def test_dca_handle_similar_open_order( assert len(trade.orders) == 3 # 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) @@ -765,12 +780,17 @@ def test_dca_handle_similar_open_order( assert trade.orders[-1].status == "open" assert trade.orders[-1].side == trade.exit_side assert len(trade.orders) == 5 + 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 @@ -778,14 +798,22 @@ def test_dca_handle_similar_open_order( # 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 + 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 From af0d719b2c50e390993cd21f49a0c4a64f7c69b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2025 15:03:29 +0100 Subject: [PATCH 63/65] chore: prevent error exiting when an order is already open and due for cancelation --- freqtrade/freqtradebot.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fd1f586b4..e1130f1f3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1974,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 From 1a59ecf98ecfc6b533b25ba1d3cefe63b67fcc34 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 Jan 2025 07:16:24 +0100 Subject: [PATCH 64/65] docs: update callback docs for new behavior --- docs/strategy-callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 14e39a447..b34e848af 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. From c90cfa896cbb3bc5233127b4513993669c2ad055 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 Jan 2025 07:19:57 +0100 Subject: [PATCH 65/65] test: never have more than 1 open order at a time. --- tests/freqtradebot/test_integration.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index cd4158734..9fc580753 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -746,6 +746,7 @@ def test_dca_handle_similar_open_order( 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( @@ -755,6 +756,7 @@ def test_dca_handle_similar_open_order( 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 @@ -780,6 +782,7 @@ def test_dca_handle_similar_open_order( 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() @@ -794,6 +797,7 @@ def test_dca_handle_similar_open_order( 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) @@ -806,6 +810,7 @@ def test_dca_handle_similar_open_order( 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) @@ -817,3 +822,4 @@ def test_dca_handle_similar_open_order( assert log_has_re(similar_msg, caplog) assert len(trade.orders) == 7 + assert len(trade.open_orders) == 1