""" Minimum age (days listed) pair list filter """ import logging from copy import deepcopy from typing import Any, Dict, List, Optional import arrow from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.exceptions import OperationalException from freqtrade.misc import plural from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.util import PeriodicCache logger = logging.getLogger(__name__) class AgeFilter(IPairList): def __init__(self, exchange, pairlistmanager, config: Dict[str, Any], pairlistconfig: Dict[str, Any], pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) # Checked symbols cache (dictionary of ticker symbol => timestamp) self._symbolsChecked: Dict[str, int] = {} self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400) self._min_days_listed = pairlistconfig.get('min_days_listed', 10) self._max_days_listed = pairlistconfig.get('max_days_listed') candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def']) if self._min_days_listed < 1: raise OperationalException("AgeFilter requires min_days_listed to be >= 1") if self._min_days_listed > candle_limit: raise OperationalException("AgeFilter requires min_days_listed to not exceed " "exchange max request size " f"({candle_limit})") if self._max_days_listed and self._max_days_listed <= self._min_days_listed: raise OperationalException("AgeFilter max_days_listed <= min_days_listed not permitted") if self._max_days_listed and self._max_days_listed > candle_limit: raise OperationalException("AgeFilter requires max_days_listed to not exceed " "exchange max request size " f"({candle_limit})") @property def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return False def short_desc(self) -> str: """ Short whitelist method description - used for startup-messages """ return ( f"{self.name} - Filtering pairs with age less than " f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}" ) + (( " or more than " f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}" ) if self._max_days_listed else '') def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ :param pairlist: pairlist to filter or sort :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: new allowlist """ needed_pairs: ListPairsWithTimeframes = [ (p, '1d', self._config['candle_type_def']) for p in pairlist if p not in self._symbolsChecked and p not in self._symbolsCheckFailed] if not needed_pairs: # Remove pairs that have been removed before return [p for p in pairlist if p not in self._symbolsCheckFailed] since_days = -( self._max_days_listed if self._max_days_listed else self._min_days_listed ) - 1 since_ms = int(arrow.utcnow() .floor('day') .shift(days=since_days) .float_timestamp) * 1000 candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, cache=False) if self._enabled: for p in deepcopy(pairlist): daily_candles = candles[(p, '1d', self._config['candle_type_def'])] if ( p, '1d', self._config['candle_type_def']) in candles else None if not self._validate_pair_loc(p, daily_candles): pairlist.remove(p) self.log_once(f"Validated {len(pairlist)} pairs.", logger.info) return pairlist def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: """ Validate age for the ticker :param pair: Pair that's currently validated :param daily_candles: Downloaded daily candles :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache if pair in self._symbolsChecked: return True if daily_candles is not None: if ( len(daily_candles) >= self._min_days_listed and (not self._max_days_listed or len(daily_candles) <= self._max_days_listed) ): # We have fetched at least the minimum required number of daily candles # Add to cache, store the time we last checked this symbol self._symbolsChecked[pair] = arrow.utcnow().int_timestamp * 1000 return True else: self.log_once(( f"Removed {pair} from whitelist, because age " f"{len(daily_candles)} is less than {self._min_days_listed} " f"{plural(self._min_days_listed, 'day')}" ) + (( " or more than " f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}" ) if self._max_days_listed else ''), logger.info) self._symbolsCheckFailed[pair] = arrow.utcnow().int_timestamp * 1000 return False return False