From 3da18c3443b74478c86221e2309ee9732ea98ba8 Mon Sep 17 00:00:00 2001 From: Robert Davey Date: Wed, 29 May 2024 14:45:49 +0100 Subject: [PATCH 01/12] Add force_enter optional args and tests --- ft_client/freqtrade_client/ft_rest_client.py | 23 +++++++++++++++++++- ft_client/test_client/test_rest_client.py | 4 ++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index 6e5f7e6c5..af2d370e0 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -312,20 +312,41 @@ class FtRestClient: data = {"pair": pair, "price": price} return self._post("forcebuy", data=data) - def forceenter(self, pair, side, price=None): + def forceenter(self, pair, side, + price=None, order_type=None, + stake_amount=None, leverage=None, + enter_tag=None): """Force entering a trade :param pair: Pair to buy (ETH/BTC) :param side: 'long' or 'short' :param price: Optional - price to buy + :param order_type: Optional - 'limit' or 'market' + :param stake_amount: Optional - stake amount (as a float) + :param leverage: Optional - leverage (as a float) + :param enter_tag: Optional - entry tag (as a string, default: 'force_enter') :return: json object of the trade """ data = { "pair": pair, "side": side, } + if price: data["price"] = price + + if order_type: + data["ordertype"] = order_type + + if stake_amount: + data["stakeamount"] = stake_amount + + if leverage: + data["leverage"] = leverage + + if enter_tag: + data["entry_tag"] = enter_tag + return self._post("forceenter", data=data) def forceexit(self, tradeid, ordertype=None, amount=None): diff --git a/ft_client/test_client/test_rest_client.py b/ft_client/test_client/test_rest_client.py index 08ccee765..05af92ad6 100644 --- a/ft_client/test_client/test_rest_client.py +++ b/ft_client/test_client/test_rest_client.py @@ -96,6 +96,10 @@ def test_FtRestClient_call_invalid(caplog): ("forcebuy", ["XRP/USDT", 1.5]), ("forceenter", ["XRP/USDT", "short"]), ("forceenter", ["XRP/USDT", "short", 1.5]), + ("forceenter", ["XRP/USDT", "short", 1.5, "market"]), + ("forceenter", ["XRP/USDT", "short", 1.5, "market", 100]), + ("forceenter", ["XRP/USDT", "short", 1.5, "market", 100, 10.0]), + ("forceenter", ["XRP/USDT", "short", 1.5, "market", 100, 10.0, "test_force_enter"]), ("forceexit", [1]), ("forceexit", [1, "limit"]), ("forceexit", [1, "limit", 100]), From 8dc70d15dbd94d6a5c2c916754a0c9bb2bb371a9 Mon Sep 17 00:00:00 2001 From: froggleston Date: Wed, 29 May 2024 14:59:32 +0100 Subject: [PATCH 02/12] ruff formetting --- ft_client/freqtrade_client/ft_rest_client.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index af2d370e0..399ce2e8d 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -312,10 +312,16 @@ class FtRestClient: data = {"pair": pair, "price": price} return self._post("forcebuy", data=data) - def forceenter(self, pair, side, - price=None, order_type=None, - stake_amount=None, leverage=None, - enter_tag=None): + def forceenter( + self, + pair, + side, + price=None, + order_type=None, + stake_amount=None, + leverage=None, + enter_tag=None, + ): """Force entering a trade :param pair: Pair to buy (ETH/BTC) From 619484a4fdc4abd139f2fdedf323ca2e6dcc73c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2024 09:11:32 +0200 Subject: [PATCH 03/12] feat: Make the new arguments kwargs only --- ft_client/freqtrade_client/ft_rest_client.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index 399ce2e8d..4e821ae5a 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -317,6 +317,7 @@ class FtRestClient: pair, side, price=None, + *, order_type=None, stake_amount=None, leverage=None, @@ -327,10 +328,10 @@ class FtRestClient: :param pair: Pair to buy (ETH/BTC) :param side: 'long' or 'short' :param price: Optional - price to buy - :param order_type: Optional - 'limit' or 'market' - :param stake_amount: Optional - stake amount (as a float) - :param leverage: Optional - leverage (as a float) - :param enter_tag: Optional - entry tag (as a string, default: 'force_enter') + :param order_type: Optional keyword argument - 'limit' or 'market' + :param stake_amount: Optional keyword argument - stake amount (as a float) + :param leverage: Optional keyword argument - leverage (as a float) + :param enter_tag: Optional keyword argument - entry tag (as a string, default: 'force_enter') :return: json object of the trade """ data = { From 61971f3949f81fc8d96d17dba7c1ff93b0ff9f8e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2024 09:11:54 +0200 Subject: [PATCH 04/12] chore: ftclient - Update naming of argument in main method --- ft_client/freqtrade_client/ft_client.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ft_client/freqtrade_client/ft_client.py b/ft_client/freqtrade_client/ft_client.py index d51858fde..9dbb69b74 100644 --- a/ft_client/freqtrade_client/ft_client.py +++ b/ft_client/freqtrade_client/ft_client.py @@ -81,12 +81,12 @@ def print_commands(): print(f"{x}\n\t{doc}\n") -def main_exec(args: Dict[str, Any]): - if args.get("show"): +def main_exec(parsed: Dict[str, Any]): + if parsed.get("show"): print_commands() sys.exit() - config = load_config(args["config"]) + config = load_config(parsed["config"]) url = config.get("api_server", {}).get("listen_ip_address", "127.0.0.1") port = config.get("api_server", {}).get("listen_port", "8080") username = config.get("api_server", {}).get("username") @@ -96,13 +96,13 @@ def main_exec(args: Dict[str, Any]): client = FtRestClient(server_url, username, password) m = [x for x, y in inspect.getmembers(client) if not x.startswith("_")] - command = args["command"] + command = parsed["command"] if command not in m: logger.error(f"Command {command} not defined") print_commands() return - print(json.dumps(getattr(client, command)(*args["command_arguments"]))) + print(json.dumps(getattr(client, command)(*parsed["command_arguments"]))) def main(): From 9d3e435162f2adfada7b751b2f9075dca0931b06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2024 09:13:57 +0200 Subject: [PATCH 05/12] Improve error for rest client --- ft_client/freqtrade_client/ft_rest_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index 4e821ae5a..b03d61d7a 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -54,7 +54,7 @@ class FtRestClient: # return resp.text return resp.json() except ConnectionError: - logger.warning("Connection error") + logger.warning(f"Connection error - could not connect to {netloc}.") def _get(self, apipath, params: ParamsT = None): return self._call("GET", apipath, params=params) From a03528406f84b7fbc3ac7255713aa4f04e67b7f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2024 09:16:38 +0200 Subject: [PATCH 06/12] Split client arguments to accept kwarguments --- ft_client/freqtrade_client/ft_client.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ft_client/freqtrade_client/ft_client.py b/ft_client/freqtrade_client/ft_client.py index 9dbb69b74..3c450d1df 100644 --- a/ft_client/freqtrade_client/ft_client.py +++ b/ft_client/freqtrade_client/ft_client.py @@ -102,7 +102,18 @@ def main_exec(parsed: Dict[str, Any]): print_commands() return - print(json.dumps(getattr(client, command)(*parsed["command_arguments"]))) + # Split arguments with = into key/value pairs + kwargs = {x.split("=")[0]: x.split("=")[1] for x in parsed["command_arguments"] if "=" in x} + args = [x for x in parsed["command_arguments"] if "=" not in x] + try: + res = getattr(client, command)(*args, **kwargs) + print(json.dumps(res)) + except TypeError as e: + logger.error(f"Error executing command {command}: {e}") + sys.exit(1) + except Exception as e: + logger.error(f"Fatal Error executing command {command}: {e}") + sys.exit(1) def main(): From 1b491e9e1566915b2fd0216baaa44d17e7c4a0ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2024 09:21:35 +0200 Subject: [PATCH 07/12] Update tests to support kwargs --- ft_client/test_client/test_rest_client.py | 139 ++++++++++++---------- 1 file changed, 76 insertions(+), 63 deletions(-) diff --git a/ft_client/test_client/test_rest_client.py b/ft_client/test_client/test_rest_client.py index 05af92ad6..84bbec0f4 100644 --- a/ft_client/test_client/test_rest_client.py +++ b/ft_client/test_client/test_rest_client.py @@ -52,74 +52,87 @@ def test_FtRestClient_call_invalid(caplog): @pytest.mark.parametrize( - "method,args", + "method,args,kwargs", [ - ("start", []), - ("stop", []), - ("stopbuy", []), - ("reload_config", []), - ("balance", []), - ("count", []), - ("entries", []), - ("exits", []), - ("mix_tags", []), - ("locks", []), - ("lock_add", ["XRP/USDT", "2024-01-01 20:00:00Z", "*", "rand"]), - ("delete_lock", [2]), - ("daily", []), - ("daily", [15]), - ("weekly", []), - ("weekly", [15]), - ("monthly", []), - ("monthly", [12]), - ("edge", []), - ("profit", []), - ("stats", []), - ("performance", []), - ("status", []), - ("version", []), - ("show_config", []), - ("ping", []), - ("logs", []), - ("logs", [55]), - ("trades", []), - ("trades", [5]), - ("trades", [5, 5]), # With offset - ("trade", [1]), - ("delete_trade", [1]), - ("cancel_open_order", [1]), - ("whitelist", []), - ("blacklist", []), - ("blacklist", ["XRP/USDT"]), - ("blacklist", ["XRP/USDT", "BTC/USDT"]), - ("forcebuy", ["XRP/USDT"]), - ("forcebuy", ["XRP/USDT", 1.5]), - ("forceenter", ["XRP/USDT", "short"]), - ("forceenter", ["XRP/USDT", "short", 1.5]), - ("forceenter", ["XRP/USDT", "short", 1.5, "market"]), - ("forceenter", ["XRP/USDT", "short", 1.5, "market", 100]), - ("forceenter", ["XRP/USDT", "short", 1.5, "market", 100, 10.0]), - ("forceenter", ["XRP/USDT", "short", 1.5, "market", 100, 10.0, "test_force_enter"]), - ("forceexit", [1]), - ("forceexit", [1, "limit"]), - ("forceexit", [1, "limit", 100]), - ("strategies", []), - ("strategy", ["sampleStrategy"]), - ("pairlists_available", []), - ("plot_config", []), - ("available_pairs", []), - ("available_pairs", ["5m"]), - ("pair_candles", ["XRP/USDT", "5m"]), - ("pair_candles", ["XRP/USDT", "5m", 500]), - ("pair_history", ["XRP/USDT", "5m", "SampleStrategy"]), - ("sysinfo", []), - ("health", []), + ("start", [], {}), + ("stop", [], {}), + ("stopbuy", [], {}), + ("reload_config", [], {}), + ("balance", [], {}), + ("count", [], {}), + ("entries", [], {}), + ("exits", [], {}), + ("mix_tags", [], {}), + ("locks", [], {}), + ("lock_add", ["XRP/USDT", "2024-01-01 20:00:00Z", "*", "rand"], {}), + ("delete_lock", [2], {}), + ("daily", [], {}), + ("daily", [15], {}), + ("weekly", [], {}), + ("weekly", [15], {}), + ("monthly", [], {}), + ("monthly", [12], {}), + ("edge", [], {}), + ("profit", [], {}), + ("stats", [], {}), + ("performance", [], {}), + ("status", [], {}), + ("version", [], {}), + ("show_config", [], {}), + ("ping", [], {}), + ("logs", [], {}), + ("logs", [55], {}), + ("trades", [], {}), + ("trades", [5], {}), + ("trades", [5, 5], {}), # With offset + ("trade", [1], {}), + ("delete_trade", [1], {}), + ("cancel_open_order", [1], {}), + ("whitelist", [], {}), + ("blacklist", [], {}), + ("blacklist", ["XRP/USDT"], {}), + ("blacklist", ["XRP/USDT", "BTC/USDT"], {}), + ("forcebuy", ["XRP/USDT"], {}), + ("forcebuy", ["XRP/USDT", 1.5], {}), + ("forceenter", ["XRP/USDT", "short"], {}), + ("forceenter", ["XRP/USDT", "short", 1.5], {}), + ("forceenter", ["XRP/USDT", "short", 1.5], {"order_type": "market"}), + ("forceenter", ["XRP/USDT", "short", 1.5], {"order_type": "market", "stake_amount": 100}), + ( + "forceenter", + ["XRP/USDT", "short", 1.5], + {"order_type": "market", "stake_amount": 100, "leverage": 10.0}, + ), + ( + "forceenter", + ["XRP/USDT", "short", 1.5], + { + "order_type": "market", + "stake_amount": 100, + "leverage": 10.0, + "enter_tag": "test_force_enter", + }, + ), + ("forceexit", [1], {}), + ("forceexit", [1, "limit"], {}), + ("forceexit", [1, "limit", 100], {}), + ("strategies", [], {}), + ("strategy", ["sampleStrategy"], {}), + ("pairlists_available", [], {}), + ("plot_config", [], {}), + ("available_pairs", [], {}), + ("available_pairs", ["5m"], {}), + ("pair_candles", ["XRP/USDT", "5m"], {}), + ("pair_candles", ["XRP/USDT", "5m", 500], {}), + ("pair_history", ["XRP/USDT", "5m", "SampleStrategy"], {}), + ("sysinfo", [], {}), + ("health", [], {}), ], ) -def test_FtRestClient_call_explicit_methods(method, args): +def test_FtRestClient_call_explicit_methods(method, args, kwargs): client, mock = get_rest_client() exec = getattr(client, method) - exec(*args) + exec(*args, **kwargs) assert mock.call_count == 1 From 5a8838aec71ba494ead936c36d422ca4a95c759a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2024 09:22:17 +0200 Subject: [PATCH 08/12] Additional test-cases --- ft_client/test_client/test_rest_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ft_client/test_client/test_rest_client.py b/ft_client/test_client/test_rest_client.py index 84bbec0f4..f68f8b893 100644 --- a/ft_client/test_client/test_rest_client.py +++ b/ft_client/test_client/test_rest_client.py @@ -124,7 +124,9 @@ def test_FtRestClient_call_invalid(caplog): ("available_pairs", ["5m"], {}), ("pair_candles", ["XRP/USDT", "5m"], {}), ("pair_candles", ["XRP/USDT", "5m", 500], {}), + ("pair_candles", ["XRP/USDT", "5m", 500], {"columns": ["close_time,close"]}), ("pair_history", ["XRP/USDT", "5m", "SampleStrategy"], {}), + ("pair_history", ["XRP/USDT", "5m"], {"strategy": "SampleStrategy"}), ("sysinfo", [], {}), ("health", [], {}), ], From c5b4d6bcedfd96d3e0e13f2c0952ec1e834887d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2024 09:43:48 +0200 Subject: [PATCH 09/12] Add teset for kwarg splitting --- ft_client/test_client/test_rest_client.py | 38 ++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/ft_client/test_client/test_rest_client.py b/ft_client/test_client/test_rest_client.py index f68f8b893..19b1c9b7d 100644 --- a/ft_client/test_client/test_rest_client.py +++ b/ft_client/test_client/test_rest_client.py @@ -1,5 +1,5 @@ import re -from unittest.mock import MagicMock +from unittest.mock import ANY, MagicMock import pytest from requests.exceptions import ConnectionError @@ -167,3 +167,39 @@ def test_ft_client(mocker, capsys, caplog): ) main_exec(args) assert log_has_re("Command whatever not defined", caplog) + + +@pytest.mark.parametrize( + "params, expected_args, expected_kwargs", + [ + ("forceenter BTC/USDT long", ["BTC/USDT", "long"], {}), + ("forceenter BTC/USDT long limit", ["BTC/USDT", "long", "limit"], {}), + ( + # Skip most parameters, only providing enter_tag + "forceenter BTC/USDT long enter_tag=deadBeef", + ["BTC/USDT", "long"], + {"enter_tag": "deadBeef"}, + ), + ( + "forceenter BTC/USDT long invalid_key=123", + [], + SystemExit, + # {"invalid_key": "deadBeef"}, + ), + ], +) +def test_ft_client_argparsing(mocker, params, expected_args, expected_kwargs, caplog): + mocked_method = params.split(" ")[0] + mm = mocker.patch( + f"freqtrade_client.ft_client.FtRestClient.{mocked_method}", return_value={}, autospec=True + ) + args = add_arguments(params.split(" ")) + if isinstance(expected_kwargs, dict): + main_exec(args) + mm.assert_called_once_with(ANY, *expected_args, **expected_kwargs) + else: + with pytest.raises(expected_kwargs): + main_exec(args) + + assert log_has_re(f"Error executing command {mocked_method}: got an unexpected .*", caplog) + mm.assert_not_called() From 6ec4907271b5c5b94aa51e7c488ad864c75ec063 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2024 09:45:34 +0200 Subject: [PATCH 10/12] Improve docstring --- ft_client/freqtrade_client/ft_rest_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index b03d61d7a..f7c895901 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -329,9 +329,9 @@ class FtRestClient: :param side: 'long' or 'short' :param price: Optional - price to buy :param order_type: Optional keyword argument - 'limit' or 'market' - :param stake_amount: Optional keyword argument - stake amount (as a float) - :param leverage: Optional keyword argument - leverage (as a float) - :param enter_tag: Optional keyword argument - entry tag (as a string, default: 'force_enter') + :param stake_amount: Optional keyword argument - stake amount (as float) + :param leverage: Optional keyword argument - leverage (as float) + :param enter_tag: Optional keyword argument - entry tag (as string, default: 'force_enter') :return: json object of the trade """ data = { From 3979801a861bb5e2fbed9e7dc91de1006298c10a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2024 09:49:08 +0200 Subject: [PATCH 11/12] Update documentation about keyword arguments --- docs/rest-api.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/rest-api.md b/docs/rest-api.md index 51573b77f..4f2eef0b2 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -118,6 +118,14 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use freqtrade-client --config rest_config.json [optional parameters] ``` +Commands with many arguments may require keyword arguments (for clarity) - which can be provided as follows: + +``` bash +freqtrade-client --config rest_config.json forceenter BTC/USDT long enter_tag=GutFeeling +``` + +This method will work for all arguments - check the "show" command for a list of available parameters. + ??? Note "Programmatic use" The `freqtrade-client` package (installable independent of freqtrade) can be used in your own scripts to interact with the freqtrade API. to do so, please use the following: From df47d154f96e03af72e8efae21823104c848a9a4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2024 08:09:35 +0200 Subject: [PATCH 12/12] Mock config loading for ft-client test --- ft_client/test_client/test_rest_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ft_client/test_client/test_rest_client.py b/ft_client/test_client/test_rest_client.py index 19b1c9b7d..541577e59 100644 --- a/ft_client/test_client/test_rest_client.py +++ b/ft_client/test_client/test_rest_client.py @@ -190,6 +190,7 @@ def test_ft_client(mocker, capsys, caplog): ) def test_ft_client_argparsing(mocker, params, expected_args, expected_kwargs, caplog): mocked_method = params.split(" ")[0] + mocker.patch("freqtrade_client.ft_client.load_config", return_value={}, autospec=True) mm = mocker.patch( f"freqtrade_client.ft_client.FtRestClient.{mocked_method}", return_value={}, autospec=True )