Merge pull request #5228 from kevinjulian/agefilter-max-days-listed
feat(agefilter): add max_days_listed
This commit is contained in:
		| @@ -81,13 +81,13 @@ Filtering instances (not the first position in the list) will not apply any cach | |||||||
|  |  | ||||||
| #### AgeFilter | #### AgeFilter | ||||||
|  |  | ||||||
| Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`). | Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity). | ||||||
|  |  | ||||||
| When pairs are first listed on an exchange they can suffer huge price drops and volatility | When pairs are first listed on an exchange they can suffer huge price drops and volatility | ||||||
| in the first few days while the pair goes through its price-discovery period. Bots can often | in the first few days while the pair goes through its price-discovery period. Bots can often | ||||||
| be caught out buying before the pair has finished dropping in price. | be caught out buying before the pair has finished dropping in price. | ||||||
|  |  | ||||||
| This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days. | This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days and listed before `max_days_listed`. | ||||||
|  |  | ||||||
| #### PerformanceFilter | #### PerformanceFilter | ||||||
|  |  | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ class AgeFilter(IPairList): | |||||||
|         super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) |         super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) | ||||||
|  |  | ||||||
|         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', None) | ||||||
|  |  | ||||||
|         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") | ||||||
| @@ -34,6 +35,12 @@ class AgeFilter(IPairList): | |||||||
|             raise OperationalException("AgeFilter requires min_days_listed to not exceed " |             raise OperationalException("AgeFilter requires min_days_listed to not exceed " | ||||||
|                                        "exchange max request size " |                                        "exchange max request size " | ||||||
|                                        f"({exchange.ohlcv_candle_limit('1d')})") |                                        f"({exchange.ohlcv_candle_limit('1d')})") | ||||||
|  |         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") | ||||||
|  |         if self._max_days_listed and self._max_days_listed > exchange.ohlcv_candle_limit('1d'): | ||||||
|  |             raise OperationalException("AgeFilter requires max_days_listed to not exceed " | ||||||
|  |                                        "exchange max request size " | ||||||
|  |                                        f"({exchange.ohlcv_candle_limit('1d')})") | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def needstickers(self) -> bool: |     def needstickers(self) -> bool: | ||||||
| @@ -48,8 +55,13 @@ class AgeFilter(IPairList): | |||||||
|         """ |         """ | ||||||
|         Short whitelist method description - used for startup-messages |         Short whitelist method description - used for startup-messages | ||||||
|         """ |         """ | ||||||
|         return (f"{self.name} - Filtering pairs with age less than " |         return ( | ||||||
|                 f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.") |             f"{self.name} - Filtering pairs with age less than " | ||||||
|  |             f"{self._min_days_listed} {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 '' | ||||||
|  |  | ||||||
|     def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: |     def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: | ||||||
|         """ |         """ | ||||||
| @@ -61,10 +73,13 @@ class AgeFilter(IPairList): | |||||||
|         if not needed_pairs: |         if not needed_pairs: | ||||||
|             return pairlist |             return pairlist | ||||||
|  |  | ||||||
|         since_ms = (arrow.utcnow() |         since_days = -( | ||||||
|  |             self._max_days_listed if self._max_days_listed else self._min_days_listed | ||||||
|  |         ) - 1 | ||||||
|  |         since_ms = int(arrow.utcnow() | ||||||
|                        .floor('day') |                        .floor('day') | ||||||
|                          .shift(days=-self._min_days_listed - 1) |                        .shift(days=since_days) | ||||||
|                          .int_timestamp) * 1000 |                        .float_timestamp) * 1000 | ||||||
|         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): | ||||||
| @@ -86,14 +101,22 @@ class AgeFilter(IPairList): | |||||||
|             return True |             return True | ||||||
|  |  | ||||||
|         if daily_candles is not None: |         if daily_candles is not None: | ||||||
|             if len(daily_candles) >= self._min_days_listed: |             if ( | ||||||
|  |                 len(daily_candles) >= self._min_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] = arrow.utcnow().int_timestamp * 1000 |                 self._symbolsChecked[pair] = arrow.utcnow().int_timestamp * 1000 | ||||||
|                 return True |                 return True | ||||||
|             else: |             else: | ||||||
|                 self.log_once(f"Removed {pair} from whitelist, because age " |                 self.log_once(( | ||||||
|  |                     f"Removed {pair} from whitelist, because age " | ||||||
|                     f"{len(daily_candles)} is less than {self._min_days_listed} " |                     f"{len(daily_candles)} is less than {self._min_days_listed} " | ||||||
|                               f"{plural(self._min_days_listed, 'day')}", logger.info) |                     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) | ||||||
|                 return False |                 return False | ||||||
|         return False |         return False | ||||||
|   | |||||||
| @@ -79,7 +79,8 @@ def whitelist_conf_agefilter(default_conf): | |||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "method": "AgeFilter", |             "method": "AgeFilter", | ||||||
|             "min_days_listed": 2 |             "min_days_listed": 2, | ||||||
|  |             "max_days_listed": 100 | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|     return default_conf |     return default_conf | ||||||
| @@ -302,7 +303,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): | |||||||
|     # No pair for ETH, all handlers |     # No pair for ETH, all handlers | ||||||
|     ([{"method": "StaticPairList"}, |     ([{"method": "StaticPairList"}, | ||||||
|       {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, |       {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, | ||||||
|       {"method": "AgeFilter", "min_days_listed": 2}, |       {"method": "AgeFilter", "min_days_listed": 2, "max_days_listed": None}, | ||||||
|       {"method": "PrecisionFilter"}, |       {"method": "PrecisionFilter"}, | ||||||
|       {"method": "PriceFilter", "low_price_ratio": 0.03}, |       {"method": "PriceFilter", "low_price_ratio": 0.03}, | ||||||
|       {"method": "SpreadFilter", "max_spread_ratio": 0.005}, |       {"method": "SpreadFilter", "max_spread_ratio": 0.005}, | ||||||
| @@ -310,12 +311,24 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): | |||||||
|      "ETH", []), |      "ETH", []), | ||||||
|     # AgeFilter and VolumePairList (require 2 days only, all should pass age test) |     # AgeFilter and VolumePairList (require 2 days only, all should pass age test) | ||||||
|     ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, |     ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, | ||||||
|       {"method": "AgeFilter", "min_days_listed": 2}], |       {"method": "AgeFilter", "min_days_listed": 2, "max_days_listed": 100}], | ||||||
|      "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']), |      "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']), | ||||||
|     # AgeFilter and VolumePairList (require 10 days, all should fail age test) |     # AgeFilter and VolumePairList (require 10 days, all should fail age test) | ||||||
|     ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, |     ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, | ||||||
|       {"method": "AgeFilter", "min_days_listed": 10}], |       {"method": "AgeFilter", "min_days_listed": 10, "max_days_listed": None}], | ||||||
|      "BTC", []), |      "BTC", []), | ||||||
|  |     # AgeFilter and VolumePairList (all pair listed > 2, all should fail age test) | ||||||
|  |     ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, | ||||||
|  |       {"method": "AgeFilter", "min_days_listed": 1, "max_days_listed": 2}], | ||||||
|  |      "BTC", []), | ||||||
|  |     # AgeFilter and VolumePairList LTC/BTC has 6 candles - removes all | ||||||
|  |     ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, | ||||||
|  |       {"method": "AgeFilter", "min_days_listed": 4, "max_days_listed": 5}], | ||||||
|  |      "BTC", []), | ||||||
|  |     # AgeFilter and VolumePairList LTC/BTC has 6 candles - passes | ||||||
|  |     ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, | ||||||
|  |       {"method": "AgeFilter", "min_days_listed": 4, "max_days_listed": 10}], | ||||||
|  |      "BTC", ["LTC/BTC"]), | ||||||
|     # Precisionfilter and quote volume |     # Precisionfilter and quote volume | ||||||
|     ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, |     ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, | ||||||
|       {"method": "PrecisionFilter"}], |       {"method": "PrecisionFilter"}], | ||||||
| @@ -431,7 +444,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t | |||||||
|     ohlcv_data = { |     ohlcv_data = { | ||||||
|         ('ETH/BTC', '1d'): ohlcv_history, |         ('ETH/BTC', '1d'): ohlcv_history, | ||||||
|         ('TKN/BTC', '1d'): ohlcv_history, |         ('TKN/BTC', '1d'): ohlcv_history, | ||||||
|         ('LTC/BTC', '1d'): ohlcv_history, |         ('LTC/BTC', '1d'): ohlcv_history.append(ohlcv_history), | ||||||
|         ('XRP/BTC', '1d'): ohlcv_history, |         ('XRP/BTC', '1d'): ohlcv_history, | ||||||
|         ('HOT/BTC', '1d'): ohlcv_history_high_vola, |         ('HOT/BTC', '1d'): ohlcv_history_high_vola, | ||||||
|     } |     } | ||||||
| @@ -480,9 +493,13 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t | |||||||
|  |  | ||||||
|         for pairlist in pairlists: |         for pairlist in pairlists: | ||||||
|             if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \ |             if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \ | ||||||
|                     len(ohlcv_history) <= pairlist['min_days_listed']: |                     len(ohlcv_history) < pairlist['min_days_listed']: | ||||||
|                 assert log_has_re(r'^Removed .* from whitelist, because age .* is less than ' |                 assert log_has_re(r'^Removed .* from whitelist, because age .* is less than ' | ||||||
|                                   r'.* day.*', caplog) |                                   r'.* day.*', caplog) | ||||||
|  |             if pairlist['method'] == 'AgeFilter' and pairlist['max_days_listed'] and \ | ||||||
|  |                     len(ohlcv_history) > pairlist['max_days_listed']: | ||||||
|  |                 assert log_has_re(r'^Removed .* from whitelist, because age .* is less than ' | ||||||
|  |                                   r'.* day.* or more than .* day', caplog) | ||||||
|             if pairlist['method'] == 'PrecisionFilter' and whitelist_result: |             if pairlist['method'] == 'PrecisionFilter' and whitelist_result: | ||||||
|                 assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' |                 assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' | ||||||
|                                   r'would be <= stop limit.*', caplog) |                                   r'would be <= stop limit.*', caplog) | ||||||
| @@ -650,6 +667,22 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick | |||||||
|         get_patched_freqtradebot(mocker, default_conf) |         get_patched_freqtradebot(mocker, default_conf) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_agefilter_max_days_lower_than_min_days(mocker, default_conf, markets, tickers): | ||||||
|  |     default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, | ||||||
|  |                                  {'method': 'AgeFilter', 'min_days_listed': 3, | ||||||
|  |                                  "max_days_listed": 2}] | ||||||
|  |  | ||||||
|  |     mocker.patch.multiple('freqtrade.exchange.Exchange', | ||||||
|  |                           markets=PropertyMock(return_value=markets), | ||||||
|  |                           exchange_has=MagicMock(return_value=True), | ||||||
|  |                           get_tickers=tickers | ||||||
|  |                           ) | ||||||
|  |  | ||||||
|  |     with pytest.raises(OperationalException, | ||||||
|  |                        match=r'AgeFilter max_days_listed <= min_days_listed not permitted'): | ||||||
|  |         get_patched_freqtradebot(mocker, default_conf) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers): | def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers): | ||||||
|     default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, |     default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, | ||||||
|                                  {'method': 'AgeFilter', 'min_days_listed': 99999}] |                                  {'method': 'AgeFilter', 'min_days_listed': 99999}] | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user