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] 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