diff --git a/docs/includes/protections.md b/docs/includes/protections.md index e0ad8189f..736148cc4 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -25,6 +25,8 @@ All protection end times are rounded up to the next candle to avoid sudden, unex * [`MaxDrawdown`](#maxdrawdown) Stop trading if max-drawdown is reached. * [`LowProfitPairs`](#low-profit-pairs) Lock pairs with low profits * [`CooldownPeriod`](#cooldown-period) Don't enter a trade right after selling a trade. +* [`ProfitLimit`](#profit-limit) Stop trading once a set profit limit is reached + ### Common settings to all Protections @@ -139,6 +141,27 @@ def protections(self): This Protection applies only at pair-level, and will never lock all pairs globally. This Protection does not consider `lookback_period` as it only looks at the latest trade. +#### Profit Limit + +`ProfitLimit` 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 greater than `profit_limit`, all pairs will be locked for `stop_duration` in minutes (or in candles when using `stop_duration_candles`). + +The below example will stop trading a pair for 180 minutes if the total profit from all trades reaches 1% (and a minimum of 3 trades) within the last 6 candles. + +``` python +@property +def protections(self): + return [ + { + "method": "ProfitLimit", + "lookback_period_candles": 6, + "trade_limit": 3, + "stop_duration": 180, + "profit_limit": 0.01 + } + ] +``` + ### Full example of Protections All protections can be combined at will, also with different parameters, creating a increasing wall for under-performing pairs. @@ -151,6 +174,7 @@ The below example assumes a timeframe of 1 hour: * Stops trading if more than 4 stoploss occur for all pairs within a 1 day (`24 * 1h candles`) limit (`StoplossGuard`). * Locks all pairs that had 4 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`). * Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades. +* Locks all pairs for 360 minutes once a profit of 0.02 (2%) is achieved within the last 800 candles , a minimum of 2 trades. ``` python from freqtrade.strategy import IStrategy @@ -192,6 +216,13 @@ class AwesomeStrategy(IStrategy) "trade_limit": 4, "stop_duration_candles": 2, "required_profit": 0.01 + }, + { + "method": "ProfitLimit", + "lookback_period_candles": 800, + "trade_limit": 2, + "stop_duration": 360, + "profit_limit": 0.02 } ] # ... diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 70f60867b..986ea8d0c 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -35,7 +35,8 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter'] -AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] +AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', + 'StoplossGuard', 'ProfitLimit'] AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5'] AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['feather', 'parquet'] BACKTEST_BREAKDOWNS = ['day', 'week', 'month'] diff --git a/freqtrade/plugins/protections/profit_limit.py b/freqtrade/plugins/protections/profit_limit.py new file mode 100644 index 000000000..ed481e27f --- /dev/null +++ b/freqtrade/plugins/protections/profit_limit.py @@ -0,0 +1,87 @@ + +import logging +from datetime import datetime, timedelta +from typing import Any, Dict, Optional + +from freqtrade.constants import Config, LongShort +from freqtrade.persistence import Trade +from freqtrade.plugins.protections import IProtection, ProtectionReturn + + +logger = logging.getLogger(__name__) + + +class ProfitLimit(IProtection): + + has_global_stop: bool = True + has_local_stop: bool = False + + def __init__(self, config: Config, protection_config: Dict[str, Any]) -> None: + super().__init__(config, protection_config) + + self._trade_limit = protection_config.get('trade_limit', 1) + self._required_profit = protection_config.get('profit_limit', 1.0) + + def short_desc(self) -> str: + """ + Short method description - used for startup-messages + """ + return (f"{self.name} - Profit Limit Protection, locks all pairs when " + f"profit > {self._required_profit} within {self.lookback_period_str}.") + + def _reason(self, profit: float) -> str: + """ + LockReason to use + """ + return (f'{profit} > {self._required_profit} in {self.lookback_period_str}, ' + f'locking for {self.stop_duration_str}.') + + def _limit_profit( + self, date_now: datetime) -> Optional[ProtectionReturn]: + """ + Evaluate recent trades for pair + """ + look_back_until = date_now - timedelta(minutes=self._lookback_period) + + trades = Trade.get_trades_proxy(is_open=False, close_date=look_back_until) + + if len(trades) < self._trade_limit: + # Not enough trades in the relevant period + return None + + profit_sum = sum(trade.close_profit_abs for trade in trades if trade.close_profit_abs) + stake_sum = sum(trade.stake_amount for trade in trades) + profit_ratio = profit_sum / stake_sum + + if profit_ratio >= self._required_profit: + self.log_once( + f"Trading stopped due to {profit_ratio:.2f} >= {self._required_profit} " + f"within {self._lookback_period} minutes.", logger.info) + until = self.calculate_lock_end(trades, self._stop_duration) + + return ProtectionReturn( + lock=True, + until=until, + reason=self._reason(profit_ratio) + ) + + return None + + def global_stop(self, date_now: datetime, side: LongShort) -> Optional[ProtectionReturn]: + """ + Stops trading (position entering) for all pairs + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, all pairs will be locked with until + """ + return self._limit_profit(date_now) + + def stop_per_pair( + self, pair: str, date_now: datetime, side: LongShort) -> Optional[ProtectionReturn]: + """ + Stops trading (position entering) for this pair + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, this pair will be locked with until + """ + return None