Merge pull request #5228 from kevinjulian/agefilter-max-days-listed
feat(agefilter): add max_days_listed
This commit is contained in:
commit
62f2597f7a
@ -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 = -(
|
||||||
.floor('day')
|
self._max_days_listed if self._max_days_listed else self._min_days_listed
|
||||||
.shift(days=-self._min_days_listed - 1)
|
) - 1
|
||||||
.int_timestamp) * 1000
|
since_ms = int(arrow.utcnow()
|
||||||
|
.floor('day')
|
||||||
|
.shift(days=since_days)
|
||||||
|
.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"{len(daily_candles)} is less than {self._min_days_listed} "
|
f"Removed {pair} from whitelist, because age "
|
||||||
f"{plural(self._min_days_listed, 'day')}", logger.info)
|
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)
|
||||||
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}]
|
||||||
|
Loading…
Reference in New Issue
Block a user