Merge pull request #4953 from freqtrade/value_filter

max-value change filter
This commit is contained in:
Matthias 2021-05-20 06:35:34 +02:00 committed by GitHub
commit 586f2a699d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 56 additions and 5 deletions

View File

@ -112,6 +112,7 @@ The `PriceFilter` allows filtering of pairs by price. Currently the following pr
* `min_price` * `min_price`
* `max_price` * `max_price`
* `max_value`
* `low_price_ratio` * `low_price_ratio`
The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs. The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs.
@ -120,6 +121,11 @@ This option is disabled by default, and will only apply if set to > 0.
The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs. The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs.
This option is disabled by default, and will only apply if set to > 0. This option is disabled by default, and will only apply if set to > 0.
The `max_value` setting removes pairs where the minimum value change is above a specified value.
This is useful when an exchange has unbalanced limits. For example, if step-size = 1 (so you can only buy 1, or 2, or 3, but not 1.1 Coins) - and the price is pretty high (like 20$) as the coin has risen sharply since the last limit adaption.
As a result of the above, you can only buy for 20$, or 40$ - but not for 25$.
On exchanges that deduct fees from the receiving currency (e.g. FTX) - this can result in high value coins / amounts that are unsellable as the amount is slightly below the limit.
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
This option is disabled by default, and will only apply if set to > 0. This option is disabled by default, and will only apply if set to > 0.

View File

@ -7,7 +7,7 @@ from copy import deepcopy
from typing import Any, Dict, List from typing import Any, Dict, List
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import market_is_active from freqtrade.exchange import Exchange, market_is_active
from freqtrade.mixins import LoggingMixin from freqtrade.mixins import LoggingMixin
@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
class IPairList(LoggingMixin, ABC): class IPairList(LoggingMixin, ABC):
def __init__(self, exchange, pairlistmanager, def __init__(self, exchange: Exchange, pairlistmanager,
config: Dict[str, Any], pairlistconfig: Dict[str, Any], config: Dict[str, Any], pairlistconfig: Dict[str, Any],
pairlist_pos: int) -> None: pairlist_pos: int) -> None:
""" """
@ -28,7 +28,7 @@ class IPairList(LoggingMixin, ABC):
""" """
self._enabled = True self._enabled = True
self._exchange = exchange self._exchange: Exchange = exchange
self._pairlistmanager = pairlistmanager self._pairlistmanager = pairlistmanager
self._config = config self._config = config
self._pairlistconfig = pairlistconfig self._pairlistconfig = pairlistconfig

View File

@ -27,9 +27,13 @@ class PriceFilter(IPairList):
self._max_price = pairlistconfig.get('max_price', 0) self._max_price = pairlistconfig.get('max_price', 0)
if self._max_price < 0: if self._max_price < 0:
raise OperationalException("PriceFilter requires max_price to be >= 0") raise OperationalException("PriceFilter requires max_price to be >= 0")
self._max_value = pairlistconfig.get('max_value', 0)
if self._max_value < 0:
raise OperationalException("PriceFilter requires max_value to be >= 0")
self._enabled = ((self._low_price_ratio > 0) or self._enabled = ((self._low_price_ratio > 0) or
(self._min_price > 0) or (self._min_price > 0) or
(self._max_price > 0)) (self._max_price > 0) or
(self._max_value > 0))
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@ -51,6 +55,8 @@ class PriceFilter(IPairList):
active_price_filters.append(f"below {self._min_price:.8f}") active_price_filters.append(f"below {self._min_price:.8f}")
if self._max_price != 0: if self._max_price != 0:
active_price_filters.append(f"above {self._max_price:.8f}") active_price_filters.append(f"above {self._max_price:.8f}")
if self._max_value != 0:
active_price_filters.append(f"Value above {self._max_value:.8f}")
if len(active_price_filters): if len(active_price_filters):
return f"{self.name} - Filtering pairs priced {' or '.join(active_price_filters)}." return f"{self.name} - Filtering pairs priced {' or '.join(active_price_filters)}."
@ -79,6 +85,32 @@ class PriceFilter(IPairList):
f"because 1 unit is {changeperc * 100:.3f}%", logger.info) f"because 1 unit is {changeperc * 100:.3f}%", logger.info)
return False return False
# Perform low_amount check
if self._max_value != 0:
price = ticker['last']
market = self._exchange.markets[pair]
limits = market['limits']
if ('amount' in limits and 'min' in limits['amount']
and limits['amount']['min'] is not None):
min_amount = limits['amount']['min']
min_precision = market['precision']['amount']
min_value = min_amount * price
if self._exchange.precisionMode == 4:
# tick size
next_value = (min_amount + min_precision) * price
else:
# Decimal places
min_precision = pow(0.1, min_precision)
next_value = (min_amount + min_precision) * price
diff = next_value - min_value
if diff > self._max_value:
self.log_once(f"Removed {pair} from whitelist, "
f"because min value change of {diff} > {self._max_value}) ",
logger.info)
return False
# Perform min_price check. # Perform min_price check.
if self._min_price != 0: if self._min_price != 0:
if ticker['last'] < self._min_price: if ticker['last'] < self._min_price:
@ -89,7 +121,7 @@ class PriceFilter(IPairList):
# Perform max_price check. # Perform max_price check.
if self._max_price != 0: if self._max_price != 0:
if ticker['last'] > self._max_price: if ticker['last'] > self._max_price:
self.log_once(f"Removed {ticker['symbol']} from whitelist, " self.log_once(f"Removed {pair} from whitelist, "
f"because last price > {self._max_price:.8f}", logger.info) f"because last price > {self._max_price:.8f}", logger.info)
return False return False

View File

@ -407,6 +407,9 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "PriceFilter", "low_price_ratio": 0.02}], {"method": "PriceFilter", "low_price_ratio": 0.02}],
"USDT", ['ETH/USDT', 'NANO/USDT']), "USDT", ['ETH/USDT', 'NANO/USDT']),
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "PriceFilter", "max_value": 0.000001}],
"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, "refresh_period": 1440}],
@ -489,6 +492,8 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
r'because last price < .*%$', caplog) or r'because last price < .*%$', caplog) or
log_has_re(r'^Removed .* from whitelist, ' log_has_re(r'^Removed .* from whitelist, '
r'because last price > .*%$', caplog) or r'because last price > .*%$', caplog) or
log_has_re(r'^Removed .* from whitelist, '
r'because min value change of .*', caplog) or
log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] " log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] "
r"is empty.*", caplog)) r"is empty.*", caplog))
if pairlist['method'] == 'VolumePairList': if pairlist['method'] == 'VolumePairList':
@ -800,6 +805,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo
"[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]", "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]",
None None
), ),
({"method": "PriceFilter", "max_value": 0.00002000},
"[{'PriceFilter': 'PriceFilter - Filtering pairs priced Value above 0.00002000.'}]",
None
),
({"method": "PriceFilter"}, ({"method": "PriceFilter"},
"[{'PriceFilter': 'PriceFilter - No price filters configured.'}]", "[{'PriceFilter': 'PriceFilter - No price filters configured.'}]",
None None
@ -816,6 +825,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo
None, None,
"PriceFilter requires max_price to be >= 0" "PriceFilter requires max_price to be >= 0"
), # OperationalException expected ), # OperationalException expected
({"method": "PriceFilter", "max_value": -1.00010000},
None,
"PriceFilter requires max_value to be >= 0"
), # OperationalException expected
({"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01}, ({"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01},
"[{'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 over the last days.'}]",