From d1a0f5a4e12e66d9a2732716fb3b1c95280f6666 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2024 16:32:29 +0200 Subject: [PATCH 01/74] Add test with invalid trade object --- tests/freqtradebot/test_freqtradebot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index db2e96b37..79224ddec 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -3810,6 +3810,9 @@ def test_get_real_amount_quote_dust( def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mocker, fee): mocker.patch(f"{EXMS}.get_trades_for_order", return_value=[]) + # Invalid nested trade object + buy_order_fee["trades"] = [{"amount": None, "cost": 22}] + amount = buy_order_fee["amount"] trade = Trade( pair="LTC/ETH", From 7c697d4ded67357909ca6ae1285979b96d6b2f2c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2024 16:38:12 +0200 Subject: [PATCH 02/74] fix: Validate trades for fee We can't rely on the "trades" subarray from the order to contain everything we need As such, we need to ensure that required properties are present and not None. closes #10398 --- freqtrade/freqtradebot.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b3ff4923d..5a33e9fa6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -2370,6 +2370,18 @@ class FreqtradeBot(LoggingMixin): trade, order, order_obj, order_amount, order.get("trades", []) ) + def _trades_valid_for_fee(self, trades: List[Dict[str, Any]]) -> bool: + """ + Check if trades are valid for fee detection. + :return: True if trades are valid for fee detection, False otherwise + """ + if not trades: + return False + # We expect amount and cost to be present in all trade objects. + if any(trade.get("amount") is None or trade.get("cost") is None for trade in trades): + return False + return True + def fee_detection_from_trades( self, trade: Trade, order: Dict, order_obj: Order, order_amount: float, trades: List ) -> Optional[float]: @@ -2377,7 +2389,7 @@ class FreqtradeBot(LoggingMixin): fee-detection fallback to Trades. Either uses provided trades list or the result of fetch_my_trades to get correct fee. """ - if not trades: + if not self._trades_valid_for_fee(trades): trades = self.exchange.get_trades_for_order( self.exchange.get_order_id_conditional(order), trade.pair, order_obj.order_date ) From 72c7f118a0d7f0b46eddc1cd150a610b7741ea50 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2024 17:43:50 +0200 Subject: [PATCH 03/74] Improve test naming to be clearer --- tests/exchange_online/test_ccxt_ws_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange_online/test_ccxt_ws_compat.py b/tests/exchange_online/test_ccxt_ws_compat.py index 49c46f516..3d7188523 100644 --- a/tests/exchange_online/test_ccxt_ws_compat.py +++ b/tests/exchange_online/test_ccxt_ws_compat.py @@ -21,7 +21,7 @@ from tests.exchange_online.conftest import EXCHANGE_WS_FIXTURE_TYPE @pytest.mark.longrun @pytest.mark.timeout(3 * 60) class TestCCXTExchangeWs: - def test_ccxt_ohlcv(self, exchange_ws: EXCHANGE_WS_FIXTURE_TYPE, caplog, mocker): + def test_ccxt_watch_ohlcv(self, exchange_ws: EXCHANGE_WS_FIXTURE_TYPE, caplog, mocker): exch, exchangename, pair = exchange_ws assert exch._ws_async is not None From cd8bd9038e8704a93bcae0efb84a1a1125cf3be6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2024 18:12:14 +0200 Subject: [PATCH 04/74] fix: Reassign "options" on market reload Ensures that all data is available after init or reload. --- freqtrade/exchange/exchange.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ac648abea..9be949ff5 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -602,9 +602,12 @@ class Exchange: # Reload async markets, then assign them to sync api self._markets = self._load_async_markets(reload=True) self._api.set_markets(self._api_async.markets, self._api_async.currencies) + # Assign options array, as it contains some temporary information from the exchange. + self._api.options = self._api_async.options if self._exchange_ws: # Set markets to avoid reloading on websocket api self._ws_async.set_markets(self._api.markets, self._api.currencies) + self._ws_async.options = self._api.options self._last_markets_refresh = dt_ts() if is_initial and self._ft_has["needs_trading_fees"]: From 53043d197890a5331a733504e63d70cae5791114 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jul 2024 07:33:26 +0200 Subject: [PATCH 05/74] Pin numpy install to <2.0 in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8865f46be..82e529767 100644 --- a/setup.py +++ b/setup.py @@ -78,7 +78,7 @@ setup( "httpx>=0.24.1", "urllib3", "jsonschema", - "numpy", + "numpy<2.0", "pandas>=2.2.0,<3.0", "TA-Lib", "pandas-ta", From ab0fd461eda8be4355c99be15c252390563582ff Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jul 2024 07:46:27 +0200 Subject: [PATCH 06/74] chore: Replace np.NAN with np.nan properly implements what #10402 tried to do. --- freqtrade/rpc/rpc.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 3513b7207..3feb4860c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -11,7 +11,7 @@ from typing import Any, Dict, Generator, List, Optional, Sequence, Tuple, Union import psutil from dateutil.relativedelta import relativedelta from dateutil.tz import tzlocal -from numpy import NAN, inf, int64, mean +from numpy import inf, int64, mean, nan from pandas import DataFrame, NaT from sqlalchemy import func, select @@ -204,9 +204,9 @@ class RPC: trade.pair, side="exit", is_short=trade.is_short, refresh=False ) except (ExchangeError, PricingError): - current_rate = NAN + current_rate = nan if len(trade.select_filled_orders(trade.entry_side)) > 0: - current_profit = current_profit_abs = current_profit_fiat = NAN + current_profit = current_profit_abs = current_profit_fiat = nan if not isnan(current_rate): prof = trade.calculate_profit(current_rate) current_profit = prof.profit_ratio @@ -277,7 +277,7 @@ class RPC: raise RPCException("no active trade") else: trades_list = [] - fiat_profit_sum = NAN + fiat_profit_sum = nan for trade in trades: # calculate profit and send message to user try: @@ -285,9 +285,9 @@ class RPC: trade.pair, side="exit", is_short=trade.is_short, refresh=False ) except (PricingError, ExchangeError): - current_rate = NAN - trade_profit = NAN - profit_str = f"{NAN:.2%}" + current_rate = nan + trade_profit = nan + profit_str = f"{nan:.2%}" else: if trade.nr_of_successful_entries > 0: profit = trade.calculate_profit(current_rate) @@ -533,9 +533,9 @@ class RPC: trade.pair, side="exit", is_short=trade.is_short, refresh=False ) except (PricingError, ExchangeError): - current_rate = NAN - profit_ratio = NAN - profit_abs = NAN + current_rate = nan + profit_ratio = nan + profit_abs = nan else: _profit = trade.calculate_profit(trade.close_rate or current_rate) @@ -1317,7 +1317,7 @@ class RPC: # replace NaT with `None` dataframe[date_column] = dataframe[date_column].astype(object).replace({NaT: None}) - dataframe = dataframe.replace({inf: None, -inf: None, NAN: None}) + dataframe = dataframe.replace({inf: None, -inf: None, nan: None}) res = { "pair": pair, From 0eadd51ff37f1d3a006ddcfd42b0b00c5cdd9f00 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jul 2024 08:18:16 +0200 Subject: [PATCH 07/74] chore: Highlight non-used variable in test --- tests/exchange_online/test_ccxt_ws_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange_online/test_ccxt_ws_compat.py b/tests/exchange_online/test_ccxt_ws_compat.py index 3d7188523..ed449bb58 100644 --- a/tests/exchange_online/test_ccxt_ws_compat.py +++ b/tests/exchange_online/test_ccxt_ws_compat.py @@ -22,7 +22,7 @@ from tests.exchange_online.conftest import EXCHANGE_WS_FIXTURE_TYPE @pytest.mark.timeout(3 * 60) class TestCCXTExchangeWs: def test_ccxt_watch_ohlcv(self, exchange_ws: EXCHANGE_WS_FIXTURE_TYPE, caplog, mocker): - exch, exchangename, pair = exchange_ws + exch, _exchangename, pair = exchange_ws assert exch._ws_async is not None timeframe = "1m" From 8d9825ac95df58e96fc689173a7a2c39539e2b82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jul 2024 08:25:24 +0200 Subject: [PATCH 08/74] chore: noqa S404 we're using static parameters with subpross, so it should be safe. --- freqtrade/__init__.py | 2 +- freqtrade/configuration/directory_operations.py | 2 +- ft_client/freqtrade_client/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index d9c41c38b..0cb8dd961 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -6,7 +6,7 @@ if "dev" in __version__: from pathlib import Path try: - import subprocess + import subprocess # noqa: S404 freqtrade_basedir = Path(__file__).parent diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 99d72dabe..3e6ed92ed 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -38,7 +38,7 @@ def chown_user_directory(directory: Path) -> None: """ if running_in_docker(): try: - import subprocess + import subprocess # noqa: S404 subprocess.check_output(["sudo", "chown", "-R", "ftuser:", str(directory.resolve())]) except Exception: diff --git a/ft_client/freqtrade_client/__init__.py b/ft_client/freqtrade_client/__init__.py index 6c0da7eae..9311fc85d 100644 --- a/ft_client/freqtrade_client/__init__.py +++ b/ft_client/freqtrade_client/__init__.py @@ -7,7 +7,7 @@ if "dev" in __version__: from pathlib import Path try: - import subprocess + import subprocess # noqa: S404 freqtrade_basedir = Path(__file__).parent From 059fb16a353529e7eb776903cab1787ff61c8126 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jul 2024 08:28:09 +0200 Subject: [PATCH 09/74] docs: improve dev setup docs --- docs/developer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer.md b/docs/developer.md index 705e8d116..8cf20d966 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -22,7 +22,7 @@ This will spin up a local server (usually on port 8000) so you can see if everyt ## Developer setup To configure a development environment, you can either use the provided [DevContainer](#devcontainer-setup), or use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". -Alternatively (e.g. if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`. +Alternatively (e.g. if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -r requirements-dev.txt` - followed by `pip3 install -e .[all]`. This will install all required tools for development, including `pytest`, `ruff`, `mypy`, and `coveralls`. From 8296e7010cd092c03aa1ecedb1d418a20a0add05 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jul 2024 08:39:35 +0200 Subject: [PATCH 10/74] chore: enable NPY ruff rules --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a0dc7aa60..3e04f4aad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -138,13 +138,14 @@ extend-select = [ # "EXE", # flake8-executable # "C4", # flake8-comprehensions "YTT", # flake8-2020 - "S", # flake8-bandit + "S", # flake8-bandit # "DTZ", # flake8-datetimez # "RSE", # flake8-raise # "TCH", # flake8-type-checking "PTH", # flake8-use-pathlib # "RUF", # ruff "ASYNC", # flake8-async + "NPY", # numpy ] extend-ignore = [ @@ -155,6 +156,7 @@ extend-ignore = [ "S603", # `subprocess` call: check for execution of untrusted input "S607", # Starting a process with a partial executable path "S608", # Possible SQL injection vector through string-based query construction + "NPY002", # Numpy legacy random generator ] [tool.ruff.lint.mccabe] From deeabbca12e1c6f84f01f39d91c8ff33943f7b52 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jul 2024 08:49:27 +0200 Subject: [PATCH 11/74] chore: Don't override builtins --- freqtrade/exchange/exchange.py | 8 ++++---- freqtrade/freqai/tensorboard/tensorboard.py | 4 ++-- freqtrade/optimize/backtesting.py | 18 +++++++++--------- freqtrade/persistence/models.py | 8 ++++---- freqtrade/persistence/trade_model.py | 2 +- freqtrade/resolvers/iresolver.py | 4 ++-- freqtrade/resolvers/strategy_resolver.py | 4 ++-- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9be949ff5..62f0ca4de 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2065,7 +2065,7 @@ class Exchange: def get_fee( self, symbol: str, - type: str = "", + order_type: str = "", side: str = "", amount: float = 1, price: float = 1, @@ -2074,13 +2074,13 @@ class Exchange: """ Retrieve fee from exchange :param symbol: Pair - :param type: Type of order (market, limit, ...) + :param order_type: Type of order (market, limit, ...) :param side: Side of order (buy, sell) :param amount: Amount of order :param price: Price of order :param taker_or_maker: 'maker' or 'taker' (ignored if "type" is provided) """ - if type and type == "market": + if order_type and order_type == "market": taker_or_maker = "taker" try: if self._config["dry_run"] and self._config.get("fee", None) is not None: @@ -2091,7 +2091,7 @@ class Exchange: return self._api.calculate_fee( symbol=symbol, - type=type, + type=order_type, side=side, amount=amount, price=price, diff --git a/freqtrade/freqai/tensorboard/tensorboard.py b/freqtrade/freqai/tensorboard/tensorboard.py index 3a306f377..81f48047e 100644 --- a/freqtrade/freqai/tensorboard/tensorboard.py +++ b/freqtrade/freqai/tensorboard/tensorboard.py @@ -45,10 +45,10 @@ class TensorBoardCallback(BaseTensorBoardCallback): return False evals = ["validation", "train"] - for metric, eval in zip(evals_log.items(), evals): + for metric, eval_ in zip(evals_log.items(), evals): for metric_name, log in metric[1].items(): score = log[-1][0] if isinstance(log[-1], tuple) else log[-1] - self.writer.add_scalar(f"{eval}-{metric_name}", score, epoch) + self.writer.add_scalar(f"{eval_}-{metric_name}", score, epoch) return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a71ec170d..c28c080f5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -465,25 +465,25 @@ class Backtesting: return data def _get_close_rate( - self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple, trade_dur: int + self, row: Tuple, trade: LocalTrade, exit_: ExitCheckTuple, trade_dur: int ) -> float: """ Get close rate for backtesting result """ # Special handling if high or low hit STOP_LOSS or ROI - if exit.exit_type in ( + if exit_.exit_type in ( ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION, ): - return self._get_close_rate_for_stoploss(row, trade, exit, trade_dur) - elif exit.exit_type == (ExitType.ROI): - return self._get_close_rate_for_roi(row, trade, exit, trade_dur) + return self._get_close_rate_for_stoploss(row, trade, exit_, trade_dur) + elif exit_.exit_type == (ExitType.ROI): + return self._get_close_rate_for_roi(row, trade, exit_, trade_dur) else: return row[OPEN_IDX] def _get_close_rate_for_stoploss( - self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple, trade_dur: int + self, row: Tuple, trade: LocalTrade, exit_: ExitCheckTuple, trade_dur: int ) -> float: # our stoploss was already lower than candle high, # possibly due to a cancelled trade exit. @@ -491,7 +491,7 @@ class Backtesting: is_short = trade.is_short or False leverage = trade.leverage or 1.0 side_1 = -1 if is_short else 1 - if exit.exit_type == ExitType.LIQUIDATION and trade.liquidation_price: + if exit_.exit_type == ExitType.LIQUIDATION and trade.liquidation_price: stoploss_value = trade.liquidation_price else: stoploss_value = trade.stop_loss @@ -506,7 +506,7 @@ class Backtesting: # Special case: trailing triggers within same candle as trade opened. Assume most # pessimistic price movement, which is moving just enough to arm stoploss and # immediately going down to stop price. - if exit.exit_type == ExitType.TRAILING_STOP_LOSS and trade_dur == 0: + if exit_.exit_type == ExitType.TRAILING_STOP_LOSS and trade_dur == 0: if ( not self.strategy.use_custom_stoploss and self.strategy.trailing_stop @@ -537,7 +537,7 @@ class Backtesting: return stoploss_value def _get_close_rate_for_roi( - self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple, trade_dur: int + self, row: Tuple, trade: LocalTrade, exit_: ExitCheckTuple, trade_dur: int ) -> float: is_short = trade.is_short or False leverage = trade.leverage or 1.0 diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 261148baa..d5d5fd144 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -32,12 +32,12 @@ def get_request_or_thread_id() -> Optional[str]: """ Helper method to get either async context (for fastapi requests), or thread id """ - id = _request_id_ctx_var.get() - if id is None: + request_id = _request_id_ctx_var.get() + if request_id is None: # when not in request context - use thread id - id = str(threading.current_thread().ident) + request_id = str(threading.current_thread().ident) - return id + return request_id _SQL_DOCS_URL = "http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls" diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 7d803ea0f..e731f7552 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -2012,7 +2012,7 @@ class Trade(ModelBase, LocalTrade): ).all() resp: List[Dict] = [] - for id, enter_tag, exit_reason, profit, profit_abs, count in mix_tag_perf: + for _, enter_tag, exit_reason, profit, profit_abs, count in mix_tag_perf: enter_tag = enter_tag if enter_tag is not None else "Other" exit_reason = exit_reason if exit_reason is not None else "Other" diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index fc6ac5ec3..bac5c08a7 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -63,8 +63,8 @@ class IResolver: # Add extra directory to the top of the search paths if extra_dirs: - for dir in extra_dirs: - abs_paths.insert(0, Path(dir).resolve()) + for directory in extra_dirs: + abs_paths.insert(0, Path(directory).resolve()) if cls.extra_path and (extra := config.get(cls.extra_path)): abs_paths.insert(0, Path(extra).resolve()) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 72b1db034..d234a680f 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -311,9 +311,9 @@ def warn_deprecated_setting(strategy: IStrategy, old: str, new: str, error=False setattr(strategy, new, getattr(strategy, f"{old}")) -def check_override(object, parentclass, attribute): +def check_override(obj, parentclass, attribute: str): """ Checks if a object overrides the parent class attribute. :returns: True if the object is overridden. """ - return getattr(type(object), attribute) != getattr(parentclass, attribute) + return getattr(type(obj), attribute) != getattr(parentclass, attribute) From 4a4e6bc95248bf04310d4479764a79efe95288aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jul 2024 08:51:11 +0200 Subject: [PATCH 12/74] chore: fix flake8-builtins overrides --- freqtrade/freqai/RL/BaseEnvironment.py | 2 +- freqtrade/freqai/data_drawer.py | 20 ++++++++++---------- pyproject.toml | 1 + 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqai/RL/BaseEnvironment.py b/freqtrade/freqai/RL/BaseEnvironment.py index ba72c90ed..5ddfdeb68 100644 --- a/freqtrade/freqai/RL/BaseEnvironment.py +++ b/freqtrade/freqai/RL/BaseEnvironment.py @@ -52,7 +52,7 @@ class BaseEnvironment(gym.Env): reward_kwargs: dict = {}, window_size=10, starting_point=True, - id: str = "baseenv-1", + id: str = "baseenv-1", # noqa: A002 seed: int = 1, config: dict = {}, live: bool = False, diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 37780a945..124ed9e26 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -238,9 +238,9 @@ class FreqaiDataDrawer: metadata, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE ) - def np_encoder(self, object): - if isinstance(object, np.generic): - return object.item() + def np_encoder(self, obj): + if isinstance(obj, np.generic): + return obj.item() def get_pair_dict_info(self, pair: str) -> Tuple[str, int]: """ @@ -448,8 +448,8 @@ class FreqaiDataDrawer: delete_dict: Dict[str, Any] = {} - for dir in model_folders: - result = pattern.match(str(dir.name)) + for directory in model_folders: + result = pattern.match(str(directory.name)) if result is None: continue coin = result.group(1) @@ -458,10 +458,10 @@ class FreqaiDataDrawer: if coin not in delete_dict: delete_dict[coin] = {} delete_dict[coin]["num_folders"] = 1 - delete_dict[coin]["timestamps"] = {int(timestamp): dir} + delete_dict[coin]["timestamps"] = {int(timestamp): directory} else: delete_dict[coin]["num_folders"] += 1 - delete_dict[coin]["timestamps"][int(timestamp)] = dir + delete_dict[coin]["timestamps"][int(timestamp)] = directory for coin in delete_dict: if delete_dict[coin]["num_folders"] > num_keep: @@ -612,9 +612,9 @@ class FreqaiDataDrawer: elif self.model_type == "pytorch": import torch - zip = torch.load(dk.data_path / f"{dk.model_filename}_model.zip") - model = zip["pytrainer"] - model = model.load_from_checkpoint(zip) + zipfile = torch.load(dk.data_path / f"{dk.model_filename}_model.zip") + model = zipfile["pytrainer"] + model = model.load_from_checkpoint(zipfile) if not model: raise OperationalException( diff --git a/pyproject.toml b/pyproject.toml index 3e04f4aad..5918e92e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,6 +134,7 @@ extend-select = [ "W", # pycodestyle "UP", # pyupgrade "I", # isort + "A", # flake8-builtins "TID", # flake8-tidy-imports # "EXE", # flake8-executable # "C4", # flake8-comprehensions From 1e287de589031639ce7f9c1050a0d0f5f70faa28 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jul 2024 08:54:49 +0200 Subject: [PATCH 13/74] chore: fix mote A violations in tests and scripts --- ft_client/test_client/test_rest_client.py | 4 ++-- scripts/ws_client.py | 8 ++++---- tests/data/test_datahandler.py | 6 +++--- tests/exchange/test_binance.py | 6 +++--- tests/leverage/test_candletype.py | 12 ++++++------ 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/ft_client/test_client/test_rest_client.py b/ft_client/test_client/test_rest_client.py index 541577e59..1a709b9c5 100644 --- a/ft_client/test_client/test_rest_client.py +++ b/ft_client/test_client/test_rest_client.py @@ -133,8 +133,8 @@ def test_FtRestClient_call_invalid(caplog): ) def test_FtRestClient_call_explicit_methods(method, args, kwargs): client, mock = get_rest_client() - exec = getattr(client, method) - exec(*args, **kwargs) + executor = getattr(client, method) + executor(*args, **kwargs) assert mock.call_count == 1 diff --git a/scripts/ws_client.py b/scripts/ws_client.py index ec6df5742..81e539e26 100755 --- a/scripts/ws_client.py +++ b/scripts/ws_client.py @@ -172,10 +172,10 @@ class ClientProtocol: return readable_timedelta(time_delta) - async def _handle_whitelist(self, name, type, data): + async def _handle_whitelist(self, name, msgtype, data): self.logger.info(data) - async def _handle_analyzed_df(self, name, type, data): + async def _handle_analyzed_df(self, name, msgtype, data): key, la, df = data["key"], data["la"], data["df"] if not df.empty: @@ -189,8 +189,8 @@ class ClientProtocol: else: self.logger.info("Empty DataFrame") - async def _handle_default(self, name, type, data): - self.logger.info("Unknown message of type {type} received...") + async def _handle_default(self, name, msgtype, data): + self.logger.info("Unknown message of type {msgtype} received...") self.logger.info(data) diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index 1f66d1b1e..b8bb5661f 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -83,7 +83,7 @@ def test_datahandler_ohlcv_regex(filename, pair, timeframe, candletype): @pytest.mark.parametrize( - "input,expected", + "pair,expected", [ ("XMR_USDT", "XMR/USDT"), ("BTC_USDT", "BTC/USDT"), @@ -95,8 +95,8 @@ def test_datahandler_ohlcv_regex(filename, pair, timeframe, candletype): ("UNITTEST_USDT", "UNITTEST/USDT"), ], ) -def test_rebuild_pair_from_filename(input, expected): - assert IDataHandler.rebuild_pair_from_filename(input) == expected +def test_rebuild_pair_from_filename(pair, expected): + assert IDataHandler.rebuild_pair_from_filename(pair) == expected def test_datahandler_ohlcv_get_available_data(testdatadir): diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index b961c2809..8f9b801f3 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -12,7 +12,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers @pytest.mark.parametrize( - "side,type,time_in_force,expected", + "side,order_type,time_in_force,expected", [ ("buy", "limit", "gtc", {"timeInForce": "GTC"}), ("buy", "limit", "IOC", {"timeInForce": "IOC"}), @@ -22,9 +22,9 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers ("sell", "market", "PO", {}), ], ) -def test__get_params_binance(default_conf, mocker, side, type, time_in_force, expected): +def test__get_params_binance(default_conf, mocker, side, order_type, time_in_force, expected): exchange = get_patched_exchange(mocker, default_conf, id="binance") - assert exchange._get_params(side, type, 1, False, time_in_force) == expected + assert exchange._get_params(side, order_type, 1, False, time_in_force) == expected @pytest.mark.parametrize("trademode", [TradingMode.FUTURES, TradingMode.SPOT]) diff --git a/tests/leverage/test_candletype.py b/tests/leverage/test_candletype.py index a424012d7..8a2c4ba3e 100644 --- a/tests/leverage/test_candletype.py +++ b/tests/leverage/test_candletype.py @@ -4,7 +4,7 @@ from freqtrade.enums import CandleType @pytest.mark.parametrize( - "input,expected", + "candle_type,expected", [ ("", CandleType.SPOT), ("spot", CandleType.SPOT), @@ -17,17 +17,17 @@ from freqtrade.enums import CandleType ("premiumIndex", CandleType.PREMIUMINDEX), ], ) -def test_CandleType_from_string(input, expected): - assert CandleType.from_string(input) == expected +def test_CandleType_from_string(candle_type, expected): + assert CandleType.from_string(candle_type) == expected @pytest.mark.parametrize( - "input,expected", + "candle_type,expected", [ ("futures", CandleType.FUTURES), ("spot", CandleType.SPOT), ("margin", CandleType.SPOT), ], ) -def test_CandleType_get_default(input, expected): - assert CandleType.get_default(input) == expected +def test_CandleType_get_default(candle_type, expected): + assert CandleType.get_default(candle_type) == expected From 36a44575c7fce8e69f06aa1caba3ba3bc90ab621 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jul 2024 09:10:55 +0200 Subject: [PATCH 14/74] chore: update patch_exchange to not override builtins --- tests/commands/test_commands.py | 14 +++++++------- tests/conftest.py | 13 +++++++------ tests/data/test_download_data.py | 4 ++-- tests/data/test_trade_converter_kraken.py | 2 +- tests/freqtradebot/test_freqtradebot.py | 2 +- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index c98c6302e..02b234b6c 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -167,7 +167,7 @@ def test_list_timeframes(mocker, capsys): "1h": "hour", "1d": "day", } - patch_exchange(mocker, api_mock=api_mock, id="bybit") + patch_exchange(mocker, api_mock=api_mock, exchange="bybit") args = [ "list-timeframes", ] @@ -213,7 +213,7 @@ def test_list_timeframes(mocker, capsys): "1d": "1d", "3d": "3d", } - patch_exchange(mocker, api_mock=api_mock, id="binance") + patch_exchange(mocker, api_mock=api_mock, exchange="binance") # Test with --exchange binance args = [ "list-timeframes", @@ -258,7 +258,7 @@ def test_list_timeframes(mocker, capsys): def test_list_markets(mocker, markets_static, capsys): api_mock = MagicMock() - patch_exchange(mocker, api_mock=api_mock, id="binance", mock_markets=markets_static) + patch_exchange(mocker, api_mock=api_mock, exchange="binance", mock_markets=markets_static) # Test with no --config args = [ @@ -286,7 +286,7 @@ def test_list_markets(mocker, markets_static, capsys): "LTC/ETH, LTC/USD, NEO/BTC, TKN/BTC, XLTCUSDT, XRP/BTC.\n" in captured.out ) - patch_exchange(mocker, api_mock=api_mock, id="binance", mock_markets=markets_static) + patch_exchange(mocker, api_mock=api_mock, exchange="binance", mock_markets=markets_static) # Test with --exchange args = ["list-markets", "--exchange", "binance"] pargs = get_args(args) @@ -295,7 +295,7 @@ def test_list_markets(mocker, markets_static, capsys): captured = capsys.readouterr() assert re.match("\nExchange Binance has 12 active markets:\n", captured.out) - patch_exchange(mocker, api_mock=api_mock, id="binance", mock_markets=markets_static) + patch_exchange(mocker, api_mock=api_mock, exchange="binance", mock_markets=markets_static) # Test with --all: all markets args = [ "list-markets", @@ -823,7 +823,7 @@ def test_download_data_no_markets(mocker, caplog): "freqtrade.data.history.history_utils.refresh_backtest_ohlcv_data", MagicMock(return_value=["ETH/BTC", "XRP/BTC"]), ) - patch_exchange(mocker, id="binance") + patch_exchange(mocker, exchange="binance") mocker.patch(f"{EXMS}.get_markets", return_value={}) args = [ "download-data", @@ -952,7 +952,7 @@ def test_download_data_trades(mocker): def test_download_data_data_invalid(mocker): - patch_exchange(mocker, id="kraken") + patch_exchange(mocker, exchange="kraken") mocker.patch(f"{EXMS}.get_markets", return_value={}) args = [ "download-data", diff --git a/tests/conftest.py b/tests/conftest.py index c4964c414..71eca24c4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -137,7 +137,7 @@ def generate_trades_history(n_rows, start_date: Optional[datetime] = None, days= random_timestamps_in_seconds = np.random.uniform(_start_timestamp, _end_timestamp, n_rows) timestamp = pd.to_datetime(random_timestamps_in_seconds, unit="s") - id = [ + trade_id = [ f"a{np.random.randint(1e6, 1e7 - 1)}cd{np.random.randint(100, 999)}" for _ in range(n_rows) ] @@ -155,7 +155,7 @@ def generate_trades_history(n_rows, start_date: Optional[datetime] = None, days= df = pd.DataFrame( { "timestamp": timestamp, - "id": id, + "id": trade_id, "type": None, "side": side, "price": price, @@ -236,12 +236,12 @@ def patched_configuration_load_config_file(mocker, config) -> None: def patch_exchange( - mocker, api_mock=None, id="binance", mock_markets=True, mock_supported_modes=True + mocker, api_mock=None, exchange="binance", mock_markets=True, mock_supported_modes=True ) -> None: mocker.patch(f"{EXMS}.validate_config", MagicMock()) mocker.patch(f"{EXMS}.validate_timeframes", MagicMock()) - mocker.patch(f"{EXMS}.id", PropertyMock(return_value=id)) - mocker.patch(f"{EXMS}.name", PropertyMock(return_value=id.title())) + mocker.patch(f"{EXMS}.id", PropertyMock(return_value=exchange)) + mocker.patch(f"{EXMS}.name", PropertyMock(return_value=exchange.title())) mocker.patch(f"{EXMS}.precisionMode", PropertyMock(return_value=2)) # Temporary patch ... mocker.patch("freqtrade.exchange.bybit.Bybit.cache_leverage_tiers") @@ -254,7 +254,8 @@ def patch_exchange( if mock_supported_modes: mocker.patch( - f"freqtrade.exchange.{id}.{id.capitalize()}._supported_trading_mode_margin_pairs", + f"freqtrade.exchange.{exchange}.{exchange.capitalize()}" + "._supported_trading_mode_margin_pairs", PropertyMock( return_value=[ (TradingMode.MARGIN, MarginMode.CROSS), diff --git a/tests/data/test_download_data.py b/tests/data/test_download_data.py index 08d56458f..f2c8a51d4 100644 --- a/tests/data/test_download_data.py +++ b/tests/data/test_download_data.py @@ -14,7 +14,7 @@ def test_download_data_main_no_markets(mocker, caplog): "freqtrade.data.history.history_utils.refresh_backtest_ohlcv_data", MagicMock(return_value=["ETH/BTC", "XRP/BTC"]), ) - patch_exchange(mocker, id="binance") + patch_exchange(mocker, exchange="binance") mocker.patch(f"{EXMS}.get_markets", return_value={}) config = setup_utils_configuration({"exchange": "binance"}, RunMode.UTIL_EXCHANGE) config.update({"days": 20, "pairs": ["ETH/BTC", "XRP/BTC"], "timeframes": ["5m", "1h"]}) @@ -91,7 +91,7 @@ def test_download_data_main_trades(mocker): def test_download_data_main_data_invalid(mocker): - patch_exchange(mocker, id="kraken") + patch_exchange(mocker, exchange="kraken") mocker.patch(f"{EXMS}.get_markets", return_value={}) config = setup_utils_configuration({"exchange": "kraken"}, RunMode.UTIL_EXCHANGE) config.update( diff --git a/tests/data/test_trade_converter_kraken.py b/tests/data/test_trade_converter_kraken.py index 480ea93ca..d8d20fd88 100644 --- a/tests/data/test_trade_converter_kraken.py +++ b/tests/data/test_trade_converter_kraken.py @@ -17,7 +17,7 @@ def test_import_kraken_trades_from_csv(testdatadir, tmp_path, caplog, default_co default_conf_usdt["exchange"]["name"] = "kraken" - patch_exchange(mocker, id="kraken") + patch_exchange(mocker, exchange="kraken") mocker.patch( f"{EXMS}.markets", PropertyMock( diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index 79224ddec..23dfbb785 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -915,7 +915,7 @@ def test_execute_entry( default_conf_usdt["margin_mode"] = margin_mode mocker.patch("freqtrade.exchange.gate.Gate.validate_ordertypes") patch_RPCManager(mocker) - patch_exchange(mocker, id=exchange_name) + patch_exchange(mocker, exchange=exchange_name) freqtrade = FreqtradeBot(default_conf_usdt) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) freqtrade.strategy.leverage = MagicMock(return_value=leverage) From 8b50973411628f1b6b816cff504893ce15380c8e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jul 2024 09:11:58 +0200 Subject: [PATCH 15/74] chore: Fix misuse of asyncio.sleep in test --- tests/exchange/test_exchange_ws.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/exchange/test_exchange_ws.py b/tests/exchange/test_exchange_ws.py index ec238000a..8b97ae1b8 100644 --- a/tests/exchange/test_exchange_ws.py +++ b/tests/exchange/test_exchange_ws.py @@ -56,11 +56,11 @@ async def test_exchangews_ohlcv(mocker): assert exchange_ws._klines_scheduled == set() exchange_ws.schedule_ohlcv("ETH/BTC", "1m", CandleType.SPOT) - asyncio.sleep(0.5) + await asyncio.sleep(0.5) assert exchange_ws._klines_watching == {("ETH/BTC", "1m", CandleType.SPOT)} assert exchange_ws._klines_scheduled == {("ETH/BTC", "1m", CandleType.SPOT)} - asyncio.sleep(0.1) + await asyncio.sleep(0.1) assert ccxt_object.watch_ohlcv.call_count == 1 except Exception as e: print(e) From f3e2dcd34205a82c1dfabdaab2aa8f4022564ef1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jul 2024 09:18:14 +0200 Subject: [PATCH 16/74] chore: update get_patched_exchange argument --- tests/conftest.py | 6 +- tests/data/test_dataprovider.py | 2 +- tests/data/test_history.py | 2 +- tests/exchange/test_binance.py | 18 +-- tests/exchange/test_bitpanda.py | 2 +- tests/exchange/test_bybit.py | 18 +-- tests/exchange/test_exchange.py | 242 ++++++++++++++++---------------- tests/exchange/test_gate.py | 10 +- tests/exchange/test_htx.py | 2 +- tests/exchange/test_kraken.py | 8 +- tests/exchange/test_kucoin.py | 4 +- tests/exchange/test_okx.py | 30 ++-- 12 files changed, 172 insertions(+), 172 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 71eca24c4..fee8cab72 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -275,10 +275,10 @@ def patch_exchange( def get_patched_exchange( - mocker, config, api_mock=None, id="binance", mock_markets=True, mock_supported_modes=True + mocker, config, api_mock=None, exchange="binance", mock_markets=True, mock_supported_modes=True ) -> Exchange: - patch_exchange(mocker, api_mock, id, mock_markets, mock_supported_modes) - config["exchange"]["name"] = id + patch_exchange(mocker, api_mock, exchange, mock_markets, mock_supported_modes) + config["exchange"]["name"] = exchange try: exchange = ExchangeResolver.load_exchange(config, load_leverage_tiers=True) except ImportError: diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 11c69f918..e7bd62df5 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -250,7 +250,7 @@ def test_refresh(mocker, default_conf): refresh_mock = MagicMock() mocker.patch(f"{EXMS}.refresh_latest_ohlcv", refresh_mock) - exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange = get_patched_exchange(mocker, default_conf, exchange="binance") timeframe = default_conf["timeframe"] pairs = [("XRP/BTC", timeframe), ("UNITTEST/BTC", timeframe)] diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 29ac89337..e3f92b1f9 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -555,7 +555,7 @@ def test_refresh_backtest_ohlcv_data( mocker.patch.object(Path, "unlink", MagicMock()) default_conf["trading_mode"] = trademode - ex = get_patched_exchange(mocker, default_conf, id="bybit") + ex = get_patched_exchange(mocker, default_conf, exchange="bybit") timerange = TimeRange.parse_timerange("20190101-20190102") refresh_backtest_ohlcv_data( exchange=ex, diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 8f9b801f3..1de464097 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -23,7 +23,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers ], ) def test__get_params_binance(default_conf, mocker, side, order_type, time_in_force, expected): - exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange = get_patched_exchange(mocker, default_conf, exchange="binance") assert exchange._get_params(side, order_type, 1, False, time_in_force) == expected @@ -159,7 +159,7 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker): "sl1,sl2,sl3,side", [(1501, 1499, 1501, "sell"), (1499, 1501, 1499, "buy")] ) def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): - exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange = get_patched_exchange(mocker, default_conf, exchange="binance") order = { "type": "stop_loss_limit", "price": 1500, @@ -378,7 +378,7 @@ def test_fill_leverage_tiers_binance(default_conf, mocker): default_conf["dry_run"] = False default_conf["trading_mode"] = TradingMode.FUTURES default_conf["margin_mode"] = MarginMode.ISOLATED - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="binance") exchange.fill_leverage_tiers() assert exchange._leverage_tiers == { @@ -497,7 +497,7 @@ def test_fill_leverage_tiers_binance_dryrun(default_conf, mocker, leverage_tiers api_mock = MagicMock() default_conf["trading_mode"] = TradingMode.FUTURES default_conf["margin_mode"] = MarginMode.ISOLATED - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="binance") exchange.fill_leverage_tiers() assert len(exchange._leverage_tiers.keys()) > 100 for key, value in leverage_tiers.items(): @@ -518,10 +518,10 @@ def test_additional_exchange_init_binance(default_conf, mocker): OperationalException, match=r"Hedge Mode is not supported.*\nMulti-Asset Mode is not supported.*", ): - get_patched_exchange(mocker, default_conf, id="binance", api_mock=api_mock) + get_patched_exchange(mocker, default_conf, exchange="binance", api_mock=api_mock) api_mock.fapiPrivateGetPositionSideDual = MagicMock(return_value={"dualSidePosition": False}) api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock(return_value={"multiAssetsMargin": False}) - exchange = get_patched_exchange(mocker, default_conf, id="binance", api_mock=api_mock) + exchange = get_patched_exchange(mocker, default_conf, exchange="binance", api_mock=api_mock) assert exchange ccxt_exceptionhandlers( mocker, @@ -541,7 +541,7 @@ def test__set_leverage_binance(mocker, default_conf): default_conf["trading_mode"] = TradingMode.FUTURES default_conf["margin_mode"] = MarginMode.ISOLATED - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="binance") exchange._set_leverage(3.2, "BTC/USDT:USDT") assert api_mock.set_leverage.call_count == 1 # Leverage is rounded to 3. @@ -574,7 +574,7 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c ] ] - exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange = get_patched_exchange(mocker, default_conf, exchange="binance") # Monkey-patch async function exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) @@ -620,7 +620,7 @@ def test_get_maintenance_ratio_and_amt_binance( amt, ): mocker.patch(f"{EXMS}.exchange_has", return_value=True) - exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange = get_patched_exchange(mocker, default_conf, exchange="binance") exchange._leverage_tiers = leverage_tiers (result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, nominal_value) assert (round(result_ratio, 8), round(result_amt, 8)) == (mm_ratio, amt) diff --git a/tests/exchange/test_bitpanda.py b/tests/exchange/test_bitpanda.py index 83561b914..b007bea5c 100644 --- a/tests/exchange/test_bitpanda.py +++ b/tests/exchange/test_bitpanda.py @@ -39,7 +39,7 @@ def test_get_trades_for_order(default_conf, mocker): } ] ) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) orders = exchange.get_trades_for_order(order_id, "LTC/BTC", since) assert len(orders) == 1 diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py index 8f09b049d..8dc11667c 100644 --- a/tests/exchange/test_bybit.py +++ b/tests/exchange/test_bybit.py @@ -18,7 +18,7 @@ def test_additional_exchange_init_bybit(default_conf, mocker, caplog): api_mock.set_position_mode = MagicMock(return_value={"dualSidePosition": False}) api_mock.is_unified_enabled = MagicMock(return_value=[False, False]) - exchange = get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock) + exchange = get_patched_exchange(mocker, default_conf, exchange="bybit", api_mock=api_mock) assert api_mock.set_position_mode.call_count == 1 assert api_mock.is_unified_enabled.call_count == 1 assert exchange.unified_account is False @@ -28,9 +28,9 @@ def test_additional_exchange_init_bybit(default_conf, mocker, caplog): api_mock.set_position_mode.reset_mock() api_mock.is_unified_enabled = MagicMock(return_value=[False, True]) with pytest.raises(OperationalException, match=r"Bybit: Unified account is not supported.*"): - get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock) + get_patched_exchange(mocker, default_conf, exchange="bybit", api_mock=api_mock) assert log_has("Bybit: Unified account.", caplog) - # exchange = get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock) + # exchange = get_patched_exchange(mocker, default_conf, exchange="bybit", api_mock=api_mock) # assert api_mock.set_position_mode.call_count == 1 # assert api_mock.is_unified_enabled.call_count == 1 # assert exchange.unified_account is True @@ -45,7 +45,7 @@ async def test_bybit_fetch_funding_rate(default_conf, mocker): default_conf["margin_mode"] = "isolated" api_mock = MagicMock() api_mock.fetch_funding_rate_history = get_mock_coro(return_value=[]) - exchange = get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock) + exchange = get_patched_exchange(mocker, default_conf, exchange="bybit", api_mock=api_mock) limit = 200 # Test fetch_funding_rate_history (current data) await exchange._fetch_funding_rate_history( @@ -77,14 +77,14 @@ async def test_bybit_fetch_funding_rate(default_conf, mocker): def test_bybit_get_funding_fees(default_conf, mocker): now = datetime.now(timezone.utc) - exchange = get_patched_exchange(mocker, default_conf, id="bybit") + exchange = get_patched_exchange(mocker, default_conf, exchange="bybit") exchange._fetch_and_calculate_funding_fees = MagicMock() exchange.get_funding_fees("BTC/USDT:USDT", 1, False, now) assert exchange._fetch_and_calculate_funding_fees.call_count == 0 default_conf["trading_mode"] = "futures" default_conf["margin_mode"] = "isolated" - exchange = get_patched_exchange(mocker, default_conf, id="bybit") + exchange = get_patched_exchange(mocker, default_conf, exchange="bybit") exchange._fetch_and_calculate_funding_fees = MagicMock() exchange.get_funding_fees("BTC/USDT:USDT", 1, False, now) @@ -105,13 +105,13 @@ def test_bybit_fetch_orders(default_conf, mocker, limit_order): mocker.patch(f"{EXMS}.exchange_has", return_value=True) start_time = datetime.now(timezone.utc) - timedelta(days=20) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="bybit") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="bybit") # Not available in dry-run assert exchange.fetch_orders("mocked", start_time) == [] assert api_mock.fetch_orders.call_count == 0 default_conf["dry_run"] = False - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="bybit") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="bybit") res = exchange.fetch_orders("mocked", start_time) # Bybit will call the endpoint 3 times, as it has a limit of 7 days per call assert api_mock.fetch_orders.call_count == 3 @@ -136,7 +136,7 @@ def test_bybit_fetch_order_canceled_empty(default_conf_usdt, mocker): ) mocker.patch(f"{EXMS}.exchange_has", return_value=True) - exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, id="bybit") + exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, exchange="bybit") res = exchange.fetch_order("123", "BTC/USDT") assert res["remaining"] is None diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 6c3d62dde..423c9b58e 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -117,19 +117,19 @@ def ccxt_exceptionhandlers( with patch("freqtrade.exchange.common.time.sleep"): with pytest.raises(DDosProtection): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DDos")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.OperationFailed("DeaDBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(OperationalException): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 @@ -303,7 +303,7 @@ def test_exchange_resolver(default_conf, mocker, caplog): def test_validate_order_time_in_force(default_conf, mocker, caplog): caplog.set_level(logging.INFO) # explicitly test bybit, exchanges implementing other policies need separate tests - ex = get_patched_exchange(mocker, default_conf, id="bybit") + ex = get_patched_exchange(mocker, default_conf, exchange="bybit") tif = { "buy": "gtc", "sell": "gtc", @@ -345,7 +345,7 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog): ) def test_price_get_one_pip(default_conf, mocker, price, precision_mode, precision, expected): markets = PropertyMock(return_value={"ETH/BTC": {"precision": {"price": precision}}}) - exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange = get_patched_exchange(mocker, default_conf, exchange="binance") mocker.patch(f"{EXMS}.markets", markets) mocker.patch(f"{EXMS}.precisionMode", PropertyMock(return_value=precision_mode)) pair = "ETH/BTC" @@ -353,7 +353,7 @@ def test_price_get_one_pip(default_conf, mocker, price, precision_mode, precisio def test__get_stake_amount_limit(mocker, default_conf) -> None: - exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange = get_patched_exchange(mocker, default_conf, exchange="binance") stoploss = -0.05 markets = {"ETH/BTC": {"symbol": "ETH/BTC"}} @@ -462,7 +462,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: markets["ETH/BTC"]["contractSize"] = "0.01" default_conf["trading_mode"] = "futures" default_conf["margin_mode"] = "isolated" - exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange = get_patched_exchange(mocker, default_conf, exchange="binance") mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets)) # Contract size 0.01 @@ -483,7 +483,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: - exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange = get_patched_exchange(mocker, default_conf, exchange="binance") stoploss = -0.05 markets = {"ETH/BTC": {"symbol": "ETH/BTC"}} @@ -564,7 +564,7 @@ def test_reload_markets(default_conf, mocker, caplog, time_machine): api_mock.load_markets = get_mock_coro(return_value=initial_markets) default_conf["exchange"]["markets_refresh_interval"] = 10 exchange = get_patched_exchange( - mocker, default_conf, api_mock, id="binance", mock_markets=False + mocker, default_conf, api_mock, exchange="binance", mock_markets=False ) lam_spy = mocker.spy(exchange, "_load_async_markets") assert exchange._last_markets_refresh == dt_ts() @@ -599,7 +599,7 @@ def test_reload_markets_exception(default_conf, mocker, caplog): api_mock.load_markets = get_mock_coro(side_effect=ccxt.NetworkError("LoadError")) default_conf["exchange"]["markets_refresh_interval"] = 10 exchange = get_patched_exchange( - mocker, default_conf, api_mock, id="binance", mock_markets=False + mocker, default_conf, api_mock, exchange="binance", mock_markets=False ) exchange._last_markets_refresh = 2 @@ -1152,7 +1152,7 @@ def test_exchange_has(default_conf, mocker): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_create_dry_run_order(default_conf, mocker, side, exchange_name, leverage): default_conf["dry_run"] = True - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) order = exchange.create_dry_run_order( pair="ETH/BTC", ordertype="limit", side=side, amount=1, rate=200, leverage=leverage @@ -1246,7 +1246,7 @@ def test_create_dry_run_order_limit_fill( leverage, ): default_conf["dry_run"] = True - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) mocker.patch.multiple( EXMS, exchange_has=MagicMock(return_value=True), @@ -1315,7 +1315,7 @@ def test_create_dry_run_order_market_fill( default_conf, mocker, side, rate, amount, endprice, exchange_name, order_book_l2_usd, leverage ): default_conf["dry_run"] = True - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) mocker.patch.multiple( EXMS, exchange_has=MagicMock(return_value=True), @@ -1364,7 +1364,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, default_conf["margin_mode"] = "isolated" mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y) mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange._set_leverage = MagicMock() exchange.set_margin_mode = MagicMock() @@ -1392,7 +1392,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, "amount": 1, } ) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.trading_mode = TradingMode.FUTURES exchange._set_leverage = MagicMock() exchange.set_margin_mode = MagicMock() @@ -1411,7 +1411,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_buy_dry_run(default_conf, mocker, exchange_name): default_conf["dry_run"] = True - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) order = exchange.create_order( pair="ETH/BTC", @@ -1439,7 +1439,7 @@ def test_buy_prod(default_conf, mocker, exchange_name): default_conf["dry_run"] = False mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y) mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) order = exchange.create_order( pair="ETH/BTC", @@ -1483,7 +1483,7 @@ def test_buy_prod(default_conf, mocker, exchange_name): # test exception handling with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("Not enough funds")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.create_order( pair="ETH/BTC", ordertype=order_type, @@ -1496,7 +1496,7 @@ def test_buy_prod(default_conf, mocker, exchange_name): with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.create_order( pair="ETH/BTC", ordertype="limit", @@ -1509,7 +1509,7 @@ def test_buy_prod(default_conf, mocker, exchange_name): with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.create_order( pair="ETH/BTC", ordertype="market", @@ -1522,7 +1522,7 @@ def test_buy_prod(default_conf, mocker, exchange_name): with pytest.raises(TemporaryError): api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("Network disconnect")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.create_order( pair="ETH/BTC", ordertype=order_type, @@ -1535,7 +1535,7 @@ def test_buy_prod(default_conf, mocker, exchange_name): with pytest.raises(OperationalException): api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("Unknown error")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.create_order( pair="ETH/BTC", ordertype=order_type, @@ -1558,7 +1558,7 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): default_conf["dry_run"] = False mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y) mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) order_type = "limit" time_in_force = "ioc" @@ -1637,7 +1637,7 @@ def test_sell_prod(default_conf, mocker, exchange_name): mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y) mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) order = exchange.create_order( pair="ETH/BTC", ordertype=order_type, side="sell", amount=1, rate=200, leverage=1.0 @@ -1669,14 +1669,14 @@ def test_sell_prod(default_conf, mocker, exchange_name): # test exception handling with pytest.raises(InsufficientFundsError): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.create_order( pair="ETH/BTC", ordertype=order_type, side="sell", amount=1, rate=200, leverage=1.0 ) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.create_order( pair="ETH/BTC", ordertype="limit", side="sell", amount=1, rate=200, leverage=1.0 ) @@ -1684,21 +1684,21 @@ def test_sell_prod(default_conf, mocker, exchange_name): # Market orders don't require price, so the behaviour is slightly different with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.create_order( pair="ETH/BTC", ordertype="market", side="sell", amount=1, rate=200, leverage=1.0 ) with pytest.raises(TemporaryError): api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No Connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.create_order( pair="ETH/BTC", ordertype=order_type, side="sell", amount=1, rate=200, leverage=1.0 ) with pytest.raises(OperationalException): api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.create_order( pair="ETH/BTC", ordertype=order_type, side="sell", amount=1, rate=200, leverage=1.0 ) @@ -1715,7 +1715,7 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): default_conf["dry_run"] = False mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y) mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) order_type = "limit" time_in_force = "ioc" @@ -1777,7 +1777,7 @@ def test_get_balances_prod(default_conf, mocker, exchange_name): return_value={"1ST": balance_item, "2ND": balance_item, "3RD": balance_item} ) default_conf["dry_run"] = False - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) assert len(exchange.get_balances()) == 3 assert exchange.get_balances()["1ST"]["free"] == 10.0 assert exchange.get_balances()["1ST"]["total"] == 10.0 @@ -1798,12 +1798,12 @@ def test_fetch_positions(default_conf, mocker, exchange_name): {"symbol": "XRP/USDT:USDT", "leverage": 5}, ] ) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) assert exchange.fetch_positions() == [] default_conf["dry_run"] = False default_conf["trading_mode"] = "futures" - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) res = exchange.fetch_positions() assert len(res) == 2 @@ -1830,13 +1830,13 @@ def test_fetch_orders(default_conf, mocker, exchange_name, limit_order): if exchange_name == "bybit": expected = 3 - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) # Not available in dry-run assert exchange.fetch_orders("mocked", start_time) == [] assert api_mock.fetch_orders.call_count == 0 default_conf["dry_run"] = False - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) res = exchange.fetch_orders("mocked", start_time) assert api_mock.fetch_orders.call_count == expected assert api_mock.fetch_open_orders.call_count == 0 @@ -1937,7 +1937,7 @@ def test_fetch_trading_fees(default_conf, mocker): default_conf["margin_mode"] = MarginMode.ISOLATED api_mock.fetch_trading_fees = MagicMock(return_value=tick) mocker.patch(f"{EXMS}.exchange_has", return_value=True) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) assert "1INCH/USDT:USDT" in exchange._trading_fees assert "ETH/USDT:USDT" in exchange._trading_fees @@ -1952,7 +1952,7 @@ def test_fetch_trading_fees(default_conf, mocker): ) api_mock.fetch_trading_fees = MagicMock(return_value={}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.fetch_trading_fees() mocker.patch(f"{EXMS}.exchange_has", return_value=True) assert exchange.fetch_trading_fees() == {} @@ -1977,7 +1977,7 @@ def test_fetch_bids_asks(default_conf, mocker): exchange_name = "binance" api_mock.fetch_bids_asks = MagicMock(return_value=tick) mocker.patch(f"{EXMS}.exchange_has", return_value=True) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) # retrieve original ticker bidsasks = exchange.fetch_bids_asks() @@ -2004,11 +2004,11 @@ def test_fetch_bids_asks(default_conf, mocker): with pytest.raises(OperationalException): api_mock.fetch_bids_asks = MagicMock(side_effect=ccxt.NotSupported("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.fetch_bids_asks() api_mock.fetch_bids_asks = MagicMock(return_value={}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.fetch_bids_asks() mocker.patch(f"{EXMS}.exchange_has", return_value=True) assert exchange.fetch_bids_asks() == {} @@ -2034,7 +2034,7 @@ def test_get_tickers(default_conf, mocker, exchange_name, caplog): mocker.patch(f"{EXMS}.exchange_has", return_value=True) api_mock.fetch_tickers = MagicMock(return_value=tick) api_mock.fetch_bids_asks = MagicMock(return_value={}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) # retrieve original ticker tickers = exchange.get_tickers() @@ -2064,19 +2064,19 @@ def test_get_tickers(default_conf, mocker, exchange_name, caplog): with pytest.raises(OperationalException): api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.get_tickers() caplog.clear() api_mock.fetch_tickers = MagicMock(side_effect=[ccxt.BadSymbol("SomeSymbol"), []]) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) x = exchange.get_tickers() assert x == [] assert log_has_re(r"Could not load tickers due to BadSymbol\..*SomeSymbol", caplog) caplog.clear() api_mock.fetch_tickers = MagicMock(return_value={}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.get_tickers() api_mock.fetch_tickers.reset_mock() @@ -2084,7 +2084,7 @@ def test_get_tickers(default_conf, mocker, exchange_name, caplog): default_conf["trading_mode"] = TradingMode.FUTURES default_conf["margin_mode"] = MarginMode.ISOLATED mocker.patch(f"{EXMS}.exchange_has", return_value=True) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.get_tickers() assert api_mock.fetch_tickers.call_count == 1 @@ -2107,7 +2107,7 @@ def test_fetch_ticker(default_conf, mocker, exchange_name): } api_mock.fetch_ticker = MagicMock(return_value=tick) api_mock.markets = {"ETH/BTC": {"active": True}} - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) # retrieve original ticker ticker = exchange.fetch_ticker(pair="ETH/BTC") @@ -2122,7 +2122,7 @@ def test_fetch_ticker(default_conf, mocker, exchange_name): "last": 42, } api_mock.fetch_ticker = MagicMock(return_value=tick) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) # if not caching the result we should get the same ticker # if not fetching a new result we should get the cached ticker @@ -2143,7 +2143,7 @@ def test_fetch_ticker(default_conf, mocker, exchange_name): ) api_mock.fetch_ticker = MagicMock(return_value={}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.fetch_ticker(pair="ETH/BTC") with pytest.raises(DependencyException, match=r"Pair XRP/ETH not available"): @@ -2152,7 +2152,7 @@ def test_fetch_ticker(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test___now_is_time_to_refresh(default_conf, mocker, exchange_name, time_machine): - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) pair = "BTC/USDT" candle_type = CandleType.SPOT start_dt = datetime(2023, 12, 1, 0, 10, 0, tzinfo=timezone.utc) @@ -2181,7 +2181,7 @@ def test___now_is_time_to_refresh(default_conf, mocker, exchange_name, time_mach @pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("candle_type", ["mark", ""]) def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_type): - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) ohlcv = [ [ dt_ts(), # unix timestamp ms @@ -2236,7 +2236,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_ 5, # volume (in quote currency) ] ] - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) # Monkey-patch async function exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) @@ -2538,7 +2538,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ ] caplog.set_level(logging.DEBUG) - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) # Monkey-patch async function exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) @@ -2570,7 +2570,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ OperationalException, match=r"Could not fetch historical candle \(OHLCV\) data.*" ): api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) await exchange._async_get_candle_history( pair, "5m", CandleType.SPOT, dt_ts(dt_now() - timedelta(seconds=2000)) ) @@ -2582,7 +2582,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ match=r"Exchange.* does not support fetching " r"historical candle \(OHLCV\) data\..*", ): api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) await exchange._async_get_candle_history( pair, "5m", CandleType.SPOT, dt_ts(dt_now() - timedelta(seconds=2000)) ) @@ -2603,7 +2603,7 @@ async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog): '{"code":"429000","msg":"Too Many Requests"}' ) ) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kucoin") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="kucoin") mocker.patch(f"{EXMS}.name", PropertyMock(return_value="KuCoin")) msg = "Kucoin 429 error, avoid triggering DDosProtection backoff delay" @@ -2725,7 +2725,7 @@ def test_fetch_l2_order_book(default_conf, mocker, order_book_l2, exchange_name) api_mock = MagicMock() api_mock.fetch_l2_order_book = order_book_l2 - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) order_book = exchange.fetch_l2_order_book(pair="ETH/BTC", limit=10) assert "bids" in order_book assert "asks" in order_book @@ -2753,15 +2753,15 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name): api_mock = MagicMock() with pytest.raises(OperationalException): api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported("Not supported")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.fetch_l2_order_book(pair="ETH/BTC", limit=50) with pytest.raises(TemporaryError): api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.fetch_l2_order_book(pair="ETH/BTC", limit=50) with pytest.raises(OperationalException): api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.fetch_l2_order_book(pair="ETH/BTC", limit=50) @@ -3058,7 +3058,7 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na [1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687], [1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867], ] - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) sort_mock = mocker.patch("freqtrade.exchange.exchange.sorted", MagicMock(side_effect=sort_data)) # Test the OHLCV data sort @@ -3128,7 +3128,7 @@ async def test__async_fetch_trades( default_conf, mocker, caplog, exchange_name, fetch_trades_result ): caplog.set_level(logging.DEBUG) - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) # Monkey-patch async function exchange._api_async.fetch_trades = get_mock_coro(fetch_trades_result) @@ -3182,7 +3182,7 @@ async def test__async_fetch_trades( api_mock = MagicMock() with pytest.raises(OperationalException, match=r"Could not fetch trade data*"): api_mock.fetch_trades = MagicMock(side_effect=ccxt.BaseError("Unknown error")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) await exchange._async_fetch_trades(pair, since=dt_ts(dt_now() - timedelta(seconds=2000))) exchange.close() @@ -3191,7 +3191,7 @@ async def test__async_fetch_trades( match=r"Exchange.* does not support fetching " r"historical trade data\..*", ): api_mock.fetch_trades = MagicMock(side_effect=ccxt.NotSupported("Not supported")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) await exchange._async_fetch_trades(pair, since=dt_ts(dt_now() - timedelta(seconds=2000))) exchange.close() @@ -3203,7 +3203,7 @@ async def test__async_fetch_trades_contract_size( caplog.set_level(logging.DEBUG) default_conf["margin_mode"] = "isolated" default_conf["trading_mode"] = "futures" - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) # Monkey-patch async function exchange._api_async.fetch_trades = get_mock_coro( [ @@ -3246,7 +3246,7 @@ async def test__async_fetch_trades_contract_size( async def test__async_get_trade_history_id( default_conf, mocker, exchange_name, fetch_trades_result ): - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) if exchange._trades_pagination != "id": exchange.close() pytest.skip("Exchange does not support pagination by trade id") @@ -3305,7 +3305,7 @@ async def test__async_get_trade_history_id( def test__valid_trade_pagination_id(mocker, default_conf_usdt, exchange_name, trade_id, expected): if exchange_name == "kraken": pytest.skip("Kraken has a different pagination id format, and an explicit test.") - exchange = get_patched_exchange(mocker, default_conf_usdt, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf_usdt, exchange=exchange_name) assert exchange._valid_trade_pagination_id("XRP/USDT", trade_id) == expected @@ -3324,7 +3324,7 @@ async def test__async_get_trade_history_time( return fetch_trades_result[-1:] caplog.set_level(logging.DEBUG) - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) if exchange._trades_pagination != "time": exchange.close() pytest.skip("Exchange does not support pagination by timestamp") @@ -3366,7 +3366,7 @@ async def test__async_get_trade_history_time_empty( return [], None caplog.set_level(logging.DEBUG) - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) # Monkey-patch async function exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist) pair = "ETH/BTC" @@ -3387,7 +3387,7 @@ async def test__async_get_trade_history_time_empty( @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_historic_trades(default_conf, mocker, caplog, exchange_name, trades_history): mocker.patch(f"{EXMS}.exchange_has", return_value=True) - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) pair = "ETH/BTC" @@ -3418,7 +3418,7 @@ def test_get_historic_trades_notsupported( default_conf, mocker, caplog, exchange_name, trades_history ): mocker.patch(f"{EXMS}.exchange_has", return_value=False) - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) pair = "ETH/BTC" @@ -3432,7 +3432,7 @@ def test_get_historic_trades_notsupported( @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_cancel_order_dry_run(default_conf, mocker, exchange_name): default_conf["dry_run"] = True - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=True) assert exchange.cancel_order(order_id="123", pair="TKN/BTC") == {} assert exchange.cancel_stoploss_order(order_id="123", pair="TKN/BTC") == {} @@ -3467,7 +3467,7 @@ def test_cancel_order_dry_run(default_conf, mocker, exchange_name): ], ) def test_check_order_canceled_empty(mocker, default_conf, exchange_name, order, result): - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) assert exchange.check_order_canceled_empty(order) == result @@ -3487,7 +3487,7 @@ def test_check_order_canceled_empty(mocker, default_conf, exchange_name, order, ], ) def test_is_cancel_order_result_suitable(mocker, default_conf, exchange_name, order, result): - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) assert exchange.is_cancel_order_result_suitable(order) == result @@ -3507,7 +3507,7 @@ def test_cancel_order_with_result( api_mock = MagicMock() api_mock.cancel_order = MagicMock(return_value=corder) api_mock.fetch_order = MagicMock(return_value={}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) res = exchange.cancel_order_with_result("1234", "ETH/BTC", 1234) assert isinstance(res, dict) assert api_mock.cancel_order.call_count == call_corder @@ -3521,7 +3521,7 @@ def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, cap api_mock = MagicMock() api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) res = exchange.cancel_order_with_result("1234", "ETH/BTC", 1541) assert isinstance(res, dict) @@ -3536,12 +3536,12 @@ def test_cancel_order(default_conf, mocker, exchange_name): default_conf["dry_run"] = False api_mock = MagicMock() api_mock.cancel_order = MagicMock(return_value={"id": "123"}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) assert exchange.cancel_order(order_id="_", pair="TKN/BTC") == {"id": "123"} with pytest.raises(InvalidOrderException): api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.cancel_order(order_id="_", pair="TKN/BTC") assert api_mock.cancel_order.call_count == 1 @@ -3562,12 +3562,12 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name): default_conf["dry_run"] = False api_mock = MagicMock() api_mock.cancel_order = MagicMock(return_value={"id": "123"}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) assert exchange.cancel_stoploss_order(order_id="_", pair="TKN/BTC") == {"id": "123"} with pytest.raises(InvalidOrderException): api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.cancel_stoploss_order(order_id="_", pair="TKN/BTC") assert api_mock.cancel_order.call_count == 1 @@ -3591,7 +3591,7 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): mock_prefix = "freqtrade.exchange.okx.Okx" mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value={"for": 123}) mocker.patch(f"{mock_prefix}.fetch_stoploss_order", return_value={"for": 123}) - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) res = {"fee": {}, "status": "canceled", "amount": 1234} mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=res) @@ -3616,7 +3616,7 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): exc = InvalidOrderException("Did not find order") mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=exc) mocker.patch(f"{mock_prefix}.cancel_stoploss_order", side_effect=exc) - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=123) @@ -3630,7 +3630,7 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog): order.symbol = "TKN/BTC" mocker.patch(f"{EXMS}.exchange_has", return_value=True) - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) exchange._dry_run_open_orders["X"] = order assert exchange.fetch_order("X", "TKN/BTC").myid == 123 @@ -3640,18 +3640,18 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog): default_conf["dry_run"] = False api_mock = MagicMock() api_mock.fetch_order = MagicMock(return_value={"id": "123", "amount": 2, "symbol": "TKN/BTC"}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) assert exchange.fetch_order("X", "TKN/BTC") == {"id": "123", "amount": 2, "symbol": "TKN/BTC"} assert log_has(("API fetch_order: {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}"), caplog) with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.fetch_order(order_id="_", pair="TKN/BTC") assert api_mock.fetch_order.call_count == 1 api_mock.fetch_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) with patch("freqtrade.exchange.common.time.sleep") as tm: with pytest.raises(InvalidOrderException): exchange.fetch_order(order_id="_", pair="TKN/BTC") @@ -3686,7 +3686,7 @@ def test_fetch_order_emulated(default_conf, mocker, exchange_name, caplog): order.myid = 123 order.symbol = "TKN/BTC" - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) mocker.patch(f"{EXMS}.exchange_has", return_value=False) exchange._dry_run_open_orders["X"] = order # Dry run - regular fetch_order behavior @@ -3704,7 +3704,7 @@ def test_fetch_order_emulated(default_conf, mocker, exchange_name, caplog): api_mock.fetch_closed_order = MagicMock( return_value={"id": "123", "amount": 2, "symbol": "TKN/BTC"} ) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) assert exchange.fetch_order("X", "TKN/BTC") == {"id": "123", "amount": 2, "symbol": "TKN/BTC"} assert log_has( ("API fetch_open_order: {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}"), caplog @@ -3718,7 +3718,7 @@ def test_fetch_order_emulated(default_conf, mocker, exchange_name, caplog): api_mock.fetch_closed_order = MagicMock( return_value={"id": "123", "amount": 2, "symbol": "TKN/BTC"} ) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) assert exchange.fetch_order("X", "TKN/BTC") == {"id": "123", "amount": 2, "symbol": "TKN/BTC"} assert log_has( ("API fetch_closed_order: {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}"), caplog @@ -3730,12 +3730,12 @@ def test_fetch_order_emulated(default_conf, mocker, exchange_name, caplog): with pytest.raises(InvalidOrderException): api_mock.fetch_open_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) api_mock.fetch_closed_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.fetch_order(order_id="_", pair="TKN/BTC") assert api_mock.fetch_open_order.call_count == 1 api_mock.fetch_open_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) ccxt_exceptionhandlers( mocker, @@ -3758,7 +3758,7 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name): mocker.patch(f"{EXMS}.exchange_has", return_value=True) order = MagicMock() order.myid = 123 - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) exchange._dry_run_open_orders["X"] = order assert exchange.fetch_stoploss_order("X", "TKN/BTC").myid == 123 @@ -3768,7 +3768,7 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name): default_conf["dry_run"] = False api_mock = MagicMock() api_mock.fetch_order = MagicMock(return_value={"id": "123", "symbol": "TKN/BTC"}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) res = {"id": "123", "symbol": "TKN/BTC"} if exchange_name == "okx": res = {"id": "123", "symbol": "TKN/BTC", "type": "stoploss"} @@ -3779,7 +3779,7 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name): return with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange.fetch_stoploss_order(order_id="_", pair="TKN/BTC") assert api_mock.fetch_order.call_count == 1 @@ -3797,7 +3797,7 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name): def test_fetch_order_or_stoploss_order(default_conf, mocker): - exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange = get_patched_exchange(mocker, default_conf, exchange="binance") fetch_order_mock = MagicMock() fetch_stoploss_order_mock = MagicMock() mocker.patch.multiple( @@ -3824,7 +3824,7 @@ def test_fetch_order_or_stoploss_order(default_conf, mocker): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_name(default_conf, mocker, exchange_name): - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) assert exchange.name == exchange_name.title() assert exchange.id == exchange_name @@ -3875,7 +3875,7 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name, trading_mode, ] ) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) orders = exchange.get_trades_for_order(order_id, "ETH/USDT:USDT", since) assert len(orders) == 1 @@ -3914,7 +3914,7 @@ def test_get_fee(default_conf, mocker, exchange_name): api_mock.calculate_fee = MagicMock( return_value={"type": "taker", "currency": "BTC", "rate": 0.025, "cost": 0.05} ) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange._config.pop("fee", None) assert exchange.get_fee("ETH/BTC") == 0.025 @@ -3932,7 +3932,7 @@ def test_get_fee(default_conf, mocker, exchange_name): def test_stoploss_order_unsupported_exchange(default_conf, mocker): - exchange = get_patched_exchange(mocker, default_conf, id="bitpanda") + exchange = get_patched_exchange(mocker, default_conf, exchange="bitpanda") with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): exchange.create_stoploss( pair="ETH/BTC", amount=1, stop_price=220, order_types={}, side="sell", leverage=1.0 @@ -3956,7 +3956,7 @@ def test_stoploss_order_unsupported_exchange(default_conf, mocker): ], ) def test__get_stop_limit_rate(default_conf_usdt, mocker, side, ratio, expected): - exchange = get_patched_exchange(mocker, default_conf_usdt, id="binance") + exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="binance") order_types = {"stoploss_on_exchange_limit_ratio": ratio} if isinstance(expected, type) and issubclass(expected, Exception): @@ -4314,7 +4314,7 @@ def test_get_markets_error(default_conf, mocker): def test_ohlcv_candle_limit(default_conf, mocker, exchange_name): if exchange_name == "okx": pytest.skip("Tested separately for okx") - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) timeframes = ("1m", "5m", "1h") expected = exchange._ft_has["ohlcv_candle_limit"] for timeframe in timeframes: @@ -4383,7 +4383,7 @@ def test_market_is_tradable( ) -> None: default_conf["trading_mode"] = trademode mocker.patch(f"{EXMS}.validate_trading_mode_and_margin_mode") - ex = get_patched_exchange(mocker, default_conf, id=exchange) + ex = get_patched_exchange(mocker, default_conf, exchange=exchange) market = { "symbol": market_symbol, "base": base, @@ -4654,7 +4654,7 @@ def test_get_funding_fees(default_conf_usdt, mocker, exchange_name, caplog): now = datetime.now(timezone.utc) default_conf_usdt["trading_mode"] = "futures" default_conf_usdt["margin_mode"] = "isolated" - exchange = get_patched_exchange(mocker, default_conf_usdt, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf_usdt, exchange=exchange_name) exchange._fetch_and_calculate_funding_fees = MagicMock(side_effect=ExchangeError) assert exchange.get_funding_fees("BTC/USDT:USDT", 1, False, now) == 0.0 assert exchange._fetch_and_calculate_funding_fees.call_count == 1 @@ -4707,7 +4707,7 @@ def test__get_funding_fees_from_exchange(default_conf, mocker, exchange_name): type(api_mock).has = PropertyMock(return_value={"fetchFundingHistory": True}) # mocker.patch(f'{EXMS}.get_funding_fees', lambda pair, since: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) date_time = datetime.strptime("2021-09-01T00:00:01.000Z", "%Y-%m-%dT%H:%M:%S.%fZ") unix_time = int(date_time.timestamp()) expected_fees = -0.001 # 0.14542341 + -0.14642341 @@ -4737,7 +4737,7 @@ def test__get_funding_fees_from_exchange(default_conf, mocker, exchange_name): def test_get_stake_amount_considering_leverage( exchange, stake_amount, leverage, min_stake_with_lev, mocker, default_conf ): - exchange = get_patched_exchange(mocker, default_conf, id=exchange) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange) assert ( exchange._get_stake_amount_considering_leverage(stake_amount, leverage) == min_stake_with_lev @@ -4804,7 +4804,7 @@ def test_validate_trading_mode_and_margin_mode( default_conf, mocker, exchange_name, trading_mode, margin_mode, exception_thrown ): exchange = get_patched_exchange( - mocker, default_conf, id=exchange_name, mock_supported_modes=False + mocker, default_conf, exchange=exchange_name, mock_supported_modes=False ) if exception_thrown: with pytest.raises(OperationalException): @@ -4831,7 +4831,7 @@ def test_validate_trading_mode_and_margin_mode( def test__ccxt_config(default_conf, mocker, exchange_name, trading_mode, ccxt_config): default_conf["trading_mode"] = trading_mode default_conf["margin_mode"] = "isolated" - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) assert exchange._ccxt_config == ccxt_config @@ -4850,7 +4850,7 @@ def test_get_max_leverage_from_margin(default_conf, mocker, pair, nominal_value, default_conf["margin_mode"] = "isolated" api_mock = MagicMock() type(api_mock).has = PropertyMock(return_value={"fetchLeverageTiers": False}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gate") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="gate") assert exchange.get_max_leverage(pair, nominal_value) == max_lev @@ -4867,7 +4867,7 @@ def test_calculate_funding_fees( default_conf, mocker, size, funding_rate, mark_price, funding_fee, kraken_fee, time_in_ratio ): exchange = get_patched_exchange(mocker, default_conf) - kraken = get_patched_exchange(mocker, default_conf, id="kraken") + kraken = get_patched_exchange(mocker, default_conf, exchange="kraken") prior_date = timeframe_to_prev_date("1h", datetime.now(timezone.utc) - timedelta(hours=1)) trade_date = timeframe_to_prev_date("1h", datetime.now(timezone.utc)) funding_rates = DataFrame( @@ -5092,7 +5092,7 @@ def test__fetch_and_calculate_funding_fees( type(api_mock).has = PropertyMock(return_value={"fetchOHLCV": True}) type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": True}) - ex = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) + ex = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange) mocker.patch(f"{EXMS}.timeframes", PropertyMock(return_value=["1h", "4h", "8h"])) funding_fees = ex._fetch_and_calculate_funding_fees( pair="ADA/USDT:USDT", amount=amount, is_short=True, open_date=d1, close_date=d2 @@ -5106,7 +5106,7 @@ def test__fetch_and_calculate_funding_fees( # Return empty "refresh_latest" mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value={}) - ex = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) + ex = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange) with pytest.raises(ExchangeError, match="Could not find funding rates."): ex._fetch_and_calculate_funding_fees( pair="ADA/USDT:USDT", amount=amount, is_short=False, open_date=d1, close_date=d2 @@ -5137,7 +5137,7 @@ def test__fetch_and_calculate_funding_fees_datetime_called( type(api_mock).has = PropertyMock(return_value={"fetchOHLCV": True}) type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": True}) mocker.patch(f"{EXMS}.timeframes", PropertyMock(return_value=["4h", "8h"])) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange) d1 = datetime.strptime("2021-08-31 23:00:01 +0000", "%Y-%m-%d %H:%M:%S %z") time_machine.move_to("2021-09-01 08:00:00 +00:00") @@ -5454,7 +5454,7 @@ def test_liquidation_price_is_none( ): default_conf["trading_mode"] = trading_mode default_conf["margin_mode"] = margin_mode - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) assert ( exchange.get_liquidation_price( pair="DOGE/USDT", @@ -5553,7 +5553,7 @@ def test_liquidation_price_binance( default_conf["trading_mode"] = trading_mode default_conf["margin_mode"] = margin_mode default_conf["liquidation_buffer"] = 0.0 - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(mm_ratio, maintenance_amt)) assert ( pytest.approx( @@ -5703,7 +5703,7 @@ def test_load_leverage_tiers(mocker, default_conf, exchange_name): ) # SPOT - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) assert exchange.load_leverage_tiers() == {} default_conf["trading_mode"] = "futures" @@ -5712,12 +5712,12 @@ def test_load_leverage_tiers(mocker, default_conf, exchange_name): if exchange_name != "binance": # FUTURES has.fetchLeverageTiers == False type(api_mock).has = PropertyMock(return_value={"fetchLeverageTiers": False}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) assert exchange.load_leverage_tiers() == {} # FUTURES regular type(api_mock).has = PropertyMock(return_value={"fetchLeverageTiers": True}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) assert exchange.load_leverage_tiers() == { "ADA/USDT:USDT": [ { @@ -5869,13 +5869,13 @@ def test_get_maintenance_ratio_and_amt( def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers): # Test Spot - exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange = get_patched_exchange(mocker, default_conf, exchange="binance") assert exchange.get_max_leverage("BNB/USDT", 100.0) == 1.0 # Test Futures default_conf["trading_mode"] = "futures" default_conf["margin_mode"] = "isolated" - exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange = get_patched_exchange(mocker, default_conf, exchange="binance") exchange._leverage_tiers = leverage_tiers @@ -5899,7 +5899,7 @@ def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers): def test__get_params(mocker, default_conf, exchange_name): api_mock = MagicMock() mocker.patch(f"{EXMS}.exchange_has", return_value=True) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange._params = {"test": True} params1 = {"test": True} @@ -5954,7 +5954,7 @@ def test__get_params(mocker, default_conf, exchange_name): default_conf["trading_mode"] = "futures" default_conf["margin_mode"] = "isolated" - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name) exchange._params = {"test": True} assert ( @@ -6171,7 +6171,7 @@ def test_get_liquidation_price( default_conf_usdt["exchange"]["name"] = exchange_name default_conf_usdt["margin_mode"] = margin_mode mocker.patch("freqtrade.exchange.gate.Gate.validate_ordertypes") - exchange = get_patched_exchange(mocker, default_conf_usdt, id=exchange_name) + exchange = get_patched_exchange(mocker, default_conf_usdt, exchange=exchange_name) exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01)) exchange.name = exchange_name diff --git a/tests/exchange/test_gate.py b/tests/exchange/test_gate.py index b4e021a5d..3f5ecacf6 100644 --- a/tests/exchange/test_gate.py +++ b/tests/exchange/test_gate.py @@ -9,7 +9,7 @@ from tests.conftest import EXMS, get_patched_exchange @pytest.mark.usefixtures("init_persistence") def test_fetch_stoploss_order_gate(default_conf, mocker): - exchange = get_patched_exchange(mocker, default_conf, id="gate") + exchange = get_patched_exchange(mocker, default_conf, exchange="gate") fetch_order_mock = MagicMock() exchange.fetch_order = fetch_order_mock @@ -23,7 +23,7 @@ def test_fetch_stoploss_order_gate(default_conf, mocker): default_conf["trading_mode"] = "futures" default_conf["margin_mode"] = "isolated" - exchange = get_patched_exchange(mocker, default_conf, id="gate") + exchange = get_patched_exchange(mocker, default_conf, exchange="gate") exchange.fetch_order = MagicMock( return_value={ @@ -41,7 +41,7 @@ def test_fetch_stoploss_order_gate(default_conf, mocker): def test_cancel_stoploss_order_gate(default_conf, mocker): - exchange = get_patched_exchange(mocker, default_conf, id="gate") + exchange = get_patched_exchange(mocker, default_conf, exchange="gate") cancel_order_mock = MagicMock() exchange.cancel_order = cancel_order_mock @@ -57,7 +57,7 @@ def test_cancel_stoploss_order_gate(default_conf, mocker): "sl1,sl2,sl3,side", [(1501, 1499, 1501, "sell"), (1499, 1501, 1499, "buy")] ) def test_stoploss_adjust_gate(mocker, default_conf, sl1, sl2, sl3, side): - exchange = get_patched_exchange(mocker, default_conf, id="gate") + exchange = get_patched_exchange(mocker, default_conf, exchange="gate") order = { "price": 1500, "stopPrice": 1500, @@ -111,7 +111,7 @@ def test_fetch_my_trades_gate(mocker, default_conf, takerormaker, rate, cost): } ] ) - exchange = get_patched_exchange(mocker, default_conf, api_mock=api_mock, id="gate") + exchange = get_patched_exchange(mocker, default_conf, api_mock=api_mock, exchange="gate") exchange._trading_fees = tick trades = exchange.get_trades_for_order("22255", "ETH/USDT:USDT", datetime.now(timezone.utc)) trade = trades[0] diff --git a/tests/exchange/test_htx.py b/tests/exchange/test_htx.py index 807d9b28f..03099c3de 100644 --- a/tests/exchange/test_htx.py +++ b/tests/exchange/test_htx.py @@ -128,7 +128,7 @@ def test_create_stoploss_order_dry_run_htx(default_conf, mocker): def test_stoploss_adjust_htx(mocker, default_conf): - exchange = get_patched_exchange(mocker, default_conf, id="htx") + exchange = get_patched_exchange(mocker, default_conf, exchange="htx") order = { "type": "stop", "price": 1500, diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 932677c68..5715c9cc0 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -32,7 +32,7 @@ def test_kraken_trading_agreement(default_conf, mocker, order_type, time_in_forc mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y) mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="kraken") order = exchange.create_order( pair="ETH/BTC", @@ -121,7 +121,7 @@ def test_get_balances_prod(default_conf, mocker): ] api_mock.fetch_open_orders = MagicMock(return_value=kraken_open_orders) default_conf["dry_run"] = False - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="kraken") balances = exchange.get_balances() assert len(balances) == 6 @@ -256,7 +256,7 @@ def test_create_stoploss_order_dry_run_kraken(default_conf, mocker, side): "sl1,sl2,sl3,side", [(1501, 1499, 1501, "sell"), (1499, 1501, 1499, "buy")] ) def test_stoploss_adjust_kraken(mocker, default_conf, sl1, sl2, sl3, side): - exchange = get_patched_exchange(mocker, default_conf, id="kraken") + exchange = get_patched_exchange(mocker, default_conf, exchange="kraken") order = { "type": "market", "stopLossPrice": 1500, @@ -278,5 +278,5 @@ def test_stoploss_adjust_kraken(mocker, default_conf, sl1, sl2, sl3, side): ], ) def test__valid_trade_pagination_id_kraken(mocker, default_conf_usdt, trade_id, expected): - exchange = get_patched_exchange(mocker, default_conf_usdt, id="kraken") + exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="kraken") assert exchange._valid_trade_pagination_id("XRP/USDT", trade_id) == expected diff --git a/tests/exchange/test_kucoin.py b/tests/exchange/test_kucoin.py index 1d297505c..c2d245927 100644 --- a/tests/exchange/test_kucoin.py +++ b/tests/exchange/test_kucoin.py @@ -134,7 +134,7 @@ def test_stoploss_order_dry_run_kucoin(default_conf, mocker): def test_stoploss_adjust_kucoin(mocker, default_conf): - exchange = get_patched_exchange(mocker, default_conf, id="kucoin") + exchange = get_patched_exchange(mocker, default_conf, exchange="kucoin") order = { "type": "limit", "price": 1500, @@ -161,7 +161,7 @@ def test_kucoin_create_order(default_conf, mocker, side, ordertype, rate): default_conf["dry_run"] = False mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y) mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kucoin") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="kucoin") exchange._set_leverage = MagicMock() exchange.set_margin_mode = MagicMock() diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index 305b16ea2..df428010f 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -12,7 +12,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers def test_okx_ohlcv_candle_limit(default_conf, mocker): - exchange = get_patched_exchange(mocker, default_conf, id="okx") + exchange = get_patched_exchange(mocker, default_conf, exchange="okx") timeframes = ("1m", "5m", "1h") start_time = int(datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp() * 1000) @@ -188,7 +188,7 @@ def test_get_maintenance_ratio_and_amt_okx( } ), ) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="okx") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="okx") assert exchange.get_maintenance_ratio_and_amt("ETH/USDT:USDT", 2000) == (0.01, None) assert exchange.get_maintenance_ratio_and_amt("ETH/USDT:USDT", 2001) == (0.015, None) assert exchange.get_maintenance_ratio_and_amt("ETH/USDT:USDT", 4001) == (0.02, None) @@ -199,12 +199,12 @@ def test_get_maintenance_ratio_and_amt_okx( def test_get_max_pair_stake_amount_okx(default_conf, mocker, leverage_tiers): - exchange = get_patched_exchange(mocker, default_conf, id="okx") + exchange = get_patched_exchange(mocker, default_conf, exchange="okx") assert exchange.get_max_pair_stake_amount("BNB/BUSD", 1.0) == float("inf") default_conf["trading_mode"] = "futures" default_conf["margin_mode"] = "isolated" - exchange = get_patched_exchange(mocker, default_conf, id="okx") + exchange = get_patched_exchange(mocker, default_conf, exchange="okx") exchange._leverage_tiers = leverage_tiers assert exchange.get_max_pair_stake_amount("XRP/USDT:USDT", 1.0) == 30000000 @@ -229,7 +229,7 @@ def test_get_max_pair_stake_amount_okx(default_conf, mocker, leverage_tiers): ], ) def test__get_posSide(default_conf, mocker, mode, side, reduceonly, result): - exchange = get_patched_exchange(mocker, default_conf, id="okx") + exchange = get_patched_exchange(mocker, default_conf, exchange="okx") exchange.net_only = mode == "net" assert exchange._get_posSide(side, reduceonly) == result @@ -257,7 +257,7 @@ def test_additional_exchange_init_okx(default_conf, mocker): ] ) default_conf["dry_run"] = False - exchange = get_patched_exchange(mocker, default_conf, id="okx", api_mock=api_mock) + exchange = get_patched_exchange(mocker, default_conf, exchange="okx", api_mock=api_mock) assert api_mock.fetch_accounts.call_count == 0 exchange.trading_mode = TradingMode.FUTURES # Default to netOnly @@ -438,7 +438,7 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmp_path, caplog default_conf["trading_mode"] = "futures" default_conf["margin_mode"] = "isolated" default_conf["stake_currency"] = "USDT" - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="okx") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="okx") exchange.trading_mode = TradingMode.FUTURES exchange.margin_mode = MarginMode.ISOLATED exchange.markets = markets @@ -520,7 +520,7 @@ def test__set_leverage_okx(mocker, default_conf): default_conf["trading_mode"] = TradingMode.FUTURES default_conf["margin_mode"] = MarginMode.ISOLATED - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="okx") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="okx") exchange._lev_prep("BTC/USDT:USDT", 3.2, "buy") assert api_mock.set_leverage.call_count == 1 # Leverage is rounded to 3. @@ -554,7 +554,7 @@ def test_fetch_stoploss_order_okx(default_conf, mocker): api_mock = MagicMock() api_mock.fetch_order = MagicMock() - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="okx") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="okx") exchange.fetch_stoploss_order("1234", "ETH/BTC") assert api_mock.fetch_order.call_count == 1 @@ -594,7 +594,7 @@ def test_fetch_stoploss_order_okx(default_conf, mocker): assert resp["type"] == "stoploss" default_conf["dry_run"] = True - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="okx") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="okx") dro_mock = mocker.patch(f"{EXMS}.fetch_dry_run_order", MagicMock(return_value={"id": "123455"})) api_mock.fetch_order.reset_mock() @@ -614,7 +614,7 @@ def test_fetch_stoploss_order_okx(default_conf, mocker): "sl1,sl2,sl3,side", [(1501, 1499, 1501, "sell"), (1499, 1501, 1499, "buy")] ) def test_stoploss_adjust_okx(mocker, default_conf, sl1, sl2, sl3, side): - exchange = get_patched_exchange(mocker, default_conf, id="okx") + exchange = get_patched_exchange(mocker, default_conf, exchange="okx") order = { "type": "stoploss", "price": 1500, @@ -625,7 +625,7 @@ def test_stoploss_adjust_okx(mocker, default_conf, sl1, sl2, sl3, side): def test_stoploss_cancel_okx(mocker, default_conf): - exchange = get_patched_exchange(mocker, default_conf, id="okx") + exchange = get_patched_exchange(mocker, default_conf, exchange="okx") exchange.cancel_order = MagicMock() @@ -639,7 +639,7 @@ def test_stoploss_cancel_okx(mocker, default_conf): def test__get_stop_params_okx(mocker, default_conf): default_conf["trading_mode"] = "futures" default_conf["margin_mode"] = "isolated" - exchange = get_patched_exchange(mocker, default_conf, id="okx") + exchange = get_patched_exchange(mocker, default_conf, exchange="okx") params = exchange._get_stop_params("ETH/USDT:USDT", 1500, "sell") assert params["tdMode"] == "isolated" @@ -660,13 +660,13 @@ def test_fetch_orders_okx(default_conf, mocker, limit_order): mocker.patch(f"{EXMS}.exchange_has", return_value=True) start_time = datetime.now(timezone.utc) - timedelta(days=20) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="okx") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="okx") # Not available in dry-run assert exchange.fetch_orders("mocked", start_time) == [] assert api_mock.fetch_orders.call_count == 0 default_conf["dry_run"] = False - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="okx") + exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="okx") def has_resp(_, endpoint): if endpoint == "fetchOrders": From bfe18573f39461c2418cc5203e290affa1270632 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jul 2024 10:52:03 +0200 Subject: [PATCH 17/74] Improve wording for list-exchanges command --- freqtrade/commands/list_commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 3a542226c..97be7bac6 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -56,11 +56,11 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: return result if args["list_exchanges_all"]: - print("All exchanges supported by the ccxt library:") exchanges = [build_entry(e, True) for e in exchanges] + print(f"All exchanges supported by the ccxt library ({len(exchanges)} exchanges):") else: - print("Exchanges available for Freqtrade:") exchanges = [build_entry(e, False) for e in exchanges if e["valid"] is not False] + print(f"Exchanges available for Freqtrade ({len(exchanges)} exchanges):") print( tabulate( From 7a67771e46f79f71c4f71ef2ee21496518a09f7a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jul 2024 10:55:10 +0200 Subject: [PATCH 18/74] feat: include orderbook / fetchTicker to exchange_has this will ensure one of the "get price" methods is available. --- freqtrade/exchange/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 251325a0c..21c67d291 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -65,6 +65,7 @@ SUPPORTED_EXCHANGES = [ EXCHANGE_HAS_REQUIRED: Dict[str, List[str]] = { # Required / private "fetchOrder": ["fetchOpenOrder", "fetchClosedOrder"], + "fetchL2OrderBook": ["fetchTicker"], "cancelOrder": [], "createOrder": [], "fetchBalance": [], From 6b98bfe9b73ecacd25b0b9ade94723ecb70ef1fe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jul 2024 11:42:40 +0200 Subject: [PATCH 19/74] Simplify output --- freqtrade/commands/list_commands.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 97be7bac6..153b1054d 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -57,11 +57,12 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: if args["list_exchanges_all"]: exchanges = [build_entry(e, True) for e in exchanges] - print(f"All exchanges supported by the ccxt library ({len(exchanges)} exchanges):") + title = f"All exchanges supported by the ccxt library ({len(exchanges)} exchanges):" else: exchanges = [build_entry(e, False) for e in exchanges if e["valid"] is not False] - print(f"Exchanges available for Freqtrade ({len(exchanges)} exchanges):") + title = f"Exchanges available for Freqtrade ({len(exchanges)} exchanges):" + print(title) print( tabulate( exchanges, From 95097d1f51fe65e96f1ea7dde7527596ba583f50 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jul 2024 11:49:26 +0200 Subject: [PATCH 20/74] chore: don't overwrite exchanges variable --- freqtrade/commands/list_commands.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 153b1054d..b8e479462 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -26,10 +26,10 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: :param args: Cli args from Arguments() :return: None """ - exchanges = list_available_exchanges(args["list_exchanges_all"]) + available_exchanges = list_available_exchanges(args["list_exchanges_all"]) if args["print_one_column"]: - print("\n".join([e["name"] for e in exchanges])) + print("\n".join([e["name"] for e in available_exchanges])) else: headers = { "name": "Exchange name", @@ -56,10 +56,12 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: return result if args["list_exchanges_all"]: - exchanges = [build_entry(e, True) for e in exchanges] + exchanges = [build_entry(e, True) for e in available_exchanges] title = f"All exchanges supported by the ccxt library ({len(exchanges)} exchanges):" else: - exchanges = [build_entry(e, False) for e in exchanges if e["valid"] is not False] + exchanges = [ + build_entry(e, False) for e in available_exchanges if e["valid"] is not False + ] title = f"Exchanges available for Freqtrade ({len(exchanges)} exchanges):" print(title) From b6b589b1f01004ef598bb64f507ab7b52ba14f6f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jul 2024 12:59:50 +0200 Subject: [PATCH 21/74] Update list-exchanges to use rich tables --- freqtrade/commands/list_commands.py | 85 ++++++++++++++++------------- tests/commands/test_commands.py | 4 +- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index b8e479462..e3c598786 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -1,11 +1,14 @@ import csv import logging import sys -from typing import Any, Dict, List, Union +from typing import Any, Dict, List import rapidjson from colorama import Fore, Style from colorama import init as colorama_init +from rich.console import Console +from rich.table import Table +from rich.text import Text from tabulate import tabulate from freqtrade.configuration import setup_utils_configuration @@ -14,7 +17,7 @@ from freqtrade.exceptions import ConfigurationError, OperationalException from freqtrade.exchange import list_available_exchanges, market_is_active from freqtrade.misc import parse_db_uri_for_logging, plural from freqtrade.resolvers import ExchangeResolver, StrategyResolver -from freqtrade.types import ValidExchangesType +from freqtrade.types.valid_exchanges_type import ValidExchangesType logger = logging.getLogger(__name__) @@ -26,51 +29,55 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: :param args: Cli args from Arguments() :return: None """ - available_exchanges = list_available_exchanges(args["list_exchanges_all"]) + available_exchanges: List[ValidExchangesType] = list_available_exchanges( + args["list_exchanges_all"] + ) if args["print_one_column"]: print("\n".join([e["name"] for e in available_exchanges])) else: - headers = { - "name": "Exchange name", - "supported": "Supported", - "trade_modes": "Markets", - "comment": "Reason", - } - headers.update({"valid": "Valid"} if args["list_exchanges_all"] else {}) + if args["list_exchanges_all"]: + title = ( + f"All exchanges supported by the ccxt library " + f"({len(available_exchanges)} exchanges):" + ) + else: + available_exchanges = [e for e in available_exchanges if e["valid"] is not False] + title = f"Exchanges available for Freqtrade ({len(available_exchanges)} exchanges):" - def build_entry(exchange: ValidExchangesType, valid: bool): - valid_entry = {"valid": exchange["valid"]} if valid else {} - result: Dict[str, Union[str, bool]] = { - "name": exchange["name"], - **valid_entry, - "supported": "Official" if exchange["supported"] else "", - "trade_modes": ("DEX: " if exchange["dex"] else "") - + ", ".join( - (f"{a['margin_mode']} " if a["margin_mode"] else "") + a["trading_mode"] + table = Table(title=title) + + table.add_column("Exchange Name") + table.add_column("Markets") + table.add_column("Reason") + + for exchange in available_exchanges: + name = Text(exchange["name"]) + if exchange["supported"]: + name.append(" (Official)", style="italic") + name.stylize("green bold") + + trade_modes = Text( + ", ".join( + (f"{a.get('margin_mode', '')} {a["trading_mode"]}").lstrip() for a in exchange["trade_modes"] ), - "comment": exchange["comment"], - } - - return result - - if args["list_exchanges_all"]: - exchanges = [build_entry(e, True) for e in available_exchanges] - title = f"All exchanges supported by the ccxt library ({len(exchanges)} exchanges):" - else: - exchanges = [ - build_entry(e, False) for e in available_exchanges if e["valid"] is not False - ] - title = f"Exchanges available for Freqtrade ({len(exchanges)} exchanges):" - - print(title) - print( - tabulate( - exchanges, - headers=headers, + style="", ) - ) + if exchange["dex"]: + trade_modes = Text("DEX: ") + trade_modes + trade_modes.stylize("bold", 0, 3) + + table.add_row( + name, + trade_modes, + exchange["comment"], + style=None if exchange["valid"] else "red", + ) + # table.add_row(*[exchange[header] for header in headers]) + + console = Console() + console.print(table) def _print_objs_tabular(objs: List, print_colorized: bool) -> None: diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 02b234b6c..818533630 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -116,7 +116,7 @@ def test_list_exchanges(capsys): start_list_exchanges(get_args(args)) captured = capsys.readouterr() - assert re.match(r"Exchanges available for Freqtrade.*", captured.out) + assert re.search(r".*Exchanges available for Freqtrade.*", captured.out) assert re.search(r".*binance.*", captured.out) assert re.search(r".*bybit.*", captured.out) @@ -139,7 +139,7 @@ def test_list_exchanges(capsys): start_list_exchanges(get_args(args)) captured = capsys.readouterr() - assert re.match(r"All exchanges supported by the ccxt library.*", captured.out) + assert re.search(r"All exchanges supported by the ccxt library.*", captured.out) assert re.search(r".*binance.*", captured.out) assert re.search(r".*bingx.*", captured.out) assert re.search(r".*bitmex.*", captured.out) From bafb6507c459c9a328860d3754c6b3bb783daec2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jul 2024 13:09:37 +0200 Subject: [PATCH 22/74] Use Rich table for pair list output --- freqtrade/commands/list_commands.py | 11 ++++++++--- tests/commands/test_commands.py | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index e3c598786..d6fdd3111 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -280,9 +280,14 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: writer.writeheader() writer.writerows(tabular_data) else: - # print data as a table, with the human-readable summary - print(f"{summary_str}:") - print(tabulate(tabular_data, headers="keys", tablefmt="psql", stralign="right")) + table = Table(title=summary_str) + for header in headers: + table.add_column(header, justify="right") + for row in tabular_data: + table.add_row(*[str(row[header]) for header in headers]) + + console = Console() + console.print(table) elif not ( args.get("print_one_column", False) or args.get("list_pairs_print_json", False) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 818533630..7e4d2e2cc 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -293,7 +293,7 @@ def test_list_markets(mocker, markets_static, capsys): pargs["config"] = None start_list_markets(pargs, False) captured = capsys.readouterr() - assert re.match("\nExchange Binance has 12 active markets:\n", captured.out) + assert re.search(r".*Exchange Binance has 12 active markets.*", captured.out) patch_exchange(mocker, api_mock=api_mock, exchange="binance", mock_markets=markets_static) # Test with --all: all markets @@ -491,7 +491,7 @@ def test_list_markets(mocker, markets_static, capsys): ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert "Exchange Binance has 12 active markets:\n" in captured.out + assert "Exchange Binance has 12 active markets" in captured.out # Test tabular output, no markets found args = [ From 2e0372d24739c2913281cd9210c2c7fcb16a2cbd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jul 2024 13:12:16 +0200 Subject: [PATCH 23/74] extract print_rich_table --- freqtrade/commands/list_commands.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index d6fdd3111..4260ed87b 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -80,6 +80,19 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: console.print(table) +def _print_rich_table(summary: str, headers: List[str], tabular_data: List[Dict[str, Any]]) -> None: + table = Table(title=summary) + + for header in headers: + table.add_column(header, justify="right") + + for row in tabular_data: + table.add_row(*[str(row[header]) for header in headers]) + + console = Console() + console.print(table) + + def _print_objs_tabular(objs: List, print_colorized: bool) -> None: if print_colorized: colorama_init(autoreset=True) @@ -280,14 +293,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: writer.writeheader() writer.writerows(tabular_data) else: - table = Table(title=summary_str) - for header in headers: - table.add_column(header, justify="right") - for row in tabular_data: - table.add_row(*[str(row[header]) for header in headers]) - - console = Console() - console.print(table) + _print_rich_table(summary_str, headers, tabular_data) elif not ( args.get("print_one_column", False) or args.get("list_pairs_print_json", False) From 8d00e1d929a7ded656f6e5876f2135cd3e595255 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jul 2024 18:09:03 +0200 Subject: [PATCH 24/74] feat: Use Rich for list-strategies command --- freqtrade/commands/list_commands.py | 40 +++++++++++++++-------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 4260ed87b..c7122afc2 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -59,7 +59,7 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: trade_modes = Text( ", ".join( - (f"{a.get('margin_mode', '')} {a["trading_mode"]}").lstrip() + (f"{a.get('margin_mode', '')} {a['trading_mode']}").lstrip() for a in exchange["trade_modes"] ), style="", @@ -94,27 +94,17 @@ def _print_rich_table(summary: str, headers: List[str], tabular_data: List[Dict[ def _print_objs_tabular(objs: List, print_colorized: bool) -> None: - if print_colorized: - colorama_init(autoreset=True) - red = Fore.RED - yellow = Fore.YELLOW - reset = Style.RESET_ALL - else: - red = "" - yellow = "" - reset = "" - names = [s["name"] for s in objs] objs_to_print = [ { - "name": s["name"] if s["name"] else "--", - "location": s["location_rel"], + "name": Text(s["name"] if s["name"] else "--"), + "location": Text(s["location_rel"]), "status": ( - red + "LOAD FAILED" + reset + Text("LOAD FAILED", style="bold red") if s["class"] is None - else "OK" + else Text("OK", style="bold green") if names.count(s["name"]) == 1 - else yellow + "DUPLICATE NAME" + reset + else Text("DUPLICATE NAME", style="bold yellow") ), } for s in objs @@ -124,11 +114,23 @@ def _print_objs_tabular(objs: List, print_colorized: bool) -> None: objs_to_print[idx].update( { "hyperoptable": "Yes" if s["hyperoptable"]["count"] > 0 else "No", - "buy-Params": len(s["hyperoptable"].get("buy", [])), - "sell-Params": len(s["hyperoptable"].get("sell", [])), + "buy-Params": str(len(s["hyperoptable"].get("buy", []))), + "sell-Params": str(len(s["hyperoptable"].get("sell", []))), } ) - print(tabulate(objs_to_print, headers="keys", tablefmt="psql", stralign="right")) + table = Table(title="Available:") + + for header in objs_to_print[0].keys(): + table.add_column(header.capitalize(), justify="right") + + for row in objs_to_print: + table.add_row(*[row[header] for header in objs_to_print[0].keys()]) + + console = Console( + color_system="auto" if print_colorized else None, + width=200 if "pytest" in sys.modules else None, + ) + console.print(table) def start_list_strategies(args: Dict[str, Any]) -> None: From 2d8470b2546fe3e47a00f20b21b28c9d4ade62c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jul 2024 18:26:08 +0200 Subject: [PATCH 25/74] Remove unused imports --- freqtrade/commands/list_commands.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index c7122afc2..e473d6e90 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -4,12 +4,9 @@ import sys from typing import Any, Dict, List import rapidjson -from colorama import Fore, Style -from colorama import init as colorama_init from rich.console import Console from rich.table import Table from rich.text import Text -from tabulate import tabulate from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode From 768a51cb9bebb70e3bd2b3b468d7e8d8a4896d1e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jul 2024 18:26:36 +0200 Subject: [PATCH 26/74] Extract rich_table print to utils --- freqtrade/commands/list_commands.py | 16 ++-------------- freqtrade/util/__init__.py | 2 ++ freqtrade/util/rich_tables.py | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 freqtrade/util/rich_tables.py diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index e473d6e90..ea2c84ae9 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -15,6 +15,7 @@ from freqtrade.exchange import list_available_exchanges, market_is_active from freqtrade.misc import parse_db_uri_for_logging, plural from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.types.valid_exchanges_type import ValidExchangesType +from freqtrade.util import print_rich_table logger = logging.getLogger(__name__) @@ -77,19 +78,6 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: console.print(table) -def _print_rich_table(summary: str, headers: List[str], tabular_data: List[Dict[str, Any]]) -> None: - table = Table(title=summary) - - for header in headers: - table.add_column(header, justify="right") - - for row in tabular_data: - table.add_row(*[str(row[header]) for header in headers]) - - console = Console() - console.print(table) - - def _print_objs_tabular(objs: List, print_colorized: bool) -> None: names = [s["name"] for s in objs] objs_to_print = [ @@ -292,7 +280,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: writer.writeheader() writer.writerows(tabular_data) else: - _print_rich_table(summary_str, headers, tabular_data) + print_rich_table(summary_str, headers, tabular_data) elif not ( args.get("print_one_column", False) or args.get("list_pairs_print_json", False) diff --git a/freqtrade/util/__init__.py b/freqtrade/util/__init__.py index 503f5861a..f478829e6 100644 --- a/freqtrade/util/__init__.py +++ b/freqtrade/util/__init__.py @@ -15,6 +15,7 @@ from freqtrade.util.formatters import decimals_per_coin, fmt_coin, round_value from freqtrade.util.ft_precise import FtPrecise from freqtrade.util.measure_time import MeasureTime from freqtrade.util.periodic_cache import PeriodicCache +from freqtrade.util.rich_tables import print_rich_table from freqtrade.util.template_renderer import render_template, render_template_with_fallback # noqa @@ -36,4 +37,5 @@ __all__ = [ "round_value", "fmt_coin", "MeasureTime", + "print_rich_table", ] diff --git a/freqtrade/util/rich_tables.py b/freqtrade/util/rich_tables.py new file mode 100644 index 000000000..f773a4fd1 --- /dev/null +++ b/freqtrade/util/rich_tables.py @@ -0,0 +1,17 @@ +from typing import Any, Dict, List + +from rich.console import Console +from rich.table import Table + + +def print_rich_table(summary: str, headers: List[str], tabular_data: List[Dict[str, Any]]) -> None: + table = Table(title=summary) + + for header in headers: + table.add_column(header, justify="right") + + for row in tabular_data: + table.add_row(*[str(row[header]) for header in headers]) + + console = Console() + console.print(table) From c9b3987d334d4e4bc9d1bd1a7d02c9b0d7bd5090 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jul 2024 18:30:48 +0200 Subject: [PATCH 27/74] chore: update rich-table print helper --- freqtrade/commands/list_commands.py | 2 +- freqtrade/util/rich_tables.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index ea2c84ae9..e0f5d2d62 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -280,7 +280,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: writer.writeheader() writer.writerows(tabular_data) else: - print_rich_table(summary_str, headers, tabular_data) + print_rich_table(tabular_data, headers, summary_str) elif not ( args.get("print_one_column", False) or args.get("list_pairs_print_json", False) diff --git a/freqtrade/util/rich_tables.py b/freqtrade/util/rich_tables.py index f773a4fd1..b8ec59d58 100644 --- a/freqtrade/util/rich_tables.py +++ b/freqtrade/util/rich_tables.py @@ -1,17 +1,22 @@ -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from rich.console import Console from rich.table import Table -def print_rich_table(summary: str, headers: List[str], tabular_data: List[Dict[str, Any]]) -> None: +def print_rich_table( + tabular_data: List[Dict[str, Any]], headers: List[str], summary: Optional[str] = None +) -> None: table = Table(title=summary) for header in headers: table.add_column(header, justify="right") for row in tabular_data: - table.add_row(*[str(row[header]) for header in headers]) + if isinstance(row, dict): + table.add_row(*[str(row[header]) for header in headers]) + else: + table.add_row(*row) console = Console() console.print(table) From c296a8cf829f4868e3cb30fc50db8ff2bea7605b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jul 2024 20:59:50 +0200 Subject: [PATCH 28/74] Improve rich tables interface --- freqtrade/util/rich_tables.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/freqtrade/util/rich_tables.py b/freqtrade/util/rich_tables.py index b8ec59d58..00c3302de 100644 --- a/freqtrade/util/rich_tables.py +++ b/freqtrade/util/rich_tables.py @@ -1,3 +1,4 @@ +import sys from typing import Any, Dict, List, Optional from rich.console import Console @@ -5,9 +6,13 @@ from rich.table import Table def print_rich_table( - tabular_data: List[Dict[str, Any]], headers: List[str], summary: Optional[str] = None + tabular_data: List[Dict[str, Any]], + headers: List[str], + summary: Optional[str] = None, + *, + table_kwargs: Optional[Dict[str, Any]] = None, ) -> None: - table = Table(title=summary) + table = Table(title=summary, **(table_kwargs or {})) for header in headers: table.add_column(header, justify="right") @@ -18,5 +23,7 @@ def print_rich_table( else: table.add_row(*row) - console = Console() + console = Console( + width=200 if "pytest" in sys.modules else None, + ) console.print(table) From 9f628309e92ce48fd894cbd926f06ca000f11dc2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jul 2024 21:00:34 +0200 Subject: [PATCH 29/74] feat: Update list-pairs command to use rich tables --- freqtrade/commands/data_commands.py | 61 +++++++++++++---------------- tests/commands/test_commands.py | 27 ++++++------- 2 files changed, 39 insertions(+), 49 deletions(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 92e60daa4..f3f56c7b2 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -16,6 +16,7 @@ from freqtrade.exceptions import ConfigurationError from freqtrade.exchange import timeframe_to_minutes from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist from freqtrade.resolvers import ExchangeResolver +from freqtrade.util import print_rich_table from freqtrade.util.migrations import migrate_data @@ -119,8 +120,6 @@ def start_list_data(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - from tabulate import tabulate - from freqtrade.data.history import get_datahandler dhc = get_datahandler(config["datadir"], config["dataformat_ohlcv"]) @@ -131,8 +130,7 @@ def start_list_data(args: Dict[str, Any]) -> None: if args["pairs"]: paircombs = [comb for comb in paircombs if comb[0] in args["pairs"]] - - print(f"Found {len(paircombs)} pair / timeframe combinations.") + title = f"Found {len(paircombs)} pair / timeframe combinations." if not config.get("show_timerange"): groupedpair = defaultdict(list) for pair, timeframe, candle_type in sorted( @@ -141,40 +139,35 @@ def start_list_data(args: Dict[str, Any]) -> None: groupedpair[(pair, candle_type)].append(timeframe) if groupedpair: - print( - tabulate( - [ - (pair, ", ".join(timeframes), candle_type) - for (pair, candle_type), timeframes in groupedpair.items() - ], - headers=("Pair", "Timeframe", "Type"), - tablefmt="psql", - stralign="right", - ) + print_rich_table( + [ + (pair, ", ".join(timeframes), candle_type) + for (pair, candle_type), timeframes in groupedpair.items() + ], + ("Pair", "Timeframe", "Type"), + title, + table_kwargs={"min_width": 50}, ) else: paircombs1 = [ (pair, timeframe, candle_type, *dhc.ohlcv_data_min_max(pair, timeframe, candle_type)) for pair, timeframe, candle_type in paircombs ] - - print( - tabulate( - [ - ( - pair, - timeframe, - candle_type, - start.strftime(DATETIME_PRINT_FORMAT), - end.strftime(DATETIME_PRINT_FORMAT), - length, - ) - for pair, timeframe, candle_type, start, end, length in sorted( - paircombs1, key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2]) - ) - ], - headers=("Pair", "Timeframe", "Type", "From", "To", "Candles"), - tablefmt="psql", - stralign="right", - ) + print_rich_table( + [ + ( + pair, + timeframe, + candle_type, + start.strftime(DATETIME_PRINT_FORMAT), + end.strftime(DATETIME_PRINT_FORMAT), + str(length), + ) + for pair, timeframe, candle_type, start, end, length in sorted( + paircombs1, key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2]) + ) + ], + ("Pair", "Timeframe", "Type", "From", "To", "Candles"), + summary=title, + table_kwargs={"min_width": 50}, ) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 7e4d2e2cc..687bff69f 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1633,8 +1633,8 @@ def test_start_list_data(testdatadir, capsys): start_list_data(pargs) captured = capsys.readouterr() assert "Found 16 pair / timeframe combinations." in captured.out - assert "\n| Pair | Timeframe | Type |\n" in captured.out - assert "\n| UNITTEST/BTC | 1m, 5m, 8m, 30m | spot |\n" in captured.out + assert re.search(r".*Pair.*Timeframe.*Type.*\n", captured.out) + assert re.search(r"\n.* UNITTEST/BTC .* 1m, 5m, 8m, 30m .* spot |\n", captured.out) args = [ "list-data", @@ -1650,9 +1650,9 @@ def test_start_list_data(testdatadir, capsys): start_list_data(pargs) captured = capsys.readouterr() assert "Found 2 pair / timeframe combinations." in captured.out - assert "\n| Pair | Timeframe | Type |\n" in captured.out + assert re.search(r".*Pair.*Timeframe.*Type.*\n", captured.out) assert "UNITTEST/BTC" not in captured.out - assert "\n| XRP/ETH | 1m, 5m | spot |\n" in captured.out + assert re.search(r"\n.* XRP/ETH .* 1m, 5m .* spot |\n", captured.out) args = [ "list-data", @@ -1667,9 +1667,9 @@ def test_start_list_data(testdatadir, capsys): captured = capsys.readouterr() assert "Found 6 pair / timeframe combinations." in captured.out - assert "\n| Pair | Timeframe | Type |\n" in captured.out - assert "\n| XRP/USDT:USDT | 5m, 1h | futures |\n" in captured.out - assert "\n| XRP/USDT:USDT | 1h, 8h | mark |\n" in captured.out + assert re.search(r".*Pair.*Timeframe.*Type.*\n", captured.out) + assert re.search(r"\n.* XRP/USDT:USDT .* 5m, 1h .* futures |\n", captured.out) + assert re.search(r"\n.* XRP/USDT:USDT .* 1h, 8h .* mark |\n", captured.out) args = [ "list-data", @@ -1684,15 +1684,12 @@ def test_start_list_data(testdatadir, capsys): start_list_data(pargs) captured = capsys.readouterr() assert "Found 2 pair / timeframe combinations." in captured.out - assert ( - "\n| Pair | Timeframe | Type " - "| From | To | Candles |\n" - ) in captured.out + assert re.search(r".*Pair.*Timeframe.*Type.*From .* To .* Candles .*\n", captured.out) assert "UNITTEST/BTC" not in captured.out - assert ( - "\n| XRP/ETH | 1m | spot | " - "2019-10-11 00:00:00 | 2019-10-13 11:19:00 | 2469 |\n" - ) in captured.out + assert re.search( + r"\n.* XRP/USDT .* 1m .* spot .* 2019-10-11 00:00:00 .* 2019-10-13 11:19:00 .* 2469 |\n", + captured.out, + ) @pytest.mark.usefixtures("init_persistence") From ffb0cf1a2c2217d08362c8d08ba91397a1718386 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 08:36:51 +0200 Subject: [PATCH 30/74] chore: Improve typing --- freqtrade/commands/list_commands.py | 8 ++++---- freqtrade/util/rich_tables.py | 10 +++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index e0f5d2d62..1696fc8f0 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -1,7 +1,7 @@ import csv import logging import sys -from typing import Any, Dict, List +from typing import Any, Dict, List, Union import rapidjson from rich.console import Console @@ -80,10 +80,10 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: def _print_objs_tabular(objs: List, print_colorized: bool) -> None: names = [s["name"] for s in objs] - objs_to_print = [ + objs_to_print: List[Dict[str, Union[Text, str]]] = [ { "name": Text(s["name"] if s["name"] else "--"), - "location": Text(s["location_rel"]), + "location": s["location_rel"], "status": ( Text("LOAD FAILED", style="bold red") if s["class"] is None @@ -103,7 +103,7 @@ def _print_objs_tabular(objs: List, print_colorized: bool) -> None: "sell-Params": str(len(s["hyperoptable"].get("sell", []))), } ) - table = Table(title="Available:") + table = Table() for header in objs_to_print[0].keys(): table.add_column(header.capitalize(), justify="right") diff --git a/freqtrade/util/rich_tables.py b/freqtrade/util/rich_tables.py index 00c3302de..d34162d66 100644 --- a/freqtrade/util/rich_tables.py +++ b/freqtrade/util/rich_tables.py @@ -1,13 +1,17 @@ import sys -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional, Sequence, Union from rich.console import Console from rich.table import Table +from rich.text import Text + + +TextOrString = Union[str, Text] def print_rich_table( - tabular_data: List[Dict[str, Any]], - headers: List[str], + tabular_data: Sequence[Union[Dict[str, Any], Sequence[TextOrString]]], + headers: Sequence[str], summary: Optional[str] = None, *, table_kwargs: Optional[Dict[str, Any]] = None, From cdae61e155c64db70ca6726a6cf579e08417e0b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 09:38:02 +0200 Subject: [PATCH 31/74] feat: use rich tables for entryexitanalysis --- freqtrade/data/entryexitanalysis.py | 3 ++- freqtrade/util/__init__.py | 3 ++- freqtrade/util/rich_tables.py | 35 ++++++++++++++++++++++++++++ tests/data/test_entryexitanalysis.py | 6 ++--- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index 9d936d295..3981423f7 100644 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -14,6 +14,7 @@ from freqtrade.data.btanalysis import ( load_backtest_stats, ) from freqtrade.exceptions import OperationalException +from freqtrade.util import print_df_rich_table logger = logging.getLogger(__name__) @@ -307,7 +308,7 @@ def _print_table( if name is not None: print(name) - print(tabulate(data, headers="keys", tablefmt="psql", showindex=show_index)) + print_df_rich_table(data, data.keys(), show_index=show_index) def process_entry_exit_reasons(config: Config): diff --git a/freqtrade/util/__init__.py b/freqtrade/util/__init__.py index f478829e6..5052e17dd 100644 --- a/freqtrade/util/__init__.py +++ b/freqtrade/util/__init__.py @@ -15,7 +15,7 @@ from freqtrade.util.formatters import decimals_per_coin, fmt_coin, round_value from freqtrade.util.ft_precise import FtPrecise from freqtrade.util.measure_time import MeasureTime from freqtrade.util.periodic_cache import PeriodicCache -from freqtrade.util.rich_tables import print_rich_table +from freqtrade.util.rich_tables import print_df_rich_table, print_rich_table from freqtrade.util.template_renderer import render_template, render_template_with_fallback # noqa @@ -38,4 +38,5 @@ __all__ = [ "fmt_coin", "MeasureTime", "print_rich_table", + "print_df_rich_table", ] diff --git a/freqtrade/util/rich_tables.py b/freqtrade/util/rich_tables.py index d34162d66..63715b70e 100644 --- a/freqtrade/util/rich_tables.py +++ b/freqtrade/util/rich_tables.py @@ -1,6 +1,7 @@ import sys from typing import Any, Dict, Optional, Sequence, Union +from pandas import DataFrame from rich.console import Console from rich.table import Table from rich.text import Text @@ -31,3 +32,37 @@ def print_rich_table( width=200 if "pytest" in sys.modules else None, ) console.print(table) + + +def _format_value(value: Any, *, floatfmt: str) -> str: + if isinstance(value, float): + return f"{value:{floatfmt}}" + return str(value) + + +def print_df_rich_table( + tabular_data: DataFrame, + headers: Sequence[str], + summary: Optional[str] = None, + *, + show_index=False, + index_name: Optional[str] = None, + table_kwargs: Optional[Dict[str, Any]] = None, +) -> None: + table = Table(title=summary, **(table_kwargs or {})) + + if show_index: + index_name = str(index_name) if index_name else tabular_data.index.name + table.add_column(index_name) + + for header in headers: + table.add_column(header, justify="right") + + for value_list in tabular_data.itertuples(index=show_index): + row = [_format_value(x, floatfmt=".3f") for x in value_list] + table.add_row(*row) + + console = Console( + width=200 if "pytest" in sys.modules else None, + ) + console.print(table) diff --git a/tests/data/test_entryexitanalysis.py b/tests/data/test_entryexitanalysis.py index 1a5309190..e7909c339 100644 --- a/tests/data/test_entryexitanalysis.py +++ b/tests/data/test_entryexitanalysis.py @@ -154,10 +154,10 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, use assert "-3.5" in captured.out assert "50" in captured.out assert "0" in captured.out - assert "0.01616" in captured.out + assert "0.016" in captured.out assert "34.049" in captured.out - assert "0.104411" in captured.out - assert "52.8292" in captured.out + assert "0.104" in captured.out + assert "52.829" in captured.out # test group 1 args = get_args(base_args + ["--analysis-groups", "1"]) From e705471946d103bdc78495dc95538060fe41f69f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 09:43:44 +0200 Subject: [PATCH 32/74] chore: don't return what ain't being needed --- freqtrade/optimize/analysis/lookahead_helpers.py | 2 +- freqtrade/optimize/analysis/recursive_helpers.py | 4 ++-- tests/optimize/test_lookahead_analysis.py | 10 +++++----- tests/optimize/test_recursive_analysis.py | 8 ++------ 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/freqtrade/optimize/analysis/lookahead_helpers.py b/freqtrade/optimize/analysis/lookahead_helpers.py index c0e6fa1ba..b502716dc 100644 --- a/freqtrade/optimize/analysis/lookahead_helpers.py +++ b/freqtrade/optimize/analysis/lookahead_helpers.py @@ -64,7 +64,7 @@ class LookaheadAnalysisSubFunctions: table = tabulate(data, headers=headers, tablefmt="orgtbl") print(table) - return table, headers, data + return data @staticmethod def export_to_csv(config: Dict[str, Any], lookahead_analysis: List[LookaheadAnalysis]): diff --git a/freqtrade/optimize/analysis/recursive_helpers.py b/freqtrade/optimize/analysis/recursive_helpers.py index cde1a214e..90ac8ed1d 100644 --- a/freqtrade/optimize/analysis/recursive_helpers.py +++ b/freqtrade/optimize/analysis/recursive_helpers.py @@ -34,9 +34,9 @@ class RecursiveAnalysisSubFunctions: table = tabulate(data, headers=headers, tablefmt="orgtbl") print(table) - return table, headers, data + return data - return None, None, data + return data @staticmethod def calculate_config_overrides(config: Config): diff --git a/tests/optimize/test_lookahead_analysis.py b/tests/optimize/test_lookahead_analysis.py index 88e3ad877..f7d38b24b 100644 --- a/tests/optimize/test_lookahead_analysis.py +++ b/tests/optimize/test_lookahead_analysis.py @@ -147,7 +147,7 @@ def test_lookahead_helper_text_table_lookahead_analysis_instances(lookahead_conf instance = LookaheadAnalysis(lookahead_conf, strategy_obj) instance.current_analysis = analysis - _table, _headers, data = LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances( + data = LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances( lookahead_conf, [instance] ) @@ -163,14 +163,14 @@ def test_lookahead_helper_text_table_lookahead_analysis_instances(lookahead_conf analysis.false_exit_signals = 10 instance = LookaheadAnalysis(lookahead_conf, strategy_obj) instance.current_analysis = analysis - _table, _headers, data = LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances( + data = LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances( lookahead_conf, [instance] ) assert data[0][2].__contains__("error") # edit it into not showing an error instance.failed_bias_check = False - _table, _headers, data = LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances( + data = LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances( lookahead_conf, [instance] ) assert data[0][0] == "strategy_test_v3_with_lookahead_bias.py" @@ -183,7 +183,7 @@ def test_lookahead_helper_text_table_lookahead_analysis_instances(lookahead_conf analysis.false_indicators.append("falseIndicator1") analysis.false_indicators.append("falseIndicator2") - _table, _headers, data = LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances( + data = LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances( lookahead_conf, [instance] ) @@ -193,7 +193,7 @@ def test_lookahead_helper_text_table_lookahead_analysis_instances(lookahead_conf assert len(data) == 1 # check amount of multiple rows - _table, _headers, data = LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances( + data = LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances( lookahead_conf, [instance, instance, instance] ) assert len(data) == 3 diff --git a/tests/optimize/test_recursive_analysis.py b/tests/optimize/test_recursive_analysis.py index 2969b4153..e16c82d24 100644 --- a/tests/optimize/test_recursive_analysis.py +++ b/tests/optimize/test_recursive_analysis.py @@ -105,9 +105,7 @@ def test_recursive_helper_text_table_recursive_analysis_instances(recursive_conf instance = RecursiveAnalysis(recursive_conf, strategy_obj) instance.dict_recursive = dict_diff - _table, _headers, data = RecursiveAnalysisSubFunctions.text_table_recursive_analysis_instances( - [instance] - ) + data = RecursiveAnalysisSubFunctions.text_table_recursive_analysis_instances([instance]) # check row contents for a try that has too few signals assert data[0][0] == "rsi" @@ -118,9 +116,7 @@ def test_recursive_helper_text_table_recursive_analysis_instances(recursive_conf dict_diff = dict() instance = RecursiveAnalysis(recursive_conf, strategy_obj) instance.dict_recursive = dict_diff - _table, _headers, data = RecursiveAnalysisSubFunctions.text_table_recursive_analysis_instances( - [instance] - ) + data = RecursiveAnalysisSubFunctions.text_table_recursive_analysis_instances([instance]) assert len(data) == 0 From 5e88bd231dd0f5d347c2939e04e8ee1646a278ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 09:56:49 +0200 Subject: [PATCH 33/74] feat: lookahead-heplpers -> rich table --- freqtrade/optimize/analysis/lookahead_helpers.py | 10 ++++++---- freqtrade/util/rich_tables.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/analysis/lookahead_helpers.py b/freqtrade/optimize/analysis/lookahead_helpers.py index b502716dc..a8fb1cd35 100644 --- a/freqtrade/optimize/analysis/lookahead_helpers.py +++ b/freqtrade/optimize/analysis/lookahead_helpers.py @@ -4,11 +4,13 @@ from pathlib import Path from typing import Any, Dict, List import pandas as pd +from rich.text import Text from freqtrade.constants import Config from freqtrade.exceptions import OperationalException from freqtrade.optimize.analysis.lookahead import LookaheadAnalysis from freqtrade.resolvers import StrategyResolver +from freqtrade.util import print_rich_table logger = logging.getLogger(__name__) @@ -53,17 +55,17 @@ class LookaheadAnalysisSubFunctions: [ inst.strategy_obj["location"].parts[-1], inst.strategy_obj["name"], - inst.current_analysis.has_bias, + Text("Yes", style="bold red") + if inst.current_analysis.has_bias + else Text("No", style="bold green"), inst.current_analysis.total_signals, inst.current_analysis.false_entry_signals, inst.current_analysis.false_exit_signals, ", ".join(inst.current_analysis.false_indicators), ] ) - from tabulate import tabulate - table = tabulate(data, headers=headers, tablefmt="orgtbl") - print(table) + print_rich_table(data, headers, summary="Lookahead Analysis") return data @staticmethod diff --git a/freqtrade/util/rich_tables.py b/freqtrade/util/rich_tables.py index 63715b70e..926eba916 100644 --- a/freqtrade/util/rich_tables.py +++ b/freqtrade/util/rich_tables.py @@ -26,7 +26,7 @@ def print_rich_table( if isinstance(row, dict): table.add_row(*[str(row[header]) for header in headers]) else: - table.add_row(*row) + table.add_row(*[r if isinstance(r, Text) else str(r) for r in row]) console = Console( width=200 if "pytest" in sys.modules else None, From 8e2f28955ee04d74f85fdc2facefdbe119acfdf1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 10:05:17 +0200 Subject: [PATCH 34/74] feat: rich table for recursive analysis --- freqtrade/optimize/analysis/recursive_helpers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/analysis/recursive_helpers.py b/freqtrade/optimize/analysis/recursive_helpers.py index 90ac8ed1d..be596fa68 100644 --- a/freqtrade/optimize/analysis/recursive_helpers.py +++ b/freqtrade/optimize/analysis/recursive_helpers.py @@ -7,6 +7,7 @@ from freqtrade.constants import Config from freqtrade.exceptions import OperationalException from freqtrade.optimize.analysis.recursive import RecursiveAnalysis from freqtrade.resolvers import StrategyResolver +from freqtrade.util import print_rich_table logger = logging.getLogger(__name__) @@ -16,9 +17,9 @@ class RecursiveAnalysisSubFunctions: @staticmethod def text_table_recursive_analysis_instances(recursive_instances: List[RecursiveAnalysis]): startups = recursive_instances[0]._startup_candle - headers = ["indicators"] + headers = ["Indicators"] for candle in startups: - headers.append(candle) + headers.append(str(candle)) data = [] for inst in recursive_instances: @@ -30,10 +31,8 @@ class RecursiveAnalysisSubFunctions: data.append(temp_data) if len(data) > 0: - from tabulate import tabulate + print_rich_table(data, headers, summary="Recursive Analysis") - table = tabulate(data, headers=headers, tablefmt="orgtbl") - print(table) return data return data From 7adc3c2ef57663158490a1bb2ef306cde689cd06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 12:47:27 +0200 Subject: [PATCH 35/74] Improve rich_tables generic --- freqtrade/util/rich_tables.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/freqtrade/util/rich_tables.py b/freqtrade/util/rich_tables.py index 926eba916..66e2d70dc 100644 --- a/freqtrade/util/rich_tables.py +++ b/freqtrade/util/rich_tables.py @@ -3,7 +3,7 @@ from typing import Any, Dict, Optional, Sequence, Union from pandas import DataFrame from rich.console import Console -from rich.table import Table +from rich.table import Column, Table from rich.text import Text @@ -17,16 +17,23 @@ def print_rich_table( *, table_kwargs: Optional[Dict[str, Any]] = None, ) -> None: - table = Table(title=summary, **(table_kwargs or {})) - - for header in headers: - table.add_column(header, justify="right") + table = Table( + *[c if isinstance(c, Column) else Column(c, justify="right") for c in headers], + title=summary, + **(table_kwargs or {}), + ) for row in tabular_data: if isinstance(row, dict): - table.add_row(*[str(row[header]) for header in headers]) + table.add_row( + *[ + row[header] if isinstance(row[header], (Text, Table)) else str(row[header]) + for header in headers + ] + ) + else: - table.add_row(*[r if isinstance(r, Text) else str(r) for r in row]) + table.add_row(*[r if isinstance(r, (Text, Table)) else str(r) for r in row]) console = Console( width=200 if "pytest" in sys.modules else None, From dba7f9968b7d219757abee6270e2e9c60f57d520 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 12:58:44 +0200 Subject: [PATCH 36/74] chore: fix minor type gotcha --- freqtrade/optimize/hyperopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b19fca9dc..99b78d09f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -260,7 +260,7 @@ class Hyperopt: result["max_open_trades"] = {"max_open_trades": strategy.max_open_trades} return result - def print_results(self, results) -> None: + def print_results(self, results: Dict[str, Any]) -> None: """ Log results if it is better than any previous evaluation TODO: this should be moved to HyperoptTools too @@ -271,7 +271,7 @@ class Hyperopt: print( HyperoptTools.get_result_table( self.config, - results, + [results], self.total_epochs, self.print_all, self.print_colorized, From 296bf9dc1d86ffb5accbc9fd7e85c67ac5f7b288 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 13:05:37 +0200 Subject: [PATCH 37/74] chore: Fix unused import --- freqtrade/data/entryexitanalysis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index 3981423f7..e76f2dff7 100644 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -4,7 +4,6 @@ from typing import List import joblib import pandas as pd -from tabulate import tabulate from freqtrade.configuration import TimeRange from freqtrade.constants import Config From 4d5b330b777b7b3c6d8fb7ec14fce9519bfb2ff4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 13:08:52 +0200 Subject: [PATCH 38/74] Improve rich generic --- freqtrade/util/rich_tables.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/util/rich_tables.py b/freqtrade/util/rich_tables.py index 66e2d70dc..23fb15d1f 100644 --- a/freqtrade/util/rich_tables.py +++ b/freqtrade/util/rich_tables.py @@ -27,13 +27,15 @@ def print_rich_table( if isinstance(row, dict): table.add_row( *[ - row[header] if isinstance(row[header], (Text, Table)) else str(row[header]) + row[header] if isinstance(row[header], Text) else str(row[header]) for header in headers ] ) else: - table.add_row(*[r if isinstance(r, (Text, Table)) else str(r) for r in row]) + table.add_row( + *[r if isinstance(r, Text) else str(r) for r in row], # type: ignore[arg-type] + ) console = Console( width=200 if "pytest" in sys.modules else None, From bc60855b93d2b66c75ad89ae3ac4b4d33700aea9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 13:18:50 +0200 Subject: [PATCH 39/74] chore: pre-commit now needs rich types --- .pre-commit-config.yaml | 1 + build_helpers/pre_commit_update.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8fa4bf9ee..44134826c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,6 +20,7 @@ repos: - types-tabulate==0.9.0.20240106 - types-python-dateutil==2.9.0.20240316 - SQLAlchemy==2.0.31 + - rich==13.7.1 # stages: [push] - repo: https://github.com/pycqa/isort diff --git a/build_helpers/pre_commit_update.py b/build_helpers/pre_commit_update.py index 9d313efd2..2e97e3ed0 100644 --- a/build_helpers/pre_commit_update.py +++ b/build_helpers/pre_commit_update.py @@ -18,7 +18,9 @@ with require.open("r") as rfile: # Extract types only type_reqs = [ - r.strip("\n") for r in requirements if r.startswith("types-") or r.startswith("SQLAlchemy") + r.strip("\n") + for r in requirements + if r.startswith("types-") or r.startswith("SQLAlchemy") or r.startswith("rich") ] with pre_commit_file.open("r") as file: From 8c807f00bbd0b3d161525fd5d30ecd494142a925 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 15:10:14 +0200 Subject: [PATCH 40/74] Extract row before exploding for better typing --- freqtrade/util/rich_tables.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/util/rich_tables.py b/freqtrade/util/rich_tables.py index 23fb15d1f..d9762c9fb 100644 --- a/freqtrade/util/rich_tables.py +++ b/freqtrade/util/rich_tables.py @@ -1,5 +1,5 @@ import sys -from typing import Any, Dict, Optional, Sequence, Union +from typing import Any, Dict, List, Optional, Sequence, Union from pandas import DataFrame from rich.console import Console @@ -33,9 +33,8 @@ def print_rich_table( ) else: - table.add_row( - *[r if isinstance(r, Text) else str(r) for r in row], # type: ignore[arg-type] - ) + row_to_add: List[Union[str, Text]] = [r if isinstance(r, Text) else str(r) for r in row] + table.add_row(*row_to_add) console = Console( width=200 if "pytest" in sys.modules else None, From 483a829d0e750bf0a8a14dc9201f17df7267dce2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 15:10:33 +0200 Subject: [PATCH 41/74] Revert "chore: pre-commit now needs rich types" This reverts commit bc60855b93d2b66c75ad89ae3ac4b4d33700aea9. --- .pre-commit-config.yaml | 1 - build_helpers/pre_commit_update.py | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 44134826c..8fa4bf9ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,6 @@ repos: - types-tabulate==0.9.0.20240106 - types-python-dateutil==2.9.0.20240316 - SQLAlchemy==2.0.31 - - rich==13.7.1 # stages: [push] - repo: https://github.com/pycqa/isort diff --git a/build_helpers/pre_commit_update.py b/build_helpers/pre_commit_update.py index 2e97e3ed0..9d313efd2 100644 --- a/build_helpers/pre_commit_update.py +++ b/build_helpers/pre_commit_update.py @@ -18,9 +18,7 @@ with require.open("r") as rfile: # Extract types only type_reqs = [ - r.strip("\n") - for r in requirements - if r.startswith("types-") or r.startswith("SQLAlchemy") or r.startswith("rich") + r.strip("\n") for r in requirements if r.startswith("types-") or r.startswith("SQLAlchemy") ] with pre_commit_file.open("r") as file: From 69628736b287e9e96e9de92c051b9f0b87073c35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 15:29:58 +0200 Subject: [PATCH 42/74] feat: add hyperopt output functionality --- freqtrade/commands/hyperopt_commands.py | 10 +++ freqtrade/optimize/hyperopt_output.py | 107 ++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 freqtrade/optimize/hyperopt_output.py diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index ac0b8453f..746fafe92 100644 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -18,6 +18,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: """ List hyperopt epochs previously evaluated """ + from freqtrade.optimize.hyperopt_output import HyperoptOutput from freqtrade.optimize.hyperopt_tools import HyperoptTools config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) @@ -50,6 +51,15 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: 0, ) ) + hot = HyperoptOutput() + hot.add_data( + config, + epochs, + total_epochs, + not config.get("hyperopt_list_best", False), + ) + hot.print(print_colorized=print_colorized) + except KeyboardInterrupt: print("User interrupted..") diff --git a/freqtrade/optimize/hyperopt_output.py b/freqtrade/optimize/hyperopt_output.py new file mode 100644 index 000000000..f7cc217ad --- /dev/null +++ b/freqtrade/optimize/hyperopt_output.py @@ -0,0 +1,107 @@ +import sys +from typing import List, Optional, Union + +from rich.console import Console +from rich.table import Table +from rich.text import Text + +from freqtrade.constants import Config +from freqtrade.optimize.optimize_reports import generate_wins_draws_losses +from freqtrade.util import fmt_coin + + +class HyperoptOutput: + def __init__(self): + self._table = Table( + title="Hyperopt results", + ) + # Headers + self._table.add_column("Best", justify="left") + self._table.add_column("Epoch", justify="right") + self._table.add_column("Trades", justify="right") + self._table.add_column("Win Draw Loss Win%", justify="right") + self._table.add_column("Avg profit", justify="right") + self._table.add_column("Profit", justify="right") + self._table.add_column("Avg duration", justify="right") + self._table.add_column("Objective", justify="right") + self._table.add_column("Max Drawdown (Acct)", justify="right") + + def _add_row(self, data: List[Union[str, Text]]): + """Add single row""" + row_to_add: List[Union[str, Text]] = [r if isinstance(r, Text) else str(r) for r in data] + + self._table.add_row(*row_to_add) + + def _add_rows(self, data: List[List[Union[str, Text]]]): + """add multiple rows""" + for row in data: + self._add_row(row) + + def print(self, console: Optional[Console] = None, *, print_colorized=True): + if not console: + console = Console( + color_system="auto" if print_colorized else None, + width=200 if "pytest" in sys.modules else None, + ) + + console.print(self._table) + + def add_data( + self, + config: Config, + results: list, + total_epochs: int, + highlight_best: bool, + ) -> str: + """Format one or multiple rows and add them""" + stake_currency = config["stake_currency"] + + res = [ + [ + # "Best": + ( + ("*" if r["is_initial_point"] or r["is_random"] else "") + + (" Best" if r["is_best"] else "") + ).lstrip(), + # "Epoch": + f"{r['current_epoch']}/{total_epochs}", + # "Trades": + r["results_metrics"]["total_trades"], + # "Win Draw Loss Win%": + generate_wins_draws_losses( + r["results_metrics"]["wins"], + r["results_metrics"]["draws"], + r["results_metrics"]["losses"], + ), + # "Avg profit": + f"{r['results_metrics']['profit_mean']:.2%}", + # "Profit": + "{} {}".format( + fmt_coin( + r["results_metrics"]["profit_total_abs"], + stake_currency, + keep_trailing_zeros=True, + ), + f"({r['results_metrics']['profit_total']:,.2%})".rjust(10, " "), + ) + if r["results_metrics"]["profit_total_abs"] != 0.0 + else "--", + # "Avg duration": + r["results_metrics"]["holding_avg"], + # "Objective": + f"{r["loss"]:,.5f}" if r["loss"] != 100000 else "N/A", + # "Max Drawdown (Acct)": + "{} {}".format( + fmt_coin( + r["results_metrics"]["max_drawdown_abs"], + stake_currency, + keep_trailing_zeros=True, + ), + (f"({r["results_metrics"]['max_drawdown_account']:,.2%})").rjust(10, " "), + ) + if r["results_metrics"]["max_drawdown_account"] != 0.0 + else "--", + ] + for r in results + ] + self._add_rows(res) From befefd449c5dbb93e39a6dacbbe78cc6f876202f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 16:13:55 +0200 Subject: [PATCH 43/74] Add a generic Progressbar which allows printing additional objects --- freqtrade/util/__init__.py | 2 ++ freqtrade/util/rich_progress.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 freqtrade/util/rich_progress.py diff --git a/freqtrade/util/__init__.py b/freqtrade/util/__init__.py index 5052e17dd..76902b176 100644 --- a/freqtrade/util/__init__.py +++ b/freqtrade/util/__init__.py @@ -15,6 +15,7 @@ from freqtrade.util.formatters import decimals_per_coin, fmt_coin, round_value from freqtrade.util.ft_precise import FtPrecise from freqtrade.util.measure_time import MeasureTime from freqtrade.util.periodic_cache import PeriodicCache +from freqtrade.util.rich_progress import CustomProgress from freqtrade.util.rich_tables import print_df_rich_table, print_rich_table from freqtrade.util.template_renderer import render_template, render_template_with_fallback # noqa @@ -39,4 +40,5 @@ __all__ = [ "MeasureTime", "print_rich_table", "print_df_rich_table", + "CustomProgress", ] diff --git a/freqtrade/util/rich_progress.py b/freqtrade/util/rich_progress.py new file mode 100644 index 000000000..6cf138bf5 --- /dev/null +++ b/freqtrade/util/rich_progress.py @@ -0,0 +1,12 @@ +from rich.console import ConsoleRenderable, Group, RichCast +from rich.progress import Progress + + +class CustomProgress(Progress): + def __init__(self, *args, cust_objs, **kwargs) -> None: + self._cust_objs = cust_objs + super().__init__(*args, **kwargs) + + def get_renderable(self) -> ConsoleRenderable | RichCast | str: + renderable = Group(*self._cust_objs, *self.get_renderables()) + return renderable From f05c019757d6647c4a6a1f2467ff664009830a1d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 16:15:09 +0200 Subject: [PATCH 44/74] feat: Update output for hyperopt to Rich --- freqtrade/optimize/hyperopt.py | 24 ++++++++++++------------ freqtrade/optimize/hyperopt_output.py | 26 +++++++++++++------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 99b78d09f..8d2f8846a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -18,10 +18,10 @@ from colorama import init as colorama_init from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects from joblib.externals import cloudpickle from pandas import DataFrame +from rich.align import Align from rich.progress import ( BarColumn, MofNCompleteColumn, - Progress, TaskProgressColumn, TextColumn, TimeElapsedColumn, @@ -40,6 +40,7 @@ from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss +from freqtrade.optimize.hyperopt_output import HyperoptOutput from freqtrade.optimize.hyperopt_tools import ( HyperoptStateContainer, HyperoptTools, @@ -47,6 +48,7 @@ from freqtrade.optimize.hyperopt_tools import ( ) from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver +from freqtrade.util import CustomProgress # Suppress scikit-learn FutureWarnings from skopt @@ -86,6 +88,8 @@ class Hyperopt: self.max_open_trades_space: List[Dimension] = [] self.dimensions: List[Dimension] = [] + self._hyper_out: HyperoptOutput = HyperoptOutput() + self.config = config self.min_date: datetime self.max_date: datetime @@ -268,17 +272,12 @@ class Hyperopt: is_best = results["is_best"] if self.print_all or is_best: - print( - HyperoptTools.get_result_table( - self.config, - [results], - self.total_epochs, - self.print_all, - self.print_colorized, - self.hyperopt_table_header, - ) + self._hyper_out.add_data( + self.config, + [results], + self.total_epochs, + self.print_all, ) - self.hyperopt_table_header = 2 def init_spaces(self): """ @@ -635,7 +634,7 @@ class Hyperopt: logger.info(f"Effective number of parallel workers used: {jobs}") # Define progressbar - with Progress( + with CustomProgress( TextColumn("[progress.description]{task.description}"), BarColumn(bar_width=None), MofNCompleteColumn(), @@ -645,6 +644,7 @@ class Hyperopt: "•", TimeRemainingColumn(), expand=True, + cust_objs=[Align.center(self._hyper_out.table)], ) as pbar: task = pbar.add_task("Epochs", total=self.total_epochs) diff --git a/freqtrade/optimize/hyperopt_output.py b/freqtrade/optimize/hyperopt_output.py index f7cc217ad..2f4e9aff8 100644 --- a/freqtrade/optimize/hyperopt_output.py +++ b/freqtrade/optimize/hyperopt_output.py @@ -12,25 +12,25 @@ from freqtrade.util import fmt_coin class HyperoptOutput: def __init__(self): - self._table = Table( + self.table = Table( title="Hyperopt results", ) # Headers - self._table.add_column("Best", justify="left") - self._table.add_column("Epoch", justify="right") - self._table.add_column("Trades", justify="right") - self._table.add_column("Win Draw Loss Win%", justify="right") - self._table.add_column("Avg profit", justify="right") - self._table.add_column("Profit", justify="right") - self._table.add_column("Avg duration", justify="right") - self._table.add_column("Objective", justify="right") - self._table.add_column("Max Drawdown (Acct)", justify="right") + self.table.add_column("Best", justify="left") + self.table.add_column("Epoch", justify="right") + self.table.add_column("Trades", justify="right") + self.table.add_column("Win Draw Loss Win%", justify="right") + self.table.add_column("Avg profit", justify="right") + self.table.add_column("Profit", justify="right") + self.table.add_column("Avg duration", justify="right") + self.table.add_column("Objective", justify="right") + self.table.add_column("Max Drawdown (Acct)", justify="right") def _add_row(self, data: List[Union[str, Text]]): """Add single row""" row_to_add: List[Union[str, Text]] = [r if isinstance(r, Text) else str(r) for r in data] - self._table.add_row(*row_to_add) + self.table.add_row(*row_to_add) def _add_rows(self, data: List[List[Union[str, Text]]]): """add multiple rows""" @@ -44,7 +44,7 @@ class HyperoptOutput: width=200 if "pytest" in sys.modules else None, ) - console.print(self._table) + console.print(self.table) def add_data( self, @@ -52,7 +52,7 @@ class HyperoptOutput: results: list, total_epochs: int, highlight_best: bool, - ) -> str: + ) -> None: """Format one or multiple rows and add them""" stake_currency = config["stake_currency"] From 8f0ac0aaeae1db678f6c69136c4c375cbab2a0be Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 16:27:53 +0200 Subject: [PATCH 45/74] Remove old output from hyperopt-list --- freqtrade/commands/hyperopt_commands.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 746fafe92..e6c264051 100644 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -41,24 +41,14 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: if not export_csv: try: - print( - HyperoptTools.get_result_table( - config, - epochs, - total_epochs, - not config.get("hyperopt_list_best", False), - print_colorized, - 0, - ) - ) - hot = HyperoptOutput() - hot.add_data( + h_out = HyperoptOutput() + h_out.add_data( config, epochs, total_epochs, not config.get("hyperopt_list_best", False), ) - hot.print(print_colorized=print_colorized) + h_out.print(print_colorized=print_colorized) except KeyboardInterrupt: print("User interrupted..") From 4d6f399131b72cca6bf36102fa35d2b6e672879d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 16:28:02 +0200 Subject: [PATCH 46/74] Implement colors for hyperopt-output --- freqtrade/optimize/hyperopt_output.py | 104 ++++++++++++++------------ 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/freqtrade/optimize/hyperopt_output.py b/freqtrade/optimize/hyperopt_output.py index 2f4e9aff8..145ac758e 100644 --- a/freqtrade/optimize/hyperopt_output.py +++ b/freqtrade/optimize/hyperopt_output.py @@ -56,52 +56,60 @@ class HyperoptOutput: """Format one or multiple rows and add them""" stake_currency = config["stake_currency"] - res = [ - [ - # "Best": - ( - ("*" if r["is_initial_point"] or r["is_random"] else "") - + (" Best" if r["is_best"] else "") - ).lstrip(), - # "Epoch": - f"{r['current_epoch']}/{total_epochs}", - # "Trades": - r["results_metrics"]["total_trades"], - # "Win Draw Loss Win%": - generate_wins_draws_losses( - r["results_metrics"]["wins"], - r["results_metrics"]["draws"], - r["results_metrics"]["losses"], + for r in results: + self.table.add_row( + *[ + # "Best": + ( + ("*" if r["is_initial_point"] or r["is_random"] else "") + + (" Best" if r["is_best"] else "") + ).lstrip(), + # "Epoch": + f"{r['current_epoch']}/{total_epochs}", + # "Trades": + str(r["results_metrics"]["total_trades"]), + # "Win Draw Loss Win%": + generate_wins_draws_losses( + r["results_metrics"]["wins"], + r["results_metrics"]["draws"], + r["results_metrics"]["losses"], + ), + # "Avg profit": + f"{r['results_metrics']['profit_mean']:.2%}", + # "Profit": + Text( + "{} {}".format( + fmt_coin( + r["results_metrics"]["profit_total_abs"], + stake_currency, + keep_trailing_zeros=True, + ), + f"({r['results_metrics']['profit_total']:,.2%})".rjust(10, " "), + ) + if r["results_metrics"]["profit_total_abs"] != 0.0 + else "--", + style="green" if r["results_metrics"]["profit_total_abs"] > 0 else "red", + ), + # "Avg duration": + r["results_metrics"]["holding_avg"], + # "Objective": + f"{r["loss"]:,.5f}" if r["loss"] != 100000 else "N/A", + # "Max Drawdown (Acct)": + "{} {}".format( + fmt_coin( + r["results_metrics"]["max_drawdown_abs"], + stake_currency, + keep_trailing_zeros=True, + ), + (f"({r["results_metrics"]['max_drawdown_account']:,.2%})").rjust(10, " "), + ) + if r["results_metrics"]["max_drawdown_account"] != 0.0 + else "--", + ], + style=" ".join( + [ + "bold " if r["is_best"] and highlight_best else "", + "italic " if r["is_initial_point"] else "", + ] ), - # "Avg profit": - f"{r['results_metrics']['profit_mean']:.2%}", - # "Profit": - "{} {}".format( - fmt_coin( - r["results_metrics"]["profit_total_abs"], - stake_currency, - keep_trailing_zeros=True, - ), - f"({r['results_metrics']['profit_total']:,.2%})".rjust(10, " "), - ) - if r["results_metrics"]["profit_total_abs"] != 0.0 - else "--", - # "Avg duration": - r["results_metrics"]["holding_avg"], - # "Objective": - f"{r["loss"]:,.5f}" if r["loss"] != 100000 else "N/A", - # "Max Drawdown (Acct)": - "{} {}".format( - fmt_coin( - r["results_metrics"]["max_drawdown_abs"], - stake_currency, - keep_trailing_zeros=True, - ), - (f"({r["results_metrics"]['max_drawdown_account']:,.2%})").rjust(10, " "), - ) - if r["results_metrics"]["max_drawdown_account"] != 0.0 - else "--", - ] - for r in results - ] - self._add_rows(res) + ) From f51b63fc37e7b41331e5fb353e46ef5addde8c3b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 16:33:47 +0200 Subject: [PATCH 47/74] Fix wrong type for live running --- freqtrade/optimize/hyperopt_output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_output.py b/freqtrade/optimize/hyperopt_output.py index 145ac758e..cd89cf035 100644 --- a/freqtrade/optimize/hyperopt_output.py +++ b/freqtrade/optimize/hyperopt_output.py @@ -91,7 +91,7 @@ class HyperoptOutput: style="green" if r["results_metrics"]["profit_total_abs"] > 0 else "red", ), # "Avg duration": - r["results_metrics"]["holding_avg"], + str(r["results_metrics"]["holding_avg"]), # "Objective": f"{r["loss"]:,.5f}" if r["loss"] != 100000 else "N/A", # "Max Drawdown (Acct)": From 879797e7c5014e61411c5c53c7fcfac471b7d2fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 16:58:46 +0200 Subject: [PATCH 48/74] chore: remove no longer used result formatting methods --- freqtrade/optimize/hyperopt_tools.py | 174 --------------------------- 1 file changed, 174 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 50c55c43d..975338cd5 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -5,10 +5,7 @@ from pathlib import Path from typing import Any, Dict, Iterator, List, Optional, Tuple import numpy as np -import pandas as pd import rapidjson -import tabulate -from colorama import Fore, Style from pandas import isna, json_normalize from freqtrade.constants import FTHYPT_FILEVERSION, Config @@ -16,8 +13,6 @@ from freqtrade.enums import HyperoptState from freqtrade.exceptions import OperationalException from freqtrade.misc import deep_merge_dicts, round_dict, safe_value_fallback2 from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs -from freqtrade.optimize.optimize_reports import generate_wins_draws_losses -from freqtrade.util import fmt_coin logger = logging.getLogger(__name__) @@ -357,175 +352,6 @@ class HyperoptTools: + f"Objective: {results['loss']:.5f}" ) - @staticmethod - def prepare_trials_columns(trials: pd.DataFrame) -> pd.DataFrame: - trials["Best"] = "" - - if "results_metrics.winsdrawslosses" not in trials.columns: - # Ensure compatibility with older versions of hyperopt results - trials["results_metrics.winsdrawslosses"] = "N/A" - - has_account_drawdown = "results_metrics.max_drawdown_account" in trials.columns - if not has_account_drawdown: - # Ensure compatibility with older versions of hyperopt results - trials["results_metrics.max_drawdown_account"] = None - if "is_random" not in trials.columns: - trials["is_random"] = False - - # New mode, using backtest result for metrics - trials["results_metrics.winsdrawslosses"] = trials.apply( - lambda x: generate_wins_draws_losses( - x["results_metrics.wins"], x["results_metrics.draws"], x["results_metrics.losses"] - ), - axis=1, - ) - - trials = trials[ - [ - "Best", - "current_epoch", - "results_metrics.total_trades", - "results_metrics.winsdrawslosses", - "results_metrics.profit_mean", - "results_metrics.profit_total_abs", - "results_metrics.profit_total", - "results_metrics.holding_avg", - "results_metrics.max_drawdown_account", - "results_metrics.max_drawdown_abs", - "loss", - "is_initial_point", - "is_random", - "is_best", - ] - ] - - trials.columns = [ - "Best", - "Epoch", - "Trades", - " Win Draw Loss Win%", - "Avg profit", - "Total profit", - "Profit", - "Avg duration", - "max_drawdown_account", - "max_drawdown_abs", - "Objective", - "is_initial_point", - "is_random", - "is_best", - ] - - return trials - - @staticmethod - def get_result_table( - config: Config, - results: list, - total_epochs: int, - highlight_best: bool, - print_colorized: bool, - remove_header: int, - ) -> str: - """ - Log result table - """ - if not results: - return "" - - tabulate.PRESERVE_WHITESPACE = True - trials = json_normalize(results, max_level=1) - - trials = HyperoptTools.prepare_trials_columns(trials) - - trials["is_profit"] = False - trials.loc[trials["is_initial_point"] | trials["is_random"], "Best"] = "* " - trials.loc[trials["is_best"], "Best"] = "Best" - trials.loc[ - (trials["is_initial_point"] | trials["is_random"]) & trials["is_best"], "Best" - ] = "* Best" - trials.loc[trials["Total profit"] > 0, "is_profit"] = True - trials["Trades"] = trials["Trades"].astype(str) - # perc_multi = 1 if legacy_mode else 100 - trials["Epoch"] = trials["Epoch"].apply( - lambda x: "{}/{}".format(str(x).rjust(len(str(total_epochs)), " "), total_epochs) - ) - trials["Avg profit"] = trials["Avg profit"].apply( - lambda x: f"{x:,.2%}".rjust(7, " ") if not isna(x) else "--".rjust(7, " ") - ) - trials["Avg duration"] = trials["Avg duration"].apply( - lambda x: ( - f"{x:,.1f} m".rjust(7, " ") - if isinstance(x, float) - else f"{x}" - if not isna(x) - else "--".rjust(7, " ") - ) - ) - trials["Objective"] = trials["Objective"].apply( - lambda x: f"{x:,.5f}".rjust(8, " ") if x != 100000 else "N/A".rjust(8, " ") - ) - - stake_currency = config["stake_currency"] - - trials["Max Drawdown (Acct)"] = trials.apply( - lambda x: ( - "{} {}".format( - fmt_coin(x["max_drawdown_abs"], stake_currency, keep_trailing_zeros=True), - (f"({x['max_drawdown_account']:,.2%})").rjust(10, " "), - ).rjust(25 + len(stake_currency)) - if x["max_drawdown_account"] != 0.0 - else "--".rjust(25 + len(stake_currency)) - ), - axis=1, - ) - - trials = trials.drop(columns=["max_drawdown_abs", "max_drawdown_account"]) - - trials["Profit"] = trials.apply( - lambda x: ( - "{} {}".format( - fmt_coin(x["Total profit"], stake_currency, keep_trailing_zeros=True), - f"({x['Profit']:,.2%})".rjust(10, " "), - ).rjust(25 + len(stake_currency)) - if x["Total profit"] != 0.0 - else "--".rjust(25 + len(stake_currency)) - ), - axis=1, - ) - trials = trials.drop(columns=["Total profit"]) - - if print_colorized: - trials2 = trials.astype(str) - for i in range(len(trials)): - if trials.loc[i]["is_profit"]: - for j in range(len(trials.loc[i]) - 3): - trials2.iat[i, j] = f"{Fore.GREEN}{str(trials.iloc[i, j])}{Fore.RESET}" - if trials.loc[i]["is_best"] and highlight_best: - for j in range(len(trials.loc[i]) - 3): - trials2.iat[i, j] = ( - f"{Style.BRIGHT}{str(trials.iloc[i, j])}{Style.RESET_ALL}" - ) - trials = trials2 - del trials2 - trials = trials.drop(columns=["is_initial_point", "is_best", "is_profit", "is_random"]) - if remove_header > 0: - table = tabulate.tabulate( - trials.to_dict(orient="list"), tablefmt="orgtbl", headers="keys", stralign="right" - ) - - table = table.split("\n", remove_header)[remove_header] - elif remove_header < 0: - table = tabulate.tabulate( - trials.to_dict(orient="list"), tablefmt="psql", headers="keys", stralign="right" - ) - table = "\n".join(table.split("\n")[0:remove_header]) - else: - table = tabulate.tabulate( - trials.to_dict(orient="list"), tablefmt="psql", headers="keys", stralign="right" - ) - return table - @staticmethod def export_csv_file(config: Config, results: list, csv_file: str) -> None: """ From 62320a361ebc3ea47b2d7e118facdd134aad1928 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 17:04:22 +0200 Subject: [PATCH 49/74] chore: fix now failing test --- tests/optimize/test_hyperopt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 2e9ae8d35..a9f697629 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -291,9 +291,10 @@ def test_log_results_if_loss_improves(hyperopt, capsys) -> None: "is_best": True, } ) + hyperopt._hyper_out.print() out, _err = capsys.readouterr() assert all( - x in out for x in ["Best", "2/2", " 1", "0.10%", "0.00100000 BTC (1.00%)", "00:20:00"] + x in out for x in ["Best", "2/2", "1", "0.10%", "0.00100000 BTC (1.00%)", "0:20:00"] ) From 004e1101e7cbfba3c8493d6f08527591a49b7dd0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 17:11:54 +0200 Subject: [PATCH 50/74] Improve resiliance, drop compatibility test --- freqtrade/optimize/hyperopt_output.py | 10 +++++++--- tests/conftest_hyperopt.py | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt_output.py b/freqtrade/optimize/hyperopt_output.py index cd89cf035..81a7ec6bd 100644 --- a/freqtrade/optimize/hyperopt_output.py +++ b/freqtrade/optimize/hyperopt_output.py @@ -75,7 +75,9 @@ class HyperoptOutput: r["results_metrics"]["losses"], ), # "Avg profit": - f"{r['results_metrics']['profit_mean']:.2%}", + f"{r['results_metrics']['profit_mean']:.2%}" + if r["results_metrics"]["profit_mean"] is not None + else "--", # "Profit": Text( "{} {}".format( @@ -86,9 +88,11 @@ class HyperoptOutput: ), f"({r['results_metrics']['profit_total']:,.2%})".rjust(10, " "), ) - if r["results_metrics"]["profit_total_abs"] != 0.0 + if r["results_metrics"].get("profit_total_abs", 0) != 0.0 else "--", - style="green" if r["results_metrics"]["profit_total_abs"] > 0 else "red", + style="green" + if r["results_metrics"].get("profit_total_abs", 0) > 0 + else "red", ), # "Avg duration": str(r["results_metrics"]["holding_avg"]), diff --git a/tests/conftest_hyperopt.py b/tests/conftest_hyperopt.py index af4039a3c..315b138cf 100644 --- a/tests/conftest_hyperopt.py +++ b/tests/conftest_hyperopt.py @@ -324,7 +324,8 @@ def hyperopt_test_result(): "profit_mean": None, "profit_median": None, "profit_total": 0, - "profit": 0.0, + "max_drawdown_account": 0.0, + "max_drawdown_abs": 0.0, "holding_avg": timedelta(), }, # noqa: E501 "results_explanation": " 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.", # noqa: E501 From 94565d0d39808f023e63f47b6b69eb902a43af42 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2024 20:03:49 +0200 Subject: [PATCH 51/74] "best" should be shown in gold --- freqtrade/optimize/hyperopt_output.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt_output.py b/freqtrade/optimize/hyperopt_output.py index 81a7ec6bd..9e0bbbb5b 100644 --- a/freqtrade/optimize/hyperopt_output.py +++ b/freqtrade/optimize/hyperopt_output.py @@ -90,9 +90,13 @@ class HyperoptOutput: ) if r["results_metrics"].get("profit_total_abs", 0) != 0.0 else "--", - style="green" - if r["results_metrics"].get("profit_total_abs", 0) > 0 - else "red", + style=( + "green" + if r["results_metrics"].get("profit_total_abs", 0) > 0 + else "red" + ) + if not r["is_best"] + else "", ), # "Avg duration": str(r["results_metrics"]["holding_avg"]), @@ -112,7 +116,7 @@ class HyperoptOutput: ], style=" ".join( [ - "bold " if r["is_best"] and highlight_best else "", + "bold gold1" if r["is_best"] and highlight_best else "", "italic " if r["is_initial_point"] else "", ] ), From c0b20f88a51be04546107f5d5b17ed3ab1e6f458 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 03:10:12 +0000 Subject: [PATCH 52/74] chore(deps): bump mkdocs-material in the mkdocs group Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material). Updates `mkdocs-material` from 9.5.27 to 9.5.28 - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.27...9.5.28) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch dependency-group: mkdocs ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 845aba7bb..27c1a125e 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.6 mkdocs==1.6.0 -mkdocs-material==9.5.27 +mkdocs-material==9.5.28 mdx_truly_sane_lists==1.3 pymdown-extensions==10.8.1 jinja2==3.1.4 From 71c29aa3673234f1ae15a84fab4c47abbcce62c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 03:10:19 +0000 Subject: [PATCH 53/74] chore(deps): bump scikit-learn from 1.5.0 to 1.5.1 Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.5.0 to 1.5.1. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.5.0...1.5.1) --- updated-dependencies: - dependency-name: scikit-learn dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-freqai.txt | 2 +- requirements-hyperopt.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index d864b45e8..c57e66d2d 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt # Required for freqai -scikit-learn==1.5.0 +scikit-learn==1.5.1 joblib==1.4.2 catboost==1.2.5; 'arm' not in platform_machine lightgbm==4.4.0 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 30ae0fac1..6475525de 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -4,6 +4,6 @@ # Required for hyperopt scipy==1.14.0; python_version >= "3.10" scipy==1.13.1; python_version < "3.10" -scikit-learn==1.5.0 +scikit-learn==1.5.1 ft-scikit-optimize==0.9.2 filelock==3.15.4 From 7e52124d8ae16559ff8d9ed2cc5d0cd69a5162ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 03:10:32 +0000 Subject: [PATCH 54/74] chore(deps): bump ccxt from 4.3.54 to 4.3.58 Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.3.54 to 4.3.58. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/4.3.54...4.3.58) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 170e5d805..8ba83afd2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ bottleneck==1.4.0 numexpr==2.10.1 pandas-ta==0.3.14b -ccxt==4.3.54 +ccxt==4.3.58 cryptography==42.0.8 aiohttp==3.9.5 SQLAlchemy==2.0.31 From 91fe16344e343dc9fbcb263b1a6d4da19d0f8d0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 03:10:42 +0000 Subject: [PATCH 55/74] chore(deps): bump orjson from 3.10.5 to 3.10.6 Bumps [orjson](https://github.com/ijl/orjson) from 3.10.5 to 3.10.6. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.10.5...3.10.6) --- updated-dependencies: - dependency-name: orjson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 170e5d805..a20c46598 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ py_find_1st==1.1.6 # Load ticker files 30% faster python-rapidjson==1.18 # Properly format api responses -orjson==3.10.5 +orjson==3.10.6 # Notify systemd sdnotify==0.3.2 From ddb4cb853af635b4908e39e56ef48349e70b78bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 03:11:01 +0000 Subject: [PATCH 56/74] chore(deps): bump pydantic from 2.7.4 to 2.8.2 Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.7.4 to 2.8.2. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v2.7.4...v2.8.2) --- updated-dependencies: - dependency-name: pydantic dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 170e5d805..322a6f444 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,7 +39,7 @@ sdnotify==0.3.2 # API Server fastapi==0.111.0 -pydantic==2.7.4 +pydantic==2.8.2 uvicorn==0.30.1 pyjwt==2.8.0 aiofiles==24.1.0 From 4d2b6d6d6e249c4351ba0718adfe979ba048e7c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 03:11:11 +0000 Subject: [PATCH 57/74] chore(deps-dev): bump ruff from 0.5.0 to 0.5.1 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.5.0 to 0.5.1. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.5.0...0.5.1) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f8d811257..8f529475b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==4.0.1 -ruff==0.5.0 +ruff==0.5.1 mypy==1.10.1 pre-commit==3.7.1 pytest==8.2.2 From 28f4e1c06898daba2b1c907343f84d96c9c2cc6c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jul 2024 06:38:29 +0200 Subject: [PATCH 58/74] Colorama is not necessary for hyperopt commands anymore --- freqtrade/commands/hyperopt_commands.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index e6c264051..d89d25796 100644 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -2,8 +2,6 @@ import logging from operator import itemgetter from typing import Any, Dict -from colorama import init as colorama_init - from freqtrade.configuration import setup_utils_configuration from freqtrade.data.btanalysis import get_latest_hyperopt_file from freqtrade.enums import RunMode @@ -36,9 +34,6 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: # Previous evaluations epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config) - if print_colorized: - colorama_init(autoreset=True) - if not export_csv: try: h_out = HyperoptOutput() From 49a60fa67f6be852eee390bbc7aa40bc6db6763d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jul 2024 06:44:21 +0200 Subject: [PATCH 59/74] Properly support "--no-color" for hyperopt --- freqtrade/optimize/hyperopt.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 8d2f8846a..b411e7752 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -14,11 +14,11 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple import rapidjson -from colorama import init as colorama_init from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects from joblib.externals import cloudpickle from pandas import DataFrame from rich.align import Align +from rich.console import Console from rich.progress import ( BarColumn, MofNCompleteColumn, @@ -625,13 +625,13 @@ class Hyperopt: self.opt = self.get_optimizer(self.dimensions, config_jobs) - if self.print_colorized: - colorama_init(autoreset=True) - try: with Parallel(n_jobs=config_jobs) as parallel: jobs = parallel._effective_n_jobs() logger.info(f"Effective number of parallel workers used: {jobs}") + console = Console( + color_system="auto" if self.print_colorized else None, + ) # Define progressbar with CustomProgress( @@ -644,6 +644,7 @@ class Hyperopt: "•", TimeRemainingColumn(), expand=True, + console=console, cust_objs=[Align.center(self._hyper_out.table)], ) as pbar: task = pbar.add_task("Epochs", total=self.total_epochs) From b208f978dbf8ebc00757fdfb73bc10be6fadafc6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jul 2024 06:44:52 +0200 Subject: [PATCH 60/74] Remove dependency on colorama --- requirements.txt | 2 -- setup.py | 1 - 2 files changed, 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 170e5d805..791e7fe08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -45,8 +45,6 @@ pyjwt==2.8.0 aiofiles==24.1.0 psutil==6.0.0 -# Support for colorized terminal output -colorama==0.4.6 # Building config files interactively questionary==2.0.1 prompt-toolkit==3.0.36 diff --git a/setup.py b/setup.py index 82e529767..6963862e0 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,6 @@ setup( "py_find_1st", "python-rapidjson", "orjson", - "colorama", "jinja2", "questionary", "prompt-toolkit", From 453ea0dbcc7c13ab3c7b19eb8f75d0af7f19b438 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jul 2024 07:05:56 +0200 Subject: [PATCH 61/74] update test for bt-output stability --- tests/optimize/test_optimize_reports.py | 29 +++++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index b26f15be5..8ea6be547 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -508,20 +508,25 @@ def test_text_table_strategy(testdatadir): bt_res_data_comparison = bt_res_data.pop("strategy_comparison") - result_str = ( - "| Strategy | Trades | Avg Profit % | Tot Profit BTC |" - " Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n" - "|----------------+----------+----------------+------------------+" - "----------------+----------------+-------------------------+-----------------------|\n" - "| StrategyTestV2 | 179 | 0.08 | 0.02608550 |" - " 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |\n" - "| TestStrategy | 179 | 0.08 | 0.02608550 |" - " 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |" - ) - strategy_results = generate_strategy_comparison(bt_stats=bt_res_data["strategy"]) assert strategy_results == bt_res_data_comparison - assert text_table_strategy(strategy_results, "BTC") == result_str + text = text_table_strategy(strategy_results, "BTC") + + assert re.search( + r".* Strategy .* Trades .* Avg Profit % .* Tot Profit BTC .* Tot Profit % .* " + r"Avg Duration .* Win Draw Loss Win% .* Drawdown .*", + text, + ) + assert re.search( + r".*StrategyTestV2 .* 179 .* 0.08 .* 0.02608550 .* " + r"260.85 .* 3:40:00 .* 170 0 9 95.0 .* 0.00308222 BTC 8.67%.*", + text, + ) + assert re.search( + r".*TestStrategy .* 179 .* 0.08 .* 0.02608550 .* " + r"260.85 .* 3:40:00 .* 170 0 9 95.0 .* 0.00308222 BTC 8.67%.*", + text, + ) def test_generate_edge_table(): From ae92807b5c3165ff0a1038d21d37b2c86cdf7c45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jul 2024 07:12:51 +0200 Subject: [PATCH 62/74] chore: Update more test reliability --- tests/optimize/test_optimize_reports.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 8ea6be547..750d15729 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -532,14 +532,10 @@ def test_text_table_strategy(testdatadir): def test_generate_edge_table(): results = {} results["ETH/BTC"] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60) - assert generate_edge_table(results).count("+") == 7 - assert generate_edge_table(results).count("| ETH/BTC |") == 1 - assert ( - generate_edge_table(results).count( - "| Risk Reward Ratio | Required Risk Reward | Expectancy |" - ) - == 1 - ) + text = generate_edge_table(results) + assert text.count("+") == 7 + assert text.count("| ETH/BTC |") == 1 + assert re.search(r".* Risk Reward Ratio .* Required Risk Reward .* Expectancy .*", text) def test_generate_periodic_breakdown_stats(testdatadir): From 76a34efe3640881ee5355ebf697bd5c42c013837 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jul 2024 07:17:58 +0200 Subject: [PATCH 63/74] chore: improve backtest test resiliance --- tests/optimize/test_optimize_reports.py | 57 ++++++++++++++----------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 750d15729..9e141e22d 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -69,21 +69,22 @@ def test_text_table_bt_results(): } ) - result_str = ( - "| Pair | Trades | Avg Profit % | Tot Profit BTC | " - "Tot Profit % | Avg Duration | Win Draw Loss Win% |\n" - "|---------+----------+----------------+------------------+" - "----------------+----------------+-------------------------|\n" - "| ETH/BTC | 3 | 8.33 | 0.50000000 | " - "12.50 | 0:20:00 | 2 0 1 66.7 |\n" - "| TOTAL | 3 | 8.33 | 0.50000000 | " - "12.50 | 0:20:00 | 2 0 1 66.7 |" - ) - pair_results = generate_pair_metrics( ["ETH/BTC"], stake_currency="BTC", starting_balance=4, results=results ) - assert text_table_bt_results(pair_results, stake_currency="BTC") == result_str + text = text_table_bt_results(pair_results, stake_currency="BTC") + re.search( + r".* Pair .* Trades .* Avg Profit % .* Tot Profit BTC .* Tot Profit % .* " + r"Avg Duration .* Win Draw Loss Win% .*", + text, + ) + re.search( + r".* ETH/BTC .* 3 .* 8.33 .* 0.50000000 .* 12.50 .* 0:20:00 .* 2 0 1 66.7 .*", + text, + ) + re.search( + r".* TOTAL .* 3 .* 8.33 .* 0.50000000 .* 12.50 .* 0:20:00 .* 2 0 1 66.7 .*", text + ) def test_generate_backtest_stats(default_conf, testdatadir, tmp_path): @@ -448,23 +449,27 @@ def test_text_table_exit_reason(): } ) - result_str = ( - "| Exit Reason | Exits | Avg Profit % | Tot Profit BTC | Tot Profit % |" - " Avg Duration | Win Draw Loss Win% |\n" - "|---------------+---------+----------------+------------------+----------------+" - "----------------+-------------------------|\n" - "| roi | 2 | 15.00 | 0.60000000 | 2.73 |" - " 0:20:00 | 2 0 0 100 |\n" - "| stop_loss | 1 | -10.00 | -0.20000000 | -0.91 |" - " 0:10:00 | 0 0 1 0 |\n" - "| TOTAL | 3 | 6.67 | 0.40000000 | 1.82 |" - " 0:17:00 | 2 0 1 66.7 |" - ) - exit_reason_stats = generate_tag_metrics( "exit_reason", starting_balance=22, results=results, skip_nan=False ) - assert text_table_tags("exit_tag", exit_reason_stats, "BTC") == result_str + text = text_table_tags("exit_tag", exit_reason_stats, "BTC") + + assert re.search( + r".* Exit Reason .* Exits .* Avg Profit % .* Tot Profit BTC .* Tot Profit % .* " + r"Avg Duration .* Win Draw Loss Win% .*", + text, + ) + assert re.search( + r".* roi .* 2 .* 15.00 .* 0.60000000 .* 2.73 .* 0:20:00 .* 2 0 0 100 .*", + text, + ) + assert re.search( + r".* stop_loss .* 1 .* -10.00 .* -0.20000000 .* -0.91 .* 0:10:00 .* 0 0 1 0 .*", + text, + ) + assert re.search( + r".* TOTAL .* 3 .* 6.67 .* 0.40000000 .* 1.82 .* 0:17:00 .* 2 0 1 66.7 .*", text + ) def test_generate_sell_reason_stats(): From 64d22bbd89a4590847c075111d2f8d3218446412 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jul 2024 18:20:39 +0200 Subject: [PATCH 64/74] chore: Fix fstring incompatibility with older python versions --- freqtrade/optimize/hyperopt_output.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_output.py b/freqtrade/optimize/hyperopt_output.py index 9e0bbbb5b..72e049745 100644 --- a/freqtrade/optimize/hyperopt_output.py +++ b/freqtrade/optimize/hyperopt_output.py @@ -101,7 +101,7 @@ class HyperoptOutput: # "Avg duration": str(r["results_metrics"]["holding_avg"]), # "Objective": - f"{r["loss"]:,.5f}" if r["loss"] != 100000 else "N/A", + f"{r['loss']:,.5f}" if r["loss"] != 100000 else "N/A", # "Max Drawdown (Acct)": "{} {}".format( fmt_coin( @@ -109,7 +109,7 @@ class HyperoptOutput: stake_currency, keep_trailing_zeros=True, ), - (f"({r["results_metrics"]['max_drawdown_account']:,.2%})").rjust(10, " "), + (f"({r['results_metrics']['max_drawdown_account']:,.2%})").rjust(10, " "), ) if r["results_metrics"]["max_drawdown_account"] != 0.0 else "--", From 0e870ab47c27e053f785ae9dfbb167b988e96102 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jul 2024 19:15:05 +0200 Subject: [PATCH 65/74] chore: don't use pipe operator, not supported on 3.9 --- freqtrade/util/rich_progress.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/util/rich_progress.py b/freqtrade/util/rich_progress.py index 6cf138bf5..d295dafd5 100644 --- a/freqtrade/util/rich_progress.py +++ b/freqtrade/util/rich_progress.py @@ -1,3 +1,5 @@ +from typing import Union + from rich.console import ConsoleRenderable, Group, RichCast from rich.progress import Progress @@ -7,6 +9,6 @@ class CustomProgress(Progress): self._cust_objs = cust_objs super().__init__(*args, **kwargs) - def get_renderable(self) -> ConsoleRenderable | RichCast | str: + def get_renderable(self) -> Union[ConsoleRenderable, RichCast, str]: renderable = Group(*self._cust_objs, *self.get_renderables()) return renderable From c679a6aab2c5c438308a86d60d481f86df60db69 Mon Sep 17 00:00:00 2001 From: xmatthias <5024695+xmatthias@users.noreply.github.com> Date: Tue, 9 Jul 2024 03:03:43 +0000 Subject: [PATCH 66/74] chore: update pre-commit hooks --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8fa4bf9ee..5d8903496 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.5.0' + rev: 'v0.5.1' hooks: - id: ruff From 5765bb4a4f83291649f65ed248ba73a9c09dfb21 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jul 2024 07:07:50 +0200 Subject: [PATCH 67/74] feat: Update initial bt table --- .../optimize/optimize_reports/bt_output.py | 20 ++++++++----------- tests/optimize/test_optimize_reports.py | 6 ++++-- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index f20d7f190..0dac7f199 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -6,7 +6,7 @@ from tabulate import tabulate from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config from freqtrade.optimize.optimize_reports.optimize_reports import generate_periodic_breakdown_stats from freqtrade.types import BacktestResultType -from freqtrade.util import decimals_per_coin, fmt_coin +from freqtrade.util import decimals_per_coin, fmt_coin, print_rich_table logger = logging.getLogger(__name__) @@ -146,14 +146,13 @@ def text_table_periodic_breakdown( return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") -def text_table_strategy(strategy_results, stake_currency: str) -> str: +def text_table_strategy(strategy_results, stake_currency: str, title: str): """ Generate summary table per strategy :param strategy_results: Dict of containing results for all strategies :param stake_currency: stake-currency - used to correctly name headers :return: pretty printed table with tabulate as string """ - floatfmt = _get_line_floatfmt(stake_currency) headers = _get_line_header("Strategy", stake_currency, "Trades") # _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless # therefore we slip this column in only for strategy summary here. @@ -177,8 +176,8 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: [ t["key"], t["trades"], - t["profit_mean_pct"], - t["profit_total_abs"], + f"{t['profit_mean_pct']:.2f}", + f"{t['profit_total_abs']:.{decimals_per_coin(stake_currency)}f}", t["profit_total_pct"], t["duration_avg"], generate_wins_draws_losses(t["wins"], t["draws"], t["losses"]), @@ -186,8 +185,7 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: ] for t, drawdown in zip(strategy_results, drawdown) ] - # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate(output, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") + print_rich_table(output, headers, summary=title) def text_table_add_metrics(strat_results: Dict) -> str: @@ -472,15 +470,13 @@ def show_backtest_results(config: Config, backtest_stats: BacktestResultType): if len(backtest_stats["strategy"]) > 0: # Print Strategy summary table - table = text_table_strategy(backtest_stats["strategy_comparison"], stake_currency) print( f"Backtested {results['backtest_start']} -> {results['backtest_end']} |" f" Max open trades : {results['max_open_trades']}" ) - print(" STRATEGY SUMMARY ".center(len(table.splitlines()[0]), "=")) - print(table) - print("=" * len(table.splitlines()[0])) - print("\nFor more details, please look at the detail tables above") + text_table_strategy( + backtest_stats["strategy_comparison"], stake_currency, "STRATEGY SUMMARY" + ) def show_sorted_pairlist(config: Config, backtest_stats: BacktestResultType): diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 9e141e22d..733f822d8 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -507,7 +507,7 @@ def test_generate_sell_reason_stats(): assert stop_result["profit_mean_pct"] == round(stop_result["profit_mean"] * 100, 2) -def test_text_table_strategy(testdatadir): +def test_text_table_strategy(testdatadir, capsys): filename = testdatadir / "backtest_results/backtest-result_multistrat.json" bt_res_data = load_backtest_stats(filename) @@ -515,8 +515,10 @@ def test_text_table_strategy(testdatadir): strategy_results = generate_strategy_comparison(bt_stats=bt_res_data["strategy"]) assert strategy_results == bt_res_data_comparison - text = text_table_strategy(strategy_results, "BTC") + text_table_strategy(strategy_results, "BTC", "STRATEGY SUMMARY") + captured = capsys.readouterr() + text = captured.out assert re.search( r".* Strategy .* Trades .* Avg Profit % .* Tot Profit BTC .* Tot Profit % .* " r"Avg Duration .* Win Draw Loss Win% .* Drawdown .*", From a605ae20a72d1ebb29b4b83858f6e6c98cb59e3a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Jul 2024 06:43:12 +0200 Subject: [PATCH 68/74] feat: initial backtest table to rich --- .../optimize/optimize_reports/bt_output.py | 25 +++++++++---------- tests/optimize/test_optimize_reports.py | 5 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index 0dac7f199..c1cf28a8b 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -46,22 +46,24 @@ def generate_wins_draws_losses(wins, draws, losses): return f"{wins:>4} {draws:>4} {losses:>4} {wl_ratio:>4}" -def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: str) -> str: +def text_table_bt_results( + pair_results: List[Dict[str, Any]], stake_currency: str, title: str +) -> str: """ Generates and returns a text table for the given backtest data and the results dataframe :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row :param stake_currency: stake-currency - used to correctly name headers + :param title: Title of the table :return: pretty printed table with tabulate as string """ headers = _get_line_header("Pair", stake_currency, "Trades") - floatfmt = _get_line_floatfmt(stake_currency) output = [ [ t["key"], t["trades"], t["profit_mean_pct"], - t["profit_total_abs"], + f"{t['profit_total_abs']:.{decimals_per_coin(stake_currency)}f}", t["profit_total_pct"], t["duration_avg"], generate_wins_draws_losses(t["wins"], t["draws"], t["losses"]), @@ -69,7 +71,7 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st for t in pair_results ] # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate(output, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") + print_rich_table(output, headers, summary=title) def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str: @@ -422,15 +424,12 @@ def show_backtest_result( """ # Print results print(f"Result for strategy {strategy}") - table = text_table_bt_results(results["results_per_pair"], stake_currency=stake_currency) - if isinstance(table, str): - print(" BACKTESTING REPORT ".center(len(table.splitlines()[0]), "=")) - print(table) - - table = text_table_bt_results(results["left_open_trades"], stake_currency=stake_currency) - if isinstance(table, str) and len(table) > 0: - print(" LEFT OPEN TRADES REPORT ".center(len(table.splitlines()[0]), "=")) - print(table) + text_table_bt_results( + results["results_per_pair"], stake_currency=stake_currency, title="BACKTESTING REPORT" + ) + text_table_bt_results( + results["left_open_trades"], stake_currency=stake_currency, title="LEFT OPEN TRADES REPORT" + ) _show_tag_subresults(results, stake_currency) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 733f822d8..dafe599c9 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -59,7 +59,7 @@ def _backup_file(file: Path, copy_file: bool = False) -> None: copyfile(file_swp, file) -def test_text_table_bt_results(): +def test_text_table_bt_results(capsys): results = pd.DataFrame( { "pair": ["ETH/BTC", "ETH/BTC", "ETH/BTC"], @@ -72,7 +72,8 @@ def test_text_table_bt_results(): pair_results = generate_pair_metrics( ["ETH/BTC"], stake_currency="BTC", starting_balance=4, results=results ) - text = text_table_bt_results(pair_results, stake_currency="BTC") + text_table_bt_results(pair_results, stake_currency="BTC", title="title") + text = capsys.readouterr().out re.search( r".* Pair .* Trades .* Avg Profit % .* Tot Profit BTC .* Tot Profit % .* " r"Avg Duration .* Win Draw Loss Win% .*", From 315351b573142a3974d3abe4e6cf23623b5d974f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Jul 2024 06:49:33 +0200 Subject: [PATCH 69/74] feat: Tag table -> rich --- .../optimize/optimize_reports/bt_output.py | 37 ++++++++----------- tests/optimize/test_optimize_reports.py | 9 +++-- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index c1cf28a8b..7da9bb02c 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Literal, Union from tabulate import tabulate @@ -74,7 +74,11 @@ def text_table_bt_results( print_rich_table(output, headers, summary=title) -def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str: +def text_table_tags( + tag_type: Literal["enter_tag", "exit_tag", "exit_reason"], + tag_results: List[Dict[str, Any]], + stake_currency: str, +) -> str: """ Generates and returns a text table for the given backtest data and the results dataframe :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row @@ -85,12 +89,15 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr fallback: str = "" is_list = False if tag_type == "enter_tag": - headers = _get_line_header("Enter Tag", stake_currency, "Entries") + title = "Enter Tag" + headers = _get_line_header(title, stake_currency, "Entries") elif tag_type == "exit_tag": - headers = _get_line_header("Exit Reason", stake_currency, "Exits") + title = "Exit Reason" + headers = _get_line_header(title, stake_currency, "Exits") fallback = "exit_reason" else: # Mix tag + title = "Mixed Tag" headers = _get_line_header(["Enter Tag", "Exit Reason"], stake_currency, "Trades") floatfmt.insert(0, "s") is_list = True @@ -108,7 +115,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr ), t["trades"], t["profit_mean_pct"], - t["profit_total_abs"], + f"{t['profit_total_abs']:.{decimals_per_coin(stake_currency)}f}", t["profit_total_pct"], t.get("duration_avg"), generate_wins_draws_losses(t["wins"], t["draws"], t["losses"]), @@ -116,7 +123,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr for t in tag_results ] # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate(output, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") + print_rich_table(output, headers, summary=f"{title.upper()} STATS") def text_table_periodic_breakdown( @@ -395,25 +402,13 @@ def _show_tag_subresults(results: Dict[str, Any], stake_currency: str): Print tag subresults (enter_tag, exit_reason_summary, mix_tag_stats) """ if (enter_tags := results.get("results_per_enter_tag")) is not None: - table = text_table_tags("enter_tag", enter_tags, stake_currency) - - if isinstance(table, str) and len(table) > 0: - print(" ENTER TAG STATS ".center(len(table.splitlines()[0]), "=")) - print(table) + text_table_tags("enter_tag", enter_tags, stake_currency) if (exit_reasons := results.get("exit_reason_summary")) is not None: - table = text_table_tags("exit_tag", exit_reasons, stake_currency) - - if isinstance(table, str) and len(table) > 0: - print(" EXIT REASON STATS ".center(len(table.splitlines()[0]), "=")) - print(table) + text_table_tags("exit_tag", exit_reasons, stake_currency) if (mix_tag := results.get("mix_tag_stats")) is not None: - table = text_table_tags("mix_tag", mix_tag, stake_currency) - - if isinstance(table, str) and len(table) > 0: - print(" MIXED TAG STATS ".center(len(table.splitlines()[0]), "=")) - print(table) + text_table_tags("mix_tag", mix_tag, stake_currency) def show_backtest_result( diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index dafe599c9..53366a234 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -436,7 +436,7 @@ def test_calc_streak(testdatadir): assert calc_streak(bt_data) == (7, 18) -def test_text_table_exit_reason(): +def test_text_table_exit_reason(capsys): results = pd.DataFrame( { "pair": ["ETH/BTC", "ETH/BTC", "ETH/BTC"], @@ -453,7 +453,8 @@ def test_text_table_exit_reason(): exit_reason_stats = generate_tag_metrics( "exit_reason", starting_balance=22, results=results, skip_nan=False ) - text = text_table_tags("exit_tag", exit_reason_stats, "BTC") + text_table_tags("exit_tag", exit_reason_stats, "BTC") + text = capsys.readouterr().out assert re.search( r".* Exit Reason .* Exits .* Avg Profit % .* Tot Profit BTC .* Tot Profit % .* " @@ -461,11 +462,11 @@ def test_text_table_exit_reason(): text, ) assert re.search( - r".* roi .* 2 .* 15.00 .* 0.60000000 .* 2.73 .* 0:20:00 .* 2 0 0 100 .*", + r".* roi .* 2 .* 15.0 .* 0.60000000 .* 2.73 .* 0:20:00 .* 2 0 0 100 .*", text, ) assert re.search( - r".* stop_loss .* 1 .* -10.00 .* -0.20000000 .* -0.91 .* 0:10:00 .* 0 0 1 0 .*", + r".* stop_loss .* 1 .* -10.0 .* -0.20000000 .* -0.91 .* 0:10:00 .* 0 0 1 0 .*", text, ) assert re.search( From 06a0264f2bedd41a25da1b750cf6d351a84f6473 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Jul 2024 06:51:11 +0200 Subject: [PATCH 70/74] feat: period breakdown -> rich table --- freqtrade/optimize/optimize_reports/bt_output.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index 7da9bb02c..9d5bebb11 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -152,7 +152,7 @@ def text_table_periodic_breakdown( ] for d in days_breakdown_stats ] - return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") + print_rich_table(output, headers, summary=f"{period.upper()} BREAKDOWN") def text_table_strategy(strategy_results, stake_currency: str, title: str): @@ -435,12 +435,9 @@ def show_backtest_result( days_breakdown_stats = generate_periodic_breakdown_stats( trade_list=results["trades"], period=period ) - table = text_table_periodic_breakdown( + text_table_periodic_breakdown( days_breakdown_stats=days_breakdown_stats, stake_currency=stake_currency, period=period ) - if isinstance(table, str) and len(table) > 0: - print(f" {period.upper()} BREAKDOWN ".center(len(table.splitlines()[0]), "=")) - print(table) table = text_table_add_metrics(results) if isinstance(table, str) and len(table) > 0: From 2d31314eb4478cc9b17f6be343a2847fc415c60a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Jul 2024 06:54:04 +0200 Subject: [PATCH 71/74] feat: bt-result to tabulate --- freqtrade/optimize/optimize_reports/bt_output.py | 12 +++--------- freqtrade/util/rich_tables.py | 3 ++- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index 9d5bebb11..3ef35236b 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -379,8 +379,8 @@ def text_table_add_metrics(strat_results: Dict) -> str: *drawdown_metrics, ("Market change", f"{strat_results['market_change']:.2%}"), ] + print_rich_table(metrics, ["Metric", "Value"], summary="SUMMARY METRICS", justify="left") - return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl") else: start_balance = fmt_coin(strat_results["starting_balance"], strat_results["stake_currency"]) stake_amount = ( @@ -394,7 +394,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: f"Your starting balance was {start_balance}, " f"and your stake was {stake_amount}." ) - return message + print(message) def _show_tag_subresults(results: Dict[str, Any], stake_currency: str): @@ -439,13 +439,7 @@ def show_backtest_result( days_breakdown_stats=days_breakdown_stats, stake_currency=stake_currency, period=period ) - table = text_table_add_metrics(results) - if isinstance(table, str) and len(table) > 0: - print(" SUMMARY METRICS ".center(len(table.splitlines()[0]), "=")) - print(table) - - if isinstance(table, str) and len(table) > 0: - print("=" * len(table.splitlines()[0])) + text_table_add_metrics(results) print() diff --git a/freqtrade/util/rich_tables.py b/freqtrade/util/rich_tables.py index d9762c9fb..d36bf9004 100644 --- a/freqtrade/util/rich_tables.py +++ b/freqtrade/util/rich_tables.py @@ -15,10 +15,11 @@ def print_rich_table( headers: Sequence[str], summary: Optional[str] = None, *, + justify="right", table_kwargs: Optional[Dict[str, Any]] = None, ) -> None: table = Table( - *[c if isinstance(c, Column) else Column(c, justify="right") for c in headers], + *[c if isinstance(c, Column) else Column(c, justify=justify) for c in headers], title=summary, **(table_kwargs or {}), ) From f9af6591535b6f0165cb9a0d99fb899074133299 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Jul 2024 07:08:10 +0200 Subject: [PATCH 72/74] feat: edge-cli output to rich --- freqtrade/optimize/edge_cli.py | 2 +- .../optimize/optimize_reports/bt_output.py | 17 ++++++----------- tests/optimize/test_optimize_reports.py | 8 ++++---- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 9bd8ff1c9..65dd30efc 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -52,4 +52,4 @@ class EdgeCli: result = self.edge.calculate(self.config["exchange"]["pair_whitelist"]) if result: print("") # blank line for readability - print(generate_edge_table(self.edge._cached_pairs)) + generate_edge_table(self.edge._cached_pairs) diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index 3ef35236b..c88a9bd06 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -475,7 +475,6 @@ def show_sorted_pairlist(config: Config, backtest_stats: BacktestResultType): def generate_edge_table(results: dict) -> str: - floatfmt = ("s", ".10g", ".2f", ".2f", ".2f", ".2f", "d", "d", "d") tabular_data = [] headers = [ "Pair", @@ -493,17 +492,13 @@ def generate_edge_table(results: dict) -> str: tabular_data.append( [ result[0], - result[1].stoploss, - result[1].winrate, - result[1].risk_reward_ratio, - result[1].required_risk_reward, - result[1].expectancy, + f"{result[1].stoploss:.10g}", + f"{result[1].winrate:.2f}", + f"{result[1].risk_reward_ratio:.2f}", + f"{result[1].required_risk_reward:.2f}", + f"{result[1].expectancy:.2f}", result[1].nb_trades, round(result[1].avg_trade_duration), ] ) - - # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate( - tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right" - ) + print_rich_table(tabular_data, headers, summary="EDGE TABLE") diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 53366a234..4c7ce06e8 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -538,12 +538,12 @@ def test_text_table_strategy(testdatadir, capsys): ) -def test_generate_edge_table(): +def test_generate_edge_table(capsys): results = {} results["ETH/BTC"] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60) - text = generate_edge_table(results) - assert text.count("+") == 7 - assert text.count("| ETH/BTC |") == 1 + generate_edge_table(results) + text = capsys.readouterr().out + assert re.search(r".* ETH/BTC .*", text) assert re.search(r".* Risk Reward Ratio .* Required Risk Reward .* Expectancy .*", text) From 9c41ab10ffc3d3032fe6dea793f2b538a743bd24 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Jul 2024 07:15:18 +0200 Subject: [PATCH 73/74] chore: remove tabulate output --- freqtrade/optimize/optimize_reports/bt_output.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index c88a9bd06..479807752 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -1,8 +1,6 @@ import logging from typing import Any, Dict, List, Literal, Union -from tabulate import tabulate - from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config from freqtrade.optimize.optimize_reports.optimize_reports import generate_periodic_breakdown_stats from freqtrade.types import BacktestResultType From a7fcd1263b7da1ae948e1089bc15d6f4596cc35f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Jul 2024 19:39:47 +0200 Subject: [PATCH 74/74] chore: Fix typing --- freqtrade/optimize/optimize_reports/bt_output.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index 479807752..620b6da7e 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -46,13 +46,12 @@ def generate_wins_draws_losses(wins, draws, losses): def text_table_bt_results( pair_results: List[Dict[str, Any]], stake_currency: str, title: str -) -> str: +) -> None: """ Generates and returns a text table for the given backtest data and the results dataframe :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row :param stake_currency: stake-currency - used to correctly name headers :param title: Title of the table - :return: pretty printed table with tabulate as string """ headers = _get_line_header("Pair", stake_currency, "Trades") @@ -73,15 +72,14 @@ def text_table_bt_results( def text_table_tags( - tag_type: Literal["enter_tag", "exit_tag", "exit_reason"], + tag_type: Literal["enter_tag", "exit_tag", "mix_tag"], tag_results: List[Dict[str, Any]], stake_currency: str, -) -> str: +) -> None: """ Generates and returns a text table for the given backtest data and the results dataframe :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row :param stake_currency: stake-currency - used to correctly name headers - :return: pretty printed table with tabulate as string """ floatfmt = _get_line_floatfmt(stake_currency) fallback: str = "" @@ -126,12 +124,11 @@ def text_table_tags( def text_table_periodic_breakdown( days_breakdown_stats: List[Dict[str, Any]], stake_currency: str, period: str -) -> str: +) -> None: """ Generate small table with Backtest results by days :param days_breakdown_stats: Days breakdown metrics :param stake_currency: Stakecurrency used - :return: pretty printed table with tabulate as string """ headers = [ period.capitalize(), @@ -158,7 +155,6 @@ def text_table_strategy(strategy_results, stake_currency: str, title: str): Generate summary table per strategy :param strategy_results: Dict of containing results for all strategies :param stake_currency: stake-currency - used to correctly name headers - :return: pretty printed table with tabulate as string """ headers = _get_line_header("Strategy", stake_currency, "Trades") # _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless @@ -195,7 +191,7 @@ def text_table_strategy(strategy_results, stake_currency: str, title: str): print_rich_table(output, headers, summary=title) -def text_table_add_metrics(strat_results: Dict) -> str: +def text_table_add_metrics(strat_results: Dict) -> None: if len(strat_results["trades"]) > 0: best_trade = max(strat_results["trades"], key=lambda x: x["profit_ratio"]) worst_trade = min(strat_results["trades"], key=lambda x: x["profit_ratio"]) @@ -472,7 +468,7 @@ def show_sorted_pairlist(config: Config, backtest_stats: BacktestResultType): print("]") -def generate_edge_table(results: dict) -> str: +def generate_edge_table(results: dict) -> None: tabular_data = [] headers = [ "Pair",