From 0d060f7c96935364c3692c38d6758a8104265de3 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 29 Jul 2026 17:09:09 +0200 Subject: [PATCH] fix(quickadapter): avoid duplicate backtest exit reserve (#152) 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 --- .../user_data/strategies/QuickAdapterV3.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py index 113c7de..45e5be9 100644 --- a/quickadapter/user_data/strategies/QuickAdapterV3.py +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -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, -- 2.53.0