Use "side" parameter when calling Pairlocks

This commit is contained in:
Matthias 2022-04-24 14:10:25 +02:00
parent 144e4da96e
commit 737bdfe844
9 changed files with 42 additions and 31 deletions

View File

@ -399,7 +399,10 @@ class FreqtradeBot(LoggingMixin):
logger.info("No currency pair in active pair whitelist, " logger.info("No currency pair in active pair whitelist, "
"but checking to exit open trades.") "but checking to exit open trades.")
return trades_created return trades_created
if PairLocks.is_global_lock(): if PairLocks.is_global_lock(side='*'):
# This only checks for total locks (both sides).
# per-side locks will be evaluated by `is_pair_locked` within create_trade,
# once the direction for the trade is clear.
lock = PairLocks.get_pair_longest_lock('*') lock = PairLocks.get_pair_longest_lock('*')
if lock: if lock:
self.log_once(f"Global pairlock active until " self.log_once(f"Global pairlock active until "
@ -433,16 +436,6 @@ class FreqtradeBot(LoggingMixin):
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe)
nowtime = analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None nowtime = analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None
if self.strategy.is_pair_locked(pair, nowtime):
lock = PairLocks.get_pair_longest_lock(pair, nowtime)
if lock:
self.log_once(f"Pair {pair} is still locked until "
f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)} "
f"due to {lock.reason}.",
logger.info)
else:
self.log_once(f"Pair {pair} is still locked.", logger.info)
return False
# get_free_open_trades is checked before create_trade is called # get_free_open_trades is checked before create_trade is called
# but it is still used here to prevent opening too many trades within one iteration # but it is still used here to prevent opening too many trades within one iteration
@ -458,6 +451,16 @@ class FreqtradeBot(LoggingMixin):
) )
if signal: if signal:
if self.strategy.is_pair_locked(pair, candle_date=nowtime, side=signal):
lock = PairLocks.get_pair_longest_lock(pair, nowtime, signal)
if lock:
self.log_once(f"Pair {pair} {lock.side} is locked until "
f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)} "
f"due to {lock.reason}.",
logger.info)
else:
self.log_once(f"Pair {pair} is currently locked.", logger.info)
return False
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {}) bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {})

View File

@ -965,7 +965,7 @@ class Backtesting:
and self.trade_slot_available(max_open_trades, open_trade_count_start) and self.trade_slot_available(max_open_trades, open_trade_count_start)
and current_time != end_date and current_time != end_date
and trade_dir is not None and trade_dir is not None
and not PairLocks.is_pair_locked(pair, row[DATE_IDX]) and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir)
): ):
trade = self._enter_trade(pair, row, trade_dir) trade = self._enter_trade(pair, row, trade_dir)
if trade: if trade:

View File

@ -268,7 +268,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
decl_base, inspector, engine, table_back_name, cols_trades, decl_base, inspector, engine, table_back_name, cols_trades,
order_table_bak_name, cols_orders) order_table_bak_name, cols_orders)
if not has_column(cols_pairlocks, 'direction'): if not has_column(cols_pairlocks, 'side'):
logger.info(f"Running database migration for pairlocks - " logger.info(f"Running database migration for pairlocks - "
f"backup: {pairlock_table_bak_name}") f"backup: {pairlock_table_bak_name}")

View File

@ -81,16 +81,17 @@ class PairLocks():
return locks return locks
@staticmethod @staticmethod
def get_pair_longest_lock(pair: str, now: Optional[datetime] = None) -> Optional[PairLock]: def get_pair_longest_lock(
pair: str, now: Optional[datetime] = None, side: str = '*') -> Optional[PairLock]:
""" """
Get the lock that expires the latest for the pair given. Get the lock that expires the latest for the pair given.
""" """
locks = PairLocks.get_pair_locks(pair, now) locks = PairLocks.get_pair_locks(pair, now, side=side)
locks = sorted(locks, key=lambda l: l.lock_end_time, reverse=True) locks = sorted(locks, key=lambda l: l.lock_end_time, reverse=True)
return locks[0] if locks else None return locks[0] if locks else None
@staticmethod @staticmethod
def unlock_pair(pair: str, now: Optional[datetime] = None) -> None: def unlock_pair(pair: str, now: Optional[datetime] = None, side: str = '*') -> None:
""" """
Release all locks for this pair. Release all locks for this pair.
:param pair: Pair to unlock :param pair: Pair to unlock
@ -101,7 +102,7 @@ class PairLocks():
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
logger.info(f"Releasing all locks for {pair}.") logger.info(f"Releasing all locks for {pair}.")
locks = PairLocks.get_pair_locks(pair, now) locks = PairLocks.get_pair_locks(pair, now, side=side)
for lock in locks: for lock in locks:
lock.active = False lock.active = False
if PairLocks.use_db: if PairLocks.use_db:

View File

