diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index e8a920908..e5625733c 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -40,7 +40,7 @@ class MaxDrawdown(IProtection): return (f'{drawdown} > {self._max_allowed_drawdown} in {self._lookback_period} min, ' f'locking for {self._stop_duration} min.') - def _max_drawdown(self, date_now: datetime, pair: str) -> ProtectionReturn: + def _max_drawdown(self, date_now: datetime) -> ProtectionReturn: """ Evaluate recent trades for drawdown ... """ @@ -49,23 +49,21 @@ class MaxDrawdown(IProtection): Trade.is_open.is_(False), Trade.close_date > look_back_until, ] - if pair: - filters.append(Trade.pair == pair) trades = Trade.get_trades(filters).all() - trades_df = pd.DataFrame(trades) + trades_df = pd.DataFrame([trade.to_json() for trade in trades]) if len(trades) < self._trade_limit: # Not enough trades in the relevant period return False, None, None # Drawdown is always positive - drawdown, _, _ = calculate_max_drawdown(trades_df) + drawdown, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') if drawdown > self._max_allowed_drawdown: self.log_once( - f"Trading for {pair} stopped due to {drawdown:.2f} < {self._max_allowed_drawdown} " - f"within {self._lookback_period} minutes.", logger.info) + f"Trading stopped due to Max Drawdown {drawdown:.2f} < {self._max_allowed_drawdown}" + f" within {self._lookback_period} minutes.", logger.info) until = self.calculate_lock_end(trades, self._stop_duration) return True, until, self._reason(drawdown) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 24594c3ac..e5bbec431 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -46,7 +46,7 @@ def test_protectionmanager(mocker, default_conf): if not handler.has_global_stop: assert handler.global_stop(datetime.utcnow()) == (False, None, None) if not handler.has_local_stop: - assert handler.local_stop('XRP/BTC', datetime.utcnow()) == (False, None, None) + assert handler.stop_per_pair('XRP/BTC', datetime.utcnow()) == (False, None, None) @pytest.mark.usefixtures("init_persistence") @@ -249,6 +249,71 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() +@pytest.mark.usefixtures("init_persistence") +def test_MaxDrawdown(mocker, default_conf, fee, caplog): + default_conf['protections'] = [{ + "method": "MaxDrawdown", + "lookback_period": 1000, + "stopduration": 60, + "trade_limit": 3, + "max_allowed_drawdown": 0.15 + }] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + message = r"Trading stopped due to Max.*" + + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + caplog.clear() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=1000, min_ago_close=900, profit_rate=1.1, + )) + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=500, min_ago_close=400, profit_rate=0.9, + )) + # Not locked with one trade + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_global_lock() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=1200, min_ago_close=1100, profit_rate=0.5, + )) + + # Not locked with 1 trade (2nd trade is outside of lookback_period) + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_global_lock() + assert not log_has_re(message, caplog) + + # Winning trade ... (should not lock, does not change drawdown!) + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, + min_ago_open=320, min_ago_close=410, profit_rate=1.5, + )) + assert not freqtrade.protections.global_stop() + assert not PairLocks.is_global_lock() + + caplog.clear() + + # Add additional negative trade, causing a loss of > 15% + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, + min_ago_open=20, min_ago_close=10, profit_rate=0.8, + )) + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + # local lock not supported + assert not PairLocks.is_pair_locked('XRP/BTC') + assert freqtrade.protections.global_stop() + assert PairLocks.is_global_lock() + assert log_has_re(message, caplog) + + @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " @@ -264,6 +329,11 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): "profit < 0.0 within 60 minutes.'}]", None ), + ({"method": "MaxDrawdown", "stopduration": 60}, + "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading if drawdown is > 0.0 " + "within 60 minutes.'}]", + None + ), ]) def test_protection_manager_desc(mocker, default_conf, protectionconf, desc_expected, exception_expected):