Repository: freqtrade/freqtrade

Issue:
Temporary blacklist pair request
<!-- 
Note: this section will not show up in the issue.
Have you search for this feature before requesting it? It's highly likely that a similar request was already filed.
-->

## Describe your environment

  * Operating system: Ubuntu18/20
  * Python Version: Python 3.7.5 
  * CCXT version: 1.60.36 
  * Freqtrade Version:  develop-335412a3a 

## Describe the enhancement


Telegram command to temporarely blacklist a pair for a defined period. For example /tempblacklist AVA 5h
Sometimes with certain strategies one sees in the ui that a coin which was bought was pumped hours before the buy. One can manually /forcesell that pair if one is quick enough but many times some strategy keep buying it even after the /forcesell

A command to temporarely blacklist a coin for some defined time could be handy in those cases. 
The above fictive command could blacklist AVA/STAKE for 5 hours and after that auto whitelist it again. (where STAKE is taken from the config file.
One could do the same with minutes (m) or days (d) instead of editting the config and reloading it or temporarely stopping the bot.

*Or* ... one could split this feature request in two separate feature requests :
- the feature of having a dynamically temporary blacklist besides the blacklist in the config files, which can be populated by strategies and/or rpc commands (one could populate it with anti-pump functions "to-be" in strategies or other needs)
- the feature of populating or depopulating that dynamicaly blacklist by rpc command(s)





Discussion:
I'd not use blacklist for temporary locking (unless you want to unlock yourself) - but the locking mechanism (which has times builtin).
This can already happen from within the strategy - so to have somewhat reproducible results, you should consider implementing this logic in your strategy instead.


You can also populate blacklist with rpc methods already (`/blacklist <pair>` - what you can't do is remove pairs from that - but that would also fill part of what you need i guess.
Major thx ! 
I didn't know the main functionallity of dynamically blacklisting was allready in Freqtrade. Any chance on adding the 'temporary' part in Freqtrade so the unlocking of "temporary" blacklisted pairs can be auto-unlocked depending on a timer/timing value ?
as said - the "unlocking" is problematic as it's not implemented (you can however `/reload_conf` to get your original blacklist back).

Temporary locking (with time) will not be added as part of /blacklist for sure. As mentioned above - i can imagine it via some `/lock` mechanism (which already accepts time - but only from within the strategy).

Resolving patch:
diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py
--- a/freqtrade/rpc/api_server/api_v1.py
+++ b/freqtrade/rpc/api_server/api_v1.py
@@ -3,7 +3,7 @@
 from pathlib import Path
 from typing import List, Optional
 
-from fastapi import APIRouter, Depends
+from fastapi import APIRouter, Depends, Query
 from fastapi.exceptions import HTTPException
 
 from freqtrade import __version__
@@ -30,7 +30,8 @@
 # Pre-1.1, no version was provided
 # Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen.
 # 1.11: forcebuy and forcesell accept ordertype
-API_VERSION = 1.11
+# 1.12: add blacklist delete endpoint
+API_VERSION = 1.12
 
 # Public API, requires no auth.
 router_public = APIRouter()
@@ -157,6 +158,13 @@ def blacklist_post(payload: BlacklistPayload, rpc: RPC = Depends(get_rpc)):
     return rpc._rpc_blacklist(payload.blacklist)
 
 
+@router.delete('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
+def blacklist_delete(pairs_to_delete: List[str] = Query([]), rpc: RPC = Depends(get_rpc)):
+    """Provide a list of pairs to delete from the blacklist"""
+
+    return rpc._rpc_blacklist_delete(pairs_to_delete)
+
+
 @router.get('/whitelist', response_model=WhitelistResponse, tags=['info', 'pairlist'])
 def whitelist(rpc: RPC = Depends(get_rpc)):
     return rpc._rpc_whitelist()
diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -860,6 +860,20 @@ def _rpc_whitelist(self) -> Dict:
                }
         return res
 
+    def _rpc_blacklist_delete(self, delete: List[str]) -> Dict:
+        """ Removes pairs from currently active blacklist """
+        errors = {}
+        for pair in delete:
+            if pair in self._freqtrade.pairlists.blacklist:
+                self._freqtrade.pairlists.blacklist.remove(pair)
+            else:
+                errors[pair] = {
+                    'error_msg': f"Pair {pair} is not in the current blacklist."
+                    }
+        resp = self._rpc_blacklist()
+        resp['errors'] = errors
+        return resp
+
     def _rpc_blacklist(self, add: List[str] = None) -> Dict:
         """ Returns the currently active blacklist"""
         errors = {}
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -111,9 +111,9 @@ def _init_keyboard(self) -> None:
                                  r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+',
                                  r'/stats$', r'/count$', r'/locks$', r'/balance$',
                                  r'/stopbuy$', r'/reload_config$', r'/show_config$',
-                                 r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$',
+                                 r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$',
                                  r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
-                                 r'/forcebuy$', r'/help$', r'/version$']
+                                 r'/forcebuy$', r'/edge$', r'/help$', r'/version$']
         # Create keys for generation
         valid_keys_print = [k.replace('$', '') for k in valid_keys]
 