@ -54,7 +54,7 @@ class ProtectionManager():
if protection_handler.has_global_stop: if protection_handler.has_global_stop:
lock = protection_handler.global_stop(date_now=now, side=side) lock = protection_handler.global_stop(date_now=now, side=side)
if lock and lock.until: if lock and lock.until:
if not PairLocks.is_global_lock(lock.until, lock.lock_side): if not PairLocks.is_global_lock(lock.until, side=lock.lock_side):
result = PairLocks.lock_pair( result = PairLocks.lock_pair(
'*', lock.until, lock.reason, now=now, side=lock.lock_side) '*', lock.until, lock.reason, now=now, side=lock.lock_side)
return result return result

View File

@ -572,7 +572,7 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
PairLocks.unlock_reason(reason, datetime.now(timezone.utc)) PairLocks.unlock_reason(reason, datetime.now(timezone.utc))
def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: def is_pair_locked(self, pair: str, *, candle_date: datetime = None, side: str = '*') -> bool:
""" """
Checks if a pair is currently locked Checks if a pair is currently locked
The 2nd, optional parameter ensures that locks are applied until the new candle arrives, The 2nd, optional parameter ensures that locks are applied until the new candle arrives,
@ -580,15 +580,16 @@ class IStrategy(ABC, HyperStrategyMixin):
of 2 seconds for an entry order to happen on an old signal. of 2 seconds for an entry order to happen on an old signal.
:param pair: "Pair to check" :param pair: "Pair to check"
: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
:param side: Side to check, can be long, short or '*'
:returns: locking state of the pair in question. :returns: locking state of the pair in question.
""" """
if not candle_date: if not candle_date:
# Simple call ... # Simple call ...
return PairLocks.is_pair_locked(pair) return PairLocks.is_pair_locked(pair, side=side)
else: else:
lock_time = timeframe_to_next_date(self.timeframe, candle_date) lock_time = timeframe_to_next_date(self.timeframe, candle_date)
return PairLocks.is_pair_locked(pair, lock_time) return PairLocks.is_pair_locked(pair, lock_time, side=side)
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """

View File

@ -666,23 +666,23 @@ def test_is_pair_locked(default_conf):
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, candle_date=lock_time + timedelta(minutes=-10))
assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) assert strategy.is_pair_locked(pair, candle_date=lock_time + timedelta(minutes=-50))
# latest candle is from 14:25 (lock should be lifted) # latest candle is from 14:25 (lock should be lifted)
# Since this is the "new candle" available at 14:30 # Since this is the "new candle" available at 14:30
assert not strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-4)) assert not strategy.is_pair_locked(pair, candle_date=lock_time + timedelta(minutes=-4))
# Should not be locked after time expired # Should not be locked after time expired
assert not strategy.is_pair_locked(pair, lock_time + timedelta(minutes=10)) assert not strategy.is_pair_locked(pair, candle_date=lock_time + timedelta(minutes=10))
# Change timeframe to 15m # Change timeframe to 15m
strategy.timeframe = '15m' strategy.timeframe = '15m'
# Candle from 14:14 - lock goes until 14:30 # Candle from 14:14 - lock goes until 14:30
assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-16)) assert strategy.is_pair_locked(pair, candle_date=lock_time + timedelta(minutes=-16))
assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-15, seconds=-2)) assert strategy.is_pair_locked(pair, candle_date=lock_time + timedelta(minutes=-15, seconds=-2))
# Candle from 14:15 - lock goes until 14:30 # Candle from 14:15 - lock goes until 14:30
assert not strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-15)) assert not strategy.is_pair_locked(pair, candle_date=lock_time + timedelta(minutes=-15))
def test_is_informative_pairs_callback(default_conf): def test_is_informative_pairs_callback(default_conf):

View File

@ -3796,13 +3796,16 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee,
exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS) exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)
) )
trade.close(ticker_usdt_sell_down()['bid']) trade.close(ticker_usdt_sell_down()['bid'])
assert freqtrade.strategy.is_pair_locked(trade.pair) assert freqtrade.strategy.is_pair_locked(trade.pair, side='*')
# Boths sides are locked
assert freqtrade.strategy.is_pair_locked(trade.pair, side='long')
assert freqtrade.strategy.is_pair_locked(trade.pair, side='short')
# reinit - should buy other pair. # reinit - should buy other pair.
caplog.clear() caplog.clear()
freqtrade.enter_positions() freqtrade.enter_positions()
assert log_has_re(f"Pair {trade.pair} is still locked.*", caplog) assert log_has_re(fr"Pair {trade.pair} \* is locked.*", caplog)
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])

View File

@ -1471,7 +1471,10 @@ def test_migrate_pairlocks(mocker, default_conf, fee, caplog):
assert len(PairLock.query.all()) == 2 assert len(PairLock.query.all()) == 2
assert len(PairLock.query.filter(PairLock.pair == '*').all()) == 1 assert len(PairLock.query.filter(PairLock.pair == '*').all()) == 1
assert len(PairLock.query.filter(PairLock.pair == 'ETH/BTC').all()) == 1 pairlocks = PairLock.query.filter(PairLock.pair == 'ETH/BTC').all()
assert len(pairlocks) == 1
pairlocks[0].pair == 'ETH/BTC'
pairlocks[0].side == '*'
def test_adjust_stop_loss(fee): def test_adjust_stop_loss(fee):