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