From aa54fd2251f6144a6c76df33585fa59b4b4886c4 Mon Sep 17 00:00:00 2001 From: untoreh Date: Mon, 3 Feb 2020 07:44:17 +0100 Subject: [PATCH] - added spread filter - minimum value to volume pairlist --- config_full.json.example | 4 +- docs/configuration.md | 6 +++ freqtrade/constants.py | 3 +- freqtrade/pairlist/SpreadFilter.py | 59 ++++++++++++++++++++++++++++ freqtrade/pairlist/VolumePairList.py | 8 +++- tests/conftest.py | 47 ++++++++++++++++++++++ tests/pairlist/test_pairlist.py | 10 ++++- 7 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 freqtrade/pairlist/SpreadFilter.py diff --git a/config_full.json.example b/config_full.json.example index 82d8bd04a..9f09d2247 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -62,8 +62,8 @@ "refresh_period": 1800 }, {"method": "PrecisionFilter"}, - {"method": "PriceFilter", "low_price_ratio": 0.01 - } + {"method": "PriceFilter", "low_price_ratio": 0.01}, + {"method": "SpreadFilter", "max_spread_ratio": 0.005} ], "exchange": { "name": "bittrex", diff --git a/docs/configuration.md b/docs/configuration.md index fe692eacb..17b9a82c5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -503,6 +503,7 @@ Inactive markets and blacklisted pairs are always removed from the resulting `pa * [`VolumePairList`](#volume-pair-list) * [`PrecisionFilter`](#precision-filter) * [`PriceFilter`](#price-pair-filter) +* [`SpreadFilter`](#spread-filter) !!! Tip "Testing pairlists" Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) subcommand to test your configuration quickly. @@ -551,6 +552,11 @@ Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0. These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses. +#### Spread Filter +Removes pairs that have a difference between asks and bids above the specified ratio (default `0.005`). +Example: +If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027 the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` + ### Full Pairlist example The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting by `quoteVolume` and applies both [`PrecisionFilter`](#precision-filter) and [`PriceFilter`](#price-pair-filter), filtering all assets where 1 priceunit is > 1%. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 53bc4af53..56876e2c9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -17,7 +17,8 @@ REQUIRED_ORDERTIF = ['buy', 'sell'] REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] -AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'PriceFilter'] +AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', + 'PrecisionFilter', 'PriceFilter', 'SpreadFilter'] DRY_RUN_WALLET = 1000 MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/pairlist/SpreadFilter.py new file mode 100644 index 000000000..9361837cc --- /dev/null +++ b/freqtrade/pairlist/SpreadFilter.py @@ -0,0 +1,59 @@ +import logging +from copy import deepcopy +from typing import Dict, List + +from freqtrade.pairlist.IPairList import IPairList + +logger = logging.getLogger(__name__) + + +class SpreadFilter(IPairList): + + def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict, + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005) + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requries tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return True + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + """ + return (f"{self.name} - Filtering pairs with ask/bid diff above " + f"{self._max_spread_ratio * 100}%.") + + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + + """ + Filters and sorts pairlist and returns the whitelist again. + Called on each bot iteration - please use internal caching if necessary + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new whitelist + """ + # Copy list since we're modifying this list + + spread = None + for p in deepcopy(pairlist): + ticker = tickers.get(p) + assert ticker is not None + if 'bid' in ticker and 'ask' in ticker: + spread = 1 - ticker['bid'] / ticker['ask'] + if not ticker or spread > self._max_spread_ratio: + logger.info(f"Removed {ticker['symbol']} from whitelist, " + f"because spread {spread * 100:.3f}% >" + f"{self._max_spread_ratio * 100}%") + pairlist.remove(p) + else: + pairlist.remove(p) + + return pairlist diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 4ac9935ba..3f31f5523 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -28,6 +28,7 @@ class VolumePairList(IPairList): 'for "pairlist.config.number_assets"') self._number_pairs = self._pairlistconfig['number_assets'] self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume') + self._min_value = self._pairlistconfig.get('min_value', 0) self.refresh_period = self._pairlistconfig.get('refresh_period', 1800) if not self._exchange.exchange_has('fetchTickers'): @@ -73,11 +74,13 @@ class VolumePairList(IPairList): tickers, self._config['stake_currency'], self._sort_key, + self._min_value ) else: return pairlist - def _gen_pair_whitelist(self, pairlist, tickers, base_currency: str, key: str) -> List[str]: + def _gen_pair_whitelist(self, pairlist, tickers, base_currency: str, + key: str, min_val: int) -> List[str]: """ Updates the whitelist with with a dynamically generated list :param base_currency: base currency as str @@ -96,6 +99,9 @@ class VolumePairList(IPairList): # If other pairlist is in front, use the incomming pairlist. filtered_tickers = [v for k, v in tickers.items() if k in pairlist] + if min_val > 0: + filtered_tickers = list(filter(lambda t: t[key] > min_val, filtered_tickers)) + sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[key]) # Validate whitelist to only have active market pairs diff --git a/tests/conftest.py b/tests/conftest.py index 395388f73..e897dbccd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -640,6 +640,31 @@ def shitcoinmarkets(markets): }, 'info': {}, }, + 'NANO/USDT': { + "percentage": True, + "tierBased": False, + "taker": 0.001, + "maker": 0.001, + "precision": { + "base": 8, + "quote": 8, + "amount": 2, + "price": 4 + }, + "limits": { + }, + "id": "NANOUSDT", + "symbol": "NANO/USDT", + "base": "NANO", + "quote": "USDT", + "baseId": "NANO", + "quoteId": "USDT", + "info": {}, + "type": "spot", + "spot": True, + "future": False, + "active": True + }, }) return shitmarkets @@ -1114,6 +1139,28 @@ def tickers(): 'quoteVolume': 1154.19266394, 'info': {} }, + "NANO/USDT": { + "symbol": "NANO/USDT", + "timestamp": 1580469388244, + "datetime": "2020-01-31T11:16:28.244Z", + "high": 0.7519, + "low": 0.7154, + "bid": 0.7305, + "bidVolume": 300.3, + "ask": 0.7342, + "askVolume": 15.14, + "vwap": 0.73645591, + "open": 0.7154, + "close": 0.7342, + "last": 0.7342, + "previousClose": 0.7189, + "change": 0.0188, + "percentage": 2.628, + "average": None, + "baseVolume": 439472.44, + "quoteVolume": 323652.075405, + "info": {} + }, }) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index ac4cbc813..b8a4be037 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -141,7 +141,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}], "BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], - "USDT", ['ETH/USDT']), + "USDT", ['ETH/USDT', 'NANO/USDT']), # No pair for ETH ... ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], "ETH", []), @@ -160,6 +160,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.02} ], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), + # HOT and XRP are removed because below 1250 quoteVolume + ([{"method": "VolumePairList", "number_assets": 5, + "sort_key": "quoteVolume", "min_value": 1250}], + "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), # StaticPairlist Only ([{"method": "StaticPairList"}, ], "BTC", ['ETH/BTC', 'TKN/BTC']), @@ -167,6 +171,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "StaticPairList"}, {"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, ], "BTC", ['TKN/BTC', 'ETH/BTC']), + # SpreadFilter + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "SpreadFilter", "max_spread": 0.005} + ], "USDT", ['ETH/USDT']), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, pairlists, base_currency, whitelist_result,