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>
pull/12809/head
google-labs-jules[bot] 3 months ago
parent a1487016e0
commit 6b164f96d6

@ -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(

@ -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]:
"""

@ -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)

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

@ -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)

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

@ -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)

@ -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,
},
)

@ -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")

Loading…
Cancel
Save