From cc96db76f0f985a05aef0ad9eb80d02372e12b3c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Dec 2021 15:53:44 +0100 Subject: [PATCH 1/5] Add possibility to delete pairs from the pairlist via api --- freqtrade/rpc/api_server/api_v1.py | 9 ++++++++- freqtrade/rpc/rpc.py | 14 ++++++++++++++ tests/rpc/test_rpc_apiserver.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 721d5dbc0..45225fe67 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -3,7 +3,7 @@ from copy import deepcopy 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__ @@ -157,6 +157,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 index b3728d67a..3328af30b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -860,6 +860,20 @@ class RPC: } 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/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 883a33e38..231b17e9c 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -955,6 +955,35 @@ 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 From 8da79d0ab2a1f1733ace1c99bb7d3d2c79d05130 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Dec 2021 16:09:20 +0100 Subject: [PATCH 2/5] Add blacklist-control to telegram --- freqtrade/rpc/telegram.py | 25 ++++++++++++++++++++++--- tests/rpc/test_rpc.py | 10 ++++++++++ tests/rpc/test_rpc_telegram.py | 9 ++++++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 3099de5ba..e7388fece 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -111,9 +111,9 @@ class Telegram(RPCHandler): 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 @@ class Telegram(RPCHandler): 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), @@ -1164,7 +1165,12 @@ class Telegram(RPCHandler): """ try: - blacklist = self._rpc._rpc_blacklist(context.args) + self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) + except RPCException as e: + self._send_msg(str(e)) + + def send_blacklist_msg(self, blacklist: Dict): + try: errmsgs = [] for pair, error in blacklist['errors'].items(): errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`") @@ -1179,6 +1185,17 @@ class Telegram(RPCHandler): except RPCException as e: self._send_msg(str(e)) + @authorized_only + def _blacklist_delete(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /bl_delete + Deletes pair(s) from current blacklist + """ + try: + self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args)) + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _logs(self, update: Update, context: CallbackContext) -> None: """ @@ -1258,6 +1275,8 @@ class Telegram(RPCHandler): "*/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 :* `Unlock this Pair (or this lock id if it's numeric)`\n" diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index b6fe1c691..3e6917dd6 100644 --- 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_telegram.py b/tests/rpc/test_rpc_telegram.py index e7db3ab19..4fda67a4a 100644 --- 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( From 857f4ec125fdce033854cff233cb296c806112e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Dec 2021 16:20:09 +0100 Subject: [PATCH 3/5] Remove exception-handlers which catch exceptions that are never raised --- freqtrade/rpc/telegram.py | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e7388fece..206e2d4f7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1163,27 +1163,20 @@ class Telegram(RPCHandler): Handler for /blacklist Shows the currently active blacklist """ - try: - - self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) - except RPCException as e: - self._send_msg(str(e)) + self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) def send_blacklist_msg(self, blacklist: Dict): - try: - 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)) + 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: @@ -1191,10 +1184,7 @@ class Telegram(RPCHandler): Handler for /bl_delete Deletes pair(s) from current blacklist """ - try: - self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args)) - except RPCException as e: - self._send_msg(str(e)) + self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args)) @authorized_only def _logs(self, update: Update, context: CallbackContext) -> None: From 8fdef2900e791a1dc5bf140975891b06924105c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Dec 2021 19:41:30 +0100 Subject: [PATCH 4/5] Increment API version to let clients know this is now available --- freqtrade/rpc/api_server/api_v1.py | 3 ++- tests/rpc/test_rpc_apiserver.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 45225fe67..1c1ff39df 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -30,7 +30,8 @@ logger = logging.getLogger(__name__) # 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() diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 231b17e9c..e6d80880b 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -976,7 +976,9 @@ def test_api_blacklist(botclient, mocker): } }, } - rc = client_delete(client, f"{BASE_URI}/blacklist?pairs_to_delete=HOT/BTC&pairs_to_delete=ETH/BTC") + 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"], From 39f0a17e6220e137387e651d3d8db69b0121f522 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Dec 2021 20:08:03 +0100 Subject: [PATCH 5/5] Fix formatting --- freqtrade/rpc/telegram.py | 6 +++--- tests/rpc/test_rpc_apiserver.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 206e2d4f7..f333dc5db 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1184,7 +1184,7 @@ class Telegram(RPCHandler): Handler for /bl_delete Deletes pair(s) from current blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args)) + self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or [])) @authorized_only def _logs(self, update: Update, context: CallbackContext) -> None: @@ -1265,8 +1265,8 @@ class Telegram(RPCHandler): "*/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" + "*/blacklist_delete [pairs]| /bl_delete [pairs]:* " + "`Delete pair / pattern from blacklist. Will reset on reload_conf.` \n" "*/reload_config:* `Reload configuration file` \n" "*/unlock :* `Unlock this Pair (or this lock id if it's numeric)`\n" diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e6d80880b..d6a58322a 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -987,6 +987,7 @@ def test_api_blacklist(botclient, mocker): "errors": {}, } + def test_api_whitelist(botclient): ftbot, client = botclient