parent
6eab20e337
commit
e513871fd5
@ -693,15 +693,15 @@ Locked pairs will show the message `Pair <pair> is currently locked.`.
|
||||
|
||||
Sometimes it may be desired to lock a pair after certain events happen (e.g. multiple losing trades in a row).
|
||||
|
||||
Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until)`.
|
||||
`until` must be a datetime object in the future, after which trading will be reenabled for that pair.
|
||||
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)`.
|
||||
|
||||
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
||||
|
||||
!!! Note
|
||||
Locked pairs are not persisted, so a restart of the bot, or calling `/reload_config` will reset locked pairs.
|
||||
Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished.
|
||||
|
||||
!!! Warning
|
||||
Locking pairs is not functioning during backtesting.
|
||||
|
@ -937,8 +937,8 @@ class FreqtradeBot:
|
||||
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
|
||||
stoploss_order=True)
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair,
|
||||
timeframe_to_next_date(self.config['timeframe']))
|
||||
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']),
|
||||
reason='auto_lock_1_candle')
|
||||
self._notify_sell(trade, "stoploss")
|
||||
return True
|
||||
|
||||
@ -1264,7 +1264,8 @@ class FreqtradeBot:
|
||||
Trade.session.flush()
|
||||
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']))
|
||||
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']),
|
||||
reason='auto_lock_1_candle')
|
||||
|
||||
self._notify_sell(trade, order_type)
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
# flake8: noqa: F401
|
||||
|
||||
from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db
|
||||
from freqtrade.persistence.models import (Order, PairLock, Trade, clean_dry_run_db, cleanup_db,
|
||||
init_db)
|
||||
|
@ -64,6 +64,9 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None:
|
||||
# Copy session attributes to order object too
|
||||
Order.session = Trade.session
|
||||
Order.query = Order.session.query_property()
|
||||
PairLock.session = Trade.session
|
||||
PairLock.query = PairLock.session.query_property()
|
||||
|
||||
previous_tables = inspect(engine).get_table_names()
|
||||
_DECL_BASE.metadata.create_all(engine)
|
||||
check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables)
|
||||
@ -655,3 +658,76 @@ class Trade(_DECL_BASE):
|
||||
trade.stop_loss = None
|
||||
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
|
||||
logger.info(f"New stoploss: {trade.stop_loss}.")
|
||||
|
||||
|
||||
class PairLock(_DECL_BASE):
|
||||
"""
|
||||
Pair Locks database model.
|
||||
"""
|
||||
__tablename__ = 'pair_lock'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
pair = Column(String, nullable=False)
|
||||
reason = Column(String, nullable=True)
|
||||
# Time the pair was locked (start time)
|
||||
lock_time = Column(DateTime, nullable=False)
|
||||
# Time until the pair is locked (end time)
|
||||
lock_end_time = Column(DateTime, nullable=False)
|
||||
|
||||
active = Column(Boolean, nullable=False, default=True)
|
||||
|
||||
def __repr__(self):
|
||||
lock_time = self.open_date.strftime(DATETIME_PRINT_FORMAT)
|
||||
lock_end_time = self.open_date.strftime(DATETIME_PRINT_FORMAT)
|
||||
return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, '
|
||||
f'lock_end_time={lock_end_time})')
|
||||
|
||||
@staticmethod
|
||||
def get_pair_locks(pair: str, now: Optional[datetime] = None) -> List['PairLock']:
|
||||
"""
|
||||
Get all locks for this pair
|
||||
:param pair: Pair to check for
|
||||
:param now: Datetime object (generated via datetime.utcnow()). defaults to datetime.utcnow()
|
||||
"""
|
||||
if not now:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
return PairLock.query.filter(
|
||||
PairLock.pair == pair,
|
||||
func.datetime(PairLock.lock_time) <= now,
|
||||
func.datetime(PairLock.lock_end_time) >= now,
|
||||
# Only active locks
|
||||
PairLock.active.is_(True),
|
||||
).all()
|
||||
|
||||
@staticmethod
|
||||
def unlock_pair(pair: str, now: Optional[datetime] = None) -> None:
|
||||
"""
|
||||
Release all locks for this pair.
|
||||
"""
|
||||
if not now:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
logger.info(f"Releasing all locks for {pair}.")
|
||||
locks = PairLock.get_pair_locks(pair, now)
|
||||
for lock in locks:
|
||||
lock.active = False
|
||||
PairLock.session.flush()
|
||||
|
||||
@staticmethod
|
||||
def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool:
|
||||
"""
|
||||
:param pair: Pair to check for
|
||||
:param now: Datetime object (generated via datetime.utcnow()). defaults to datetime.utcnow()
|
||||
"""
|
||||
if not now:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
return PairLock.query.filter(
|
||||
PairLock.pair == pair,
|
||||
func.datetime(PairLock.lock_time) <= now,
|
||||
func.datetime(PairLock.lock_end_time) >= now,
|
||||
# Only active locks
|
||||
PairLock.active.is_(True),
|
||||
).scalar() is not None
|
||||
|
@ -17,7 +17,7 @@ from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.exceptions import OperationalException, StrategyError
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.persistence import PairLock, Trade
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.wallets import Wallets
|
||||
|
||||
@ -133,7 +133,6 @@ class IStrategy(ABC):
|
||||
self.config = config
|
||||
# Dict to determine if analysis is necessary
|
||||
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
||||
self._pair_locked_until: Dict[str, datetime] = {}
|
||||
|
||||
@abstractmethod
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
@ -278,7 +277,7 @@ class IStrategy(ABC):
|
||||
"""
|
||||
return self.__class__.__name__
|
||||
|
||||
def lock_pair(self, pair: str, until: datetime) -> None:
|
||||
def lock_pair(self, pair: str, until: datetime, reason: str = None) -> None:
|
||||
"""
|
||||
Locks pair until a given timestamp happens.
|
||||
Locked pairs are not analyzed, and are prevented from opening new trades.
|
||||
@ -288,8 +287,15 @@ class IStrategy(ABC):
|
||||
:param until: datetime in UTC until the pair should be blocked from opening new trades.
|
||||
Needs to be timezone aware `datetime.now(timezone.utc)`
|
||||
"""
|
||||
if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until:
|
||||
self._pair_locked_until[pair] = until
|
||||
lock = PairLock(
|
||||
pair=pair,
|
||||
lock_time=datetime.now(timezone.utc),
|
||||
lock_end_time=until,
|
||||
reason=reason,
|
||||
active=True
|
||||
)
|
||||
PairLock.session.add(lock)
|
||||
PairLock.session.flush()
|
||||
|
||||
def unlock_pair(self, pair: str) -> None:
|
||||
"""
|
||||
@ -298,8 +304,7 @@ class IStrategy(ABC):
|
||||
manually from within the strategy, to allow an easy way to unlock pairs.
|
||||
:param pair: Unlock pair to allow trading again
|
||||
"""
|
||||
if pair in self._pair_locked_until:
|
||||
del self._pair_locked_until[pair]
|
||||
PairLock.unlock_pair(pair, datetime.now(timezone.utc))
|
||||
|
||||
def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool:
|
||||
"""
|
||||
@ -311,15 +316,13 @@ class IStrategy(ABC):
|
||||
:param candle_date: Date of the last candle. Optional, defaults to current date
|
||||
:returns: locking state of the pair in question.
|
||||
"""
|
||||
if pair not in self._pair_locked_until:
|
||||
return False
|
||||
|
||||
if not candle_date:
|
||||
return self._pair_locked_until[pair] >= datetime.now(timezone.utc)
|
||||
# Simple call ...
|
||||
return PairLock.is_pair_locked(pair, candle_date)
|
||||
else:
|
||||
# Locking should happen until a new candle arrives
|
||||
lock_time = timeframe_to_next_date(self.timeframe, candle_date)
|
||||
# lock_time = candle_date + timedelta(minutes=timeframe_to_minutes(self.timeframe))
|
||||
return self._pair_locked_until[pair] > lock_time
|
||||
return PairLock.is_pair_locked(pair, lock_time)
|
||||
|
||||
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
|
@ -1,5 +1,4 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import MagicMock
|
||||
@ -12,7 +11,7 @@ from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.data.history import load_data
|
||||
from freqtrade.exceptions import StrategyError
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.persistence import PairLock, Trade
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from tests.conftest import log_has, log_has_re
|
||||
@ -360,11 +359,12 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) ->
|
||||
assert log_has('Skipping TA Analysis for already analyzed candle', caplog)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_is_pair_locked(default_conf):
|
||||
default_conf.update({'strategy': 'DefaultStrategy'})
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
# dict should be empty
|
||||
assert not strategy._pair_locked_until
|
||||
# No lock should be present
|
||||
assert len(PairLock.query.all()) == 0
|
||||
|
||||
pair = 'ETH/BTC'
|
||||
assert not strategy.is_pair_locked(pair)
|
||||
@ -372,11 +372,6 @@ def test_is_pair_locked(default_conf):
|
||||
# ETH/BTC locked for 4 minutes
|
||||
assert strategy.is_pair_locked(pair)
|
||||
|
||||
# Test lock does not change
|
||||
lock = strategy._pair_locked_until[pair]
|
||||
strategy.lock_pair(pair, arrow.utcnow().shift(minutes=2).datetime)
|
||||
assert lock == strategy._pair_locked_until[pair]
|
||||
|
||||
# XRP/BTC should not be locked now
|
||||
pair = 'XRP/BTC'
|
||||
assert not strategy.is_pair_locked(pair)
|
||||
@ -393,7 +388,10 @@ def test_is_pair_locked(default_conf):
|
||||
# Lock until 14:30
|
||||
lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc)
|
||||
strategy.lock_pair(pair, lock_time)
|
||||
# Lock is in the past ...
|
||||
# Lock is in the past, so we must fake the lock
|
||||
lock = PairLock.query.filter(PairLock.pair == pair).first()
|
||||
lock.lock_time = lock_time - timedelta(hours=2)
|
||||
|
||||
assert not strategy.is_pair_locked(pair)
|
||||
# latest candle is from 14:20, lock goes to 14:30
|
||||
assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-10))
|
||||
|
@ -15,7 +15,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie
|
||||
InvalidOrderException, OperationalException, PricingError,
|
||||
TemporaryError)
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.persistence import Order, Trade
|
||||
from freqtrade.persistence import Order, PairLock, Trade
|
||||
from freqtrade.rpc import RPCMessageType
|
||||
from freqtrade.state import RunMode, State
|
||||
from freqtrade.strategy.interface import SellCheckTuple, SellType
|
||||
@ -2799,6 +2799,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c
|
||||
|
||||
trade = Trade.query.first()
|
||||
Trade.session = MagicMock()
|
||||
PairLock.session = MagicMock()
|
||||
|
||||
freqtrade.config['dry_run'] = False
|
||||
trade.stoploss_order_id = "abcd"
|
||||
@ -3249,7 +3250,6 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo
|
||||
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
|
||||
sell_reason=SellType.STOP_LOSS)
|
||||
trade.close(ticker_sell_down()['bid'])
|
||||
assert trade.pair in freqtrade.strategy._pair_locked_until
|
||||
assert freqtrade.strategy.is_pair_locked(trade.pair)
|
||||
|
||||
# reinit - should buy other pair.
|
||||
|
Loading…
Reference in New Issue
Block a user