Merge pull request #10723 from freqtrade/remove/deprecated_protection-setting

Remove long deprecated protections from config setting
This commit is contained in:
Matthias
2024-09-30 06:41:22 +02:00
committed by GitHub
20 changed files with 132 additions and 250 deletions

View File

@@ -579,57 +579,6 @@
]
}
},
"protections": {
"description": "Configuration for various protections.",
"type": "array",
"items": {
"type": "object",
"properties": {
"method": {
"description": "Method used for the protection.",
"type": "string",
"enum": [
"CooldownPeriod",
"LowProfitPairs",
"MaxDrawdown",
"StoplossGuard"
]
},
"stop_duration": {
"description": "Duration to lock the pair after a protection is triggered, in minutes.",
"type": "number",
"minimum": 0.0
},
"stop_duration_candles": {
"description": "Duration to lock the pair after a protection is triggered, in number of candles.",
"type": "number",
"minimum": 0
},
"unlock_at": {
"description": "Time when trading will be unlocked regularly. Format: HH:MM",
"type": "string"
},
"trade_limit": {
"description": "Minimum number of trades required during lookback period.",
"type": "number",
"minimum": 1
},
"lookback_period": {
"description": "Period to look back for protection checks, in minutes.",
"type": "number",
"minimum": 1
},
"lookback_period_candles": {
"description": "Period to look back for protection checks, in number of candles.",
"type": "number",
"minimum": 1
}
},
"required": [
"method"
]
}
},
"telegram": {
"description": "Telegram settings.",
"type": "object",

View File

@@ -229,7 +229,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| | **Plugins**
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options.
| `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
| `protections` | Define one or more protections to be used. [More information](plugins.md#protections). <br> **Datatype:** List of Dicts
| | **Telegram**
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String

View File

@@ -75,7 +75,10 @@ Webhook terminology changed from "sell" to "exit", and from "buy" to "entry", re
* `webhooksellfill`, `webhookexitfill` -> `exit_fill`
* `webhooksellcancel`, `webhookexitcancel` -> `exit_cancel`
## Removal of `populate_any_indicators`
version 2023.3 saw the removal of `populate_any_indicators` in favor of split methods for feature engineering and targets. Please read the [migration document](strategy_migration.md#freqai-strategy) for full details.
## Removal of `protections` from configuration
Setting protections from the configuration via `"protections": [],` has been removed in 2024.10, after having raised deprecation warnings for over 3 years.

View File

@@ -241,7 +241,6 @@ No protection should use datetime directly, but use the provided `date_now` vari
!!! Tip "Writing a new Protection"
Best copy one of the existing Protections to have a good example.
Don't forget to register your protection in `constants.py` under the variable `AVAILABLE_PROTECTIONS` - otherwise it will not be selectable.
#### Implementation of a new protection

View File

@@ -445,7 +445,6 @@ While this strategy is most likely too simple to provide consistent profit, it s
Whether you are using `.range` functionality or the alternatives above, you should try to use space ranges as small as possible since this will improve CPU/RAM usage.
## Optimizing protections
Freqtrade can also optimize protections. How you optimize protections is up to you, and the following should be considered as example only.

View File

@@ -1,24 +1,16 @@
## Protections
!!! Warning "Beta feature"
This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord or via Github Issue.
Protections will protect your strategy from unexpected events and market conditions by temporarily stop trading for either one pair, or for all pairs.
All protection end times are rounded up to the next candle to avoid sudden, unexpected intra-candle buys.
!!! Note
!!! Tip "Usage tips"
Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy to improve performance.
!!! Tip
Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term).
!!! Note "Backtesting"
Protections are supported by backtesting and hyperopt, but must be explicitly enabled by using the `--enable-protections` flag.
!!! Warning "Setting protections from the configuration"
Setting protections from the configuration via `"protections": [],` key should be considered deprecated and will be removed in a future version.
It is also no longer guaranteed that your protections apply to the strategy in cases where the strategy defines [protections as property](hyperopt.md#optimizing-protections).
### Available Protections
* [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window.

View File

@@ -4,7 +4,6 @@ from typing import Dict
from freqtrade.constants import (
AVAILABLE_DATAHANDLERS,
AVAILABLE_PAIRLISTS,
AVAILABLE_PROTECTIONS,
BACKTEST_BREAKDOWNS,
DRY_RUN_WALLET,
EXPORT_OPTIONS,
@@ -449,60 +448,6 @@ CONF_SCHEMA = {
"required": ["method"],
},
},
"protections": {
"description": "Configuration for various protections.",
"type": "array",
"items": {
"type": "object",
"properties": {
"method": {
"description": "Method used for the protection.",
"type": "string",
"enum": AVAILABLE_PROTECTIONS,
},
"stop_duration": {
"description": (
"Duration to lock the pair after a protection is triggered, "
"in minutes."
),
"type": "number",
"minimum": 0.0,
},
"stop_duration_candles": {
"description": (
"Duration to lock the pair after a protection is triggered, in "
"number of candles."
),
"type": "number",
"minimum": 0,
},
"unlock_at": {
"description": (
"Time when trading will be unlocked regularly. Format: HH:MM"
),
"type": "string",
},
"trade_limit": {
"description": "Minimum number of trades required during lookback period.",
"type": "number",
"minimum": 1,
},
"lookback_period": {
"description": "Period to look back for protection checks, in minutes.",
"type": "number",
"minimum": 1,
},
"lookback_period_candles": {
"description": (
"Period to look back for protection checks, in number " "of candles."
),
"type": "number",
"minimum": 1,
},
},
"required": ["method"],
},
},
# RPC section
"telegram": {
"description": "Telegram settings.",

View File

@@ -1,7 +1,6 @@
import logging
from collections import Counter
from copy import deepcopy
from datetime import datetime
from typing import Any, Dict
from jsonschema import Draft4Validator, validators
@@ -84,7 +83,6 @@ def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = Fal
_validate_price_config(conf)
_validate_edge(conf)
_validate_whitelist(conf)
_validate_protections(conf)
_validate_unlimited_amount(conf)
_validate_ask_orderbook(conf)
_validate_freqai_hyperopt(conf)
@@ -196,41 +194,6 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None:
raise ConfigurationError("StaticPairList requires pair_whitelist to be set.")
def _validate_protections(conf: Dict[str, Any]) -> None:
"""
Validate protection configuration validity
"""
for prot in conf.get("protections", []):
parsed_unlock_at = None
if (config_unlock_at := prot.get("unlock_at")) is not None:
try:
parsed_unlock_at = datetime.strptime(config_unlock_at, "%H:%M")
except ValueError:
raise ConfigurationError(f"Invalid date format for unlock_at: {config_unlock_at}.")
if "stop_duration" in prot and "stop_duration_candles" in prot:
raise ConfigurationError(
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
f"Please fix the protection {prot.get('method')}."
)
if "lookback_period" in prot and "lookback_period_candles" in prot:
raise ConfigurationError(
"Protections must specify either `lookback_period` or `lookback_period_candles`.\n"
f"Please fix the protection {prot.get('method')}."
)
if parsed_unlock_at is not None and (
"stop_duration" in prot or "stop_duration_candles" in prot
):
raise ConfigurationError(
"Protections must specify either `unlock_at`, `stop_duration` or "
"`stop_duration_candles`.\n"
f"Please fix the protection {prot.get('method')}."
)
def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
ask_strategy = conf.get("exit_pricing", {})
ob_min = ask_strategy.get("order_book_min")

View File

@@ -177,4 +177,6 @@ def process_temporary_deprecated_settings(config: Config) -> None:
)
if "protections" in config:
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
raise ConfigurationError(
"DEPRECATED: Setting 'protections' in the configuration is deprecated."
)

View File

@@ -57,7 +57,6 @@ AVAILABLE_PAIRLISTS = [
"SpreadFilter",
"VolatilityFilter",
]
AVAILABLE_PROTECTIONS = ["CooldownPeriod", "LowProfitPairs", "MaxDrawdown", "StoplossGuard"]
AVAILABLE_DATAHANDLERS = ["json", "jsongz", "hdf5", "feather", "parquet"]
BACKTEST_BREAKDOWNS = ["day", "week", "month"]
BACKTEST_CACHE_AGE = ["none", "day", "week", "month"]

View File

@@ -273,10 +273,6 @@ class Backtesting:
def _load_protections(self, strategy: IStrategy):
if self.config.get("enable_protections", False):
conf = self.config
if hasattr(strategy, "protections"):
conf = deepcopy(conf)
conf["protections"] = strategy.protections
self.protections = ProtectionManager(self.config, strategy.protections)
def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]:

View File

@@ -4,9 +4,10 @@ Protection manager class
import logging
from datetime import datetime, timezone
from typing import Dict, List, Optional
from typing import Any, Dict, List, Optional
from freqtrade.constants import Config, LongShort
from freqtrade.exceptions import ConfigurationError
from freqtrade.persistence import PairLocks
from freqtrade.persistence.models import PairLock
from freqtrade.plugins.protections import IProtection
@@ -21,6 +22,7 @@ class ProtectionManager:
self._config = config
self._protection_handlers: List[IProtection] = []
self.validate_protections(protections)
for protection_handler_config in protections:
protection_handler = ProtectionResolver.load_protection(
protection_handler_config["method"],
@@ -76,3 +78,40 @@ class ProtectionManager:
pair, lock.until, lock.reason, now=now, side=lock.lock_side
)
return result
@staticmethod
def validate_protections(protections: List[Dict[str, Any]]) -> None:
"""
Validate protection setup validity
"""
for prot in protections:
parsed_unlock_at = None
if (config_unlock_at := prot.get("unlock_at")) is not None:
try:
parsed_unlock_at = datetime.strptime(config_unlock_at, "%H:%M")
except ValueError:
raise ConfigurationError(
f"Invalid date format for unlock_at: {config_unlock_at}."
)
if "stop_duration" in prot and "stop_duration_candles" in prot:
raise ConfigurationError(
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
f"Please fix the protection {prot.get('method')}."
)
if "lookback_period" in prot and "lookback_period_candles" in prot:
raise ConfigurationError(
"Protections must specify either `lookback_period` or "
f"`lookback_period_candles`.\n Please fix the protection {prot.get('method')}."
)
if parsed_unlock_at is not None and (
"stop_duration" in prot or "stop_duration_candles" in prot
):
raise ConfigurationError(
"Protections must specify either `unlock_at`, `stop_duration` or "
"`stop_duration_candles`.\n"
f"Please fix the protection {prot.get('method')}."
)

View File

@@ -69,7 +69,6 @@ class StrategyResolver(IResolver):
("order_time_in_force", None),
("stake_currency", None),
("stake_amount", None),
("protections", None),
("startup_candle_count", None),
("unfilledtimeout", None),
("use_exit_signal", True),

View File

@@ -77,7 +77,6 @@ def __run_backtest_bg(btconfig: Config):
lastconfig["timerange"] = btconfig["timerange"]
lastconfig["timeframe"] = strat.timeframe
lastconfig["protections"] = btconfig.get("protections", [])
lastconfig["enable_protections"] = btconfig.get("enable_protections")
lastconfig["dry_run_wallet"] = btconfig.get("dry_run_wallet")

View File

@@ -553,7 +553,7 @@ def test_enter_positions_global_pairlock(
@pytest.mark.parametrize("is_short", [False, True])
def test_handle_protections(mocker, default_conf_usdt, fee, is_short):
default_conf_usdt["protections"] = [
default_conf_usdt["_strategy_protections"] = [
{"method": "CooldownPeriod", "stop_duration": 60},
{
"method": "StoplossGuard",

View File

@@ -1299,7 +1299,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
# While this test IS a copy of test_backtest_pricecontours, it's needed to ensure
# results do not carry-over to the next run, which is not given by using parametrize.
patch_exchange(mocker)
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "CooldownPeriod",
"stop_duration": 3,
@@ -1358,7 +1358,7 @@ def test_backtest_pricecontours(
default_conf, mocker, testdatadir, protections, contour, expected
) -> None:
if protections:
default_conf["protections"] = protections
default_conf["_strategy_protections"] = protections
default_conf["enable_protections"] = True
patch_exchange(mocker)

View File

@@ -3,14 +3,17 @@ from datetime import datetime, timedelta, timezone
import pytest
from freqtrade import constants
from freqtrade.enums import ExitType
from freqtrade.exceptions import OperationalException
from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.trade_model import Order
from freqtrade.plugins.protectionmanager import ProtectionManager
from tests.conftest import get_patched_freqtradebot, log_has_re
AVAILABLE_PROTECTIONS = ["CooldownPeriod", "LowProfitPairs", "MaxDrawdown", "StoplossGuard"]
def generate_mock_trade(
pair: str,
fee: float,
@@ -88,19 +91,76 @@ def generate_mock_trade(
def test_protectionmanager(mocker, default_conf):
default_conf["protections"] = [
{"method": protection} for protection in constants.AVAILABLE_PROTECTIONS
default_conf["_strategy_protections"] = [
{"method": protection} for protection in AVAILABLE_PROTECTIONS
]
freqtrade = get_patched_freqtradebot(mocker, default_conf)
for handler in freqtrade.protections._protection_handlers:
assert handler.name in constants.AVAILABLE_PROTECTIONS
assert handler.name in AVAILABLE_PROTECTIONS
if not handler.has_global_stop:
assert handler.global_stop(datetime.now(timezone.utc), "*") is None
if not handler.has_local_stop:
assert handler.stop_per_pair("XRP/BTC", datetime.now(timezone.utc), "*") is None
@pytest.mark.parametrize(
"protconf,expected",
[
([], None),
([{"method": "StoplossGuard", "lookback_period": 2000, "stop_duration_candles": 10}], None),
([{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}], None),
(
[
{
"method": "StoplossGuard",
"lookback_period_candles": 20,
"lookback_period": 2000,
"stop_duration": 10,
}
],
r"Protections must specify either `lookback_period`.*",
),
(
[
{
"method": "StoplossGuard",
"lookback_period": 20,
"stop_duration": 10,
"stop_duration_candles": 10,
}
],
r"Protections must specify either `stop_duration`.*",
),
(
[
{
"method": "StoplossGuard",
"lookback_period": 20,
"stop_duration": 10,
"unlock_at": "20:02",
}
],
r"Protections must specify either `unlock_at`, `stop_duration` or.*",
),
(
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "20:02"}],
None,
),
(
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "55:102"}],
"Invalid date format for unlock_at: 55:102.",
),
],
)
def test_validate_protections(protconf, expected):
if expected:
with pytest.raises(OperationalException, match=expected):
ProtectionManager.validate_protections(protconf)
else:
ProtectionManager.validate_protections(protconf)
@pytest.mark.parametrize(
"timeframe,expected_lookback,expected_stop,protconf",
[
@@ -196,7 +256,7 @@ def test_protections_init(default_conf, timeframe, expected_lookback, expected_s
@pytest.mark.usefixtures("init_persistence")
def test_stoploss_guard(mocker, default_conf, fee, caplog, is_short):
# Active for both sides (long and short)
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{"method": "StoplossGuard", "lookback_period": 60, "stop_duration": 40, "trade_limit": 3}
]
freqtrade = get_patched_freqtradebot(mocker, default_conf)
@@ -268,7 +328,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog, is_short):
@pytest.mark.parametrize("only_per_side", [False, True])
@pytest.mark.usefixtures("init_persistence")
def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair, only_per_side):
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "StoplossGuard",
"lookback_period": 60,
@@ -379,7 +439,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair
@pytest.mark.usefixtures("init_persistence")
def test_CooldownPeriod(mocker, default_conf, fee, caplog):
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "CooldownPeriod",
"stop_duration": 60,
@@ -425,7 +485,7 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog):
@pytest.mark.usefixtures("init_persistence")
def test_CooldownPeriod_unlock_at(mocker, default_conf, fee, caplog, time_machine):
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "CooldownPeriod",
"unlock_at": "05:00",
@@ -509,7 +569,7 @@ def test_CooldownPeriod_unlock_at(mocker, default_conf, fee, caplog, time_machin
@pytest.mark.parametrize("only_per_side", [False, True])
@pytest.mark.usefixtures("init_persistence")
def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side):
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "LowProfitPairs",
"lookback_period": 400,
@@ -599,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):
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "MaxDrawdown",
"lookback_period": 1000,
@@ -812,7 +872,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
def test_protection_manager_desc(
mocker, default_conf, protectionconf, desc_expected, exception_expected
):
default_conf["protections"] = [protectionconf]
default_conf["_strategy_protections"] = [protectionconf]
freqtrade = get_patched_freqtradebot(mocker, default_conf)
short_desc = str(freqtrade.protections.short_desc())

View File

@@ -173,7 +173,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf) -> None:
telegram_mock.reset_mock()
default_conf["dry_run"] = True
default_conf["whitelist"] = {"method": "VolumePairList", "config": {"number_assets": 20}}
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2, "stop_duration": 60}
]
freqtradebot = get_patched_freqtradebot(mocker, default_conf)

View File

@@ -75,15 +75,13 @@ class StrategyTestV3(IStrategy):
protection_cooldown_lookback = IntParameter([0, 50], default=30)
# TODO: Can this work with protection tests? (replace HyperoptableStrategy implicitly ... )
# @property
# def protections(self):
# prot = []
# if self.protection_enabled.value:
# prot.append({
# "method": "CooldownPeriod",
# "stop_duration_candles": self.protection_cooldown_lookback.value
# })
# return prot
@property
def protections(self):
prot = []
if self.protection_enabled.value:
# Workaround to simplify tests. This will not work in real scenarios.
prot = self.config.get("_strategy_protections", {})
return prot
bot_started = False

View File

@@ -812,65 +812,6 @@ def test_validate_whitelist(default_conf):
validate_config_consistency(conf)
@pytest.mark.parametrize(
"protconf,expected",
[
([], None),
([{"method": "StoplossGuard", "lookback_period": 2000, "stop_duration_candles": 10}], None),
([{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}], None),
(
[
{
"method": "StoplossGuard",
"lookback_period_candles": 20,
"lookback_period": 2000,
"stop_duration": 10,
}
],
r"Protections must specify either `lookback_period`.*",
),
(
[
{
"method": "StoplossGuard",
"lookback_period": 20,
"stop_duration": 10,
"stop_duration_candles": 10,
}
],
r"Protections must specify either `stop_duration`.*",
),
(
[
{
"method": "StoplossGuard",
"lookback_period": 20,
"stop_duration": 10,
"unlock_at": "20:02",
}
],
r"Protections must specify either `unlock_at`, `stop_duration` or.*",
),
(
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "20:02"}],
None,
),
(
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "55:102"}],
"Invalid date format for unlock_at: 55:102.",
),
],
)
def test_validate_protections(default_conf, protconf, expected):
conf = deepcopy(default_conf)
conf["protections"] = protconf
if expected:
with pytest.raises(OperationalException, match=expected):
validate_config_consistency(conf)
else:
validate_config_consistency(conf)
def test_validate_ask_orderbook(default_conf, caplog) -> None:
conf = deepcopy(default_conf)
conf["exit_pricing"]["use_order_book"] = True
@@ -1533,8 +1474,8 @@ def test_process_deprecated_protections(default_conf, caplog):
assert not log_has(message, caplog)
config["protections"] = []
process_temporary_deprecated_settings(config)
assert log_has(message, caplog)
with pytest.raises(ConfigurationError, match=message):
process_temporary_deprecated_settings(config)
def test_flat_vars_to_nested_dict(caplog):