2020-06-24 22:58:12 +00:00
|
|
|
"""
|
|
|
|
Minimum age (days listed) pair list filter
|
|
|
|
"""
|
|
|
|
import logging
|
2020-12-17 12:32:19 +00:00
|
|
|
from copy import deepcopy
|
|
|
|
from typing import Any, Dict, List, Optional
|
2020-06-24 22:58:12 +00:00
|
|
|
|
2020-09-28 17:39:41 +00:00
|
|
|
import arrow
|
2020-12-17 12:32:19 +00:00
|
|
|
from pandas import DataFrame
|
2020-09-28 17:39:41 +00:00
|
|
|
|
2022-09-18 11:31:52 +00:00
|
|
|
from freqtrade.constants import Config, ListPairsWithTimeframes
|
2020-07-15 11:40:54 +00:00
|
|
|
from freqtrade.exceptions import OperationalException
|
2022-10-11 19:33:02 +00:00
|
|
|
from freqtrade.exchange.types import Tickers
|
2020-06-24 22:58:12 +00:00
|
|
|
from freqtrade.misc import plural
|
2020-12-23 15:54:35 +00:00
|
|
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
2022-08-10 08:57:19 +00:00
|
|
|
from freqtrade.util import PeriodicCache
|
2020-06-24 22:58:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class AgeFilter(IPairList):
|
|
|
|
|
|
|
|
def __init__(self, exchange, pairlistmanager,
|
2022-09-18 11:31:52 +00:00
|
|
|
config: Config, pairlistconfig: Dict[str, Any],
|
2020-06-24 22:58:12 +00:00
|
|
|
pairlist_pos: int) -> None:
|
|
|
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
|
|
|
2021-09-13 17:33:28 +00:00
|
|
|
# Checked symbols cache (dictionary of ticker symbol => timestamp)
|
|
|
|
self._symbolsChecked: Dict[str, int] = {}
|
2021-09-14 04:45:26 +00:00
|
|
|
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
|
2021-09-13 17:33:28 +00:00
|
|
|
|
2020-06-24 22:58:12 +00:00
|
|
|
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
2022-05-17 22:11:10 +00:00
|
|
|
self._max_days_listed = pairlistconfig.get('max_days_listed')
|
2020-07-08 17:06:30 +00:00
|
|
|
|
2022-05-14 11:27:36 +00:00
|
|
|
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
|
2020-07-08 17:06:30 +00:00
|
|
|
if self._min_days_listed < 1:
|
2020-08-15 07:08:50 +00:00
|
|
|
raise OperationalException("AgeFilter requires min_days_listed to be >= 1")
|
2022-05-14 11:27:36 +00:00
|
|
|
if self._min_days_listed > candle_limit:
|
2020-08-15 07:08:50 +00:00
|
|
|
raise OperationalException("AgeFilter requires min_days_listed to not exceed "
|
2020-07-15 11:40:54 +00:00
|
|
|
"exchange max request size "
|
2022-05-14 11:27:36 +00:00
|
|
|
f"({candle_limit})")
|
2021-07-03 16:58:04 +00:00
|
|
|
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")
|
2022-05-14 11:27:36 +00:00
|
|
|
if self._max_days_listed and self._max_days_listed > candle_limit:
|
2021-07-03 19:20:53 +00:00
|
|
|
raise OperationalException("AgeFilter requires max_days_listed to not exceed "
|
|
|
|
"exchange max request size "
|
2022-05-14 11:27:36 +00:00
|
|
|
f"({candle_limit})")
|
2020-06-24 22:58:12 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def needstickers(self) -> bool:
|
|
|
|
"""
|
|
|
|
Boolean property defining if tickers are necessary.
|
2020-11-24 19:24:51 +00:00
|
|
|
If no Pairlist requires tickers, an empty Dict is passed
|
2020-06-24 22:58:12 +00:00
|
|
|
as tickers argument to filter_pairlist
|
|
|
|
"""
|
2020-12-20 19:08:54 +00:00
|
|
|
return False
|
2020-06-24 22:58:12 +00:00
|
|
|
|
|
|
|
def short_desc(self) -> str:
|
|
|
|
"""
|
|
|
|
Short whitelist method description - used for startup-messages
|
|
|
|
"""
|
2021-07-06 12:36:42 +00:00
|
|
|
return (
|
|
|
|
f"{self.name} - Filtering pairs with age less than "
|
|
|
|
f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}"
|
2021-07-08 06:42:52 +00:00
|
|
|
) + ((
|
2021-07-06 12:36:42 +00:00
|
|
|
" or more than "
|
|
|
|
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
|
2021-07-08 06:42:52 +00:00
|
|
|
) if self._max_days_listed else '')
|
2020-06-24 22:58:12 +00:00
|
|
|
|
2022-10-11 19:33:02 +00:00
|
|
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
2020-06-24 22:58:12 +00:00
|
|
|
"""
|
2020-12-15 19:38:26 +00:00
|
|
|
:param pairlist: pairlist to filter or sort
|
2022-10-10 11:54:13 +00:00
|
|
|
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
2020-12-15 19:38:26 +00:00
|
|
|
:return: new allowlist
|
2020-06-24 22:58:12 +00:00
|
|
|
"""
|
2021-12-03 14:08:00 +00:00
|
|
|
needed_pairs: ListPairsWithTimeframes = [
|
2021-12-08 13:10:08 +00:00
|
|
|
(p, '1d', self._config['candle_type_def']) for p in pairlist
|
2021-09-14 04:45:26 +00:00
|
|
|
if p not in self._symbolsChecked and p not in self._symbolsCheckFailed]
|
2020-12-15 19:38:26 +00:00
|
|
|
if not needed_pairs:
|
2021-09-14 04:45:26 +00:00
|
|
|
# Remove pairs that have been removed before
|
|
|
|
return [p for p in pairlist if p not in self._symbolsCheckFailed]
|
2021-09-15 19:04:25 +00:00
|
|
|
|
2021-07-03 18:46:51 +00:00
|
|
|
since_days = -(
|
2021-07-03 19:20:53 +00:00
|
|
|
self._max_days_listed if self._max_days_listed else self._min_days_listed
|
2021-07-03 18:46:51 +00:00
|
|
|
) - 1
|
2020-06-24 22:58:12 +00:00
|
|
|
since_ms = int(arrow.utcnow()
|
|
|
|
.floor('day')
|
2021-07-03 16:58:04 +00:00
|
|
|
.shift(days=since_days)
|
2020-06-24 22:58:12 +00:00
|
|
|
.float_timestamp) * 1000
|
2020-12-15 19:38:26 +00:00
|
|
|
candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, cache=False)
|
|
|
|
if self._enabled:
|
2020-12-17 12:32:19 +00:00
|
|
|
for p in deepcopy(pairlist):
|
2021-12-08 13:10:08 +00:00
|
|
|
daily_candles = candles[(p, '1d', self._config['candle_type_def'])] if (
|
|
|
|
p, '1d', self._config['candle_type_def']) in candles else None
|
2020-12-17 12:32:19 +00:00
|
|
|
if not self._validate_pair_loc(p, daily_candles):
|
|
|
|
pairlist.remove(p)
|
2021-04-30 17:42:41 +00:00
|
|
|
self.log_once(f"Validated {len(pairlist)} pairs.", logger.info)
|
2020-12-17 12:32:19 +00:00
|
|
|
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
|
2022-03-13 14:06:32 +00:00
|
|
|
:param daily_candles: Downloaded daily candles
|
2020-12-17 12:32:19 +00:00
|
|
|
: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:
|
2021-07-07 18:05:56 +00:00
|
|
|
if (
|
|
|
|
len(daily_candles) >= self._min_days_listed
|
|
|
|
and (not self._max_days_listed or len(daily_candles) <= self._max_days_listed)
|
|
|
|
):
|
2020-12-17 12:32:19 +00:00
|
|
|
# We have fetched at least the minimum required number of daily candles
|
|
|
|
# Add to cache, store the time we last checked this symbol
|
2021-07-05 17:51:14 +00:00
|
|
|
self._symbolsChecked[pair] = arrow.utcnow().int_timestamp * 1000
|
2020-12-17 12:32:19 +00:00
|
|
|
return True
|
|
|
|
else:
|
2021-07-06 12:36:42 +00:00
|
|
|
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')}"
|
2021-07-07 14:24:44 +00:00
|
|
|
) + ((
|
2021-07-06 12:36:42 +00:00
|
|
|
" or more than "
|
|
|
|
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
|
2021-07-07 14:24:44 +00:00
|
|
|
) if self._max_days_listed else ''), logger.info)
|
2021-09-14 04:45:26 +00:00
|
|
|
self._symbolsCheckFailed[pair] = arrow.utcnow().int_timestamp * 1000
|
2020-12-17 12:32:19 +00:00
|
|
|
return False
|
|
|
|
return False
|