stable/freqtrade/plugins/pairlist/rangestabilityfilter.py

127 lines
5.3 KiB
Python
Raw Normal View History

2020-11-21 14:29:11 +00:00
"""
2020-11-22 18:59:18 +00:00
Rate of change pairlist filter
2020-11-21 14:29:11 +00:00
"""
import logging
2020-12-15 19:59:58 +00:00
from copy import deepcopy
from typing import Any, Dict, List, Optional
2020-11-21 14:29:11 +00:00
import arrow
from cachetools import TTLCache
2020-12-15 19:59:58 +00:00
from pandas import DataFrame
2020-11-21 14:29:11 +00:00
2022-09-18 11:31:52 +00:00
from freqtrade.constants import Config, ListPairsWithTimeframes
2020-11-21 14:29:11 +00:00
from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers
2020-11-21 14:29:11 +00:00
from freqtrade.misc import plural
from freqtrade.plugins.pairlist.IPairList import IPairList
2020-11-21 14:29:11 +00:00
logger = logging.getLogger(__name__)
class RangeStabilityFilter(IPairList):
2020-11-21 14:29:11 +00:00
def __init__(self, exchange, pairlistmanager,
2022-09-18 11:31:52 +00:00
config: Config, pairlistconfig: Dict[str, Any],
2020-11-21 14:29:11 +00:00
pairlist_pos: int) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._days = pairlistconfig.get('lookback_days', 10)
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
self._max_rate_of_change = pairlistconfig.get('max_rate_of_change')
2020-11-21 14:29:11 +00:00
self._refresh_period = pairlistconfig.get('refresh_period', 1440)
2021-12-08 13:35:15 +00:00
self._def_candletype = self._config['candle_type_def']
2020-11-21 14:29:11 +00:00
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
2020-11-21 14:29:11 +00:00
2022-05-14 11:27:36 +00:00
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
2020-11-21 14:29:11 +00:00
if self._days < 1:
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
2022-05-14 11:27:36 +00:00
if self._days > candle_limit:
raise OperationalException("RangeStabilityFilter requires lookback_days to not "
2022-05-14 11:27:36 +00:00
f"exceed exchange max request size ({candle_limit})")
2020-11-21 14:29:11 +00:00
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty List is passed
as tickers argument to filter_pairlist
"""
return False
2020-11-21 14:29:11 +00:00
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
"""
max_rate_desc = ""
if 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 "
f"{self._min_rate_of_change}{max_rate_desc} over the "
f"last {plural(self._days, 'day')}.")
2020-11-21 14:29:11 +00:00
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
"""
Validate trading range
:param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: new allowlist
"""
2021-12-03 14:08:00 +00:00
needed_pairs: ListPairsWithTimeframes = [
2021-12-08 13:35:15 +00:00
(p, '1d', self._def_candletype) for p in pairlist if p not in self._pair_cache]
2021-07-05 08:39:22 +00:00
since_ms = (arrow.utcnow()
.floor('day')
.shift(days=-self._days - 1)
.int_timestamp) * 1000
# Get all candles
candles = {}
if needed_pairs:
candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms,
cache=False)
if self._enabled:
for p in deepcopy(pairlist):
2021-12-08 13:35:15 +00:00
daily_candles = candles[(p, '1d', self._def_candletype)] if (
p, '1d', self._def_candletype) in candles else None
if not self._validate_pair_loc(p, daily_candles):
pairlist.remove(p)
return pairlist
2020-12-15 19:59:58 +00:00
def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool:
2020-11-21 14:29:11 +00:00
"""
Validate trading range
:param pair: Pair that's currently validated
2022-03-13 13:57:32 +00:00
:param daily_candles: Downloaded daily candles
:return: True if the pair can stay, false if it should be removed
2020-11-21 14:29:11 +00:00
"""
# Check symbol in cache
cached_res = self._pair_cache.get(pair, None)
if cached_res is not None:
return cached_res
2020-11-21 14:29:11 +00:00
result = True
if daily_candles is not None and not daily_candles.empty:
2020-11-21 14:29:11 +00:00
highest_high = daily_candles['high'].max()
lowest_low = daily_candles['low'].min()
2020-11-22 14:50:44 +00:00
pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0
if pct_change < self._min_rate_of_change:
2020-11-22 10:49:41 +00:00
self.log_once(f"Removed {pair} from whitelist, because rate of change "
f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, "
2020-11-22 10:49:41 +00:00
f"which is below the threshold of {self._min_rate_of_change}.",
logger.info)
2020-11-21 14:29:11 +00:00
result = False
2021-08-02 02:49:49 +00:00
if self._max_rate_of_change:
if pct_change > self._max_rate_of_change:
2021-08-02 02:49:49 +00:00
self.log_once(
f"Removed {pair} from whitelist, because rate of change "
f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, "
f"which is above the threshold of {self._max_rate_of_change}.",
logger.info)
result = False
2020-11-21 14:29:11 +00:00
self._pair_cache[pair] = result
2021-08-15 17:28:36 +00:00
else:
self.log_once(f"Removed {pair} from whitelist, no candles found.", logger.info)
2020-11-21 14:29:11 +00:00
return result