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
|
2021-12-27 18:30:17 +00:00
|
|
|
from cachetools import TTLCache
|
2020-12-15 19:59:58 +00:00
|
|
|
from pandas import DataFrame
|
2020-11-21 14:29:11 +00:00
|
|
|
|
2021-12-03 14:08:00 +00:00
|
|
|
from freqtrade.constants import ListPairsWithTimeframes
|
2020-11-21 14:29:11 +00:00
|
|
|
from freqtrade.exceptions import OperationalException
|
|
|
|
from freqtrade.misc import plural
|
2020-12-23 15:54:35 +00:00
|
|
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
2020-11-21 14:29:11 +00:00
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2020-11-22 18:47:27 +00:00
|
|
|
class RangeStabilityFilter(IPairList):
|
2020-11-21 14:29:11 +00:00
|
|
|
|
|
|
|
def __init__(self, exchange, pairlistmanager,
|
|
|
|
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
|
|
|
pairlist_pos: int) -> None:
|
|
|
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
|
|
|
2020-11-22 18:47:27 +00:00
|
|
|
self._days = pairlistconfig.get('lookback_days', 10)
|
|
|
|
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
|
2021-08-02 02:49:49 +00:00
|
|
|
self._max_rate_of_change = pairlistconfig.get('max_rate_of_change', None)
|
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
|
|
|
|
2021-03-06 18:55:02 +00:00
|
|
|
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
|
2020-11-21 14:29:11 +00:00
|
|
|
|
|
|
|
if self._days < 1:
|
2020-11-22 18:47:27 +00:00
|
|
|
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
|
2021-02-14 09:29:45 +00:00
|
|
|
if self._days > exchange.ohlcv_candle_limit('1d'):
|
2020-11-22 18:47:27 +00:00
|
|
|
raise OperationalException("RangeStabilityFilter requires lookback_days to not "
|
2020-11-21 15:01:52 +00:00
|
|
|
"exceed exchange max request size "
|
2021-02-14 09:29:45 +00:00
|
|
|
f"({exchange.ohlcv_candle_limit('1d')})")
|
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
|
|
|
|
"""
|
2020-12-15 07:36:42 +00:00
|
|
|
return False
|
2020-11-21 14:29:11 +00:00
|
|
|
|
|
|
|
def short_desc(self) -> str:
|
|
|
|
"""
|
|
|
|
Short whitelist method description - used for startup-messages
|
|
|
|
"""
|
2021-08-02 05:15:09 +00:00
|
|
|
max_rate_desc = ""
|
|
|
|
if self._max_rate_of_change:
|
|
|
|
max_rate_desc = (f" and above {self._max_rate_of_change}")
|
2020-11-22 18:47:27 +00:00
|
|
|
return (f"{self.name} - Filtering pairs with rate of change below "
|
2021-08-02 05:15:09 +00:00
|
|
|
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
|
|
|
|
2020-12-15 19:38:26 +00:00
|
|
|
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> 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]
|
2020-12-15 19:38:26 +00:00
|
|
|
|
2021-07-05 08:39:22 +00:00
|
|
|
since_ms = (arrow.utcnow()
|
|
|
|
.floor('day')
|
|
|
|
.shift(days=-self._days - 1)
|
|
|
|
.int_timestamp) * 1000
|
2020-12-15 19:38:26 +00:00
|
|
|
# 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
|
2020-12-15 19:38:26 +00:00
|
|
|
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
|
|
|
"""
|
2020-11-22 18:47:27 +00:00
|
|
|
Validate trading range
|
2020-12-15 07:36:42 +00:00
|
|
|
:param pair: Pair that's currently validated
|
2022-03-13 13:57:32 +00:00
|
|
|
:param daily_candles: Downloaded daily candles
|
2020-12-15 07:36:42 +00:00
|
|
|
: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
|
2021-04-07 04:52:34 +00:00
|
|
|
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 = False
|
2020-11-24 19:05:06 +00:00
|
|
|
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
|
2020-11-22 18:47:27 +00:00
|
|
|
if pct_change >= self._min_rate_of_change:
|
2020-11-21 14:29:11 +00:00
|
|
|
result = True
|
|
|
|
else:
|
2020-11-22 10:49:41 +00:00
|
|
|
self.log_once(f"Removed {pair} from whitelist, because rate of change "
|
2020-12-15 19:38:26 +00:00
|
|
|
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:
|
|
|
|
result = True
|
|
|
|
else:
|
|
|
|
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
|