From 5c9455c32aef9ddd454eb3ea553a698021f2dc83 Mon Sep 17 00:00:00 2001 From: ABS <53243996+ABSllk@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:31:43 +0800 Subject: [PATCH 01/15] Refactor: update IProtection interface to support starting_balance --- freqtrade/freqtradebot.py | 6 ++++-- freqtrade/plugins/protectionmanager.py | 15 +++++++++++---- freqtrade/plugins/protections/cooldown_period.py | 6 ++++-- freqtrade/plugins/protections/iprotection.py | 6 ++++-- freqtrade/plugins/protections/low_profit_pairs.py | 6 ++++-- freqtrade/plugins/protections/stoploss_guard.py | 6 ++++-- 6 files changed, 31 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b80442ca0..c6e676df8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -2421,7 +2421,9 @@ class FreqtradeBot(LoggingMixin): def handle_protections(self, pair: str, side: LongShort) -> None: # Lock pair for one candle to prevent immediate re-entries self.strategy.lock_pair(pair, datetime.now(UTC), reason="Auto lock", side=side) - prot_trig = self.protections.stop_per_pair(pair, side=side) + starting_balance = self.wallets.get_starting_balance() + prot_trig = self.protections.stop_per_pair( + pair, side=side, starting_balance=starting_balance) if prot_trig: msg: RPCProtectionMsg = { "type": RPCMessageType.PROTECTION_TRIGGER, @@ -2430,7 +2432,7 @@ class FreqtradeBot(LoggingMixin): } self.rpc.send_msg(msg) - prot_trig_glb = self.protections.global_stop(side=side) + prot_trig_glb = self.protections.global_stop(side=side, starting_balance=starting_balance) if prot_trig_glb: msg = { "type": RPCMessageType.PROTECTION_TRIGGER_GLOBAL, diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index 0eda8422f..12732b86d 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -47,13 +47,17 @@ class ProtectionManager: """ return [{p.name: p.short_desc()} for p in self._protection_handlers] - def global_stop(self, now: datetime | None = None, side: LongShort = "long") -> PairLock | None: + def global_stop( + self, now: datetime | None = None, side: LongShort = "long", starting_balance: float = 0.0 + ) -> PairLock | None: if not now: now = datetime.now(UTC) result = None for protection_handler in self._protection_handlers: if protection_handler.has_global_stop: - lock = protection_handler.global_stop(date_now=now, side=side) + lock = protection_handler.global_stop( + date_now=now, side=side, starting_balance=starting_balance + ) if lock and lock.until: if not PairLocks.is_global_lock(lock.until, side=lock.lock_side): result = PairLocks.lock_pair( @@ -62,14 +66,17 @@ class ProtectionManager: return result def stop_per_pair( - self, pair, now: datetime | None = None, side: LongShort = "long" + self, pair, now: datetime | None = None, side: LongShort = "long", + starting_balance: float = 0.0 ) -> PairLock | None: if not now: now = datetime.now(UTC) result = None for protection_handler in self._protection_handlers: if protection_handler.has_local_stop: - lock = protection_handler.stop_per_pair(pair=pair, date_now=now, side=side) + lock = protection_handler.stop_per_pair( + pair=pair, date_now=now, side=side, starting_balance=starting_balance + ) if lock and lock.until: if not PairLocks.is_pair_locked(pair, lock.until, lock.lock_side): result = PairLocks.lock_pair( diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 0f6aaa79a..40c7b1aee 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -52,7 +52,9 @@ class CooldownPeriod(IProtection): return None - def global_stop(self, date_now: datetime, side: LongShort) -> ProtectionReturn | None: + def global_stop( + self, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + ) -> ProtectionReturn | None: """ Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". @@ -63,7 +65,7 @@ class CooldownPeriod(IProtection): return None def stop_per_pair( - self, pair: str, date_now: datetime, side: LongShort + self, pair: str, date_now: datetime, side: LongShort, starting_balance: float = 0.0 ) -> ProtectionReturn | None: """ Stops trading (position entering) for this pair diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 8f2f51729..35c5e02bc 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -102,7 +102,9 @@ class IProtection(LoggingMixin, ABC): """ @abstractmethod - def global_stop(self, date_now: datetime, side: LongShort) -> ProtectionReturn | None: + def global_stop( + self, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + ) -> ProtectionReturn | None: """ Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". @@ -110,7 +112,7 @@ class IProtection(LoggingMixin, ABC): @abstractmethod def stop_per_pair( - self, pair: str, date_now: datetime, side: LongShort + self, pair: str, date_now: datetime, side: LongShort, starting_balance: float = 0.0 ) -> ProtectionReturn | None: """ Stops trading (position entering) for this pair diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 84934f394..6e4ee6942 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -81,7 +81,9 @@ class LowProfitPairs(IProtection): return None - def global_stop(self, date_now: datetime, side: LongShort) -> ProtectionReturn | None: + def global_stop( + self, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + ) -> ProtectionReturn | None: """ Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". @@ -91,7 +93,7 @@ class LowProfitPairs(IProtection): return None def stop_per_pair( - self, pair: str, date_now: datetime, side: LongShort + self, pair: str, date_now: datetime, side: LongShort, starting_balance: float = 0.0 ) -> ProtectionReturn | None: """ Stops trading (position entering) for this pair diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index a429a2f80..d7b267b5c 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -86,7 +86,9 @@ class StoplossGuard(IProtection): lock_side=(side if self._only_per_side else "*"), ) - def global_stop(self, date_now: datetime, side: LongShort) -> ProtectionReturn | None: + def global_stop( + self, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + ) -> ProtectionReturn | None: """ Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". @@ -98,7 +100,7 @@ class StoplossGuard(IProtection): return self._stoploss_guard(date_now, None, side) def stop_per_pair( - self, pair: str, date_now: datetime, side: LongShort + self, pair: str, date_now: datetime, side: LongShort, starting_balance: float = 0.0 ) -> ProtectionReturn | None: """ Stops trading (position entering) for this pair From 38934ddda283696e7c90ac749a72fc768cad9bfd Mon Sep 17 00:00:00 2001 From: ABS <53243996+ABSllk@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:32:02 +0800 Subject: [PATCH 02/15] Feat: implement accurate drawdown calculation in MaxDrawdown protection --- .../protections/max_drawdown_protection.py | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index e8996d3a8..3b2e64731 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -1,5 +1,5 @@ import logging -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any import pandas as pd @@ -42,25 +42,47 @@ class MaxDrawdown(IProtection): f"locking {self.unlock_reason_time_element}." ) - def _max_drawdown(self, date_now: datetime) -> ProtectionReturn | None: + def _max_drawdown(self, date_now: datetime, starting_balance: float) -> ProtectionReturn | None: """ Evaluate recent trades for drawdown ... """ look_back_until = date_now - timedelta(minutes=self._lookback_period) - trades = Trade.get_trades_proxy(is_open=False, close_date=look_back_until) + # Get all closed trades to calculate balance at the start of the window + all_closed_trades = Trade.get_trades_proxy(is_open=False) - trades_df = pd.DataFrame([trade.to_json() for trade in trades]) + trades_in_window = [] + profit_before_window = 0.0 + for trade in all_closed_trades: + if trade.close_date: + # Ensure close_date is aware for comparison + close_date = (trade.close_date.replace(tzinfo=UTC) + if trade.close_date.tzinfo is None else trade.close_date) + if close_date > look_back_until: + trades_in_window.append(trade) + else: + profit_before_window += (trade.close_profit_abs or 0.0) - if len(trades) < self._trade_limit: + if len(trades_in_window) < self._trade_limit: # Not enough trades in the relevant period return None + # Calculate actual balance at the start of the lookback window + actual_starting_balance = starting_balance + profit_before_window + + trades_df = pd.DataFrame([trade.to_json() for trade in trades_in_window]) + # Drawdown is always positive try: - # TODO: This should use absolute profit calculation, considering account balance. - drawdown_obj = calculate_max_drawdown(trades_df, value_col="close_profit") - drawdown = drawdown_obj.drawdown_abs + # Use absolute profit calculation with the actual balance at window start. + drawdown_obj = calculate_max_drawdown( + trades_df, + value_col="profit_abs", + starting_balance=actual_starting_balance, + relative=True + ) + # Use relative drawdown to compare against max_allowed_drawdown percentage + drawdown = drawdown_obj.relative_account_drawdown except ValueError: return None @@ -71,7 +93,7 @@ class MaxDrawdown(IProtection): logger.info, ) - until = self.calculate_lock_end(trades) + until = self.calculate_lock_end(trades_in_window) return ProtectionReturn( lock=True, @@ -81,17 +103,19 @@ class MaxDrawdown(IProtection): return None - def global_stop(self, date_now: datetime, side: LongShort) -> ProtectionReturn | None: + def global_stop( + self, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + ) -> ProtectionReturn | None: """ Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". :return: Tuple of [bool, until, reason]. If true, all pairs will be locked with until """ - return self._max_drawdown(date_now) + return self._max_drawdown(date_now, starting_balance) def stop_per_pair( - self, pair: str, date_now: datetime, side: LongShort + self, pair: str, date_now: datetime, side: LongShort, starting_balance: float = 0.0 ) -> ProtectionReturn | None: """ Stops trading (position entering) for this pair From edd33ab77e940ec32462d93fab31c40e5b147ab5 Mon Sep 17 00:00:00 2001 From: ABS <53243996+ABSllk@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:32:18 +0800 Subject: [PATCH 03/15] Test: update MaxDrawdown tests to match new equity-based logic --- tests/plugins/test_protections.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 164fdbb08..2814252fa 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -670,9 +670,10 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): ] freqtrade = get_patched_freqtradebot(mocker, default_conf) message = r"Trading stopped due to Max.*" + starting_balance = 0.05 - assert not freqtrade.protections.global_stop() - assert not freqtrade.protections.stop_per_pair("XRP/BTC") + assert not freqtrade.protections.global_stop(starting_balance=starting_balance) + assert not freqtrade.protections.stop_per_pair("XRP/BTC", starting_balance=starting_balance) caplog.clear() generate_mock_trade( @@ -704,8 +705,8 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): ) Trade.commit() # No losing trade yet ... so max_drawdown will raise exception - assert not freqtrade.protections.global_stop() - assert not freqtrade.protections.stop_per_pair("XRP/BTC") + assert not freqtrade.protections.global_stop(starting_balance=starting_balance) + assert not freqtrade.protections.stop_per_pair("XRP/BTC", starting_balance=starting_balance) generate_mock_trade( "XRP/BTC", @@ -717,8 +718,8 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): profit_rate=0.9, ) # Not locked with one trade - assert not freqtrade.protections.global_stop() - assert not freqtrade.protections.stop_per_pair("XRP/BTC") + assert not freqtrade.protections.global_stop(starting_balance=starting_balance) + assert not freqtrade.protections.stop_per_pair("XRP/BTC", starting_balance=starting_balance) assert not PairLocks.is_pair_locked("XRP/BTC") assert not PairLocks.is_global_lock() @@ -734,8 +735,8 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): Trade.commit() # Not locked with 1 trade (2nd trade is outside of lookback_period) - assert not freqtrade.protections.global_stop() - assert not freqtrade.protections.stop_per_pair("XRP/BTC") + assert not freqtrade.protections.global_stop(starting_balance=starting_balance) + assert not freqtrade.protections.stop_per_pair("XRP/BTC", starting_balance=starting_balance) assert not PairLocks.is_pair_locked("XRP/BTC") assert not PairLocks.is_global_lock() assert not log_has_re(message, caplog) @@ -751,7 +752,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): profit_rate=1.5, ) Trade.commit() - assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.global_stop(starting_balance=starting_balance) assert not PairLocks.is_global_lock() caplog.clear() @@ -764,13 +765,13 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): exit_reason=ExitType.ROI.value, min_ago_open=20, min_ago_close=10, - profit_rate=0.8, + profit_rate=0.2, ) Trade.commit() - assert not freqtrade.protections.stop_per_pair("XRP/BTC") + assert not freqtrade.protections.stop_per_pair("XRP/BTC", starting_balance=starting_balance) # local lock not supported assert not PairLocks.is_pair_locked("XRP/BTC") - assert freqtrade.protections.global_stop() + assert freqtrade.protections.global_stop(starting_balance=starting_balance) assert PairLocks.is_global_lock() assert log_has_re(message, caplog) From dbaece94623c66fa2b6917e58ad65ab303ea87be Mon Sep 17 00:00:00 2001 From: ABS <53243996+ABSllk@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:43:39 +0800 Subject: [PATCH 04/15] docs: update MaxDrawdown description to reflect equity-based logic --- docs/includes/protections.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index c32846165..23c7d9654 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -69,7 +69,8 @@ def protections(self): #### MaxDrawdown -`MaxDrawdown` uses all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after the last trade - assuming that the bot needs some time to let markets recover. +`MaxDrawdown` calculates the maximum relative drawdown using the account's equity curve within the `lookback_period` in minutes (or in candles when using `lookback_period_candles`). +It evaluates the portfolio's peak-to-trough declines by considering the starting balance and the cumulative profit of all trades within the window. If the observed drawdown exceeds `max_allowed_drawdown`, trading will stop for `stop_duration` after the last trade - assuming that the bot needs some time to let markets recover. The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used. From 5c737d977d7ed52ec707ea106de2a3aa2b86637a Mon Sep 17 00:00:00 2001 From: ABS <53243996+ABSllk@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:18:22 +0800 Subject: [PATCH 05/15] refactor: optimize performance and remove redundant logic in protections --- freqtrade/freqtradebot.py | 3 +- freqtrade/plugins/protectionmanager.py | 7 +- .../plugins/protections/cooldown_period.py | 4 +- freqtrade/plugins/protections/iprotection.py | 4 +- .../plugins/protections/low_profit_pairs.py | 4 +- .../protections/max_drawdown_protection.py | 71 +++++++++++-------- .../plugins/protections/stoploss_guard.py | 4 +- 7 files changed, 55 insertions(+), 42 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c6e676df8..bfb9a1841 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -2423,7 +2423,8 @@ class FreqtradeBot(LoggingMixin): self.strategy.lock_pair(pair, datetime.now(UTC), reason="Auto lock", side=side) starting_balance = self.wallets.get_starting_balance() prot_trig = self.protections.stop_per_pair( - pair, side=side, starting_balance=starting_balance) + pair, side=side, starting_balance=starting_balance + ) if prot_trig: msg: RPCProtectionMsg = { "type": RPCMessageType.PROTECTION_TRIGGER, diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index 12732b86d..e90e5eac2 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -66,8 +66,11 @@ class ProtectionManager: return result def stop_per_pair( - self, pair, now: datetime | None = None, side: LongShort = "long", - starting_balance: float = 0.0 + self, + pair, + now: datetime | None = None, + side: LongShort = "long", + starting_balance: float = 0.0, ) -> PairLock | None: if not now: now = datetime.now(UTC) diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 40c7b1aee..a28bb14b4 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -53,7 +53,7 @@ class CooldownPeriod(IProtection): return None def global_stop( - self, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + self, date_now: datetime, side: LongShort, starting_balance: float ) -> ProtectionReturn | None: """ Stops trading (position entering) for all pairs @@ -65,7 +65,7 @@ class CooldownPeriod(IProtection): return None def stop_per_pair( - self, pair: str, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + self, pair: str, date_now: datetime, side: LongShort, starting_balance: float ) -> ProtectionReturn | None: """ Stops trading (position entering) for this pair diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 35c5e02bc..0722e8b6e 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -103,7 +103,7 @@ class IProtection(LoggingMixin, ABC): @abstractmethod def global_stop( - self, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + self, date_now: datetime, side: LongShort, starting_balance: float ) -> ProtectionReturn | None: """ Stops trading (position entering) for all pairs @@ -112,7 +112,7 @@ class IProtection(LoggingMixin, ABC): @abstractmethod def stop_per_pair( - self, pair: str, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + self, pair: str, date_now: datetime, side: LongShort, starting_balance: float ) -> ProtectionReturn | None: """ Stops trading (position entering) for this pair diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 6e4ee6942..31bc728b8 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -82,7 +82,7 @@ class LowProfitPairs(IProtection): return None def global_stop( - self, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + self, date_now: datetime, side: LongShort, starting_balance: float ) -> ProtectionReturn | None: """ Stops trading (position entering) for all pairs @@ -93,7 +93,7 @@ class LowProfitPairs(IProtection): return None def stop_per_pair( - self, pair: str, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + self, pair: str, date_now: datetime, side: LongShort, starting_balance: float ) -> ProtectionReturn | None: """ Stops trading (position entering) for this pair diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index 3b2e64731..77e8dfe58 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -1,5 +1,5 @@ import logging -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta from typing import Any import pandas as pd @@ -48,41 +48,50 @@ class MaxDrawdown(IProtection): """ look_back_until = date_now - timedelta(minutes=self._lookback_period) - # Get all closed trades to calculate balance at the start of the window - all_closed_trades = Trade.get_trades_proxy(is_open=False) - - trades_in_window = [] - profit_before_window = 0.0 - for trade in all_closed_trades: - if trade.close_date: - # Ensure close_date is aware for comparison - close_date = (trade.close_date.replace(tzinfo=UTC) - if trade.close_date.tzinfo is None else trade.close_date) - if close_date > look_back_until: - trades_in_window.append(trade) - else: - profit_before_window += (trade.close_profit_abs or 0.0) + trades_in_window = Trade.get_trades_proxy(is_open=False, close_date=look_back_until) if len(trades_in_window) < self._trade_limit: - # Not enough trades in the relevant period return None - # Calculate actual balance at the start of the lookback window - actual_starting_balance = starting_balance + profit_before_window + # Get all trades to calculate cumulative profit before the window + all_closed_trades = Trade.get_trades_proxy(is_open=False) + profit_before_window = sum( + trade.close_profit_abs or 0.0 + for trade in all_closed_trades + if trade.close_date_utc <= look_back_until + ) - trades_df = pd.DataFrame([trade.to_json() for trade in trades_in_window]) + # Get calculation mode + method = self._protection_config.get("method", "ratios") - # Drawdown is always positive try: - # Use absolute profit calculation with the actual balance at window start. - drawdown_obj = calculate_max_drawdown( - trades_df, - value_col="profit_abs", - starting_balance=actual_starting_balance, - relative=True - ) - # Use relative drawdown to compare against max_allowed_drawdown percentage - drawdown = drawdown_obj.relative_account_drawdown + if method == "equity": + # Standard equity-based drawdown + trades_df = pd.DataFrame( + [ + {"close_date": t.close_date_utc, "profit_abs": t.close_profit_abs} + for t in trades_in_window + ] + ) + actual_starting_balance = starting_balance + profit_before_window + drawdown_obj = calculate_max_drawdown( + trades_df, + value_col="profit_abs", + starting_balance=actual_starting_balance, + relative=True, + ) + drawdown = drawdown_obj.relative_account_drawdown + else: + # Legacy ratios-based calculation (default) + trades_df = pd.DataFrame( + [ + {"close_date": t.close_date_utc, "close_profit": t.close_profit} + for t in trades_in_window + ] + ) + drawdown_obj = calculate_max_drawdown(trades_df, value_col="close_profit") + # In ratios mode, drawdown_abs is the cumulative ratio drop + drawdown = drawdown_obj.drawdown_abs except ValueError: return None @@ -104,7 +113,7 @@ class MaxDrawdown(IProtection): return None def global_stop( - self, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + self, date_now: datetime, side: LongShort, starting_balance: float ) -> ProtectionReturn | None: """ Stops trading (position entering) for all pairs @@ -115,7 +124,7 @@ class MaxDrawdown(IProtection): return self._max_drawdown(date_now, starting_balance) def stop_per_pair( - self, pair: str, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + self, pair: str, date_now: datetime, side: LongShort, starting_balance: float ) -> ProtectionReturn | None: """ Stops trading (position entering) for this pair diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index d7b267b5c..abb084177 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -87,7 +87,7 @@ class StoplossGuard(IProtection): ) def global_stop( - self, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + self, date_now: datetime, side: LongShort, starting_balance: float ) -> ProtectionReturn | None: """ Stops trading (position entering) for all pairs @@ -100,7 +100,7 @@ class StoplossGuard(IProtection): return self._stoploss_guard(date_now, None, side) def stop_per_pair( - self, pair: str, date_now: datetime, side: LongShort, starting_balance: float = 0.0 + self, pair: str, date_now: datetime, side: LongShort, starting_balance: float ) -> ProtectionReturn | None: """ Stops trading (position entering) for this pair From d59338421965a48e0bb45a12bcce3bab852f1202 Mon Sep 17 00:00:00 2001 From: ABS <53243996+ABSllk@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:19:26 +0800 Subject: [PATCH 06/15] docs: update MaxDrawdown protections documentation with new calculation methods --- docs/includes/protections.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 23c7d9654..795478745 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -72,6 +72,8 @@ def protections(self): `MaxDrawdown` calculates the maximum relative drawdown using the account's equity curve within the `lookback_period` in minutes (or in candles when using `lookback_period_candles`). It evaluates the portfolio's peak-to-trough declines by considering the starting balance and the cumulative profit of all trades within the window. If the observed drawdown exceeds `max_allowed_drawdown`, trading will stop for `stop_duration` after the last trade - assuming that the bot needs some time to let markets recover. +The default calculation method is the sum-of-profit-ratios method (`method: "ratios"`) for backward compatibility. To use the standard peak-to-trough equity drawdown (recommended), set `method: "equity"`. + The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used. ``` python From e1d7c7a081e516817c29c8430af8ff49c08a21fe Mon Sep 17 00:00:00 2001 From: ABS <53243996+ABSllk@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:20:12 +0800 Subject: [PATCH 07/15] test: update MaxDrawdown protection tests for starting_balance and calculation modes --- tests/plugins/test_protections.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 2814252fa..a70148b35 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -99,9 +99,9 @@ def test_protectionmanager(mocker, default_conf): for handler in freqtrade.protections._protection_handlers: assert handler.name in AVAILABLE_PROTECTIONS if not handler.has_global_stop: - assert handler.global_stop(datetime.now(UTC), "*") is None + assert handler.global_stop(datetime.now(UTC), "*", 1000.0) is None if not handler.has_local_stop: - assert handler.stop_per_pair("XRP/BTC", datetime.now(UTC), "*") is None + assert handler.stop_per_pair("XRP/BTC", datetime.now(UTC), "*", 1000.0) is None @pytest.mark.parametrize( From d27aef2063ed20c22d4bcdd5aa4bc5dca8d4847c Mon Sep 17 00:00:00 2001 From: ABS <53243996+ABSllk@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:59:02 +0800 Subject: [PATCH 08/15] fix: correct parameter naming --- docs/includes/protections.md | 2 +- freqtrade/plugins/protections/max_drawdown_protection.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 795478745..0a37f8d10 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -72,7 +72,7 @@ def protections(self): `MaxDrawdown` calculates the maximum relative drawdown using the account's equity curve within the `lookback_period` in minutes (or in candles when using `lookback_period_candles`). It evaluates the portfolio's peak-to-trough declines by considering the starting balance and the cumulative profit of all trades within the window. If the observed drawdown exceeds `max_allowed_drawdown`, trading will stop for `stop_duration` after the last trade - assuming that the bot needs some time to let markets recover. -The default calculation method is the sum-of-profit-ratios method (`method: "ratios"`) for backward compatibility. To use the standard peak-to-trough equity drawdown (recommended), set `method: "equity"`. +The default calculation method is the sum-of-profit-ratios method (`calculation_mode: "ratios"`) for backward compatibility. To use the standard peak-to-trough equity drawdown (recommended), set `calculation_mode: "equity"`. The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used. diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index 77e8dfe58..6f80acf22 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -22,6 +22,7 @@ class MaxDrawdown(IProtection): self._trade_limit = protection_config.get("trade_limit", 1) self._max_allowed_drawdown = protection_config.get("max_allowed_drawdown", 0.0) + self._calculation_mode = protection_config.get("calculation_mode", "ratios") # TODO: Implement checks to limit max_drawdown to sensible values def short_desc(self) -> str: @@ -61,11 +62,8 @@ class MaxDrawdown(IProtection): if trade.close_date_utc <= look_back_until ) - # Get calculation mode - method = self._protection_config.get("method", "ratios") - try: - if method == "equity": + if self._calculation_mode == "equity": # Standard equity-based drawdown trades_df = pd.DataFrame( [ From 0672d8f094ebd56a5852bfa942e4a5253a080669 Mon Sep 17 00:00:00 2001 From: ABS <53243996+ABSllk@users.noreply.github.com> Date: Thu, 19 Feb 2026 04:14:41 +0800 Subject: [PATCH 09/15] refactor: limit pre-window balance aggregation to equity mode --- .../protections/max_drawdown_protection.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index 6f80acf22..396791d10 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -54,17 +54,17 @@ class MaxDrawdown(IProtection): if len(trades_in_window) < self._trade_limit: return None - # Get all trades to calculate cumulative profit before the window - all_closed_trades = Trade.get_trades_proxy(is_open=False) - profit_before_window = sum( - trade.close_profit_abs or 0.0 - for trade in all_closed_trades - if trade.close_date_utc <= look_back_until - ) - try: if self._calculation_mode == "equity": # Standard equity-based drawdown + # Get all trades to calculate cumulative profit before the window + all_closed_trades = Trade.get_trades_proxy(is_open=False) + profit_before_window = sum( + trade.close_profit_abs or 0.0 + for trade in all_closed_trades + if trade.close_date_utc <= look_back_until + ) + trades_df = pd.DataFrame( [ {"close_date": t.close_date_utc, "profit_abs": t.close_profit_abs} From e19d96f85c827efc9c6fc89067502b4a42c23ea9 Mon Sep 17 00:00:00 2001 From: ABS <53243996+ABSllk@users.noreply.github.com> Date: Thu, 19 Feb 2026 04:20:21 +0800 Subject: [PATCH 10/15] test: improve MaxDrawdown mode coverage and naming --- tests/plugins/test_protections.py | 201 +++++++++++++++++++++++++++++- 1 file changed, 200 insertions(+), 1 deletion(-) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index a70148b35..dc654b7b5 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -1,5 +1,6 @@ import random from datetime import UTC, datetime, timedelta +from types import SimpleNamespace import pytest @@ -658,7 +659,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side): @pytest.mark.usefixtures("init_persistence") -def test_MaxDrawdown(mocker, default_conf, fee, caplog): +def test_MaxDrawdown_ratio_mode(mocker, default_conf, fee, caplog): default_conf["_strategy_protections"] = [ { "method": "MaxDrawdown", @@ -776,6 +777,204 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert log_has_re(message, caplog) +@pytest.mark.usefixtures("init_persistence") +def test_MaxDrawdown_equity_mode(mocker, default_conf, fee): + default_conf["_strategy_protections"] = [ + { + "method": "MaxDrawdown", + "lookback_period": 1000, + "stop_duration": 60, + "trade_limit": 1, + "max_allowed_drawdown": 0.01, + "calculation_mode": "equity", + } + ] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + starting_balance = 0.01 + + assert not freqtrade.protections.global_stop(starting_balance=starting_balance) + + generate_mock_trade( + "XRP/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=30, + min_ago_close=10, + profit_rate=0.5, + ) + Trade.commit() + + assert freqtrade.protections.global_stop(starting_balance=starting_balance) + assert PairLocks.is_global_lock() + + +@pytest.mark.parametrize( + "calculation_mode,expected_locked", + [("ratios", True), ("equity", False)], +) +@pytest.mark.usefixtures("init_persistence") +def test_MaxDrawdown_mode_comparison(mocker, default_conf, fee, calculation_mode, expected_locked): + default_conf["_strategy_protections"] = [ + { + "method": "MaxDrawdown", + "lookback_period": 1000, + "stop_duration": 60, + "trade_limit": 3, + "max_allowed_drawdown": 0.15, + "calculation_mode": calculation_mode, + } + ] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + starting_balance = 1000.0 + + # Same trade sequence for both modes: ratios mode lock expected,equity mode no lock expected. + generate_mock_trade( + "XRP/BTC", + fee.return_value, + False, + exit_reason=ExitType.ROI.value, + min_ago_open=120, + min_ago_close=50, + profit_rate=1.2, + ) + generate_mock_trade( + "ETH/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=80, + min_ago_close=20, + profit_rate=0.9, + ) + generate_mock_trade( + "NEO/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=40, + min_ago_close=10, + profit_rate=0.9, + ) + Trade.commit() + + lock = freqtrade.protections.global_stop(starting_balance=starting_balance) + assert bool(lock) is expected_locked + assert PairLocks.is_global_lock(side="long") is expected_locked + + +@pytest.mark.parametrize("calculation_mode", ["ratios", "equity"]) +@pytest.mark.usefixtures("init_persistence") +def test_MaxDrawdown_threshold_boundary(mocker, default_conf, calculation_mode): + threshold = 0.15 + default_conf["_strategy_protections"] = [ + { + "method": "MaxDrawdown", + "lookback_period": 1000, + "stop_duration": 60, + "trade_limit": 1, + "max_allowed_drawdown": threshold, + "calculation_mode": calculation_mode, + } + ] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + handler = next(p for p in freqtrade.protections._protection_handlers if p.name == "MaxDrawdown") + md_globals = handler._max_drawdown.__globals__ + + now = datetime.now(UTC) + trades_in_window = [ + SimpleNamespace( + close_date_utc=now - timedelta(minutes=10), close_profit_abs=-1.0, close_profit=-0.1 + ) + ] + all_closed_trades = [ + SimpleNamespace( + close_date_utc=now - timedelta(minutes=1500), close_profit_abs=5.0, close_profit=0.2 + ), + trades_in_window[0], + ] + + proxy_side_effect = [trades_in_window] + if calculation_mode == "equity": + proxy_side_effect.append(all_closed_trades) + + mocker.patch.object( + md_globals["Trade"], + "get_trades_proxy", + side_effect=proxy_side_effect, + ) + calc_mock = mocker.Mock( + return_value=SimpleNamespace(relative_account_drawdown=threshold, drawdown_abs=threshold) + ) + mocker.patch.dict(md_globals, {"calculate_max_drawdown": calc_mock}) + + assert not handler.global_stop(datetime.now(UTC), "long", starting_balance=1000.0) + assert not PairLocks.is_global_lock() + + +@pytest.mark.parametrize( + "calculation_mode,expected_value_col,expected_proxy_calls", + [("ratios", "close_profit", 1), ("equity", "profit_abs", 2)], +) +@pytest.mark.usefixtures("init_persistence") +def test_MaxDrawdown_calculation_mode_dispatch( + mocker, default_conf, calculation_mode, expected_value_col, expected_proxy_calls +): + default_conf["_strategy_protections"] = [ + { + "method": "MaxDrawdown", + "lookback_period": 1000, + "stop_duration": 60, + "trade_limit": 1, + "max_allowed_drawdown": 0.15, + "calculation_mode": calculation_mode, + } + ] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + handler = next(p for p in freqtrade.protections._protection_handlers if p.name == "MaxDrawdown") + md_globals = handler._max_drawdown.__globals__ + + now = datetime.now(UTC) + trades_in_window = [ + SimpleNamespace( + close_date_utc=now - timedelta(minutes=10), close_profit_abs=-1.0, close_profit=-0.1 + ) + ] + all_closed_trades = [ + SimpleNamespace( + close_date_utc=now - timedelta(minutes=1500), close_profit_abs=5.0, close_profit=0.2 + ), + trades_in_window[0], + ] + + proxy_side_effect = [trades_in_window] + if calculation_mode == "equity": + proxy_side_effect.append(all_closed_trades) + + proxy_mock = mocker.patch.object( + md_globals["Trade"], + "get_trades_proxy", + side_effect=proxy_side_effect, + ) + calc_mock = mocker.Mock( + return_value=SimpleNamespace(relative_account_drawdown=0.0, drawdown_abs=0.0) + ) + mocker.patch.dict(md_globals, {"calculate_max_drawdown": calc_mock}) + + assert not handler.global_stop(datetime.now(UTC), "long", starting_balance=1000.0) + + assert proxy_mock.call_count == expected_proxy_calls + kwargs = calc_mock.call_args.kwargs + assert kwargs["value_col"] == expected_value_col + + if calculation_mode == "equity": + assert kwargs["starting_balance"] == 1005.0 + assert kwargs["relative"] is True + else: + assert "starting_balance" not in kwargs + assert "relative" not in kwargs + + @pytest.mark.parametrize( "protectionconf,desc_expected,exception_expected", [ From e524e030ef9c6e605fb8be52994e927db0c13b3c Mon Sep 17 00:00:00 2001 From: ABS <53243996+ABSllk@users.noreply.github.com> Date: Thu, 19 Feb 2026 04:21:32 +0800 Subject: [PATCH 11/15] docs: clarify mode behavior and sample config in MaxDrawdown protection --- docs/includes/protections.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 0a37f8d10..786879e15 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -69,10 +69,16 @@ def protections(self): #### MaxDrawdown -`MaxDrawdown` calculates the maximum relative drawdown using the account's equity curve within the `lookback_period` in minutes (or in candles when using `lookback_period_candles`). -It evaluates the portfolio's peak-to-trough declines by considering the starting balance and the cumulative profit of all trades within the window. If the observed drawdown exceeds `max_allowed_drawdown`, trading will stop for `stop_duration` after the last trade - assuming that the bot needs some time to let markets recover. +`MaxDrawdown` supports 2 calculation modes within the `lookback_period` in minutes (or in candles when using `lookback_period_candles`): -The default calculation method is the sum-of-profit-ratios method (`calculation_mode: "ratios"`) for backward compatibility. To use the standard peak-to-trough equity drawdown (recommended), set `calculation_mode: "equity"`. +- `calculation_mode: "ratios"` (default): Legacy approximation based on cumulative profit ratios. +- `calculation_mode: "equity"`: Standard peak-to-trough drawdown on the account equity curve, using starting balance and cumulative absolute profit. + +With `calculation_mode: "ratios"`, drawdown is derived from cumulative trade profit ratios, not from the account equity curve. This is kept for backward compatibility and can differ from account-level drawdown when position sizing changes over time. + +For new setups, `calculation_mode: "equity"` is recommended. Prefer `calculation_mode: "ratios"` only when you intentionally rely on legacy behavior, especially with fixed stake amount configurations where ratio-based behavior is easier to reason about. + +If the observed drawdown exceeds `max_allowed_drawdown`, trading will stop for `stop_duration` after the last trade - assuming that the bot needs some time to let markets recover. The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used. @@ -82,6 +88,7 @@ def protections(self): return [ { "method": "MaxDrawdown", + "calculation_mode": "equity", "lookback_period_candles": 48, "trade_limit": 20, "stop_duration_candles": 12, From 50f914d6d5d293db1d29861c3d8f80d41c87149e Mon Sep 17 00:00:00 2001 From: ABS <53243996+ABSllk@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:34:41 +0800 Subject: [PATCH 12/15] fix: pass starting_balance to protection manager in backtest --- docs/includes/protections.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 786879e15..f2cb32fc0 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -69,7 +69,7 @@ def protections(self): #### MaxDrawdown -`MaxDrawdown` supports 2 calculation modes within the `lookback_period` in minutes (or in candles when using `lookback_period_candles`): +The `MaxDrawdown` protection evaluates trades that closed within the current `lookback_period` (or `lookback_period_candles`). It supports 2 calculation modes: - `calculation_mode: "ratios"` (default): Legacy approximation based on cumulative profit ratios. - `calculation_mode: "equity"`: Standard peak-to-trough drawdown on the account equity curve, using starting balance and cumulative absolute profit. @@ -173,7 +173,8 @@ class AwesomeStrategy(IStrategy) "lookback_period_candles": 48, "trade_limit": 20, "stop_duration_candles": 4, - "max_allowed_drawdown": 0.2 + "max_allowed_drawdown": 0.2, + "calculation_mode": "equity" }, { "method": "StoplossGuard", From a5d24f14cb49e542ed427d388caa656458959610 Mon Sep 17 00:00:00 2001 From: ABS <53243996+ABSllk@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:34:59 +0800 Subject: [PATCH 13/15] docs: clarify MaxDrawdown calculation modes and update example --- freqtrade/optimize/backtesting.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 26fd3f454..bdfce6e42 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1271,8 +1271,9 @@ class Backtesting: def run_protections(self, pair: str, current_time: datetime, side: LongShort): if self.enable_protections: - self.protections.stop_per_pair(pair, current_time, side) - self.protections.global_stop(current_time, side) + starting_balance = self.wallets.get_starting_balance() + self.protections.stop_per_pair(pair, current_time, side, starting_balance) + self.protections.global_stop(current_time, side, starting_balance) def manage_open_orders(self, trade: LocalTrade, current_time: datetime, row: tuple) -> bool: """ From 58079a8b5092b3dc0efe3a50e590070ab8c605ac Mon Sep 17 00:00:00 2001 From: ABS <53243996+ABSllk@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:15:43 +0800 Subject: [PATCH 14/15] refactor: fetch starting_balance only once at backtest initialization --- freqtrade/optimize/backtesting.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bdfce6e42..4311df21a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -136,6 +136,7 @@ class Backtesting: "exited": {}, } self.rejected_dict: dict[str, list] = {} + self.starting_balance: float = 0.0 self._exchange_name = self.config["exchange"]["name"] self.__initial_backtest = exchange is None @@ -277,6 +278,7 @@ class Backtesting: self.reset_backtest(False) self.wallets = Wallets(self.config, self.exchange, is_backtest=True) + self.starting_balance = self.wallets.get_starting_balance() self.progress = BTProgress() self.abort = False @@ -1271,9 +1273,8 @@ class Backtesting: def run_protections(self, pair: str, current_time: datetime, side: LongShort): if self.enable_protections: - starting_balance = self.wallets.get_starting_balance() - self.protections.stop_per_pair(pair, current_time, side, starting_balance) - self.protections.global_stop(current_time, side, starting_balance) + self.protections.stop_per_pair(pair, current_time, side, self.starting_balance) + self.protections.global_stop(current_time, side, self.starting_balance) def manage_open_orders(self, trade: LocalTrade, current_time: datetime, row: tuple) -> bool: """ From b6993f0b245dbe606eb0e5ee59904fe221d0b63f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Feb 2026 20:14:20 +0100 Subject: [PATCH 15/15] test: fix random test failure due to missing mandatory key --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index 1c0435ff3..abd15a6a1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -604,6 +604,7 @@ def get_default_conf(testdatadir): "cancel_open_orders_on_exit": False, "minimal_roi": {"40": 0.0, "30": 0.01, "20": 0.02, "0": 0.04}, "dry_run_wallet": 1000, + "tradable_balance_ratio": 0.99, "stoploss": -0.10, "unfilledtimeout": {"entry": 10, "exit": 30}, "entry_pricing": {