From 7735ea91bb9cb117c719e13bab40cb154da63e6a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Mar 2025 20:32:34 +0100 Subject: [PATCH 1/5] fix: adjust_order_price return type --- docs/strategy-callbacks.md | 4 ++-- freqtrade/strategy/interface.py | 12 ++++++------ .../strategy_methods_advanced.j2 | 5 ++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 8286ee9bf42..fb8b8070dda 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -976,7 +976,7 @@ class AwesomeStrategy(IStrategy): side: str, is_entry: bool, **kwargs, - ) -> float: + ) -> float | None: """ Exit and entry order price re-adjustment logic, returning the user desired limit price. This only executes when a order was already placed, still open (unfilled fully or partially) @@ -998,7 +998,7 @@ class AwesomeStrategy(IStrategy): :param side: 'long' or 'short' - indicating the direction of the proposed trade :param is_entry: True if the order is an entry order, False if it's an exit order. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: New entry price value if provided + :return float or None: New entry price value if provided """ # Limit entry orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 79ea094c973..98e02cdd63f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -664,7 +664,7 @@ def adjust_entry_price( entry_tag: str | None, side: str, **kwargs, - ) -> float: + ) -> float | None: """ Entry price re-adjustment logic, returning the user desired limit price. This only executes when a order was already placed, still open (unfilled fully or partially) @@ -685,7 +685,7 @@ def adjust_entry_price( :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param side: 'long' or 'short' - indicating the direction of the proposed trade :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: New entry price value if provided + :return float or None: New entry price value if provided """ return current_order_rate @@ -701,7 +701,7 @@ def adjust_exit_price( entry_tag: str | None, side: str, **kwargs, - ) -> float: + ) -> float | None: """ Exit price re-adjustment logic, returning the user desired limit price. This only executes when a order was already placed, still open (unfilled fully or partially) @@ -722,7 +722,7 @@ def adjust_exit_price( :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param side: 'long' or 'short' - indicating the direction of the proposed trade :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: New entry price value if provided + :return float or None: New exit price value if provided """ return current_order_rate @@ -739,7 +739,7 @@ def adjust_order_price( side: str, is_entry: bool, **kwargs, - ) -> float: + ) -> float | None: """ Exit and entry order price re-adjustment logic, returning the user desired limit price. This only executes when a order was already placed, still open (unfilled fully or partially) @@ -761,7 +761,7 @@ def adjust_order_price( :param side: 'long' or 'short' - indicating the direction of the proposed trade :param is_entry: True if the order is an entry order, False if it's an exit order. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: New entry price value if provided + :return float or None: New entry price value if provided """ if is_entry: return self.adjust_entry_price( diff --git a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 index 5ff48324371..dc133c75a2f 100644 --- a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 @@ -52,7 +52,7 @@ def adjust_order_price( side: str, is_entry: bool, **kwargs, -) -> float: +) -> float | None: """ Exit and entry order price re-adjustment logic, returning the user desired limit price. This only executes when a order was already placed, still open (unfilled fully or partially) @@ -74,8 +74,7 @@ def adjust_order_price( :param side: 'long' or 'short' - indicating the direction of the proposed trade :param is_entry: True if the order is an entry order, False if it's an exit order. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: New entry price value if provided - + :return float or None: New entry price value if provided """ return current_order_rate From 02b0f0abd6a053c762c8bf0e01ae79cbcb612798 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Mar 2025 20:35:17 +0100 Subject: [PATCH 2/5] refactor: extract replace_order handling --- freqtrade/freqtradebot.py | 100 +++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 39 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7bfeab592d7..4156b50e17a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -789,6 +789,7 @@ def check_and_call_adjust_trade_position(self, trade: Trade): return else: logger.debug("Max adjustment entries is set to unlimited.") + self.execute_entry( trade.pair, stake_amount, @@ -1711,47 +1712,68 @@ def replace_order(self, order: CcxtOrder, order_obj: Order | None, trade: Trade) cancel_reason = constants.CANCEL_REASON["USER_CANCEL"] if order_obj.safe_placement_price != adjusted_price: - # cancel existing order if new price is supplied or None - res = self.handle_cancel_order( - order, order_obj, trade, cancel_reason, replacing=replacing + self.handle_replace_order( + order, + order_obj, + trade, + adjusted_price, + is_entry, + cancel_reason, + replacing=replacing, ) - if not res: - self.replace_order_failed( - trade, f"Could not fully cancel order for {trade}, therefore not replacing." + + def handle_replace_order( + self, + order: CcxtOrder | None, + order_obj: Order, + trade: Trade, + new_order_price: float | None, + is_entry: bool, + cancel_reason: str, + replacing: bool = False, + ) -> None: + """ + Cancel existing order if new price is supplied, and if the cancel is successful, + places a new order with the remaining capital. + """ + if not order: + order = self.exchange.fetch_order(order_obj.order_id, trade.pair) + res = self.handle_cancel_order(order, order_obj, trade, cancel_reason, replacing=replacing) + if not res: + self.replace_order_failed( + trade, f"Could not fully cancel order for {trade}, therefore not replacing." + ) + return + if new_order_price: + # place new order only if new price is supplied + try: + if is_entry: + succeeded = self.execute_entry( + pair=trade.pair, + stake_amount=( + order_obj.safe_remaining * order_obj.safe_price / trade.leverage + ), + price=new_order_price, + trade=trade, + is_short=trade.is_short, + mode="replace", ) - return - if adjusted_price: - # place new order only if new price is supplied - try: - if is_entry: - succeeded = self.execute_entry( - pair=trade.pair, - stake_amount=( - order_obj.safe_remaining * order_obj.safe_price / trade.leverage - ), - price=adjusted_price, - trade=trade, - is_short=trade.is_short, - mode="replace", - ) - else: - succeeded = self.execute_trade_exit( - trade, - adjusted_price, - exit_check=ExitCheckTuple( - exit_type=ExitType.CUSTOM_EXIT, - exit_reason=order_obj.ft_order_tag or "order_replaced", - ), - ordertype="limit", - sub_trade_amt=order_obj.safe_remaining, - ) - if not succeeded: - self.replace_order_failed( - trade, f"Could not replace order for {trade}." - ) - except DependencyException as exception: - logger.warning(f"Unable to replace order for {trade.pair}: {exception}") - self.replace_order_failed(trade, f"Could not replace order for {trade}.") + else: + succeeded = self.execute_trade_exit( + trade, + new_order_price, + exit_check=ExitCheckTuple( + exit_type=ExitType.CUSTOM_EXIT, + exit_reason=order_obj.ft_order_tag or "order_replaced", + ), + ordertype="limit", + sub_trade_amt=order_obj.safe_remaining, + ) + if not succeeded: + self.replace_order_failed(trade, f"Could not replace order for {trade}.") + except DependencyException as exception: + logger.warning(f"Unable to replace order for {trade.pair}: {exception}") + self.replace_order_failed(trade, f"Could not replace order for {trade}.") def cancel_open_orders_of_trade( self, trade: Trade, sides: list[str], reason: str, replacing: bool = False From e3e924d88835e97d225252a34086e8dd0f2a2cd2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Mar 2025 06:40:20 +0100 Subject: [PATCH 3/5] docs: minor update to realign summary metric docs --- docs/backtesting.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backtesting.md b/docs/backtesting.md index d72c5468615..981f889a270 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -209,6 +209,7 @@ A backtesting result will look like that: | Sortino | 1.88 | | Sharpe | 2.97 | | Calmar | 6.29 | +| SQN | 2.45 | | Profit factor | 1.11 | | Expectancy (Ratio) | -0.15 (-0.05) | | Avg. stake amount | 0.001 BTC | From 5ea7ba6b9a51c287085e7c7b90fa459126c82d82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Mar 2025 07:05:21 +0100 Subject: [PATCH 4/5] docs: improve adjust_trade_position docs further part of #11461 --- docs/strategy-callbacks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index fb8b8070dda..fb2d42fbcaa 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 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. +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. Also partially filled orders will be canceled, and will be replaced with the new amount as returned by the callback. `adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. @@ -770,9 +770,9 @@ Modifications to leverage are not possible, and the stake-amount returned is ass The combined stake currently allocated to the position is held in `trade.stake_amount`. Therefore `trade.stake_amount` will always be updated on every additional entry and partial exit made through `adjust_trade_position()`. !!! Danger "Loose Logic" - On dry and live run, this function will be called every `throttle_process_secs` (default to 5s). If you have a loose logic, for example your logic for extra entry is only to check RSI of last candle is below 30, then when such condition fulfilled, your bot will do extra re-entry every 5 secs until either it run out of money, it hit the `max_position_adjustment` limit, or a new candle with RSI more than 30 arrived. + On dry and live run, this function will be called every `throttle_process_secs` (default to 5s). If you have a loose logic, (e.g. increase position if RSI of the last candle is below 30), your bot will do extra re-entry every 5 secs until you either it run out of money, hit the `max_position_adjustment` limit, or a new candle with RSI more than 30 arrived. - Same thing also can happen with partial exit. So be sure to have a strict logic and/or check for the last filled order. + Same thing also can happen with partial exit. So be sure to have a strict logic and/or check for the last filled order and if an order is already open. !!! Warning "Performance with many position adjustments" Position adjustments can be a good approach to increase a strategy's output - but it can also have drawbacks if using this feature extensively. From 8e2de9ef7eae01cecc9d436a1c5cb28d65d08588 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Mar 2025 07:11:08 +0100 Subject: [PATCH 5/5] docs: improve adjust_trade_position formatting --- docs/strategy-callbacks.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index fb2d42fbcaa..4564a0f0303 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -772,7 +772,8 @@ The combined stake currently allocated to the position is held in `trade.stake_a !!! Danger "Loose Logic" On dry and live run, this function will be called every `throttle_process_secs` (default to 5s). If you have a loose logic, (e.g. increase position if RSI of the last candle is below 30), your bot will do extra re-entry every 5 secs until you either it run out of money, hit the `max_position_adjustment` limit, or a new candle with RSI more than 30 arrived. - Same thing also can happen with partial exit. So be sure to have a strict logic and/or check for the last filled order and if an order is already open. + Same thing also can happen with partial exit. + So be sure to have a strict logic and/or check for the last filled order and if an order is already open. !!! Warning "Performance with many position adjustments" Position adjustments can be a good approach to increase a strategy's output - but it can also have drawbacks if using this feature extensively.