@@ -170,6 +170,7 @@ def _init(self) -> None:
             CommandHandler('stopbuy', self._stopbuy),
             CommandHandler('whitelist', self._whitelist),
             CommandHandler('blacklist', self._blacklist),
+            CommandHandler(['blacklist_delete', 'bl_delete'], self._blacklist_delete),
             CommandHandler('logs', self._logs),
             CommandHandler('edge', self._edge),
             CommandHandler('help', self._help),
@@ -1162,22 +1163,28 @@ def _blacklist(self, update: Update, context: CallbackContext) -> None:
         Handler for /blacklist
         Shows the currently active blacklist
         """
-        try:
+        self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args))
 
-            blacklist = self._rpc._rpc_blacklist(context.args)
-            errmsgs = []
-            for pair, error in blacklist['errors'].items():
-                errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`")
-            if errmsgs:
-                self._send_msg('\n'.join(errmsgs))
+    def send_blacklist_msg(self, blacklist: Dict):
+        errmsgs = []
+        for pair, error in blacklist['errors'].items():
+            errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`")
+        if errmsgs:
+            self._send_msg('\n'.join(errmsgs))
 
-            message = f"Blacklist contains {blacklist['length']} pairs\n"
-            message += f"`{', '.join(blacklist['blacklist'])}`"
+        message = f"Blacklist contains {blacklist['length']} pairs\n"
+        message += f"`{', '.join(blacklist['blacklist'])}`"
 
-            logger.debug(message)
-            self._send_msg(message)
-        except RPCException as e:
-            self._send_msg(str(e))
+        logger.debug(message)
+        self._send_msg(message)
+
+    @authorized_only
+    def _blacklist_delete(self, update: Update, context: CallbackContext) -> None:
+        """
+        Handler for /bl_delete
+        Deletes pair(s) from current blacklist
+        """
+        self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []))
 
     @authorized_only
     def _logs(self, update: Update, context: CallbackContext) -> None:
@@ -1258,6 +1265,8 @@ def _help(self, update: Update, context: CallbackContext) -> None:
             "*/whitelist:* `Show current whitelist` \n"
             "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
             "to the blacklist.` \n"
+            "*/blacklist_delete [pairs]| /bl_delete [pairs]:* "
+            "`Delete pair / pattern from blacklist. Will reset on reload_conf.` \n"
             "*/reload_config:* `Reload configuration file` \n"
             "*/unlock <pair|id>:* `Unlock this Pair (or this lock id if it's numeric)`\n"
 


Test patch:
diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py
--- a/tests/rpc/test_rpc.py
+++ b/tests/rpc/test_rpc.py
@@ -1225,6 +1225,16 @@ def test_rpc_blacklist(mocker, default_conf) -> None:
     assert 'errors' in ret
     assert isinstance(ret['errors'], dict)
 
+    ret = rpc._rpc_blacklist_delete(["DOGE/BTC", 'HOT/BTC'])
+
+    assert 'StaticPairList' in ret['method']
+    assert len(ret['blacklist']) == 2
+    assert ret['blacklist'] == default_conf['exchange']['pair_blacklist']
+    assert ret['blacklist'] == ['ETH/BTC', 'XRP/.*']
+    assert ret['blacklist_expanded'] == ['ETH/BTC', 'XRP/BTC', 'XRP/USDT']
+    assert 'errors' in ret
+    assert isinstance(ret['errors'], dict)
+
 
 def test_rpc_edge_disabled(mocker, default_conf) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py
--- a/tests/rpc/test_rpc_apiserver.py
+++ b/tests/rpc/test_rpc_apiserver.py
@@ -955,6 +955,38 @@ def test_api_blacklist(botclient, mocker):
                          "errors": {},
                          }
 
+    rc = client_delete(client, f"{BASE_URI}/blacklist?pairs_to_delete=DOGE/BTC")
+    assert_response(rc)
+    assert rc.json() == {"blacklist": ["HOT/BTC", "ETH/BTC", "XRP/.*"],
+                         "blacklist_expanded": ["ETH/BTC", "XRP/BTC", "XRP/USDT"],
+                         "length": 3,
+                         "method": ["StaticPairList"],
+                         "errors": {},
+                         }
+
+    rc = client_delete(client, f"{BASE_URI}/blacklist?pairs_to_delete=NOTHING/BTC")
+    assert_response(rc)
+    assert rc.json() == {"blacklist": ["HOT/BTC", "ETH/BTC", "XRP/.*"],
+                         "blacklist_expanded": ["ETH/BTC", "XRP/BTC", "XRP/USDT"],
+                         "length": 3,
+                         "method": ["StaticPairList"],
+                         "errors": {
+                             "NOTHING/BTC": {
+                                 "error_msg": "Pair NOTHING/BTC is not in the current blacklist."
+                             }
+                             },
+                         }
+    rc = client_delete(
+        client,
+        f"{BASE_URI}/blacklist?pairs_to_delete=HOT/BTC&pairs_to_delete=ETH/BTC")
+    assert_response(rc)
+    assert rc.json() == {"blacklist": ["XRP/.*"],
+                         "blacklist_expanded": ["XRP/BTC", "XRP/USDT"],
+                         "length": 1,
+                         "method": ["StaticPairList"],
+                         "errors": {},
+                         }
+
 
 def test_api_whitelist(botclient):
     ftbot, client = botclient
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -98,7 +98,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
                    "['stats'], ['daily'], ['weekly'], ['monthly'], "
                    "['count'], ['locks'], ['unlock', 'delete_locks'], "
                    "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], "
-                   "['stopbuy'], ['whitelist'], ['blacklist'], "
+                   "['stopbuy'], ['whitelist'], ['blacklist'], ['blacklist_delete', 'bl_delete'], "
                    "['logs'], ['edge'], ['help'], ['version']"
                    "]")
 
@@ -1470,6 +1470,13 @@ def test_blacklist_static(default_conf, update, mocker) -> None:
             in msg_mock.call_args_list[0][0][0])
     assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC", "XRP/.*"]
 
+    msg_mock.reset_mock()
+    context.args = ["DOGE/BTC"]
+    telegram._blacklist_delete(update=update, context=context)
+    assert msg_mock.call_count == 1
+    assert ("Blacklist contains 3 pairs\n`HOT/BTC, ETH/BTC, XRP/.*`"
+            in msg_mock.call_args_list[0][0][0])
+
 
 def test_telegram_logs(default_conf, update, mocker) -> None:
     mocker.patch.multiple(