diff --git a/ft_client/freqtrade_client/__init__.py b/ft_client/freqtrade_client/__init__.py index 8c79630d5..ac0b906b4 100644 --- a/ft_client/freqtrade_client/__init__.py +++ b/ft_client/freqtrade_client/__init__.py @@ -1,26 +1,37 @@ from freqtrade_client.ft_rest_client import FtRestClient -__version__ = '2024.5-dev' +__version__ = "2024.5-dev" -if 'dev' in __version__: +if "dev" in __version__: from pathlib import Path + try: import subprocess + freqtrade_basedir = Path(__file__).parent - __version__ = __version__ + '-' + subprocess.check_output( - ['git', 'log', '--format="%h"', '-n 1'], - stderr=subprocess.DEVNULL, cwd=freqtrade_basedir).decode("utf-8").rstrip().strip('"') + __version__ = ( + __version__ + + "-" + + subprocess.check_output( + ["git", "log", '--format="%h"', "-n 1"], + stderr=subprocess.DEVNULL, + cwd=freqtrade_basedir, + ) + .decode("utf-8") + .rstrip() + .strip('"') + ) except Exception: # pragma: no cover # git not available, ignore try: # Try Fallback to freqtrade_commit file (created by CI while building docker image) - versionfile = Path('./freqtrade_commit') + versionfile = Path("./freqtrade_commit") if versionfile.is_file(): __version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}" except Exception: pass -__all__ = ['FtRestClient'] +__all__ = ["FtRestClient"] diff --git a/ft_client/freqtrade_client/ft_client.py b/ft_client/freqtrade_client/ft_client.py index ba9a7172d..d51858fde 100644 --- a/ft_client/freqtrade_client/ft_client.py +++ b/ft_client/freqtrade_client/ft_client.py @@ -15,41 +15,44 @@ from freqtrade_client.ft_rest_client import FtRestClient logging.basicConfig( level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) logger = logging.getLogger("ft_rest_client") def add_arguments(args: Any = None): parser = argparse.ArgumentParser( - prog="freqtrade-client", - description="Client for the freqtrade REST API", + prog="freqtrade-client", + description="Client for the freqtrade REST API", + ) + parser.add_argument( + "command", help="Positional argument defining the command to execute.", nargs="?" + ) + parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}") + parser.add_argument( + "--show", + help="Show possible methods with this client", + dest="show", + action="store_true", + default=False, ) - parser.add_argument("command", - help="Positional argument defining the command to execute.", - nargs="?" - ) - parser.add_argument('-V', '--version', action='version', version=f'%(prog)s {__version__}') - parser.add_argument('--show', - help='Show possible methods with this client', - dest='show', - action='store_true', - default=False - ) - parser.add_argument('-c', '--config', - help='Specify configuration file (default: %(default)s). ', - dest='config', - type=str, - metavar='PATH', - default='config.json' - ) + parser.add_argument( + "-c", + "--config", + help="Specify configuration file (default: %(default)s). ", + dest="config", + type=str, + metavar="PATH", + default="config.json", + ) - parser.add_argument("command_arguments", - help="Positional arguments for the parameters for [command]", - nargs="*", - default=[] - ) + parser.add_argument( + "command_arguments", + help="Positional arguments for the parameters for [command]", + nargs="*", + default=[], + ) pargs = parser.parse_args(args) return vars(pargs) @@ -59,8 +62,9 @@ def load_config(configfile): file = Path(configfile) if file.is_file(): with file.open("r") as f: - config = rapidjson.load(f, parse_mode=rapidjson.PM_COMMENTS | - rapidjson.PM_TRAILING_COMMAS) + config = rapidjson.load( + f, parse_mode=rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS + ) return config else: logger.warning(f"Could not load config file {file}.") @@ -72,27 +76,26 @@ def print_commands(): client = FtRestClient(None) print("Possible commands:\n") for x, _ in inspect.getmembers(client): - if not x.startswith('_'): - doc = re.sub(':return:.*', '', getattr(client, x).__doc__, flags=re.MULTILINE).rstrip() + if not x.startswith("_"): + doc = re.sub(":return:.*", "", getattr(client, x).__doc__, flags=re.MULTILINE).rstrip() print(f"{x}\n\t{doc}\n") def main_exec(args: Dict[str, Any]): - if args.get("show"): print_commands() sys.exit() - config = load_config(args['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') - password = config.get('api_server', {}).get('password') + config = load_config(args["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") + password = config.get("api_server", {}).get("password") server_url = f"http://{url}:{port}" client = FtRestClient(server_url, username, password) - m = [x for x, y in inspect.getmembers(client) if not x.startswith('_')] + m = [x for x, y in inspect.getmembers(client) if not x.startswith("_")] command = args["command"] if command not in m: logger.error(f"Command {command} not defined") diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index 56290a9d8..6e5f7e6c5 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -21,31 +21,26 @@ PostDataT = Optional[Union[Dict[str, Any], List[Dict[str, Any]]]] class FtRestClient: - - def __init__(self, serverurl, username=None, password=None, *, - pool_connections=10, pool_maxsize=10): - + def __init__( + self, serverurl, username=None, password=None, *, pool_connections=10, pool_maxsize=10 + ): self._serverurl = serverurl self._session = requests.Session() # allow configuration of pool adapter = requests.adapters.HTTPAdapter( - pool_connections=pool_connections, - pool_maxsize=pool_maxsize + pool_connections=pool_connections, pool_maxsize=pool_maxsize ) - self._session.mount('http://', adapter) + self._session.mount("http://", adapter) self._session.auth = (username, password) def _call(self, method, apipath, params: Optional[dict] = None, data=None, files=None): - - if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'): - raise ValueError(f'invalid method <{method}>') + if str(method).upper() not in ("GET", "POST", "PUT", "DELETE"): + raise ValueError(f"invalid method <{method}>") basepath = f"{self._serverurl}/api/v1/{apipath}" - hd = {"Accept": "application/json", - "Content-Type": "application/json" - } + hd = {"Accept": "application/json", "Content-Type": "application/json"} # Split url schema, netloc, path, par, query, fragment = urlparse(basepath) @@ -151,7 +146,7 @@ class FtRestClient: """ return self._delete(f"locks/{lock_id}") - def lock_add(self, pair: str, until: str, side: str = '*', reason: str = ''): + def lock_add(self, pair: str, until: str, side: str = "*", reason: str = ""): """Lock pair :param pair: Pair to lock @@ -160,14 +155,7 @@ class FtRestClient: :param reason: Reason for the lock :return: json object """ - data = [ - { - "pair": pair, - "until": until, - "side": side, - "reason": reason - } - ] + data = [{"pair": pair, "until": until, "side": side, "reason": reason}] return self._post("locks", data=data) def daily(self, days=None): @@ -234,7 +222,7 @@ class FtRestClient: return self._get("version") def show_config(self): - """ Returns part of the configuration, relevant for trading operations. + """Returns part of the configuration, relevant for trading operations. :return: json object containing the version """ return self._get("show_config") @@ -244,7 +232,7 @@ class FtRestClient: configstatus = self.show_config() if not configstatus: return {"status": "not_running"} - elif configstatus['state'] == "running": + elif configstatus["state"] == "running": return {"status": "pong"} else: return {"status": "not_running"} @@ -266,9 +254,9 @@ class FtRestClient: """ params = {} if limit: - params['limit'] = limit + params["limit"] = limit if offset: - params['offset'] = offset + params["offset"] = offset return self._get("trades", params) def trade(self, trade_id): @@ -321,9 +309,7 @@ class FtRestClient: :param price: Optional - price to buy :return: json object of the trade """ - data = {"pair": pair, - "price": price - } + data = {"pair": pair, "price": price} return self._post("forcebuy", data=data) def forceenter(self, pair, side, price=None): @@ -334,11 +320,12 @@ class FtRestClient: :param price: Optional - price to buy :return: json object of the trade """ - data = {"pair": pair, - "side": side, - } + data = { + "pair": pair, + "side": side, + } if price: - data['price'] = price + data["price"] = price return self._post("forceenter", data=data) def forceexit(self, tradeid, ordertype=None, amount=None): @@ -350,11 +337,14 @@ class FtRestClient: :return: json object """ - return self._post("forceexit", data={ - "tradeid": tradeid, - "ordertype": ordertype, - "amount": amount, - }) + return self._post( + "forceexit", + data={ + "tradeid": tradeid, + "ordertype": ordertype, + "amount": amount, + }, + ) def strategies(self): """Lists available strategies @@ -392,10 +382,13 @@ class FtRestClient: :param stake_currency: Only pairs that include this timeframe :return: json object """ - return self._get("available_pairs", params={ - "stake_currency": stake_currency if timeframe else '', - "timeframe": timeframe if timeframe else '', - }) + return self._get( + "available_pairs", + params={ + "stake_currency": stake_currency if timeframe else "", + "timeframe": timeframe if timeframe else "", + }, + ) def pair_candles(self, pair, timeframe, limit=None, columns=None): """Return live dataframe for . @@ -411,14 +404,11 @@ class FtRestClient: "timeframe": timeframe, } if limit: - params['limit'] = limit + params["limit"] = limit if columns is not None: - params['columns'] = columns - return self._post( - "pair_candles", - data=params - ) + params["columns"] = columns + return self._post("pair_candles", data=params) return self._get("pair_candles", params=params) @@ -432,13 +422,16 @@ class FtRestClient: :param timerange: Timerange to get data for (same format than --timerange endpoints) :return: json object """ - return self._get("pair_history", params={ - "pair": pair, - "timeframe": timeframe, - "strategy": strategy, - "freqaimodel": freqaimodel, - "timerange": timerange if timerange else '', - }) + return self._get( + "pair_history", + params={ + "pair": pair, + "timeframe": timeframe, + "strategy": strategy, + "freqaimodel": freqaimodel, + "timerange": timerange if timerange else "", + }, + ) def sysinfo(self): """Provides system information (CPU, RAM usage) diff --git a/ft_client/test_client/test_rest_client.py b/ft_client/test_client/test_rest_client.py index 5a86c1b38..08ccee765 100644 --- a/ft_client/test_client/test_rest_client.py +++ b/ft_client/test_client/test_rest_client.py @@ -14,7 +14,7 @@ def log_has_re(line, logs): def get_rest_client(): - client = FtRestClient('http://localhost:8080', 'freqtrader', 'password') + client = FtRestClient("http://localhost:8080", "freqtrader", "password") client._session = MagicMock() request_mock = MagicMock() client._session.request = request_mock @@ -22,93 +22,96 @@ def get_rest_client(): def test_FtRestClient_init(): - client = FtRestClient('http://localhost:8080', 'freqtrader', 'password') + client = FtRestClient("http://localhost:8080", "freqtrader", "password") assert client is not None - assert client._serverurl == 'http://localhost:8080' + assert client._serverurl == "http://localhost:8080" assert client._session is not None assert client._session.auth is not None - assert client._session.auth == ('freqtrader', 'password') + assert client._session.auth == ("freqtrader", "password") -@pytest.mark.parametrize('method', ['GET', 'POST', 'DELETE']) +@pytest.mark.parametrize("method", ["GET", "POST", "DELETE"]) def test_FtRestClient_call(method): client, mock = get_rest_client() - client._call(method, '/dummytest') + client._call(method, "/dummytest") assert mock.call_count == 1 - getattr(client, f"_{method.lower()}")('/dummytest') + getattr(client, f"_{method.lower()}")("/dummytest") assert mock.call_count == 2 def test_FtRestClient_call_invalid(caplog): client, _ = get_rest_client() with pytest.raises(ValueError): - client._call('PUTTY', '/dummytest') + client._call("PUTTY", "/dummytest") client._session.request = MagicMock(side_effect=ConnectionError()) - client._call('GET', '/dummytest') + client._call("GET", "/dummytest") - assert log_has_re('Connection error', caplog) + assert log_has_re("Connection error", caplog) -@pytest.mark.parametrize('method,args', [ - ('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', []), -]) +@pytest.mark.parametrize( + "method,args", + [ + ("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", []), + ], +) def test_FtRestClient_call_explicit_methods(method, args): client, mock = get_rest_client() exec = getattr(client, method) @@ -118,37 +121,30 @@ def test_FtRestClient_call_explicit_methods(method, args): def test_ft_client(mocker, capsys, caplog): with pytest.raises(SystemExit): - args = add_arguments(['-V']) + args = add_arguments(["-V"]) - args = add_arguments(['--show']) + args = add_arguments(["--show"]) assert isinstance(args, dict) - assert args['show'] is True + assert args["show"] is True with pytest.raises(SystemExit): main_exec(args) captured = capsys.readouterr() - assert 'Possible commands' in captured.out + assert "Possible commands" in captured.out - mock = mocker.patch('freqtrade_client.ft_client.FtRestClient._call') - args = add_arguments([ - '--config', - 'tests/testdata/testconfigs/main_test_config.json', - 'ping' - ]) + mock = mocker.patch("freqtrade_client.ft_client.FtRestClient._call") + args = add_arguments(["--config", "tests/testdata/testconfigs/main_test_config.json", "ping"]) main_exec(args) captured = capsys.readouterr() assert mock.call_count == 1 with pytest.raises(SystemExit): - args = add_arguments(['--config', 'tests/testdata/testconfigs/nonexisting.json']) + args = add_arguments(["--config", "tests/testdata/testconfigs/nonexisting.json"]) main_exec(args) - assert log_has_re(r'Could not load config file .*nonexisting\.json\.', - caplog) + assert log_has_re(r"Could not load config file .*nonexisting\.json\.", caplog) - args = add_arguments([ - '--config', - 'tests/testdata/testconfigs/main_test_config.json', - 'whatever' - ]) + args = add_arguments( + ["--config", "tests/testdata/testconfigs/main_test_config.json", "whatever"] + ) main_exec(args) - assert log_has_re('Command whatever not defined', caplog) + assert log_has_re("Command whatever not defined", caplog)