ruff format: pairlist plugins

This commit is contained in:
Matthias
2024-05-12 16:37:11 +02:00
parent c9d301e4f9
commit 700b7acb6f
18 changed files with 652 additions and 446 deletions
+57 -35
View File
@@ -1,6 +1,7 @@
""" """
Minimum age (days listed) pair list filter Minimum age (days listed) pair list filter
""" """
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import timedelta from datetime import timedelta
@@ -20,32 +21,40 @@ logger = logging.getLogger(__name__)
class AgeFilter(IPairList): class AgeFilter(IPairList):
def __init__(
def __init__(self, exchange, pairlistmanager, self,
config: Config, pairlistconfig: Dict[str, Any], exchange,
pairlist_pos: int) -> None: pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
# Checked symbols cache (dictionary of ticker symbol => timestamp) # Checked symbols cache (dictionary of ticker symbol => timestamp)
self._symbolsChecked: Dict[str, int] = {} self._symbolsChecked: Dict[str, int] = {}
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400) self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
self._min_days_listed = pairlistconfig.get('min_days_listed', 10) self._min_days_listed = pairlistconfig.get("min_days_listed", 10)
self._max_days_listed = pairlistconfig.get('max_days_listed') self._max_days_listed = pairlistconfig.get("max_days_listed")
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def']) candle_limit = exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"])
if self._min_days_listed < 1: if self._min_days_listed < 1:
raise OperationalException("AgeFilter requires min_days_listed to be >= 1") raise OperationalException("AgeFilter requires min_days_listed to be >= 1")
if self._min_days_listed > candle_limit: if self._min_days_listed > candle_limit:
raise OperationalException("AgeFilter requires min_days_listed to not exceed " raise OperationalException(
"exchange max request size " "AgeFilter requires min_days_listed to not exceed "
f"({candle_limit})") "exchange max request size "
f"({candle_limit})"
)
if self._max_days_listed and self._max_days_listed <= self._min_days_listed: 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") raise OperationalException("AgeFilter max_days_listed <= min_days_listed not permitted")
if self._max_days_listed and self._max_days_listed > candle_limit: if self._max_days_listed and self._max_days_listed > candle_limit:
raise OperationalException("AgeFilter requires max_days_listed to not exceed " raise OperationalException(
"exchange max request size " "AgeFilter requires max_days_listed to not exceed "
f"({candle_limit})") "exchange max request size "
f"({candle_limit})"
)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@@ -63,10 +72,11 @@ class AgeFilter(IPairList):
return ( return (
f"{self.name} - Filtering pairs with age less than " f"{self.name} - Filtering pairs with age less than "
f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}" f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}"
) + (( ) + (
" or more than " (" or more than " f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}")
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}" if self._max_days_listed
) if self._max_days_listed else '') else ""
)
@staticmethod @staticmethod
def description() -> str: def description() -> str:
@@ -96,21 +106,26 @@ class AgeFilter(IPairList):
:return: new allowlist :return: new allowlist
""" """
needed_pairs: ListPairsWithTimeframes = [ needed_pairs: ListPairsWithTimeframes = [
(p, '1d', self._config['candle_type_def']) for p in pairlist (p, "1d", self._config["candle_type_def"])
if p not in self._symbolsChecked and p not in self._symbolsCheckFailed] for p in pairlist
if p not in self._symbolsChecked and p not in self._symbolsCheckFailed
]
if not needed_pairs: if not needed_pairs:
# Remove pairs that have been removed before # Remove pairs that have been removed before
return [p for p in pairlist if p not in self._symbolsCheckFailed] return [p for p in pairlist if p not in self._symbolsCheckFailed]
since_days = -( since_days = (
self._max_days_listed if self._max_days_listed else self._min_days_listed -(self._max_days_listed if self._max_days_listed else self._min_days_listed) - 1
) - 1 )
since_ms = dt_ts(dt_floor_day(dt_now()) + timedelta(days=since_days)) since_ms = dt_ts(dt_floor_day(dt_now()) + timedelta(days=since_days))
candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, cache=False) candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, cache=False)
if self._enabled: if self._enabled:
for p in deepcopy(pairlist): for p in deepcopy(pairlist):
daily_candles = candles[(p, '1d', self._config['candle_type_def'])] if ( daily_candles = (
p, '1d', self._config['candle_type_def']) in candles else None 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): if not self._validate_pair_loc(p, daily_candles):
pairlist.remove(p) pairlist.remove(p)
self.log_once(f"Validated {len(pairlist)} pairs.", logger.info) self.log_once(f"Validated {len(pairlist)} pairs.", logger.info)
@@ -128,23 +143,30 @@ class AgeFilter(IPairList):
return True return True
if daily_candles is not None: if daily_candles is not None:
if ( if len(daily_candles) >= self._min_days_listed and (
len(daily_candles) >= self._min_days_listed not self._max_days_listed or len(daily_candles) <= self._max_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 # We have fetched at least the minimum required number of daily candles
# Add to cache, store the time we last checked this symbol # Add to cache, store the time we last checked this symbol
self._symbolsChecked[pair] = dt_ts() self._symbolsChecked[pair] = dt_ts()
return True return True
else: else:
self.log_once(( self.log_once(
f"Removed {pair} from whitelist, because age " (
f"{len(daily_candles)} is less than {self._min_days_listed} " f"Removed {pair} from whitelist, because age "
f"{plural(self._min_days_listed, 'day')}" 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) (
" 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] = dt_ts() self._symbolsCheckFailed[pair] = dt_ts()
return False return False
return False return False
+10 -5
View File
@@ -1,6 +1,7 @@
""" """
Full trade slots pair list filter Full trade slots pair list filter
""" """
import logging import logging
from typing import Any, Dict, List from typing import Any, Dict, List
@@ -14,10 +15,14 @@ logger = logging.getLogger(__name__)
class FullTradesFilter(IPairList): class FullTradesFilter(IPairList):
def __init__(
def __init__(self, exchange, pairlistmanager, self,
config: Config, pairlistconfig: Dict[str, Any], exchange,
pairlist_pos: int) -> None: pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
@property @property
@@ -49,7 +54,7 @@ class FullTradesFilter(IPairList):
""" """
# Get the number of open trades and max open trades config # Get the number of open trades and max open trades config
num_open = Trade.get_open_trade_count() num_open = Trade.get_open_trade_count()
max_trades = self._config['max_open_trades'] max_trades = self._config["max_open_trades"]
if (num_open >= max_trades) and (max_trades > 0): if (num_open >= max_trades) and (max_trades > 0):
return [] return []
+36 -21
View File
@@ -1,6 +1,7 @@
""" """
PairList Handler base class PairList Handler base class
""" """
import logging import logging
from abc import ABC, abstractmethod, abstractproperty from abc import ABC, abstractmethod, abstractproperty
from copy import deepcopy from copy import deepcopy
@@ -46,17 +47,21 @@ PairlistParameter = Union[
__NumberPairlistParameter, __NumberPairlistParameter,
__StringPairlistParameter, __StringPairlistParameter,
__OptionPairlistParameter, __OptionPairlistParameter,
__BoolPairlistParameter __BoolPairlistParameter,
] ]
class IPairList(LoggingMixin, ABC): class IPairList(LoggingMixin, ABC):
is_pairlist_generator = False is_pairlist_generator = False
def __init__(self, exchange: Exchange, pairlistmanager, def __init__(
config: Config, pairlistconfig: Dict[str, Any], self,
pairlist_pos: int) -> None: exchange: Exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
""" """
:param exchange: Exchange instance :param exchange: Exchange instance
:param pairlistmanager: Instantiated Pairlist manager :param pairlistmanager: Instantiated Pairlist manager
@@ -71,7 +76,7 @@ class IPairList(LoggingMixin, ABC):
self._config = config self._config = config
self._pairlistconfig = pairlistconfig self._pairlistconfig = pairlistconfig
self._pairlist_pos = pairlist_pos self._pairlist_pos = pairlist_pos
self.refresh_period = self._pairlistconfig.get('refresh_period', 1800) self.refresh_period = self._pairlistconfig.get("refresh_period", 1800)
LoggingMixin.__init__(self, logger, self.refresh_period) LoggingMixin.__init__(self, logger, self.refresh_period)
@property @property
@@ -155,8 +160,10 @@ class IPairList(LoggingMixin, ABC):
:param tickers: Tickers (from exchange.get_tickers). May be cached. :param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: List of pairs :return: List of pairs
""" """
raise OperationalException("This Pairlist Handler should not be used " raise OperationalException(
"at the first position in the list of Pairlist Handlers.") "This Pairlist Handler should not be used "
"at the first position in the list of Pairlist Handlers."
)
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]: def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
""" """
@@ -191,8 +198,9 @@ class IPairList(LoggingMixin, ABC):
""" """
return self._pairlistmanager.verify_blacklist(pairlist, logmethod) return self._pairlistmanager.verify_blacklist(pairlist, logmethod)
def verify_whitelist(self, pairlist: List[str], logmethod, def verify_whitelist(
keep_invalid: bool = False) -> List[str]: self, pairlist: List[str], logmethod, keep_invalid: bool = False
) -> List[str]:
""" """
Proxy method to verify_whitelist for easy access for child classes. Proxy method to verify_whitelist for easy access for child classes.
:param pairlist: Pairlist to validate :param pairlist: Pairlist to validate
@@ -212,26 +220,33 @@ class IPairList(LoggingMixin, ABC):
markets = self._exchange.markets markets = self._exchange.markets
if not markets: if not markets:
raise OperationalException( raise OperationalException(
'Markets not loaded. Make sure that exchange is initialized correctly.') "Markets not loaded. Make sure that exchange is initialized correctly."
)
sanitized_whitelist: List[str] = [] sanitized_whitelist: List[str] = []
for pair in pairlist: for pair in pairlist:
# pair is not in the generated dynamic market or has the wrong stake currency # pair is not in the generated dynamic market or has the wrong stake currency
if pair not in markets: if pair not in markets:
self.log_once(f"Pair {pair} is not compatible with exchange " self.log_once(
f"{self._exchange.name}. Removing it from whitelist..", f"Pair {pair} is not compatible with exchange "
logger.warning) f"{self._exchange.name}. Removing it from whitelist..",
logger.warning,
)
continue continue
if not self._exchange.market_is_tradable(markets[pair]): if not self._exchange.market_is_tradable(markets[pair]):
self.log_once(f"Pair {pair} is not tradable with Freqtrade." self.log_once(
"Removing it from whitelist..", logger.warning) f"Pair {pair} is not tradable with Freqtrade." "Removing it from whitelist..",
logger.warning,
)
continue continue
if self._exchange.get_pair_quote_currency(pair) != self._config['stake_currency']: if self._exchange.get_pair_quote_currency(pair) != self._config["stake_currency"]:
self.log_once(f"Pair {pair} is not compatible with your stake currency " self.log_once(
f"{self._config['stake_currency']}. Removing it from whitelist..", f"Pair {pair} is not compatible with your stake currency "
logger.warning) f"{self._config['stake_currency']}. Removing it from whitelist..",
logger.warning,
)
continue continue
# Check if market is active # Check if market is active
+42 -30
View File
@@ -3,6 +3,7 @@ Market Cap PairList provider
Provides dynamic pair list based on Market Cap Provides dynamic pair list based on Market Cap
""" """
import logging import logging
from typing import Any, Dict, List from typing import Any, Dict, List
@@ -19,31 +20,34 @@ logger = logging.getLogger(__name__)
class MarketCapPairList(IPairList): class MarketCapPairList(IPairList):
is_pairlist_generator = True is_pairlist_generator = True
def __init__(self, exchange, pairlistmanager, def __init__(
config: Config, pairlistconfig: Dict[str, Any], self,
pairlist_pos: int) -> None: exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if 'number_assets' not in self._pairlistconfig: if "number_assets" not in self._pairlistconfig:
raise OperationalException( raise OperationalException(
'`number_assets` not specified. Please check your configuration ' "`number_assets` not specified. Please check your configuration "
'for "pairlist.config.number_assets"') 'for "pairlist.config.number_assets"'
)
self._stake_currency = config['stake_currency'] self._stake_currency = config["stake_currency"]
self._number_assets = self._pairlistconfig['number_assets'] self._number_assets = self._pairlistconfig["number_assets"]
self._max_rank = self._pairlistconfig.get('max_rank', 30) self._max_rank = self._pairlistconfig.get("max_rank", 30)
self._refresh_period = self._pairlistconfig.get('refresh_period', 86400) self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
self._marketcap_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period) self._marketcap_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
self._def_candletype = self._config['candle_type_def'] self._def_candletype = self._config["candle_type_def"]
self._coingecko: CoinGeckoAPI = CoinGeckoAPI() self._coingecko: CoinGeckoAPI = CoinGeckoAPI()
if self._max_rank > 250: if self._max_rank > 250:
raise OperationalException( raise OperationalException("This filter only support marketcap rank up to 250.")
"This filter only support marketcap rank up to 250."
)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@@ -87,7 +91,7 @@ class MarketCapPairList(IPairList):
"default": 86400, "default": 86400,
"description": "Refresh period", "description": "Refresh period",
"help": "Refresh period in seconds", "help": "Refresh period in seconds",
} },
} }
def gen_pairlist(self, tickers: Tickers) -> List[str]: def gen_pairlist(self, tickers: Tickers) -> List[str]:
@@ -98,21 +102,24 @@ class MarketCapPairList(IPairList):
""" """
# Generate dynamic whitelist # Generate dynamic whitelist
# Must always run if this pairlist is the first in the list. # Must always run if this pairlist is the first in the list.
pairlist = self._marketcap_cache.get('pairlist_mc') pairlist = self._marketcap_cache.get("pairlist_mc")
if pairlist: if pairlist:
# Item found - no refresh necessary # Item found - no refresh necessary
return pairlist.copy() return pairlist.copy()
else: else:
# Use fresh pairlist # Use fresh pairlist
# Check if pair quote currency equals to the stake currency. # Check if pair quote currency equals to the stake currency.
_pairlist = [k for k in self._exchange.get_markets( _pairlist = [
quote_currencies=[self._stake_currency], k
tradable_only=True, active_only=True).keys()] for k in self._exchange.get_markets(
quote_currencies=[self._stake_currency], tradable_only=True, active_only=True
).keys()
]
# No point in testing for blacklisted pairs... # No point in testing for blacklisted pairs...
_pairlist = self.verify_blacklist(_pairlist, logger.info) _pairlist = self.verify_blacklist(_pairlist, logger.info)
pairlist = self.filter_pairlist(_pairlist, tickers) pairlist = self.filter_pairlist(_pairlist, tickers)
self._marketcap_cache['pairlist_mc'] = pairlist.copy() self._marketcap_cache["pairlist_mc"] = pairlist.copy()
return pairlist return pairlist
@@ -124,25 +131,30 @@ class MarketCapPairList(IPairList):
:param tickers: Tickers (from exchange.get_tickers). May be cached. :param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: new whitelist :return: new whitelist
""" """
marketcap_list = self._marketcap_cache.get('marketcap') marketcap_list = self._marketcap_cache.get("marketcap")
if marketcap_list is None: if marketcap_list is None:
data = self._coingecko.get_coins_markets(vs_currency='usd', order='market_cap_desc', data = self._coingecko.get_coins_markets(
per_page='250', page='1', sparkline='false', vs_currency="usd",
locale='en') order="market_cap_desc",
per_page="250",
page="1",
sparkline="false",
locale="en",
)
if data: if data:
marketcap_list = [row['symbol'] for row in data] marketcap_list = [row["symbol"] for row in data]
self._marketcap_cache['marketcap'] = marketcap_list self._marketcap_cache["marketcap"] = marketcap_list
if marketcap_list: if marketcap_list:
filtered_pairlist = [] filtered_pairlist = []
market = self._config['trading_mode'] market = self._config["trading_mode"]
pair_format = f"{self._stake_currency.upper()}" pair_format = f"{self._stake_currency.upper()}"
if (market == 'futures'): if market == "futures":
pair_format += f":{self._stake_currency.upper()}" pair_format += f":{self._stake_currency.upper()}"
top_marketcap = marketcap_list[:self._max_rank:] top_marketcap = marketcap_list[: self._max_rank :]
for mc_pair in top_marketcap: for mc_pair in top_marketcap:
test_pair = f"{mc_pair.upper()}/{pair_format}" test_pair = f"{mc_pair.upper()}/{pair_format}"
+17 -10
View File
@@ -1,6 +1,7 @@
""" """
Offset pair list filter Offset pair list filter
""" """
import logging import logging
from typing import Any, Dict, List from typing import Any, Dict, List
@@ -14,14 +15,18 @@ logger = logging.getLogger(__name__)
class OffsetFilter(IPairList): class OffsetFilter(IPairList):
def __init__(
def __init__(self, exchange, pairlistmanager, self,
config: Config, pairlistconfig: Dict[str, Any], exchange,
pairlist_pos: int) -> None: pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._offset = pairlistconfig.get('offset', 0) self._offset = pairlistconfig.get("offset", 0)
self._number_pairs = pairlistconfig.get('number_assets', 0) self._number_pairs = pairlistconfig.get("number_assets", 0)
if self._offset < 0: if self._offset < 0:
raise OperationalException("OffsetFilter requires offset to be >= 0") raise OperationalException("OffsetFilter requires offset to be >= 0")
@@ -73,11 +78,13 @@ class OffsetFilter(IPairList):
:return: new whitelist :return: new whitelist
""" """
if self._offset > len(pairlist): if self._offset > len(pairlist):
self.log_once(f"Offset of {self._offset} is larger than " + self.log_once(
f"pair count of {len(pairlist)}", logger.warning) f"Offset of {self._offset} is larger than " + f"pair count of {len(pairlist)}",
pairs = pairlist[self._offset:] logger.warning,
)
pairs = pairlist[self._offset :]
if self._number_pairs: if self._number_pairs:
pairs = pairs[:self._number_pairs] pairs = pairs[: self._number_pairs]
self.log_once(f"Searching {len(pairs)} pairs: {pairs}", logger.info) self.log_once(f"Searching {len(pairs)} pairs: {pairs}", logger.info)
+24 -15
View File
@@ -1,6 +1,7 @@
""" """
Performance pair list filter Performance pair list filter
""" """
import logging import logging
from typing import Any, Dict, List from typing import Any, Dict, List
@@ -16,14 +17,18 @@ logger = logging.getLogger(__name__)
class PerformanceFilter(IPairList): class PerformanceFilter(IPairList):
def __init__(
def __init__(self, exchange, pairlistmanager, self,
config: Config, pairlistconfig: Dict[str, Any], exchange,
pairlist_pos: int) -> None: pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._minutes = pairlistconfig.get('minutes', 0) self._minutes = pairlistconfig.get("minutes", 0)
self._min_profit = pairlistconfig.get('min_profit') self._min_profit = pairlistconfig.get("min_profit")
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@@ -82,25 +87,29 @@ class PerformanceFilter(IPairList):
return pairlist return pairlist
# Get pairlist from performance dataframe values # Get pairlist from performance dataframe values
list_df = pd.DataFrame({'pair': pairlist}) list_df = pd.DataFrame({"pair": pairlist})
list_df['prior_idx'] = list_df.index list_df["prior_idx"] = list_df.index
# Set initial value for pairs with no trades to 0 # Set initial value for pairs with no trades to 0
# Sort the list using: # Sort the list using:
# - primarily performance (high to low) # - primarily performance (high to low)
# - then count (low to high, so as to favor same performance with fewer trades) # - then count (low to high, so as to favor same performance with fewer trades)
# - then by prior index, keeping original sorting order # - then by prior index, keeping original sorting order
sorted_df = list_df.merge(performance, on='pair', how='left')\ sorted_df = (
.fillna(0).sort_values(by=['profit_ratio', 'count', 'prior_idx'], list_df.merge(performance, on="pair", how="left")
ascending=[False, True, True]) .fillna(0)
.sort_values(by=["profit_ratio", "count", "prior_idx"], ascending=[False, True, True])
)
if self._min_profit is not None: if self._min_profit is not None:
removed = sorted_df[sorted_df['profit_ratio'] < self._min_profit] removed = sorted_df[sorted_df["profit_ratio"] < self._min_profit]
for _, row in removed.iterrows(): for _, row in removed.iterrows():
self.log_once( self.log_once(
f"Removing pair {row['pair']} since {row['profit_ratio']} is " f"Removing pair {row['pair']} since {row['profit_ratio']} is "
f"below {self._min_profit}", logger.info) f"below {self._min_profit}",
sorted_df = sorted_df[sorted_df['profit_ratio'] >= self._min_profit] logger.info,
)
sorted_df = sorted_df[sorted_df["profit_ratio"] >= self._min_profit]
pairlist = sorted_df['pair'].tolist() pairlist = sorted_df["pair"].tolist()
return pairlist return pairlist
+29 -17
View File
@@ -1,6 +1,7 @@
""" """
Precision pair list filter Precision pair list filter
""" """
import logging import logging
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
@@ -15,17 +16,22 @@ logger = logging.getLogger(__name__)
class PrecisionFilter(IPairList): class PrecisionFilter(IPairList):
def __init__(
def __init__(self, exchange, pairlistmanager, self,
config: Config, pairlistconfig: Dict[str, Any], exchange,
pairlist_pos: int) -> None: pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if 'stoploss' not in self._config: if "stoploss" not in self._config:
raise OperationalException( raise OperationalException(
'PrecisionFilter can only work with stoploss defined. Please add the ' "PrecisionFilter can only work with stoploss defined. Please add the "
'stoploss key to your configuration (overwrites eventual strategy settings).') "stoploss key to your configuration (overwrites eventual strategy settings)."
self._stoploss = self._config['stoploss'] )
self._stoploss = self._config["stoploss"]
self._enabled = self._stoploss != 0 self._enabled = self._stoploss != 0
# Precalculate sanitized stoploss value to avoid recalculation for every pair # Precalculate sanitized stoploss value to avoid recalculation for every pair
@@ -58,23 +64,29 @@ class PrecisionFilter(IPairList):
:param ticker: ticker dict as returned from ccxt.fetch_ticker :param ticker: ticker dict as returned from ccxt.fetch_ticker
:return: True if the pair can stay, false if it should be removed :return: True if the pair can stay, false if it should be removed
""" """
if not ticker or ticker.get('last', None) is None: if not ticker or ticker.get("last", None) is None:
self.log_once(f"Removed {pair} from whitelist, because " self.log_once(
"ticker['last'] is empty (Usually no trade in the last 24h).", f"Removed {pair} from whitelist, because "
logger.info) "ticker['last'] is empty (Usually no trade in the last 24h).",
logger.info,
)
return False return False
stop_price = ticker['last'] * self._stoploss stop_price = ticker["last"] * self._stoploss
# Adjust stop-prices to precision # Adjust stop-prices to precision
sp = self._exchange.price_to_precision(pair, stop_price, rounding_mode=ROUND_UP) sp = self._exchange.price_to_precision(pair, stop_price, rounding_mode=ROUND_UP)
stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99, stop_gap_price = self._exchange.price_to_precision(
rounding_mode=ROUND_UP) pair, stop_price * 0.99, rounding_mode=ROUND_UP
)
logger.debug(f"{pair} - {sp} : {stop_gap_price}") logger.debug(f"{pair} - {sp} : {stop_gap_price}")
if sp <= stop_gap_price: if sp <= stop_gap_price:
self.log_once(f"Removed {pair} from whitelist, because " self.log_once(
f"stop price {sp} would be <= stop limit {stop_gap_price}", logger.info) f"Removed {pair} from whitelist, because "
f"stop price {sp} would be <= stop limit {stop_gap_price}",
logger.info,
)
return False return False
return True return True
+52 -32
View File
@@ -1,6 +1,7 @@
""" """
Price pair list filter Price pair list filter
""" """
import logging import logging
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
@@ -14,28 +15,34 @@ logger = logging.getLogger(__name__)
class PriceFilter(IPairList): class PriceFilter(IPairList):
def __init__(
def __init__(self, exchange, pairlistmanager, self,
config: Config, pairlistconfig: Dict[str, Any], exchange,
pairlist_pos: int) -> None: pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0) self._low_price_ratio = pairlistconfig.get("low_price_ratio", 0)
if self._low_price_ratio < 0: if self._low_price_ratio < 0:
raise OperationalException("PriceFilter requires low_price_ratio to be >= 0") raise OperationalException("PriceFilter requires low_price_ratio to be >= 0")
self._min_price = pairlistconfig.get('min_price', 0) self._min_price = pairlistconfig.get("min_price", 0)
if self._min_price < 0: if self._min_price < 0:
raise OperationalException("PriceFilter requires min_price to be >= 0") raise OperationalException("PriceFilter requires min_price to be >= 0")
self._max_price = pairlistconfig.get('max_price', 0) self._max_price = pairlistconfig.get("max_price", 0)
if self._max_price < 0: if self._max_price < 0:
raise OperationalException("PriceFilter requires max_price to be >= 0") raise OperationalException("PriceFilter requires max_price to be >= 0")
self._max_value = pairlistconfig.get('max_value', 0) self._max_value = pairlistconfig.get("max_value", 0)
if self._max_value < 0: if self._max_value < 0:
raise OperationalException("PriceFilter requires max_value to be >= 0") raise OperationalException("PriceFilter requires max_value to be >= 0")
self._enabled = ((self._low_price_ratio > 0) or self._enabled = (
(self._min_price > 0) or (self._low_price_ratio > 0)
(self._max_price > 0) or or (self._min_price > 0)
(self._max_value > 0)) or (self._max_price > 0)
or (self._max_value > 0)
)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@@ -76,8 +83,9 @@ class PriceFilter(IPairList):
"type": "number", "type": "number",
"default": 0, "default": 0,
"description": "Low price ratio", "description": "Low price ratio",
"help": ("Remove pairs where a price move of 1 price unit (pip) " "help": (
"is above this ratio."), "Remove pairs where a price move of 1 price unit (pip) " "is above this ratio."
),
}, },
"min_price": { "min_price": {
"type": "number", "type": "number",
@@ -106,12 +114,14 @@ class PriceFilter(IPairList):
:param ticker: ticker dict as returned from ccxt.fetch_ticker :param ticker: ticker dict as returned from ccxt.fetch_ticker
:return: True if the pair can stay, false if it should be removed :return: True if the pair can stay, false if it should be removed
""" """
if ticker and 'last' in ticker and ticker['last'] is not None and ticker.get('last') != 0: if ticker and "last" in ticker and ticker["last"] is not None and ticker.get("last") != 0:
price: float = ticker['last'] price: float = ticker["last"]
else: else:
self.log_once(f"Removed {pair} from whitelist, because " self.log_once(
"ticker['last'] is empty (Usually no trade in the last 24h).", f"Removed {pair} from whitelist, because "
logger.info) "ticker['last'] is empty (Usually no trade in the last 24h).",
logger.info,
)
return False return False
# Perform low_price_ratio check. # Perform low_price_ratio check.
@@ -119,17 +129,19 @@ class PriceFilter(IPairList):
compare = self._exchange.price_get_one_pip(pair, price) compare = self._exchange.price_get_one_pip(pair, price)
changeperc = compare / price changeperc = compare / price
if changeperc > self._low_price_ratio: if changeperc > self._low_price_ratio:
self.log_once(f"Removed {pair} from whitelist, " self.log_once(
f"because 1 unit is {changeperc:.3%}", logger.info) f"Removed {pair} from whitelist, " f"because 1 unit is {changeperc:.3%}",
logger.info,
)
return False return False
# Perform low_amount check # Perform low_amount check
if self._max_value != 0: if self._max_value != 0:
market = self._exchange.markets[pair] market = self._exchange.markets[pair]
limits = market['limits'] limits = market["limits"]
if (limits['amount']['min'] is not None): if limits["amount"]["min"] is not None:
min_amount = limits['amount']['min'] min_amount = limits["amount"]["min"]
min_precision = market['precision']['amount'] min_precision = market["precision"]["amount"]
min_value = min_amount * price min_value = min_amount * price
if self._exchange.precisionMode == 4: if self._exchange.precisionMode == 4:
@@ -142,23 +154,31 @@ class PriceFilter(IPairList):
diff = next_value - min_value diff = next_value - min_value
if diff > self._max_value: if diff > self._max_value:
self.log_once(f"Removed {pair} from whitelist, " self.log_once(
f"because min value change of {diff} > {self._max_value}.", f"Removed {pair} from whitelist, "
logger.info) f"because min value change of {diff} > {self._max_value}.",
logger.info,
)
return False return False
# Perform min_price check. # Perform min_price check.
if self._min_price != 0: if self._min_price != 0:
if price < self._min_price: if price < self._min_price:
self.log_once(f"Removed {pair} from whitelist, " self.log_once(
f"because last price < {self._min_price:.8f}", logger.info) f"Removed {pair} from whitelist, "
f"because last price < {self._min_price:.8f}",
logger.info,
)
return False return False
# Perform max_price check. # Perform max_price check.
if self._max_price != 0: if self._max_price != 0:
if price > self._max_price: if price > self._max_price:
self.log_once(f"Removed {pair} from whitelist, " self.log_once(
f"because last price > {self._max_price:.8f}", logger.info) f"Removed {pair} from whitelist, "
f"because last price > {self._max_price:.8f}",
logger.info,
)
return False return False
return True return True
+22 -11
View File
@@ -3,6 +3,7 @@ External Pair List provider
Provides pair list from Leader data Provides pair list from Leader data
""" """
import logging import logging
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
@@ -28,18 +29,25 @@ class ProducerPairList(IPairList):
} }
], ],
""" """
is_pairlist_generator = True is_pairlist_generator = True
def __init__(self, exchange, pairlistmanager, def __init__(
config: Dict[str, Any], pairlistconfig: Dict[str, Any], self,
pairlist_pos: int) -> None: exchange,
pairlistmanager,
config: Dict[str, Any],
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._num_assets: int = self._pairlistconfig.get('number_assets', 0) self._num_assets: int = self._pairlistconfig.get("number_assets", 0)
self._producer_name = self._pairlistconfig.get('producer_name', 'default') self._producer_name = self._pairlistconfig.get("producer_name", "default")
if not config.get('external_message_consumer', {}).get('enabled'): if not config.get("external_message_consumer", {}).get("enabled"):
raise OperationalException( raise OperationalException(
"ProducerPairList requires external_message_consumer to be enabled.") "ProducerPairList requires external_message_consumer to be enabled."
)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@@ -74,21 +82,24 @@ class ProducerPairList(IPairList):
"type": "string", "type": "string",
"default": "default", "default": "default",
"description": "Producer name", "description": "Producer name",
"help": ("Name of the producer to use. Requires additional " "help": (
"external_message_consumer configuration.") "Name of the producer to use. Requires additional "
"external_message_consumer configuration."
),
}, },
} }
def _filter_pairlist(self, pairlist: Optional[List[str]]): def _filter_pairlist(self, pairlist: Optional[List[str]]):
upstream_pairlist = self._pairlistmanager._dataprovider.get_producer_pairs( upstream_pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(
self._producer_name) self._producer_name
)
if pairlist is None: if pairlist is None:
pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(self._producer_name) pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(self._producer_name)
pairs = list(dict.fromkeys(pairlist + upstream_pairlist)) pairs = list(dict.fromkeys(pairlist + upstream_pairlist))
if self._num_assets: if self._num_assets:
pairs = pairs[:self._num_assets] pairs = pairs[: self._num_assets]
return pairs return pairs
+66 -59
View File
@@ -3,6 +3,7 @@ Remote PairList provider
Provides pair list fetched from a remote source Provides pair list fetched from a remote source
""" """
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Tuple from typing import Any, Dict, List, Tuple
@@ -24,51 +25,59 @@ logger = logging.getLogger(__name__)
class RemotePairList(IPairList): class RemotePairList(IPairList):
is_pairlist_generator = True is_pairlist_generator = True
def __init__(self, exchange, pairlistmanager, def __init__(
config: Config, pairlistconfig: Dict[str, Any], self,
pairlist_pos: int) -> None: exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if 'number_assets' not in self._pairlistconfig: if "number_assets" not in self._pairlistconfig:
raise OperationalException( raise OperationalException(
'`number_assets` not specified. Please check your configuration ' "`number_assets` not specified. Please check your configuration "
'for "pairlist.config.number_assets"') 'for "pairlist.config.number_assets"'
)
if 'pairlist_url' not in self._pairlistconfig: if "pairlist_url" not in self._pairlistconfig:
raise OperationalException( raise OperationalException(
'`pairlist_url` not specified. Please check your configuration ' "`pairlist_url` not specified. Please check your configuration "
'for "pairlist.config.pairlist_url"') 'for "pairlist.config.pairlist_url"'
)
self._mode = self._pairlistconfig.get('mode', 'whitelist') self._mode = self._pairlistconfig.get("mode", "whitelist")
self._processing_mode = self._pairlistconfig.get('processing_mode', 'filter') self._processing_mode = self._pairlistconfig.get("processing_mode", "filter")
self._number_pairs = self._pairlistconfig['number_assets'] self._number_pairs = self._pairlistconfig["number_assets"]
self._refresh_period: int = self._pairlistconfig.get('refresh_period', 1800) self._refresh_period: int = self._pairlistconfig.get("refresh_period", 1800)
self._keep_pairlist_on_failure = self._pairlistconfig.get('keep_pairlist_on_failure', True) self._keep_pairlist_on_failure = self._pairlistconfig.get("keep_pairlist_on_failure", True)
self._pair_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
self._pairlist_url = self._pairlistconfig.get('pairlist_url', '') self._pairlist_url = self._pairlistconfig.get("pairlist_url", "")
self._read_timeout = self._pairlistconfig.get('read_timeout', 60) self._read_timeout = self._pairlistconfig.get("read_timeout", 60)
self._bearer_token = self._pairlistconfig.get('bearer_token', '') self._bearer_token = self._pairlistconfig.get("bearer_token", "")
self._init_done = False self._init_done = False
self._save_to_file = self._pairlistconfig.get('save_to_file', None) self._save_to_file = self._pairlistconfig.get("save_to_file", None)
self._last_pairlist: List[Any] = list() self._last_pairlist: List[Any] = list()
if self._mode not in ['whitelist', 'blacklist']: if self._mode not in ["whitelist", "blacklist"]:
raise OperationalException( raise OperationalException(
'`mode` not configured correctly. Supported Modes ' "`mode` not configured correctly. Supported Modes " 'are "whitelist","blacklist"'
'are "whitelist","blacklist"') )
if self._processing_mode not in ['filter', 'append']: if self._processing_mode not in ["filter", "append"]:
raise OperationalException( raise OperationalException(
'`processing_mode` not configured correctly. Supported Modes ' "`processing_mode` not configured correctly. Supported Modes "
'are "filter","append"') 'are "filter","append"'
)
if self._pairlist_pos == 0 and self._mode == 'blacklist': if self._pairlist_pos == 0 and self._mode == "blacklist":
raise OperationalException( raise OperationalException(
'A `blacklist` mode RemotePairList can not be on the first ' "A `blacklist` mode RemotePairList can not be on the first "
'position of your pairlist.') "position of your pairlist."
)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@@ -146,13 +155,15 @@ class RemotePairList(IPairList):
} }
def process_json(self, jsonparse) -> List[str]: def process_json(self, jsonparse) -> List[str]:
pairlist = jsonparse.get("pairs", [])
pairlist = jsonparse.get('pairs', []) remote_refresh_period = int(jsonparse.get("refresh_period", self._refresh_period))
remote_refresh_period = int(jsonparse.get('refresh_period', self._refresh_period))
if self._refresh_period < remote_refresh_period: if self._refresh_period < remote_refresh_period:
self.log_once(f'Refresh Period has been increased from {self._refresh_period}' self.log_once(
f' to minimum allowed: {remote_refresh_period} from Remote.', logger.info) f"Refresh Period has been increased from {self._refresh_period}"
f" to minimum allowed: {remote_refresh_period} from Remote.",
logger.info,
)
self._refresh_period = remote_refresh_period self._refresh_period = remote_refresh_period
self._pair_cache = TTLCache(maxsize=1, ttl=remote_refresh_period) self._pair_cache = TTLCache(maxsize=1, ttl=remote_refresh_period)
@@ -164,25 +175,21 @@ class RemotePairList(IPairList):
def return_last_pairlist(self) -> List[str]: def return_last_pairlist(self) -> List[str]:
if self._keep_pairlist_on_failure: if self._keep_pairlist_on_failure:
pairlist = self._last_pairlist pairlist = self._last_pairlist
self.log_once('Keeping last fetched pairlist', logger.info) self.log_once("Keeping last fetched pairlist", logger.info)
else: else:
pairlist = [] pairlist = []
return pairlist return pairlist
def fetch_pairlist(self) -> Tuple[List[str], float]: def fetch_pairlist(self) -> Tuple[List[str], float]:
headers = {"User-Agent": "Freqtrade/" + __version__ + " Remotepairlist"}
headers = {
'User-Agent': 'Freqtrade/' + __version__ + ' Remotepairlist'
}
if self._bearer_token: if self._bearer_token:
headers['Authorization'] = f'Bearer {self._bearer_token}' headers["Authorization"] = f"Bearer {self._bearer_token}"
try: try:
response = requests.get(self._pairlist_url, headers=headers, response = requests.get(self._pairlist_url, headers=headers, timeout=self._read_timeout)
timeout=self._read_timeout) content_type = response.headers.get("content-type")
content_type = response.headers.get('content-type')
time_elapsed = response.elapsed.total_seconds() time_elapsed = response.elapsed.total_seconds()
if "application/json" in str(content_type): if "application/json" in str(content_type):
@@ -191,14 +198,16 @@ class RemotePairList(IPairList):
try: try:
pairlist = self.process_json(jsonparse) pairlist = self.process_json(jsonparse)
except Exception as e: except Exception as e:
pairlist = self._handle_error(f'Failed processing JSON data: {type(e)}') pairlist = self._handle_error(f"Failed processing JSON data: {type(e)}")
else: else:
pairlist = self._handle_error(f'RemotePairList is not of type JSON.' pairlist = self._handle_error(
f' {self._pairlist_url}') f"RemotePairList is not of type JSON." f" {self._pairlist_url}"
)
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
pairlist = self._handle_error(f'Was not able to fetch pairlist from:' pairlist = self._handle_error(
f' {self._pairlist_url}') f"Was not able to fetch pairlist from:" f" {self._pairlist_url}"
)
time_elapsed = 0 time_elapsed = 0
@@ -219,7 +228,7 @@ class RemotePairList(IPairList):
""" """
if self._init_done: if self._init_done:
pairlist = self._pair_cache.get('pairlist') pairlist = self._pair_cache.get("pairlist")
if pairlist == [None]: if pairlist == [None]:
# Valid but empty pairlist. # Valid but empty pairlist.
return [] return []
@@ -243,7 +252,7 @@ class RemotePairList(IPairList):
jsonparse = rapidjson.load(json_file, parse_mode=CONFIG_PARSE_MODE) jsonparse = rapidjson.load(json_file, parse_mode=CONFIG_PARSE_MODE)
pairlist = self.process_json(jsonparse) pairlist = self.process_json(jsonparse)
except Exception as e: except Exception as e:
pairlist = self._handle_error(f'processing JSON data: {type(e)}') pairlist = self._handle_error(f"processing JSON data: {type(e)}")
else: else:
pairlist = self._handle_error(f"{self._pairlist_url} does not exist.") pairlist = self._handle_error(f"{self._pairlist_url} does not exist.")
@@ -255,18 +264,18 @@ class RemotePairList(IPairList):
pairlist = expand_pairlist(pairlist, list(self._exchange.get_markets().keys())) pairlist = expand_pairlist(pairlist, list(self._exchange.get_markets().keys()))
pairlist = self._whitelist_for_active_markets(pairlist) pairlist = self._whitelist_for_active_markets(pairlist)
pairlist = pairlist[:self._number_pairs] pairlist = pairlist[: self._number_pairs]
if pairlist: if pairlist:
self._pair_cache['pairlist'] = pairlist.copy() self._pair_cache["pairlist"] = pairlist.copy()
else: else:
# If pairlist is empty, set a dummy value to avoid fetching again # If pairlist is empty, set a dummy value to avoid fetching again
self._pair_cache['pairlist'] = [None] self._pair_cache["pairlist"] = [None]
if time_elapsed != 0.0: if time_elapsed != 0.0:
self.log_once(f'Pairlist Fetched in {time_elapsed} seconds.', logger.info) self.log_once(f"Pairlist Fetched in {time_elapsed} seconds.", logger.info)
else: else:
self.log_once('Fetched Pairlist.', logger.info) self.log_once("Fetched Pairlist.", logger.info)
self._last_pairlist = list(pairlist) self._last_pairlist = list(pairlist)
@@ -276,12 +285,10 @@ class RemotePairList(IPairList):
return pairlist return pairlist
def save_pairlist(self, pairlist: List[str], filename: str) -> None: def save_pairlist(self, pairlist: List[str], filename: str) -> None:
pairlist_data = { pairlist_data = {"pairs": pairlist}
"pairs": pairlist
}
try: try:
file_path = Path(filename) file_path = Path(filename)
with file_path.open('w') as json_file: with file_path.open("w") as json_file:
rapidjson.dump(pairlist_data, json_file) rapidjson.dump(pairlist_data, json_file)
logger.info(f"Processed pairlist saved to {filename}") logger.info(f"Processed pairlist saved to {filename}")
except Exception as e: except Exception as e:
@@ -314,5 +321,5 @@ class RemotePairList(IPairList):
if filtered: if filtered:
self.log_once(f"Blacklist - Filtered out pairs: {filtered}", logger.info) self.log_once(f"Blacklist - Filtered out pairs: {filtered}", logger.info)
merged_list = merged_list[:self._number_pairs] merged_list = merged_list[: self._number_pairs]
return merged_list return merged_list
+19 -12
View File
@@ -1,6 +1,7 @@
""" """
Shuffle pair list filter Shuffle pair list filter
""" """
import logging import logging
import random import random
from typing import Any, Dict, List, Literal from typing import Any, Dict, List, Literal
@@ -15,29 +16,34 @@ from freqtrade.util.periodic_cache import PeriodicCache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ShuffleValues = Literal['candle', 'iteration'] ShuffleValues = Literal["candle", "iteration"]
class ShuffleFilter(IPairList): class ShuffleFilter(IPairList):
def __init__(
def __init__(self, exchange, pairlistmanager, self,
config: Config, pairlistconfig: Dict[str, Any], exchange,
pairlist_pos: int) -> None: pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
# Apply seed in backtesting mode to get comparable results, # Apply seed in backtesting mode to get comparable results,
# but not in live modes to get a non-repeating order of pairs during live modes. # but not in live modes to get a non-repeating order of pairs during live modes.
if config.get('runmode') in (RunMode.LIVE, RunMode.DRY_RUN): if config.get("runmode") in (RunMode.LIVE, RunMode.DRY_RUN):
self._seed = None self._seed = None
logger.info("Live mode detected, not applying seed.") logger.info("Live mode detected, not applying seed.")
else: else:
self._seed = pairlistconfig.get('seed') self._seed = pairlistconfig.get("seed")
logger.info(f"Backtesting mode detected, applying seed value: {self._seed}") logger.info(f"Backtesting mode detected, applying seed value: {self._seed}")
self._random = random.Random(self._seed) self._random = random.Random(self._seed)
self._shuffle_freq: ShuffleValues = pairlistconfig.get('shuffle_frequency', 'candle') self._shuffle_freq: ShuffleValues = pairlistconfig.get("shuffle_frequency", "candle")
self.__pairlist_cache = PeriodicCache( self.__pairlist_cache = PeriodicCache(
maxsize=1000, ttl=timeframe_to_seconds(self._config['timeframe'])) maxsize=1000, ttl=timeframe_to_seconds(self._config["timeframe"])
)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@@ -52,8 +58,9 @@ class ShuffleFilter(IPairList):
""" """
Short whitelist method description - used for startup-messages Short whitelist method description - used for startup-messages
""" """
return (f"{self.name} - Shuffling pairs every {self._shuffle_freq}" + return f"{self.name} - Shuffling pairs every {self._shuffle_freq}" + (
(f", seed = {self._seed}." if self._seed is not None else ".")) f", seed = {self._seed}." if self._seed is not None else "."
)
@staticmethod @staticmethod
def description() -> str: def description() -> str:
@@ -87,7 +94,7 @@ class ShuffleFilter(IPairList):
""" """
pairlist_bef = tuple(pairlist) pairlist_bef = tuple(pairlist)
pairlist_new = self.__pairlist_cache.get(pairlist_bef) pairlist_new = self.__pairlist_cache.get(pairlist_bef)
if pairlist_new and self._shuffle_freq == 'candle': if pairlist_new and self._shuffle_freq == "candle":
# Use cached pairlist. # Use cached pairlist.
return pairlist_new return pairlist_new
# Shuffle is done inplace # Shuffle is done inplace
+25 -15
View File
@@ -1,6 +1,7 @@
""" """
Spread pair list filter Spread pair list filter
""" """
import logging import logging
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
@@ -14,16 +15,20 @@ logger = logging.getLogger(__name__)
class SpreadFilter(IPairList): class SpreadFilter(IPairList):
def __init__(
def __init__(self, exchange, pairlistmanager, self,
config: Config, pairlistconfig: Dict[str, Any], exchange,
pairlist_pos: int) -> None: pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005) self._max_spread_ratio = pairlistconfig.get("max_spread_ratio", 0.005)
self._enabled = self._max_spread_ratio != 0 self._enabled = self._max_spread_ratio != 0
if not self._exchange.get_option('tickers_have_bid_ask'): if not self._exchange.get_option("tickers_have_bid_ask"):
raise OperationalException( raise OperationalException(
f"{self.name} requires exchange to have bid/ask data for tickers, " f"{self.name} requires exchange to have bid/ask data for tickers, "
"which is not available for the selected exchange / trading mode." "which is not available for the selected exchange / trading mode."
@@ -42,8 +47,10 @@ class SpreadFilter(IPairList):
""" """
Short whitelist method description - used for startup-messages Short whitelist method description - used for startup-messages
""" """
return (f"{self.name} - Filtering pairs with ask/bid diff above " return (
f"{self._max_spread_ratio:.2%}.") f"{self.name} - Filtering pairs with ask/bid diff above "
f"{self._max_spread_ratio:.2%}."
)
@staticmethod @staticmethod
def description() -> str: def description() -> str:
@@ -67,15 +74,18 @@ class SpreadFilter(IPairList):
:param ticker: ticker dict as returned from ccxt.fetch_ticker :param ticker: ticker dict as returned from ccxt.fetch_ticker
:return: True if the pair can stay, false if it should be removed :return: True if the pair can stay, false if it should be removed
""" """
if ticker and 'bid' in ticker and 'ask' in ticker and ticker['ask'] and ticker['bid']: if ticker and "bid" in ticker and "ask" in ticker and ticker["ask"] and ticker["bid"]:
spread = 1 - ticker['bid'] / ticker['ask'] spread = 1 - ticker["bid"] / ticker["ask"]
if spread > self._max_spread_ratio: if spread > self._max_spread_ratio:
self.log_once(f"Removed {pair} from whitelist, because spread " self.log_once(
f"{spread:.3%} > {self._max_spread_ratio:.3%}", f"Removed {pair} from whitelist, because spread "
logger.info) f"{spread:.3%} > {self._max_spread_ratio:.3%}",
logger.info,
)
return False return False
else: else:
return True return True
self.log_once(f"Removed {pair} from whitelist due to invalid ticker data: {ticker}", self.log_once(
logger.info) f"Removed {pair} from whitelist due to invalid ticker data: {ticker}", logger.info
)
return False return False
+14 -8
View File
@@ -3,6 +3,7 @@ Static Pair List provider
Provides pair white list as it configured in config Provides pair white list as it configured in config
""" """
import logging import logging
from copy import deepcopy from copy import deepcopy
from typing import Any, Dict, List from typing import Any, Dict, List
@@ -16,15 +17,19 @@ logger = logging.getLogger(__name__)
class StaticPairList(IPairList): class StaticPairList(IPairList):
is_pairlist_generator = True is_pairlist_generator = True
def __init__(self, exchange, pairlistmanager, def __init__(
config: Config, pairlistconfig: Dict[str, Any], self,
pairlist_pos: int) -> None: exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._allow_inactive = self._pairlistconfig.get('allow_inactive', False) self._allow_inactive = self._pairlistconfig.get("allow_inactive", False)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@@ -65,11 +70,12 @@ class StaticPairList(IPairList):
""" """
if self._allow_inactive: if self._allow_inactive:
return self.verify_whitelist( return self.verify_whitelist(
self._config['exchange']['pair_whitelist'], logger.info, keep_invalid=True self._config["exchange"]["pair_whitelist"], logger.info, keep_invalid=True
) )
else: else:
return self._whitelist_for_active_markets( return self._whitelist_for_active_markets(
self.verify_whitelist(self._config['exchange']['pair_whitelist'], logger.info)) self.verify_whitelist(self._config["exchange"]["pair_whitelist"], logger.info)
)
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]: def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
""" """
@@ -80,7 +86,7 @@ class StaticPairList(IPairList):
:return: new whitelist :return: new whitelist
""" """
pairlist_ = deepcopy(pairlist) pairlist_ = deepcopy(pairlist)
for pair in self._config['exchange']['pair_whitelist']: for pair in self._config["exchange"]["pair_whitelist"]:
if pair not in pairlist_: if pair not in pairlist_:
pairlist_.append(pair) pairlist_.append(pair)
return pairlist_ return pairlist_
+49 -32
View File
@@ -1,6 +1,7 @@
""" """
Volatility pairlist filter Volatility pairlist filter
""" """
import logging import logging
import sys import sys
from datetime import timedelta from datetime import timedelta
@@ -26,29 +27,38 @@ class VolatilityFilter(IPairList):
Filters pairs by volatility Filters pairs by volatility
""" """
def __init__(self, exchange, pairlistmanager, def __init__(
config: Config, pairlistconfig: Dict[str, Any], self,
pairlist_pos: int) -> None: exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._days = pairlistconfig.get('lookback_days', 10) self._days = pairlistconfig.get("lookback_days", 10)
self._min_volatility = pairlistconfig.get('min_volatility', 0) self._min_volatility = pairlistconfig.get("min_volatility", 0)
self._max_volatility = pairlistconfig.get('max_volatility', sys.maxsize) self._max_volatility = pairlistconfig.get("max_volatility", sys.maxsize)
self._refresh_period = pairlistconfig.get('refresh_period', 1440) self._refresh_period = pairlistconfig.get("refresh_period", 1440)
self._def_candletype = self._config['candle_type_def'] self._def_candletype = self._config["candle_type_def"]
self._sort_direction: Optional[str] = pairlistconfig.get('sort_direction', None) self._sort_direction: Optional[str] = pairlistconfig.get("sort_direction", None)
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def']) candle_limit = exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"])
if self._days < 1: if self._days < 1:
raise OperationalException("VolatilityFilter requires lookback_days to be >= 1") raise OperationalException("VolatilityFilter requires lookback_days to be >= 1")
if self._days > candle_limit: if self._days > candle_limit:
raise OperationalException("VolatilityFilter requires lookback_days to not " raise OperationalException(
f"exceed exchange max request size ({candle_limit})") "VolatilityFilter requires lookback_days to not "
if self._sort_direction not in [None, 'asc', 'desc']: f"exceed exchange max request size ({candle_limit})"
raise OperationalException("VolatilityFilter requires sort_direction to be " )
"either None (undefined), 'asc' or 'desc'") if self._sort_direction not in [None, "asc", "desc"]:
raise OperationalException(
"VolatilityFilter requires sort_direction to be "
"either None (undefined), 'asc' or 'desc'"
)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@@ -63,9 +73,11 @@ class VolatilityFilter(IPairList):
""" """
Short whitelist method description - used for startup-messages Short whitelist method description - used for startup-messages
""" """
return (f"{self.name} - Filtering pairs with volatility range " return (
f"{self._min_volatility}-{self._max_volatility} " f"{self.name} - Filtering pairs with volatility range "
f" the last {self._days} {plural(self._days, 'day')}.") f"{self._min_volatility}-{self._max_volatility} "
f" the last {self._days} {plural(self._days, 'day')}."
)
@staticmethod @staticmethod
def description() -> str: def description() -> str:
@@ -99,7 +111,7 @@ class VolatilityFilter(IPairList):
"description": "Sort pairlist", "description": "Sort pairlist",
"help": "Sort Pairlist ascending or descending by volatility.", "help": "Sort Pairlist ascending or descending by volatility.",
}, },
**IPairList.refresh_period_parameter() **IPairList.refresh_period_parameter(),
} }
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]: def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
@@ -110,7 +122,8 @@ class VolatilityFilter(IPairList):
:return: new allowlist :return: new allowlist
""" """
needed_pairs: ListPairsWithTimeframes = [ needed_pairs: ListPairsWithTimeframes = [
(p, '1d', self._def_candletype) for p in pairlist if p not in self._pair_cache] (p, "1d", self._def_candletype) for p in pairlist if p not in self._pair_cache
]
since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days)) since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days))
candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms) candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms)
@@ -118,7 +131,7 @@ class VolatilityFilter(IPairList):
resulting_pairlist: List[str] = [] resulting_pairlist: List[str] = []
volatilitys: Dict[str, float] = {} volatilitys: Dict[str, float] = {}
for p in pairlist: for p in pairlist:
daily_candles = candles.get((p, '1d', self._def_candletype), None) daily_candles = candles.get((p, "1d", self._def_candletype), None)
volatility_avg = self._calculate_volatility(p, daily_candles) volatility_avg = self._calculate_volatility(p, daily_candles)
@@ -132,18 +145,20 @@ class VolatilityFilter(IPairList):
self.log_once(f"Removed {p} from whitelist, no candles found.", logger.info) self.log_once(f"Removed {p} from whitelist, no candles found.", logger.info)
if self._sort_direction: if self._sort_direction:
resulting_pairlist = sorted(resulting_pairlist, resulting_pairlist = sorted(
key=lambda p: volatilitys[p], resulting_pairlist,
reverse=self._sort_direction == 'desc') key=lambda p: volatilitys[p],
reverse=self._sort_direction == "desc",
)
return resulting_pairlist return resulting_pairlist
def _calculate_volatility(self, pair: str, daily_candles: DataFrame) -> Optional[float]: def _calculate_volatility(self, pair: str, daily_candles: DataFrame) -> Optional[float]:
# Check symbol in cache # Check symbol in cache
if (volatility_avg := self._pair_cache.get(pair, None)) is not None: if (volatility_avg := self._pair_cache.get(pair, None)) is not None:
return volatility_avg return volatility_avg
if daily_candles is not None and not daily_candles.empty: if daily_candles is not None and not daily_candles.empty:
returns = (np.log(daily_candles["close"].shift(1) / daily_candles["close"])) returns = np.log(daily_candles["close"].shift(1) / daily_candles["close"])
returns.fillna(0, inplace=True) returns.fillna(0, inplace=True)
volatility_series = returns.rolling(window=self._days).std() * np.sqrt(self._days) volatility_series = returns.rolling(window=self._days).std() * np.sqrt(self._days)
@@ -165,11 +180,13 @@ class VolatilityFilter(IPairList):
if self._min_volatility <= volatility_avg <= self._max_volatility: if self._min_volatility <= volatility_avg <= self._max_volatility:
result = True result = True
else: else:
self.log_once(f"Removed {pair} from whitelist, because volatility " self.log_once(
f"over {self._days} {plural(self._days, 'day')} " f"Removed {pair} from whitelist, because volatility "
f"is: {volatility_avg:.3f} " f"over {self._days} {plural(self._days, 'day')} "
f"which is not in the configured range of " f"is: {volatility_avg:.3f} "
f"{self._min_volatility}-{self._max_volatility}.", f"which is not in the configured range of "
logger.info) f"{self._min_volatility}-{self._max_volatility}.",
logger.info,
)
result = False result = False
return result return result
+111 -81
View File
@@ -3,6 +3,7 @@ Volume PairList provider
Provides dynamic pair list based on trade volumes Provides dynamic pair list based on trade volumes
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta
from typing import Any, Dict, List, Literal from typing import Any, Dict, List, Literal
@@ -20,45 +21,50 @@ from freqtrade.util import dt_now, format_ms_time
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SORT_VALUES = ['quoteVolume'] SORT_VALUES = ["quoteVolume"]
class VolumePairList(IPairList): class VolumePairList(IPairList):
is_pairlist_generator = True is_pairlist_generator = True
def __init__(self, exchange, pairlistmanager, def __init__(
config: Config, pairlistconfig: Dict[str, Any], self,
pairlist_pos: int) -> None: exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if 'number_assets' not in self._pairlistconfig: if "number_assets" not in self._pairlistconfig:
raise OperationalException( raise OperationalException(
'`number_assets` not specified. Please check your configuration ' "`number_assets` not specified. Please check your configuration "
'for "pairlist.config.number_assets"') 'for "pairlist.config.number_assets"'
)
self._stake_currency = config['stake_currency'] self._stake_currency = config["stake_currency"]
self._number_pairs = self._pairlistconfig['number_assets'] self._number_pairs = self._pairlistconfig["number_assets"]
self._sort_key: Literal['quoteVolume'] = self._pairlistconfig.get('sort_key', 'quoteVolume') self._sort_key: Literal["quoteVolume"] = self._pairlistconfig.get("sort_key", "quoteVolume")
self._min_value = self._pairlistconfig.get('min_value', 0) self._min_value = self._pairlistconfig.get("min_value", 0)
self._max_value = self._pairlistconfig.get("max_value", None) self._max_value = self._pairlistconfig.get("max_value", None)
self._refresh_period = self._pairlistconfig.get('refresh_period', 1800) self._refresh_period = self._pairlistconfig.get("refresh_period", 1800)
self._pair_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
self._lookback_days = self._pairlistconfig.get('lookback_days', 0) self._lookback_days = self._pairlistconfig.get("lookback_days", 0)
self._lookback_timeframe = self._pairlistconfig.get('lookback_timeframe', '1d') self._lookback_timeframe = self._pairlistconfig.get("lookback_timeframe", "1d")
self._lookback_period = self._pairlistconfig.get('lookback_period', 0) self._lookback_period = self._pairlistconfig.get("lookback_period", 0)
self._def_candletype = self._config['candle_type_def'] self._def_candletype = self._config["candle_type_def"]
if (self._lookback_days > 0) & (self._lookback_period > 0): if (self._lookback_days > 0) & (self._lookback_period > 0):
raise OperationalException( raise OperationalException(
'Ambigous configuration: lookback_days and lookback_period both set in pairlist ' "Ambigous configuration: lookback_days and lookback_period both set in pairlist "
'config. Please set lookback_days only or lookback_period and lookback_timeframe ' "config. Please set lookback_days only or lookback_period and lookback_timeframe "
'and restart the bot.' "and restart the bot."
) )
# overwrite lookback timeframe and days when lookback_days is set # overwrite lookback timeframe and days when lookback_days is set
if self._lookback_days > 0: if self._lookback_days > 0:
self._lookback_timeframe = '1d' self._lookback_timeframe = "1d"
self._lookback_period = self._lookback_days self._lookback_period = self._lookback_days
# get timeframe in minutes and seconds # get timeframe in minutes and seconds
@@ -70,14 +76,15 @@ class VolumePairList(IPairList):
if self._use_range & (self._refresh_period < _tf_in_sec): if self._use_range & (self._refresh_period < _tf_in_sec):
raise OperationalException( raise OperationalException(
f'Refresh period of {self._refresh_period} seconds is smaller than one ' f"Refresh period of {self._refresh_period} seconds is smaller than one "
f'timeframe of {self._lookback_timeframe}. Please adjust refresh_period ' f"timeframe of {self._lookback_timeframe}. Please adjust refresh_period "
f'to at least {_tf_in_sec} and restart the bot.' f"to at least {_tf_in_sec} and restart the bot."
) )
if (not self._use_range and not ( if not self._use_range and not (
self._exchange.exchange_has('fetchTickers') self._exchange.exchange_has("fetchTickers")
and self._exchange.get_option("tickers_have_quoteVolume"))): and self._exchange.get_option("tickers_have_quoteVolume")
):
raise OperationalException( raise OperationalException(
"Exchange does not support dynamic whitelist in this configuration. " "Exchange does not support dynamic whitelist in this configuration. "
"Please edit your config and either remove Volumepairlist, " "Please edit your config and either remove Volumepairlist, "
@@ -85,16 +92,18 @@ class VolumePairList(IPairList):
) )
if not self._validate_keys(self._sort_key): if not self._validate_keys(self._sort_key):
raise OperationalException( raise OperationalException(f"key {self._sort_key} not in {SORT_VALUES}")
f'key {self._sort_key} not in {SORT_VALUES}')
candle_limit = exchange.ohlcv_candle_limit( candle_limit = exchange.ohlcv_candle_limit(
self._lookback_timeframe, self._config['candle_type_def']) self._lookback_timeframe, self._config["candle_type_def"]
)
if self._lookback_period < 0: if self._lookback_period < 0:
raise OperationalException("VolumeFilter requires lookback_period to be >= 0") raise OperationalException("VolumeFilter requires lookback_period to be >= 0")
if self._lookback_period > candle_limit: if self._lookback_period > candle_limit:
raise OperationalException("VolumeFilter requires lookback_period to not " raise OperationalException(
f"exceed exchange max request size ({candle_limit})") "VolumeFilter requires lookback_period to not "
f"exceed exchange max request size ({candle_limit})"
)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@@ -175,30 +184,37 @@ class VolumePairList(IPairList):
""" """
# Generate dynamic whitelist # Generate dynamic whitelist
# Must always run if this pairlist is not the first in the list. # Must always run if this pairlist is not the first in the list.
pairlist = self._pair_cache.get('pairlist') pairlist = self._pair_cache.get("pairlist")
if pairlist: if pairlist:
# Item found - no refresh necessary # Item found - no refresh necessary
return pairlist.copy() return pairlist.copy()
else: else:
# Use fresh pairlist # Use fresh pairlist
# Check if pair quote currency equals to the stake currency. # Check if pair quote currency equals to the stake currency.
_pairlist = [k for k in self._exchange.get_markets( _pairlist = [
quote_currencies=[self._stake_currency], k
tradable_only=True, active_only=True).keys()] for k in self._exchange.get_markets(
quote_currencies=[self._stake_currency], tradable_only=True, active_only=True
).keys()
]
# No point in testing for blacklisted pairs... # No point in testing for blacklisted pairs...
_pairlist = self.verify_blacklist(_pairlist, logger.info) _pairlist = self.verify_blacklist(_pairlist, logger.info)
if not self._use_range: if not self._use_range:
filtered_tickers = [ filtered_tickers = [
v for k, v in tickers.items() v
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency for k, v in tickers.items()
if (
self._exchange.get_pair_quote_currency(k) == self._stake_currency
and (self._use_range or v.get(self._sort_key) is not None) and (self._use_range or v.get(self._sort_key) is not None)
and v['symbol'] in _pairlist)] and v["symbol"] in _pairlist
pairlist = [s['symbol'] for s in filtered_tickers] )
]
pairlist = [s["symbol"] for s in filtered_tickers]
else: else:
pairlist = _pairlist pairlist = _pairlist
pairlist = self.filter_pairlist(pairlist, tickers) pairlist = self.filter_pairlist(pairlist, tickers)
self._pair_cache['pairlist'] = pairlist.copy() self._pair_cache["pairlist"] = pairlist.copy()
return pairlist return pairlist
@@ -212,81 +228,95 @@ class VolumePairList(IPairList):
""" """
if self._use_range: if self._use_range:
# Create bare minimum from tickers structure. # Create bare minimum from tickers structure.
filtered_tickers: List[Dict[str, Any]] = [{'symbol': k} for k in pairlist] filtered_tickers: List[Dict[str, Any]] = [{"symbol": k} for k in pairlist]
# get lookback period in ms, for exchange ohlcv fetch # get lookback period in ms, for exchange ohlcv fetch
since_ms = int(timeframe_to_prev_date( since_ms = (
self._lookback_timeframe, int(
dt_now() + timedelta( timeframe_to_prev_date(
minutes=-(self._lookback_period * self._tf_in_min) - self._tf_in_min) self._lookback_timeframe,
).timestamp()) * 1000 dt_now()
+ timedelta(
minutes=-(self._lookback_period * self._tf_in_min) - self._tf_in_min
),
).timestamp()
)
* 1000
)
to_ms = int(timeframe_to_prev_date( to_ms = (
self._lookback_timeframe, int(
dt_now() - timedelta(minutes=self._tf_in_min) timeframe_to_prev_date(
).timestamp()) * 1000 self._lookback_timeframe, dt_now() - timedelta(minutes=self._tf_in_min)
).timestamp()
)
* 1000
)
# todo: utc date output for starting date # todo: utc date output for starting date
self.log_once(f"Using volume range of {self._lookback_period} candles, timeframe: " self.log_once(
f"{self._lookback_timeframe}, starting from {format_ms_time(since_ms)} " f"Using volume range of {self._lookback_period} candles, timeframe: "
f"till {format_ms_time(to_ms)}", logger.info) f"{self._lookback_timeframe}, starting from {format_ms_time(since_ms)} "
f"till {format_ms_time(to_ms)}",
logger.info,
)
needed_pairs: ListPairsWithTimeframes = [ needed_pairs: ListPairsWithTimeframes = [
(p, self._lookback_timeframe, self._def_candletype) for p in (p, self._lookback_timeframe, self._def_candletype)
[s['symbol'] for s in filtered_tickers] for p in [s["symbol"] for s in filtered_tickers]
if p not in self._pair_cache if p not in self._pair_cache
] ]
candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms) candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms)
for i, p in enumerate(filtered_tickers): for i, p in enumerate(filtered_tickers):
contract_size = self._exchange.markets[p['symbol']].get('contractSize', 1.0) or 1.0 contract_size = self._exchange.markets[p["symbol"]].get("contractSize", 1.0) or 1.0
pair_candles = candles[ pair_candles = (
(p['symbol'], self._lookback_timeframe, self._def_candletype) candles[(p["symbol"], self._lookback_timeframe, self._def_candletype)]
] if ( if (p["symbol"], self._lookback_timeframe, self._def_candletype) in candles
p['symbol'], self._lookback_timeframe, self._def_candletype else None
) in candles else None )
# in case of candle data calculate typical price and quoteVolume for candle # in case of candle data calculate typical price and quoteVolume for candle
if pair_candles is not None and not pair_candles.empty: if pair_candles is not None and not pair_candles.empty:
if self._exchange.get_option("ohlcv_volume_currency") == "base": if self._exchange.get_option("ohlcv_volume_currency") == "base":
pair_candles['typical_price'] = (pair_candles['high'] + pair_candles['low'] pair_candles["typical_price"] = (
+ pair_candles['close']) / 3 pair_candles["high"] + pair_candles["low"] + pair_candles["close"]
) / 3
pair_candles['quoteVolume'] = ( pair_candles["quoteVolume"] = (
pair_candles['volume'] * pair_candles['typical_price'] pair_candles["volume"] * pair_candles["typical_price"] * contract_size
* contract_size
) )
else: else:
# Exchange ohlcv data is in quote volume already. # Exchange ohlcv data is in quote volume already.
pair_candles['quoteVolume'] = pair_candles['volume'] pair_candles["quoteVolume"] = pair_candles["volume"]
# ensure that a rolling sum over the lookback_period is built # ensure that a rolling sum over the lookback_period is built
# if pair_candles contains more candles than lookback_period # if pair_candles contains more candles than lookback_period
quoteVolume = (pair_candles['quoteVolume'] quoteVolume = (
.rolling(self._lookback_period) pair_candles["quoteVolume"]
.sum() .rolling(self._lookback_period)
.fillna(0) .sum()
.iloc[-1]) .fillna(0)
.iloc[-1]
)
# replace quoteVolume with range quoteVolume sum calculated above # replace quoteVolume with range quoteVolume sum calculated above
filtered_tickers[i]['quoteVolume'] = quoteVolume filtered_tickers[i]["quoteVolume"] = quoteVolume
else: else:
filtered_tickers[i]['quoteVolume'] = 0 filtered_tickers[i]["quoteVolume"] = 0
else: else:
# Tickers mode - filter based on incoming pairlist. # Tickers mode - filter based on incoming pairlist.
filtered_tickers = [v for k, v in tickers.items() if k in pairlist] filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
if self._min_value > 0: if self._min_value > 0:
filtered_tickers = [ filtered_tickers = [v for v in filtered_tickers if v[self._sort_key] > self._min_value]
v for v in filtered_tickers if v[self._sort_key] > self._min_value]
if self._max_value is not None: if self._max_value is not None:
filtered_tickers = [ filtered_tickers = [v for v in filtered_tickers if v[self._sort_key] < self._max_value]
v for v in filtered_tickers if v[self._sort_key] < self._max_value]
sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[self._sort_key]) sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[self._sort_key])
# Validate whitelist to only have active market pairs # Validate whitelist to only have active market pairs
pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers]) pairs = self._whitelist_for_active_markets([s["symbol"] for s in sorted_tickers])
pairs = self.verify_blacklist(pairs, logmethod=logger.info) pairs = self.verify_blacklist(pairs, logmethod=logger.info)
# Limit pairlist to the requested number of pairs # Limit pairlist to the requested number of pairs
pairs = pairs[:self._number_pairs] pairs = pairs[: self._number_pairs]
return pairs return pairs
+10 -14
View File
@@ -4,8 +4,9 @@ from typing import List
from freqtrade.constants import Config from freqtrade.constants import Config
def expand_pairlist(wildcardpl: List[str], available_pairs: List[str], def expand_pairlist(
keep_invalid: bool = False) -> List[str]: wildcardpl: List[str], available_pairs: List[str], keep_invalid: bool = False
) -> List[str]:
""" """
Expand pairlist potentially containing wildcards based on available markets. Expand pairlist potentially containing wildcards based on available markets.
This will implicitly filter all pairs in the wildcard-list which are not in available_pairs. This will implicitly filter all pairs in the wildcard-list which are not in available_pairs.
@@ -20,34 +21,29 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
for pair_wc in wildcardpl: for pair_wc in wildcardpl:
try: try:
comp = re.compile(pair_wc, re.IGNORECASE) comp = re.compile(pair_wc, re.IGNORECASE)
result_partial = [ result_partial = [pair for pair in available_pairs if re.fullmatch(comp, pair)]
pair for pair in available_pairs if re.fullmatch(comp, pair)
]
# Add all matching pairs. # Add all matching pairs.
# If there are no matching pairs (Pair not on exchange) keep it. # If there are no matching pairs (Pair not on exchange) keep it.
result += result_partial or [pair_wc] result += result_partial or [pair_wc]
except re.error as err: except re.error as err:
raise ValueError(f"Wildcard error in {pair_wc}, {err}") raise ValueError(f"Wildcard error in {pair_wc}, {err}")
result = [element for element in result if re.fullmatch(r'^[A-Za-z0-9:/-]+$', element)] result = [element for element in result if re.fullmatch(r"^[A-Za-z0-9:/-]+$", element)]
else: else:
for pair_wc in wildcardpl: for pair_wc in wildcardpl:
try: try:
comp = re.compile(pair_wc, re.IGNORECASE) comp = re.compile(pair_wc, re.IGNORECASE)
result += [ result += [pair for pair in available_pairs if re.fullmatch(comp, pair)]
pair for pair in available_pairs if re.fullmatch(comp, pair)
]
except re.error as err: except re.error as err:
raise ValueError(f"Wildcard error in {pair_wc}, {err}") raise ValueError(f"Wildcard error in {pair_wc}, {err}")
return result return result
def dynamic_expand_pairlist(config: Config, markets: List[str]) -> List[str]: def dynamic_expand_pairlist(config: Config, markets: List[str]) -> List[str]:
expanded_pairs = expand_pairlist(config['pairs'], markets) expanded_pairs = expand_pairlist(config["pairs"], markets)
if config.get('freqai', {}).get('enabled', False): if config.get("freqai", {}).get("enabled", False):
corr_pairlist = config['freqai']['feature_parameters']['include_corr_pairlist'] corr_pairlist = config["freqai"]["feature_parameters"]["include_corr_pairlist"]
expanded_pairs += [pair for pair in corr_pairlist expanded_pairs += [pair for pair in corr_pairlist if pair not in config["pairs"]]
if pair not in config['pairs']]
return expanded_pairs return expanded_pairs
@@ -1,6 +1,7 @@
""" """
Rate of change pairlist filter Rate of change pairlist filter
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
@@ -20,30 +21,38 @@ logger = logging.getLogger(__name__)
class RangeStabilityFilter(IPairList): class RangeStabilityFilter(IPairList):
def __init__(
def __init__(self, exchange, pairlistmanager, self,
config: Config, pairlistconfig: Dict[str, Any], exchange,
pairlist_pos: int) -> None: pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._days = pairlistconfig.get('lookback_days', 10) self._days = pairlistconfig.get("lookback_days", 10)
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01) self._min_rate_of_change = pairlistconfig.get("min_rate_of_change", 0.01)
self._max_rate_of_change = pairlistconfig.get('max_rate_of_change') self._max_rate_of_change = pairlistconfig.get("max_rate_of_change")
self._refresh_period = pairlistconfig.get('refresh_period', 86400) self._refresh_period = pairlistconfig.get("refresh_period", 86400)
self._def_candletype = self._config['candle_type_def'] self._def_candletype = self._config["candle_type_def"]
self._sort_direction: Optional[str] = pairlistconfig.get('sort_direction', None) self._sort_direction: Optional[str] = pairlistconfig.get("sort_direction", None)
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def']) candle_limit = exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"])
if self._days < 1: if self._days < 1:
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1") raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
if self._days > candle_limit: if self._days > candle_limit:
raise OperationalException("RangeStabilityFilter requires lookback_days to not " raise OperationalException(
f"exceed exchange max request size ({candle_limit})") "RangeStabilityFilter requires lookback_days to not "
if self._sort_direction not in [None, 'asc', 'desc']: f"exceed exchange max request size ({candle_limit})"
raise OperationalException("RangeStabilityFilter requires sort_direction to be " )
"either None (undefined), 'asc' or 'desc'") if self._sort_direction not in [None, "asc", "desc"]:
raise OperationalException(
"RangeStabilityFilter requires sort_direction to be "
"either None (undefined), 'asc' or 'desc'"
)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@@ -60,10 +69,12 @@ class RangeStabilityFilter(IPairList):
""" """
max_rate_desc = "" max_rate_desc = ""
if self._max_rate_of_change: if self._max_rate_of_change:
max_rate_desc = (f" and above {self._max_rate_of_change}") max_rate_desc = f" and above {self._max_rate_of_change}"
return (f"{self.name} - Filtering pairs with rate of change below " return (
f"{self._min_rate_of_change}{max_rate_desc} over the " f"{self.name} - Filtering pairs with rate of change below "
f"last {plural(self._days, 'day')}.") f"{self._min_rate_of_change}{max_rate_desc} over the "
f"last {plural(self._days, 'day')}."
)
@staticmethod @staticmethod
def description() -> str: def description() -> str:
@@ -97,7 +108,7 @@ class RangeStabilityFilter(IPairList):
"description": "Sort pairlist", "description": "Sort pairlist",
"help": "Sort Pairlist ascending or descending by rate of change.", "help": "Sort Pairlist ascending or descending by rate of change.",
}, },
**IPairList.refresh_period_parameter() **IPairList.refresh_period_parameter(),
} }
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]: def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
@@ -108,7 +119,8 @@ class RangeStabilityFilter(IPairList):
:return: new allowlist :return: new allowlist
""" """
needed_pairs: ListPairsWithTimeframes = [ needed_pairs: ListPairsWithTimeframes = [
(p, '1d', self._def_candletype) for p in pairlist if p not in self._pair_cache] (p, "1d", self._def_candletype) for p in pairlist if p not in self._pair_cache
]
since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days + 1)) since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days + 1))
candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms) candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms)
@@ -117,7 +129,7 @@ class RangeStabilityFilter(IPairList):
pct_changes: Dict[str, float] = {} pct_changes: Dict[str, float] = {}
for p in pairlist: for p in pairlist:
daily_candles = candles.get((p, '1d', self._def_candletype), None) daily_candles = candles.get((p, "1d", self._def_candletype), None)
pct_change = self._calculate_rate_of_change(p, daily_candles) pct_change = self._calculate_rate_of_change(p, daily_candles)
@@ -129,9 +141,11 @@ class RangeStabilityFilter(IPairList):
self.log_once(f"Removed {p} from whitelist, no candles found.", logger.info) self.log_once(f"Removed {p} from whitelist, no candles found.", logger.info)
if self._sort_direction: if self._sort_direction:
resulting_pairlist = sorted(resulting_pairlist, resulting_pairlist = sorted(
key=lambda p: pct_changes[p], resulting_pairlist,
reverse=self._sort_direction == 'desc') key=lambda p: pct_changes[p],
reverse=self._sort_direction == "desc",
)
return resulting_pairlist return resulting_pairlist
def _calculate_rate_of_change(self, pair: str, daily_candles: DataFrame) -> Optional[float]: def _calculate_rate_of_change(self, pair: str, daily_candles: DataFrame) -> Optional[float]:
@@ -139,9 +153,8 @@ class RangeStabilityFilter(IPairList):
if (pct_change := self._pair_cache.get(pair, None)) is not None: if (pct_change := self._pair_cache.get(pair, None)) is not None:
return pct_change return pct_change
if daily_candles is not None and not daily_candles.empty: if daily_candles is not None and not daily_candles.empty:
highest_high = daily_candles["high"].max()
highest_high = daily_candles['high'].max() lowest_low = daily_candles["low"].min()
lowest_low = daily_candles['low'].min()
pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0 pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0
self._pair_cache[pair] = pct_change self._pair_cache[pair] = pct_change
return pct_change return pct_change
@@ -158,10 +171,12 @@ class RangeStabilityFilter(IPairList):
result = True result = True
if pct_change < self._min_rate_of_change: if pct_change < self._min_rate_of_change:
self.log_once(f"Removed {pair} from whitelist, because rate of change " self.log_once(
f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, " f"Removed {pair} from whitelist, because rate of change "
f"which is below the threshold of {self._min_rate_of_change}.", f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, "
logger.info) f"which is below the threshold of {self._min_rate_of_change}.",
logger.info,
)
result = False result = False
if self._max_rate_of_change: if self._max_rate_of_change:
if pct_change > self._max_rate_of_change: if pct_change > self._max_rate_of_change:
@@ -169,6 +184,7 @@ class RangeStabilityFilter(IPairList):
f"Removed {pair} from whitelist, because rate of change " f"Removed {pair} from whitelist, because rate of change "
f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, " f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, "
f"which is above the threshold of {self._max_rate_of_change}.", f"which is above the threshold of {self._max_rate_of_change}.",
logger.info) logger.info,
)
result = False result = False
return result return result
+19 -15
View File
@@ -1,6 +1,7 @@
""" """
PairList manager class PairList manager class
""" """
import logging import logging
from functools import partial from functools import partial
from typing import Dict, List, Optional from typing import Dict, List, Optional
@@ -22,24 +23,24 @@ logger = logging.getLogger(__name__)
class PairListManager(LoggingMixin): class PairListManager(LoggingMixin):
def __init__( def __init__(
self, exchange, config: Config, dataprovider: Optional[DataProvider] = None) -> None: self, exchange, config: Config, dataprovider: Optional[DataProvider] = None
) -> None:
self._exchange = exchange self._exchange = exchange
self._config = config self._config = config
self._whitelist = self._config['exchange'].get('pair_whitelist') self._whitelist = self._config["exchange"].get("pair_whitelist")
self._blacklist = self._config['exchange'].get('pair_blacklist', []) self._blacklist = self._config["exchange"].get("pair_blacklist", [])
self._pairlist_handlers: List[IPairList] = [] self._pairlist_handlers: List[IPairList] = []
self._tickers_needed = False self._tickers_needed = False
self._dataprovider: Optional[DataProvider] = dataprovider self._dataprovider: Optional[DataProvider] = dataprovider
for pairlist_handler_config in self._config.get('pairlists', []): for pairlist_handler_config in self._config.get("pairlists", []):
pairlist_handler = PairListResolver.load_pairlist( pairlist_handler = PairListResolver.load_pairlist(
pairlist_handler_config['method'], pairlist_handler_config["method"],
exchange=exchange, exchange=exchange,
pairlistmanager=self, pairlistmanager=self,
config=config, config=config,
pairlistconfig=pairlist_handler_config, pairlistconfig=pairlist_handler_config,
pairlist_pos=len(self._pairlist_handlers) pairlist_pos=len(self._pairlist_handlers),
) )
self._tickers_needed |= pairlist_handler.needstickers self._tickers_needed |= pairlist_handler.needstickers
self._pairlist_handlers.append(pairlist_handler) self._pairlist_handlers.append(pairlist_handler)
@@ -47,7 +48,7 @@ class PairListManager(LoggingMixin):
if not self._pairlist_handlers: if not self._pairlist_handlers:
raise OperationalException("No Pairlist Handlers defined") raise OperationalException("No Pairlist Handlers defined")
if self._tickers_needed and not self._exchange.exchange_has('fetchTickers'): if self._tickers_needed and not self._exchange.exchange_has("fetchTickers"):
invalid = ". ".join([p.name for p in self._pairlist_handlers if p.needstickers]) invalid = ". ".join([p.name for p in self._pairlist_handlers if p.needstickers])
raise OperationalException( raise OperationalException(
@@ -56,7 +57,7 @@ class PairListManager(LoggingMixin):
f"{invalid}." f"{invalid}."
) )
refresh_period = config.get('pairlist_refresh_period', 3600) refresh_period = config.get("pairlist_refresh_period", 3600)
LoggingMixin.__init__(self, logger, refresh_period) LoggingMixin.__init__(self, logger, refresh_period)
@property @property
@@ -135,8 +136,9 @@ class PairListManager(LoggingMixin):
pairlist.remove(pair) pairlist.remove(pair)
return pairlist return pairlist
def verify_whitelist(self, pairlist: List[str], logmethod, def verify_whitelist(
keep_invalid: bool = False) -> List[str]: self, pairlist: List[str], logmethod, keep_invalid: bool = False
) -> List[str]:
""" """
Verify and remove items from pairlist - returning a filtered pairlist. Verify and remove items from pairlist - returning a filtered pairlist.
Logs a warning or info depending on `aswarning`. Logs a warning or info depending on `aswarning`.
@@ -155,14 +157,16 @@ class PairListManager(LoggingMixin):
return whitelist return whitelist
def create_pair_list( def create_pair_list(
self, pairs: List[str], timeframe: Optional[str] = None) -> ListPairsWithTimeframes: self, pairs: List[str], timeframe: Optional[str] = None
) -> ListPairsWithTimeframes:
""" """
Create list of pair tuples with (pair, timeframe) Create list of pair tuples with (pair, timeframe)
""" """
return [ return [
( (
pair, pair,
timeframe or self._config['timeframe'], timeframe or self._config["timeframe"],
self._config.get('candle_type_def', CandleType.SPOT) self._config.get("candle_type_def", CandleType.SPOT),
) for pair in pairs )
for pair in pairs
] ]