diff --git a/docs/includes/protections.md b/docs/includes/protections.md index bb4a7eb35..d67924cfe 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -96,6 +96,8 @@ def protections(self): `LowProfitPairs` uses all trades for a pair within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the overall profit ratio. If that ratio is below `required_profit`, that pair will be locked for `stop_duration` in minutes (or in candles when using `stop_duration_candles`). +For futures bots, setting `only_per_side` will make the bot only consider one side, and will then only lock this one side, allowing for example shorts to continue after a series of long losses. + The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 candles. ``` python @@ -107,7 +109,8 @@ def protections(self): "lookback_period_candles": 6, "trade_limit": 2, "stop_duration": 60, - "required_profit": 0.02 + "required_profit": 0.02, + "only_per_pair": False, } ] ``` diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 7d5d6054d..099242b8d 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -21,6 +21,7 @@ class LowProfitPairs(IProtection): self._trade_limit = protection_config.get('trade_limit', 1) self._required_profit = protection_config.get('required_profit', 0.0) + self._only_per_side = protection_config.get('only_per_side', False) def short_desc(self) -> str: """ @@ -36,7 +37,8 @@ class LowProfitPairs(IProtection): return (f'{profit} < {self._required_profit} in {self.lookback_period_str}, ' f'locking for {self.stop_duration_str}.') - def _low_profit(self, date_now: datetime, pair: str) -> Optional[ProtectionReturn]: + def _low_profit( + self, date_now: datetime, pair: str, side: LongShort) -> Optional[ProtectionReturn]: """ Evaluate recent trades for pair """ @@ -54,7 +56,10 @@ class LowProfitPairs(IProtection): # Not enough trades in the relevant period return None - profit = sum(trade.close_profit for trade in trades if trade.close_profit) + profit = sum( + trade.close_profit for trade in trades if trade.close_profit + and (not self._only_per_side or trade.trade_direction == side) + ) if profit < self._required_profit: self.log_once( f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} " @@ -65,6 +70,7 @@ class LowProfitPairs(IProtection): lock=True, until=until, reason=self._reason(profit), + lock_side=(side if self._only_per_side else '*') ) return None @@ -86,4 +92,4 @@ class LowProfitPairs(IProtection): :return: Tuple of [bool, until, reason]. If true, this pair will be locked with until """ - return self._low_profit(date_now, pair=pair) + return self._low_profit(date_now, pair=pair, side=side) diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index f9fe039d6..713a2da07 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -38,8 +38,8 @@ class StoplossGuard(IProtection): return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, ' f'locking for {self._stop_duration} min.') - def _stoploss_guard( - self, date_now: datetime, pair: Optional[str], side: str) -> Optional[ProtectionReturn]: + def _stoploss_guard(self, date_now: datetime, pair: Optional[str], + side: LongShort) -> Optional[ProtectionReturn]: """ Evaluate recent trades """ diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index b2dc99610..172e1f077 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -250,14 +250,16 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() +@pytest.mark.parametrize('only_per_side', [False, True]) @pytest.mark.usefixtures("init_persistence") -def test_LowProfitPairs(mocker, default_conf, fee, caplog): +def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side): default_conf['protections'] = [{ "method": "LowProfitPairs", "lookback_period": 400, "stop_duration": 60, "trade_limit": 2, "required_profit": 0.0, + "only_per_side": only_per_side, }] freqtrade = get_patched_freqtradebot(mocker, default_conf) message = r"Trading stopped due to .*" @@ -292,10 +294,11 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): # Add positive trade Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value, - min_ago_open=20, min_ago_close=10, profit_rate=1.15, + min_ago_open=20, min_ago_close=10, profit_rate=1.15, is_short=True )) - assert not freqtrade.protections.stop_per_pair('XRP/BTC') - assert not PairLocks.is_pair_locked('XRP/BTC') + assert freqtrade.protections.stop_per_pair('XRP/BTC') != only_per_side + assert not PairLocks.is_pair_locked('XRP/BTC', side='*') + assert PairLocks.is_pair_locked('XRP/BTC', side='long') == only_per_side Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, @@ -303,9 +306,10 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): )) # Locks due to 2nd trade - assert not freqtrade.protections.global_stop() - assert freqtrade.protections.stop_per_pair('XRP/BTC') - assert PairLocks.is_pair_locked('XRP/BTC') + assert freqtrade.protections.global_stop() != only_per_side + assert freqtrade.protections.stop_per_pair('XRP/BTC') != only_per_side + assert PairLocks.is_pair_locked('XRP/BTC', side='long') + assert PairLocks.is_pair_locked('XRP/BTC', side='*') != only_per_side assert not PairLocks.is_global_lock()