From 6b164f96d6831853b0a760bba8d760e1c41dc9b2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:19:12 +0000 Subject: [PATCH] Refactor MaxDrawdown protection to use absolute profit and account balance - Updated `ProtectionManager`, `ProtectionResolver`, `IProtection` and subclasses to accept `wallets`. - Updated `MaxDrawdown` protection to calculate drawdown using `profit_abs` and account balance. - Updated `FreqtradeBot` and `Backtesting` to pass `wallets` to `ProtectionManager`. - Updated tests to support new dependency injection and verify absolute profit calculation logic. Co-authored-by: Corax-CoLAB <239841157+Corax-CoLAB@users.noreply.github.com> --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/plugins/protectionmanager.py | 10 +++++-- freqtrade/plugins/protections/iprotection.py | 10 +++++-- .../plugins/protections/low_profit_pairs.py | 11 +++++--- .../protections/max_drawdown_protection.py | 27 ++++++++++++++----- .../plugins/protections/stoploss_guard.py | 11 +++++--- freqtrade/resolvers/protection_resolver.py | 11 +++++++- tests/plugins/test_protections.py | 4 ++- 9 files changed, 68 insertions(+), 20 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4cf226a6b..ec77b6c7b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -175,7 +175,7 @@ class FreqtradeBot(LoggingMixin): self.strategy.ft_bot_start() # Initialize protections AFTER bot start - otherwise parameters are not loaded. - self.protections = ProtectionManager(self.config, self.strategy.protections) + self.protections = ProtectionManager(self.config, self.strategy.protections, self.wallets) def log_took_too_long(duration: float, time_limit: float): logger.warning( diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8cbdd0dea..939bd9419 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -300,7 +300,7 @@ class Backtesting: def _load_protections(self, strategy: IStrategy): if self.config.get("enable_protections", False): - self.protections = ProtectionManager(self.config, strategy.protections) + self.protections = ProtectionManager(self.config, strategy.protections, self.wallets) def load_bt_data(self) -> tuple[dict[str, DataFrame], TimeRange]: """ diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index 0eda8422f..7c4294e1e 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -4,7 +4,7 @@ Protection manager class import logging from datetime import UTC, datetime -from typing import Any +from typing import TYPE_CHECKING, Any from freqtrade.constants import Config, LongShort from freqtrade.exceptions import ConfigurationError @@ -13,13 +13,18 @@ from freqtrade.persistence.models import PairLock from freqtrade.plugins.protections import IProtection from freqtrade.resolvers import ProtectionResolver +if TYPE_CHECKING: + from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) class ProtectionManager: - def __init__(self, config: Config, protections: list) -> None: + def __init__( + self, config: Config, protections: list, wallets: "Wallets | None" = None + ) -> None: self._config = config + self._wallets = wallets self._protection_handlers: list[IProtection] = [] self.validate_protections(protections) @@ -28,6 +33,7 @@ class ProtectionManager: protection_handler_config["method"], config=config, protection_config=protection_handler_config, + wallets=wallets, ) self._protection_handlers.append(protection_handler) diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 8f2f51729..5b4f21b10 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -2,7 +2,7 @@ import logging from abc import ABC, abstractmethod from dataclasses import dataclass from datetime import UTC, datetime, timedelta -from typing import Any +from typing import TYPE_CHECKING, Any from freqtrade.constants import Config, LongShort from freqtrade.exchange import timeframe_to_minutes @@ -10,6 +10,9 @@ from freqtrade.misc import plural from freqtrade.mixins import LoggingMixin from freqtrade.persistence import LocalTrade +if TYPE_CHECKING: + from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) @@ -28,9 +31,12 @@ class IProtection(LoggingMixin, ABC): # Can stop trading for one pair has_local_stop: bool = False - def __init__(self, config: Config, protection_config: dict[str, Any]) -> None: + def __init__( + self, config: Config, protection_config: dict[str, Any], wallets: "Wallets | None" = None + ) -> None: self._config = config self._protection_config = protection_config + self._wallets = wallets self._stop_duration_candles: int | None = None self._stop_duration: int = 0 self._lookback_period_candles: int | None = None diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 84934f394..88126df49 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -1,11 +1,14 @@ import logging from datetime import datetime, timedelta -from typing import Any +from typing import TYPE_CHECKING, Any from freqtrade.constants import Config, LongShort from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn +if TYPE_CHECKING: + from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) @@ -14,8 +17,10 @@ class LowProfitPairs(IProtection): has_global_stop: bool = False has_local_stop: bool = True - def __init__(self, config: Config, protection_config: dict[str, Any]) -> None: - super().__init__(config, protection_config) + def __init__( + self, config: Config, protection_config: dict[str, Any], wallets: "Wallets | None" = None + ) -> None: + super().__init__(config, protection_config, wallets) self._trade_limit = protection_config.get("trade_limit", 1) self._required_profit = protection_config.get("required_profit", 0.0) diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index e8996d3a8..12ad700a1 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -1,6 +1,6 @@ import logging from datetime import datetime, timedelta -from typing import Any +from typing import TYPE_CHECKING, Any import pandas as pd @@ -9,6 +9,9 @@ from freqtrade.data.metrics import calculate_max_drawdown from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn +if TYPE_CHECKING: + from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) @@ -17,8 +20,10 @@ class MaxDrawdown(IProtection): has_global_stop: bool = True has_local_stop: bool = False - def __init__(self, config: Config, protection_config: dict[str, Any]) -> None: - super().__init__(config, protection_config) + def __init__( + self, config: Config, protection_config: dict[str, Any], wallets: "Wallets | None" = None + ) -> None: + super().__init__(config, protection_config, wallets) self._trade_limit = protection_config.get("trade_limit", 1) self._max_allowed_drawdown = protection_config.get("max_allowed_drawdown", 0.0) @@ -58,9 +63,19 @@ class MaxDrawdown(IProtection): # 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 + starting_balance = 0 + if self._wallets: + # We need to know the starting balance for the period (or current balance) + # to calculate the relative drawdown. + # Assuming wallets.get_total() returns current total balance. + current_balance = self._wallets.get_total(self._config["stake_currency"]) + # Calculate starting balance based on closed trades profit. + starting_balance = current_balance - trades_df["profit_abs"].sum() + + drawdown_obj = calculate_max_drawdown( + trades_df, value_col="profit_abs", starting_balance=starting_balance + ) + drawdown = drawdown_obj.relative_account_drawdown except ValueError: return None diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index a429a2f80..54faa3285 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -1,12 +1,15 @@ import logging from datetime import datetime, timedelta -from typing import Any +from typing import TYPE_CHECKING, Any from freqtrade.constants import Config, LongShort from freqtrade.enums import ExitType from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn +if TYPE_CHECKING: + from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) @@ -15,8 +18,10 @@ class StoplossGuard(IProtection): has_global_stop: bool = True has_local_stop: bool = True - def __init__(self, config: Config, protection_config: dict[str, Any]) -> None: - super().__init__(config, protection_config) + def __init__( + self, config: Config, protection_config: dict[str, Any], wallets: "Wallets | None" = None + ) -> None: + super().__init__(config, protection_config, wallets) self._trade_limit = protection_config.get("trade_limit", 10) self._disable_global_stop = protection_config.get("only_per_pair", False) diff --git a/freqtrade/resolvers/protection_resolver.py b/freqtrade/resolvers/protection_resolver.py index 661791ace..506ff7907 100644 --- a/freqtrade/resolvers/protection_resolver.py +++ b/freqtrade/resolvers/protection_resolver.py @@ -4,11 +4,15 @@ This module load custom pairlists import logging from pathlib import Path +from typing import TYPE_CHECKING from freqtrade.constants import Config from freqtrade.plugins.protections import IProtection from freqtrade.resolvers import IResolver +if TYPE_CHECKING: + from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) @@ -25,13 +29,17 @@ class ProtectionResolver(IResolver): @staticmethod def load_protection( - protection_name: str, config: Config, protection_config: dict + protection_name: str, + config: Config, + protection_config: dict, + wallets: "Wallets | None" = None, ) -> IProtection: """ Load the protection with protection_name :param protection_name: Classname of the pairlist :param config: configuration dictionary :param protection_config: Configuration dedicated to this pairlist + :param wallets: Wallets object :return: initialized Protection class """ return ProtectionResolver.load_object( @@ -40,5 +48,6 @@ class ProtectionResolver(IResolver): kwargs={ "config": config, "protection_config": protection_config, + "wallets": wallets, }, ) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 164fdbb08..f10fb1388 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -668,6 +668,8 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): "max_allowed_drawdown": 0.15, } ] + # Set a small wallet size so that losses are significant relative to account balance + default_conf["dry_run_wallet"] = 0.01 freqtrade = get_patched_freqtradebot(mocker, default_conf) message = r"Trading stopped due to Max.*" @@ -764,7 +766,7 @@ 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.5, ) Trade.commit() assert not freqtrade.protections.stop_per_pair("XRP/BTC")