remove second filter, add max option
This commit is contained in:
parent
8b0a02db8e
commit
34c8a5afaf
@ -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
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
|
Loading…
Reference in New Issue
Block a user