diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index d7fb0a353..430703b9b 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -6,6 +6,8 @@ from pathlib import Path from typing import Dict, List, Optional, Tuple import ccxt +from cachetools import TTLCache, cached +from threading import Lock from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError @@ -53,6 +55,11 @@ class Binance(Exchange): (TradingMode.FUTURES, MarginMode.ISOLATED) ] + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._spot_delist_schedule_cache: TTLCache = TTLCache(maxsize=100, ttl=300) + self._spot_delist_schedule_cache_lock = Lock() + def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers: tickers = super().get_tickers(symbols=symbols, cached=cached) if self.trading_mode == TradingMode.FUTURES: @@ -214,3 +221,58 @@ class Binance(Exchange): return self.get_leverage_tiers() else: return {} + + + + @retrier + @cached(cache=TTLCache(maxsize=100, ttl=10), lock=Lock()) + def get_deslist_schedule(self): + try: + + delist_schedule = self._api.sapi_get_spot_delist_schedule() + + return delist_schedule + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get delist schedule {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + def get_spot_pair_delist_time(self, pair, refresh: bool = True) -> int | None: + """ + Get the delisting time for a pair if it will be delisted + :param pair: Pair to get the delisting time for + :param refresh: true if you need fresh data + :return: int: delisting time None if not delisting + """ + + if not pair: + return + + cache = self._spot_delist_schedule_cache + lock = self._spot_delist_schedule_cache_lock + + schedule_pair = pair.replace('/', '') + + if not refresh: + with lock: + delist_time = cache[f"{schedule_pair}"] + + if delist_time: + return delist_time + + delist_schedule = self.get_deslist_schedule() + + if delist_schedule is None: + return None + + with lock: + for schedule in delist_schedule: + for symbol in schedule['symbols']: + cache[f"{symbol}"] = int(schedule['delistTime']) + + + with lock: + return cache.get(f"{schedule_pair}") diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 7b0831520..51011b628 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -723,3 +723,16 @@ def test_get_maintenance_ratio_and_amt_binance( exchange._leverage_tiers = leverage_tiers (result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, notional_value) assert (round(result_ratio, 8), round(result_amt, 8)) == (mm_ratio, amt) + + +def test_get_spot_delist_schedule(mocker, default_conf) -> None: + exchange = get_patched_exchange(mocker, default_conf, id='binance') + return_value = [{ + 'delistTime': '1712113200000', + 'symbols': ['DREPBTC', 'DREPUSDT', 'MOBBTC', 'MOBUSDT', 'PNTUSDT'] + }] + + exchange._api.sapi_get_spot_delist_schedule = MagicMock(return_value=return_value) + + assert exchange.get_spot_pair_delist_time('DREP/USDT') == 1712113200000 + assert exchange.get_spot_pair_delist_time('BTC/USDT') is None