From 56975db2ed5387e8a14b5a17e290c0d45a0cdba6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Oct 2020 08:07:09 +0200 Subject: [PATCH] Add more tests --- config_full.json.example | 3 ++- freqtrade/plugins/protectionmanager.py | 13 ++++----- freqtrade/plugins/protections/__init__.py | 2 +- freqtrade/plugins/protections/iprotection.py | 6 +++-- .../plugins/protections/stoploss_guard.py | 27 ++++++++++++++----- tests/plugins/test_protections.py | 24 ++++++++++++++--- 6 files changed, 54 insertions(+), 21 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 96aa82d5f..eb20065ce 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -79,7 +79,8 @@ { "method": "StoplossGuard", "lookback_period": 60, - "trade_limit": 4 + "trade_limit": 4, + "stopduration": 60 } ], "exchange": { diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index dd6076ec1..c4822a323 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -2,10 +2,10 @@ Protection manager class """ import logging -from datetime import datetime +from datetime import datetime, timezone from typing import Dict, List -from freqtrade.exceptions import OperationalException +from freqtrade.persistence import PairLocks from freqtrade.plugins.protections import IProtection from freqtrade.resolvers import ProtectionResolver @@ -47,12 +47,13 @@ class ProtectionManager(): return [{p.name: p.short_desc()} for p in self._protection_handlers] def global_stop(self) -> bool: - now = datetime.utcnow() + now = datetime.now(timezone.utc) for protection_handler in self._protection_handlers: - result = protection_handler.global_stop(now) + result, until, reason = protection_handler.global_stop(now) - # Early stopping - first positive result stops the application - if result: + # Early stopping - first positive result blocks further trades + if result and until: + PairLocks.lock_pair('*', until, reason) return True return False diff --git a/freqtrade/plugins/protections/__init__.py b/freqtrade/plugins/protections/__init__.py index 5ecae7888..936355052 100644 --- a/freqtrade/plugins/protections/__init__.py +++ b/freqtrade/plugins/protections/__init__.py @@ -1,2 +1,2 @@ # flake8: noqa: F401 -from freqtrade.plugins.protections.iprotection import IProtection +from freqtrade.plugins.protections.iprotection import IProtection, ProtectionReturn diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index ecb4cad09..cadf01184 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -2,13 +2,15 @@ import logging from abc import ABC, abstractmethod from datetime import datetime -from typing import Any, Dict +from typing import Any, Dict, Optional, Tuple from freqtrade.mixins import LoggingMixin logger = logging.getLogger(__name__) +ProtectionReturn = Tuple[bool, Optional[datetime], Optional[str]] + class IProtection(LoggingMixin, ABC): @@ -29,7 +31,7 @@ class IProtection(LoggingMixin, ABC): """ @abstractmethod - def global_stop(self, date_now: datetime) -> bool: + def global_stop(self, date_now: datetime) -> ProtectionReturn: """ Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 3b0b8c773..db3655a38 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -1,12 +1,12 @@ import logging from datetime import datetime, timedelta -from typing import Any, Dict +from typing import Any, Dict, Tuple from sqlalchemy import and_, or_ from freqtrade.persistence import Trade -from freqtrade.plugins.protections import IProtection +from freqtrade.plugins.protections import IProtection, ProtectionReturn from freqtrade.strategy.interface import SellType @@ -17,16 +17,26 @@ class StoplossGuard(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) + self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 10) + self._stopduration = protection_config.get('stopduration', 60) + + def _reason(self) -> str: + """ + LockReason to use + """ + return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, ' + f'locking for {self._stopduration} min.') def short_desc(self) -> str: """ Short method description - used for startup-messages """ - return f"{self.name} - Frequent Stoploss Guard" + return (f"{self.name} - Frequent Stoploss Guard, {self._trade_limit} stoplosses " + f"within {self._lookback_period} minutes.") - def _stoploss_guard(self, date_now: datetime, pair: str = None) -> bool: + def _stoploss_guard(self, date_now: datetime, pair: str = None) -> ProtectionReturn: """ Evaluate recent trades """ @@ -45,13 +55,16 @@ class StoplossGuard(IProtection): if len(trades) > self._trade_limit: self.log_on_refresh(logger.info, f"Trading stopped due to {self._trade_limit} " f"stoplosses within {self._lookback_period} minutes.") - return True + until = date_now + timedelta(minutes=self._stopduration) + return True, until, self._reason() - return False + return False, None, None - def global_stop(self, date_now: datetime) -> bool: + def global_stop(self, date_now: datetime) -> ProtectionReturn: """ 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._stoploss_guard(date_now, pair=None) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 2bb0886ee..d2815338e 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -1,11 +1,10 @@ -from freqtrade.strategy.interface import SellType -from unittest.mock import MagicMock, PropertyMock import random -import pytest from datetime import datetime, timedelta -from freqtrade.constants import AVAILABLE_PROTECTIONS +import pytest + from freqtrade.persistence import Trade +from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has_re @@ -77,3 +76,20 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert freqtrade.protections.global_stop() assert log_has_re(message, caplog) + + +@pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ + ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, + "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " + "2 stoplosses within 60 minutes.'}]", + None + ), +]) +def test_protection_manager_desc(mocker, default_conf, protectionconf, + desc_expected, exception_expected): + + default_conf['protections'] = [protectionconf] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + short_desc = str(freqtrade.protections.short_desc()) + assert short_desc == desc_expected