From 6764b317eb3bba09b3ea59bb4f25b1f633944403 Mon Sep 17 00:00:00 2001 From: smarmau <42020297+smarmau@users.noreply.github.com> Date: Thu, 29 Sep 2022 21:07:42 +1000 Subject: [PATCH 01/10] Add files via upload --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index e14e81343..6803151ba 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -35,7 +35,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', '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'] From e5c5c84d62ea332db68b520642af1a271fe544c7 Mon Sep 17 00:00:00 2001 From: smarmau <42020297+smarmau@users.noreply.github.com> Date: Thu, 29 Sep 2022 21:08:49 +1000 Subject: [PATCH 02/10] Add files via upload --- freqtrade/plugins/protections/profit_limit.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 freqtrade/plugins/protections/profit_limit.py diff --git a/freqtrade/plugins/protections/profit_limit.py b/freqtrade/plugins/protections/profit_limit.py new file mode 100644 index 000000000..5ffb5bb5c --- /dev/null +++ b/freqtrade/plugins/protections/profit_limit.py @@ -0,0 +1,92 @@ + +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) + # filters = [ + # Trade.is_open.is_(False), + # Trade.close_date > look_back_until, + # ] + # if pair: + # filters.append(Trade.pair == pair) + + trades = Trade.get_trades_proxy(is_open=False, close_date=look_back_until) + # trades = Trade.get_trades(filters).all() + if len(trades) < self._trade_limit: + # Not enough trades in the relevant period + return None + + profit = sum( + trade.close_profit for trade in trades if trade.close_profit + ) + if profit >= self._required_profit: + self.log_once( + f"Trading stopped due to {profit:.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) + ) + + 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 From e2dcb93445a803fb1ef9d9d3a0a072158dba61af Mon Sep 17 00:00:00 2001 From: smarmau <42020297+smarmau@users.noreply.github.com> Date: Thu, 29 Sep 2022 21:11:15 +1000 Subject: [PATCH 03/10] Add files via upload --- docs/includes/protections.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index e0ad8189f..a7f6d4d91 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`](#profitlimit) 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 } ] # ... From 0384da5c9b422b83da323c05c925fb5d672c16c2 Mon Sep 17 00:00:00 2001 From: smarmau <42020297+smarmau@users.noreply.github.com> Date: Thu, 29 Sep 2022 21:13:01 +1000 Subject: [PATCH 04/10] Update protections.md --- docs/includes/protections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index a7f6d4d91..736148cc4 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -25,7 +25,7 @@ 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`](#profitlimit) Stop trading once a set profit limit is reached +* [`ProfitLimit`](#profit-limit) Stop trading once a set profit limit is reached ### Common settings to all Protections From e3012db2bb98b239cf02929a23efb5b4ebe928fd Mon Sep 17 00:00:00 2001 From: smarmau <42020297+smarmau@users.noreply.github.com> Date: Thu, 29 Sep 2022 21:37:25 +1000 Subject: [PATCH 05/10] Update constants.py made flake8 compliant --- freqtrade/constants.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 6803151ba..08b4e5a26 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -35,7 +35,8 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter'] -AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard', 'ProfitLimit'] +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'] From a0b7e4d4c8f19ed1aa780a056813b436b4e52e19 Mon Sep 17 00:00:00 2001 From: smarmau <42020297+smarmau@users.noreply.github.com> Date: Thu, 29 Sep 2022 21:40:08 +1000 Subject: [PATCH 06/10] Update profit_limit.py make flake8 compliant --- freqtrade/plugins/protections/profit_limit.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/plugins/protections/profit_limit.py b/freqtrade/plugins/protections/profit_limit.py index 5ffb5bb5c..ddf165282 100644 --- a/freqtrade/plugins/protections/profit_limit.py +++ b/freqtrade/plugins/protections/profit_limit.py @@ -1,4 +1,3 @@ - import logging from datetime import datetime, timedelta from typing import Any, Dict, Optional @@ -21,7 +20,7 @@ class ProfitLimit(IProtection): 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 From be99f5d82d09303495abc7f7d875715cb80a01e1 Mon Sep 17 00:00:00 2001 From: smarmau <42020297+smarmau@users.noreply.github.com> Date: Sun, 2 Oct 2022 21:05:05 +1100 Subject: [PATCH 07/10] updates profit_limit xmatthias suggestions for calc --- freqtrade/plugins/protections/profit_limit.py | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/freqtrade/plugins/protections/profit_limit.py b/freqtrade/plugins/protections/profit_limit.py index ddf165282..db2a4e166 100644 --- a/freqtrade/plugins/protections/profit_limit.py +++ b/freqtrade/plugins/protections/profit_limit.py @@ -1,15 +1,15 @@ + import logging from datetime import datetime, timedelta +from turtle import pd 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 @@ -20,7 +20,7 @@ class ProfitLimit(IProtection): 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 @@ -41,32 +41,27 @@ class ProfitLimit(IProtection): Evaluate recent trades for pair """ look_back_until = date_now - timedelta(minutes=self._lookback_period) - # filters = [ - # Trade.is_open.is_(False), - # Trade.close_date > look_back_until, - # ] - # if pair: - # filters.append(Trade.pair == pair) trades = Trade.get_trades_proxy(is_open=False, close_date=look_back_until) - # trades = Trade.get_trades(filters).all() + if len(trades) < self._trade_limit: # Not enough trades in the relevant period return None - profit = sum( - trade.close_profit for trade in trades if trade.close_profit - ) - if profit >= self._required_profit: + profit_sum = trades['profit_abs'].sum() + stake_sum = trades['stake_amount'].sum() + profit_ratio = profit_sum / stake_sum + + if profit_ratio >= self._required_profit: self.log_once( - f"Trading stopped due to {profit:.2f} >= {self._required_profit} " + 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) + reason=self._reason(profit_ratio) ) return None From 256588af484a96786ac65c22c62823d421d72b64 Mon Sep 17 00:00:00 2001 From: smarmau <42020297+smarmau@users.noreply.github.com> Date: Sun, 2 Oct 2022 21:08:44 +1100 Subject: [PATCH 08/10] Update profit_limit.py --- freqtrade/plugins/protections/profit_limit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/plugins/protections/profit_limit.py b/freqtrade/plugins/protections/profit_limit.py index db2a4e166..17d10d467 100644 --- a/freqtrade/plugins/protections/profit_limit.py +++ b/freqtrade/plugins/protections/profit_limit.py @@ -1,7 +1,6 @@ import logging from datetime import datetime, timedelta -from turtle import pd from typing import Any, Dict, Optional from freqtrade.constants import Config, LongShort From 729ccdb2d788d8c5ede442884269197d0fd525c0 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 4 Oct 2022 21:20:02 +0200 Subject: [PATCH 09/10] fix flake8 and mypy --- freqtrade/constants.py | 2 +- freqtrade/plugins/protections/profit_limit.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 0a49bca13..cf4f11e7c 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -36,7 +36,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter'] AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', - 'StoplossGuard', 'ProfitLimit'] + '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 index 17d10d467..ca49795a7 100644 --- a/freqtrade/plugins/protections/profit_limit.py +++ b/freqtrade/plugins/protections/profit_limit.py @@ -9,6 +9,7 @@ from freqtrade.plugins.protections import IProtection, ProtectionReturn logger = logging.getLogger(__name__) + class ProfitLimit(IProtection): has_global_stop: bool = True @@ -16,7 +17,7 @@ class ProfitLimit(IProtection): 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) @@ -47,10 +48,10 @@ class ProfitLimit(IProtection): # Not enough trades in the relevant period return None - profit_sum = trades['profit_abs'].sum() - stake_sum = trades['stake_amount'].sum() + 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} " From 423645652b7b6b5bc36a4c49ab9f8bfb3755a4d5 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 5 Oct 2022 09:23:47 +0200 Subject: [PATCH 10/10] isort --- freqtrade/plugins/protections/profit_limit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/plugins/protections/profit_limit.py b/freqtrade/plugins/protections/profit_limit.py index ca49795a7..ed481e27f 100644 --- a/freqtrade/plugins/protections/profit_limit.py +++ b/freqtrade/plugins/protections/profit_limit.py @@ -7,6 +7,7 @@ from freqtrade.constants import Config, LongShort from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn + logger = logging.getLogger(__name__)