From dce236467224f4d80ce7c6c90927796fdff54722 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 11:11:55 +0100 Subject: [PATCH] Add stoploss per pair support --- docs/includes/protections.md | 4 +- .../plugins/protections/stoploss_guard.py | 3 + tests/plugins/test_protections.py | 58 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 91b10cf65..644b98e64 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -17,6 +17,7 @@ Protections will protect your strategy from unexpected events and market conditi #### Stoploss Guard `StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. +This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time. ```json "protections": [ @@ -24,7 +25,8 @@ Protections will protect your strategy from unexpected events and market conditi "method": "StoplossGuard", "lookback_period": 60, "trade_limit": 4, - "stop_duration": 60 + "stop_duration": 60, + "only_per_pair": false } ], ``` diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 0645d366b..1ad839f3d 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -26,6 +26,7 @@ class StoplossGuard(IProtection): self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 10) self._stop_duration = protection_config.get('stop_duration', 60) + self._disable_global_stop = protection_config.get('only_per_pair', False) def short_desc(self) -> str: """ @@ -72,6 +73,8 @@ class StoplossGuard(IProtection): :return: Tuple of [bool, until, reason]. If true, all pairs will be locked with until """ + if self._disable_global_stop: + return False, None, None return self._stoploss_guard(date_now, None) def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index ce0ad7d5e..7eac737ef 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -95,6 +95,64 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert PairLocks.is_global_lock() +@pytest.mark.parametrize('only_per_pair', [False, True]) +@pytest.mark.usefixtures("init_persistence") +def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair): + default_conf['protections'] = [{ + "method": "StoplossGuard", + "lookback_period": 60, + "trade_limit": 1, + "only_per_pair": only_per_pair + }] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + message = r"Trading stopped due to .*" + pair = 'XRP/BTC' + assert not freqtrade.protections.stop_per_pair(pair) + assert not freqtrade.protections.global_stop() + assert not log_has_re(message, caplog) + caplog.clear() + + Trade.session.add(generate_mock_trade( + pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=200, min_ago_close=30, profit_rate=0.9, + )) + + assert not freqtrade.protections.stop_per_pair(pair) + assert not freqtrade.protections.global_stop() + assert not log_has_re(message, caplog) + caplog.clear() + # This trade does not count, as it's closed too long ago + Trade.session.add(generate_mock_trade( + pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=250, min_ago_close=100, profit_rate=0.9, + )) + # Trade does not count for per pair stop as it's the wrong pair. + Trade.session.add(generate_mock_trade( + 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=240, min_ago_close=30, profit_rate=0.9, + )) + # 3 Trades closed - but the 2nd has been closed too long ago. + assert not freqtrade.protections.stop_per_pair(pair) + assert freqtrade.protections.global_stop() != only_per_pair + if not only_per_pair: + assert log_has_re(message, caplog) + else: + assert not log_has_re(message, caplog) + + caplog.clear() + + # 2nd Trade that counts with correct pair + Trade.session.add(generate_mock_trade( + pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=180, min_ago_close=30, profit_rate=0.9, + )) + + assert freqtrade.protections.stop_per_pair(pair) + assert freqtrade.protections.global_stop() != only_per_pair + assert PairLocks.is_pair_locked(pair) + assert PairLocks.is_global_lock() != only_per_pair + + @pytest.mark.usefixtures("init_persistence") def test_CooldownPeriod(mocker, default_conf, fee, caplog): default_conf['protections'] = [{