freqtrade_origin/freqtrade/persistence/pairlock_middleware.py

190 lines
6.3 KiB
Python
Raw Normal View History

2020-10-25 09:54:30 +00:00
import logging
from datetime import datetime, timezone
2023-03-16 05:44:53 +00:00
from typing import List, Optional, Sequence
2020-10-25 09:54:30 +00:00
2023-03-15 20:19:36 +00:00
from sqlalchemy import select
from freqtrade.exchange import timeframe_to_next_date
2020-10-27 07:09:18 +00:00
from freqtrade.persistence.models import PairLock
2020-10-25 09:54:30 +00:00
logger = logging.getLogger(__name__)
class PairLocks:
2020-10-25 09:54:30 +00:00
"""
2020-10-27 07:09:18 +00:00
Pairlocks middleware class
Abstracts the database layer away so it becomes optional - which will be necessary to support
backtesting and hyperopt in the future.
2020-10-25 09:54:30 +00:00
"""
use_db = True
locks: List[PairLock] = []
2024-05-12 14:48:11 +00:00
timeframe: str = ""
@staticmethod
def reset_locks() -> None:
"""
Resets all locks. Only active for backtesting mode.
"""
if not PairLocks.use_db:
PairLocks.locks = []
2020-10-25 09:54:30 +00:00
@staticmethod
2024-05-12 14:48:11 +00:00
def lock_pair(
pair: str,
until: datetime,
reason: Optional[str] = None,
*,
now: Optional[datetime] = None,
side: str = "*",
) -> PairLock:
2020-11-18 06:21:59 +00:00
"""
Create PairLock from now to "until".
Uses database by default, unless PairLocks.use_db is set to False,
in which case a list is maintained.
:param pair: pair to lock. use '*' to lock all pairs
:param until: End time of the lock. Will be rounded up to the next candle.
:param reason: Reason string that will be shown as reason for the lock
:param now: Current timestamp. Used to determine lock start time.
2022-04-24 09:23:26 +00:00
:param side: Side to lock pair, can be 'long', 'short' or '*'
2020-11-18 06:21:59 +00:00
"""
2020-10-25 09:54:30 +00:00
lock = PairLock(
pair=pair,
2020-11-18 06:21:59 +00:00
lock_time=now or datetime.now(timezone.utc),
lock_end_time=timeframe_to_next_date(PairLocks.timeframe, until),
2020-10-25 09:54:30 +00:00
reason=reason,
2022-04-24 09:24:15 +00:00
side=side,
2024-05-12 14:48:11 +00:00
active=True,
2020-10-25 09:54:30 +00:00
)
if PairLocks.use_db:
2023-03-15 20:12:06 +00:00
PairLock.session.add(lock)
PairLock.session.commit()
2020-10-25 09:54:30 +00:00
else:
PairLocks.locks.append(lock)
return lock
2020-10-25 09:54:30 +00:00
@staticmethod
2024-05-12 14:48:11 +00:00
def get_pair_locks(
pair: Optional[str], now: Optional[datetime] = None, side: str = "*"
) -> Sequence[PairLock]:
2020-10-25 09:54:30 +00:00
"""
Get all currently active locks for this pair
:param pair: Pair to check for. Returns all current locks if pair is empty
:param now: Datetime object (generated via datetime.now(timezone.utc)).
2020-10-27 07:09:18 +00:00
defaults to datetime.now(timezone.utc)
2020-10-25 09:54:30 +00:00
"""
if not now:
now = datetime.now(timezone.utc)
if PairLocks.use_db:
2022-04-24 09:23:26 +00:00
return PairLock.query_pair_locks(pair, now, side).all()
2020-10-25 09:54:30 +00:00
else:
2024-05-12 14:48:11 +00:00
locks = [
lock
for lock in PairLocks.locks
if (
lock.lock_end_time >= now
and lock.active is True
and (pair is None or lock.pair == pair)
and (lock.side == "*" or lock.side == side)
)
]
2020-10-25 09:54:30 +00:00
return locks
2020-11-25 10:54:11 +00:00
@staticmethod
def get_pair_longest_lock(
2024-05-12 14:48:11 +00:00
pair: str, now: Optional[datetime] = None, side: str = "*"
) -> Optional[PairLock]:
2020-11-25 10:54:11 +00:00
"""
Get the lock that expires the latest for the pair given.
"""
locks = PairLocks.get_pair_locks(pair, now, side=side)
locks = sorted(locks, key=lambda lock: lock.lock_end_time, reverse=True)
2020-11-25 10:54:11 +00:00
return locks[0] if locks else None
2020-10-25 09:54:30 +00:00
@staticmethod
2024-05-12 14:48:11 +00:00
def unlock_pair(pair: str, now: Optional[datetime] = None, side: str = "*") -> None:
2020-10-25 09:54:30 +00:00
"""
Release all locks for this pair.
:param pair: Pair 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)
logger.info(f"Releasing all locks for {pair}.")
locks = PairLocks.get_pair_locks(pair, now, side=side)
2020-10-25 09:54:30 +00:00
for lock in locks:
lock.active = False
if PairLocks.use_db:
2023-03-15 20:12:06 +00:00
PairLock.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}':")
2024-05-12 14:48:11 +00:00
filters = [
PairLock.lock_end_time > now,
PairLock.active.is_(True),
PairLock.reason == reason,
]
2023-03-15 20:19:36 +00:00
locks = PairLock.session.scalars(select(PairLock).filter(*filters)).all()
for lock in locks:
logger.info(f"Releasing lock for {lock.pair} with reason '{reason}'.")
lock.active = False
2023-03-15 20:12:06 +00:00
PairLock.session.commit()
else:
# used in backtesting mode; don't show log messages for speed
2023-02-21 05:49:15 +00:00
locksb = PairLocks.get_pair_locks(None)
for lock in locksb:
if lock.reason == reason:
lock.active = False
2020-10-25 09:54:30 +00:00
@staticmethod
2024-05-12 14:48:11 +00:00
def is_global_lock(now: Optional[datetime] = None, side: str = "*") -> bool:
2020-10-25 09:54:30 +00:00
"""
:param now: Datetime object (generated via datetime.now(timezone.utc)).
defaults to datetime.now(timezone.utc)
"""
if not now:
now = datetime.now(timezone.utc)
2024-05-12 14:48:11 +00:00
return len(PairLocks.get_pair_locks("*", now, side)) > 0
2020-10-25 09:54:30 +00:00
@staticmethod
2024-05-12 14:48:11 +00:00
def is_pair_locked(pair: str, now: Optional[datetime] = None, side: str = "*") -> bool:
2020-10-25 09:54:30 +00:00
"""
:param pair: Pair to check for
:param now: Datetime object (generated via datetime.now(timezone.utc)).
defaults to datetime.now(timezone.utc)
"""
if not now:
now = datetime.now(timezone.utc)
2024-05-12 14:48:11 +00:00
return len(PairLocks.get_pair_locks(pair, now, side)) > 0 or PairLocks.is_global_lock(
now, side
2022-04-24 09:23:26 +00:00
)
@staticmethod
2023-03-16 05:44:53 +00:00
def get_all_locks() -> Sequence[PairLock]:
"""
Return all locks, also locks with expired end date
"""
if PairLocks.use_db:
2023-03-16 05:48:12 +00:00
return PairLock.get_all_locks().all()
else:
return PairLocks.locks