remove second filter, add max option
This commit is contained in:
		| @@ -155,10 +155,10 @@ If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio | |||||||
|  |  | ||||||
| #### RangeStabilityFilter | #### RangeStabilityFilter | ||||||
|  |  | ||||||
| Removes pairs where the difference between lowest low and highest high over `lookback_days` days is below `min_rate_of_change`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. | Removes pairs where the difference between lowest low and highest high over `lookback_days` days is below `min_rate_of_change` or above `max_rate_of_change`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. | ||||||
|  |  | ||||||
| In the below example: | In the below example: | ||||||
| If the trading range over the last 10 days is <1%, remove the pair from the whitelist. | If the trading range over the last 10 days is <1% or >99%, remove the pair from the whitelist. | ||||||
|  |  | ||||||
| ```json | ```json | ||||||
| "pairlists": [ | "pairlists": [ | ||||||
| @@ -166,6 +166,7 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit | |||||||
|         "method": "RangeStabilityFilter", |         "method": "RangeStabilityFilter", | ||||||
|         "lookback_days": 10, |         "lookback_days": 10, | ||||||
|         "min_rate_of_change": 0.01, |         "min_rate_of_change": 0.01, | ||||||
|  |         "max_rate_of_change": 0.99, | ||||||
|         "refresh_period": 1440 |         "refresh_period": 1440 | ||||||
|     } |     } | ||||||
| ] | ] | ||||||
| @@ -173,24 +174,7 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit | |||||||
|  |  | ||||||
| !!! Tip | !!! Tip | ||||||
|     This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit. |     This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit. | ||||||
|  |     Additionally, it can also be used to automatically remove pairs with extreme high/low variance over a given amount of time. | ||||||
| #### RangeStabilityFilterMax |  | ||||||
|  |  | ||||||
| Same function as `RangeStabilityFilter` but instead of a minimum value, it uses a maximum value for rate of change, i.e. `max_rate_of_change` as seen in the example below. |  | ||||||
|  |  | ||||||
| ```json |  | ||||||
| "pairlists": [ |  | ||||||
|     { |  | ||||||
|         "method": "RangeStabilityFilterMax", |  | ||||||
|         "lookback_days": 10, |  | ||||||
|         "max_rate_of_change": 1.01, |  | ||||||
|         "refresh_period": 1440 |  | ||||||
|     } |  | ||||||
| ] |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| !!! Tip |  | ||||||
|     This Filter can be used to automatically remove pairs with extreme high/low variance over a given amount of time (`lookback_days`). |  | ||||||
|  |  | ||||||
| #### VolatilityFilter | #### VolatilityFilter | ||||||
|  |  | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', | |||||||
| AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', | AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', | ||||||
|                        'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', |                        'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', | ||||||
|                        'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', |                        'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', | ||||||
|                        'SpreadFilter', 'VolatilityFilter', 'RangeStabilityFilterMax'] |                        'SpreadFilter', 'VolatilityFilter'] | ||||||
| AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] | AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] | ||||||
| AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] | AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] | ||||||
| DRY_RUN_WALLET = 1000 | DRY_RUN_WALLET = 1000 | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ class RangeStabilityFilter(IPairList): | |||||||
|  |  | ||||||
|         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', 0.99) | ||||||
|         self._refresh_period = pairlistconfig.get('refresh_period', 1440) |         self._refresh_period = pairlistconfig.get('refresh_period', 1440) | ||||||
|  |  | ||||||
|         self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) |         self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) | ||||||
| @@ -51,7 +52,8 @@ class RangeStabilityFilter(IPairList): | |||||||
|         Short whitelist method description - used for startup-messages |         Short whitelist method description - used for startup-messages | ||||||
|         """ |         """ | ||||||
|         return (f"{self.name} - Filtering pairs with rate of change below " |         return (f"{self.name} - Filtering pairs with rate of change below " | ||||||
|                 f"{self._min_rate_of_change} over the last {plural(self._days, 'day')}.") |                 f"{self._min_rate_of_change} and above " | ||||||
|  |                 f"{self._max_rate_of_change} over the last {plural(self._days, 'day')}.") | ||||||
|  |  | ||||||
|     def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: |     def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: | ||||||
|         """ |         """ | ||||||
| @@ -104,6 +106,14 @@ class RangeStabilityFilter(IPairList): | |||||||
|                               f"which is below the threshold of {self._min_rate_of_change}.", |                               f"which is below the threshold of {self._min_rate_of_change}.", | ||||||
|                               logger.info) |                               logger.info) | ||||||
|                 result = False |                 result = False | ||||||
|  |             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 | ||||||
|             self._pair_cache[pair] = result |             self._pair_cache[pair] = result | ||||||
|  |  | ||||||
|         return result |         return result | ||||||
|   | |||||||
| @@ -1,109 +0,0 @@ | |||||||
| """ |  | ||||||
| Rate of change pairlist filter |  | ||||||
| """ |  | ||||||
| import logging |  | ||||||
| from copy import deepcopy |  | ||||||
| from typing import Any, Dict, List, Optional |  | ||||||
|  |  | ||||||
| import arrow |  | ||||||
| from cachetools.ttl import TTLCache |  | ||||||
| from pandas import DataFrame |  | ||||||
|  |  | ||||||
| from freqtrade.exceptions import OperationalException |  | ||||||
| from freqtrade.misc import plural |  | ||||||
| from freqtrade.plugins.pairlist.IPairList import IPairList |  | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class RangeStabilityFilterMax(IPairList): |  | ||||||
|  |  | ||||||
|     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) |  | ||||||
|  |  | ||||||
|         self._days = pairlistconfig.get('lookback_days', 10) |  | ||||||
|         self._max_rate_of_change = pairlistconfig.get('max_rate_of_change', 0.02) |  | ||||||
|         self._refresh_period = pairlistconfig.get('refresh_period', 1440) |  | ||||||
|  |  | ||||||
|         self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) |  | ||||||
|  |  | ||||||
|         if self._days < 1: |  | ||||||
|             raise OperationalException("RangeStabilityFilterMax requires lookback_days to be >= 1") |  | ||||||
|         if self._days > exchange.ohlcv_candle_limit('1d'): |  | ||||||
|             raise OperationalException("RangeStabilityFilterMax requires lookback_days to not " |  | ||||||
|                                        "exceed exchange max request size " |  | ||||||
|                                        f"({exchange.ohlcv_candle_limit('1d')})") |  | ||||||
|  |  | ||||||
|     @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 |  | ||||||
|  |  | ||||||
|     def short_desc(self) -> str: |  | ||||||
|         """ |  | ||||||
|         Short whitelist method description - used for startup-messages |  | ||||||
|         """ |  | ||||||
|         return (f"{self.name} - Filtering pairs with rate of change below " |  | ||||||
|                 f"{self._max_rate_of_change} over the last {plural(self._days, 'day')}.") |  | ||||||
|  |  | ||||||
|     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 |  | ||||||
|         """ |  | ||||||
|         needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache] |  | ||||||
|  |  | ||||||
|         since_ms = int(arrow.utcnow() |  | ||||||
|                        .floor('day') |  | ||||||
|                        .shift(days=-self._days - 1) |  | ||||||
|                        .float_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): |  | ||||||
|                 daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None |  | ||||||
|                 if not self._validate_pair_loc(p, daily_candles): |  | ||||||
|                     pairlist.remove(p) |  | ||||||
|         return pairlist |  | ||||||
|  |  | ||||||
|     def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: |  | ||||||
|         """ |  | ||||||
|         Validate trading range |  | ||||||
|         :param pair: Pair that's currently validated |  | ||||||
|         :param ticker: ticker dict as returned from ccxt.load_markets() |  | ||||||
|         :return: True if the pair can stay, false if it should be removed |  | ||||||
|         """ |  | ||||||
|         # Check symbol in cache |  | ||||||
|         cached_res = self._pair_cache.get(pair, None) |  | ||||||
|         if cached_res is not None: |  | ||||||
|             return cached_res |  | ||||||
|  |  | ||||||
|         result = False |  | ||||||
|         if daily_candles is not None and not daily_candles.empty: |  | ||||||
|             highest_high = daily_candles['high'].max() |  | ||||||
|             lowest_low = daily_candles['low'].min() |  | ||||||
|             pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0 |  | ||||||
|             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 |  | ||||||
|             self._pair_cache[pair] = result |  | ||||||
|  |  | ||||||
|         return result |  | ||||||
| @@ -412,7 +412,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): | |||||||
|         "USDT", ['NANO/USDT']), |         "USDT", ['NANO/USDT']), | ||||||
|     ([{"method": "StaticPairList"}, |     ([{"method": "StaticPairList"}, | ||||||
|       {"method": "RangeStabilityFilter", "lookback_days": 10, |       {"method": "RangeStabilityFilter", "lookback_days": 10, | ||||||
|        "min_rate_of_change": 0.01, "refresh_period": 1440}], |        "min_rate_of_change": 0.01, "max_rate_of_change": 0.99, "refresh_period": 1440}], | ||||||
|      "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), |      "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), | ||||||
|     ([{"method": "StaticPairList"}, |     ([{"method": "StaticPairList"}, | ||||||
|       {"method": "VolatilityFilter", "lookback_days": 3, |       {"method": "VolatilityFilter", "lookback_days": 3, | ||||||
| @@ -718,15 +718,16 @@ def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): | |||||||
|         get_patched_freqtradebot(mocker, default_conf) |         get_patched_freqtradebot(mocker, default_conf) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize('min_rate_of_change,expected_length', [ | @pytest.mark.parametrize('min_rate_of_change,max_rate_of_change,expected_length', [ | ||||||
|     (0.01, 5), |     (0.01, 0.99, 5), | ||||||
|     (0.05, 0),  # Setting rate_of_change to 5% removes all pairs from the whitelist. |     (0.05, 0.0, 0),  # Setting min rate_of_change to 5% removes all pairs from the whitelist. | ||||||
| ]) | ]) | ||||||
| def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history, | def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history, | ||||||
|                                       min_rate_of_change, expected_length): |                                       min_rate_of_change, max_rate_of_change, expected_length): | ||||||
|     default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, |     default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, | ||||||
|                                  {'method': 'RangeStabilityFilter', 'lookback_days': 2, |                                  {'method': 'RangeStabilityFilter', 'lookback_days': 2, | ||||||
|                                   'min_rate_of_change': min_rate_of_change}] |                                   'min_rate_of_change': min_rate_of_change, | ||||||
|  |                                   "max_rate_of_change": max_rate_of_change}] | ||||||
|  |  | ||||||
|     mocker.patch.multiple('freqtrade.exchange.Exchange', |     mocker.patch.multiple('freqtrade.exchange.Exchange', | ||||||
|                           markets=PropertyMock(return_value=markets), |                           markets=PropertyMock(return_value=markets), | ||||||
| @@ -828,9 +829,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo | |||||||
|      None, |      None, | ||||||
|      "PriceFilter requires max_value to be >= 0" |      "PriceFilter requires max_value to be >= 0" | ||||||
|      ),  # OperationalException expected |      ),  # OperationalException expected | ||||||
|     ({"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01}, |     ({"method": "RangeStabilityFilter", "lookback_days": 10, | ||||||
|  |      "min_rate_of_change": 0.01, "max_rate_of_change": 0.99}, | ||||||
|      "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate of change below " |      "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate of change below " | ||||||
|      "0.01 over the last days.'}]", |      "0.01 and above 0.99 over the last days.'}]", | ||||||
|         None |         None | ||||||
|      ), |      ), | ||||||
| ]) | ]) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user