Merge branch 'freqtrade:develop' into feature/stoploss-start-at

This commit is contained in:
Simon Waiblinger 2024-07-05 22:23:56 +02:00 committed by GitHub
commit f126120421
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
92 changed files with 1011 additions and 373 deletions

View File

@ -9,7 +9,7 @@ repos:
# stages: [push] # stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.10.0" rev: "v1.10.1"
hooks: hooks:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
@ -31,7 +31,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: 'v0.4.10' rev: 'v0.5.0'
hooks: hooks:
- id: ruff - id: ruff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `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.<br>**Keep it in secret, do not disclose publicly.** <br> **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). <br> **Datatype:** List | `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). <br> **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). <br> **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). <br> **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. <br> **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. <br> **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) <br> **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) <br> **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) <br> **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) <br> **Datatype:** Dict
| `exchange.enable_ws` | Enable the usage of Websockets for the exchange. <br>[More information](#consuming-exchange-websockets).<br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`*<br> **Datatype:** Boolean | `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`*<br> **Datatype:** Boolean
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean | `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean
@ -409,6 +410,8 @@ Or another example if your position adjustment assumes it can do 1 additional bu
--8<-- "includes/pricing.md" --8<-- "includes/pricing.md"
## Further Configuration details
### Understand minimal_roi ### Understand minimal_roi
The `minimal_roi` configuration parameter is a JSON object where the key is a duration 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. 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. 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 ## Using Dry-run mode
We recommend starting the bot in the Dry-run mode to see how your bot will 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. * 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`. * Wallets (`/balance`) are simulated based on `dry_run_wallet`.
* Orders are simulated, and will not be posted to the exchange. * 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 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. * 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. * 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! **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. 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. 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 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. 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": { "exchange": {
"ccxt_config": { "ccxt_config": {
"httpsProxy": "http://addr:port", "httpsProxy": "http://addr:port",
"wsProxy": "http://addr:port",
} }
} }
} }

View File

@ -22,7 +22,7 @@ This will spin up a local server (usually on port 8000) so you can see if everyt
## Developer setup ## 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]? ". 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`. This will install all required tools for development, including `pytest`, `ruff`, `mypy`, and `coveralls`.

View File

@ -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. 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. 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. 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 ### 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. 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.

View File

@ -1,12 +1,12 @@
"""Freqtrade bot""" """Freqtrade bot"""
__version__ = "2024.6-dev" __version__ = "2024.7-dev"
if "dev" in __version__: if "dev" in __version__:
from pathlib import Path from pathlib import Path
try: try:
import subprocess import subprocess # noqa: S404
freqtrade_basedir = Path(__file__).parent freqtrade_basedir = Path(__file__).parent

View File

@ -2,10 +2,10 @@
This module contains the argument manager class This module contains the argument manager class
""" """
import argparse from argparse import ArgumentParser, Namespace, _ArgumentGroup
from functools import partial from functools import partial
from pathlib import Path 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.commands.cli_options import AVAILABLE_CLI_OPTIONS
from freqtrade.constants import DEFAULT_CONFIG from freqtrade.constants import DEFAULT_CONFIG
@ -226,6 +226,19 @@ ARGS_ANALYZE_ENTRIES_EXITS = [
"analysis_csv_path", "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 = [ NO_CONF_REQURIED = [
"convert-data", "convert-data",
"convert-trade-data", "convert-trade-data",
@ -248,14 +261,6 @@ NO_CONF_REQURIED = [
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"] 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: class Arguments:
""" """
@ -264,7 +269,7 @@ class Arguments:
def __init__(self, args: Optional[List[str]]) -> None: def __init__(self, args: Optional[List[str]]) -> None:
self.args = args self.args = args
self._parsed_arg: Optional[argparse.Namespace] = None self._parsed_arg: Optional[Namespace] = None
def get_parsed_arg(self) -> Dict[str, Any]: def get_parsed_arg(self) -> Dict[str, Any]:
""" """
@ -277,7 +282,7 @@ class Arguments:
return vars(self._parsed_arg) 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. Parses given arguments and returns an argparse Namespace instance.
""" """
@ -306,7 +311,9 @@ class Arguments:
return parsed_arg 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: for val in optionlist:
opt = AVAILABLE_CLI_OPTIONS[val] opt = AVAILABLE_CLI_OPTIONS[val]
parser.add_argument(*opt.cli, dest=val, **opt.kwargs) parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
@ -317,16 +324,16 @@ class Arguments:
:return: None :return: None
""" """
# Build shared arguments (as group Common Options) # 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") group = _common_parser.add_argument_group("Common arguments")
self._build_args(optionlist=ARGS_COMMON, parser=group) 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") strategy_group = _strategy_parser.add_argument_group("Strategy arguments")
self._build_args(optionlist=ARGS_STRATEGY, parser=strategy_group) self._build_args(optionlist=ARGS_STRATEGY, parser=strategy_group)
# Build main command # Build main command
self.parser = argparse.ArgumentParser( self.parser = ArgumentParser(
prog="freqtrade", description="Free, open source crypto trading bot" prog="freqtrade", description="Free, open source crypto trading bot"
) )
self._build_args(optionlist=["version"], parser=self.parser) self._build_args(optionlist=["version"], parser=self.parser)

View File

@ -45,7 +45,8 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
"name": exchange["name"], "name": exchange["name"],
**valid_entry, **valid_entry,
"supported": "Official" if exchange["supported"] else "", "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"] (f"{a['margin_mode']} " if a["margin_mode"] else "") + a["trading_mode"]
for a in exchange["trade_modes"] for a in exchange["trade_modes"]
), ),

View File

@ -38,7 +38,7 @@ def chown_user_directory(directory: Path) -> None:
""" """
if running_in_docker(): if running_in_docker():
try: try:
import subprocess import subprocess # noqa: S404
subprocess.check_output(["sudo", "chown", "-R", "ftuser:", str(directory.resolve())]) subprocess.check_output(["sudo", "chown", "-R", "ftuser:", str(directory.resolve())])
except Exception: except Exception:

View File

@ -540,6 +540,7 @@ CONF_SCHEMA = {
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"type": "string"}, "name": {"type": "string"},
"enable_ws": {"type": "boolean", "default": True},
"key": {"type": "string", "default": ""}, "key": {"type": "string", "default": ""},
"secret": {"type": "string", "default": ""}, "secret": {"type": "string", "default": ""},
"password": {"type": "string", "default": ""}, "password": {"type": "string", "default": ""},

View File

@ -2,5 +2,8 @@
Module to handle data operations for freqtrade Module to handle data operations for freqtrade
""" """
from freqtrade.data import converter
# limit what's imported when using `from freqtrade.data import *` # limit what's imported when using `from freqtrade.data import *`
__all__ = ["converter"] __all__ = ["converter"]

View File

