From 90332128b178dd49550bd0de23d9aa72d09312d6 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Tue, 5 Dec 2023 17:10:15 +0900 Subject: [PATCH 1/9] allow trade to be closed via partial exit call if remaining amount is exactly 0 --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b6ab24529..beb89251b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -685,7 +685,7 @@ class FreqtradeBot(LoggingMixin): return remaining = (trade.amount - amount) * current_exit_rate - if min_exit_stake and remaining < min_exit_stake: + if min_exit_stake and (0 < remaining < min_exit_stake): logger.info(f"Remaining amount of {remaining} would be smaller " f"than the minimum of {min_exit_stake}.") return From e541c95c46f3928532c0669d9c6032454b5fa950 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 10 Dec 2023 17:27:24 +0900 Subject: [PATCH 2/9] fix partial exit test --- tests/test_integration.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 12647f6e2..8a928ea40 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -654,24 +654,24 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-50) freqtrade.process() assert log_has_re("Adjusting amount to trade.amount as it is higher.*", caplog) - assert log_has_re("Remaining amount of 0.0 would be smaller than the minimum of 10.", caplog) + # assert log_has_re("Remaining amount of 0.0 would be smaller than the minimum of 10.", caplog) trade = Trade.get_trades().first() - assert len(trade.orders) == 2 + assert len(trade.orders) == 3 assert trade.orders[-1].ft_order_side == 'sell' assert pytest.approx(trade.stake_amount) == 40.198 - assert trade.is_open + assert trade.is_open == False # use amount that would trunc to 0.0 once selling mocker.patch(f"{EXMS}.amount_to_contract_precision", lambda s, p, v: round(v, 1)) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-0.01) freqtrade.process() trade = Trade.get_trades().first() - assert len(trade.orders) == 2 + assert len(trade.orders) == 3 assert trade.orders[-1].ft_order_side == 'sell' assert pytest.approx(trade.stake_amount) == 40.198 - assert trade.is_open + assert trade.is_open == False assert log_has_re('Amount to exit is 0.0 due to exchange limits - not exiting.', caplog) - expected_profit = starting_amount - 40.1980 + trade.realized_profit + expected_profit = starting_amount - 60 + trade.realized_profit assert pytest.approx(freqtrade.wallets.get_free('USDT')) == expected_profit if spot: assert pytest.approx(freqtrade.wallets.get_total('USDT')) == expected_profit From 9f1599b7f0ecda1a68f71ea2d018e298564294b9 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 10 Dec 2023 17:34:02 +0900 Subject: [PATCH 3/9] fix pre-commit --- tests/test_integration.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 8a928ea40..b3b17a7d2 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -654,12 +654,11 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-50) freqtrade.process() assert log_has_re("Adjusting amount to trade.amount as it is higher.*", caplog) - # assert log_has_re("Remaining amount of 0.0 would be smaller than the minimum of 10.", caplog) trade = Trade.get_trades().first() assert len(trade.orders) == 3 assert trade.orders[-1].ft_order_side == 'sell' assert pytest.approx(trade.stake_amount) == 40.198 - assert trade.is_open == False + assert trade.is_open is False # use amount that would trunc to 0.0 once selling mocker.patch(f"{EXMS}.amount_to_contract_precision", lambda s, p, v: round(v, 1)) @@ -669,7 +668,7 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera assert len(trade.orders) == 3 assert trade.orders[-1].ft_order_side == 'sell' assert pytest.approx(trade.stake_amount) == 40.198 - assert trade.is_open == False + assert trade.is_open is False assert log_has_re('Amount to exit is 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 From e96f4f0e53f5a5f5112c09138ac5d60a66d2e8e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Dec 2023 20:06:45 +0100 Subject: [PATCH 4/9] Improve adjust_trade_position documentation --- docs/strategy-callbacks.md | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 34d72a6ef..dbfa900ff 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -760,21 +760,30 @@ The `position_adjustment_enable` strategy property enables the usage of `adjust_ For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled. `adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging) or to increase or decrease positions. -`max_entry_position_adjustment` property is used to limit the number of additional entries per trade (on top of the first entry order) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment entries. - -The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional entry order should be made (position is increased -> buy order for long trades, sell order for short trades). -If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored. 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. `adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. -Additional entries are ignored once you have reached the maximum amount of extra entries that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits. - Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. -Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage. +Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applxying leverage. + +### Increase position + +The strategy is expected to return a positive stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional entry order should be made (position is increased -> buy order for long trades, sell order for short trades). + +If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored. +`max_entry_position_adjustment` property is used to limit the number of additional entries per trade (on top of the first entry order) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment entries. + +Additional entries are ignored once you have reached the maximum amount of extra entries that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits. + +### Decrease position + +The strategy is expected to return a negative stake_amount (in stake currency) for a partial exit. +Returning `-trade.stake_amount` results in a full exit. +Returning a value more than `-trade.stake_amount` (so remaining stake_amount would become negative) will result in the bot ignoring the signal. !!! Note "About stake size" Using fixed stake size means it will be the amount used for the first order, just like without position adjustment. From bb2024f78928972edc31abe476b7d1ae32cf065d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Dec 2023 20:08:03 +0100 Subject: [PATCH 5/9] Add "full partial exit" logic to backtesting --- 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 d5caf7070..ce4c1a098 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -572,7 +572,7 @@ class Backtesting: # This is currently ineffective as remaining would become < min tradable amount = trade.amount remaining = (trade.amount - amount) * current_rate - if remaining < min_stake: + if 0 < remaining < min_stake: # Remaining stake is too low to be sold. return trade exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT) From 9e2e60e7ad6c5b809598b51fdc4c3b4fb3d0189e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Dec 2023 20:34:58 +0100 Subject: [PATCH 6/9] Correct conditions for remaining stake checking --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index beb89251b..3a20bfbbe 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -685,7 +685,7 @@ class FreqtradeBot(LoggingMixin): return remaining = (trade.amount - amount) * current_exit_rate - if min_exit_stake and (0 < remaining < min_exit_stake): + if min_exit_stake and remaining != 0 and remaining < min_exit_stake: logger.info(f"Remaining amount of {remaining} would be smaller " f"than the minimum of {min_exit_stake}.") return diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ce4c1a098..791317373 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -568,11 +568,8 @@ class Backtesting: self.precision_mode, trade.contract_size) if amount == 0.0: return trade - if amount > trade.amount: - # This is currently ineffective as remaining would become < min tradable - amount = trade.amount remaining = (trade.amount - amount) * current_rate - if 0 < remaining < min_stake: + if remaining != 0 and remaining < min_stake: # Remaining stake is too low to be sold. return trade exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT) From 39b2a096abc29280a2f6242a6f39549dbf413052 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 22 Dec 2023 07:06:03 +0100 Subject: [PATCH 7/9] Ignore adjust_trade values that would invert position --- freqtrade/freqtradebot.py | 7 ------- tests/test_integration.py | 19 +++++++++++++++++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c8eafc022..b33554d1c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -673,13 +673,6 @@ class FreqtradeBot(LoggingMixin): amount = self.exchange.amount_to_contract_precision( trade.pair, abs(float(FtPrecise(stake_amount * trade.leverage) / FtPrecise(current_exit_rate)))) - if amount > trade.amount: - # This is currently ineffective as remaining would become < min tradable - # Fixing this would require checking for 0.0 there - - # if we decide that this callback is allowed to "fully exit" - logger.info( - f"Adjusting amount to trade.amount as it is higher. {amount} > {trade.amount}") - amount = trade.amount if amount == 0.0: logger.info("Amount to exit is 0.0 due to exchange limits - not exiting.") diff --git a/tests/test_integration.py b/tests/test_integration.py index 45c424170..3dcd91af0 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -650,12 +650,27 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera caplog.clear() # Sell more than what we got (we got ~20 coins left) - # First adjusts the amount to 20 - then rejects. + # Doesn't exit, as the amount is too high. freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-50) freqtrade.process() - assert log_has_re("Adjusting amount to trade.amount as it is higher.*", caplog) + trade = Trade.get_trades().first() + assert len(trade.orders) == 2 + + # Amount too low... + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-(trade.stake_amount * 0.99)) + freqtrade.process() + + trade = Trade.get_trades().first() + assert len(trade.orders) == 2 + + # Amount exactly comes out as exactly 0 + freqtrade.strategy.adjust_trade_position = MagicMock( + return_value=-(trade.amount / trade.leverage * 2.02)) + freqtrade.process() + trade = Trade.get_trades().first() assert len(trade.orders) == 3 + assert trade.orders[-1].ft_order_side == 'sell' assert pytest.approx(trade.stake_amount) == 40.198 assert trade.is_open is False From 063b55d41a64e34148e56954b70e71829ff17630 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Dec 2023 18:32:33 +0100 Subject: [PATCH 8/9] Fix doc typo --- docs/strategy-callbacks.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index dbfa900ff..c9cb4a0b2 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -768,13 +768,13 @@ This callback is **not** called when there is an open order (either buy or sell) Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. -Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applxying leverage. +Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage. ### Increase position -The strategy is expected to return a positive stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional entry order should be made (position is increased -> buy order for long trades, sell order for short trades). +The strategy is expected to return a positive **stake_amount** (in stake currency) between `min_stake` and `max_stake` if and when an additional entry order should be made (position is increased -> buy order for long trades, sell order for short trades). -If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored. +If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored. `max_entry_position_adjustment` property is used to limit the number of additional entries per trade (on top of the first entry order) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment entries. Additional entries are ignored once you have reached the maximum amount of extra entries that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits. @@ -782,8 +782,8 @@ Additional entries are ignored once you have reached the maximum amount of extra ### Decrease position The strategy is expected to return a negative stake_amount (in stake currency) for a partial exit. -Returning `-trade.stake_amount` results in a full exit. -Returning a value more than `-trade.stake_amount` (so remaining stake_amount would become negative) will result in the bot ignoring the signal. +Returning the full owned stake at that point (based on the current price) (`-(trade.amount / trade.leverage) * current_exit_rate`) results in a full exit. +Returning a value more than the above (so remaining stake_amount would become negative) will result in the bot ignoring the signal. !!! Note "About stake size" Using fixed stake size means it will be the amount used for the first order, just like without position adjustment. From e664527da6ba5e800609480d9f28dd425f8743d2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Dec 2023 19:31:54 +0100 Subject: [PATCH 9/9] Align backtest and bot method --- 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 853c2c14d..915abd469 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -530,7 +530,7 @@ class Backtesting: def _get_adjust_trade_entry_for_candle( self, trade: LocalTrade, row: Tuple, current_time: datetime ) -> LocalTrade: - current_rate = row[OPEN_IDX] + current_rate: float = row[OPEN_IDX] current_profit = trade.calc_profit_ratio(current_rate) min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1) max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate) @@ -564,7 +564,7 @@ class Backtesting: if amount == 0.0: return trade remaining = (trade.amount - amount) * current_rate - if remaining != 0 and remaining < min_stake: + if min_stake and remaining != 0 and remaining < min_stake: # Remaining stake is too low to be sold. return trade exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT)