2019-11-09 05:55:16 +00:00
|
|
|
"""
|
2020-05-17 11:10:11 +00:00
|
|
|
PairList manager class
|
|
|
|
"""
|
2019-11-09 05:55:16 +00:00
|
|
|
import logging
|
2021-12-21 08:11:57 +00:00
|
|
|
from functools import partial
|
2022-09-18 12:59:16 +00:00
|
|
|
from typing import Dict, List, Optional
|
2019-11-09 05:55:16 +00:00
|
|
|
|
2019-12-30 14:02:17 +00:00
|
|
|
from cachetools import TTLCache, cached
|
|
|
|
|
2022-09-18 11:31:52 +00:00
|
|
|
from freqtrade.constants import Config, ListPairsWithTimeframes
|
2022-09-18 12:59:16 +00:00
|
|
|
from freqtrade.data.dataprovider import DataProvider
|
2021-12-03 13:11:24 +00:00
|
|
|
from freqtrade.enums import CandleType
|
2019-12-30 14:02:17 +00:00
|
|
|
from freqtrade.exceptions import OperationalException
|
2022-10-11 19:33:02 +00:00
|
|
|
from freqtrade.exchange.types import Tickers
|
2021-12-21 08:11:57 +00:00
|
|
|
from freqtrade.mixins import LoggingMixin
|
2020-12-23 15:54:35 +00:00
|
|
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
2020-12-30 09:21:05 +00:00
|
|
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
2019-11-09 05:55:16 +00:00
|
|
|
from freqtrade.resolvers import PairListResolver
|
|
|
|
|
2020-05-18 10:54:21 +00:00
|
|
|
|
2019-11-09 05:55:16 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2021-12-21 08:11:57 +00:00
|
|
|
class PairListManager(LoggingMixin):
|
2019-11-09 05:55:16 +00:00
|
|
|
|
2023-01-21 14:01:56 +00:00
|
|
|
def __init__(
|
|
|
|
self, exchange, config: Config, dataprovider: Optional[DataProvider] = None) -> None:
|
2019-11-09 05:55:16 +00:00
|
|
|
self._exchange = exchange
|
|
|
|
self._config = config
|
|
|
|
self._whitelist = self._config['exchange'].get('pair_whitelist')
|
|
|
|
self._blacklist = self._config['exchange'].get('pair_blacklist', [])
|
2020-05-19 00:35:01 +00:00
|
|
|
self._pairlist_handlers: List[IPairList] = []
|
2019-11-09 06:05:17 +00:00
|
|
|
self._tickers_needed = False
|
2022-09-18 12:59:16 +00:00
|
|
|
self._dataprovider: Optional[DataProvider] = dataprovider
|
2022-05-17 22:11:10 +00:00
|
|
|
for pairlist_handler_config in self._config.get('pairlists', []):
|
2020-05-19 00:35:01 +00:00
|
|
|
pairlist_handler = PairListResolver.load_pairlist(
|
2021-08-06 22:19:36 +00:00
|
|
|
pairlist_handler_config['method'],
|
|
|
|
exchange=exchange,
|
|
|
|
pairlistmanager=self,
|
|
|
|
config=config,
|
|
|
|
pairlistconfig=pairlist_handler_config,
|
|
|
|
pairlist_pos=len(self._pairlist_handlers)
|
|
|
|
)
|
2020-05-19 00:35:01 +00:00
|
|
|
self._tickers_needed |= pairlist_handler.needstickers
|
|
|
|
self._pairlist_handlers.append(pairlist_handler)
|
|
|
|
|
|
|
|
if not self._pairlist_handlers:
|
|
|
|
raise OperationalException("No Pairlist Handlers defined")
|
2019-11-09 05:55:16 +00:00
|
|
|
|
2022-10-13 04:58:17 +00:00
|
|
|
if self._tickers_needed and not self._exchange.exchange_has('fetchTickers'):
|
|
|
|
invalid = ". ".join([p.name for p in self._pairlist_handlers if p.needstickers])
|
|
|
|
|
|
|
|
raise OperationalException(
|
|
|
|
"Exchange does not support fetchTickers, therefore the following pairlists "
|
|
|
|
"cannot be used. Please edit your config and restart the bot.\n"
|
|
|
|
f"{invalid}."
|
|
|
|
)
|
|
|
|
|
2021-12-21 18:20:09 +00:00
|
|
|
refresh_period = config.get('pairlist_refresh_period', 3600)
|
2021-12-21 08:11:57 +00:00
|
|
|
LoggingMixin.__init__(self, logger, refresh_period)
|
|
|
|
|
2019-11-09 05:55:16 +00:00
|
|
|
@property
|
|
|
|
def whitelist(self) -> List[str]:
|
2020-12-31 08:43:24 +00:00
|
|
|
"""The current whitelist"""
|
2019-11-09 05:55:16 +00:00
|
|
|
return self._whitelist
|
|
|
|
|
|
|
|
@property
|
|
|
|
def blacklist(self) -> List[str]:
|
|
|
|
"""
|
2020-12-31 08:43:24 +00:00
|
|
|
The current blacklist
|
2019-11-09 05:55:16 +00:00
|
|
|
-> no need to overwrite in subclasses
|
|
|
|
"""
|
|
|
|
return self._blacklist
|
|
|
|
|
2020-12-30 08:55:44 +00:00
|
|
|
@property
|
|
|
|
def expanded_blacklist(self) -> List[str]:
|
2020-12-31 08:43:24 +00:00
|
|
|
"""The expanded blacklist (including wildcard expansion)"""
|
2020-12-30 08:55:44 +00:00
|
|
|
return expand_pairlist(self._blacklist, self._exchange.get_markets().keys())
|
|
|
|
|
2019-11-09 08:07:46 +00:00
|
|
|
@property
|
2019-11-09 13:00:32 +00:00
|
|
|
def name_list(self) -> List[str]:
|
2020-12-31 08:43:24 +00:00
|
|
|
"""Get list of loaded Pairlist Handler names"""
|
2020-05-19 00:35:01 +00:00
|
|
|
return [p.name for p in self._pairlist_handlers]
|
2019-11-09 08:07:46 +00:00
|
|
|
|
|
|
|
def short_desc(self) -> List[Dict]:
|
2020-12-31 08:43:24 +00:00
|
|
|
"""List of short_desc for each Pairlist Handler"""
|
2020-05-19 00:35:01 +00:00
|
|
|
return [{p.name: p.short_desc()} for p in self._pairlist_handlers]
|
2019-11-09 08:07:46 +00:00
|
|
|
|
2019-11-09 18:45:09 +00:00
|
|
|
@cached(TTLCache(maxsize=1, ttl=1800))
|
2022-10-11 19:33:02 +00:00
|
|
|
def _get_cached_tickers(self) -> Tickers:
|
2019-11-09 18:45:09 +00:00
|
|
|
return self._exchange.get_tickers()
|
|
|
|
|
2019-11-09 05:55:16 +00:00
|
|
|
def refresh_pairlist(self) -> None:
|
2020-12-31 08:43:24 +00:00
|
|
|
"""Run pairlist through all configured Pairlist Handlers."""
|
2020-05-17 11:10:11 +00:00
|
|
|
# Tickers should be cached to avoid calling the exchange on each call.
|
2019-11-09 12:40:36 +00:00
|
|
|
tickers: Dict = {}
|
2019-11-09 06:05:17 +00:00
|
|
|
if self._tickers_needed:
|
2019-11-09 18:45:09 +00:00
|
|
|
tickers = self._get_cached_tickers()
|
2019-11-09 06:05:17 +00:00
|
|
|
|
2020-05-22 12:03:49 +00:00
|
|
|
# Generate the pairlist with first Pairlist Handler in the chain
|
2021-04-25 18:10:47 +00:00
|
|
|
pairlist = self._pairlist_handlers[0].gen_pairlist(tickers)
|
2020-05-22 12:03:49 +00:00
|
|
|
|
2020-05-19 00:35:01 +00:00
|
|
|
# Process all Pairlist Handlers in the chain
|
2021-06-19 17:37:27 +00:00
|
|
|
# except for the first one, which is the generator.
|
|
|
|
for pairlist_handler in self._pairlist_handlers[1:]:
|
2020-05-19 00:35:01 +00:00
|
|
|
pairlist = pairlist_handler.filter_pairlist(pairlist, tickers)
|
2019-11-09 05:55:16 +00:00
|
|
|
|
2020-05-19 00:35:01 +00:00
|
|
|
# Validation against blacklist happens after the chain of Pairlist Handlers
|
|
|
|
# to ensure blacklist is respected.
|
2020-05-19 20:51:39 +00:00
|
|
|
pairlist = self.verify_blacklist(pairlist, logger.warning)
|
2019-11-09 06:23:34 +00:00
|
|
|
|
2022-09-25 07:43:39 +00:00
|
|
|
self.log_once(f"Whitelist with {len(pairlist)} pairs: {pairlist}", logger.info)
|
|
|
|
|
2019-11-09 05:55:16 +00:00
|
|
|
self._whitelist = pairlist
|
2020-05-18 10:54:21 +00:00
|
|
|
|
2020-05-19 20:51:39 +00:00
|
|
|
def verify_blacklist(self, pairlist: List[str], logmethod) -> List[str]:
|
2020-05-19 20:13:51 +00:00
|
|
|
"""
|
|
|
|
Verify and remove items from pairlist - returning a filtered pairlist.
|
|
|
|
Logs a warning or info depending on `aswarning`.
|
2020-05-19 20:51:39 +00:00
|
|
|
Pairlist Handlers explicitly using this method shall use
|
|
|
|
`logmethod=logger.info` to avoid spamming with warning messages
|
2020-05-19 20:13:51 +00:00
|
|
|
:param pairlist: Pairlist to validate
|
2020-05-19 20:51:39 +00:00
|
|
|
:param logmethod: Function that'll be called, `logger.info` or `logger.warning`.
|
2020-05-19 20:13:51 +00:00
|
|
|
:return: pairlist - blacklisted pairs
|
|
|
|
"""
|
2020-12-30 09:14:22 +00:00
|
|
|
try:
|
|
|
|
blacklist = self.expanded_blacklist
|
|
|
|
except ValueError as err:
|
|
|
|
logger.error(f"Pair blacklist contains an invalid Wildcard: {err}")
|
|
|
|
return []
|
2021-12-21 08:11:57 +00:00
|
|
|
log_once = partial(self.log_once, logmethod=logmethod)
|
2021-07-29 05:34:21 +00:00
|
|
|
for pair in pairlist.copy():
|
2020-12-30 09:14:22 +00:00
|
|
|
if pair in blacklist:
|
2021-12-21 08:11:57 +00:00
|
|
|
log_once(f"Pair {pair} in your blacklist. Removing it from whitelist...")
|
2020-05-19 20:13:51 +00:00
|
|
|
pairlist.remove(pair)
|
|
|
|
return pairlist
|
|
|
|
|
2021-01-14 23:13:11 +00:00
|
|
|
def verify_whitelist(self, pairlist: List[str], logmethod,
|
|
|
|
keep_invalid: bool = False) -> List[str]:
|
2021-01-12 00:13:58 +00:00
|
|
|
"""
|
|
|
|
Verify and remove items from pairlist - returning a filtered pairlist.
|
|
|
|
Logs a warning or info depending on `aswarning`.
|
|
|
|
Pairlist Handlers explicitly using this method shall use
|
|
|
|
`logmethod=logger.info` to avoid spamming with warning messages
|
2021-01-14 23:13:11 +00:00
|
|
|
:param pairlist: Pairlist to validate
|
|
|
|
:param logmethod: Function that'll be called, `logger.info` or `logger.warning`
|
|
|
|
:param keep_invalid: If sets to True, drops invalid pairs silently while expanding regexes.
|
|
|
|
:return: pairlist - whitelisted pairs
|
2021-01-12 00:13:58 +00:00
|
|
|
"""
|
2021-01-15 05:56:15 +00:00
|
|
|
try:
|
2021-02-01 18:40:31 +00:00
|
|
|
whitelist = expand_pairlist(pairlist, self._exchange.get_markets().keys(), keep_invalid)
|
2021-01-15 05:56:15 +00:00
|
|
|
except ValueError as err:
|
|
|
|
logger.error(f"Pair whitelist contains an invalid Wildcard: {err}")
|
|
|
|
return []
|
2021-01-12 00:13:58 +00:00
|
|
|
return whitelist
|
|
|
|
|
2023-01-21 14:01:56 +00:00
|
|
|
def create_pair_list(
|
|
|
|
self, pairs: List[str], timeframe: Optional[str] = None) -> ListPairsWithTimeframes:
|
2020-05-18 10:54:21 +00:00
|
|
|
"""
|
2020-06-01 18:49:40 +00:00
|
|
|
Create list of pair tuples with (pair, timeframe)
|
2020-05-18 10:54:21 +00:00
|
|
|
"""
|
2021-12-27 23:51:02 +00:00
|
|
|
return [
|
|
|
|
(
|
|
|
|
pair,
|
|
|
|
timeframe or self._config['timeframe'],
|
|
|
|
self._config.get('candle_type_def', CandleType.SPOT)
|
|
|
|
) for pair in pairs
|
|
|
|
]
|