]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
fix(quickadapter): avoid duplicate backtest exit reserve (#152)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Wed, 29 Jul 2026 15:09:09 +0000 (17:09 +0200)
committerGitHub <noreply@github.com>
Wed, 29 Jul 2026 15:09:09 +0000 (17:09 +0200)
The partial-exit minimum bound previously multiplied `min_stake` by the
stoploss/rate factor `max(current_exit_rate/current_entry_rate,
1/(1-|stoploss|))` in every runmode. In backtest/hyperopt, Freqtrade already
passes the adjusted minimum it guards the remainder against, so that factor
duplicated the stoploss reserve. Apply the factor only in live/dry-run
(is_trade_runmode == TRADE_MODES = {LIVE, DRY_RUN}), where the callback supplies
min_entry_stake (stoploss 0.0) but the guard uses the larger min_exit_stake;
keep the 0.1% numerical clearance in both modes. Live/dry-run behavior is
bit-for-bit unchanged. Freqtrade remains responsible for amount precision and
final pre-order minimum validation.

Also rename the local bound to `min_remaining_position_value` (it is the minimum
acceptable remaining position value, not an exit stake).

Fixes #138

quickadapter/user_data/strategies/QuickAdapterV3.py

index 113c7de5539551612f4d81b6ba2d60a79454121e..45e5be9ce68e7f3a7a8c2c0f7aa4a8d2da1bb306 100644 (file)
@@ -1681,34 +1681,35 @@ class QuickAdapterV3(IStrategy):
             trade_partial_stake_amount = trade_stake_percent * trade.stake_amount
             if min_stake is not None and min_stake > 0:
                 current_position_value = trade.amount * current_exit_rate
-                # min_stake is freqtrade's min_entry_stake, but its exit guard uses
-                # the larger min_exit_stake. For both the cost- and amount-driven
-                # minimum, min_exit_stake <= min_stake * max(exit/entry, 1/(1-|sl|)),
-                # so this upper bound keeps the shrunk remainder above the guard.
-                min_exit_stake_bound = (
-                    min_stake
-                    * max(
+                # Live/dry-run passes min_entry_stake, while freqtrade's
+                # backtesting path already passes the adjusted minimum it guards.
+                min_remaining_position_value = min_stake
+                if self.is_trade_runmode:
+                    # For both the cost- and amount-driven minimum, min_exit_stake
+                    # <= min_stake * max(exit/entry, 1/(1-|sl|)).
+                    min_remaining_position_value *= max(
                         current_exit_rate / current_entry_rate,
                         1.0 / (1.0 - abs(self.stoploss)),
                     )
-                    * (1.0 + QuickAdapterV3._PARTIAL_EXIT_MIN_STAKE_MARGIN)
+                min_remaining_position_value *= (
+                    1.0 + QuickAdapterV3._PARTIAL_EXIT_MIN_STAKE_MARGIN
                 )
-                if current_position_value <= min_exit_stake_bound:
+                if current_position_value <= min_remaining_position_value:
                     return None
                 remaining_position_value = current_position_value * (
                     1 - trade_stake_percent
                 )
-                if remaining_position_value < min_exit_stake_bound:
+                if remaining_position_value < min_remaining_position_value:
                     initial_trade_partial_stake_amount = trade_partial_stake_amount
                     trade_partial_stake_amount = trade.stake_amount * (
-                        1 - min_exit_stake_bound / current_position_value
+                        1 - min_remaining_position_value / current_position_value
                     )
                     logger.info(
                         f"[{pair}] Trade {trade.trade_direction} stage "
                         f"{trade_exit_stage} | partial stake "
                         f"{format_number(initial_trade_partial_stake_amount)} -> "
                         f"{format_number(trade_partial_stake_amount)} to preserve "
-                        f"min_exit_stake_bound {format_number(min_exit_stake_bound)}"
+                        f"min_remaining_position_value {format_number(min_remaining_position_value)}"
                     )
             return (
                 -trade_partial_stake_amount,