diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b344d872..8fa4bf9ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: # stages: [push] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.10.0" + rev: "v1.10.1" hooks: - id: mypy exclude: build_helpers @@ -31,7 +31,7 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.4.10' + rev: 'v0.5.0' hooks: - id: ruff diff --git a/build_helpers/TA_Lib-0.4.31-cp310-cp310-win_amd64.whl b/build_helpers/TA_Lib-0.4.31-cp310-cp310-win_amd64.whl deleted file mode 100644 index 4ce492a40..000000000 Binary files a/build_helpers/TA_Lib-0.4.31-cp310-cp310-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.31-cp311-cp311-linux_armv7l.whl b/build_helpers/TA_Lib-0.4.31-cp311-cp311-linux_armv7l.whl deleted file mode 100644 index a664f12e6..000000000 Binary files a/build_helpers/TA_Lib-0.4.31-cp311-cp311-linux_armv7l.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.31-cp311-cp311-win_amd64.whl b/build_helpers/TA_Lib-0.4.31-cp311-cp311-win_amd64.whl deleted file mode 100644 index e5b8cb4ef..000000000 Binary files a/build_helpers/TA_Lib-0.4.31-cp311-cp311-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.31-cp312-cp312-win_amd64.whl b/build_helpers/TA_Lib-0.4.31-cp312-cp312-win_amd64.whl deleted file mode 100644 index 79596cf0c..000000000 Binary files a/build_helpers/TA_Lib-0.4.31-cp312-cp312-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.31-cp39-cp39-win_amd64.whl b/build_helpers/TA_Lib-0.4.31-cp39-cp39-win_amd64.whl deleted file mode 100644 index 01d3c626e..000000000 Binary files a/build_helpers/TA_Lib-0.4.31-cp39-cp39-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.32-cp310-cp310-win_amd64.whl b/build_helpers/TA_Lib-0.4.32-cp310-cp310-win_amd64.whl new file mode 100644 index 000000000..d31d63db8 Binary files /dev/null and b/build_helpers/TA_Lib-0.4.32-cp310-cp310-win_amd64.whl differ diff --git a/build_helpers/TA_Lib-0.4.32-cp311-cp311-linux_armv7l.whl b/build_helpers/TA_Lib-0.4.32-cp311-cp311-linux_armv7l.whl new file mode 100644 index 000000000..e69bdd97d Binary files /dev/null and b/build_helpers/TA_Lib-0.4.32-cp311-cp311-linux_armv7l.whl differ diff --git a/build_helpers/TA_Lib-0.4.32-cp311-cp311-win_amd64.whl b/build_helpers/TA_Lib-0.4.32-cp311-cp311-win_amd64.whl new file mode 100644 index 000000000..e992ece01 Binary files /dev/null and b/build_helpers/TA_Lib-0.4.32-cp311-cp311-win_amd64.whl differ diff --git a/build_helpers/TA_Lib-0.4.32-cp312-cp312-win_amd64.whl b/build_helpers/TA_Lib-0.4.32-cp312-cp312-win_amd64.whl new file mode 100644 index 000000000..98845c409 Binary files /dev/null and b/build_helpers/TA_Lib-0.4.32-cp312-cp312-win_amd64.whl differ diff --git a/build_helpers/TA_Lib-0.4.31-cp39-cp39-linux_armv7l.whl b/build_helpers/TA_Lib-0.4.32-cp39-cp39-linux_armv7l.whl similarity index 54% rename from build_helpers/TA_Lib-0.4.31-cp39-cp39-linux_armv7l.whl rename to build_helpers/TA_Lib-0.4.32-cp39-cp39-linux_armv7l.whl index bceb21773..03bc79df8 100644 Binary files a/build_helpers/TA_Lib-0.4.31-cp39-cp39-linux_armv7l.whl and b/build_helpers/TA_Lib-0.4.32-cp39-cp39-linux_armv7l.whl differ diff --git a/build_helpers/TA_Lib-0.4.32-cp39-cp39-win_amd64.whl b/build_helpers/TA_Lib-0.4.32-cp39-cp39-win_amd64.whl new file mode 100644 index 000000000..f0c46dafe Binary files /dev/null and b/build_helpers/TA_Lib-0.4.32-cp39-cp39-win_amd64.whl differ diff --git a/docs/configuration.md b/docs/configuration.md index e2501cf48..ec8134281 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -204,9 +204,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.uid` | API uid to use for the exchange. Only required when you are in production mode and for exchanges that use uid for API requests.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList. [More information](plugins.md#pairlists-and-pairlist-handlers).
**Datatype:** List | `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting. [More information](plugins.md#pairlists-and-pairlist-handlers).
**Datatype:** List -| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for additional ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation). Please avoid adding exchange secrets here (use the dedicated fields instead), as they may be contained in logs.
**Datatype:** Dict -| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict -| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict +| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for additional ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://docs.ccxt.com/#/README?id=overriding-exchange-properties-upon-instantiation). Please avoid adding exchange secrets here (use the dedicated fields instead), as they may be contained in logs.
**Datatype:** Dict +| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://docs.ccxt.com/#/README?id=overriding-exchange-properties-upon-instantiation)
**Datatype:** Dict +| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://docs.ccxt.com/#/README?id=overriding-exchange-properties-upon-instantiation)
**Datatype:** Dict +| `exchange.enable_ws` | Enable the usage of Websockets for the exchange.
[More information](#consuming-exchange-websockets).
*Defaults to `true`.*
**Datatype:** Boolean | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer | `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`*
**Datatype:** Boolean | `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`*
**Datatype:** Boolean @@ -409,6 +410,8 @@ Or another example if your position adjustment assumes it can do 1 additional bu --8<-- "includes/pricing.md" +## Further Configuration details + ### Understand minimal_roi The `minimal_roi` configuration parameter is a JSON object where the key is a duration @@ -614,6 +617,30 @@ Freqtrade supports both Demo and Pro coingecko API keys. The Coingecko API key is NOT required for the bot to function correctly. It is only used for the conversion of coin to fiat in the Telegram reports, which usually also work without API key. +## Consuming exchange Websockets + +Freqtrade can consume websockets through ccxt.pro. + +Freqtrade aims ensure data is available at all times. +Should the websocket connection fail (or be disabled), the bot will fall back to REST API calls. + +Should you experience problems you suspect are caused by websockets, you can disable these via the setting `exchange.enable_ws`, which defaults to true. + +```jsonc +"exchange": { + // ... + "enable_ws": false, + // ... +} +``` + +Should you be required to use a proxy, please refer to the [proxy section](#using-proxy-with-freqtrade) for more information. + +!!! Info "Rollout" + We're implementing this out slowly, ensuring stability of your bots. + Currently, usage is limited to ohlcv data streams. + It's also limited to a few exchanges, with new exchanges being added on an ongoing basis. + ## Using Dry-run mode We recommend starting the bot in the Dry-run mode to see how your bot will @@ -650,9 +677,9 @@ Once you will be happy with your bot performance running in the Dry-run mode, yo * API-keys may or may not be provided. Only Read-Only operations (i.e. operations that do not alter account state) on the exchange are performed in dry-run mode. * Wallets (`/balance`) are simulated based on `dry_run_wallet`. * Orders are simulated, and will not be posted to the exchange. -* Market orders fill based on orderbook volume the moment the order is placed. +* Market orders fill based on orderbook volume the moment the order is placed, with a maximum slippage of 5%. * Limit orders fill once the price reaches the defined level - or time out based on `unfilledtimeout` settings. -* Limit orders will be converted to market orders if they cross the price by more than 1%. +* Limit orders will be converted to market orders if they cross the price by more than 1%, and will be filled immediately based regular market order rules (see point about Market orders above). * In combination with `stoploss_on_exchange`, the stop_loss price is assumed to be filled. * Open orders (not trades, which are stored in the database) are kept open after bot restarts, with the assumption that they were not filled while being offline. @@ -702,7 +729,7 @@ You should also make sure to read the [Exchanges](exchanges.md) section of the d **NEVER** share your private configuration file or your exchange keys with anyone! -### Using proxy with Freqtrade +## Using a proxy with Freqtrade To use a proxy with freqtrade, export your proxy settings using the variables `"HTTP_PROXY"` and `"HTTPS_PROXY"` set to the appropriate values. This will have the proxy settings applied to everything (telegram, coingecko, ...) **except** for exchange requests. @@ -713,7 +740,7 @@ export HTTPS_PROXY="http://addr:port" freqtrade ``` -#### Proxy exchange requests +### Proxy exchange requests To use a proxy for exchange connections - you will have to define the proxies as part of the ccxt configuration. @@ -722,6 +749,7 @@ To use a proxy for exchange connections - you will have to define the proxies as "exchange": { "ccxt_config": { "httpsProxy": "http://addr:port", + "wsProxy": "http://addr:port", } } } 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`. diff --git a/docs/freqai-running.md b/docs/freqai-running.md index 553a8b698..1b721b658 100644 --- a/docs/freqai-running.md +++ b/docs/freqai-running.md @@ -73,11 +73,11 @@ Backtesting mode requires [downloading the necessary data](#downloading-data-to- --- -### Saving prediction data +### Saving backtesting prediction data To allow for tweaking your strategy (**not** the features!), FreqAI will automatically save the predictions during backtesting so that they can be reused for future backtests and live runs using the same `identifier` model. This provides a performance enhancement geared towards enabling **high-level hyperopting** of entry/exit criteria. -An additional directory called `backtesting_predictions`, which contains all the predictions stored in `hdf` format, will be created in the `unique-id` folder. +An additional directory called `backtesting_predictions`, which contains all the predictions stored in `feather` format, will be created in the `unique-id` folder. To change your **features**, you **must** set a new `identifier` in the config to signal to FreqAI to train new models. @@ -89,7 +89,6 @@ FreqAI allow you to reuse live historic predictions through the backtest paramet The `--timerange` parameter must not be informed, as it will be automatically calculated through the data in the historic predictions file. - ### Downloading data to cover the full backtest period For live/dry deployments, FreqAI will download the necessary data automatically. However, to use backtesting functionality, you need to download the necessary data using `download-data` (details [here](data-download.md#data-downloading)). You need to pay careful attention to understanding how much *additional* data needs to be downloaded to ensure that there is a sufficient amount of training data *before* the start of the backtesting time range. The amount of additional data can be roughly estimated by moving the start date of the time range backwards by `train_period_days` and the `startup_candle_count` (see the [parameter table](freqai-parameter-table.md) for detailed descriptions of these parameters) from the beginning of the desired backtesting time range. diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 5c4e6df5d..0cb8dd961 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,12 +1,12 @@ """Freqtrade bot""" -__version__ = "2024.6-dev" +__version__ = "2024.7-dev" if "dev" in __version__: from pathlib import Path try: - import subprocess + import subprocess # noqa: S404 freqtrade_basedir = Path(__file__).parent diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index c527a80d6..0c93af78a 100755 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -2,10 +2,10 @@ This module contains the argument manager class """ -import argparse +from argparse import ArgumentParser, Namespace, _ArgumentGroup from functools import partial from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union from freqtrade.commands.cli_options import AVAILABLE_CLI_OPTIONS from freqtrade.constants import DEFAULT_CONFIG @@ -226,6 +226,19 @@ ARGS_ANALYZE_ENTRIES_EXITS = [ "analysis_csv_path", ] + +ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_search"] + +ARGS_LOOKAHEAD_ANALYSIS = [ + a + for a in ARGS_BACKTEST + if a + not in ("position_stacking", "use_max_market_positions", "backtest_cache", "backtest_breakdown") +] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"] + +ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"] + +# Command level configs - keep at the bottom of the above definitions NO_CONF_REQURIED = [ "convert-data", "convert-trade-data", @@ -248,14 +261,6 @@ NO_CONF_REQURIED = [ NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"] -ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_search"] - -ARGS_LOOKAHEAD_ANALYSIS = [ - a for a in ARGS_BACKTEST if a not in ("position_stacking", "use_max_market_positions", "cache") -] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"] - -ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"] - class Arguments: """ @@ -264,7 +269,7 @@ class Arguments: def __init__(self, args: Optional[List[str]]) -> None: self.args = args - self._parsed_arg: Optional[argparse.Namespace] = None + self._parsed_arg: Optional[Namespace] = None def get_parsed_arg(self) -> Dict[str, Any]: """ @@ -277,7 +282,7 @@ class Arguments: return vars(self._parsed_arg) - def _parse_args(self) -> argparse.Namespace: + def _parse_args(self) -> Namespace: """ Parses given arguments and returns an argparse Namespace instance. """ @@ -306,7 +311,9 @@ class Arguments: return parsed_arg - def _build_args(self, optionlist, parser): + def _build_args( + self, optionlist: List[str], parser: Union[ArgumentParser, _ArgumentGroup] + ) -> None: for val in optionlist: opt = AVAILABLE_CLI_OPTIONS[val] parser.add_argument(*opt.cli, dest=val, **opt.kwargs) @@ -317,16 +324,16 @@ class Arguments: :return: None """ # Build shared arguments (as group Common Options) - _common_parser = argparse.ArgumentParser(add_help=False) + _common_parser = ArgumentParser(add_help=False) group = _common_parser.add_argument_group("Common arguments") self._build_args(optionlist=ARGS_COMMON, parser=group) - _strategy_parser = argparse.ArgumentParser(add_help=False) + _strategy_parser = ArgumentParser(add_help=False) strategy_group = _strategy_parser.add_argument_group("Strategy arguments") self._build_args(optionlist=ARGS_STRATEGY, parser=strategy_group) # Build main command - self.parser = argparse.ArgumentParser( + self.parser = ArgumentParser( prog="freqtrade", description="Free, open source crypto trading bot" ) self._build_args(optionlist=["version"], parser=self.parser) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 257166f9c..3a542226c 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -45,7 +45,8 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: "name": exchange["name"], **valid_entry, "supported": "Official" if exchange["supported"] else "", - "trade_modes": ", ".join( + "trade_modes": ("DEX: " if exchange["dex"] else "") + + ", ".join( (f"{a['margin_mode']} " if a["margin_mode"] else "") + a["trading_mode"] for a in exchange["trade_modes"] ), 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/freqtrade/constants.py b/freqtrade/constants.py index 88031d65b..7d53aab1c 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -540,6 +540,7 @@ CONF_SCHEMA = { "type": "object", "properties": { "name": {"type": "string"}, + "enable_ws": {"type": "boolean", "default": True}, "key": {"type": "string", "default": ""}, "secret": {"type": "string", "default": ""}, "password": {"type": "string", "default": ""}, diff --git a/freqtrade/data/__init__.py b/freqtrade/data/__init__.py index f716abfc5..492155031 100644 --- a/freqtrade/data/__init__.py +++ b/freqtrade/data/__init__.py @@ -2,5 +2,8 @@ Module to handle data operations for freqtrade """ +from freqtrade.data import converter + + # limit what's imported when using `from freqtrade.data import *` __all__ = ["converter"] diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 4c0eaf9ce..d347c2cd9 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -30,6 +30,7 @@ class Binance(Exchange): "trades_pagination_arg": "fromId", "trades_has_history": True, "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], + "ws.enabled": True, } _ft_has_futures: Dict = { "stoploss_order_types": {"limit": "stop", "market": "stop_market"}, @@ -42,6 +43,7 @@ class Binance(Exchange): PriceType.LAST: "CONTRACT_PRICE", PriceType.MARK: "MARK_PRICE", }, + "ws.enabled": False, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ diff --git a/freqtrade/exchange/binance_leverage_tiers.json b/freqtrade/exchange/binance_leverage_tiers.json index be9f68d98..ae797c9e5 100644 --- a/freqtrade/exchange/binance_leverage_tiers.json +++ b/freqtrade/exchange/binance_leverage_tiers.json @@ -8437,7 +8437,7 @@ } } ], - "BTC/USDT:USDT-240628": [ + "BTC/USDT:USDT-240927": [ { "tier": 1.0, "currency": "USDT", @@ -8567,7 +8567,7 @@ } } ], - "BTC/USDT:USDT-240927": [ + "BTC/USDT:USDT-241227": [ { "tier": 1.0, "currency": "USDT", @@ -13805,7 +13805,7 @@ } } ], - "ETH/USDT:USDT-240628": [ + "ETH/USDT:USDT-240927": [ { "tier": 1.0, "currency": "USDT", @@ -13935,7 +13935,7 @@ } } ], - "ETH/USDT:USDT-240927": [ + "ETH/USDT:USDT-241227": [ { "tier": 1.0, "currency": "USDT", diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 252f0a29b..16932947d 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -33,6 +33,7 @@ class Bybit(Exchange): "ohlcv_candle_limit": 1000, "ohlcv_has_history": True, "order_time_in_force": ["GTC", "FOK", "IOC", "PO"], + "ws.enabled": True, "trades_has_history": False, # Endpoint doesn't support pagination } _ft_has_futures: Dict = { diff --git a/freqtrade/exchange/check_exchange.py b/freqtrade/exchange/check_exchange.py index 73b1c8a97..6d82bae04 100644 --- a/freqtrade/exchange/check_exchange.py +++ b/freqtrade/exchange/check_exchange.py @@ -47,7 +47,7 @@ def check_exchange(config: Config, check_for_bad: bool = True) -> bool: f'{", ".join(available_exchanges())}' ) - valid, reason = validate_exchange(exchange) + valid, reason, _ = validate_exchange(exchange) if not valid: if check_for_bad: raise OperationalException( diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 99f891836..251325a0c 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -92,6 +92,8 @@ EXCHANGE_HAS_OPTIONAL = [ # 'fetchMarketLeverageTiers', # Futures initialization # 'fetchOpenOrder', 'fetchClosedOrder', # replacement for fetchOrder # 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance... + # ccxt.pro + "watchOHLCV", ] diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c7139c8fa..62f0ca4de 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -14,7 +14,7 @@ from threading import Lock from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union import ccxt -import ccxt.async_support as ccxt_async +import ccxt.pro as ccxt_pro from cachetools import TTLCache from ccxt import TICK_SIZE from dateutil import parser @@ -34,7 +34,15 @@ from freqtrade.constants import ( PairWithTimeframe, ) from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, PriceType, RunMode, TradingMode +from freqtrade.enums import ( + OPTIMIZE_MODES, + TRADE_MODES, + CandleType, + MarginMode, + PriceType, + RunMode, + TradingMode, +) from freqtrade.exceptions import ( ConfigurationError, DDosProtection, @@ -56,7 +64,6 @@ from freqtrade.exchange.exchange_utils import ( ROUND, ROUND_DOWN, ROUND_UP, - CcxtModuleType, amount_to_contract_precision, amount_to_contracts, amount_to_precision, @@ -73,6 +80,7 @@ from freqtrade.exchange.exchange_utils_timeframe import ( timeframe_to_prev_date, timeframe_to_seconds, ) +from freqtrade.exchange.exchange_ws import ExchangeWS from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers from freqtrade.misc import ( chunks, @@ -83,7 +91,7 @@ from freqtrade.misc import ( ) from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.util import dt_from_ts, dt_now -from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts +from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_ms_time from freqtrade.util.periodic_cache import PeriodicCache @@ -130,6 +138,7 @@ class Exchange: "marketOrderRequiresPrice": False, "exchange_has_overrides": {}, # Dictionary overriding ccxt's "has". # Expected to be in the format {"fetchOHLCV": True} or {"fetchOHLCV": False} + "ws.enabled": False, # Set to true for exchanges with tested websocket support } _ft_has: Dict = {} _ft_has_futures: Dict = {} @@ -152,7 +161,9 @@ class Exchange: :return: None """ self._api: ccxt.Exchange - self._api_async: ccxt_async.Exchange + self._api_async: ccxt_pro.Exchange + self._ws_async: ccxt_pro.Exchange = None + self._exchange_ws: Optional[ExchangeWS] = None self._markets: Dict = {} self._trading_fees: Dict[str, Any] = {} self._leverage_tiers: Dict[str, List[Dict]] = {} @@ -219,7 +230,7 @@ class Exchange: ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_config", {}), ccxt_config) ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_sync_config", {}), ccxt_config) - self._api = self._init_ccxt(exchange_conf, ccxt_kwargs=ccxt_config) + self._api = self._init_ccxt(exchange_conf, True, ccxt_config) ccxt_async_config = self._ccxt_config ccxt_async_config = deep_merge_dicts( @@ -228,7 +239,15 @@ class Exchange: ccxt_async_config = deep_merge_dicts( exchange_conf.get("ccxt_async_config", {}), ccxt_async_config ) - self._api_async = self._init_ccxt(exchange_conf, ccxt_async, ccxt_kwargs=ccxt_async_config) + self._api_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) + self._has_watch_ohlcv = self.exchange_has("watchOHLCV") and self._ft_has["ws.enabled"] + if ( + self._config["runmode"] in TRADE_MODES + and exchange_conf.get("enable_ws", True) + and self._has_watch_ohlcv + ): + self._ws_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) + self._exchange_ws = ExchangeWS(self._config, self._ws_async) logger.info(f'Using Exchange "{self.name}"') self.required_candle_call_count = 1 @@ -257,6 +276,8 @@ class Exchange: self.close() def close(self): + if self._exchange_ws: + self._exchange_ws.cleanup() logger.debug("Exchange object destroyed, closing async loop") if ( getattr(self, "_api_async", None) @@ -265,6 +286,14 @@ class Exchange: ): logger.debug("Closing async ccxt session.") self.loop.run_until_complete(self._api_async.close()) + if ( + self._ws_async + and inspect.iscoroutinefunction(self._ws_async.close) + and self._ws_async.session + ): + logger.debug("Closing ws ccxt session.") + self.loop.run_until_complete(self._ws_async.close()) + if self.loop and not self.loop.is_closed(): self.loop.close() @@ -288,18 +317,22 @@ class Exchange: self.validate_pricing(config["entry_pricing"]) def _init_ccxt( - self, - exchange_config: Dict[str, Any], - ccxt_module: CcxtModuleType = ccxt, - *, - ccxt_kwargs: Dict, + self, exchange_config: Dict[str, Any], sync: bool, ccxt_kwargs: Dict[str, Any] ) -> ccxt.Exchange: """ - Initialize ccxt with given config and return valid - ccxt instance. + Initialize ccxt with given config and return valid ccxt instance. """ # Find matching class for the given exchange name name = exchange_config["name"] + if sync: + ccxt_module = ccxt + else: + ccxt_module = ccxt_pro + if not is_exchange_known_ccxt(name, ccxt_module): + # Fall back to async if pro doesn't support this exchange + import ccxt.async_support as ccxt_async + + ccxt_module = ccxt_async if not is_exchange_known_ccxt(name, ccxt_module): raise OperationalException(f"Exchange {name} is not supported by ccxt") @@ -531,6 +564,13 @@ class Exchange: amount, self.get_precision_amount(pair), self.precisionMode, contract_size ) + def ws_connection_reset(self): + """ + called at regular intervals to reset the websocket connection + """ + if self._exchange_ws: + self._exchange_ws.reset_connections() + def _load_async_markets(self, reload: bool = False) -> Dict[str, Any]: try: markets = self.loop.run_until_complete( @@ -562,6 +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"]: @@ -795,7 +841,7 @@ class Exchange: """ if endpoint in self._ft_has.get("exchange_has_overrides", {}): return self._ft_has["exchange_has_overrides"][endpoint] - return endpoint in self._api.has and self._api.has[endpoint] + return endpoint in self._api_async.has and self._api_async.has[endpoint] def get_precision_amount(self, pair: str) -> Optional[float]: """ @@ -2019,7 +2065,7 @@ class Exchange: def get_fee( self, symbol: str, - type: str = "", + order_type: str = "", side: str = "", amount: float = 1, price: float = 1, @@ -2028,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: @@ -2045,7 +2091,7 @@ class Exchange: return self._api.calculate_fee( symbol=symbol, - type=type, + type=order_type, side=side, amount=amount, price=price, @@ -2228,9 +2274,40 @@ class Exchange: cache: bool, ) -> Coroutine[Any, Any, OHLCVResponse]: not_all_data = cache and self.required_candle_call_count > 1 + if cache and candle_type in (CandleType.SPOT, CandleType.FUTURES): + if self._has_watch_ohlcv and self._exchange_ws: + # Subscribe to websocket + self._exchange_ws.schedule_ohlcv(pair, timeframe, candle_type) + if cache and (pair, timeframe, candle_type) in self._klines: candle_limit = self.ohlcv_candle_limit(timeframe, candle_type) - min_date = date_minus_candles(timeframe, candle_limit - 5).timestamp() + min_date = int(date_minus_candles(timeframe, candle_limit - 5).timestamp()) + + if self._exchange_ws: + candle_date = int(timeframe_to_prev_date(timeframe).timestamp() * 1000) + prev_candle_date = int(date_minus_candles(timeframe, 1).timestamp() * 1000) + candles = self._exchange_ws.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) + half_candle = int(candle_date - (candle_date - prev_candle_date) * 0.5) + last_refresh_time = int( + self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type), 0) + ) + + if ( + candles + and candles[-1][0] >= prev_candle_date + and last_refresh_time >= half_candle + ): + # Usable result, candle contains the previous candle. + # Also, we check if the last refresh time is no more than half the candle ago. + logger.debug(f"reuse watch result for {pair}, {timeframe}, {last_refresh_time}") + + return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_date) + logger.info( + f"Failed to reuse watch {pair}, {timeframe}, {candle_date < last_refresh_time}," + f" {candle_date}, {last_refresh_time}, " + f"{format_ms_time(candle_date)}, {format_ms_time(last_refresh_time)} " + ) + # Check if 1 call can get us updated candles without hole in the data. if min_date < self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0): # Cache can be used - do one-off call. @@ -2263,7 +2340,7 @@ class Exchange: def _build_ohlcv_dl_jobs( self, pair_list: ListPairsWithTimeframes, since_ms: Optional[int], cache: bool - ) -> Tuple[List[Coroutine], List[Tuple[str, str, CandleType]]]: + ) -> Tuple[List[Coroutine], List[PairWithTimeframe]]: """ Build Coroutines to execute as part of refresh_latest_ohlcv """ diff --git a/freqtrade/exchange/exchange_utils.py b/freqtrade/exchange/exchange_utils.py index dcae1ab3b..9c2514f92 100644 --- a/freqtrade/exchange/exchange_utils.py +++ b/freqtrade/exchange/exchange_utils.py @@ -53,16 +53,22 @@ def available_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[st return [x for x in exchanges if validate_exchange(x)[0]] -def validate_exchange(exchange: str) -> Tuple[bool, str]: +def validate_exchange(exchange: str) -> Tuple[bool, str, bool]: """ returns: can_use, reason with Reason including both missing and missing_opt """ - ex_mod = getattr(ccxt, exchange.lower())() + try: + ex_mod = getattr(ccxt.pro, exchange.lower())() + except AttributeError: + ex_mod = getattr(ccxt.async_support, exchange.lower())() + + if not ex_mod or not ex_mod.has: + return False, "", False + result = True reason = "" - if not ex_mod or not ex_mod.has: - return False, "" + is_dex = getattr(ex_mod, "dex", False) missing = [ k for k, v in EXCHANGE_HAS_REQUIRED.items() @@ -81,18 +87,19 @@ def validate_exchange(exchange: str) -> Tuple[bool, str]: if missing_opt: reason += f"{'. ' if reason else ''}missing opt: {', '.join(missing_opt)}. " - return result, reason + return result, reason, is_dex def _build_exchange_list_entry( exchange_name: str, exchangeClasses: Dict[str, Any] ) -> ValidExchangesType: - valid, comment = validate_exchange(exchange_name) + valid, comment, is_dex = validate_exchange(exchange_name) result: ValidExchangesType = { "name": exchange_name, "valid": valid, "supported": exchange_name.lower() in SUPPORTED_EXCHANGES, "comment": comment, + "dex": is_dex, "trade_modes": [{"trading_mode": "spot", "margin_mode": ""}], } if resolved := exchangeClasses.get(exchange_name.lower()): diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py new file mode 100644 index 000000000..06ac45e21 --- /dev/null +++ b/freqtrade/exchange/exchange_ws.py @@ -0,0 +1,186 @@ +import asyncio +import logging +import time +from copy import deepcopy +from functools import partial +from threading import Thread +from typing import Dict, Set + +import ccxt + +from freqtrade.constants import Config, PairWithTimeframe +from freqtrade.enums.candletype import CandleType +from freqtrade.exchange.exchange import timeframe_to_seconds +from freqtrade.exchange.types import OHLCVResponse +from freqtrade.util import dt_ts, format_ms_time + + +logger = logging.getLogger(__name__) + + +class ExchangeWS: + def __init__(self, config: Config, ccxt_object: ccxt.Exchange) -> None: + self.config = config + self.ccxt_object = ccxt_object + self._background_tasks: Set[asyncio.Task] = set() + + self._klines_watching: Set[PairWithTimeframe] = set() + self._klines_scheduled: Set[PairWithTimeframe] = set() + self.klines_last_refresh: Dict[PairWithTimeframe, float] = {} + self.klines_last_request: Dict[PairWithTimeframe, float] = {} + self._thread = Thread(name="ccxt_ws", target=self._start_forever) + self._thread.start() + self.__cleanup_called = False + + def _start_forever(self) -> None: + self._loop = asyncio.new_event_loop() + try: + self._loop.run_forever() + finally: + if self._loop.is_running(): + self._loop.stop() + + def cleanup(self) -> None: + logger.debug("Cleanup called - stopping") + self._klines_watching.clear() + for task in self._background_tasks: + task.cancel() + if hasattr(self, "_loop") and not self._loop.is_closed(): + self.reset_connections() + + self._loop.call_soon_threadsafe(self._loop.stop) + time.sleep(0.1) + if not self._loop.is_closed(): + self._loop.close() + + self._thread.join() + logger.debug("Stopped") + + def reset_connections(self) -> None: + """ + Reset all connections - avoids "connection-reset" errors that happen after ~9 days + """ + if hasattr(self, "_loop") and not self._loop.is_closed(): + logger.info("Resetting WS connections.") + asyncio.run_coroutine_threadsafe(self._cleanup_async(), loop=self._loop) + while not self.__cleanup_called: + time.sleep(0.1) + self.__cleanup_called = False + + async def _cleanup_async(self) -> None: + try: + await self.ccxt_object.close() + # Clear the cache. + # Not doing this will cause problems on startup with dynamic pairlists + self.ccxt_object.ohlcvs.clear() + except Exception: + logger.exception("Exception in _cleanup_async") + finally: + self.__cleanup_called = True + + def cleanup_expired(self) -> None: + """ + Remove pairs from watchlist if they've not been requested within + the last timeframe (+ offset) + """ + changed = False + for p in list(self._klines_watching): + _, timeframe, _ = p + timeframe_s = timeframe_to_seconds(timeframe) + last_refresh = self.klines_last_request.get(p, 0) + if last_refresh > 0 and (dt_ts() - last_refresh) > ((timeframe_s + 20) * 1000): + logger.info(f"Removing {p} from watchlist") + self._klines_watching.discard(p) + changed = True + if changed: + logger.info(f"Removal done: new watch list ({len(self._klines_watching)})") + + async def _schedule_while_true(self) -> None: + # For the ones we should be watching + for p in self._klines_watching: + # Check if they're already scheduled + if p not in self._klines_scheduled: + self._klines_scheduled.add(p) + pair, timeframe, candle_type = p + task = asyncio.create_task( + self._continuously_async_watch_ohlcv(pair, timeframe, candle_type) + ) + self._background_tasks.add(task) + task.add_done_callback( + partial( + self._continuous_stopped, + pair=pair, + timeframe=timeframe, + candle_type=candle_type, + ) + ) + + def _continuous_stopped( + self, task: asyncio.Task, pair: str, timeframe: str, candle_type: CandleType + ): + self._background_tasks.discard(task) + result = "done" + if task.cancelled(): + result = "cancelled" + else: + if (result1 := task.result()) is not None: + result = str(result1) + + logger.info(f"{pair}, {timeframe}, {candle_type} - Task finished - {result}") + self._klines_scheduled.discard((pair, timeframe, candle_type)) + + async def _continuously_async_watch_ohlcv( + self, pair: str, timeframe: str, candle_type: CandleType + ) -> None: + try: + while (pair, timeframe, candle_type) in self._klines_watching: + start = dt_ts() + data = await self.ccxt_object.watch_ohlcv(pair, timeframe) + self.klines_last_refresh[(pair, timeframe, candle_type)] = dt_ts() + logger.debug( + f"watch done {pair}, {timeframe}, data {len(data)} " + f"in {dt_ts() - start:.2f}s" + ) + except ccxt.ExchangeClosedByUser: + logger.debug("Exchange connection closed by user") + except ccxt.BaseError: + logger.exception(f"Exception in continuously_async_watch_ohlcv for {pair}, {timeframe}") + finally: + self._klines_watching.discard((pair, timeframe, candle_type)) + + def schedule_ohlcv(self, pair: str, timeframe: str, candle_type: CandleType) -> None: + """ + Schedule a pair/timeframe combination to be watched + """ + self._klines_watching.add((pair, timeframe, candle_type)) + self.klines_last_request[(pair, timeframe, candle_type)] = dt_ts() + # asyncio.run_coroutine_threadsafe(self.schedule_schedule(), loop=self._loop) + asyncio.run_coroutine_threadsafe(self._schedule_while_true(), loop=self._loop) + self.cleanup_expired() + + async def get_ohlcv( + self, + pair: str, + timeframe: str, + candle_type: CandleType, + candle_date: int, + ) -> OHLCVResponse: + """ + Returns cached klines from ccxt's "watch" cache. + :param candle_date: timestamp of the end-time of the candle. + """ + # Deepcopy the response - as it might be modified in the background as new messages arrive + candles = deepcopy(self.ccxt_object.ohlcvs.get(pair, {}).get(timeframe)) + refresh_date = self.klines_last_refresh[(pair, timeframe, candle_type)] + drop_hint = False + if refresh_date > candle_date: + # Refreshed after candle was complete. + # logger.info(f"{candles[-1][0]} >= {candle_date}") + drop_hint = candles[-1][0] >= candle_date + logger.debug( + f"watch result for {pair}, {timeframe} with length {len(candles)}, " + f"{format_ms_time(candles[-1][0])}, " + f"lref={format_ms_time(refresh_date)}, " + f"candle_date={format_ms_time(candle_date)}, {drop_hint=}" + ) + return pair, timeframe, candle_type, candles, drop_hint 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/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/freqtradebot.py b/freqtrade/freqtradebot.py index 8e4fa1d5f..5a33e9fa6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -168,6 +168,8 @@ class FreqtradeBot(LoggingMixin): t = str(time(time_slot, minutes, 2)) self._schedule.every().day.at(t).do(update) + self._schedule.every().day.at("00:02").do(self.exchange.ws_connection_reset) + self.strategy.ft_bot_start() # Initialize protections AFTER bot start - otherwise parameters are not loaded. self.protections = ProtectionManager(self.config, self.strategy.protections) @@ -289,8 +291,7 @@ class FreqtradeBot(LoggingMixin): # Then looking for entry opportunities if self.get_free_open_trades(): self.enter_positions() - if self.trading_mode == TradingMode.FUTURES: - self._schedule.run_pending() + self._schedule.run_pending() Trade.commit() self.rpc.process_msg_queue(self.dataprovider._msg_queue) self.last_process = datetime.now(timezone.utc) @@ -2369,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]: @@ -2376,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 ) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 9a33fe430..23e2779a0 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -33,7 +33,7 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = if log: logger.info(f'dumping json to "{filename}"') - with gzip.open(filename, "w") as fpz: + with gzip.open(filename, "wt", encoding="utf-8") as fpz: rapidjson.dump(data, fpz, default=str, number_mode=rapidjson.NM_NATIVE) else: if log: @@ -60,7 +60,7 @@ def file_dump_joblib(filename: Path, data: Any, log: bool = True) -> None: logger.debug(f'done joblib dump to "{filename}"') -def json_load(datafile: Union[gzip.GzipFile, TextIO]) -> Any: +def json_load(datafile: TextIO) -> Any: """ load data with rapidjson Use this to have a consistent experience, @@ -77,7 +77,7 @@ def file_load_json(file: Path): # Try gzip file first, otherwise regular json file. if gzipfile.is_file(): logger.debug(f"Loading historical data from file {gzipfile}") - with gzip.open(gzipfile) as datafile: + with gzip.open(gzipfile, "rt", encoding="utf-8") as datafile: pairdata = json_load(datafile) elif file.is_file(): logger.debug(f"Loading historical data from file {file}") diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 03b026744..c28c080f5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -217,8 +217,6 @@ class Backtesting: raise OperationalException( "VolumePairList not allowed for backtesting. Please use StaticPairList instead." ) - if "PerformanceFilter" in self.pairlists.name_list: - raise OperationalException("PerformanceFilter not allowed for backtesting.") if len(self.strategylist) > 1 and "PrecisionFilter" in self.pairlists.name_list: raise OperationalException( @@ -467,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. @@ -493,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 @@ -508,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 @@ -539,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/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index 917dad45c..88f0d23d8 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -13,7 +13,7 @@ from freqtrade.constants import ListPairsWithTimeframes from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Tickers from freqtrade.misc import plural -from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter +from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting from freqtrade.util import PeriodicCache, dt_floor_day, dt_now, dt_ts @@ -21,6 +21,8 @@ logger = logging.getLogger(__name__) class AgeFilter(IPairList): + supports_backtesting = SupportsBacktesting.NO + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/freqtrade/plugins/pairlist/FullTradesFilter.py b/freqtrade/plugins/pairlist/FullTradesFilter.py index 13586611d..caa69cb1e 100644 --- a/freqtrade/plugins/pairlist/FullTradesFilter.py +++ b/freqtrade/plugins/pairlist/FullTradesFilter.py @@ -7,13 +7,15 @@ from typing import List from freqtrade.exchange.types import Tickers from freqtrade.persistence import Trade -from freqtrade.plugins.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList, SupportsBacktesting logger = logging.getLogger(__name__) class FullTradesFilter(IPairList): + supports_backtesting = SupportsBacktesting.NO_ACTION + @property def needstickers(self) -> bool: """ diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index a2e70e649..e84700f8f 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -5,6 +5,7 @@ PairList Handler base class import logging from abc import ABC, abstractmethod from copy import deepcopy +from enum import Enum from typing import Any, Dict, List, Literal, Optional, TypedDict, Union from freqtrade.constants import Config @@ -51,8 +52,20 @@ PairlistParameter = Union[ ] +class SupportsBacktesting(str, Enum): + """ + Enum to indicate if a Pairlist Handler supports backtesting. + """ + + YES = "yes" + NO = "no" + NO_ACTION = "no_action" + BIASED = "biased" + + class IPairList(LoggingMixin, ABC): is_pairlist_generator = False + supports_backtesting: SupportsBacktesting = SupportsBacktesting.NO def __init__( self, diff --git a/freqtrade/plugins/pairlist/MarketCapPairList.py b/freqtrade/plugins/pairlist/MarketCapPairList.py index 648766e20..677abed4b 100644 --- a/freqtrade/plugins/pairlist/MarketCapPairList.py +++ b/freqtrade/plugins/pairlist/MarketCapPairList.py @@ -11,7 +11,7 @@ from cachetools import TTLCache from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Tickers -from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter +from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting from freqtrade.util.coin_gecko import FtCoinGeckoApi @@ -20,6 +20,7 @@ logger = logging.getLogger(__name__) class MarketCapPairList(IPairList): is_pairlist_generator = True + supports_backtesting = SupportsBacktesting.BIASED def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/freqtrade/plugins/pairlist/OffsetFilter.py b/freqtrade/plugins/pairlist/OffsetFilter.py index bd981358e..5defaaf60 100644 --- a/freqtrade/plugins/pairlist/OffsetFilter.py +++ b/freqtrade/plugins/pairlist/OffsetFilter.py @@ -7,13 +7,15 @@ from typing import Dict, List from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Tickers -from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter +from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting logger = logging.getLogger(__name__) class OffsetFilter(IPairList): + supports_backtesting = SupportsBacktesting.YES + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index c10ae7394..77a2caf56 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -9,13 +9,15 @@ import pandas as pd from freqtrade.exchange.types import Tickers from freqtrade.persistence import Trade -from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter +from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting logger = logging.getLogger(__name__) class PerformanceFilter(IPairList): + supports_backtesting = SupportsBacktesting.NO_ACTION + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/freqtrade/plugins/pairlist/PrecisionFilter.py b/freqtrade/plugins/pairlist/PrecisionFilter.py index b2f767a67..660ff8fea 100644 --- a/freqtrade/plugins/pairlist/PrecisionFilter.py +++ b/freqtrade/plugins/pairlist/PrecisionFilter.py @@ -8,13 +8,15 @@ from typing import Optional from freqtrade.exceptions import OperationalException from freqtrade.exchange import ROUND_UP from freqtrade.exchange.types import Ticker -from freqtrade.plugins.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList, SupportsBacktesting logger = logging.getLogger(__name__) class PrecisionFilter(IPairList): + supports_backtesting = SupportsBacktesting.BIASED + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index d651533ce..3da7d8443 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -7,13 +7,15 @@ from typing import Dict, Optional from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Ticker -from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter +from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting logger = logging.getLogger(__name__) class PriceFilter(IPairList): + supports_backtesting = SupportsBacktesting.BIASED + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/freqtrade/plugins/pairlist/ProducerPairList.py b/freqtrade/plugins/pairlist/ProducerPairList.py index 09a0c49d2..18af7a734 100644 --- a/freqtrade/plugins/pairlist/ProducerPairList.py +++ b/freqtrade/plugins/pairlist/ProducerPairList.py @@ -9,7 +9,7 @@ from typing import Dict, List, Optional from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Tickers -from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter +from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting logger = logging.getLogger(__name__) @@ -31,6 +31,7 @@ class ProducerPairList(IPairList): """ is_pairlist_generator = True + supports_backtesting = SupportsBacktesting.NO def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/freqtrade/plugins/pairlist/RemotePairList.py b/freqtrade/plugins/pairlist/RemotePairList.py index 26fadb9ae..317aad20b 100644 --- a/freqtrade/plugins/pairlist/RemotePairList.py +++ b/freqtrade/plugins/pairlist/RemotePairList.py @@ -16,7 +16,7 @@ from freqtrade import __version__ from freqtrade.configuration.load_config import CONFIG_PARSE_MODE from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Tickers -from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter +from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist @@ -25,6 +25,8 @@ logger = logging.getLogger(__name__) class RemotePairList(IPairList): is_pairlist_generator = True + # Potential winner bias + supports_backtesting = SupportsBacktesting.BIASED def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/freqtrade/plugins/pairlist/ShuffleFilter.py b/freqtrade/plugins/pairlist/ShuffleFilter.py index 59ac1ac7c..3882ec8a8 100644 --- a/freqtrade/plugins/pairlist/ShuffleFilter.py +++ b/freqtrade/plugins/pairlist/ShuffleFilter.py @@ -9,7 +9,7 @@ from typing import Dict, List, Literal from freqtrade.enums import RunMode from freqtrade.exchange import timeframe_to_seconds from freqtrade.exchange.types import Tickers -from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter +from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting from freqtrade.util.periodic_cache import PeriodicCache @@ -19,6 +19,8 @@ ShuffleValues = Literal["candle", "iteration"] class ShuffleFilter(IPairList): + supports_backtesting = SupportsBacktesting.YES + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/freqtrade/plugins/pairlist/SpreadFilter.py b/freqtrade/plugins/pairlist/SpreadFilter.py index 736903abd..00109abb3 100644 --- a/freqtrade/plugins/pairlist/SpreadFilter.py +++ b/freqtrade/plugins/pairlist/SpreadFilter.py @@ -7,13 +7,15 @@ from typing import Dict, Optional from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Ticker -from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter +from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting logger = logging.getLogger(__name__) class SpreadFilter(IPairList): + supports_backtesting = SupportsBacktesting.NO + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/freqtrade/plugins/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py index 922d0fd94..c4f322353 100644 --- a/freqtrade/plugins/pairlist/StaticPairList.py +++ b/freqtrade/plugins/pairlist/StaticPairList.py @@ -9,7 +9,7 @@ from copy import deepcopy from typing import Dict, List from freqtrade.exchange.types import Tickers -from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter +from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting logger = logging.getLogger(__name__) @@ -17,6 +17,7 @@ logger = logging.getLogger(__name__) class StaticPairList(IPairList): is_pairlist_generator = True + supports_backtesting = SupportsBacktesting.YES def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index f5af2d0a7..2d11e45ef 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -15,7 +15,7 @@ from freqtrade.constants import ListPairsWithTimeframes from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Tickers from freqtrade.misc import plural -from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter +from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting from freqtrade.util import dt_floor_day, dt_now, dt_ts @@ -27,6 +27,8 @@ class VolatilityFilter(IPairList): Filters pairs by volatility """ + supports_backtesting = SupportsBacktesting.NO + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index ea172f140..7cc91f743 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -14,7 +14,7 @@ from freqtrade.constants import ListPairsWithTimeframes from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.exchange.types import Tickers -from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter +from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting from freqtrade.util import dt_now, format_ms_time @@ -26,6 +26,7 @@ SORT_VALUES = ["quoteVolume"] class VolumePairList(IPairList): is_pairlist_generator = True + supports_backtesting = SupportsBacktesting.NO def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index 473e003b6..175e5b18a 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -13,7 +13,7 @@ from freqtrade.constants import ListPairsWithTimeframes from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Tickers from freqtrade.misc import plural -from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter +from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting from freqtrade.util import dt_floor_day, dt_now, dt_ts @@ -21,6 +21,8 @@ logger = logging.getLogger(__name__) class RangeStabilityFilter(IPairList): + supports_backtesting = SupportsBacktesting.NO + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index a6afd5e64..803a60d18 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -11,10 +11,11 @@ from cachetools import TTLCache, cached from freqtrade.constants import Config, ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import CandleType +from freqtrade.enums.runmode import RunMode from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Tickers from freqtrade.mixins import LoggingMixin -from freqtrade.plugins.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList, SupportsBacktesting from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.resolvers import PairListResolver @@ -57,9 +58,44 @@ class PairListManager(LoggingMixin): f"{invalid}." ) + self._check_backtest() + refresh_period = config.get("pairlist_refresh_period", 3600) LoggingMixin.__init__(self, logger, refresh_period) + def _check_backtest(self) -> None: + if self._config["runmode"] not in (RunMode.BACKTEST, RunMode.EDGE, RunMode.HYPEROPT): + return + + pairlist_errors: List[str] = [] + noaction_pairlists: List[str] = [] + biased_pairlists: List[str] = [] + for pairlist_handler in self._pairlist_handlers: + if pairlist_handler.supports_backtesting == SupportsBacktesting.NO: + pairlist_errors.append(pairlist_handler.name) + if pairlist_handler.supports_backtesting == SupportsBacktesting.NO_ACTION: + noaction_pairlists.append(pairlist_handler.name) + if pairlist_handler.supports_backtesting == SupportsBacktesting.BIASED: + biased_pairlists.append(pairlist_handler.name) + + if noaction_pairlists: + logger.warning( + f"Pairlist Handlers {', '.join(noaction_pairlists)} do not generate " + "any changes during backtesting. While it's safe to leave them enabled, they will " + "not behave like in dry/live modes. " + ) + + if biased_pairlists: + logger.warning( + f"Pairlist Handlers {', '.join(biased_pairlists)} will introduce a lookahead bias " + "to your backtest results, as they use today's data - which inheritly suffers from " + "'winner bias'." + ) + if pairlist_errors: + raise OperationalException( + f"Pairlist Handlers {', '.join(pairlist_errors)} do not support backtesting." + ) + @property def whitelist(self) -> List[str]: """The current whitelist""" 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) 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, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 39137b605..990ce4d82 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1787,7 +1787,7 @@ class Telegram(RPCHandler): "_Bot Control_\n" "------------\n" "*/start:* `Starts the trader`\n" - "*/stop:* Stops the trader\n" + "*/stop:* `Stops the trader`\n" "*/stopentry:* `Stops entering, but handles open trades gracefully` \n" "*/forceexit |all:* `Instantly exits the given trade or all trades, " "regardless of profit`\n" @@ -1820,7 +1820,7 @@ class Telegram(RPCHandler): "that represents the current market direction. If no direction is provided `" "`the currently set market direction will be output.` \n" "*/list_custom_data :* `List custom_data for Trade ID & Key combo.`\n" - "`If no Key is supplied it will list all key-value pairs found for that Trade ID.`" + "`If no Key is supplied it will list all key-value pairs found for that Trade ID.`\n" "_Statistics_\n" "------------\n" "*/status |[table]:* `Lists all open trades`\n" diff --git a/freqtrade/types/valid_exchanges_type.py b/freqtrade/types/valid_exchanges_type.py index c01149455..9de05b964 100644 --- a/freqtrade/types/valid_exchanges_type.py +++ b/freqtrade/types/valid_exchanges_type.py @@ -14,4 +14,5 @@ class ValidExchangesType(TypedDict): valid: bool supported: bool comment: str + dex: bool trade_modes: List[TradeModeType] diff --git a/ft_client/freqtrade_client/__init__.py b/ft_client/freqtrade_client/__init__.py index 60b5d1164..9311fc85d 100644 --- a/ft_client/freqtrade_client/__init__.py +++ b/ft_client/freqtrade_client/__init__.py @@ -1,13 +1,13 @@ from freqtrade_client.ft_rest_client import FtRestClient -__version__ = "2024.6-dev" +__version__ = "2024.7-dev" if "dev" in __version__: from pathlib import Path try: - import subprocess + import subprocess # noqa: S404 freqtrade_basedir = Path(__file__).parent diff --git a/ft_client/requirements.txt b/ft_client/requirements.txt index 35406c1d0..5e6856e92 100644 --- a/ft_client/requirements.txt +++ b/ft_client/requirements.txt @@ -1,3 +1,3 @@ # Requirements for freqtrade client library requests==2.32.3 -python-rapidjson==1.17 +python-rapidjson==1.18 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/pyproject.toml b/pyproject.toml index 55ff28db6..5918e92e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,16 +134,19 @@ extend-select = [ "W", # pycodestyle "UP", # pyupgrade "I", # isort + "A", # flake8-builtins "TID", # flake8-tidy-imports # "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 = [ @@ -154,6 +157,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] diff --git a/requirements-dev.txt b/requirements-dev.txt index f06316438..f8d811257 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,18 +7,19 @@ -r docs/requirements-docs.txt coveralls==4.0.1 -ruff==0.4.10 -mypy==1.10.0 +ruff==0.5.0 +mypy==1.10.1 pre-commit==3.7.1 pytest==8.2.2 pytest-asyncio==0.23.7 pytest-cov==5.0.0 pytest-mock==3.14.0 pytest-random-order==1.1.1 +pytest-timeout==2.3.1 pytest-xdist==3.6.1 isort==5.13.2 # For datetime mocking -time-machine==2.14.1 +time-machine==2.14.2 # Convert jupyter notebooks to markdown documents nbconvert==7.16.4 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index ffde73b84..30ae0fac1 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,8 @@ -r requirements.txt # Required for hyperopt -scipy==1.13.1 +scipy==1.14.0; python_version >= "3.10" +scipy==1.13.1; python_version < "3.10" scikit-learn==1.5.0 ft-scikit-optimize==0.9.2 filelock==3.15.4 diff --git a/requirements.txt b/requirements.txt index 5dec7bb5d..170e5d805 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.50 +ccxt==4.3.54 cryptography==42.0.8 aiohttp==3.9.5 SQLAlchemy==2.0.31 @@ -16,7 +16,7 @@ cachetools==5.3.3 requests==2.32.3 urllib3==2.2.2 jsonschema==4.22.0 -TA-Lib==0.4.31 +TA-Lib==0.4.32 technical==1.4.3 tabulate==0.9.0 pycoingecko==3.1.0 @@ -30,7 +30,7 @@ pyarrow==16.1.0; platform_machine != 'armv7l' py_find_1st==1.1.6 # Load ticker files 30% faster -python-rapidjson==1.17 +python-rapidjson==1.18 # Properly format api responses orjson==3.10.5 @@ -42,7 +42,7 @@ fastapi==0.111.0 pydantic==2.7.4 uvicorn==0.30.1 pyjwt==2.8.0 -aiofiles==23.2.1 +aiofiles==24.1.0 psutil==6.0.0 # Support for colorized terminal output 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/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", 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 c9f923c13..fee8cab72 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), @@ -274,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: @@ -587,6 +588,7 @@ def get_default_conf(testdatadir): "exchange": { "name": "binance", "key": "key", + "enable_ws": False, "secret": "secret", "pair_whitelist": ["ETH/BTC", "LTC/BTC", "XRP/BTC", "NEO/BTC"], "pair_blacklist": [ @@ -628,6 +630,7 @@ def get_default_conf_usdt(testdatadir): "name": "binance", "enabled": True, "key": "key", + "enable_ws": False, "secret": "secret", "pair_whitelist": [ "ETH/USDT", 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/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_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_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/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/exchange/test_binance.py b/tests/exchange/test_binance.py index b961c2809..1de464097 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): - exchange = get_patched_exchange(mocker, default_conf, id="binance") - assert exchange._get_params(side, type, 1, False, 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, exchange="binance") + assert exchange._get_params(side, order_type, 1, False, time_in_force) == expected @pytest.mark.parametrize("trademode", [TradingMode.FUTURES, TradingMode.SPOT]) @@ -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_exchange_ws.py b/tests/exchange/test_exchange_ws.py new file mode 100644 index 000000000..8b97ae1b8 --- /dev/null +++ b/tests/exchange/test_exchange_ws.py @@ -0,0 +1,69 @@ +import asyncio +import threading +from time import sleep +from unittest.mock import AsyncMock, MagicMock + +from freqtrade.enums import CandleType +from freqtrade.exchange.exchange_ws import ExchangeWS + + +def test_exchangews_init(mocker): + config = MagicMock() + ccxt_object = MagicMock() + mocker.patch("freqtrade.exchange.exchange_ws.ExchangeWS._start_forever", MagicMock()) + + exchange_ws = ExchangeWS(config, ccxt_object) + sleep(0.1) + + assert exchange_ws.config == config + assert exchange_ws.ccxt_object == ccxt_object + assert exchange_ws._thread.name == "ccxt_ws" + assert exchange_ws._background_tasks == set() + assert exchange_ws._klines_watching == set() + assert exchange_ws._klines_scheduled == set() + assert exchange_ws.klines_last_refresh == {} + assert exchange_ws.klines_last_request == {} + # Cleanup + exchange_ws.cleanup() + + +def patch_eventloop_threading(exchange): + is_init = False + + def thread_fuck(): + nonlocal is_init + exchange._loop = asyncio.new_event_loop() + is_init = True + exchange._loop.run_forever() + + x = threading.Thread(target=thread_fuck, daemon=True) + x.start() + while not is_init: + pass + + +async def test_exchangews_ohlcv(mocker): + config = MagicMock() + ccxt_object = MagicMock() + ccxt_object.watch_ohlcv = AsyncMock() + ccxt_object.close = AsyncMock() + mocker.patch("freqtrade.exchange.exchange_ws.ExchangeWS._start_forever", MagicMock()) + + exchange_ws = ExchangeWS(config, ccxt_object) + patch_eventloop_threading(exchange_ws) + try: + assert exchange_ws._klines_watching == set() + assert exchange_ws._klines_scheduled == set() + + exchange_ws.schedule_ohlcv("ETH/BTC", "1m", CandleType.SPOT) + 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)} + await asyncio.sleep(0.1) + assert ccxt_object.watch_ohlcv.call_count == 1 + except Exception as e: + print(e) + finally: + # Cleanup + exchange_ws.cleanup() 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": diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py index 8820ce3e7..a2e6c8a74 100644 --- a/tests/exchange_online/conftest.py +++ b/tests/exchange_online/conftest.py @@ -11,6 +11,8 @@ from tests.conftest import EXMS, get_default_conf_usdt EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str] +EXCHANGE_WS_FIXTURE_TYPE = Tuple[Exchange, str, str] + # Exchanges that should be tested online EXCHANGES = { @@ -360,6 +362,7 @@ def set_test_proxy(config: Config, use_proxy: bool) -> Config: config1 = deepcopy(config) config1["exchange"]["ccxt_config"] = { "httpsProxy": proxy, + "wsProxy": proxy, } return config1 @@ -376,7 +379,7 @@ def get_exchange(exchange_name, exchange_conf): exchange_conf, validate=True, load_leverage_tiers=True ) - yield exchange, exchange_name + return exchange, exchange_name def get_futures_exchange(exchange_name, exchange_conf, class_mocker): @@ -398,15 +401,41 @@ def get_futures_exchange(exchange_name, exchange_conf, class_mocker): class_mocker.patch(f"{EXMS}.load_cached_leverage_tiers", return_value=None) class_mocker.patch(f"{EXMS}.cache_leverage_tiers") - yield from get_exchange(exchange_name, exchange_conf) + return get_exchange(exchange_name, exchange_conf) @pytest.fixture(params=EXCHANGES, scope="class") def exchange(request, exchange_conf, class_mocker): class_mocker.patch("freqtrade.exchange.bybit.Bybit.additional_exchange_init") - yield from get_exchange(request.param, exchange_conf) + return get_exchange(request.param, exchange_conf) @pytest.fixture(params=EXCHANGES, scope="class") def exchange_futures(request, exchange_conf, class_mocker): - yield from get_futures_exchange(request.param, exchange_conf, class_mocker) + return get_futures_exchange(request.param, exchange_conf, class_mocker) + + +@pytest.fixture(params=["spot", "futures"], scope="class") +def exchange_mode(request): + return request.param + + +@pytest.fixture(params=EXCHANGES, scope="class") +def exchange_ws(request, exchange_conf, exchange_mode, class_mocker): + class_mocker.patch("freqtrade.exchange.bybit.Bybit.additional_exchange_init") + exchange_conf["exchange"]["enable_ws"] = True + if exchange_mode == "spot": + exchange, name = get_exchange(request.param, exchange_conf) + pair = EXCHANGES[request.param]["pair"] + elif EXCHANGES[request.param].get("futures"): + exchange, name = get_futures_exchange( + request.param, exchange_conf, class_mocker=class_mocker + ) + pair = EXCHANGES[request.param]["futures_pair"] + else: + pytest.skip("Exchange does not support futures.") + + if not exchange._has_watch_ohlcv: + pytest.skip("Exchange does not support watch_ohlcv.") + yield exchange, name, pair + exchange.close() diff --git a/tests/exchange_online/test_ccxt_ws_compat.py b/tests/exchange_online/test_ccxt_ws_compat.py new file mode 100644 index 000000000..ed449bb58 --- /dev/null +++ b/tests/exchange_online/test_ccxt_ws_compat.py @@ -0,0 +1,64 @@ +""" +Tests in this file do NOT mock network calls, so they are expected to be fluky at times. + +However, these tests aim to test ccxt compatibility, specifically regarding websockets. +""" + +import logging +from datetime import timedelta +from time import sleep + +import pytest + +from freqtrade.enums import CandleType +from freqtrade.exchange.exchange_utils import timeframe_to_prev_date +from freqtrade.loggers.set_log_levels import set_loggers +from freqtrade.util.datetime_helpers import dt_now +from tests.conftest import log_has_re +from tests.exchange_online.conftest import EXCHANGE_WS_FIXTURE_TYPE + + +@pytest.mark.longrun +@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 + + assert exch._ws_async is not None + timeframe = "1m" + pair_tf = (pair, timeframe, CandleType.SPOT) + m_hist = mocker.spy(exch, "_async_get_historic_ohlcv") + m_cand = mocker.spy(exch, "_async_get_candle_history") + + res = exch.refresh_latest_ohlcv([pair_tf]) + assert m_cand.call_count == 1 + + # Currently open candle + next_candle = timeframe_to_prev_date(timeframe, dt_now()) + now = next_candle - timedelta(seconds=1) + # Currently closed candle + curr_candle = timeframe_to_prev_date(timeframe, now) + + assert pair_tf in exch._exchange_ws._klines_watching + assert pair_tf in exch._exchange_ws._klines_scheduled + assert res[pair_tf] is not None + df1 = res[pair_tf] + caplog.set_level(logging.DEBUG) + set_loggers(1) + assert df1.iloc[-1]["date"] == curr_candle + + # Wait until the next candle (might be up to 1 minute). + while True: + caplog.clear() + res = exch.refresh_latest_ohlcv([pair_tf]) + df2 = res[pair_tf] + assert df2 is not None + if df2.iloc[-1]["date"] == next_candle: + break + assert df2.iloc[-1]["date"] == curr_candle + sleep(1) + + assert m_hist.call_count == 0 + # shouldn't have tried fetch_ohlcv a second time. + assert m_cand.call_count == 1 + assert log_has_re(r"watch result.*", caplog) diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index db2e96b37..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) @@ -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", 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 diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 6e182e6e8..e9829a8cc 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -429,7 +429,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> backtesting.start() -def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> None: +def test_backtesting_no_pair_left(default_conf, mocker) -> None: mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) mocker.patch( "freqtrade.data.history.history_utils.load_pair_history", @@ -449,13 +449,6 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> with pytest.raises(OperationalException, match="No pair in whitelist."): Backtesting(default_conf) - default_conf["pairlists"] = [{"method": "VolumePairList", "number_assets": 5}] - with pytest.raises( - OperationalException, - match=r"VolumePairList not allowed for backtesting\..*StaticPairList.*", - ): - Backtesting(default_conf) - default_conf.update( { "pairlists": [{"method": "StaticPairList"}], @@ -469,7 +462,7 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> Backtesting(default_conf) -def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, tickers) -> None: +def test_backtesting_pairlist_list(default_conf, mocker, tickers) -> None: mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) mocker.patch(f"{EXMS}.get_tickers", tickers) mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y: y) @@ -495,12 +488,6 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti ): Backtesting(default_conf) - default_conf["pairlists"] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}] - with pytest.raises( - OperationalException, match="PerformanceFilter not allowed for backtesting." - ): - Backtesting(default_conf) - default_conf["pairlists"] = [ {"method": "StaticPairList"}, {"method": "PrecisionFilter"}, diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index f6c58a1e7..37ebdc58b 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -38,6 +38,7 @@ TESTABLE_PAIRLISTS = [p for p in AVAILABLE_PAIRLISTS if p not in ["RemotePairLis @pytest.fixture(scope="function") def whitelist_conf(default_conf): + default_conf["runmode"] = "dry_run" default_conf["stake_currency"] = "BTC" default_conf["exchange"]["pair_whitelist"] = [ "ETH/BTC", @@ -68,6 +69,7 @@ def whitelist_conf(default_conf): @pytest.fixture(scope="function") def whitelist_conf_2(default_conf): + default_conf["runmode"] = "dry_run" default_conf["stake_currency"] = "BTC" default_conf["exchange"]["pair_whitelist"] = [ "ETH/BTC", @@ -94,6 +96,7 @@ def whitelist_conf_2(default_conf): @pytest.fixture(scope="function") def whitelist_conf_agefilter(default_conf): + default_conf["runmode"] = "dry_run" default_conf["stake_currency"] = "BTC" default_conf["exchange"]["pair_whitelist"] = [ "ETH/BTC", @@ -773,7 +776,7 @@ def test_VolumePairList_whitelist_gen( whitelist_result, caplog, ) -> None: - whitelist_conf["runmode"] = "backtest" + whitelist_conf["runmode"] = "util_exchange" whitelist_conf["pairlists"] = pairlists whitelist_conf["stake_currency"] = base_currency @@ -2387,3 +2390,65 @@ def test_MarketCapPairList_exceptions(mocker, default_conf_usdt): OperationalException, match="This filter only support marketcap rank up to 250." ): PairListManager(exchange, default_conf_usdt) + + +@pytest.mark.parametrize( + "pairlists,expected_error,expected_warning", + [ + ( + [{"method": "StaticPairList"}], + None, # Error + None, # Warning + ), + ( + [{"method": "VolumePairList", "number_assets": 10}], + "VolumePairList", # Error + None, # Warning + ), + ( + [{"method": "MarketCapPairList", "number_assets": 10}], + None, # Error + r"MarketCapPairList.*lookahead.*", # Warning + ), + ( + [{"method": "StaticPairList"}, {"method": "FullTradesFilter"}], + None, # Error + r"FullTradesFilter do not generate.*", # Warning + ), + ( # combi, fails and warns + [ + {"method": "VolumePairList", "number_assets": 10}, + {"method": "MarketCapPairList", "number_assets": 10}, + ], + "VolumePairList", # Error + r"MarketCapPairList.*lookahead.*", # Warning + ), + ], +) +def test_backtesting_modes( + mocker, default_conf_usdt, pairlists, expected_error, expected_warning, caplog, markets, tickers +): + default_conf_usdt["runmode"] = "dry_run" + default_conf_usdt["pairlists"] = pairlists + + mocker.patch.multiple( + EXMS, + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers, + ) + exchange = get_patched_exchange(mocker, default_conf_usdt) + + # Dry run mode - works always + PairListManager(exchange, default_conf_usdt) + + default_conf_usdt["runmode"] = "backtest" + if expected_error: + with pytest.raises(OperationalException, match=f"Pairlist Handlers {expected_error}.*"): + PairListManager(exchange, default_conf_usdt) + + if not expected_error: + PairListManager(exchange, default_conf_usdt) + + if expected_warning: + assert log_has_re(f"Pairlist Handlers {expected_warning}", caplog) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index ffb2408f1..22264ae54 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -2154,6 +2154,7 @@ def test_api_exchanges(botclient): "valid": True, "supported": True, "comment": "", + "dex": False, "trade_modes": [ {"trading_mode": "spot", "margin_mode": ""}, {"trading_mode": "futures", "margin_mode": "isolated"}, @@ -2165,9 +2166,19 @@ def test_api_exchanges(botclient): "name": "mexc", "valid": True, "supported": False, + "dex": False, "comment": "", "trade_modes": [{"trading_mode": "spot", "margin_mode": ""}], } + waves = [x for x in response["exchanges"] if x["name"] == "wavesexchange"][0] + assert waves == { + "name": "wavesexchange", + "valid": True, + "supported": False, + "dex": True, + "comment": ANY, + "trade_modes": [{"trading_mode": "spot", "margin_mode": ""}], + } def test_api_freqaimodels(botclient, tmp_path, mocker):