Use "side" parameter when calling Pairlocks
This commit is contained in:
parent
144e4da96e
commit
737bdfe844
@ -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', {})
|
||||||
|
@ -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:
|
||||||
|
@ -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}")
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -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):
|
||||||
|
@ -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])
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user