@ -30,6 +30,7 @@ class Binance(Exchange):
"trades_pagination_arg": "fromId", "trades_pagination_arg": "fromId",
"trades_has_history": True, "trades_has_history": True,
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
"ws.enabled": True,
} }
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"stoploss_order_types": {"limit": "stop", "market": "stop_market"}, "stoploss_order_types": {"limit": "stop", "market": "stop_market"},
@ -42,6 +43,7 @@ class Binance(Exchange):
PriceType.LAST: "CONTRACT_PRICE", PriceType.LAST: "CONTRACT_PRICE",
PriceType.MARK: "MARK_PRICE", PriceType.MARK: "MARK_PRICE",
}, },
"ws.enabled": False,
} }
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [

View File

@ -8437,7 +8437,7 @@
} }
} }
], ],
"BTC/USDT:USDT-240628": [ "BTC/USDT:USDT-240927": [
{ {
"tier": 1.0, "tier": 1.0,
"currency": "USDT", "currency": "USDT",
@ -8567,7 +8567,7 @@
} }
} }
], ],
"BTC/USDT:USDT-240927": [ "BTC/USDT:USDT-241227": [
{ {
"tier": 1.0, "tier": 1.0,
"currency": "USDT", "currency": "USDT",
@ -13805,7 +13805,7 @@
} }
} }
], ],
"ETH/USDT:USDT-240628": [ "ETH/USDT:USDT-240927": [
{ {
"tier": 1.0, "tier": 1.0,
"currency": "USDT", "currency": "USDT",
@ -13935,7 +13935,7 @@
} }
} }
], ],
"ETH/USDT:USDT-240927": [ "ETH/USDT:USDT-241227": [
{ {
"tier": 1.0, "tier": 1.0,
"currency": "USDT", "currency": "USDT",

View File

@ -33,6 +33,7 @@ class Bybit(Exchange):
"ohlcv_candle_limit": 1000, "ohlcv_candle_limit": 1000,
"ohlcv_has_history": True, "ohlcv_has_history": True,
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"], "order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
"ws.enabled": True,
"trades_has_history": False, # Endpoint doesn't support pagination "trades_has_history": False, # Endpoint doesn't support pagination
} }
_ft_has_futures: Dict = { _ft_has_futures: Dict = {

View File

@ -47,7 +47,7 @@ def check_exchange(config: Config, check_for_bad: bool = True) -> bool:
f'{", ".join(available_exchanges())}' f'{", ".join(available_exchanges())}'
) )
valid, reason = validate_exchange(exchange) valid, reason, _ = validate_exchange(exchange)
if not valid: if not valid:
if check_for_bad: if check_for_bad:
raise OperationalException( raise OperationalException(

View File

@ -92,6 +92,8 @@ EXCHANGE_HAS_OPTIONAL = [
# 'fetchMarketLeverageTiers', # Futures initialization # 'fetchMarketLeverageTiers', # Futures initialization
# 'fetchOpenOrder', 'fetchClosedOrder', # replacement for fetchOrder # 'fetchOpenOrder', 'fetchClosedOrder', # replacement for fetchOrder
# 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance... # 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance...
# ccxt.pro
"watchOHLCV",
] ]

View File

@ -14,7 +14,7 @@ from threading import Lock
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
import ccxt import ccxt
import ccxt.async_support as ccxt_async import ccxt.pro as ccxt_pro
from cachetools import TTLCache from cachetools import TTLCache
from ccxt import TICK_SIZE from ccxt import TICK_SIZE
from dateutil import parser from dateutil import parser
@ -34,7 +34,15 @@ from freqtrade.constants import (
PairWithTimeframe, PairWithTimeframe,
) )
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list 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 ( from freqtrade.exceptions import (
ConfigurationError, ConfigurationError,
DDosProtection, DDosProtection,
@ -56,7 +64,6 @@ from freqtrade.exchange.exchange_utils import (
ROUND, ROUND,
ROUND_DOWN, ROUND_DOWN,
ROUND_UP, ROUND_UP,
CcxtModuleType,
amount_to_contract_precision, amount_to_contract_precision,
amount_to_contracts, amount_to_contracts,
amount_to_precision, amount_to_precision,
@ -73,6 +80,7 @@ from freqtrade.exchange.exchange_utils_timeframe import (
timeframe_to_prev_date, timeframe_to_prev_date,
timeframe_to_seconds, timeframe_to_seconds,
) )
from freqtrade.exchange.exchange_ws import ExchangeWS
from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers
from freqtrade.misc import ( from freqtrade.misc import (
chunks, chunks,
@ -83,7 +91,7 @@ from freqtrade.misc import (
) )
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.util import dt_from_ts, dt_now 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 from freqtrade.util.periodic_cache import PeriodicCache
@ -130,6 +138,7 @@ class Exchange:
"marketOrderRequiresPrice": False, "marketOrderRequiresPrice": False,
"exchange_has_overrides": {}, # Dictionary overriding ccxt's "has". "exchange_has_overrides": {}, # Dictionary overriding ccxt's "has".
# Expected to be in the format {"fetchOHLCV": True} or {"fetchOHLCV": False} # 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: Dict = {}
_ft_has_futures: Dict = {} _ft_has_futures: Dict = {}
@ -152,7 +161,9 @@ class Exchange:
:return: None :return: None
""" """
self._api: ccxt.Exchange 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._markets: Dict = {}
self._trading_fees: Dict[str, Any] = {} self._trading_fees: Dict[str, Any] = {}
self._leverage_tiers: Dict[str, List[Dict]] = {} 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_config", {}), ccxt_config)
ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_sync_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 = self._ccxt_config
ccxt_async_config = deep_merge_dicts( ccxt_async_config = deep_merge_dicts(
@ -228,7 +239,15 @@ class Exchange:
ccxt_async_config = deep_merge_dicts( ccxt_async_config = deep_merge_dicts(
exchange_conf.get("ccxt_async_config", {}), ccxt_async_config 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}"') logger.info(f'Using Exchange "{self.name}"')
self.required_candle_call_count = 1 self.required_candle_call_count = 1
@ -257,6 +276,8 @@ class Exchange:
self.close() self.close()
def close(self): def close(self):
if self._exchange_ws:
self._exchange_ws.cleanup()
logger.debug("Exchange object destroyed, closing async loop") logger.debug("Exchange object destroyed, closing async loop")
if ( if (
getattr(self, "_api_async", None) getattr(self, "_api_async", None)
@ -265,6 +286,14 @@ class Exchange:
): ):
logger.debug("Closing async ccxt session.") logger.debug("Closing async ccxt session.")
self.loop.run_until_complete(self._api_async.close()) 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(): if self.loop and not self.loop.is_closed():
self.loop.close() self.loop.close()
@ -288,18 +317,22 @@ class Exchange:
self.validate_pricing(config["entry_pricing"]) self.validate_pricing(config["entry_pricing"])
def _init_ccxt( def _init_ccxt(
self, self, exchange_config: Dict[str, Any], sync: bool, ccxt_kwargs: Dict[str, Any]
exchange_config: Dict[str, Any],
ccxt_module: CcxtModuleType = ccxt,
*,
ccxt_kwargs: Dict,
) -> ccxt.Exchange: ) -> ccxt.Exchange:
""" """
Initialize ccxt with given config and return valid Initialize ccxt with given config and return valid ccxt instance.
ccxt instance.
""" """
# Find matching class for the given exchange name # Find matching class for the given exchange name
name = exchange_config["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): if not is_exchange_known_ccxt(name, ccxt_module):
raise OperationalException(f"Exchange {name} is not supported by ccxt") 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 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]: def _load_async_markets(self, reload: bool = False) -> Dict[str, Any]:
try: try:
markets = self.loop.run_until_complete( markets = self.loop.run_until_complete(
@ -562,6 +602,12 @@ class Exchange:
# Reload async markets, then assign them to sync api # Reload async markets, then assign them to sync api
self._markets = self._load_async_markets(reload=True) self._markets = self._load_async_markets(reload=True)
self._api.set_markets(self._api_async.markets, self._api_async.currencies) 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() self._last_markets_refresh = dt_ts()
if is_initial and self._ft_has["needs_trading_fees"]: 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", {}): if endpoint in self._ft_has.get("exchange_has_overrides", {}):
return self._ft_has["exchange_has_overrides"][endpoint] 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]: def get_precision_amount(self, pair: str) -> Optional[float]:
""" """
@ -2019,7 +2065,7 @@ class Exchange:
def get_fee( def get_fee(
self, self,
symbol: str, symbol: str,
type: str = "", order_type: str = "",
side: str = "", side: str = "",
amount: float = 1, amount: float = 1,
price: float = 1, price: float = 1,
@ -2028,13 +2074,13 @@ class Exchange:
""" """
Retrieve fee from exchange Retrieve fee from exchange
:param symbol: Pair :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 side: Side of order (buy, sell)
:param amount: Amount of order :param amount: Amount of order
:param price: Price of order :param price: Price of order
:param taker_or_maker: 'maker' or 'taker' (ignored if "type" is provided) :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" taker_or_maker = "taker"
try: try:
if self._config["dry_run"] and self._config.get("fee", None) is not None: 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( return self._api.calculate_fee(
symbol=symbol, symbol=symbol,
type=type, type=order_type,
side=side, side=side,
amount=amount, amount=amount,
price=price, price=price,
@ -2228,9 +2274,40 @@ class Exchange:
cache: bool, cache: bool,
) -> Coroutine[Any, Any, OHLCVResponse]: ) -> Coroutine[Any, Any, OHLCVResponse]:
not_all_data = cache and self.required_candle_call_count > 1 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: if cache and (pair, timeframe, candle_type) in self._klines:
candle_limit = self.ohlcv_candle_limit(timeframe, candle_type) 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. # 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): if min_date < self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0):
# Cache can be used - do one-off call. # Cache can be used - do one-off call.
@ -2263,7 +2340,7 @@ class Exchange:
def _build_ohlcv_dl_jobs( def _build_ohlcv_dl_jobs(
self, pair_list: ListPairsWithTimeframes, since_ms: Optional[int], cache: bool 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 Build Coroutines to execute as part of refresh_latest_ohlcv
""" """

View File

@ -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]] 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 returns: can_use, reason
with Reason including both missing and missing_opt 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 result = True
reason = "" reason = ""
if not ex_mod or not ex_mod.has: is_dex = getattr(ex_mod, "dex", False)
return False, ""
missing = [ missing = [
k k
for k, v in EXCHANGE_HAS_REQUIRED.items() for k, v in EXCHANGE_HAS_REQUIRED.items()
@ -81,18 +87,19 @@ def validate_exchange(exchange: str) -> Tuple[bool, str]:
if missing_opt: if missing_opt:
reason += f"{'. ' if reason else ''}missing opt: {', '.join(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( def _build_exchange_list_entry(
exchange_name: str, exchangeClasses: Dict[str, Any] exchange_name: str, exchangeClasses: Dict[str, Any]
) -> ValidExchangesType: ) -> ValidExchangesType:
valid, comment = validate_exchange(exchange_name) valid, comment, is_dex = validate_exchange(exchange_name)
result: ValidExchangesType = { result: ValidExchangesType = {
"name": exchange_name, "name": exchange_name,
"valid": valid, "valid": valid,
"supported": exchange_name.lower() in SUPPORTED_EXCHANGES, "supported": exchange_name.lower() in SUPPORTED_EXCHANGES,
"comment": comment, "comment": comment,
"dex": is_dex,
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}], "trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
} }
if resolved := exchangeClasses.get(exchange_name.lower()): if resolved := exchangeClasses.get(exchange_name.lower()):

View File

@ -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

View File

@ -52,7 +52,7 @@ class BaseEnvironment(gym.Env):
reward_kwargs: dict = {}, reward_kwargs: dict = {},
window_size=10, window_size=10,
starting_point=True, starting_point=True,
id: str = "baseenv-1", id: str = "baseenv-1", # noqa: A002
seed: int = 1, seed: int = 1,
config: dict = {}, config: dict = {},
live: bool = False, live: bool = False,

View File

@ -238,9 +238,9 @@ class FreqaiDataDrawer:
metadata, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE metadata, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE
) )
def np_encoder(self, object): def np_encoder(self, obj):
if isinstance(object, np.generic): if isinstance(obj, np.generic):
return object.item() return obj.item()
def get_pair_dict_info(self, pair: str) -> Tuple[str, int]: def get_pair_dict_info(self, pair: str) -> Tuple[str, int]:
""" """
@ -448,8 +448,8 @@ class FreqaiDataDrawer:
delete_dict: Dict[str, Any] = {} delete_dict: Dict[str, Any] = {}
for dir in model_folders: for directory in model_folders:
result = pattern.match(str(dir.name)) result = pattern.match(str(directory.name))
if result is None: if result is None:
continue continue
coin = result.group(1) coin = result.group(1)
@ -458,10 +458,10 @@ class FreqaiDataDrawer:
if coin not in delete_dict: if coin not in delete_dict:
delete_dict[coin] = {} delete_dict[coin] = {}
delete_dict[coin]["num_folders"] = 1 delete_dict[coin]["num_folders"] = 1
delete_dict[coin]["timestamps"] = {int(timestamp): dir} delete_dict[coin]["timestamps"] = {int(timestamp): directory}
else: else:
delete_dict[coin]["num_folders"] += 1 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: for coin in delete_dict:
if delete_dict[coin]["num_folders"] > num_keep: if delete_dict[coin]["num_folders"] > num_keep:
@ -612,9 +612,9 @@ class FreqaiDataDrawer:
elif self.model_type == "pytorch": elif self.model_type == "pytorch":
import torch import torch
zip = torch.load(dk.data_path / f"{dk.model_filename}_model.zip") zipfile = torch.load(dk.data_path / f"{dk.model_filename}_model.zip")
model = zip["pytrainer"] model = zipfile["pytrainer"]
model = model.load_from_checkpoint(zip) model = model.load_from_checkpoint(zipfile)
if not model: if not model:
raise OperationalException( raise OperationalException(

View File

@ -45,10 +45,10 @@ class TensorBoardCallback(BaseTensorBoardCallback):
return False return False
evals = ["validation", "train"] 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(): for metric_name, log in metric[1].items():
score = log[-1][0] if isinstance(log[-1], tuple) else log[-1] 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 return False

View File

@ -168,6 +168,8 @@ class FreqtradeBot(LoggingMixin):
t = str(time(time_slot, minutes, 2)) t = str(time(time_slot, minutes, 2))
self._schedule.every().day.at(t).do(update) 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() self.strategy.ft_bot_start()
# Initialize protections AFTER bot start - otherwise parameters are not loaded. # Initialize protections AFTER bot start - otherwise parameters are not loaded.
self.protections = ProtectionManager(self.config, self.strategy.protections) self.protections = ProtectionManager(self.config, self.strategy.protections)
@ -289,7 +291,6 @@ class FreqtradeBot(LoggingMixin):
# Then looking for entry opportunities # Then looking for entry opportunities
if self.get_free_open_trades(): if self.get_free_open_trades():
self.enter_positions() self.enter_positions()
if self.trading_mode == TradingMode.FUTURES:
self._schedule.run_pending() self._schedule.run_pending()
Trade.commit() Trade.commit()
self.rpc.process_msg_queue(self.dataprovider._msg_queue) self.rpc.process_msg_queue(self.dataprovider._msg_queue)
@ -2369,6 +2370,18 @@ class FreqtradeBot(LoggingMixin):
trade, order, order_obj, order_amount, order.get("trades", []) 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( def fee_detection_from_trades(
self, trade: Trade, order: Dict, order_obj: Order, order_amount: float, trades: List self, trade: Trade, order: Dict, order_obj: Order, order_amount: float, trades: List
) -> Optional[float]: ) -> Optional[float]:
@ -2376,7 +2389,7 @@ class FreqtradeBot(LoggingMixin):
fee-detection fallback to Trades. fee-detection fallback to Trades.
Either uses provided trades list or the result of fetch_my_trades to get correct fee. 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( trades = self.exchange.get_trades_for_order(
self.exchange.get_order_id_conditional(order), trade.pair, order_obj.order_date self.exchange.get_order_id_conditional(order), trade.pair, order_obj.order_date
) )

View File

@ -33,7 +33,7 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool =
if log: if log:
logger.info(f'dumping json to "{filename}"') 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) rapidjson.dump(data, fpz, default=str, number_mode=rapidjson.NM_NATIVE)
else: else:
if log: 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}"') 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 load data with rapidjson
Use this to have a consistent experience, 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. # Try gzip file first, otherwise regular json file.
if gzipfile.is_file(): if gzipfile.is_file():
logger.debug(f"Loading historical data from file {gzipfile}") 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) pairdata = json_load(datafile)
elif file.is_file(): elif file.is_file():
logger.debug(f"Loading historical data from file {file}") logger.debug(f"Loading historical data from file {file}")

View File

@ -217,8 +217,6 @@ class Backtesting:
raise OperationalException( raise OperationalException(
"VolumePairList not allowed for backtesting. Please use StaticPairList instead." "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: if len(self.strategylist) > 1 and "PrecisionFilter" in self.pairlists.name_list:
raise OperationalException( raise OperationalException(
@ -467,25 +465,25 @@ class Backtesting:
return data return data
def _get_close_rate( 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: ) -> float:
""" """
Get close rate for backtesting result Get close rate for backtesting result
""" """
# Special handling if high or low hit STOP_LOSS or ROI # 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.STOP_LOSS,
ExitType.TRAILING_STOP_LOSS, ExitType.TRAILING_STOP_LOSS,
ExitType.LIQUIDATION, ExitType.LIQUIDATION,
): ):
return self._get_close_rate_for_stoploss(row, trade, exit, trade_dur) return self._get_close_rate_for_stoploss(row, trade, exit_, trade_dur)
elif exit.exit_type == (ExitType.ROI): elif exit_.exit_type == (ExitType.ROI):
return self._get_close_rate_for_roi(row, trade, exit, trade_dur) return self._get_close_rate_for_roi(row, trade, exit_, trade_dur)
else: else:
return row[OPEN_IDX] return row[OPEN_IDX]
def _get_close_rate_for_stoploss( 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: ) -> float:
# our stoploss was already lower than candle high, # our stoploss was already lower than candle high,
# possibly due to a cancelled trade exit. # possibly due to a cancelled trade exit.
@ -493,7 +491,7 @@ class Backtesting:
is_short = trade.is_short or False is_short = trade.is_short or False
leverage = trade.leverage or 1.0 leverage = trade.leverage or 1.0
side_1 = -1 if is_short else 1 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 stoploss_value = trade.liquidation_price
else: else:
stoploss_value = trade.stop_loss stoploss_value = trade.stop_loss
@ -508,7 +506,7 @@ class Backtesting:
# Special case: trailing triggers within same candle as trade opened. Assume most # Special case: trailing triggers within same candle as trade opened. Assume most
# pessimistic price movement, which is moving just enough to arm stoploss and # pessimistic price movement, which is moving just enough to arm stoploss and
# immediately going down to stop price. # 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 ( if (
not self.strategy.use_custom_stoploss not self.strategy.use_custom_stoploss
and self.strategy.trailing_stop and self.strategy.trailing_stop
@ -539,7 +537,7 @@ class Backtesting:
return stoploss_value return stoploss_value
def _get_close_rate_for_roi( 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: ) -> float:
is_short = trade.is_short or False is_short = trade.is_short or False
leverage = trade.leverage or 1.0 leverage = trade.leverage or 1.0

View File

@ -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 Helper method to get either async context (for fastapi requests), or thread id
""" """
id = _request_id_ctx_var.get() request_id = _request_id_ctx_var.get()
if id is None: if request_id is None:
# when not in request context - use thread id # 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" _SQL_DOCS_URL = "http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls"

View File

@ -2012,7 +2012,7 @@ class Trade(ModelBase, LocalTrade):
).all() ).all()
resp: List[Dict] = [] 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" enter_tag = enter_tag if enter_tag is not None else "Other"
exit_reason = exit_reason if exit_reason is not None else "Other" exit_reason = exit_reason if exit_reason is not None else "Other"

View File

@ -13,7 +13,7 @@ from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.misc import plural 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 from freqtrade.util import PeriodicCache, dt_floor_day, dt_now, dt_ts
@ -21,6 +21,8 @@ logger = logging.getLogger(__name__)
class AgeFilter(IPairList): class AgeFilter(IPairList):
supports_backtesting = SupportsBacktesting.NO
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -7,13 +7,15 @@ from typing import List
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.plugins.pairlist.IPairList import IPairList, SupportsBacktesting
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FullTradesFilter(IPairList): class FullTradesFilter(IPairList):
supports_backtesting = SupportsBacktesting.NO_ACTION
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
""" """

View File

@ -5,6 +5,7 @@ PairList Handler base class
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from copy import deepcopy from copy import deepcopy
from enum import Enum
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
from freqtrade.constants import Config 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): class IPairList(LoggingMixin, ABC):
is_pairlist_generator = False is_pairlist_generator = False
supports_backtesting: SupportsBacktesting = SupportsBacktesting.NO
def __init__( def __init__(
self, self,

View File

@ -11,7 +11,7 @@ from cachetools import TTLCache
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers 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 from freqtrade.util.coin_gecko import FtCoinGeckoApi
@ -20,6 +20,7 @@ logger = logging.getLogger(__name__)
class MarketCapPairList(IPairList): class MarketCapPairList(IPairList):
is_pairlist_generator = True is_pairlist_generator = True
supports_backtesting = SupportsBacktesting.BIASED
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -7,13 +7,15 @@ from typing import Dict, List
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers 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__) logger = logging.getLogger(__name__)
class OffsetFilter(IPairList): class OffsetFilter(IPairList):
supports_backtesting = SupportsBacktesting.YES
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -9,13 +9,15 @@ import pandas as pd
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.persistence import Trade 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__) logger = logging.getLogger(__name__)
class PerformanceFilter(IPairList): class PerformanceFilter(IPairList):
supports_backtesting = SupportsBacktesting.NO_ACTION
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -8,13 +8,15 @@ from typing import Optional
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import ROUND_UP from freqtrade.exchange import ROUND_UP
from freqtrade.exchange.types import Ticker 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__) logger = logging.getLogger(__name__)
class PrecisionFilter(IPairList): class PrecisionFilter(IPairList):
supports_backtesting = SupportsBacktesting.BIASED
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -7,13 +7,15 @@ from typing import Dict, Optional
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Ticker 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__) logger = logging.getLogger(__name__)
class PriceFilter(IPairList): class PriceFilter(IPairList):
supports_backtesting = SupportsBacktesting.BIASED
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -9,7 +9,7 @@ from typing import Dict, List, Optional
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers 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__) logger = logging.getLogger(__name__)
@ -31,6 +31,7 @@ class ProducerPairList(IPairList):
""" """
is_pairlist_generator = True is_pairlist_generator = True
supports_backtesting = SupportsBacktesting.NO
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -16,7 +16,7 @@ from freqtrade import __version__
from freqtrade.configuration.load_config import CONFIG_PARSE_MODE from freqtrade.configuration.load_config import CONFIG_PARSE_MODE
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers 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 from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
@ -25,6 +25,8 @@ logger = logging.getLogger(__name__)
class RemotePairList(IPairList): class RemotePairList(IPairList):
is_pairlist_generator = True is_pairlist_generator = True
# Potential winner bias
supports_backtesting = SupportsBacktesting.BIASED
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -9,7 +9,7 @@ from typing import Dict, List, Literal
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.exchange import timeframe_to_seconds from freqtrade.exchange import timeframe_to_seconds
from freqtrade.exchange.types import Tickers 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 from freqtrade.util.periodic_cache import PeriodicCache
@ -19,6 +19,8 @@ ShuffleValues = Literal["candle", "iteration"]
class ShuffleFilter(IPairList): class ShuffleFilter(IPairList):
supports_backtesting = SupportsBacktesting.YES
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -7,13 +7,15 @@ from typing import Dict, Optional
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Ticker 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__) logger = logging.getLogger(__name__)
class SpreadFilter(IPairList): class SpreadFilter(IPairList):
supports_backtesting = SupportsBacktesting.NO
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -9,7 +9,7 @@ from copy import deepcopy
from typing import Dict, List from typing import Dict, List
from freqtrade.exchange.types import Tickers 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__) logger = logging.getLogger(__name__)
@ -17,6 +17,7 @@ logger = logging.getLogger(__name__)
class StaticPairList(IPairList): class StaticPairList(IPairList):
is_pairlist_generator = True is_pairlist_generator = True
supports_backtesting = SupportsBacktesting.YES
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -15,7 +15,7 @@ from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.misc import plural 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 from freqtrade.util import dt_floor_day, dt_now, dt_ts
@ -27,6 +27,8 @@ class VolatilityFilter(IPairList):
Filters pairs by volatility Filters pairs by volatility
""" """
supports_backtesting = SupportsBacktesting.NO
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -14,7 +14,7 @@ from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
from freqtrade.exchange.types import Tickers 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 from freqtrade.util import dt_now, format_ms_time
@ -26,6 +26,7 @@ SORT_VALUES = ["quoteVolume"]
class VolumePairList(IPairList): class VolumePairList(IPairList):
is_pairlist_generator = True is_pairlist_generator = True
supports_backtesting = SupportsBacktesting.NO
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -13,7 +13,7 @@ from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.misc import plural 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 from freqtrade.util import dt_floor_day, dt_now, dt_ts
@ -21,6 +21,8 @@ logger = logging.getLogger(__name__)
class RangeStabilityFilter(IPairList): class RangeStabilityFilter(IPairList):
supports_backtesting = SupportsBacktesting.NO
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -11,10 +11,11 @@ from cachetools import TTLCache, cached
from freqtrade.constants import Config, ListPairsWithTimeframes from freqtrade.constants import Config, ListPairsWithTimeframes
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.enums import CandleType from freqtrade.enums import CandleType
from freqtrade.enums.runmode import RunMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.mixins import LoggingMixin 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.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.resolvers import PairListResolver from freqtrade.resolvers import PairListResolver
@ -57,9 +58,44 @@ class PairListManager(LoggingMixin):
f"{invalid}." f"{invalid}."
) )
self._check_backtest()
refresh_period = config.get("pairlist_refresh_period", 3600) refresh_period = config.get("pairlist_refresh_period", 3600)
LoggingMixin.__init__(self, logger, refresh_period) 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 @property
def whitelist(self) -> List[str]: def whitelist(self) -> List[str]:
"""The current whitelist""" """The current whitelist"""

