diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index d0c9e5f4f..90cdfcbcc 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -896,7 +896,8 @@ Sometimes it may be desired to lock a pair after certain events happen (e.g. mul Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until, [reason])`. `until` must be a datetime object in the future, after which trading will be re-enabled for that pair, while `reason` is an optional string detailing why the pair was locked. -Locks can also be lifted manually, by calling `self.unlock_pair(pair)`. +Locks can also be lifted manually, by calling `self.unlock_pair(pair)` or `self.unlock_reason()` - providing reason the pair was locked with. +`self.unlock_reason()` will unlock all pairs currently locked with the provided reason. To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index bc5ef961a..04f1d67b2 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -896,7 +896,7 @@ class PairLock(_DECL_BASE): lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT) lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT) return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, ' - f'lock_end_time={lock_end_time})') + f'lock_end_time={lock_end_time}, reason={self.reason}, active={self.active})') @staticmethod def query_pair_locks(pair: Optional[str], now: datetime) -> Query: @@ -905,7 +905,6 @@ class PairLock(_DECL_BASE): :param pair: Pair to check for. Returns all current locks if pair is empty :param now: Datetime object (generated via datetime.now(timezone.utc)). """ - filters = [PairLock.lock_end_time > now, # Only active locks PairLock.active.is_(True), ] diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 8662fc36d..afbd9781b 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -103,6 +103,36 @@ class PairLocks(): if PairLocks.use_db: PairLock.query.session.commit() + @staticmethod + def unlock_reason(reason: str, now: Optional[datetime] = None) -> None: + """ + Release all locks for this reason. + :param reason: Which reason to unlock + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.now(timezone.utc) + """ + if not now: + now = datetime.now(timezone.utc) + + if PairLocks.use_db: + # used in live modes + logger.info(f"Releasing all locks with reason '{reason}':") + filters = [PairLock.lock_end_time > now, + PairLock.active.is_(True), + PairLock.reason == reason + ] + locks = PairLock.query.filter(*filters) + for lock in locks: + logger.info(f"Releasing lock for {lock.pair} with reason '{reason}'.") + lock.active = False + PairLock.query.session.commit() + else: + # used in backtesting mode; don't show log messages for speed + locks = PairLocks.get_pair_locks(None) + for lock in locks: + if lock.reason == reason: + lock.active = False + @staticmethod def is_global_lock(now: Optional[datetime] = None) -> bool: """ @@ -128,7 +158,9 @@ class PairLocks(): @staticmethod def get_all_locks() -> List[PairLock]: - + """ + Return all locks, also locks with expired end date + """ if PairLocks.use_db: return PairLock.query.all() else: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 834ba5975..0bbfc8906 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -443,6 +443,15 @@ class IStrategy(ABC, HyperStrategyMixin): """ PairLocks.unlock_pair(pair, datetime.now(timezone.utc)) + def unlock_reason(self, reason: str) -> None: + """ + Unlocks all pairs previously locked using lock_pair with specified reason. + Not used by freqtrade itself, but intended to be used if users lock pairs + manually from within the strategy, to allow an easy way to unlock pairs. + :param reason: Unlock pairs to allow trading again + """ + PairLocks.unlock_reason(reason, datetime.now(timezone.utc)) + def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: """ Checks if a pair is currently locked diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index c694fd7c1..f9e5583ed 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -116,3 +116,28 @@ def test_PairLocks_getlongestlock(use_db): PairLocks.reset_locks() PairLocks.use_db = True + + +@pytest.mark.parametrize('use_db', (False, True)) +@pytest.mark.usefixtures("init_persistence") +def test_PairLocks_reason(use_db): + PairLocks.timeframe = '5m' + PairLocks.use_db = use_db + # No lock should be present + if use_db: + assert len(PairLock.query.all()) == 0 + + assert PairLocks.use_db == use_db + + PairLocks.lock_pair('XRP/USDT', arrow.utcnow().shift(minutes=4).datetime, 'TestLock1') + PairLocks.lock_pair('ETH/USDT', arrow.utcnow().shift(minutes=4).datetime, 'TestLock2') + + assert PairLocks.is_pair_locked('XRP/USDT') + assert PairLocks.is_pair_locked('ETH/USDT') + + PairLocks.unlock_reason('TestLock1') + assert not PairLocks.is_pair_locked('XRP/USDT') + assert PairLocks.is_pair_locked('ETH/USDT') + + PairLocks.reset_locks() + PairLocks.use_db = True diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index dcb9e3e64..ebd950fd6 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -575,6 +575,13 @@ def test_is_pair_locked(default_conf): strategy.unlock_pair(pair) assert not strategy.is_pair_locked(pair) + # Lock with reason + reason = "TestLockR" + strategy.lock_pair(pair, arrow.now(timezone.utc).shift(minutes=4).datetime, reason) + assert strategy.is_pair_locked(pair) + strategy.unlock_reason(reason) + assert not strategy.is_pair_locked(pair) + pair = 'BTC/USDT' # Lock until 14:30 lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc)