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).
|
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)`.
|
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 reenabled for that pair.
|
`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)`.
|
||||||
|
|
||||||
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
||||||
|
|
||||||
!!! Note
|
!!! 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
|
!!! Warning
|
||||||
Locking pairs is not functioning during backtesting.
|
Locking pairs is not functioning during backtesting.
|
||||||
|
@ -937,8 +937,8 @@ class FreqtradeBot:
|
|||||||
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
|
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
|
||||||
stoploss_order=True)
|
stoploss_order=True)
|
||||||
# Lock pair for one candle to prevent immediate rebuys
|
# Lock pair for one candle to prevent immediate rebuys
|
||||||
self.strategy.lock_pair(trade.pair,
|
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']),
|
||||||
timeframe_to_next_date(self.config['timeframe']))
|
reason='auto_lock_1_candle')
|
||||||
self._notify_sell(trade, "stoploss")
|
self._notify_sell(trade, "stoploss")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -1264,7 +1264,8 @@ class FreqtradeBot:
|
|||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
# Lock pair for one candle to prevent immediate rebuys
|
# 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)
|
self._notify_sell(trade, order_type)
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
# flake8: noqa: F401
|
# 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
|
# Copy session attributes to order object too
|
||||||
Order.session = Trade.session
|
Order.session = Trade.session
|
||||||
Order.query = Order.session.query_property()
|
Order.query = Order.session.query_property()
|
||||||
|
PairLock.session = Trade.session
|
||||||
|
PairLock.query = PairLock.session.query_property()
|
||||||
|
|
||||||
previous_tables = inspect(engine).get_table_names()
|
previous_tables = inspect(engine).get_table_names()
|
||||||
_DECL_BASE.metadata.create_all(engine)
|
_DECL_BASE.metadata.create_all(engine)
|
||||||
check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables)
|
check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables)
|
||||||
@ -655,3 +658,76 @@ class Trade(_DECL_BASE):
|
|||||||
trade.stop_loss = None
|
trade.stop_loss = None
|
||||||
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
|
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
|
||||||
logger.info(f"New stoploss: {trade.stop_loss}.")
|
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.exceptions import OperationalException, StrategyError
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
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.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from freqtrade.wallets import Wallets
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
@ -133,7 +133,6 @@ class IStrategy(ABC):
|
|||||||
self.config = config
|
self.config = config
|
||||||
# Dict to determine if analysis is necessary
|
# Dict to determine if analysis is necessary
|
||||||
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
||||||
self._pair_locked_until: Dict[str, datetime] = {}
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
@ -278,7 +277,7 @@ class IStrategy(ABC):
|
|||||||
"""
|
"""
|
||||||
return self.__class__.__name__
|
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.
|
Locks pair until a given timestamp happens.
|
||||||
Locked pairs are not analyzed, and are prevented from opening new trades.
|
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.
|
:param until: datetime in UTC until the pair should be blocked from opening new trades.
|
||||||
Needs to be timezone aware `datetime.now(timezone.utc)`
|
Needs to be timezone aware `datetime.now(timezone.utc)`
|
||||||
"""
|
"""
|
||||||
if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until:
|
lock = PairLock(
|
||||||
self._pair_locked_until[pair] = until
|
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:
|
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.
|
manually from within the strategy, to allow an easy way to unlock pairs.
|
||||||
:param pair: Unlock pair to allow trading again
|
:param pair: Unlock pair to allow trading again
|
||||||
"""
|
"""
|
||||||
if pair in self._pair_locked_until:
|
PairLock.unlock_pair(pair, datetime.now(timezone.utc))
|
||||||
del self._pair_locked_until[pair]
|
|
||||||
|
|
||||||
def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool:
|
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
|
:param candle_date: Date of the last candle. Optional, defaults to current date
|
||||||
:returns: locking state of the pair in question.
|
:returns: locking state of the pair in question.
|
||||||
"""
|
"""
|
||||||
if pair not in self._pair_locked_until:
|
|
||||||
return False
|
|
||||||
if not candle_date:
|
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:
|
else:
|
||||||
# Locking should happen until a new candle arrives
|
|
||||||
lock_time = timeframe_to_next_date(self.timeframe, candle_date)
|
lock_time = timeframe_to_next_date(self.timeframe, candle_date)
|
||||||
# lock_time = candle_date + timedelta(minutes=timeframe_to_minutes(self.timeframe))
|
return PairLock.is_pair_locked(pair, lock_time)
|
||||||
return self._pair_locked_until[pair] > lock_time
|
|
||||||
|
|
||||||
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
@ -12,7 +11,7 @@ from freqtrade.configuration import TimeRange
|
|||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.data.history import load_data
|
from freqtrade.data.history import load_data
|
||||||
from freqtrade.exceptions import StrategyError
|
from freqtrade.exceptions import StrategyError
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import PairLock, Trade
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from tests.conftest import log_has, log_has_re
|
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)
|
assert log_has('Skipping TA Analysis for already analyzed candle', caplog)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_is_pair_locked(default_conf):
|
def test_is_pair_locked(default_conf):
|
||||||
default_conf.update({'strategy': 'DefaultStrategy'})
|
default_conf.update({'strategy': 'DefaultStrategy'})
|
||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
# dict should be empty
|
# No lock should be present
|
||||||
assert not strategy._pair_locked_until
|
assert len(PairLock.query.all()) == 0
|
||||||
|
|
||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
assert not strategy.is_pair_locked(pair)
|
assert not strategy.is_pair_locked(pair)
|
||||||
@ -372,11 +372,6 @@ def test_is_pair_locked(default_conf):
|
|||||||
# ETH/BTC locked for 4 minutes
|
# ETH/BTC locked for 4 minutes
|
||||||
assert strategy.is_pair_locked(pair)
|
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
|
# XRP/BTC should not be locked now
|
||||||
pair = 'XRP/BTC'
|
pair = 'XRP/BTC'
|
||||||
assert not strategy.is_pair_locked(pair)
|
assert not strategy.is_pair_locked(pair)
|
||||||
@ -393,7 +388,10 @@ def test_is_pair_locked(default_conf):
|
|||||||
# Lock until 14:30
|
# Lock until 14:30
|
||||||
lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc)
|
lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc)
|
||||||
strategy.lock_pair(pair, lock_time)
|
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)
|
assert not strategy.is_pair_locked(pair)
|
||||||
# latest candle is from 14:20, lock goes to 14:30
|
# latest candle is from 14:20, lock goes to 14:30
|
||||||
assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-10))
|
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,
|
InvalidOrderException, OperationalException, PricingError,
|
||||||
TemporaryError)
|
TemporaryError)
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
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.rpc import RPCMessageType
|
||||||
from freqtrade.state import RunMode, State
|
from freqtrade.state import RunMode, State
|
||||||
from freqtrade.strategy.interface import SellCheckTuple, SellType
|
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 = Trade.query.first()
|
||||||
Trade.session = MagicMock()
|
Trade.session = MagicMock()
|
||||||
|
PairLock.session = MagicMock()
|
||||||
|
|
||||||
freqtrade.config['dry_run'] = False
|
freqtrade.config['dry_run'] = False
|
||||||
trade.stoploss_order_id = "abcd"
|
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'],
|
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
|
||||||
sell_reason=SellType.STOP_LOSS)
|
sell_reason=SellType.STOP_LOSS)
|
||||||
trade.close(ticker_sell_down()['bid'])
|
trade.close(ticker_sell_down()['bid'])
|
||||||
assert trade.pair in freqtrade.strategy._pair_locked_until
|
|
||||||
assert freqtrade.strategy.is_pair_locked(trade.pair)
|
assert freqtrade.strategy.is_pair_locked(trade.pair)
|
||||||
|
|
||||||
# reinit - should buy other pair.
|
# reinit - should buy other pair.
|
||||||
|
Loading…
Reference in New Issue
Block a user