diff --git a/docs/rest-api.md b/docs/rest-api.md index 2b55c2563..3b5c8928f 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: diff --git a/ft_client/freqtrade_client/ft_client.py b/ft_client/freqtrade_client/ft_client.py index d51858fde..3c450d1df 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,24 @@ 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"]))) + # 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(): diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index 6e5f7e6c5..f7c895901 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) @@ -312,20 +312,48 @@ 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 keyword argument - 'limit' or 'market' + :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 = { "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..541577e59 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 @@ -52,70 +52,89 @@ 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]), - ("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_candles", ["XRP/USDT", "5m", 500], {"columns": ["close_time,close"]}), + ("pair_history", ["XRP/USDT", "5m", "SampleStrategy"], {}), + ("pair_history", ["XRP/USDT", "5m"], {"strategy": "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 @@ -148,3 +167,40 @@ 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] + 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 + ) + 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()