View File

@ -63,8 +63,8 @@ class IResolver:
# Add extra directory to the top of the search paths # Add extra directory to the top of the search paths
if extra_dirs: if extra_dirs:
for dir in extra_dirs: for directory in extra_dirs:
abs_paths.insert(0, Path(dir).resolve()) abs_paths.insert(0, Path(directory).resolve())
if cls.extra_path and (extra := config.get(cls.extra_path)): if cls.extra_path and (extra := config.get(cls.extra_path)):
abs_paths.insert(0, Path(extra).resolve()) abs_paths.insert(0, Path(extra).resolve())

View File

@ -311,9 +311,9 @@ def warn_deprecated_setting(strategy: IStrategy, old: str, new: str, error=False
setattr(strategy, new, getattr(strategy, f"{old}")) 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. Checks if a object overrides the parent class attribute.
:returns: True if the object is overridden. :returns: True if the object is overridden.
""" """
return getattr(type(object), attribute) != getattr(parentclass, attribute) return getattr(type(obj), attribute) != getattr(parentclass, attribute)

View File

@ -11,7 +11,7 @@ from typing import Any, Dict, Generator, List, Optional, Sequence, Tuple, Union
import psutil import psutil
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from dateutil.tz import tzlocal 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 pandas import DataFrame, NaT
from sqlalchemy import func, select from sqlalchemy import func, select
@ -204,9 +204,9 @@ class RPC:
trade.pair, side="exit", is_short=trade.is_short, refresh=False trade.pair, side="exit", is_short=trade.is_short, refresh=False
) )
except (ExchangeError, PricingError): except (ExchangeError, PricingError):
current_rate = NAN current_rate = nan
if len(trade.select_filled_orders(trade.entry_side)) > 0: 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): if not isnan(current_rate):
prof = trade.calculate_profit(current_rate) prof = trade.calculate_profit(current_rate)
current_profit = prof.profit_ratio current_profit = prof.profit_ratio
@ -277,7 +277,7 @@ class RPC:
raise RPCException("no active trade") raise RPCException("no active trade")
else: else:
trades_list = [] trades_list = []
fiat_profit_sum = NAN fiat_profit_sum = nan
for trade in trades: for trade in trades:
# calculate profit and send message to user # calculate profit and send message to user
try: try:
@ -285,9 +285,9 @@ class RPC:
trade.pair, side="exit", is_short=trade.is_short, refresh=False trade.pair, side="exit", is_short=trade.is_short, refresh=False
) )
except (PricingError, ExchangeError): except (PricingError, ExchangeError):
current_rate = NAN current_rate = nan
trade_profit = NAN trade_profit = nan
profit_str = f"{NAN:.2%}" profit_str = f"{nan:.2%}"
else: else:
if trade.nr_of_successful_entries > 0: if trade.nr_of_successful_entries > 0:
profit = trade.calculate_profit(current_rate) profit = trade.calculate_profit(current_rate)
@ -533,9 +533,9 @@ class RPC:
trade.pair, side="exit", is_short=trade.is_short, refresh=False trade.pair, side="exit", is_short=trade.is_short, refresh=False
) )
except (PricingError, ExchangeError): except (PricingError, ExchangeError):
current_rate = NAN current_rate = nan
profit_ratio = NAN profit_ratio = nan
profit_abs = NAN profit_abs = nan
else: else:
_profit = trade.calculate_profit(trade.close_rate or current_rate) _profit = trade.calculate_profit(trade.close_rate or current_rate)
@ -1317,7 +1317,7 @@ class RPC:
# replace NaT with `None` # replace NaT with `None`
dataframe[date_column] = dataframe[date_column].astype(object).replace({NaT: 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 = { res = {
"pair": pair, "pair": pair,

View File

@ -1787,7 +1787,7 @@ class Telegram(RPCHandler):
"_Bot Control_\n" "_Bot Control_\n"
"------------\n" "------------\n"
"*/start:* `Starts the trader`\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" "*/stopentry:* `Stops entering, but handles open trades gracefully` \n"
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, " "*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
"regardless of profit`\n" "regardless of profit`\n"
@ -1820,7 +1820,7 @@ class Telegram(RPCHandler):
"that represents the current market direction. If no direction is provided `" "that represents the current market direction. If no direction is provided `"
"`the currently set market direction will be output.` \n" "`the currently set market direction will be output.` \n"
"*/list_custom_data <trade_id> <key>:* `List custom_data for Trade ID & Key combo.`\n" "*/list_custom_data <trade_id> <key>:* `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" "_Statistics_\n"
"------------\n" "------------\n"
"*/status <trade_id>|[table]:* `Lists all open trades`\n" "*/status <trade_id>|[table]:* `Lists all open trades`\n"

View File

@ -14,4 +14,5 @@ class ValidExchangesType(TypedDict):
valid: bool valid: bool
supported: bool supported: bool
comment: str comment: str
dex: bool
trade_modes: List[TradeModeType] trade_modes: List[TradeModeType]

View File

@ -1,13 +1,13 @@
from freqtrade_client.ft_rest_client import FtRestClient from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2024.6-dev" __version__ = "2024.7-dev"
if "dev" in __version__: if "dev" in __version__:
from pathlib import Path from pathlib import Path
try: try:
import subprocess import subprocess # noqa: S404
freqtrade_basedir = Path(__file__).parent freqtrade_basedir = Path(__file__).parent

View File

@ -1,3 +1,3 @@
# Requirements for freqtrade client library # Requirements for freqtrade client library
requests==2.32.3 requests==2.32.3
python-rapidjson==1.17 python-rapidjson==1.18

View File

@ -133,8 +133,8 @@ def test_FtRestClient_call_invalid(caplog):
) )
def test_FtRestClient_call_explicit_methods(method, args, kwargs): def test_FtRestClient_call_explicit_methods(method, args, kwargs):
client, mock = get_rest_client() client, mock = get_rest_client()
exec = getattr(client, method) executor = getattr(client, method)
exec(*args, **kwargs) executor(*args, **kwargs)
assert mock.call_count == 1 assert mock.call_count == 1

View File

@ -134,6 +134,7 @@ extend-select = [
"W", # pycodestyle "W", # pycodestyle
"UP", # pyupgrade "UP", # pyupgrade
"I", # isort "I", # isort
"A", # flake8-builtins
"TID", # flake8-tidy-imports "TID", # flake8-tidy-imports
# "EXE", # flake8-executable # "EXE", # flake8-executable
# "C4", # flake8-comprehensions # "C4", # flake8-comprehensions
@ -144,6 +145,8 @@ extend-select = [
# "TCH", # flake8-type-checking # "TCH", # flake8-type-checking
"PTH", # flake8-use-pathlib "PTH", # flake8-use-pathlib
# "RUF", # ruff # "RUF", # ruff
"ASYNC", # flake8-async
"NPY", # numpy
] ]
extend-ignore = [ extend-ignore = [
@ -154,6 +157,7 @@ extend-ignore = [
"S603", # `subprocess` call: check for execution of untrusted input "S603", # `subprocess` call: check for execution of untrusted input
"S607", # Starting a process with a partial executable path "S607", # Starting a process with a partial executable path
"S608", # Possible SQL injection vector through string-based query construction "S608", # Possible SQL injection vector through string-based query construction
"NPY002", # Numpy legacy random generator
] ]
[tool.ruff.lint.mccabe] [tool.ruff.lint.mccabe]

View File

@ -7,18 +7,19 @@
-r docs/requirements-docs.txt -r docs/requirements-docs.txt
coveralls==4.0.1 coveralls==4.0.1
ruff==0.4.10 ruff==0.5.0
mypy==1.10.0 mypy==1.10.1
pre-commit==3.7.1 pre-commit==3.7.1
pytest==8.2.2 pytest==8.2.2
pytest-asyncio==0.23.7 pytest-asyncio==0.23.7
pytest-cov==5.0.0 pytest-cov==5.0.0
pytest-mock==3.14.0 pytest-mock==3.14.0
pytest-random-order==1.1.1 pytest-random-order==1.1.1
pytest-timeout==2.3.1
pytest-xdist==3.6.1 pytest-xdist==3.6.1
isort==5.13.2 isort==5.13.2
# For datetime mocking # For datetime mocking
time-machine==2.14.1 time-machine==2.14.2
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==7.16.4 nbconvert==7.16.4

View File

@ -2,7 +2,8 @@
-r requirements.txt -r requirements.txt
# Required for hyperopt # 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 scikit-learn==1.5.0
ft-scikit-optimize==0.9.2 ft-scikit-optimize==0.9.2
filelock==3.15.4 filelock==3.15.4

View File

@ -4,7 +4,7 @@ bottleneck==1.4.0
numexpr==2.10.1 numexpr==2.10.1
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==4.3.50 ccxt==4.3.54
cryptography==42.0.8 cryptography==42.0.8
aiohttp==3.9.5 aiohttp==3.9.5
SQLAlchemy==2.0.31 SQLAlchemy==2.0.31
@ -16,7 +16,7 @@ cachetools==5.3.3
requests==2.32.3 requests==2.32.3
urllib3==2.2.2 urllib3==2.2.2
jsonschema==4.22.0 jsonschema==4.22.0
TA-Lib==0.4.31 TA-Lib==0.4.32
technical==1.4.3 technical==1.4.3
tabulate==0.9.0 tabulate==0.9.0
pycoingecko==3.1.0 pycoingecko==3.1.0
@ -30,7 +30,7 @@ pyarrow==16.1.0; platform_machine != 'armv7l'
py_find_1st==1.1.6 py_find_1st==1.1.6
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.17 python-rapidjson==1.18
# Properly format api responses # Properly format api responses
orjson==3.10.5 orjson==3.10.5
@ -42,7 +42,7 @@ fastapi==0.111.0
pydantic==2.7.4 pydantic==2.7.4
uvicorn==0.30.1 uvicorn==0.30.1
pyjwt==2.8.0 pyjwt==2.8.0
aiofiles==23.2.1 aiofiles==24.1.0
psutil==6.0.0 psutil==6.0.0
# Support for colorized terminal output # Support for colorized terminal output

View File

@ -172,10 +172,10 @@ class ClientProtocol:
return readable_timedelta(time_delta) 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) 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"] key, la, df = data["key"], data["la"], data["df"]
if not df.empty: if not df.empty:
@ -189,8 +189,8 @@ class ClientProtocol:
else: else:
self.logger.info("Empty DataFrame") self.logger.info("Empty DataFrame")
async def _handle_default(self, name, type, data): async def _handle_default(self, name, msgtype, data):
self.logger.info("Unknown message of type {type} received...") self.logger.info("Unknown message of type {msgtype} received...")
self.logger.info(data) self.logger.info(data)

View File

@ -78,7 +78,7 @@ setup(
"httpx>=0.24.1", "httpx>=0.24.1",
"urllib3", "urllib3",
"jsonschema", "jsonschema",
"numpy", "numpy<2.0",
"pandas>=2.2.0,<3.0", "pandas>=2.2.0,<3.0",
"TA-Lib", "TA-Lib",
"pandas-ta", "pandas-ta",

View File

@ -167,7 +167,7 @@ def test_list_timeframes(mocker, capsys):
"1h": "hour", "1h": "hour",
"1d": "day", "1d": "day",
} }
patch_exchange(mocker, api_mock=api_mock, id="bybit") patch_exchange(mocker, api_mock=api_mock, exchange="bybit")
args = [ args = [
"list-timeframes", "list-timeframes",
] ]
@ -213,7 +213,7 @@ def test_list_timeframes(mocker, capsys):
"1d": "1d", "1d": "1d",
"3d": "3d", "3d": "3d",
} }
patch_exchange(mocker, api_mock=api_mock, id="binance") patch_exchange(mocker, api_mock=api_mock, exchange="binance")
# Test with --exchange binance # Test with --exchange binance
args = [ args = [
"list-timeframes", "list-timeframes",
@ -258,7 +258,7 @@ def test_list_timeframes(mocker, capsys):
def test_list_markets(mocker, markets_static, capsys): def test_list_markets(mocker, markets_static, capsys):
api_mock = MagicMock() 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 # Test with no --config
args = [ 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 "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 # Test with --exchange
args = ["list-markets", "--exchange", "binance"] args = ["list-markets", "--exchange", "binance"]
pargs = get_args(args) pargs = get_args(args)
@ -295,7 +295,7 @@ def test_list_markets(mocker, markets_static, capsys):
captured = capsys.readouterr() captured = capsys.readouterr()
assert re.match("\nExchange Binance has 12 active markets:\n", captured.out) 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 # Test with --all: all markets
args = [ args = [
"list-markets", "list-markets",
@ -823,7 +823,7 @@ def test_download_data_no_markets(mocker, caplog):
"freqtrade.data.history.history_utils.refresh_backtest_ohlcv_data", "freqtrade.data.history.history_utils.refresh_backtest_ohlcv_data",
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]), 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={}) mocker.patch(f"{EXMS}.get_markets", return_value={})
args = [ args = [
"download-data", "download-data",
@ -952,7 +952,7 @@ def test_download_data_trades(mocker):
def test_download_data_data_invalid(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={}) mocker.patch(f"{EXMS}.get_markets", return_value={})
args = [ args = [
"download-data", "download-data",

View File

@ -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) random_timestamps_in_seconds = np.random.uniform(_start_timestamp, _end_timestamp, n_rows)
timestamp = pd.to_datetime(random_timestamps_in_seconds, unit="s") 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) 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( df = pd.DataFrame(
{ {
"timestamp": timestamp, "timestamp": timestamp,
"id": id, "id": trade_id,
"type": None, "type": None,
"side": side, "side": side,
"price": price, "price": price,
@ -236,12 +236,12 @@ def patched_configuration_load_config_file(mocker, config) -> None:
def patch_exchange( 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: ) -> None:
mocker.patch(f"{EXMS}.validate_config", MagicMock()) mocker.patch(f"{EXMS}.validate_config", MagicMock())
mocker.patch(f"{EXMS}.validate_timeframes", MagicMock()) mocker.patch(f"{EXMS}.validate_timeframes", MagicMock())
mocker.patch(f"{EXMS}.id", PropertyMock(return_value=id)) mocker.patch(f"{EXMS}.id", PropertyMock(return_value=exchange))
mocker.patch(f"{EXMS}.name", PropertyMock(return_value=id.title())) mocker.patch(f"{EXMS}.name", PropertyMock(return_value=exchange.title()))
mocker.patch(f"{EXMS}.precisionMode", PropertyMock(return_value=2)) mocker.patch(f"{EXMS}.precisionMode", PropertyMock(return_value=2))
# Temporary patch ... # Temporary patch ...
mocker.patch("freqtrade.exchange.bybit.Bybit.cache_leverage_tiers") mocker.patch("freqtrade.exchange.bybit.Bybit.cache_leverage_tiers")
@ -254,7 +254,8 @@ def patch_exchange(
if mock_supported_modes: if mock_supported_modes:
mocker.patch( 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( PropertyMock(
return_value=[ return_value=[
(TradingMode.MARGIN, MarginMode.CROSS), (TradingMode.MARGIN, MarginMode.CROSS),
@ -274,10 +275,10 @@ def patch_exchange(
def get_patched_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: ) -> Exchange:
patch_exchange(mocker, api_mock, id, mock_markets, mock_supported_modes) patch_exchange(mocker, api_mock, exchange, mock_markets, mock_supported_modes)
config["exchange"]["name"] = id config["exchange"]["name"] = exchange
try: try:
exchange = ExchangeResolver.load_exchange(config, load_leverage_tiers=True) exchange = ExchangeResolver.load_exchange(config, load_leverage_tiers=True)
except ImportError: except ImportError:
@ -587,6 +588,7 @@ def get_default_conf(testdatadir):
"exchange": { "exchange": {
"name": "binance", "name": "binance",
"key": "key", "key": "key",
"enable_ws": False,
"secret": "secret", "secret": "secret",
"pair_whitelist": ["ETH/BTC", "LTC/BTC", "XRP/BTC", "NEO/BTC"], "pair_whitelist": ["ETH/BTC", "LTC/BTC", "XRP/BTC", "NEO/BTC"],
"pair_blacklist": [ "pair_blacklist": [
@ -628,6 +630,7 @@ def get_default_conf_usdt(testdatadir):
"name": "binance", "name": "binance",
"enabled": True, "enabled": True,
"key": "key", "key": "key",
"enable_ws": False,
"secret": "secret", "secret": "secret",
"pair_whitelist": [ "pair_whitelist": [
"ETH/USDT", "ETH/USDT",

View File

@ -83,7 +83,7 @@ def test_datahandler_ohlcv_regex(filename, pair, timeframe, candletype):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"input,expected", "pair,expected",
[ [
("XMR_USDT", "XMR/USDT"), ("XMR_USDT", "XMR/USDT"),
("BTC_USDT", "BTC/USDT"), ("BTC_USDT", "BTC/USDT"),
@ -95,8 +95,8 @@ def test_datahandler_ohlcv_regex(filename, pair, timeframe, candletype):
("UNITTEST_USDT", "UNITTEST/USDT"), ("UNITTEST_USDT", "UNITTEST/USDT"),
], ],
) )
def test_rebuild_pair_from_filename(input, expected): def test_rebuild_pair_from_filename(pair, expected):
assert IDataHandler.rebuild_pair_from_filename(input) == expected assert IDataHandler.rebuild_pair_from_filename(pair) == expected
def test_datahandler_ohlcv_get_available_data(testdatadir): def test_datahandler_ohlcv_get_available_data(testdatadir):

View File

@ -250,7 +250,7 @@ def test_refresh(mocker, default_conf):
refresh_mock = MagicMock() refresh_mock = MagicMock()
mocker.patch(f"{EXMS}.refresh_latest_ohlcv", refresh_mock) 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"] timeframe = default_conf["timeframe"]
pairs = [("XRP/BTC", timeframe), ("UNITTEST/BTC", timeframe)] pairs = [("XRP/BTC", timeframe), ("UNITTEST/BTC", timeframe)]

View File

@ -14,7 +14,7 @@ def test_download_data_main_no_markets(mocker, caplog):
"freqtrade.data.history.history_utils.refresh_backtest_ohlcv_data", "freqtrade.data.history.history_utils.refresh_backtest_ohlcv_data",
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]), 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={}) mocker.patch(f"{EXMS}.get_markets", return_value={})
config = setup_utils_configuration({"exchange": "binance"}, RunMode.UTIL_EXCHANGE) config = setup_utils_configuration({"exchange": "binance"}, RunMode.UTIL_EXCHANGE)
config.update({"days": 20, "pairs": ["ETH/BTC", "XRP/BTC"], "timeframes": ["5m", "1h"]}) 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): 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={}) mocker.patch(f"{EXMS}.get_markets", return_value={})
config = setup_utils_configuration({"exchange": "kraken"}, RunMode.UTIL_EXCHANGE) config = setup_utils_configuration({"exchange": "kraken"}, RunMode.UTIL_EXCHANGE)
config.update( config.update(

View File

@ -555,7 +555,7 @@ def test_refresh_backtest_ohlcv_data(
mocker.patch.object(Path, "unlink", MagicMock()) mocker.patch.object(Path, "unlink", MagicMock())
default_conf["trading_mode"] = trademode 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") timerange = TimeRange.parse_timerange("20190101-20190102")
refresh_backtest_ohlcv_data( refresh_backtest_ohlcv_data(
exchange=ex, exchange=ex,

View File

@ -17,7 +17,7 @@ def test_import_kraken_trades_from_csv(testdatadir, tmp_path, caplog, default_co
default_conf_usdt["exchange"]["name"] = "kraken" default_conf_usdt["exchange"]["name"] = "kraken"
patch_exchange(mocker, id="kraken") patch_exchange(mocker, exchange="kraken")
mocker.patch( mocker.patch(
f"{EXMS}.markets", f"{EXMS}.markets",
PropertyMock( PropertyMock(

View File

@ -12,7 +12,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
@pytest.mark.parametrize( @pytest.mark.parametrize(
"side,type,time_in_force,expected", "side,order_type,time_in_force,expected",
[ [
("buy", "limit", "gtc", {"timeInForce": "GTC"}), ("buy", "limit", "gtc", {"timeInForce": "GTC"}),
("buy", "limit", "IOC", {"timeInForce": "IOC"}), ("buy", "limit", "IOC", {"timeInForce": "IOC"}),
@ -22,9 +22,9 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
("sell", "market", "PO", {}), ("sell", "market", "PO", {}),
], ],
) )
def test__get_params_binance(default_conf, mocker, side, type, time_in_force, expected): def test__get_params_binance(default_conf, mocker, side, order_type, time_in_force, expected):
exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange = get_patched_exchange(mocker, default_conf, exchange="binance")
assert exchange._get_params(side, type, 1, False, time_in_force) == expected assert exchange._get_params(side, order_type, 1, False, time_in_force) == expected
@pytest.mark.parametrize("trademode", [TradingMode.FUTURES, TradingMode.SPOT]) @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")] "sl1,sl2,sl3,side", [(1501, 1499, 1501, "sell"), (1499, 1501, 1499, "buy")]
) )
def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): 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 = { order = {
"type": "stop_loss_limit", "type": "stop_loss_limit",
"price": 1500, "price": 1500,
@ -378,7 +378,7 @@ def test_fill_leverage_tiers_binance(default_conf, mocker):
default_conf["dry_run"] = False default_conf["dry_run"] = False
default_conf["trading_mode"] = TradingMode.FUTURES default_conf["trading_mode"] = TradingMode.FUTURES
default_conf["margin_mode"] = MarginMode.ISOLATED 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() exchange.fill_leverage_tiers()
assert exchange._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() api_mock = MagicMock()
default_conf["trading_mode"] = TradingMode.FUTURES default_conf["trading_mode"] = TradingMode.FUTURES
default_conf["margin_mode"] = MarginMode.ISOLATED 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() exchange.fill_leverage_tiers()
assert len(exchange._leverage_tiers.keys()) > 100 assert len(exchange._leverage_tiers.keys()) > 100
for key, value in leverage_tiers.items(): for key, value in leverage_tiers.items():
@ -518,10 +518,10 @@ def test_additional_exchange_init_binance(default_conf, mocker):
OperationalException, OperationalException,
match=r"Hedge Mode is not supported.*\nMulti-Asset Mode is not supported.*", 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.fapiPrivateGetPositionSideDual = MagicMock(return_value={"dualSidePosition": False})
api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock(return_value={"multiAssetsMargin": 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 assert exchange
ccxt_exceptionhandlers( ccxt_exceptionhandlers(
mocker, mocker,
@ -541,7 +541,7 @@ def test__set_leverage_binance(mocker, default_conf):
default_conf["trading_mode"] = TradingMode.FUTURES default_conf["trading_mode"] = TradingMode.FUTURES
default_conf["margin_mode"] = MarginMode.ISOLATED 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") exchange._set_leverage(3.2, "BTC/USDT:USDT")
assert api_mock.set_leverage.call_count == 1 assert api_mock.set_leverage.call_count == 1
# Leverage is rounded to 3. # 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 # Monkey-patch async function
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
@ -620,7 +620,7 @@ def test_get_maintenance_ratio_and_amt_binance(
amt, amt,
): ):
mocker.patch(f"{EXMS}.exchange_has", return_value=True) 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 exchange._leverage_tiers = leverage_tiers
(result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, nominal_value) (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) assert (round(result_ratio, 8), round(result_amt, 8)) == (mm_ratio, amt)

View File

@ -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) orders = exchange.get_trades_for_order(order_id, "LTC/BTC", since)
assert len(orders) == 1 assert len(orders) == 1

View File

@ -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.set_position_mode = MagicMock(return_value={"dualSidePosition": False})
api_mock.is_unified_enabled = MagicMock(return_value=[False, 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.set_position_mode.call_count == 1
assert api_mock.is_unified_enabled.call_count == 1 assert api_mock.is_unified_enabled.call_count == 1
assert exchange.unified_account is False 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.set_position_mode.reset_mock()
api_mock.is_unified_enabled = MagicMock(return_value=[False, True]) api_mock.is_unified_enabled = MagicMock(return_value=[False, True])
with pytest.raises(OperationalException, match=r"Bybit: Unified account is not supported.*"): 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) 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.set_position_mode.call_count == 1
# assert api_mock.is_unified_enabled.call_count == 1 # assert api_mock.is_unified_enabled.call_count == 1
# assert exchange.unified_account is True # 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" default_conf["margin_mode"] = "isolated"
api_mock = MagicMock() api_mock = MagicMock()
api_mock.fetch_funding_rate_history = get_mock_coro(return_value=[]) 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 limit = 200
# Test fetch_funding_rate_history (current data) # Test fetch_funding_rate_history (current data)
await exchange._fetch_funding_rate_history( 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): def test_bybit_get_funding_fees(default_conf, mocker):
now = datetime.now(timezone.utc) 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._fetch_and_calculate_funding_fees = MagicMock()
exchange.get_funding_fees("BTC/USDT:USDT", 1, False, now) exchange.get_funding_fees("BTC/USDT:USDT", 1, False, now)
assert exchange._fetch_and_calculate_funding_fees.call_count == 0 assert exchange._fetch_and_calculate_funding_fees.call_count == 0
default_conf["trading_mode"] = "futures" default_conf["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated" 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._fetch_and_calculate_funding_fees = MagicMock()
exchange.get_funding_fees("BTC/USDT:USDT", 1, False, now) 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) mocker.patch(f"{EXMS}.exchange_has", return_value=True)
start_time = datetime.now(timezone.utc) - timedelta(days=20) 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 # Not available in dry-run
assert exchange.fetch_orders("mocked", start_time) == [] assert exchange.fetch_orders("mocked", start_time) == []
assert api_mock.fetch_orders.call_count == 0 assert api_mock.fetch_orders.call_count == 0
default_conf["dry_run"] = False 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) res = exchange.fetch_orders("mocked", start_time)
# Bybit will call the endpoint 3 times, as it has a limit of 7 days per call # 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 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) 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") res = exchange.fetch_order("123", "BTC/USDT")
assert res["remaining"] is None assert res["remaining"] is None

View File

@ -117,19 +117,19 @@ def ccxt_exceptionhandlers(
with patch("freqtrade.exchange.common.time.sleep"): with patch("freqtrade.exchange.common.time.sleep"):
with pytest.raises(DDosProtection): with pytest.raises(DDosProtection):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DDos")) 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) getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.OperationFailed("DeaDBeef")) 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) getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) 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) getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 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): def test_validate_order_time_in_force(default_conf, mocker, caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
# explicitly test bybit, exchanges implementing other policies need separate tests # 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 = { tif = {
"buy": "gtc", "buy": "gtc",
"sell": "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): def test_price_get_one_pip(default_conf, mocker, price, precision_mode, precision, expected):
markets = PropertyMock(return_value={"ETH/BTC": {"precision": {"price": precision}}}) 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}.markets", markets)
mocker.patch(f"{EXMS}.precisionMode", PropertyMock(return_value=precision_mode)) mocker.patch(f"{EXMS}.precisionMode", PropertyMock(return_value=precision_mode))
pair = "ETH/BTC" 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: 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 stoploss = -0.05
markets = {"ETH/BTC": {"symbol": "ETH/BTC"}} 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" markets["ETH/BTC"]["contractSize"] = "0.01"
default_conf["trading_mode"] = "futures" default_conf["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated" 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)) mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets))
# Contract size 0.01 # 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: 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 stoploss = -0.05
markets = {"ETH/BTC": {"symbol": "ETH/BTC"}} 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) api_mock.load_markets = get_mock_coro(return_value=initial_markets)
default_conf["exchange"]["markets_refresh_interval"] = 10 default_conf["exchange"]["markets_refresh_interval"] = 10
exchange = get_patched_exchange( 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") lam_spy = mocker.spy(exchange, "_load_async_markets")
assert exchange._last_markets_refresh == dt_ts() 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")) api_mock.load_markets = get_mock_coro(side_effect=ccxt.NetworkError("LoadError"))
default_conf["exchange"]["markets_refresh_interval"] = 10 default_conf["exchange"]["markets_refresh_interval"] = 10
exchange = get_patched_exchange( 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 exchange._last_markets_refresh = 2
@ -1152,7 +1152,7 @@ def test_exchange_has(default_conf, mocker):
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_create_dry_run_order(default_conf, mocker, side, exchange_name, leverage): def test_create_dry_run_order(default_conf, mocker, side, exchange_name, leverage):
default_conf["dry_run"] = True 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( order = exchange.create_dry_run_order(
pair="ETH/BTC", ordertype="limit", side=side, amount=1, rate=200, leverage=leverage 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, leverage,
): ):
default_conf["dry_run"] = True 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( mocker.patch.multiple(
EXMS, EXMS,
exchange_has=MagicMock(return_value=True), 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, mocker, side, rate, amount, endprice, exchange_name, order_book_l2_usd, leverage
): ):
default_conf["dry_run"] = True 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( mocker.patch.multiple(
EXMS, EXMS,
exchange_has=MagicMock(return_value=True), 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" default_conf["margin_mode"] = "isolated"
mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y) mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y)
mocker.patch(f"{EXMS}.price_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_leverage = MagicMock()
exchange.set_margin_mode = MagicMock() exchange.set_margin_mode = MagicMock()
@ -1392,7 +1392,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
"amount": 1, "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.trading_mode = TradingMode.FUTURES
exchange._set_leverage = MagicMock() exchange._set_leverage = MagicMock()
exchange.set_margin_mode = 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) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_buy_dry_run(default_conf, mocker, exchange_name): def test_buy_dry_run(default_conf, mocker, exchange_name):
default_conf["dry_run"] = True 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( order = exchange.create_order(
pair="ETH/BTC", pair="ETH/BTC",
@ -1439,7 +1439,7 @@ def test_buy_prod(default_conf, mocker, exchange_name):
default_conf["dry_run"] = False default_conf["dry_run"] = False
mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y) mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y)
mocker.patch(f"{EXMS}.price_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( order = exchange.create_order(
pair="ETH/BTC", pair="ETH/BTC",
@ -1483,7 +1483,7 @@ def test_buy_prod(default_conf, mocker, exchange_name):
# test exception handling # test exception handling
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("Not enough funds")) 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( exchange.create_order(
pair="ETH/BTC", pair="ETH/BTC",
ordertype=order_type, ordertype=order_type,
@ -1496,7 +1496,7 @@ def test_buy_prod(default_conf, mocker, exchange_name):
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) 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( exchange.create_order(
pair="ETH/BTC", pair="ETH/BTC",
ordertype="limit", ordertype="limit",
@ -1509,7 +1509,7 @@ def test_buy_prod(default_conf, mocker, exchange_name):
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) 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( exchange.create_order(
pair="ETH/BTC", pair="ETH/BTC",
ordertype="market", ordertype="market",
@ -1522,7 +1522,7 @@ def test_buy_prod(default_conf, mocker, exchange_name):
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("Network disconnect")) 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( exchange.create_order(
pair="ETH/BTC", pair="ETH/BTC",
ordertype=order_type, ordertype=order_type,
@ -1535,7 +1535,7 @@ def test_buy_prod(default_conf, mocker, exchange_name):
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("Unknown error")) 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( exchange.create_order(
pair="ETH/BTC", pair="ETH/BTC",
ordertype=order_type, ordertype=order_type,
@ -1558,7 +1558,7 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name):
default_conf["dry_run"] = False default_conf["dry_run"] = False
mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y) mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y)
mocker.patch(f"{EXMS}.price_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" order_type = "limit"
time_in_force = "ioc" 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}.amount_to_precision", lambda s, x, y: y)
mocker.patch(f"{EXMS}.price_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( order = exchange.create_order(
pair="ETH/BTC", ordertype=order_type, side="sell", amount=1, rate=200, leverage=1.0 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 # test exception handling
with pytest.raises(InsufficientFundsError): with pytest.raises(InsufficientFundsError):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) 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( exchange.create_order(
pair="ETH/BTC", ordertype=order_type, side="sell", amount=1, rate=200, leverage=1.0 pair="ETH/BTC", ordertype=order_type, side="sell", amount=1, rate=200, leverage=1.0
) )
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) 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( exchange.create_order(
pair="ETH/BTC", ordertype="limit", side="sell", amount=1, rate=200, leverage=1.0 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 # Market orders don't require price, so the behaviour is slightly different
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) 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( exchange.create_order(
pair="ETH/BTC", ordertype="market", side="sell", amount=1, rate=200, leverage=1.0 pair="ETH/BTC", ordertype="market", side="sell", amount=1, rate=200, leverage=1.0
) )
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No Connection")) 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( exchange.create_order(
pair="ETH/BTC", ordertype=order_type, side="sell", amount=1, rate=200, leverage=1.0 pair="ETH/BTC", ordertype=order_type, side="sell", amount=1, rate=200, leverage=1.0
) )
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) 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( exchange.create_order(
pair="ETH/BTC", ordertype=order_type, side="sell", amount=1, rate=200, leverage=1.0 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 default_conf["dry_run"] = False
mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y) mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y)
mocker.patch(f"{EXMS}.price_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" order_type = "limit"
time_in_force = "ioc" 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} return_value={"1ST": balance_item, "2ND": balance_item, "3RD": balance_item}
) )
default_conf["dry_run"] = False 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 len(exchange.get_balances()) == 3
assert exchange.get_balances()["1ST"]["free"] == 10.0 assert exchange.get_balances()["1ST"]["free"] == 10.0
assert exchange.get_balances()["1ST"]["total"] == 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}, {"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() == [] assert exchange.fetch_positions() == []
default_conf["dry_run"] = False default_conf["dry_run"] = False
default_conf["trading_mode"] = "futures" 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() res = exchange.fetch_positions()
assert len(res) == 2 assert len(res) == 2
@ -1830,13 +1830,13 @@ def test_fetch_orders(default_conf, mocker, exchange_name, limit_order):
if exchange_name == "bybit": if exchange_name == "bybit":
expected = 3 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 # Not available in dry-run
assert exchange.fetch_orders("mocked", start_time) == [] assert exchange.fetch_orders("mocked", start_time) == []
assert api_mock.fetch_orders.call_count == 0 assert api_mock.fetch_orders.call_count == 0
default_conf["dry_run"] = False 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) res = exchange.fetch_orders("mocked", start_time)
assert api_mock.fetch_orders.call_count == expected assert api_mock.fetch_orders.call_count == expected
assert api_mock.fetch_open_orders.call_count == 0 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 default_conf["margin_mode"] = MarginMode.ISOLATED
api_mock.fetch_trading_fees = MagicMock(return_value=tick) api_mock.fetch_trading_fees = MagicMock(return_value=tick)
mocker.patch(f"{EXMS}.exchange_has", return_value=True) 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 "1INCH/USDT:USDT" in exchange._trading_fees
assert "ETH/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={}) 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() exchange.fetch_trading_fees()
mocker.patch(f"{EXMS}.exchange_has", return_value=True) mocker.patch(f"{EXMS}.exchange_has", return_value=True)
assert exchange.fetch_trading_fees() == {} assert exchange.fetch_trading_fees() == {}
@ -1977,7 +1977,7 @@ def test_fetch_bids_asks(default_conf, mocker):
exchange_name = "binance" exchange_name = "binance"
api_mock.fetch_bids_asks = MagicMock(return_value=tick) api_mock.fetch_bids_asks = MagicMock(return_value=tick)
mocker.patch(f"{EXMS}.exchange_has", return_value=True) 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 # retrieve original ticker
bidsasks = exchange.fetch_bids_asks() bidsasks = exchange.fetch_bids_asks()
@ -2004,11 +2004,11 @@ def test_fetch_bids_asks(default_conf, mocker):
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.fetch_bids_asks = MagicMock(side_effect=ccxt.NotSupported("DeadBeef")) 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() exchange.fetch_bids_asks()
api_mock.fetch_bids_asks = MagicMock(return_value={}) 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() exchange.fetch_bids_asks()
mocker.patch(f"{EXMS}.exchange_has", return_value=True) mocker.patch(f"{EXMS}.exchange_has", return_value=True)
assert exchange.fetch_bids_asks() == {} 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) mocker.patch(f"{EXMS}.exchange_has", return_value=True)
api_mock.fetch_tickers = MagicMock(return_value=tick) api_mock.fetch_tickers = MagicMock(return_value=tick)
api_mock.fetch_bids_asks = MagicMock(return_value={}) 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 # retrieve original ticker
tickers = exchange.get_tickers() tickers = exchange.get_tickers()
@ -2064,19 +2064,19 @@ def test_get_tickers(default_conf, mocker, exchange_name, caplog):
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported("DeadBeef")) 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() exchange.get_tickers()
caplog.clear() caplog.clear()
api_mock.fetch_tickers = MagicMock(side_effect=[ccxt.BadSymbol("SomeSymbol"), []]) 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() x = exchange.get_tickers()
assert x == [] assert x == []
assert log_has_re(r"Could not load tickers due to BadSymbol\..*SomeSymbol", caplog) assert log_has_re(r"Could not load tickers due to BadSymbol\..*SomeSymbol", caplog)
caplog.clear() caplog.clear()
api_mock.fetch_tickers = MagicMock(return_value={}) 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() exchange.get_tickers()
api_mock.fetch_tickers.reset_mock() 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["trading_mode"] = TradingMode.FUTURES
default_conf["margin_mode"] = MarginMode.ISOLATED default_conf["margin_mode"] = MarginMode.ISOLATED
mocker.patch(f"{EXMS}.exchange_has", return_value=True) 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() exchange.get_tickers()
assert api_mock.fetch_tickers.call_count == 1 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.fetch_ticker = MagicMock(return_value=tick)
api_mock.markets = {"ETH/BTC": {"active": True}} 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 # retrieve original ticker
ticker = exchange.fetch_ticker(pair="ETH/BTC") ticker = exchange.fetch_ticker(pair="ETH/BTC")
@ -2122,7 +2122,7 @@ def test_fetch_ticker(default_conf, mocker, exchange_name):
"last": 42, "last": 42,
} }
api_mock.fetch_ticker = MagicMock(return_value=tick) 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 caching the result we should get the same ticker
# if not fetching a new result we should get the cached 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={}) 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") exchange.fetch_ticker(pair="ETH/BTC")
with pytest.raises(DependencyException, match=r"Pair XRP/ETH not available"): 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) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test___now_is_time_to_refresh(default_conf, mocker, exchange_name, time_machine): 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" pair = "BTC/USDT"
candle_type = CandleType.SPOT candle_type = CandleType.SPOT
start_dt = datetime(2023, 12, 1, 0, 10, 0, tzinfo=timezone.utc) 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("exchange_name", EXCHANGES)
@pytest.mark.parametrize("candle_type", ["mark", ""]) @pytest.mark.parametrize("candle_type", ["mark", ""])
def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_type): 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 = [ ohlcv = [
[ [
dt_ts(), # unix timestamp ms 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) 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 # Monkey-patch async function
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) 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) 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 # Monkey-patch async function
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) 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.*" OperationalException, match=r"Could not fetch historical candle \(OHLCV\) data.*"
): ):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error")) 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( await exchange._async_get_candle_history(
pair, "5m", CandleType.SPOT, dt_ts(dt_now() - timedelta(seconds=2000)) 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\..*", match=r"Exchange.* does not support fetching " r"historical candle \(OHLCV\) data\..*",
): ):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported")) 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( await exchange._async_get_candle_history(
pair, "5m", CandleType.SPOT, dt_ts(dt_now() - timedelta(seconds=2000)) 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"}' '{"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")) mocker.patch(f"{EXMS}.name", PropertyMock(return_value="KuCoin"))
msg = "Kucoin 429 error, avoid triggering DDosProtection backoff delay" 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 = MagicMock()
api_mock.fetch_l2_order_book = order_book_l2 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) order_book = exchange.fetch_l2_order_book(pair="ETH/BTC", limit=10)
assert "bids" in order_book assert "bids" in order_book
assert "asks" 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() api_mock = MagicMock()
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported("Not supported")) 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) exchange.fetch_l2_order_book(pair="ETH/BTC", limit=50)
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) 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) exchange.fetch_l2_order_book(pair="ETH/BTC", limit=50)
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) 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) 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], [1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687],
[1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867], [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) exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
sort_mock = mocker.patch("freqtrade.exchange.exchange.sorted", MagicMock(side_effect=sort_data)) sort_mock = mocker.patch("freqtrade.exchange.exchange.sorted", MagicMock(side_effect=sort_data))
# Test the OHLCV data sort # Test the OHLCV data sort
@ -3128,7 +3128,7 @@ async def test__async_fetch_trades(
default_conf, mocker, caplog, exchange_name, fetch_trades_result default_conf, mocker, caplog, exchange_name, fetch_trades_result
): ):
caplog.set_level(logging.DEBUG) 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 # Monkey-patch async function
exchange._api_async.fetch_trades = get_mock_coro(fetch_trades_result) exchange._api_async.fetch_trades = get_mock_coro(fetch_trades_result)
@ -3182,7 +3182,7 @@ async def test__async_fetch_trades(
api_mock = MagicMock() api_mock = MagicMock()
with pytest.raises(OperationalException, match=r"Could not fetch trade data*"): with pytest.raises(OperationalException, match=r"Could not fetch trade data*"):
api_mock.fetch_trades = MagicMock(side_effect=ccxt.BaseError("Unknown error")) 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))) await exchange._async_fetch_trades(pair, since=dt_ts(dt_now() - timedelta(seconds=2000)))
exchange.close() exchange.close()
@ -3191,7 +3191,7 @@ async def test__async_fetch_trades(
match=r"Exchange.* does not support fetching " r"historical trade data\..*", match=r"Exchange.* does not support fetching " r"historical trade data\..*",
): ):
api_mock.fetch_trades = MagicMock(side_effect=ccxt.NotSupported("Not supported")) 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))) await exchange._async_fetch_trades(pair, since=dt_ts(dt_now() - timedelta(seconds=2000)))
exchange.close() exchange.close()
@ -3203,7 +3203,7 @@ async def test__async_fetch_trades_contract_size(
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
default_conf["margin_mode"] = "isolated" default_conf["margin_mode"] = "isolated"
default_conf["trading_mode"] = "futures" 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 # Monkey-patch async function
exchange._api_async.fetch_trades = get_mock_coro( 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( async def test__async_get_trade_history_id(
default_conf, mocker, exchange_name, fetch_trades_result 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": if exchange._trades_pagination != "id":
exchange.close() exchange.close()
pytest.skip("Exchange does not support pagination by trade id") 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): def test__valid_trade_pagination_id(mocker, default_conf_usdt, exchange_name, trade_id, expected):
if exchange_name == "kraken": if exchange_name == "kraken":
pytest.skip("Kraken has a different pagination id format, and an explicit test.") 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 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:] return fetch_trades_result[-1:]
caplog.set_level(logging.DEBUG) 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": if exchange._trades_pagination != "time":
exchange.close() exchange.close()
pytest.skip("Exchange does not support pagination by timestamp") pytest.skip("Exchange does not support pagination by timestamp")
@ -3366,7 +3366,7 @@ async def test__async_get_trade_history_time_empty(
return [], None return [], None
caplog.set_level(logging.DEBUG) 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 # Monkey-patch async function
exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist) exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist)
pair = "ETH/BTC" pair = "ETH/BTC"
@ -3387,7 +3387,7 @@ async def test__async_get_trade_history_time_empty(
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_historic_trades(default_conf, mocker, caplog, exchange_name, trades_history): def test_get_historic_trades(default_conf, mocker, caplog, exchange_name, trades_history):
mocker.patch(f"{EXMS}.exchange_has", return_value=True) 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" pair = "ETH/BTC"
@ -3418,7 +3418,7 @@ def test_get_historic_trades_notsupported(
default_conf, mocker, caplog, exchange_name, trades_history default_conf, mocker, caplog, exchange_name, trades_history
): ):
mocker.patch(f"{EXMS}.exchange_has", return_value=False) 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" pair = "ETH/BTC"
@ -3432,7 +3432,7 @@ def test_get_historic_trades_notsupported(
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_cancel_order_dry_run(default_conf, mocker, exchange_name): def test_cancel_order_dry_run(default_conf, mocker, exchange_name):
default_conf["dry_run"] = True 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) mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=True)
assert exchange.cancel_order(order_id="123", pair="TKN/BTC") == {} assert exchange.cancel_order(order_id="123", pair="TKN/BTC") == {}
assert exchange.cancel_stoploss_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): 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 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): 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 assert exchange.is_cancel_order_result_suitable(order) == result
@ -3507,7 +3507,7 @@ def test_cancel_order_with_result(
api_mock = MagicMock() api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value=corder) api_mock.cancel_order = MagicMock(return_value=corder)
api_mock.fetch_order = MagicMock(return_value={}) 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) res = exchange.cancel_order_with_result("1234", "ETH/BTC", 1234)
assert isinstance(res, dict) assert isinstance(res, dict)
assert api_mock.cancel_order.call_count == call_corder 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 = MagicMock()
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) 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")) 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) res = exchange.cancel_order_with_result("1234", "ETH/BTC", 1541)
assert isinstance(res, dict) assert isinstance(res, dict)
@ -3536,12 +3536,12 @@ def test_cancel_order(default_conf, mocker, exchange_name):
default_conf["dry_run"] = False default_conf["dry_run"] = False
api_mock = MagicMock() api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value={"id": "123"}) 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"} assert exchange.cancel_order(order_id="_", pair="TKN/BTC") == {"id": "123"}
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) 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") exchange.cancel_order(order_id="_", pair="TKN/BTC")
assert api_mock.cancel_order.call_count == 1 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 default_conf["dry_run"] = False
api_mock = MagicMock() api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value={"id": "123"}) 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"} assert exchange.cancel_stoploss_order(order_id="_", pair="TKN/BTC") == {"id": "123"}
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) 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") exchange.cancel_stoploss_order(order_id="_", pair="TKN/BTC")
assert api_mock.cancel_order.call_count == 1 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" mock_prefix = "freqtrade.exchange.okx.Okx"
mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value={"for": 123}) mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value={"for": 123})
mocker.patch(f"{mock_prefix}.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} res = {"fee": {}, "status": "canceled", "amount": 1234}
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=res) 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") exc = InvalidOrderException("Did not find order")
mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=exc) mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=exc)
mocker.patch(f"{mock_prefix}.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) 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" order.symbol = "TKN/BTC"
mocker.patch(f"{EXMS}.exchange_has", return_value=True) 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 exchange._dry_run_open_orders["X"] = order
assert exchange.fetch_order("X", "TKN/BTC").myid == 123 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 default_conf["dry_run"] = False
api_mock = MagicMock() api_mock = MagicMock()
api_mock.fetch_order = MagicMock(return_value={"id": "123", "amount": 2, "symbol": "TKN/BTC"}) 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 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) assert log_has(("API fetch_order: {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}"), caplog)
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) 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") exchange.fetch_order(order_id="_", pair="TKN/BTC")
assert api_mock.fetch_order.call_count == 1 assert api_mock.fetch_order.call_count == 1
api_mock.fetch_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found")) 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 patch("freqtrade.exchange.common.time.sleep") as tm:
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
exchange.fetch_order(order_id="_", pair="TKN/BTC") 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.myid = 123
order.symbol = "TKN/BTC" 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) mocker.patch(f"{EXMS}.exchange_has", return_value=False)
exchange._dry_run_open_orders["X"] = order exchange._dry_run_open_orders["X"] = order
# Dry run - regular fetch_order behavior # 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( api_mock.fetch_closed_order = MagicMock(
return_value={"id": "123", "amount": 2, "symbol": "TKN/BTC"} 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 exchange.fetch_order("X", "TKN/BTC") == {"id": "123", "amount": 2, "symbol": "TKN/BTC"}
assert log_has( assert log_has(
("API fetch_open_order: {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}"), caplog ("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( api_mock.fetch_closed_order = MagicMock(
return_value={"id": "123", "amount": 2, "symbol": "TKN/BTC"} 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 exchange.fetch_order("X", "TKN/BTC") == {"id": "123", "amount": 2, "symbol": "TKN/BTC"}
assert log_has( assert log_has(
("API fetch_closed_order: {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}"), caplog ("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): with pytest.raises(InvalidOrderException):
api_mock.fetch_open_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) 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")) 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") exchange.fetch_order(order_id="_", pair="TKN/BTC")
assert api_mock.fetch_open_order.call_count == 1 assert api_mock.fetch_open_order.call_count == 1
api_mock.fetch_open_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found")) 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( ccxt_exceptionhandlers(
mocker, mocker,
@ -3758,7 +3758,7 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name):
mocker.patch(f"{EXMS}.exchange_has", return_value=True) mocker.patch(f"{EXMS}.exchange_has", return_value=True)
order = MagicMock() order = MagicMock()
order.myid = 123 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 exchange._dry_run_open_orders["X"] = order
assert exchange.fetch_stoploss_order("X", "TKN/BTC").myid == 123 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 default_conf["dry_run"] = False
api_mock = MagicMock() api_mock = MagicMock()
api_mock.fetch_order = MagicMock(return_value={"id": "123", "symbol": "TKN/BTC"}) 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"} res = {"id": "123", "symbol": "TKN/BTC"}
if exchange_name == "okx": if exchange_name == "okx":
res = {"id": "123", "symbol": "TKN/BTC", "type": "stoploss"} res = {"id": "123", "symbol": "TKN/BTC", "type": "stoploss"}
@ -3779,7 +3779,7 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name):
return return
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) 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") exchange.fetch_stoploss_order(order_id="_", pair="TKN/BTC")
assert api_mock.fetch_order.call_count == 1 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): 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_order_mock = MagicMock()
fetch_stoploss_order_mock = MagicMock() fetch_stoploss_order_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
@ -3824,7 +3824,7 @@ def test_fetch_order_or_stoploss_order(default_conf, mocker):
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_name(default_conf, mocker, exchange_name): 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.name == exchange_name.title()
assert exchange.id == exchange_name 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) orders = exchange.get_trades_for_order(order_id, "ETH/USDT:USDT", since)
assert len(orders) == 1 assert len(orders) == 1
@ -3914,7 +3914,7 @@ def test_get_fee(default_conf, mocker, exchange_name):
api_mock.calculate_fee = MagicMock( api_mock.calculate_fee = MagicMock(
return_value={"type": "taker", "currency": "BTC", "rate": 0.025, "cost": 0.05} 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) exchange._config.pop("fee", None)
assert exchange.get_fee("ETH/BTC") == 0.025 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): 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 .*"): with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
exchange.create_stoploss( exchange.create_stoploss(
pair="ETH/BTC", amount=1, stop_price=220, order_types={}, side="sell", leverage=1.0 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): 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} order_types = {"stoploss_on_exchange_limit_ratio": ratio}
if isinstance(expected, type) and issubclass(expected, Exception): 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): def test_ohlcv_candle_limit(default_conf, mocker, exchange_name):
if exchange_name == "okx": if exchange_name == "okx":
pytest.skip("Tested separately for 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") timeframes = ("1m", "5m", "1h")
expected = exchange._ft_has["ohlcv_candle_limit"] expected = exchange._ft_has["ohlcv_candle_limit"]
for timeframe in timeframes: for timeframe in timeframes:
@ -4383,7 +4383,7 @@ def test_market_is_tradable(
) -> None: ) -> None:
default_conf["trading_mode"] = trademode default_conf["trading_mode"] = trademode
mocker.patch(f"{EXMS}.validate_trading_mode_and_margin_mode") 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 = { market = {
"symbol": market_symbol, "symbol": market_symbol,
"base": base, "base": base,
@ -4654,7 +4654,7 @@ def test_get_funding_fees(default_conf_usdt, mocker, exchange_name, caplog):
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
default_conf_usdt["trading_mode"] = "futures" default_conf_usdt["trading_mode"] = "futures"
default_conf_usdt["margin_mode"] = "isolated" 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) 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.get_funding_fees("BTC/USDT:USDT", 1, False, now) == 0.0
assert exchange._fetch_and_calculate_funding_fees.call_count == 1 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}) type(api_mock).has = PropertyMock(return_value={"fetchFundingHistory": True})
# mocker.patch(f'{EXMS}.get_funding_fees', lambda pair, since: y) # 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") date_time = datetime.strptime("2021-09-01T00:00:01.000Z", "%Y-%m-%dT%H:%M:%S.%fZ")
unix_time = int(date_time.timestamp()) unix_time = int(date_time.timestamp())
expected_fees = -0.001 # 0.14542341 + -0.14642341 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( def test_get_stake_amount_considering_leverage(
exchange, stake_amount, leverage, min_stake_with_lev, mocker, default_conf 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 ( assert (
exchange._get_stake_amount_considering_leverage(stake_amount, leverage) exchange._get_stake_amount_considering_leverage(stake_amount, leverage)
== min_stake_with_lev == 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 default_conf, mocker, exchange_name, trading_mode, margin_mode, exception_thrown
): ):
exchange = get_patched_exchange( 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: if exception_thrown:
with pytest.raises(OperationalException): 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): def test__ccxt_config(default_conf, mocker, exchange_name, trading_mode, ccxt_config):
default_conf["trading_mode"] = trading_mode default_conf["trading_mode"] = trading_mode
default_conf["margin_mode"] = "isolated" 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 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" default_conf["margin_mode"] = "isolated"
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).has = PropertyMock(return_value={"fetchLeverageTiers": False}) 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 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 default_conf, mocker, size, funding_rate, mark_price, funding_fee, kraken_fee, time_in_ratio
): ):
exchange = get_patched_exchange(mocker, default_conf) 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)) 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)) trade_date = timeframe_to_prev_date("1h", datetime.now(timezone.utc))
funding_rates = DataFrame( 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={"fetchOHLCV": True})
type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": 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"])) mocker.patch(f"{EXMS}.timeframes", PropertyMock(return_value=["1h", "4h", "8h"]))
funding_fees = ex._fetch_and_calculate_funding_fees( funding_fees = ex._fetch_and_calculate_funding_fees(
pair="ADA/USDT:USDT", amount=amount, is_short=True, open_date=d1, close_date=d2 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" # Return empty "refresh_latest"
mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value={}) 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."): with pytest.raises(ExchangeError, match="Could not find funding rates."):
ex._fetch_and_calculate_funding_fees( ex._fetch_and_calculate_funding_fees(
pair="ADA/USDT:USDT", amount=amount, is_short=False, open_date=d1, close_date=d2 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={"fetchOHLCV": True})
type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": True}) type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": True})
mocker.patch(f"{EXMS}.timeframes", PropertyMock(return_value=["4h", "8h"])) 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") 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") 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["trading_mode"] = trading_mode
default_conf["margin_mode"] = margin_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 ( assert (
exchange.get_liquidation_price( exchange.get_liquidation_price(
pair="DOGE/USDT", pair="DOGE/USDT",
@ -5553,7 +5553,7 @@ def test_liquidation_price_binance(
default_conf["trading_mode"] = trading_mode default_conf["trading_mode"] = trading_mode
default_conf["margin_mode"] = margin_mode default_conf["margin_mode"] = margin_mode
default_conf["liquidation_buffer"] = 0.0 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)) exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(mm_ratio, maintenance_amt))
assert ( assert (
pytest.approx( pytest.approx(
@ -5703,7 +5703,7 @@ def test_load_leverage_tiers(mocker, default_conf, exchange_name):
) )
# SPOT # 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() == {} assert exchange.load_leverage_tiers() == {}
default_conf["trading_mode"] = "futures" default_conf["trading_mode"] = "futures"
@ -5712,12 +5712,12 @@ def test_load_leverage_tiers(mocker, default_conf, exchange_name):
if exchange_name != "binance": if exchange_name != "binance":
# FUTURES has.fetchLeverageTiers == False # FUTURES has.fetchLeverageTiers == False
type(api_mock).has = PropertyMock(return_value={"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() == {} assert exchange.load_leverage_tiers() == {}
# FUTURES regular # FUTURES regular
type(api_mock).has = PropertyMock(return_value={"fetchLeverageTiers": True}) 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() == { assert exchange.load_leverage_tiers() == {
"ADA/USDT:USDT": [ "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): def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers):
# Test Spot # 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 assert exchange.get_max_leverage("BNB/USDT", 100.0) == 1.0
# Test Futures # Test Futures
default_conf["trading_mode"] = "futures" default_conf["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated" 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 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): def test__get_params(mocker, default_conf, exchange_name):
api_mock = MagicMock() api_mock = MagicMock()
mocker.patch(f"{EXMS}.exchange_has", return_value=True) 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} exchange._params = {"test": True}
params1 = {"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["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated" 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} exchange._params = {"test": True}
assert ( assert (
@ -6171,7 +6171,7 @@ def test_get_liquidation_price(
default_conf_usdt["exchange"]["name"] = exchange_name default_conf_usdt["exchange"]["name"] = exchange_name
default_conf_usdt["margin_mode"] = margin_mode default_conf_usdt["margin_mode"] = margin_mode
mocker.patch("freqtrade.exchange.gate.Gate.validate_ordertypes") 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.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01))
exchange.name = exchange_name exchange.name = exchange_name

View File

@ -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()

View File

@ -9,7 +9,7 @@ from tests.conftest import EXMS, get_patched_exchange
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_fetch_stoploss_order_gate(default_conf, mocker): 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() fetch_order_mock = MagicMock()
exchange.fetch_order = fetch_order_mock 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["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated" 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( exchange.fetch_order = MagicMock(
return_value={ return_value={
@ -41,7 +41,7 @@ def test_fetch_stoploss_order_gate(default_conf, mocker):
def test_cancel_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() cancel_order_mock = MagicMock()
exchange.cancel_order = cancel_order_mock 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")] "sl1,sl2,sl3,side", [(1501, 1499, 1501, "sell"), (1499, 1501, 1499, "buy")]
) )
def test_stoploss_adjust_gate(mocker, default_conf, sl1, sl2, sl3, side): 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 = { order = {
"price": 1500, "price": 1500,
"stopPrice": 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 exchange._trading_fees = tick
trades = exchange.get_trades_for_order("22255", "ETH/USDT:USDT", datetime.now(timezone.utc)) trades = exchange.get_trades_for_order("22255", "ETH/USDT:USDT", datetime.now(timezone.utc))
trade = trades[0] trade = trades[0]

View File

@ -128,7 +128,7 @@ def test_create_stoploss_order_dry_run_htx(default_conf, mocker):
def test_stoploss_adjust_htx(mocker, default_conf): 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 = { order = {
"type": "stop", "type": "stop",
"price": 1500, "price": 1500,

View File

@ -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}.amount_to_precision", lambda s, x, y: y)
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: 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( order = exchange.create_order(
pair="ETH/BTC", 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) api_mock.fetch_open_orders = MagicMock(return_value=kraken_open_orders)
default_conf["dry_run"] = False 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() balances = exchange.get_balances()
assert len(balances) == 6 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")] "sl1,sl2,sl3,side", [(1501, 1499, 1501, "sell"), (1499, 1501, 1499, "buy")]
) )
def test_stoploss_adjust_kraken(mocker, default_conf, sl1, sl2, sl3, side): 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 = { order = {
"type": "market", "type": "market",
"stopLossPrice": 1500, "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): 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 assert exchange._valid_trade_pagination_id("XRP/USDT", trade_id) == expected

View File

@ -134,7 +134,7 @@ def test_stoploss_order_dry_run_kucoin(default_conf, mocker):
def test_stoploss_adjust_kucoin(mocker, default_conf): 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 = { order = {
"type": "limit", "type": "limit",
"price": 1500, "price": 1500,
@ -161,7 +161,7 @@ def test_kucoin_create_order(default_conf, mocker, side, ordertype, rate):
default_conf["dry_run"] = False default_conf["dry_run"] = False
mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y) mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y)
mocker.patch(f"{EXMS}.price_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_leverage = MagicMock()
exchange.set_margin_mode = MagicMock() exchange.set_margin_mode = MagicMock()

View File

@ -12,7 +12,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
def test_okx_ohlcv_candle_limit(default_conf, mocker): 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") timeframes = ("1m", "5m", "1h")
start_time = int(datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp() * 1000) 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", 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", 2001) == (0.015, None)
assert exchange.get_maintenance_ratio_and_amt("ETH/USDT:USDT", 4001) == (0.02, 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): 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") assert exchange.get_max_pair_stake_amount("BNB/BUSD", 1.0) == float("inf")
default_conf["trading_mode"] = "futures" default_conf["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated" 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 exchange._leverage_tiers = leverage_tiers
assert exchange.get_max_pair_stake_amount("XRP/USDT:USDT", 1.0) == 30000000 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): 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" exchange.net_only = mode == "net"
assert exchange._get_posSide(side, reduceonly) == result 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 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 assert api_mock.fetch_accounts.call_count == 0
exchange.trading_mode = TradingMode.FUTURES exchange.trading_mode = TradingMode.FUTURES
# Default to netOnly # 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["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated" default_conf["margin_mode"] = "isolated"
default_conf["stake_currency"] = "USDT" 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.trading_mode = TradingMode.FUTURES
exchange.margin_mode = MarginMode.ISOLATED exchange.margin_mode = MarginMode.ISOLATED
exchange.markets = markets exchange.markets = markets
@ -520,7 +520,7 @@ def test__set_leverage_okx(mocker, default_conf):
default_conf["trading_mode"] = TradingMode.FUTURES default_conf["trading_mode"] = TradingMode.FUTURES
default_conf["margin_mode"] = MarginMode.ISOLATED 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") exchange._lev_prep("BTC/USDT:USDT", 3.2, "buy")
assert api_mock.set_leverage.call_count == 1 assert api_mock.set_leverage.call_count == 1
# Leverage is rounded to 3. # Leverage is rounded to 3.
@ -554,7 +554,7 @@ def test_fetch_stoploss_order_okx(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.fetch_order = 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") exchange.fetch_stoploss_order("1234", "ETH/BTC")
assert api_mock.fetch_order.call_count == 1 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" assert resp["type"] == "stoploss"
default_conf["dry_run"] = True 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"})) dro_mock = mocker.patch(f"{EXMS}.fetch_dry_run_order", MagicMock(return_value={"id": "123455"}))
api_mock.fetch_order.reset_mock() 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")] "sl1,sl2,sl3,side", [(1501, 1499, 1501, "sell"), (1499, 1501, 1499, "buy")]
) )
def test_stoploss_adjust_okx(mocker, default_conf, sl1, sl2, sl3, side): 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 = { order = {
"type": "stoploss", "type": "stoploss",
"price": 1500, "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): 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() 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): def test__get_stop_params_okx(mocker, default_conf):
default_conf["trading_mode"] = "futures" default_conf["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated" 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") params = exchange._get_stop_params("ETH/USDT:USDT", 1500, "sell")
assert params["tdMode"] == "isolated" 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) mocker.patch(f"{EXMS}.exchange_has", return_value=True)
start_time = datetime.now(timezone.utc) - timedelta(days=20) 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 # Not available in dry-run
assert exchange.fetch_orders("mocked", start_time) == [] assert exchange.fetch_orders("mocked", start_time) == []
assert api_mock.fetch_orders.call_count == 0 assert api_mock.fetch_orders.call_count == 0
default_conf["dry_run"] = False 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): def has_resp(_, endpoint):
if endpoint == "fetchOrders": if endpoint == "fetchOrders":

View File

@ -11,6 +11,8 @@ from tests.conftest import EXMS, get_default_conf_usdt
EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str] EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str]
EXCHANGE_WS_FIXTURE_TYPE = Tuple[Exchange, str, str]
# Exchanges that should be tested online # Exchanges that should be tested online
EXCHANGES = { EXCHANGES = {
@ -360,6 +362,7 @@ def set_test_proxy(config: Config, use_proxy: bool) -> Config:
config1 = deepcopy(config) config1 = deepcopy(config)
config1["exchange"]["ccxt_config"] = { config1["exchange"]["ccxt_config"] = {
"httpsProxy": proxy, "httpsProxy": proxy,
"wsProxy": proxy,
} }
return config1 return config1
@ -376,7 +379,7 @@ def get_exchange(exchange_name, exchange_conf):
exchange_conf, validate=True, load_leverage_tiers=True 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): 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}.load_cached_leverage_tiers", return_value=None)
class_mocker.patch(f"{EXMS}.cache_leverage_tiers") 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") @pytest.fixture(params=EXCHANGES, scope="class")
def exchange(request, exchange_conf, class_mocker): def exchange(request, exchange_conf, class_mocker):
class_mocker.patch("freqtrade.exchange.bybit.Bybit.additional_exchange_init") 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") @pytest.fixture(params=EXCHANGES, scope="class")
def exchange_futures(request, exchange_conf, class_mocker): 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()

View File

@ -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)

View File

@ -915,7 +915,7 @@ def test_execute_entry(
default_conf_usdt["margin_mode"] = margin_mode default_conf_usdt["margin_mode"] = margin_mode
mocker.patch("freqtrade.exchange.gate.Gate.validate_ordertypes") mocker.patch("freqtrade.exchange.gate.Gate.validate_ordertypes")
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker, id=exchange_name) patch_exchange(mocker, exchange=exchange_name)
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False)
freqtrade.strategy.leverage = MagicMock(return_value=leverage) 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): 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=[]) 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"] amount = buy_order_fee["amount"]
trade = Trade( trade = Trade(
pair="LTC/ETH", pair="LTC/ETH",

View File

@ -4,7 +4,7 @@ from freqtrade.enums import CandleType
@pytest.mark.parametrize( @pytest.mark.parametrize(
"input,expected", "candle_type,expected",
[ [
("", CandleType.SPOT), ("", CandleType.SPOT),
("spot", CandleType.SPOT), ("spot", CandleType.SPOT),
@ -17,17 +17,17 @@ from freqtrade.enums import CandleType
("premiumIndex", CandleType.PREMIUMINDEX), ("premiumIndex", CandleType.PREMIUMINDEX),
], ],
) )
def test_CandleType_from_string(input, expected): def test_CandleType_from_string(candle_type, expected):
assert CandleType.from_string(input) == expected assert CandleType.from_string(candle_type) == expected
@pytest.mark.parametrize( @pytest.mark.parametrize(
"input,expected", "candle_type,expected",
[ [
("futures", CandleType.FUTURES), ("futures", CandleType.FUTURES),
("spot", CandleType.SPOT), ("spot", CandleType.SPOT),
("margin", CandleType.SPOT), ("margin", CandleType.SPOT),
], ],
) )
def test_CandleType_get_default(input, expected): def test_CandleType_get_default(candle_type, expected):
assert CandleType.get_default(input) == expected assert CandleType.get_default(candle_type) == expected

View File

@ -429,7 +429,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
backtesting.start() 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(f"{EXMS}.exchange_has", MagicMock(return_value=True))
mocker.patch( mocker.patch(
"freqtrade.data.history.history_utils.load_pair_history", "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."): with pytest.raises(OperationalException, match="No pair in whitelist."):
Backtesting(default_conf) 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( default_conf.update(
{ {
"pairlists": [{"method": "StaticPairList"}], "pairlists": [{"method": "StaticPairList"}],
@ -469,7 +462,7 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) ->
Backtesting(default_conf) 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}.exchange_has", MagicMock(return_value=True))
mocker.patch(f"{EXMS}.get_tickers", tickers) mocker.patch(f"{EXMS}.get_tickers", tickers)
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y: y) 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) 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"] = [ default_conf["pairlists"] = [
{"method": "StaticPairList"}, {"method": "StaticPairList"},
{"method": "PrecisionFilter"}, {"method": "PrecisionFilter"},

View File

@ -38,6 +38,7 @@ TESTABLE_PAIRLISTS = [p for p in AVAILABLE_PAIRLISTS if p not in ["RemotePairLis
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def whitelist_conf(default_conf): def whitelist_conf(default_conf):
default_conf["runmode"] = "dry_run"
default_conf["stake_currency"] = "BTC" default_conf["stake_currency"] = "BTC"
default_conf["exchange"]["pair_whitelist"] = [ default_conf["exchange"]["pair_whitelist"] = [
"ETH/BTC", "ETH/BTC",
@ -68,6 +69,7 @@ def whitelist_conf(default_conf):
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def whitelist_conf_2(default_conf): def whitelist_conf_2(default_conf):
default_conf["runmode"] = "dry_run"
default_conf["stake_currency"] = "BTC" default_conf["stake_currency"] = "BTC"
default_conf["exchange"]["pair_whitelist"] = [ default_conf["exchange"]["pair_whitelist"] = [
"ETH/BTC", "ETH/BTC",
@ -94,6 +96,7 @@ def whitelist_conf_2(default_conf):
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def whitelist_conf_agefilter(default_conf): def whitelist_conf_agefilter(default_conf):
default_conf["runmode"] = "dry_run"
default_conf["stake_currency"] = "BTC" default_conf["stake_currency"] = "BTC"
default_conf["exchange"]["pair_whitelist"] = [ default_conf["exchange"]["pair_whitelist"] = [
"ETH/BTC", "ETH/BTC",
@ -773,7 +776,7 @@ def test_VolumePairList_whitelist_gen(
whitelist_result, whitelist_result,
caplog, caplog,
) -> None: ) -> None:
whitelist_conf["runmode"] = "backtest" whitelist_conf["runmode"] = "util_exchange"
whitelist_conf["pairlists"] = pairlists whitelist_conf["pairlists"] = pairlists
whitelist_conf["stake_currency"] = base_currency 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." OperationalException, match="This filter only support marketcap rank up to 250."
): ):
PairListManager(exchange, default_conf_usdt) 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)

View File

@ -2154,6 +2154,7 @@ def test_api_exchanges(botclient):
"valid": True, "valid": True,
"supported": True, "supported": True,
"comment": "", "comment": "",
"dex": False,
"trade_modes": [ "trade_modes": [
{"trading_mode": "spot", "margin_mode": ""}, {"trading_mode": "spot", "margin_mode": ""},
{"trading_mode": "futures", "margin_mode": "isolated"}, {"trading_mode": "futures", "margin_mode": "isolated"},
@ -2165,9 +2166,19 @@ def test_api_exchanges(botclient):
"name": "mexc", "name": "mexc",
"valid": True, "valid": True,
"supported": False, "supported": False,
"dex": False,
"comment": "", "comment": "",
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}], "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): def test_api_freqaimodels(botclient, tmp_path, mocker):