mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge branch 'freqtrade:develop' into feature/stoploss-start-at
This commit is contained in:
commit
f126120421
|
@ -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.
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.32-cp310-cp310-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.32-cp310-cp310-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.32-cp311-cp311-linux_armv7l.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.32-cp311-cp311-linux_armv7l.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.32-cp311-cp311-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.32-cp311-cp311-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.32-cp312-cp312-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.32-cp312-cp312-win_amd64.whl
Normal file
Binary file not shown.
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.32-cp39-cp39-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.32-cp39-cp39-win_amd64.whl
Normal file
Binary file not shown.
|
@ -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",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"]
|
||||||
),
|
),
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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": ""},
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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]] = [
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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()):
|
||||||
|
|
186
freqtrade/exchange/exchange_ws.py
Normal file
186
freqtrade/exchange/exchange_ws.py
Normal 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
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,8 +291,7 @@ 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)
|
||||||
self.last_process = datetime.now(timezone.utc)
|
self.last_process = datetime.now(timezone.utc)
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -134,16 +134,19 @@ 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
|
||||||
"YTT", # flake8-2020
|
"YTT", # flake8-2020
|
||||||
"S", # flake8-bandit
|
"S", # flake8-bandit
|
||||||
# "DTZ", # flake8-datetimez
|
# "DTZ", # flake8-datetimez
|
||||||
# "RSE", # flake8-raise
|
# "RSE", # flake8-raise
|
||||||
# "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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
69
tests/exchange/test_exchange_ws.py
Normal file
69
tests/exchange/test_exchange_ws.py
Normal 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()
|
|
@ -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]
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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()
|
||||||
|
|
64
tests/exchange_online/test_ccxt_ws_compat.py
Normal file
64
tests/exchange_online/test_ccxt_ws_compat.py
Normal 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)
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"},
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user