diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 25d59a992..2f704d83f 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -21,10 +21,14 @@ All protection end times are rounded up to the next candle to avoid sudden, unex ### Common settings to all Protections -* `method` - Protection name to use. -* `stop_duration` (minutes) - how long should protections be locked. -* `lookback_period` (minutes) - Only trades that completed after `current_time - lookback_period` will be considered (may be ignored by some Protections). -* `trade_limit` - How many trades are required at minimum (not used by all Protections). +| Parameter| Description | +|------------|-------------| +| method | Protection name to use.
**Datatype:** String, selected from [available Protections](#available-protections) +| stop_duration_candles | For how many candles should the lock be set?
**Datatype:** Positive integer (in candles) +| stop_duration | how many minutes should protections be locked.
Cannot be used together with `stop_duration_candles`.
**Datatype:** Float (in minutes) +| `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections.
**Datatype:** Positive integer (in candles). +| lookback_period | Only trades that completed after `current_time - lookback_period` will be considered.
Cannot be used together with `lookback_period_candles`.
This setting may be ignored by some Protections.
**Datatype:** Float (in minutes) +| trade_limit | Number of trades required at minimum (not used by all Protections).
**Datatype:** Positive integer #### Stoploss Guard diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index ab21bc686..a6435d0e6 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -74,6 +74,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: _validate_trailing_stoploss(conf) _validate_edge(conf) _validate_whitelist(conf) + _validate_protections(conf) _validate_unlimited_amount(conf) # validate configuration before returning @@ -155,3 +156,22 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None: if (pl.get('method') == 'StaticPairList' and not conf.get('exchange', {}).get('pair_whitelist')): raise OperationalException("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', []): + if ('stop_duration' in prot and 'stop_duration_candles' in prot): + raise OperationalException( + "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_candle' in prot): + raise OperationalException( + "Protections must specify either `lookback_period` or `lookback_period_candles`.\n" + f"Please fix the protection {prot.get('method')}" + ) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index dfc21b678..e7d7e80f6 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -204,8 +204,10 @@ CONF_SCHEMA = { 'properties': { 'method': {'type': 'string', 'enum': AVAILABLE_PROTECTIONS}, 'stop_duration': {'type': 'number', 'minimum': 0.0}, + 'stop_duration_candles': {'type': 'number', 'minimum': 0}, 'trade_limit': {'type': 'number', 'minimum': 1}, 'lookback_period': {'type': 'number', 'minimum': 1}, + 'lookback_period_candles': {'type': 'number', 'minimum': 1}, }, 'required': ['method'], } diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index e5bbec431..29ff4e069 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -103,6 +103,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair "method": "StoplossGuard", "lookback_period": 60, "trade_limit": 1, + "stop_duration": 60, "only_per_pair": only_per_pair }] freqtrade = get_patched_freqtradebot(mocker, default_conf) @@ -158,7 +159,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair def test_CooldownPeriod(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ "method": "CooldownPeriod", - "stopduration": 60, + "stop_duration": 60, }] freqtrade = get_patched_freqtradebot(mocker, default_conf) message = r"Trading stopped due to .*" @@ -195,7 +196,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ "method": "LowProfitPairs", "lookback_period": 400, - "stopduration": 60, + "stop_duration": 60, "trade_limit": 2, "required_profit": 0.0, }] @@ -254,7 +255,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ "method": "MaxDrawdown", "lookback_period": 1000, - "stopduration": 60, + "stop_duration": 60, "trade_limit": 3, "max_allowed_drawdown": 0.15 }] @@ -315,21 +316,21 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ - ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, + ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2, "stop_duration": 60}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " "2 stoplosses within 60 minutes.'}]", None ), - ({"method": "CooldownPeriod", "stopduration": 60}, + ({"method": "CooldownPeriod", "stop_duration": 60}, "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 60 min.'}]", None ), - ({"method": "LowProfitPairs", "stopduration": 60}, + ({"method": "LowProfitPairs", "lookback_period": 60, "stop_duration": 60}, "[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with " "profit < 0.0 within 60 minutes.'}]", None ), - ({"method": "MaxDrawdown", "stopduration": 60}, + ({"method": "MaxDrawdown", "lookback_period": 60, "stop_duration": 60}, "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading if drawdown is > 0.0 " "within 60 minutes.'}]", None diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 167215f29..283f6a0f9 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -879,6 +879,24 @@ 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_candle": 20, "stop_duration": 10}], None), + ([{"method": "StoplossGuard", "lookback_period_candle": 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`.*'), +]) +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_load_config_test_comments() -> None: """