From 109824c9a80cb78c7c4ec9d6f90cb1c8c3afa640 Mon Sep 17 00:00:00 2001
From: Matthias <xmatthias@outlook.com>
Date: Sat, 21 Nov 2020 15:29:11 +0100
Subject: [PATCH] Add VolatilityFilter

---
 freqtrade/constants.py                 |  2 +-
 freqtrade/pairlist/AgeFilter.py        |  2 +-
 freqtrade/pairlist/volatilityfilter.py | 89 ++++++++++++++++++++++++++
 3 files changed, 91 insertions(+), 2 deletions(-)
 create mode 100644 freqtrade/pairlist/volatilityfilter.py

diff --git a/freqtrade/constants.py b/freqtrade/constants.py
index 3271dda39..55d802587 100644
--- a/freqtrade/constants.py
+++ b/freqtrade/constants.py
@@ -25,7 +25,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
                          'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily']
 AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
                        'AgeFilter', 'PrecisionFilter', 'PriceFilter',
-                       'ShuffleFilter', 'SpreadFilter']
+                       'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
 AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5']
 DRY_RUN_WALLET = 1000
 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S'
diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py
index 19cf1c090..352fff082 100644
--- a/freqtrade/pairlist/AgeFilter.py
+++ b/freqtrade/pairlist/AgeFilter.py
@@ -49,7 +49,7 @@ class AgeFilter(IPairList):
         return (f"{self.name} - Filtering pairs with age less than "
                 f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.")
 
-    def _validate_pair(self, ticker: dict) -> bool:
+    def _validate_pair(self, ticker: Dict) -> bool:
         """
         Validate age for the ticker
         :param ticker: ticker dict as returned from ccxt.load_markets()
diff --git a/freqtrade/pairlist/volatilityfilter.py b/freqtrade/pairlist/volatilityfilter.py
new file mode 100644
index 000000000..6039f6f69
--- /dev/null
+++ b/freqtrade/pairlist/volatilityfilter.py
@@ -0,0 +1,89 @@
+"""
+Minimum age (days listed) pair list filter
+"""
+import logging
+from typing import Any, Dict
+
+import arrow
+from cachetools.ttl import TTLCache
+
+from freqtrade.exceptions import OperationalException
+from freqtrade.misc import plural
+from freqtrade.pairlist.IPairList import IPairList
+
+
+logger = logging.getLogger(__name__)
+
+
+class VolatilityFilter(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('volatility_over_days', 10)
+        self._min_volatility = pairlistconfig.get('min_volatility', 0.01)
+        self._refresh_period = pairlistconfig.get('refresh_period', 1440)
+
+        self._pair_cache: TTLCache = TTLCache(maxsize=100, ttl=self._refresh_period)
+
+        if self._days < 1:
+            raise OperationalException("VolatilityFilter requires min_days_listed to be >= 1")
+        if self._days > exchange.ohlcv_candle_limit:
+            raise OperationalException("VolatilityFilter requires min_days_listed to not exceed "
+                                       "exchange max request size "
+                                       f"({exchange.ohlcv_candle_limit})")
+
+    @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 True
+
+    def short_desc(self) -> str:
+        """
+        Short whitelist method description - used for startup-messages
+        """
+        return (f"{self.name} - Filtering pairs with volatility below {self._min_volatility} "
+                f"over the last {plural(self._days, 'day')}.")
+
+    def _validate_pair(self, ticker: Dict) -> bool:
+        """
+        Validate volatility
+        :param ticker: ticker dict as returned from ccxt.load_markets()
+        :return: True if the pair can stay, False if it should be removed
+        """
+        pair = ticker['symbol']
+        # Check symbol in cache
+        if pair in self._pair_cache:
+            return self._pair_cache[pair]
+
+        since_ms = int(arrow.utcnow()
+                       .floor('day')
+                       .shift(days=-self._days)
+                       .float_timestamp) * 1000
+
+        daily_candles = self._exchange.get_historic_ohlcv_as_df(pair=pair,
+                                                                timeframe='1d',
+                                                                since_ms=since_ms)
+        result = False
+        if daily_candles is not None:
+            highest_high = daily_candles['high'].max()
+            lowest_low = daily_candles['low'].min()
+            pct_change = (highest_high - lowest_low) / lowest_low
+            if pct_change >= self._min_volatility:
+                result = True
+            else:
+                self.log_on_refresh(logger.info,
+                                    f"Removed {pair} from whitelist, "
+                                    f"because volatility over {plural(self._days, 'day')} is "
+                                    f"{pct_change:.3f}, which is below the "
+                                    f"threshold of {self._min_volatility}.")
+                result = False
+            self._pair_cache[pair] = result
+
+        return result