Merge branch 'develop' into feature/fetch-public-trades

This commit is contained in:
Matthias 2024-06-04 19:49:27 +02:00
commit 50bf770351
96 changed files with 2656 additions and 2536 deletions

View File

@ -19,7 +19,7 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.12"
- name: Install ccxt
run: pip install ccxt

View File

@ -318,6 +318,17 @@ jobs:
run: |
mypy freqtrade scripts tests
- name: Run Pester tests (PowerShell)
run: |
$PSVersionTable
Set-PSRepository psgallery -InstallationPolicy trusted
Install-Module -Name Pester -RequiredVersion 5.3.1 -Confirm:$false -Force
$Error.clear()
Invoke-Pester -Path "tests" -CI
if ($Error.Length -gt 0) {exit 1}
shell: powershell
- name: Discord notification
uses: rjstone/discord-webhook-notify@v1
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
@ -334,7 +345,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
- name: pre-commit dependencies
run: |
@ -348,7 +359,7 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
- uses: pre-commit/action@v3.0.1
docs-check:
@ -363,7 +374,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.12"
- name: Documentation build
run: |
@ -389,7 +400,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.12"
- name: Cache_dependencies
uses: actions/cache@v4
@ -471,7 +482,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.12"
- name: Build distribution
run: |
@ -542,7 +553,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.12"
- name: Extract branch name
id: extract-branch
@ -565,12 +576,12 @@ jobs:
sudo systemctl restart docker
docker version -f '{{.Server.Experimental}}'
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
id: buildx
uses: crazy-max/ghaction-docker-buildx@v3.3.1
with:
buildx-version: latest
qemu-version: latest
uses: docker/setup-buildx-action@v3
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}

View File

@ -17,7 +17,7 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.12"
- name: Install pre-commit
@ -26,9 +26,6 @@ jobs:
- name: Run auto-update
run: pre-commit autoupdate
- name: Run pre-commit
run: pre-commit run --all-files
- uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.REPO_SCOPED_TOKEN }}

View File

@ -16,7 +16,7 @@ repos:
additional_dependencies:
- types-cachetools==5.3.0.7
- types-filelock==3.2.7
- types-requests==2.31.0.20240406
- types-requests==2.32.0.20240602
- types-tabulate==0.9.0.20240106
- types-python-dateutil==2.9.0.20240316
- SQLAlchemy==2.0.30
@ -31,7 +31,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.4.4'
rev: 'v0.4.7'
hooks:
- id: ruff
@ -56,7 +56,7 @@ repos:
)$
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
rev: v2.3.0
hooks:
- id: codespell
additional_dependencies:

View File

@ -29,6 +29,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
- [X] [Binance](https://www.binance.com/)
- [X] [Bitmart](https://bitmart.com/)
- [X] [BingX](https://bingx.com/invite/0EM9RX)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [HTX](https://www.htx.com/) (Former Huobi)
- [X] [Kraken](https://kraken.com/)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -35,7 +35,7 @@ COPY build_helpers/* /tmp/
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
USER ftuser
RUN pip install --user --no-cache-dir numpy \
&& pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib==0.4.28 \
&& pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib \
&& pip install --user --no-cache-dir -r requirements.txt
# Copy dependencies to runtime-image

View File

@ -568,7 +568,14 @@ The possible values are: `GTC` (default), `FOK` or `IOC`.
This is ongoing work. For now, it is supported only for binance, gate and kucoin.
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
### What values can be used for fiat_display_currency?
### Fiat conversion
Freqtrade uses the Coingecko API to convert the coin value to it's corresponding fiat value for the Telegram reports.
The FIAT currency can be set in the configuration file as `fiat_display_currency`.
Removing `fiat_display_currency` completely from the configuration will skip initializing coingecko, and will not show any FIAT currency conversion. This has no importance for the correct functioning of the bot.
#### What values can be used for fiat_display_currency?
The `fiat_display_currency` configuration parameter sets the base currency to use for the
conversion from coin to fiat in the bot Telegram reports.
@ -587,7 +594,25 @@ The valid values are:
"BTC", "ETH", "XRP", "LTC", "BCH", "BNB"
```
Removing `fiat_display_currency` completely from the configuration will skip initializing coingecko, and will not show any FIAT currency conversion. This has no importance for the correct functioning of the bot.
#### Coingecko Rate limit problems
On some IP ranges, coingecko is heavily rate-limiting.
In such cases, you may want to add your coingecko API key to the configuration.
``` json
{
"fiat_display_currency": "USD",
"coingecko": {
"api_key": "your-api",
"is_demo": true
}
}
```
Freqtrade supports both Demo and Pro coingecko API keys.
The Coingecko API key is NOT required for the bot to function correctly.
It is only used for the conversion of coin to fiat in the Telegram reports, which usually also work without API key.
## Using Dry-run mode

View File

@ -24,10 +24,10 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--days INT] [--new-pairs-days INT]
[--include-inactive-pairs]
[--timerange TIMERANGE] [--dl-trades]
[--exchange EXCHANGE]
[--convert] [--exchange EXCHANGE]
[-t TIMEFRAMES [TIMEFRAMES ...]] [--erase]
[--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
[--data-format-trades {json,jsongz,hdf5,feather}]
[--data-format-trades {json,jsongz,hdf5,feather,parquet}]
[--trading-mode {spot,margin,futures}]
[--prepend]
@ -48,6 +48,11 @@ options:
--dl-trades Download trades instead of OHLCV data. The bot will
resample trades to the desired timeframe as specified
as --timeframes/-t.
--convert Convert downloaded trades to OHLCV data. Only
applicable in combination with `--dl-trades`. Will be
automatic for exchanges which don't have historic
OHLCV (e.g. Kraken). If not provided, use `trades-to-
ohlcv` to convert trades data to OHLCV data.
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
-t TIMEFRAMES [TIMEFRAMES ...], --timeframes TIMEFRAMES [TIMEFRAMES ...]
Specify which tickers to download. Space-separated
@ -57,7 +62,7 @@ options:
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
Storage format for downloaded candle (OHLCV) data.
(default: `feather`).
--data-format-trades {json,jsongz,hdf5,feather}
--data-format-trades {json,jsongz,hdf5,feather,parquet}
Storage format for downloaded trades data. (default:
`feather`).
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
@ -471,15 +476,20 @@ ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
## Trades (tick) data
By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API.
By default, `download-data` sub-command downloads Candles (OHLCV) data. Most exchanges also provide historic trade-data via their API.
This data can be useful if you need many different timeframes, since it is only downloaded once, and then resampled locally to the desired timeframes.
Since this data is large by default, the files use the feather fileformat by default. They are stored in your data-directory with the naming convention of `<pair>-trades.feather` (`ETH_BTC-trades.feather`). Incremental mode is also supported, as for historic OHLCV data, so downloading the data once per week with `--days 8` will create an incremental data-repository.
Since this data is large by default, the files use the feather file format by default. They are stored in your data-directory with the naming convention of `<pair>-trades.feather` (`ETH_BTC-trades.feather`). Incremental mode is also supported, as for historic OHLCV data, so downloading the data once per week with `--days 8` will create an incremental data-repository.
To use this mode, simply add `--dl-trades` to your call. This will swap the download method to download trades, and resamples the data locally.
To use this mode, simply add `--dl-trades` to your call. This will swap the download method to download trades.
If `--convert` is also provided, the resample step will happen automatically and overwrite eventually existing OHLCV data for the given pair/timeframe combinations.
!!! Warning "do not use"
You should not use this unless you're a kraken user. Most other exchanges provide OHLCV data with sufficient history.
!!! Warning "Do not use"
You should not use this unless you're a kraken user (Kraken does not provide historic OHLCV data).
Most other exchanges provide OHLCV data with sufficient history, so downloading multiple timeframes through that method will still proof to be a lot faster than downloading trades data.
!!! Note "Kraken user"
Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data.
Example call:
@ -490,12 +500,6 @@ freqtrade download-data --exchange kraken --pairs XRP/EUR ETH/EUR --days 20 --dl
!!! Note
While this method uses async calls, it will be slow, since it requires the result of the previous call to generate the next request to the exchange.
!!! Warning
The historic trades are not available during Freqtrade dry-run and live trade modes because all exchanges tested provide this data with a delay of few 100 candles, so it's not suitable for real-time trading.
!!! Note "Kraken user"
Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data.
## Next step
Great, you now have backtest data downloaded, so you can now start [backtesting](backtesting.md) your strategy.
Great, you now have some data downloaded, so you can now start [backtesting](backtesting.md) your strategy.

View File

@ -127,6 +127,13 @@ These settings will be checked on startup, and freqtrade will show an error if t
Freqtrade will not attempt to change these settings.
## Bingx
BingX supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "IOC" (immediate-or-cancel) and "PO" (Post only) settings.
!!! Tip "Stoploss on Exchange"
Bingx supports `stoploss_on_exchange` and can use both stop-limit and stop-market orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
## Kraken
Kraken supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "IOC" (immediate-or-cancel) and "PO" (Post only) settings.

View File

@ -224,7 +224,7 @@ where $W_i$ is the weight of data point $i$ in a total set of $n$ data points. B
## Building the data pipeline
By default, FreqAI builds a dynamic pipeline based on user congfiguration settings. The default settings are robust and designed to work with a variety of methods. These two steps are a `MinMaxScaler(-1,1)` and a `VarianceThreshold` which removes any column that has 0 variance. Users can activate other steps with more configuration parameters. For example if users add `use_SVM_to_remove_outliers: true` to the `freqai` config, then FreqAI will automatically add the [`SVMOutlierExtractor`](#identifying-outliers-using-a-support-vector-machine-svm) to the pipeline. Likewise, users can add `principal_component_analysis: true` to the `freqai` config to activate PCA. The [DissimilarityIndex](#identifying-outliers-with-the-dissimilarity-index-di) is activated with `DI_threshold: 1`. Finally, noise can also be added to the data with `noise_standard_deviation: 0.1`. Finally, users can add [DBSCAN](#identifying-outliers-with-dbscan) outlier removal with `use_DBSCAN_to_remove_outliers: true`.
By default, FreqAI builds a dynamic pipeline based on user configuration settings. The default settings are robust and designed to work with a variety of methods. These two steps are a `MinMaxScaler(-1,1)` and a `VarianceThreshold` which removes any column that has 0 variance. Users can activate other steps with more configuration parameters. For example if users add `use_SVM_to_remove_outliers: true` to the `freqai` config, then FreqAI will automatically add the [`SVMOutlierExtractor`](#identifying-outliers-using-a-support-vector-machine-svm) to the pipeline. Likewise, users can add `principal_component_analysis: true` to the `freqai` config to activate PCA. The [DissimilarityIndex](#identifying-outliers-with-the-dissimilarity-index-di) is activated with `DI_threshold: 1`. Finally, noise can also be added to the data with `noise_standard_deviation: 0.1`. Finally, users can add [DBSCAN](#identifying-outliers-with-dbscan) outlier removal with `use_DBSCAN_to_remove_outliers: true`.
!!! note "More information available"
Please review the [parameter table](freqai-parameter-table.md) for more information on these parameters.

View File

@ -41,6 +41,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
- [X] [Binance](https://www.binance.com/)
- [X] [Bitmart](https://bitmart.com/)
- [X] [BingX](https://bingx.com/invite/0EM9RX)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [HTX](https://www.htx.com/) (Former Huobi)
- [X] [Kraken](https://kraken.com/)

View File

@ -286,7 +286,7 @@ cd freqtrade
#### Freqtrade install: Conda Environment
```bash
conda create --name freqtrade python=3.11
conda create --name freqtrade python=3.12
```
!!! Note "Creating Conda Environment"

View File

@ -1,6 +1,6 @@
markdown==3.6
mkdocs==1.6.0
mkdocs-material==9.5.22
mkdocs-material==9.5.25
mdx_truly_sane_lists==1.3
pymdown-extensions==10.8.1
jinja2==3.1.4

View File

@ -161,7 +161,7 @@ freqtrade-client --config rest_config.json <command> [optional parameters]
| `delete_lock <lock_id>` | Deletes (disables) the lock by id.
| `locks add <pair>, <until>, [side], [reason]` | Locks a pair until "until". (Until will be rounded up to the nearest timeframe).
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
| `forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `forceexit <trade_id> [order_type] [amount]` | Instantly exits the given trade (ignoring `minimum_roi`), using the given order type ("market" or "limit", uses your config setting if not specified), and the chosen amount (full sell if not specified).
| `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).
| `forceenter <pair> [rate]` | Instantly enters the given pair. Rate is optional. (`force_entry_enable` must be set to True)
| `forceenter <pair> <side> [rate]` | Instantly longs or shorts the given pair. Rate is optional. (`force_entry_enable` must be set to True)

View File

@ -30,6 +30,7 @@ The Order-type will be ignored if only one mode is available.
|----------|-------------|
| Binance | limit |
| Binance Futures | market, limit |
| Bingx | market, limit |
| HTX (former Huobi) | limit |
| kraken | market, limit |
| Gate | limit |

View File

@ -5,6 +5,30 @@ We **strongly** recommend that Windows users use [Docker](docker_quickstart.md)
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
Otherwise, please follow the instructions below.
All instructions assume that python 3.9+ is installed and available.
## Clone the git repository
First of all clone the repository by running:
``` powershell
git clone https://github.com/freqtrade/freqtrade.git
```
Now, choose your installation method, either automatically via script (recommended) or manually following the corresponding instructions.
## Install freqtrade automatically
### Run the installation script
The script will ask you a few questions to determine which parts should be installed.
```powershell
Set-ExecutionPolicy -ExecutionPolicy Bypass
cd freqtrade
. .\setup.ps1
```
## Install freqtrade manually
!!! Note "64bit Python version"
@ -14,17 +38,11 @@ Otherwise, please follow the instructions below.
!!! Hint
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information.
### 1. Clone the git repository
```bash
git clone https://github.com/freqtrade/freqtrade.git
```
### 2. Install ta-lib
### Install ta-lib
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows).
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.9, 3.10 and 3.11) and for 64bit Windows.
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.9, 3.10, 3.11 and 3.12) and for 64bit Windows.
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
Other versions must be downloaded from the above link.

View File

@ -1,6 +1,6 @@
"""Freqtrade bot"""
__version__ = "2024.5-dev"
__version__ = "2024.6-dev"
if "dev" in __version__:
from pathlib import Path

View File

@ -142,6 +142,7 @@ ARGS_DOWNLOAD_DATA = [
"include_inactive",
"timerange",
"download_trades",
"convert_trades",
"exchange",
"timeframes",
"erase",

View File

@ -100,7 +100,10 @@ def ask_user_config() -> Dict[str, Any]:
{
"type": "text",
"name": "fiat_display_currency",
"message": "Please insert your display Currency (for reporting):",
"message": (
"Please insert your display Currency for reporting "
"(leave empty to disable FIAT conversion):"
),
"default": "USD",
},
{
@ -110,6 +113,7 @@ def ask_user_config() -> Dict[str, Any]:
"choices": [
"binance",
"binanceus",
"bingx",
"gate",
"htx",
"kraken",
@ -125,7 +129,7 @@ def ask_user_config() -> Dict[str, Any]:
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
"default": False,
"filter": lambda val: "futures" if val else "spot",
"when": lambda x: x["exchange_name"] in ["binance", "gate", "okx"],
"when": lambda x: x["exchange_name"] in ["binance", "gate", "okx", "bybit"],
},
{
"type": "autocomplete",

View File

@ -450,6 +450,14 @@ AVAILABLE_CLI_OPTIONS = {
"desired timeframe as specified as --timeframes/-t.",
action="store_true",
),
"convert_trades": Arg(
"--convert",
help="Convert downloaded trades to OHLCV data. Only applicable in combination with "
"`--dl-trades`. "
"Will be automatic for exchanges which don't have historic OHLCV (e.g. Kraken). "
"If not provided, use `trades-to-ohlcv` to convert trades data to OHLCV data.",
action="store_true",
),
"format_from_trades": Arg(
"--format-from",
help="Source format for data conversion.",

View File

@ -370,6 +370,7 @@ class Configuration:
("days", "Detected --days: {}"),
("include_inactive", "Detected --include-inactive-pairs: {}"),
("download_trades", "Detected --dl-trades: {}"),
("convert_trades", "Detected --convert: {} - Converting Trade data to OHCV {}"),
("dataformat_ohlcv", 'Using "{}" to store OHLCV data.'),
("dataformat_trades", 'Using "{}" to store trades data.'),
("show_timerange", "Detected --show-timerange"),

View File

@ -157,6 +157,7 @@ SUPPORTED_FIAT = [
"LTC",
"BCH",
"BNB",
"", # Allow empty field in config.
]
MINIMAL_CONFIG = {
@ -323,6 +324,14 @@ CONF_SCHEMA = {
},
"required": REQUIRED_ORDERTIF,
},
"coingecko": {
"type": "object",
"properties": {
"is_demo": {"type": "boolean", "default": True},
"api_key": {"type": "string"},
},
"required": ["is_demo", "api_key"],
},
"exchange": {"$ref": "#/definitions/exchange"},
"edge": {"$ref": "#/definitions/edge"},
"freqai": {"$ref": "#/definitions/freqai"},

View File

@ -629,17 +629,20 @@ def download_data_main(config: Config) -> None:
trading_mode=config.get("trading_mode", TradingMode.SPOT),
)
# Convert downloaded trade data to different timeframes
convert_trades_to_ohlcv(
pairs=expanded_pairs,
timeframes=config["timeframes"],
datadir=config["datadir"],
timerange=timerange,
erase=bool(config.get("erase")),
data_format_ohlcv=config["dataformat_ohlcv"],
data_format_trades=config["dataformat_trades"],
candle_type=config.get("candle_type_def", CandleType.SPOT),
)
if config.get("convert_trades") or not exchange.get_option("ohlcv_has_history", True):
# Convert downloaded trade data to different timeframes
# Only auto-convert for exchanges without historic klines
convert_trades_to_ohlcv(
pairs=expanded_pairs,
timeframes=config["timeframes"],
datadir=config["datadir"],
timerange=timerange,
erase=bool(config.get("erase")),
data_format_ohlcv=config["dataformat_ohlcv"],
data_format_trades=config["dataformat_trades"],
candle_type=config.get("candle_type_def", CandleType.SPOT),
)
else:
if not exchange.get_option("ohlcv_has_history", True):
raise OperationalException(

View File

@ -1,5 +1,6 @@
import logging
import math
from dataclasses import dataclass
from datetime import datetime
from typing import Dict, Tuple
@ -160,6 +161,16 @@ def calculate_underwater(
return max_drawdown_df
@dataclass()
class DrawDownResult:
drawdown_abs: float = 0.0
high_date: pd.Timestamp = None
low_date: pd.Timestamp = None
high_value: float = 0.0
low_value: float = 0.0
relative_account_drawdown: float = 0.0
def calculate_max_drawdown(
trades: pd.DataFrame,
*,
@ -167,14 +178,14 @@ def calculate_max_drawdown(
value_col: str = "profit_abs",
starting_balance: float = 0,
relative: bool = False,
) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float, float]:
) -> DrawDownResult:
"""
Calculate max drawdown and the corresponding close dates
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
:param date_col: Column in DataFrame to use for dates (defaults to 'close_date')
:param value_col: Column in DataFrame to use for values (defaults to 'profit_abs')
:param starting_balance: Portfolio starting balance - properly calculate relative drawdown.
:return: Tuple (float, highdate, lowdate, highvalue, lowvalue, relative_drawdown)
:return: DrawDownResult object
with absolute max drawdown, high and low time and high and low value,
and the relative account drawdown
:raise: ValueError if trade-dataframe was found empty.
@ -201,13 +212,13 @@ def calculate_max_drawdown(
low_val = max_drawdown_df.loc[idxmin, "cumulative"]
max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"]
return (
abs(max_drawdown_df.loc[idxmin, "drawdown"]),
high_date,
low_date,
high_val,
low_val,
max_drawdown_rel,
return DrawDownResult(
drawdown_abs=abs(max_drawdown_df.loc[idxmin, "drawdown"]),
high_date=high_date,
low_date=low_date,
high_value=high_val,
low_value=low_val,
relative_account_drawdown=max_drawdown_rel,
)
@ -350,9 +361,10 @@ def calculate_calmar(
# calculate max drawdown
try:
_, _, _, _, _, max_drawdown = calculate_max_drawdown(
drawdown = calculate_max_drawdown(
trades, value_col="profit_abs", starting_balance=starting_balance
)
max_drawdown = drawdown.relative_account_drawdown
except ValueError:
max_drawdown = 0

View File

@ -1,7 +1,7 @@
from enum import Enum
class RunMode(Enum):
class RunMode(str, Enum):
"""
Bot running mode (backtest, hyperopt, ...)
can be "live", "dry-run", "backtest", "edge", "hyperopt".

View File

@ -201,7 +201,6 @@ class Binance(Exchange):
"Freqtrade only supports isolated futures for leverage trading"
)
@retrier
def load_leverage_tiers(self) -> Dict[str, List[Dict]]:
if self.trading_mode == TradingMode.FUTURES:
if self._config["dry_run"]:
@ -209,16 +208,6 @@ class Binance(Exchange):
with leverage_tiers_path.open() as json_file:
return json_load(json_file)
else:
try:
return self._api.fetch_leverage_tiers()
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError(
f"Could not fetch leverage amounts due to"
f"{e.__class__.__name__}. Message: {e}"
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
return self.get_leverage_tiers()
else:
return {}

File diff suppressed because it is too large Load Diff

View File

@ -17,4 +17,7 @@ class Bingx(Exchange):
_ft_has: Dict = {
"ohlcv_candle_limit": 1000,
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "limit", "market": "market"},
"order_time_in_force": ["GTC", "IOC", "PO"],
}

View File

@ -250,38 +250,19 @@ class Bybit(Exchange):
@retrier
def get_leverage_tiers(self) -> Dict[str, List[Dict]]:
"""
Temporary workaround for https://github.com/freqtrade/freqtrade/issues/10196
should be removed or updated once https://github.com/ccxt/ccxt/issues/22448 is fixed.
Cache leverage tiers for 1 day, since they are not expected to change often, and
bybit requires pagination to fetch all tiers.
"""
# Load cached tiers
tiers_cached = self.load_cached_leverage_tiers(self._config["stake_currency"])
tiers_cached = self.load_cached_leverage_tiers(
self._config["stake_currency"], timedelta(days=1)
)
if tiers_cached:
tiers = tiers_cached
return tiers
return tiers_cached
# Fetch tiers from exchange
symbols = self._api.market_symbols([])
def parse_resp(response):
result = self._api.safe_dict(response, "result", {})
data = self._api.safe_list(result, "list", [])
return self._api.parse_leverage_tiers(data, symbols, "symbol")
params = {
"category": "linear",
}
tiers = {}
# 20 pairs ... should be sufficient assuming 30 pairs per page
# Aimed to avoid a potential infinite loop
for _ in range(20):
# Fetch from private endpoint
response = self._api.publicGetV5MarketRiskLimit(params)
tiers = tiers | parse_resp(response)
if (cursor := response["result"]["nextPageCursor"]) == "":
break
params.update({"cursor": cursor})
tiers = super().get_leverage_tiers()
self.cache_leverage_tiers(tiers, self._config["stake_currency"])
return tiers

View File

@ -53,6 +53,7 @@ MAP_EXCHANGE_CHILDCLASS = {
SUPPORTED_EXCHANGES = [
"binance",
"bingx",
"bitmart",
"gate",
"htx",

View File

@ -159,7 +159,7 @@ class Exchange:
:return: None
"""
self._api: ccxt.Exchange
self._api_async: ccxt_async.Exchange = None
self._api_async: ccxt_async.Exchange
self._markets: Dict = {}
self._trading_fees: Dict[str, Any] = {}
self._leverage_tiers: Dict[str, List[Dict]] = {}
@ -246,7 +246,7 @@ class Exchange:
self.required_candle_call_count = 1
if validate:
# Initial markets load
self._load_markets()
self.reload_markets(True, load_leverage_tiers=False)
self.validate_config(config)
self._startup_candle_count: int = config.get("startup_candle_count", 0)
self.required_candle_call_count = self.validate_required_startup_candles(
@ -367,7 +367,7 @@ class Exchange:
"""exchange ccxt markets"""
if not self._markets:
logger.info("Markets were not loaded. Loading them now..")
self._load_markets()
self.reload_markets(True)
return self._markets
@property
@ -552,30 +552,26 @@ class Exchange:
amount, self.get_precision_amount(pair), self.precisionMode, contract_size
)
def _load_async_markets(self, reload: bool = False) -> None:
def _load_async_markets(self, reload: bool = False) -> Dict[str, Any]:
try:
if self._api_async:
self.loop.run_until_complete(self._api_async.load_markets(reload=reload, params={}))
markets = self.loop.run_until_complete(
self._api_async.load_markets(reload=reload, params={})
)
except (asyncio.TimeoutError, ccxt.BaseError) as e:
logger.warning("Could not load async markets. Reason: %s", e)
return
if isinstance(markets, Exception):
raise markets
return markets
except asyncio.TimeoutError as e:
logger.warning("Could not load markets. Reason: %s", e)
raise TemporaryError from e
def _load_markets(self) -> None:
"""Initialize markets both sync and async"""
try:
self._markets = self._api.load_markets(params={})
self._load_async_markets()
self._last_markets_refresh = dt_ts()
if self._ft_has["needs_trading_fees"]:
self._trading_fees = self.fetch_trading_fees()
def reload_markets(self, force: bool = False, *, load_leverage_tiers: bool = True) -> None:
"""
Reload / Initialize markets both sync and async if refresh interval has passed
except ccxt.BaseError:
logger.exception("Unable to initialize markets.")
def reload_markets(self, force: bool = False) -> None:
"""Reload markets both sync and async if refresh interval has passed"""
"""
# Check whether markets have to be reloaded
is_initial = self._last_markets_refresh == 0
if (
not force
and self._last_markets_refresh > 0
@ -584,13 +580,18 @@ class Exchange:
return None
logger.debug("Performing scheduled market reload..")
try:
self._markets = self._api.load_markets(reload=True, params={})
# Also reload async markets to avoid issues with newly listed pairs
self._load_async_markets(reload=True)
# Reload async markets, then assign them to sync api
self._markets = self._load_async_markets(reload=True)
self._api.set_markets(self._api_async.markets, self._api_async.currencies)
self._last_markets_refresh = dt_ts()
self.fill_leverage_tiers()
except ccxt.BaseError:
logger.exception("Could not reload markets.")
if is_initial and self._ft_has["needs_trading_fees"]:
self._trading_fees = self.fetch_trading_fees()
if load_leverage_tiers and self.trading_mode == TradingMode.FUTURES:
self.fill_leverage_tiers()
except (ccxt.BaseError, TemporaryError):
logger.exception("Could not load markets.")
def validate_stakecurrency(self, stake_currency: str) -> None:
"""
@ -3040,7 +3041,16 @@ class Exchange:
}
file_dump_json(filename, data)
def load_cached_leverage_tiers(self, stake_currency: str) -> Optional[Dict[str, List[Dict]]]:
def load_cached_leverage_tiers(
self, stake_currency: str, cache_time: Optional[timedelta] = None
) -> Optional[Dict[str, List[Dict]]]:
"""
Load cached leverage tiers from disk
:param cache_time: The maximum age of the cache before it is considered outdated
"""
if not cache_time:
# Default to 4 weeks
cache_time = timedelta(weeks=4)
filename = self._config["datadir"] / "futures" / f"leverage_tiers_{stake_currency}.json"
if filename.is_file():
try:
@ -3048,7 +3058,7 @@ class Exchange:
updated = tiers.get("updated")
if updated:
updated_dt = parser.parse(updated)
if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4):
if updated_dt < datetime.now(timezone.utc) - cache_time:
logger.info("Cached leverage tiers are outdated. Will update.")
return None
return tiers["data"]

View File

@ -24,6 +24,10 @@ class Htx(Exchange):
"ohlcv_candle_limit": 1000,
"l2_limit_range": [5, 10, 20],
"l2_limit_range_required": False,
"ohlcv_candle_limit_per_timeframe": {
"1w": 500,
"1M": 500,
},
}
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:

View File

@ -960,7 +960,7 @@ class FreqaiDataKitchen:
"""
Remove all special characters from feature strings (:)
:param dataframe: the dataframe that just finished indicator population. (unfiltered)
:return: dataframe with cleaned featrue names
:return: dataframe with cleaned feature names
"""
spec_chars = [":"]

View File

@ -1,5 +1,4 @@
import logging
import sys
from pathlib import Path
from typing import Any, Dict
@ -57,8 +56,6 @@ class CatboostClassifier(BaseClassifierModel):
X=train_data,
eval_set=test_data,
init_model=init_model,
log_cout=sys.stdout,
log_cerr=sys.stderr,
)
return cbr

View File

@ -1,5 +1,4 @@
import logging
import sys
from pathlib import Path
from typing import Any, Dict
@ -68,8 +67,6 @@ class CatboostClassifierMultiTarget(BaseClassifierModel):
{
"eval_set": eval_sets[i],
"init_model": init_models[i],
"log_cout": sys.stdout,
"log_cerr": sys.stderr,
}
)

View File

@ -1,5 +1,4 @@
import logging
import sys
from pathlib import Path
from typing import Any, Dict
@ -56,8 +55,6 @@ class CatboostRegressor(BaseRegressionModel):
X=train_data,
eval_set=test_data,
init_model=init_model,
log_cout=sys.stdout,
log_cerr=sys.stderr,
)
return model

View File

@ -1,5 +1,4 @@
import logging
import sys
from pathlib import Path
from typing import Any, Dict
@ -67,8 +66,6 @@ class CatboostRegressorMultiTarget(BaseRegressionModel):
{
"eval_set": eval_sets[i],
"init_model": init_models[i],
"log_cout": sys.stdout,
"log_cerr": sys.stderr,
}
)

View File

@ -492,10 +492,11 @@ class FreqtradeBot(LoggingMixin):
except ExchangeError:
logger.warning(f"Error updating {order.order_id}.")
def handle_onexchange_order(self, trade: Trade):
def handle_onexchange_order(self, trade: Trade) -> bool:
"""
Try refinding a order that is not in the database.
Only used balance disappeared, which would make exiting impossible.
:return: True if the trade was deleted, False otherwise
"""
try:
orders = self.exchange.fetch_orders(
@ -541,6 +542,19 @@ class FreqtradeBot(LoggingMixin):
trade.exit_reason = prev_exit_reason
total = self.wallets.get_total(trade.base_currency) if trade.base_currency else 0
if total < trade.amount:
if trade.fully_canceled_entry_order_count == len(trade.orders):
logger.warning(
f"Trade only had fully canceled entry orders. "
f"Removing {trade} from database."
)
self._notify_enter_cancel(
trade,
order_type=self.strategy.order_types["entry"],
reason=constants.CANCEL_REASON["FULLY_CANCELLED"],
)
trade.delete()
return True
if total > trade.amount * 0.98:
logger.warning(
f"{trade} has a total of {trade.amount} {trade.base_currency}, "
@ -566,6 +580,7 @@ class FreqtradeBot(LoggingMixin):
except Exception:
# catching https://github.com/freqtrade/freqtrade/issues/9025
logger.warning("Error finding onexchange order", exc_info=True)
return False
#
# enter positions / open trades logic and methods
@ -1007,7 +1022,13 @@ class FreqtradeBot(LoggingMixin):
# Update fees if order is non-opened
if order_status in constants.NON_OPEN_EXCHANGE_STATES:
self.update_trade_state(trade, order_id, order)
fully_canceled = self.update_trade_state(trade, order_id, order)
if fully_canceled and mode != "replace":
# Fully canceled orders, may happen with some time in force setups (IOC).
# Should be handled immediately.
self.handle_cancel_enter(
trade, order, order_obj, constants.CANCEL_REASON["TIMEOUT"]
)
return True
@ -1229,7 +1250,9 @@ class FreqtradeBot(LoggingMixin):
f"Not enough {trade.safe_base_currency} in wallet to exit {trade}. "
"Trying to recover."
)
self.handle_onexchange_order(trade)
if self.handle_onexchange_order(trade):
# Trade was deleted. Don't continue.
continue
try:
try:

View File

@ -42,4 +42,4 @@ class MaxDrawDownHyperOptLoss(IHyperOptLoss):
except ValueError:
# No losing trade, therefore no drawdown.
return -total_profit
return -total_profit / max_drawdown[0]
return -total_profit / max_drawdown.drawdown_abs

View File

@ -10,22 +10,28 @@ individual needs.
from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_max_drawdown
from freqtrade.optimize.hyperopt import IHyperOptLoss
# higher numbers penalize drawdowns more severely
# smaller numbers penalize drawdowns more severely
DRAWDOWN_MULT = 0.075
class ProfitDrawDownHyperOptLoss(IHyperOptLoss):
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int, *args, **kwargs) -> float:
def hyperopt_loss_function(results: DataFrame, config: Config, *args, **kwargs) -> float:
total_profit = results["profit_abs"].sum()
try:
max_drawdown_abs = calculate_max_drawdown(results, value_col="profit_abs")[5]
drawdown = calculate_max_drawdown(
results, starting_balance=config["dry_run_wallet"], value_col="profit_abs"
)
relative_account_drawdown = drawdown.relative_account_drawdown
except ValueError:
max_drawdown_abs = 0
relative_account_drawdown = 0
return -1 * (total_profit * (1 - max_drawdown_abs * DRAWDOWN_MULT))
return -1 * (
total_profit - (relative_account_drawdown * total_profit) * (1 - DRAWDOWN_MULT)
)

View File

@ -358,14 +358,15 @@ class HyperoptTools:
)
@staticmethod
def prepare_trials_columns(trials: pd.DataFrame, has_drawdown: bool) -> pd.DataFrame:
def prepare_trials_columns(trials: pd.DataFrame) -> pd.DataFrame:
trials["Best"] = ""
if "results_metrics.winsdrawslosses" not in trials.columns:
# Ensure compatibility with older versions of hyperopt results
trials["results_metrics.winsdrawslosses"] = "N/A"
if not has_drawdown:
has_account_drawdown = "results_metrics.max_drawdown_account" in trials.columns
if not has_account_drawdown:
# Ensure compatibility with older versions of hyperopt results
trials["results_metrics.max_drawdown_account"] = None
if "is_random" not in trials.columns:
@ -389,7 +390,6 @@ class HyperoptTools:
"results_metrics.profit_total_abs",
"results_metrics.profit_total",
"results_metrics.holding_avg",
"results_metrics.max_drawdown",
"results_metrics.max_drawdown_account",
"results_metrics.max_drawdown_abs",
"loss",
@ -408,7 +408,6 @@ class HyperoptTools:
"Total profit",
"Profit",
"Avg duration",
"max_drawdown",
"max_drawdown_account",
"max_drawdown_abs",
"Objective",
@ -437,9 +436,7 @@ class HyperoptTools:
tabulate.PRESERVE_WHITESPACE = True
trials = json_normalize(results, max_level=1)
has_account_drawdown = "results_metrics.max_drawdown_account" in trials.columns
trials = HyperoptTools.prepare_trials_columns(trials, has_account_drawdown)
trials = HyperoptTools.prepare_trials_columns(trials)
trials["is_profit"] = False
trials.loc[trials["is_initial_point"] | trials["is_random"], "Best"] = "* "
@ -471,23 +468,19 @@ class HyperoptTools:
stake_currency = config["stake_currency"]
trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply(
trials["Max Drawdown (Acct)"] = trials.apply(
lambda x: (
"{} {}".format(
fmt_coin(x["max_drawdown_abs"], stake_currency, keep_trailing_zeros=True),
(
f"({x['max_drawdown_account']:,.2%})"
if has_account_drawdown
else f"({x['max_drawdown']:,.2%})"
).rjust(10, " "),
(f"({x['max_drawdown_account']:,.2%})").rjust(10, " "),
).rjust(25 + len(stake_currency))
if x["max_drawdown"] != 0.0 or x["max_drawdown_account"] != 0.0
if x["max_drawdown_account"] != 0.0
else "--".rjust(25 + len(stake_currency))
),
axis=1,
)
trials = trials.drop(columns=["max_drawdown_abs", "max_drawdown", "max_drawdown_account"])
trials = trials.drop(columns=["max_drawdown_abs", "max_drawdown_account"])
trials["Profit"] = trials.apply(
lambda x: (

View File

@ -497,29 +497,25 @@ def generate_strategy_stats(
}
try:
max_drawdown_legacy, _, _, _, _, _ = calculate_max_drawdown(
results, value_col="profit_ratio"
)
(drawdown_abs, drawdown_start, drawdown_end, high_val, low_val, max_drawdown) = (
calculate_max_drawdown(results, value_col="profit_abs", starting_balance=start_balance)
drawdown = calculate_max_drawdown(
results, value_col="profit_abs", starting_balance=start_balance
)
# max_relative_drawdown = Underwater
(_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown(
underwater = calculate_max_drawdown(
results, value_col="profit_abs", starting_balance=start_balance, relative=True
)
strat_stats.update(
{
"max_drawdown": max_drawdown_legacy, # Deprecated - do not use
"max_drawdown_account": max_drawdown,
"max_relative_drawdown": max_relative_drawdown,
"max_drawdown_abs": drawdown_abs,
"drawdown_start": drawdown_start.strftime(DATETIME_PRINT_FORMAT),
"drawdown_start_ts": drawdown_start.timestamp() * 1000,
"drawdown_end": drawdown_end.strftime(DATETIME_PRINT_FORMAT),
"drawdown_end_ts": drawdown_end.timestamp() * 1000,
"max_drawdown_low": low_val,
"max_drawdown_high": high_val,
"max_drawdown_account": drawdown.relative_account_drawdown,
"max_relative_drawdown": underwater.relative_account_drawdown,
"max_drawdown_abs": drawdown.drawdown_abs,
"drawdown_start": drawdown.high_date.strftime(DATETIME_PRINT_FORMAT),
"drawdown_start_ts": drawdown.high_date.timestamp() * 1000,
"drawdown_end": drawdown.low_date.strftime(DATETIME_PRINT_FORMAT),
"drawdown_end_ts": drawdown.low_date.timestamp() * 1000,
"max_drawdown_low": drawdown.low_value,
"max_drawdown_high": drawdown.high_value,
}
)
@ -529,7 +525,6 @@ def generate_strategy_stats(
except ValueError:
strat_stats.update(
{
"max_drawdown": 0.0,
"max_drawdown_account": 0.0,
"max_relative_drawdown": 0.0,
"max_drawdown_abs": 0.0,

View File

@ -957,7 +957,24 @@ class LocalTrade:
def update_order(self, order: Dict) -> None:
Order.update_orders(self.orders, order)
def get_canceled_exit_order_count(self) -> int:
@property
def fully_canceled_entry_order_count(self) -> int:
"""
Get amount of failed exiting orders
assumes full exits.
"""
return len(
[
o
for o in self.orders
if o.ft_order_side == self.entry_side
and o.status in CANCELED_EXCHANGE_STATES
and o.filled == 0
]
)
@property
def canceled_exit_order_count(self) -> int:
"""
Get amount of failed exiting orders
assumes full exits.
@ -970,6 +987,13 @@ class LocalTrade:
]
)
def get_canceled_exit_order_count(self) -> int:
"""
Get amount of failed exiting orders
assumes full exits.
"""
return self.canceled_exit_order_count
def _calc_open_trade_value(self, amount: float, open_rate: float) -> float:
"""
Calculate the open_rate including open_fee.

View File

@ -179,19 +179,17 @@ def add_max_drawdown(
Add scatter points indicating max drawdown
"""
try:
_, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown(
trades, starting_balance=starting_balance
)
drawdown = calculate_max_drawdown(trades, starting_balance=starting_balance)
drawdown = go.Scatter(
x=[highdate, lowdate],
x=[drawdown.high_date, drawdown.low_date],
y=[
df_comb.loc[timeframe_to_prev_date(timeframe, highdate), "cum_profit"],
df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), "cum_profit"],
df_comb.loc[timeframe_to_prev_date(timeframe, drawdown.high_date), "cum_profit"],
df_comb.loc[timeframe_to_prev_date(timeframe, drawdown.low_date), "cum_profit"],
],
mode="markers",
name=f"Max drawdown {max_drawdown:.2%}",
text=f"Max drawdown {max_drawdown:.2%}",
name=f"Max drawdown {drawdown.relative_account_drawdown:.2%}",
text=f"Max drawdown {drawdown.relative_account_drawdown:.2%}",
marker=dict(symbol="square-open", size=9, line=dict(width=2), color="green"),
)
fig.add_trace(drawdown, row, 1)

View File

@ -8,12 +8,12 @@ import logging
from typing import Any, Dict, List
from cachetools import TTLCache
from pycoingecko import CoinGeckoAPI
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
from freqtrade.util.coin_gecko import FtCoinGeckoApi
logger = logging.getLogger(__name__)
@ -44,7 +44,13 @@ class MarketCapPairList(IPairList):
self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
self._marketcap_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
self._def_candletype = self._config["candle_type_def"]
self._coingecko: CoinGeckoAPI = CoinGeckoAPI()
_coingecko_config = config.get("coingecko", {})
self._coingecko: FtCoinGeckoApi = FtCoinGeckoApi(
api_key=_coingecko_config.get("api_key", ""),
is_demo=_coingecko_config.get("is_demo", True),
)
if self._max_rank > 250:
raise OperationalException("This filter only support marketcap rank up to 250.")

View File

@ -59,7 +59,8 @@ class MaxDrawdown(IProtection):
# Drawdown is always positive
try:
# TODO: This should use absolute profit calculation, considering account balance.
drawdown, _, _, _, _, _ = calculate_max_drawdown(trades_df, value_col="close_profit")
drawdown_obj = calculate_max_drawdown(trades_df, value_col="close_profit")
drawdown = drawdown_obj.drawdown_abs
except ValueError:
return None

View File

@ -1,5 +1,5 @@
import logging
from ipaddress import IPv4Address
from ipaddress import ip_address
from typing import Any, Optional
import orjson
@ -180,7 +180,7 @@ class ApiServer(RPCHandler):
rest_port = self._config["api_server"]["listen_port"]
logger.info(f"Starting HTTP Server at {rest_ip}:{rest_port}")
if not IPv4Address(rest_ip).is_loopback and not running_in_docker():
if not ip_address(rest_ip).is_loopback and not running_in_docker():
logger.warning("SECURITY WARNING - Local Rest Server listening to external connections")
logger.warning(
"SECURITY WARNING - This is insecure please set to your loopback,"

View File

@ -5,14 +5,14 @@ e.g BTC to USD
import logging
from datetime import datetime
from typing import Dict, List
from typing import Any, Dict, List
from cachetools import TTLCache
from pycoingecko import CoinGeckoAPI
from requests.exceptions import RequestException
from freqtrade.constants import SUPPORTED_FIAT
from freqtrade.constants import SUPPORTED_FIAT, Config
from freqtrade.mixins.logging_mixin import LoggingMixin
from freqtrade.util.coin_gecko import FtCoinGeckoApi
logger = logging.getLogger(__name__)
@ -40,28 +40,28 @@ class CryptoToFiatConverter(LoggingMixin):
"""
__instance = None
_coingecko: CoinGeckoAPI = None
_coinlistings: List[Dict] = []
_backoff: float = 0.0
def __new__(cls):
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
"""
This class is a singleton - cannot be instantiated twice.
Singleton pattern to ensure only one instance is created.
"""
if CryptoToFiatConverter.__instance is None:
CryptoToFiatConverter.__instance = object.__new__(cls)
try:
# Limit retires to 1 (0 and 1)
# otherwise we risk bot impact if coingecko is down.
CryptoToFiatConverter._coingecko = CoinGeckoAPI(retries=1)
except BaseException:
CryptoToFiatConverter._coingecko = None
return CryptoToFiatConverter.__instance
if not cls.__instance:
cls.__instance = super().__new__(cls)
return cls.__instance
def __init__(self) -> None:
def __init__(self, config: Config) -> None:
# Timeout: 6h
self._pair_price: TTLCache = TTLCache(maxsize=500, ttl=6 * 60 * 60)
_coingecko_config = config.get("coingecko", {})
self._coingecko = FtCoinGeckoApi(
api_key=_coingecko_config.get("api_key", ""),
is_demo=_coingecko_config.get("is_demo", True),
retries=1,
)
LoggingMixin.__init__(self, logger, 3600)
self._load_cryptomap()

View File

@ -19,7 +19,7 @@ from freqtrade import __version__
from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON, DEFAULT_DATAFRAME_COLUMNS, Config
from freqtrade.data.history import load_data
from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown
from freqtrade.data.metrics import DrawDownResult, calculate_expectancy, calculate_max_drawdown
from freqtrade.enums import (
CandleType,
ExitCheckTuple,
@ -107,7 +107,7 @@ class RPC:
self._freqtrade = freqtrade
self._config: Config = freqtrade.config
if self._config.get("fiat_display_currency"):
self._fiat_converter = CryptoToFiatConverter()
self._fiat_converter = CryptoToFiatConverter(self._config)
@staticmethod
def _rpc_show_config(
@ -592,21 +592,10 @@ class RPC:
expectancy, expectancy_ratio = calculate_expectancy(trades_df)
max_drawdown_abs = 0.0
max_drawdown = 0.0
drawdown_start: Optional[datetime] = None
drawdown_end: Optional[datetime] = None
dd_high_val = dd_low_val = 0.0
drawdown = DrawDownResult()
if len(trades_df) > 0:
try:
(
max_drawdown_abs,
drawdown_start,
drawdown_end,
dd_high_val,
dd_low_val,
max_drawdown,
) = calculate_max_drawdown(
drawdown = calculate_max_drawdown(
trades_df,
value_col="profit_abs",
date_col="close_date_dt",
@ -663,14 +652,14 @@ class RPC:
"winrate": winrate,
"expectancy": expectancy,
"expectancy_ratio": expectancy_ratio,
"max_drawdown": max_drawdown,
"max_drawdown_abs": max_drawdown_abs,
"max_drawdown_start": format_date(drawdown_start),
"max_drawdown_start_timestamp": dt_ts_def(drawdown_start),
"max_drawdown_end": format_date(drawdown_end),
"max_drawdown_end_timestamp": dt_ts_def(drawdown_end),
"drawdown_high": dd_high_val,
"drawdown_low": dd_low_val,
"max_drawdown": drawdown.relative_account_drawdown,
"max_drawdown_abs": drawdown.drawdown_abs,
"max_drawdown_start": format_date(drawdown.high_date),
"max_drawdown_start_timestamp": dt_ts_def(drawdown.high_date),
"max_drawdown_end": format_date(drawdown.low_date),
"max_drawdown_end_timestamp": dt_ts_def(drawdown.low_date),
"drawdown_high": drawdown.high_value,
"drawdown_low": drawdown.low_value,
"trading_volume": trading_volume,
"bot_start_timestamp": dt_ts_def(bot_start, 0),
"bot_start_date": format_date(bot_start),

View File

@ -10,7 +10,8 @@
"stake_currency": "{{ stake_currency }}",
"stake_amount": {{ stake_amount }},
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "{{ fiat_display_currency }}",{{ ('\n "timeframe": "' + timeframe + '",') if timeframe else '' }}
{{- ('\n "fiat_display_currency": "' + fiat_display_currency + '",') if fiat_display_currency else ''}}
{{- ('\n "timeframe": "' + timeframe + '",') if timeframe else '' }}
"dry_run": {{ dry_run | lower }},
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": false,

View File

@ -0,0 +1,26 @@
from pycoingecko import CoinGeckoAPI
class FtCoinGeckoApi(CoinGeckoAPI):
"""
Simple wrapper around pycoingecko's api to support Demo API keys.
"""
__API_URL_BASE = "https://api.coingecko.com/api/v3/"
__PRO_API_URL_BASE = "https://pro-api.coingecko.com/api/v3/"
_api_key: str = ""
def __init__(self, api_key: str = "", *, is_demo=True, retries=5):
super().__init__(retries=retries)
# Doint' pass api_key to parent, instead set the header on the session directly
self._api_key = api_key
if api_key and not is_demo:
self.api_base_url = self.__PRO_API_URL_BASE
self.session.params.update({"x_cg_pro_api_key": api_key})
else:
# Use demo api key
self.api_base_url = self.__API_URL_BASE
if api_key:
self.session.params.update({"x_cg_demo_api_key": api_key})

View File

@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2024.5-dev"
__version__ = "2024.6-dev"
if "dev" in __version__:
from pathlib import Path

View File

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

View File

@ -1,5 +1,6 @@
site_name: Freqtrade
site_url: https://www.freqtrade.io/en/latest/
site_url: !ENV [READTHEDOCS_CANONICAL_URL, 'https://www.freqtrade.io/en/latest/']
site_description: Freqtrade is a free and open source crypto trading bot written in Python, designed to support all major exchanges and be controlled via Telegram or builtin Web UI
repo_url: https://github.com/freqtrade/freqtrade
edit_uri: edit/develop/docs/
use_directory_urls: True
@ -49,9 +50,9 @@ nav:
- Advanced Hyperopt: advanced-hyperopt.md
- Advanced Orderflow: advanced-orderflow.md
- Producer/Consumer mode: producer-consumer.md
- SQL Cheat-sheet: sql_cheatsheet.md
- Edge Positioning: edge.md
- FAQ: faq.md
- SQL Cheat-sheet: sql_cheatsheet.md
- Strategy migration: strategy_migration.md
- Updating Freqtrade: updating.md
- Deprecated Features: deprecated.md

View File

@ -82,6 +82,9 @@ skip_glob = ["**/.env*", "**/env/*", "**/.venv/*", "**/docs/*", "**/user_data/*"
known_first_party = ["freqtrade_client"]
[tool.pytest.ini_options]
log_format = "%(asctime)s %(levelname)s %(message)s"
log_date_format = "%Y-%m-%d %H:%M:%S"
asyncio_mode = "auto"
addopts = "--dist loadscope"
@ -181,4 +184,4 @@ exclude = [
[tool.codespell]
ignore-words-list = "coo,fo,strat,zar,selectin"
skip="*.svg,./user_data,./freqtrade/rpc/api_server/ui/installed"
skip="*.svg,./user_data,freqtrade/rpc/api_server/ui/installed,freqtrade/exchange/*.json"

View File

@ -6,12 +6,12 @@
-r requirements-freqai-rl.txt
-r docs/requirements-docs.txt
coveralls==4.0.0
ruff==0.4.4
coveralls==4.0.1
ruff==0.4.7
mypy==1.10.0
pre-commit==3.7.1
pytest==8.2.0
pytest-asyncio==0.23.6
pytest==8.2.1
pytest-asyncio==0.23.7
pytest-cov==5.0.0
pytest-mock==3.14.0
pytest-random-order==1.1.1
@ -26,6 +26,6 @@ nbconvert==7.16.4
# mypy types
types-cachetools==5.3.0.7
types-filelock==3.2.7
types-requests==2.31.0.20240406
types-requests==2.32.0.20240602
types-tabulate==0.9.0.20240106
types-python-dateutil==2.9.0.20240316

View File

@ -3,7 +3,7 @@
-r requirements-plot.txt
# Required for freqai
scikit-learn==1.4.2
scikit-learn==1.5.0
joblib==1.4.2
catboost==1.2.5; 'arm' not in platform_machine
lightgbm==4.3.0

View File

@ -2,7 +2,7 @@
-r requirements.txt
# Required for hyperopt
scipy==1.13.0
scikit-learn==1.4.2
scipy==1.13.1
scikit-learn==1.5.0
ft-scikit-optimize==0.9.2
filelock==3.14.0

View File

@ -1,20 +1,22 @@
numpy==1.26.4
pandas==2.2.2
bottleneck==1.3.8
numexpr==2.10.0
pandas-ta==0.3.14b
ccxt==4.3.21
ccxt==4.3.38
cryptography==42.0.7
aiohttp==3.9.5
SQLAlchemy==2.0.30
python-telegram-bot==21.1.1
python-telegram-bot==21.2
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1
humanize==4.9.0
cachetools==5.3.3
requests==2.31.0
requests==2.32.3
urllib3==2.2.1
jsonschema==4.22.0
TA-Lib==0.4.28
TA-Lib==0.4.30
technical==1.4.3
tabulate==0.9.0
pycoingecko==3.1.0
@ -22,13 +24,13 @@ jinja2==3.1.4
tables==3.9.1
joblib==1.4.2
rich==13.7.1
pyarrow==16.0.0; platform_machine != 'armv7l'
pyarrow==16.1.0; platform_machine != 'armv7l'
# find first, C search in arrays
py_find_1st==1.1.6
# Load ticker files 30% faster
python-rapidjson==1.16
python-rapidjson==1.17
# Properly format api responses
orjson==3.10.3
@ -37,8 +39,8 @@ sdnotify==0.3.2
# API Server
fastapi==0.111.0
pydantic==2.7.1
uvicorn==0.29.0
pydantic==2.7.2
uvicorn==0.30.1
pyjwt==2.8.0
aiofiles==23.2.1
psutil==5.9.8
@ -53,7 +55,7 @@ python-dateutil==2.9.0.post0
pytz==2024.1
#Futures
schedule==1.2.1
schedule==1.2.2
#WS Messages
websockets==12.0

285
setup.ps1 Normal file
View File

@ -0,0 +1,285 @@
Clear-Host
$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$Global:LogFilePath = Join-Path $env:TEMP "script_log_$Timestamp.txt"
$RequirementFiles = @("requirements.txt", "requirements-dev.txt", "requirements-hyperopt.txt", "requirements-freqai.txt", "requirements-freqai-rl.txt", "requirements-plot.txt")
$VenvName = ".venv"
$VenvDir = Join-Path $PSScriptRoot $VenvName
function Write-Log {
param (
[string]$Message,
[string]$Level = 'INFO'
)
if (-not (Test-Path -Path $LogFilePath)) {
New-Item -ItemType File -Path $LogFilePath -Force | Out-Null
}
switch ($Level) {
'INFO' { Write-Host $Message -ForegroundColor Green }
'WARNING' { Write-Host $Message -ForegroundColor Yellow }
'ERROR' { Write-Host $Message -ForegroundColor Red }
'PROMPT' { Write-Host $Message -ForegroundColor Cyan }
}
"${Level}: $Message" | Out-File $LogFilePath -Append
}
function Get-UserSelection {
param (
[string]$Prompt,
[string[]]$Options,
[string]$DefaultChoice = 'A',
[bool]$AllowMultipleSelections = $true
)
Write-Log "$Prompt`n" -Level 'PROMPT'
for ($I = 0; $I -lt $Options.Length; $I++) {
Write-Log "$([char](65 + $I)). $($Options[$I])" -Level 'PROMPT'
}
if ($AllowMultipleSelections) {
Write-Log "`nSelect one or more options by typing the corresponding letters, separated by commas." -Level 'PROMPT'
}
else {
Write-Log "`nSelect an option by typing the corresponding letter." -Level 'PROMPT'
}
[string]$UserInput = Read-Host
if ([string]::IsNullOrEmpty($UserInput)) {
$UserInput = $DefaultChoice
}
$UserInput = $UserInput.ToUpper()
if ($AllowMultipleSelections) {
$Selections = $UserInput.Split(',') | ForEach-Object { $_.Trim() }
$SelectedIndices = @()
foreach ($Selection in $Selections) {
if ($Selection -match '^[A-Z]$') {
$Index = [int][char]$Selection - [int][char]'A'
if ($Index -ge 0 -and $Index -lt $Options.Length) {
$SelectedIndices += $Index
}
else {
Write-Log "Invalid input: $Selection. Please enter letters within the valid range of options." -Level 'ERROR'
return -1
}
}
else {
Write-Log "Invalid input: $Selection. Please enter a letter between A and Z." -Level 'ERROR'
return -1
}
}
return $SelectedIndices
}
else {
if ($UserInput -match '^[A-Z]$') {
$SelectedIndex = [int][char]$UserInput - [int][char]'A'
if ($SelectedIndex -ge 0 -and $SelectedIndex -lt $Options.Length) {
return $SelectedIndex
}
else {
Write-Log "Invalid input: $UserInput. Please enter a letter within the valid range of options." -Level 'ERROR'
return -1
}
}
else {
Write-Log "Invalid input: $UserInput. Please enter a letter between A and Z." -Level 'ERROR'
return -1
}
}
}
function Exit-Script {
param (
[int]$ExitCode,
[bool]$WaitForKeypress = $true
)
if ($ExitCode -ne 0) {
Write-Log "Script failed. Would you like to open the log file? (Y/N)" -Level 'PROMPT'
$openLog = Read-Host
if ($openLog -eq 'Y' -or $openLog -eq 'y') {
Start-Process notepad.exe -ArgumentList $LogFilePath
}
}
elseif ($WaitForKeypress) {
Write-Log "Press any key to exit..."
$host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") | Out-Null
}
return $ExitCode
}
function Test-PythonExecutable {
param(
[string]$PythonExecutable
)
$DeactivateVenv = Join-Path $VenvDir "Scripts\Deactivate.bat"
if (Test-Path $DeactivateVenv) {
Write-Host "Deactivating virtual environment..." 2>&1 | Out-File $LogFilePath -Append
& $DeactivateVenv
Write-Host "Virtual environment deactivated." 2>&1 | Out-File $LogFilePath -Append
}
else {
Write-Host "Deactivation script not found: $DeactivateVenv" 2>&1 | Out-File $LogFilePath -Append
}
$PythonCmd = Get-Command $PythonExecutable -ErrorAction SilentlyContinue
if ($PythonCmd) {
$VersionOutput = & $PythonCmd.Source --version 2>&1
if ($LASTEXITCODE -eq 0) {
$Version = $VersionOutput | Select-String -Pattern "Python (\d+\.\d+\.\d+)" | ForEach-Object { $_.Matches.Groups[1].Value }
Write-Log "Python version $Version found using executable '$PythonExecutable'."
return $true
}
else {
Write-Log "Python executable '$PythonExecutable' not working correctly." -Level 'ERROR'
return $false
}
}
else {
Write-Log "Python executable '$PythonExecutable' not found." -Level 'ERROR'
return $false
}
}
function Find-PythonExecutable {
$PythonExecutables = @(
"python",
"python3.12",
"python3.11",
"python3.10",
"python3.9",
"python3",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python312\python.exe",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python311\python.exe",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python310\python.exe",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python39\python.exe",
"C:\Python312\python.exe",
"C:\Python311\python.exe",
"C:\Python310\python.exe",
"C:\Python39\python.exe"
)
foreach ($Executable in $PythonExecutables) {
if (Test-PythonExecutable -PythonExecutable $Executable) {
return $Executable
}
}
return $null
}
function Main {
"Starting the operations..." | Out-File $LogFilePath -Append
"Current directory: $(Get-Location)" | Out-File $LogFilePath -Append
# Exit on lower versions than Python 3.9 or when Python executable not found
$PythonExecutable = Find-PythonExecutable
if ($null -eq $PythonExecutable) {
Write-Log "No suitable Python executable found. Please ensure that Python 3.9 or higher is installed and available in the system PATH." -Level 'ERROR'
Exit 1
}
# Define the path to the Python executable in the virtual environment
$ActivateVenv = "$VenvDir\Scripts\Activate.ps1"
# Check if the virtual environment exists, if not, create it
if (-Not (Test-Path $ActivateVenv)) {
Write-Log "Virtual environment not found. Creating virtual environment..." -Level 'ERROR'
& $PythonExecutable -m venv $VenvName 2>&1 | Out-File $LogFilePath -Append
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to create virtual environment." -Level 'ERROR'
Exit-Script -exitCode 1
}
else {
Write-Log "Virtual environment created."
}
}
# Activate the virtual environment and check if it was successful
Write-Log "Virtual environment found. Activating virtual environment..."
& $ActivateVenv 2>&1 | Out-File $LogFilePath -Append
# Check if virtual environment is activated
if ($env:VIRTUAL_ENV) {
Write-Log "Virtual environment is activated at: $($env:VIRTUAL_ENV)"
}
else {
Write-Log "Failed to activate virtual environment." -Level 'ERROR'
Exit-Script -exitCode 1
}
# Ensure pip
python -m ensurepip --default-pip 2>&1 | Out-File $LogFilePath -Append
# Pull latest updates only if the repository state is not dirty
Write-Log "Checking if the repository is clean..."
$Status = & "git" status --porcelain
if ($Status) {
Write-Log "Changes in local git repository. Skipping git pull."
}
else {
Write-Log "Pulling latest updates..."
& "git" pull 2>&1 | Out-File $LogFilePath -Append
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to pull updates from Git." -Level 'ERROR'
Exit-Script -exitCode 1
}
}
if (-not (Test-Path "$VenvDir\Lib\site-packages\talib")) {
# Install TA-Lib using the virtual environment's pip
Write-Log "Installing TA-Lib using virtual environment's pip..."
python -m pip install --find-links=build_helpers\ --prefer-binary TA-Lib 2>&1 | Out-File $LogFilePath -Append
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to install TA-Lib." -Level 'ERROR'
Exit-Script -exitCode 1
}
}
# Present options for requirement files
$SelectedIndices = Get-UserSelection -prompt "Select which requirement files to install:" -options $RequirementFiles -defaultChoice 'A'
# Cache the selected requirement files
$SelectedRequirementFiles = @()
$PipInstallArguments = @()
foreach ($Index in $SelectedIndices) {
$RelativePath = $RequirementFiles[$Index]
if (Test-Path $RelativePath) {
$SelectedRequirementFiles += $RelativePath
$PipInstallArguments += "-r", $RelativePath # Add each flag and path as separate elements
}
else {
Write-Log "Requirement file not found: $RelativePath" -Level 'ERROR'
Exit-Script -exitCode 1
}
}
if ($PipInstallArguments.Count -ne 0) {
& pip install @PipInstallArguments # Use array splatting to pass arguments correctly
}
# Install freqtrade from setup using the virtual environment's Python
Write-Log "Installing freqtrade from setup..."
pip install -e . 2>&1 | Out-File $LogFilePath -Append
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to install freqtrade." -Level 'ERROR'
Exit-Script -exitCode 1
}
Write-Log "Installing freqUI..."
python freqtrade install-ui 2>&1 | Out-File $LogFilePath -Append
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to install freqUI." -Level 'ERROR'
Exit-Script -exitCode 1
}
Write-Log "Installation/Update complete!"
Exit-Script -exitCode 0
}
# Call the Main function
Main

View File

@ -69,7 +69,7 @@ setup(
],
install_requires=[
# from requirements.txt
"ccxt>=4.2.47",
"ccxt>=4.3.24",
"SQLAlchemy>=2.0.6",
"python-telegram-bot>=20.1",
"humanize>=4.0.0",

View File

@ -25,7 +25,7 @@ function check_installed_python() {
exit 2
fi
for v in 11 10 9
for v in 12 11 10 9
do
PYTHON="python3.${v}"
which $PYTHON
@ -277,7 +277,7 @@ function install() {
install_redhat
else
echo "This script does not support your OS."
echo "If you have Python version 3.9 - 3.11, pip, virtualenv, ta-lib you can continue."
echo "If you have Python version 3.9 - 3.12, pip, virtualenv, ta-lib you can continue."
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
sleep 10
fi

View File

@ -54,6 +54,7 @@ from tests.conftest import (
patch_exchange,
patched_configuration_load_config_file,
)
from tests.conftest_hyperopt import hyperopt_test_result
from tests.conftest_trades import MOCK_TRADE_COUNT
@ -1137,7 +1138,8 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
pytest.fail(f"Expected well formed JSON, but failed to parse: {captured.out}")
def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmp_path):
def test_hyperopt_list(mocker, capsys, caplog, tmp_path):
saved_hyperopt_results = hyperopt_test_result()
csv_file = tmp_path / "test.csv"
mocker.patch(
"freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist",
@ -1507,7 +1509,8 @@ def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmp_path)
csv_file.unlink()
def test_hyperopt_show(mocker, capsys, saved_hyperopt_results):
def test_hyperopt_show(mocker, capsys):
saved_hyperopt_results = hyperopt_test_result()
mocker.patch(
"freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist",
return_value=True,

File diff suppressed because it is too large Load Diff

1000
tests/conftest_hyperopt.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -343,17 +343,15 @@ def test_create_cum_profit1(testdatadir):
def test_calculate_max_drawdown(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
_, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown(
bt_data, value_col="profit_abs"
)
assert isinstance(drawdown, float)
assert pytest.approx(drawdown) == 0.29753914
assert isinstance(hdate, Timestamp)
assert isinstance(lowdate, Timestamp)
assert isinstance(hval, float)
assert isinstance(lval, float)
assert hdate == Timestamp("2018-01-16 19:30:00", tz="UTC")
assert lowdate == Timestamp("2018-01-16 22:25:00", tz="UTC")
drawdown = calculate_max_drawdown(bt_data, value_col="profit_abs")
assert isinstance(drawdown.relative_account_drawdown, float)
assert pytest.approx(drawdown.relative_account_drawdown) == 0.29753914
assert isinstance(drawdown.high_date, Timestamp)
assert isinstance(drawdown.low_date, Timestamp)
assert isinstance(drawdown.high_value, float)
assert isinstance(drawdown.low_value, float)
assert drawdown.high_date == Timestamp("2018-01-16 19:30:00", tz="UTC")
assert drawdown.low_date == Timestamp("2018-01-16 22:25:00", tz="UTC")
underwater = calculate_underwater(bt_data)
assert isinstance(underwater, DataFrame)
@ -509,19 +507,20 @@ def test_calculate_max_drawdown2():
# sort by profit and reset index
df = df.sort_values("profit").reset_index(drop=True)
df1 = df.copy()
drawdown, hdate, ldate, hval, lval, drawdown_rel = calculate_max_drawdown(
df, date_col="open_date", value_col="profit"
drawdown = calculate_max_drawdown(
df, date_col="open_date", starting_balance=0.2, value_col="profit"
)
# Ensure df has not been altered.
assert df.equals(df1)
assert isinstance(drawdown, float)
assert isinstance(drawdown_rel, float)
assert isinstance(drawdown.drawdown_abs, float)
assert isinstance(drawdown.relative_account_drawdown, float)
# High must be before low
assert hdate < ldate
assert drawdown.high_date < drawdown.low_date
# High value must be higher than low value
assert hval > lval
assert drawdown == 0.091755
assert drawdown.high_value > drawdown.low_value
assert drawdown.drawdown_abs == 0.091755
assert pytest.approx(drawdown.relative_account_drawdown) == 0.32129575
df = DataFrame(zip(values[:5], dates[:5]), columns=["profit", "open_date"])
with pytest.raises(ValueError, match="No losing trade, therefore no drawdown."):
@ -530,10 +529,8 @@ def test_calculate_max_drawdown2():
df1 = DataFrame(zip(values[:5], dates[:5]), columns=["profit", "open_date"])
df1.loc[:, "profit"] = df1["profit"] * -1
# No winning trade ...
drawdown, hdate, ldate, hval, lval, drawdown_rel = calculate_max_drawdown(
df1, date_col="open_date", value_col="profit"
)
assert drawdown == 0.043965
drawdown = calculate_max_drawdown(df1, date_col="open_date", value_col="profit")
assert drawdown.drawdown_abs == 0.043965
@pytest.mark.parametrize(
@ -555,20 +552,20 @@ def test_calculate_max_drawdown_abs(profits, relative, highd, lowdays, result, r
# sort by profit and reset index
df = df.sort_values("profit_abs").reset_index(drop=True)
df1 = df.copy()
drawdown, hdate, ldate, hval, lval, drawdown_rel = calculate_max_drawdown(
drawdown = calculate_max_drawdown(
df, date_col="open_date", starting_balance=1000, relative=relative
)
# Ensure df has not been altered.
assert df.equals(df1)
assert isinstance(drawdown, float)
assert isinstance(drawdown_rel, float)
assert hdate == init_date + timedelta(days=highd)
assert ldate == init_date + timedelta(days=lowdays)
assert isinstance(drawdown.drawdown_abs, float)
assert isinstance(drawdown.relative_account_drawdown, float)
assert drawdown.high_date == init_date + timedelta(days=highd)
assert drawdown.low_date == init_date + timedelta(days=lowdays)
# High must be before low
assert hdate < ldate
assert drawdown.high_date < drawdown.low_date
# High value must be higher than low value
assert hval > lval
assert drawdown == result
assert pytest.approx(drawdown_rel) == result_rel
assert drawdown.high_value > drawdown.low_value
assert drawdown.drawdown_abs == result
assert pytest.approx(drawdown.relative_account_drawdown) == result_rel

View File

@ -69,13 +69,19 @@ def test_download_data_main_trades(mocker):
assert dl_mock.call_args[1]["timerange"].starttype == "date"
assert dl_mock.call_count == 1
assert convert_mock.call_count == 1
assert convert_mock.call_count == 0
dl_mock.reset_mock()
config.update(
{
"download_trades": True,
"trading_mode": "futures",
"convert_trades": True,
}
)
download_data_main(config)
assert dl_mock.call_args[1]["timerange"].starttype == "date"
assert dl_mock.call_count == 1
assert convert_mock.call_count == 1
def test_download_data_main_data_invalid(mocker):

View File

@ -182,7 +182,7 @@ def test_remove_exchange_credentials(default_conf) -> None:
def test_init_ccxt_kwargs(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency")
aei_mock = mocker.patch(f"{EXMS}.additional_exchange_init")
@ -519,7 +519,7 @@ def test__load_async_markets(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_markets")
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
exchange = Exchange(default_conf)
@ -528,28 +528,26 @@ def test__load_async_markets(default_conf, mocker, caplog):
assert exchange._api_async.load_markets.call_count == 1
caplog.set_level(logging.DEBUG)
exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef"))
exchange._load_async_markets()
assert log_has("Could not load async markets. Reason: deadbeef", caplog)
exchange._api_async.load_markets = get_mock_coro(side_effect=ccxt.BaseError("deadbeef"))
with pytest.raises(ccxt.BaseError, match="deadbeef"):
exchange._load_async_markets()
def test__load_markets(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
api_mock = MagicMock()
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError"))
api_mock.load_markets = get_mock_coro(side_effect=ccxt.BaseError("SomeError"))
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf)
assert log_has("Unable to initialize markets.", caplog)
assert log_has("Could not load markets.", caplog)
expected_return = {"ETH/BTC": "available"}
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value=expected_return)
api_mock.load_markets = get_mock_coro(return_value=expected_return)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
default_conf["exchange"]["pair_whitelist"] = ["ETH/BTC"]
ex = Exchange(default_conf)
@ -564,12 +562,12 @@ def test_reload_markets(default_conf, mocker, caplog, time_machine):
start_dt = dt_now()
time_machine.move_to(start_dt, tick=False)
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value=initial_markets)
api_mock.load_markets = get_mock_coro(return_value=initial_markets)
default_conf["exchange"]["markets_refresh_interval"] = 10
exchange = get_patched_exchange(
mocker, default_conf, api_mock, id="binance", mock_markets=False
)
exchange._load_async_markets = MagicMock()
lam_spy = mocker.spy(exchange, "_load_async_markets")
assert exchange._last_markets_refresh == dt_ts()
assert exchange.markets == initial_markets
@ -578,42 +576,45 @@ def test_reload_markets(default_conf, mocker, caplog, time_machine):
# less than 10 minutes have passed, no reload
exchange.reload_markets()
assert exchange.markets == initial_markets
assert exchange._load_async_markets.call_count == 0
assert lam_spy.call_count == 0
api_mock.load_markets = MagicMock(return_value=updated_markets)
api_mock.load_markets = get_mock_coro(return_value=updated_markets)
# more than 10 minutes have passed, reload is executed
time_machine.move_to(start_dt + timedelta(minutes=11), tick=False)
exchange.reload_markets()
assert exchange.markets == updated_markets
assert exchange._load_async_markets.call_count == 1
assert lam_spy.call_count == 1
assert log_has("Performing scheduled market reload..", caplog)
# Not called again
exchange._load_async_markets.reset_mock()
lam_spy.reset_mock()
exchange.reload_markets()
assert exchange._load_async_markets.call_count == 0
assert lam_spy.call_count == 0
def test_reload_markets_exception(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG)
api_mock = MagicMock()
api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError("LoadError"))
api_mock.load_markets = get_mock_coro(side_effect=ccxt.NetworkError("LoadError"))
default_conf["exchange"]["markets_refresh_interval"] = 10
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
exchange = get_patched_exchange(
mocker, default_conf, api_mock, id="binance", mock_markets=False
)
exchange._last_markets_refresh = 2
# less than 10 minutes have passed, no reload
exchange.reload_markets()
assert exchange._last_markets_refresh == 0
assert log_has_re(r"Could not reload markets.*", caplog)
assert exchange._last_markets_refresh == 2
assert log_has_re(r"Could not load markets\..*", caplog)
@pytest.mark.parametrize("stake_currency", ["ETH", "BTC", "USDT"])
def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
default_conf["stake_currency"] = stake_currency
api_mock = MagicMock()
type(api_mock).load_markets = MagicMock(
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
@ -624,7 +625,6 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf)
@ -632,7 +632,7 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
def test_validate_stakecurrency_error(default_conf, mocker, caplog):
default_conf["stake_currency"] = "XRP"
api_mock = MagicMock()
type(api_mock).load_markets = MagicMock(
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
@ -643,14 +643,13 @@ def test_validate_stakecurrency_error(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
with pytest.raises(
ConfigurationError,
match=r"XRP is not available as stake on .*Available currencies are: BTC, ETH, USDT",
):
Exchange(default_conf)
type(api_mock).load_markets = MagicMock(side_effect=ccxt.NetworkError("No connection."))
type(api_mock).load_markets = get_mock_coro(side_effect=ccxt.NetworkError("No connection."))
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
with pytest.raises(
@ -695,24 +694,26 @@ def test_get_pair_base_currency(default_conf, mocker, pair, expected):
assert ex.get_pair_base_currency(pair) == expected
def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly
def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock()
type(api_mock).load_markets = MagicMock(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
}
)
id_mock = PropertyMock(return_value="test_exchange")
type(api_mock).id = id_mock
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(
f"{EXMS}._load_async_markets",
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
},
)
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
# test exchange.validate_pairs directly
# No assert - but this should not fail (!)
Exchange(default_conf)
@ -752,7 +753,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
def test_validate_pairs_restricted(default_conf, mocker, caplog):
api_mock = MagicMock()
type(api_mock).load_markets = MagicMock(
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
@ -762,7 +763,6 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}.validate_stakecurrency")
@ -775,9 +775,9 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
)
def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
def test_validate_pairs_stakecompatibility(default_conf, mocker):
api_mock = MagicMock()
type(api_mock).load_markets = MagicMock(
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
@ -788,17 +788,16 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf)
def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, caplog):
def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker):
api_mock = MagicMock()
default_conf["stake_currency"] = ""
type(api_mock).load_markets = MagicMock(
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
@ -809,7 +808,6 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
@ -817,10 +815,10 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca
assert type(api_mock).load_markets.call_count == 1
def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog):
def test_validate_pairs_stakecompatibility_fail(default_conf, mocker):
default_conf["exchange"]["pair_whitelist"].append("HELLO-WORLD")
api_mock = MagicMock()
type(api_mock).load_markets = MagicMock(
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
@ -831,7 +829,6 @@ def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog):
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"):
@ -848,7 +845,7 @@ def test_validate_timeframes(default_conf, mocker, timeframe):
type(api_mock).timeframes = timeframes
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
@ -866,7 +863,7 @@ def test_validate_timeframes_failed(default_conf, mocker):
type(api_mock).timeframes = timeframes
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
@ -896,7 +893,7 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
del api_mock.timeframes
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises(
@ -918,7 +915,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
del api_mock.timeframes
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={"timeframes": None}))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs", MagicMock())
mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises(
@ -940,7 +937,7 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
type(api_mock).timeframes = timeframes
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
@ -956,7 +953,7 @@ def test_validate_pricing(default_conf, mocker):
}
type(api_mock).has = PropertyMock(return_value=has)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_trading_mode_and_margin_mode")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
@ -992,7 +989,7 @@ def test_validate_ordertypes(default_conf, mocker):
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
@ -1051,7 +1048,7 @@ def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name,
default_conf["margin_mode"] = MarginMode.ISOLATED
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
@ -1076,7 +1073,7 @@ def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name,
def test_validate_order_types_not_in_config(default_conf, mocker):
api_mock = MagicMock()
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_pricing")
@ -1948,7 +1945,9 @@ def test_fetch_trading_fees(default_conf, mocker):
assert api_mock.fetch_trading_fees.call_count == 1
api_mock.fetch_trading_fees.reset_mock()
# Reload-markets calls fetch_trading_fees, too - so the explicit calls in the below
# exception test would be called twice.
mocker.patch(f"{EXMS}.reload_markets")
ccxt_exceptionhandlers(
mocker, default_conf, api_mock, exchange_name, "fetch_trading_fees", "fetch_trading_fees"
)

View File

@ -45,7 +45,25 @@ EXCHANGES = {
"workingTime": 1674493798550,
"fills": [],
"selfTradePreventionMode": "NONE",
}
},
{
"symbol": "SOLUSDT",
"orderId": 3551312894,
"orderListId": -1,
"clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba",
"transactTime": 1674493798550,
"price": "15.50000000",
"origQty": "1.10000000",
"executedQty": "1.10000000",
"cummulativeQuoteQty": "17.05",
"status": "FILLED",
"timeInForce": "GTC",
"type": "LIMIT",
"side": "BUY",
"workingTime": 1674493798550,
"fills": [],
"selfTradePreventionMode": "NONE",
},
],
},
"binanceus": {
@ -200,6 +218,24 @@ EXCHANGES = {
"rebated_fee_currency": "USDT",
},
],
"sample_my_trades": [
{
"id": "123412341234",
"create_time": "167997798",
"create_time_ms": "167997798825.566200",
"currency_pair": "ETH_USDT",
"side": "sell",
"role": "taker",
"amount": "0.0115",
"price": "1712.63",
"order_id": "1234123412",
"fee": "0.0",
"fee_currency": "USDT",
"point_fee": "0.03939049",
"gt_fee": "0.0",
"amend_text": "-",
}
],
},
"okx": {
"pair": "BTC/USDT",
@ -270,6 +306,36 @@ EXCHANGES = {
"hasQuoteVolume": True,
"timeframe": "1h",
"futures": False,
"sample_order": [
{
"symbol": "SOL-USDT",
"orderId": "1762393630149869568",
"transactTime": "1674493798550",
"price": "15.5",
"stopPrice": "0",
"origQty": "1.1",
"executedQty": "1.1",
"cummulativeQuoteQty": "17.05",
"status": "FILLED",
"type": "LIMIT",
"side": "BUY",
"clientOrderID": "",
},
{
"symbol": "SOL-USDT",
"orderId": "1762393630149869568",
"transactTime": "1674493798550",
"price": "15.5",
"stopPrice": "0",
"origQty": "1.1",
"executedQty": "1.1",
"cummulativeQuoteQty": "17.05",
"status": "FILLED",
"type": "MARKET",
"side": "BUY",
"clientOrderID": "",
},
],
},
}

View File

@ -76,7 +76,8 @@ class TestCCXTExchange:
assert isinstance(po["timestamp"], int)
assert isinstance(po["price"], float)
assert po["price"] == 15.5
if po["average"] is not None:
if po["status"] == "closed":
# Filled orders should have average assigned.
assert isinstance(po["average"], float)
assert po["average"] == 15.5
assert po["symbol"] == pair
@ -86,6 +87,33 @@ class TestCCXTExchange:
else:
pytest.skip(f"No sample order available for exchange {exchange_name}")
def test_ccxt_my_trades_parse(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchange_name = exchange
if trades := EXCHANGES[exchange_name].get("sample_my_trades"):
pair = "SOL/USDT"
for trade in trades:
market = exch._api.markets[pair]
po = exch._api.parse_trade(trade)
(trade, market)
assert isinstance(po["id"], str)
assert isinstance(po["side"], str)
assert isinstance(po["amount"], float)
assert isinstance(po["price"], float)
assert isinstance(po["datetime"], str)
assert isinstance(po["timestamp"], int)
if fees := po.get("fees"):
assert isinstance(fees, list)
for fee in fees:
assert isinstance(fee, dict)
assert isinstance(fee["cost"], str)
# TODO: this should be a float!
# assert isinstance(fee["cost"], float)
assert isinstance(fee["currency"], str)
else:
pytest.skip(f"No sample Trades available for exchange {exchange_name}")
def test_ccxt_fetch_tickers(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange
pair = EXCHANGES[exchangename]["pair"]

View File

@ -50,6 +50,7 @@ def freqai_conf(default_conf, tmp_path):
freqaiconf.update(
{
"datadir": Path(default_conf["datadir"]),
"runmode": "backtest",
"strategy": "freqai_test_strat",
"user_data_dir": tmp_path,
"strategy-path": "freqtrade/tests/strategy/strats",

View File

@ -699,18 +699,20 @@ def test_process_trade_creation(
def test_process_exchange_failures(default_conf_usdt, ticker_usdt, mocker) -> None:
# TODO: Move this test to test_worker
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
EXMS,
fetch_ticker=ticker_usdt,
reload_markets=MagicMock(side_effect=TemporaryError),
reload_markets=MagicMock(),
create_order=MagicMock(side_effect=TemporaryError),
)
sleep_mock = mocker.patch("time.sleep")
worker = Worker(args=None, config=default_conf_usdt)
patch_get_signal(worker.freqtrade)
mocker.patch(f"{EXMS}.reload_markets", MagicMock(side_effect=TemporaryError))
worker._process_running()
assert sleep_mock.called is True
@ -1146,6 +1148,36 @@ def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_order
assert not freqtrade.execute_entry(pair, stake_amount)
@pytest.mark.parametrize("is_short", [False, True])
def test_execute_entry_fully_canceled_on_create(
mocker, default_conf_usdt, fee, limit_order_open, is_short
) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mock_hce = mocker.spy(freqtrade, "handle_cancel_enter")
order = limit_order_open[entry_side(is_short)]
pair = "ETH/USDT"
order["symbol"] = pair
order["status"] = "canceled"
order["filled"] = 0.0
mocker.patch.multiple(
EXMS,
fetch_ticker=MagicMock(return_value={"bid": 1.9, "ask": 2.2, "last": 1.9}),
create_order=MagicMock(return_value=order),
get_rate=MagicMock(return_value=0.11),
get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee,
)
stake_amount = 2
assert freqtrade.execute_entry(pair, stake_amount)
assert mock_hce.call_count == 1
# an order that immediately cancels completely should delete the order.
trades = Trade.get_trades().all()
assert len(trades) == 0
@pytest.mark.parametrize("is_short", [False, True])
def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order, is_short) -> None:
default_conf_usdt["trading_mode"] = "futures"
@ -4978,6 +5010,47 @@ def test_handle_onexchange_order_exit(mocker, default_conf_usdt, limit_order, is
assert trade.amount == 5.0
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize("is_short", [False, True])
def test_handle_onexchange_order_fully_canceled_enter(
mocker, default_conf_usdt, limit_order, is_short, caplog
):
default_conf_usdt["dry_run"] = False
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
entry_order = limit_order[entry_side(is_short)]
entry_order["status"] = "canceled"
entry_order["filled"] = 0.0
mock_fo = mocker.patch(
f"{EXMS}.fetch_orders",
return_value=[
entry_order,
],
)
mocker.patch(f"{EXMS}.get_rate", return_value=entry_order["price"])
trade = Trade(
pair="ETH/USDT",
fee_open=0.001,
fee_close=0.001,
open_rate=entry_order["price"],
open_date=dt_now(),
stake_amount=entry_order["cost"],
amount=entry_order["amount"],
exchange="binance",
is_short=is_short,
leverage=1,
)
trade.orders.append(Order.parse_from_ccxt_object(entry_order, "ADA/USDT", entry_side(is_short)))
Trade.session.add(trade)
assert freqtrade.handle_onexchange_order(trade) is True
assert log_has_re(r"Trade only had fully canceled entry orders\. .*", caplog)
assert mock_fo.call_count == 1
trades = Trade.get_trades().all()
assert len(trades) == 0
def test_get_valid_price(mocker, default_conf_usdt) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)

View File

@ -47,7 +47,7 @@ def generate_result_metrics():
"profit_total_abs": 0.001,
"profit_total": 0.01,
"holding_avg": timedelta(minutes=20),
"max_drawdown": 0.001,
"max_drawdown_account": 0.001,
"max_drawdown_abs": 0.001,
"loss": 0.001,
"is_initial_point": 0.001,
@ -1063,7 +1063,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmp_path, fee) -> None
def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmp_path, fee) -> None:
mocker.patch(f"{EXMS}.validate_config", MagicMock())
mocker.patch(f"{EXMS}.get_fee", fee)
mocker.patch(f"{EXMS}._load_markets")
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=get_markets()))
(tmp_path / "hyperopt_results").mkdir(parents=True)
# Dummy-reduce points to ensure scikit-learn is forced to generate new values

View File

@ -1961,9 +1961,25 @@ def test_get_canceled_exit_order_count(fee, is_short):
trade = Trade.get_trades([Trade.pair == "ETC/BTC"]).first()
# No canceled order.
assert trade.get_canceled_exit_order_count() == 0
# Property returns the same result
assert trade.canceled_exit_order_count == 0
trade.orders[-1].status = "canceled"
assert trade.get_canceled_exit_order_count() == 1
assert trade.canceled_exit_order_count == 1
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize("is_short", [True, False])
def test_fully_canceled_entry_order_count(fee, is_short):
create_mock_trades(fee, is_short=is_short)
trade = Trade.get_trades([Trade.pair == "ETC/BTC"]).first()
# No canceled order.
assert trade.fully_canceled_entry_order_count == 0
trade.orders[0].status = "canceled"
trade.orders[0].filled = 0
assert trade.fully_canceled_entry_order_count == 1
@pytest.mark.usefixtures("init_persistence")

View File

@ -773,6 +773,7 @@ def test_VolumePairList_whitelist_gen(
whitelist_result,
caplog,
) -> None:
whitelist_conf["runmode"] = "backtest"
whitelist_conf["pairlists"] = pairlists
whitelist_conf["stake_currency"] = base_currency
@ -1270,6 +1271,7 @@ def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None:
{"method": "StaticPairList"},
{"method": "ShuffleFilter", "seed": 43},
]
whitelist_conf["runmode"] = "backtest"
exchange = get_patched_exchange(mocker, whitelist_conf)
plm = PairListManager(exchange, whitelist_conf)
@ -2306,7 +2308,7 @@ def test_MarketCapPairList_filter(
)
mocker.patch(
"freqtrade.plugins.pairlist.MarketCapPairList.CoinGeckoAPI.get_coins_markets",
"freqtrade.plugins.pairlist.MarketCapPairList.FtCoinGeckoApi.get_coins_markets",
return_value=test_value,
)
@ -2344,7 +2346,7 @@ def test_MarketCapPairList_timing(mocker, default_conf_usdt, markets, time_machi
)
mocker.patch(
"freqtrade.plugins.pairlist.MarketCapPairList.CoinGeckoAPI.get_coins_markets",
"freqtrade.plugins.pairlist.MarketCapPairList.FtCoinGeckoApi.get_coins_markets",
return_value=test_value,
)

View File

@ -8,11 +8,20 @@ import pytest
from requests.exceptions import RequestException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.util.coin_gecko import FtCoinGeckoApi
from tests.conftest import log_has, log_has_re
def test_fiat_convert_is_supported(mocker):
fiat_convert = CryptoToFiatConverter()
def test_fiat_convert_is_singleton():
fiat_convert = CryptoToFiatConverter({"a": 22})
fiat_convert2 = CryptoToFiatConverter({})
assert fiat_convert is fiat_convert2
assert id(fiat_convert) == id(fiat_convert2)
def test_fiat_convert_is_supported():
fiat_convert = CryptoToFiatConverter({})
assert fiat_convert._is_supported_fiat(fiat="USD") is True
assert fiat_convert._is_supported_fiat(fiat="usd") is True
assert fiat_convert._is_supported_fiat(fiat="abc") is False
@ -20,7 +29,7 @@ def test_fiat_convert_is_supported(mocker):
def test_fiat_convert_find_price(mocker):
fiat_convert = CryptoToFiatConverter()
fiat_convert = CryptoToFiatConverter({})
fiat_convert._coinlistings = {}
fiat_convert._backoff = 0
@ -48,7 +57,7 @@ def test_fiat_convert_find_price(mocker):
def test_fiat_convert_unsupported_crypto(mocker, caplog):
mocker.patch("freqtrade.rpc.fiat_convert.CryptoToFiatConverter._coinlistings", return_value=[])
fiat_convert = CryptoToFiatConverter()
fiat_convert = CryptoToFiatConverter({})
assert fiat_convert._find_price(crypto_symbol="CRYPTO_123", fiat_symbol="EUR") == 0.0
assert log_has("unsupported crypto-symbol CRYPTO_123 - returning 0.0", caplog)
@ -58,7 +67,7 @@ def test_fiat_convert_get_price(mocker):
"freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price", return_value=28000.0
)
fiat_convert = CryptoToFiatConverter()
fiat_convert = CryptoToFiatConverter({})
with pytest.raises(ValueError, match=r"The fiat us dollar is not supported."):
fiat_convert.get_price(crypto_symbol="btc", fiat_symbol="US Dollar")
@ -77,20 +86,20 @@ def test_fiat_convert_get_price(mocker):
assert find_price.call_count == 1
def test_fiat_convert_same_currencies(mocker):
fiat_convert = CryptoToFiatConverter()
def test_fiat_convert_same_currencies():
fiat_convert = CryptoToFiatConverter({})
assert fiat_convert.get_price(crypto_symbol="USD", fiat_symbol="USD") == 1.0
def test_fiat_convert_two_FIAT(mocker):
fiat_convert = CryptoToFiatConverter()
def test_fiat_convert_two_FIAT():
fiat_convert = CryptoToFiatConverter({})
assert fiat_convert.get_price(crypto_symbol="USD", fiat_symbol="EUR") == 0.0
def test_loadcryptomap(mocker):
fiat_convert = CryptoToFiatConverter()
def test_loadcryptomap():
fiat_convert = CryptoToFiatConverter({})
assert len(fiat_convert._coinlistings) == 2
assert fiat_convert._get_gecko_id("btc") == "bitcoin"
@ -100,28 +109,28 @@ def test_fiat_init_network_exception(mocker):
# Because CryptoToFiatConverter is a Singleton we reset the listings
listmock = MagicMock(side_effect=RequestException)
mocker.patch.multiple(
"freqtrade.rpc.fiat_convert.CoinGeckoAPI",
"freqtrade.rpc.fiat_convert.FtCoinGeckoApi",
get_coins_list=listmock,
)
# with pytest.raises(RequestEsxception):
fiat_convert = CryptoToFiatConverter()
fiat_convert = CryptoToFiatConverter({})
fiat_convert._coinlistings = {}
fiat_convert._load_cryptomap()
assert len(fiat_convert._coinlistings) == 0
def test_fiat_convert_without_network(mocker):
def test_fiat_convert_without_network():
# Because CryptoToFiatConverter is a Singleton we reset the value of _coingecko
fiat_convert = CryptoToFiatConverter()
fiat_convert = CryptoToFiatConverter({})
cmc_temp = CryptoToFiatConverter._coingecko
CryptoToFiatConverter._coingecko = None
cmc_temp = fiat_convert._coingecko
fiat_convert._coingecko = None
assert fiat_convert._coingecko is None
assert fiat_convert._find_price(crypto_symbol="btc", fiat_symbol="usd") == 0.0
CryptoToFiatConverter._coingecko = cmc_temp
fiat_convert._coingecko = cmc_temp
def test_fiat_too_many_requests_response(mocker, caplog):
@ -129,11 +138,11 @@ def test_fiat_too_many_requests_response(mocker, caplog):
req_exception = "429 Too Many Requests"
listmock = MagicMock(return_value="{}", side_effect=RequestException(req_exception))
mocker.patch.multiple(
"freqtrade.rpc.fiat_convert.CoinGeckoAPI",
"freqtrade.rpc.fiat_convert.FtCoinGeckoApi",
get_coins_list=listmock,
)
# with pytest.raises(RequestEsxception):
fiat_convert = CryptoToFiatConverter()
fiat_convert = CryptoToFiatConverter({})
fiat_convert._coinlistings = {}
fiat_convert._load_cryptomap()
@ -144,8 +153,8 @@ def test_fiat_too_many_requests_response(mocker, caplog):
)
def test_fiat_multiple_coins(mocker, caplog):
fiat_convert = CryptoToFiatConverter()
def test_fiat_multiple_coins(caplog):
fiat_convert = CryptoToFiatConverter({})
fiat_convert._coinlistings = [
{"id": "helium", "symbol": "hnt", "name": "Helium"},
{"id": "hymnode", "symbol": "hnt", "name": "Hymnode"},
@ -165,11 +174,11 @@ def test_fiat_invalid_response(mocker, caplog):
# Because CryptoToFiatConverter is a Singleton we reset the listings
listmock = MagicMock(return_value=None)
mocker.patch.multiple(
"freqtrade.rpc.fiat_convert.CoinGeckoAPI",
"freqtrade.rpc.fiat_convert.FtCoinGeckoApi",
get_coins_list=listmock,
)
# with pytest.raises(RequestEsxception):
fiat_convert = CryptoToFiatConverter()
fiat_convert = CryptoToFiatConverter({})
fiat_convert._coinlistings = []
fiat_convert._load_cryptomap()
@ -182,7 +191,7 @@ def test_fiat_invalid_response(mocker, caplog):
def test_convert_amount(mocker):
mocker.patch("freqtrade.rpc.fiat_convert.CryptoToFiatConverter.get_price", return_value=12345.0)
fiat_convert = CryptoToFiatConverter()
fiat_convert = CryptoToFiatConverter({})
result = fiat_convert.convert_amount(crypto_amount=1.23, crypto_symbol="BTC", fiat_symbol="USD")
assert result == 15184.35
@ -193,3 +202,18 @@ def test_convert_amount(mocker):
crypto_amount="1.23", crypto_symbol="BTC", fiat_symbol="BTC"
)
assert result == 1.23
def test_FtCoinGeckoApi():
ftc = FtCoinGeckoApi()
assert ftc._api_key == ""
assert ftc.api_base_url == "https://api.coingecko.com/api/v3/"
# defaults to demo
ftc = FtCoinGeckoApi(api_key="123456")
assert ftc._api_key == "123456"
assert ftc.api_base_url == "https://api.coingecko.com/api/v3/"
ftc = FtCoinGeckoApi(api_key="123456", is_demo=False)
assert ftc._api_key == "123456"
assert ftc.api_base_url == "https://pro-api.coingecko.com/api/v3/"

View File

@ -225,7 +225,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
mocker.patch.multiple(
"freqtrade.rpc.fiat_convert.CoinGeckoAPI",
"freqtrade.rpc.fiat_convert.FtCoinGeckoApi",
get_price=MagicMock(return_value={"bitcoin": {"usd": 15000.0}}),
)
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=15000.0)
@ -266,7 +266,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
assert "-0.00" == f"{fiat_profit_sum:.2f}"
# Test with fiat convert
rpc._fiat_converter = CryptoToFiatConverter()
rpc._fiat_converter = CryptoToFiatConverter({})
result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf["stake_currency"], "USD")
assert "Since" in headers
assert "Pair" in headers
@ -312,7 +312,7 @@ def test__rpc_timeunit_profit(
fiat_display_currency = default_conf_usdt["fiat_display_currency"]
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter()
rpc._fiat_converter = CryptoToFiatConverter({})
# Try valid data
days = rpc._rpc_timeunit_profit(7, stake_currency, fiat_display_currency)
@ -344,7 +344,7 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee, is_short):
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
create_mock_trades(fee, is_short)
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter()
rpc._fiat_converter = CryptoToFiatConverter({})
trades = rpc._rpc_trade_history(2)
assert len(trades["trades"]) == 2
assert trades["trades_count"] == 2
@ -434,7 +434,7 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
fiat_display_currency = default_conf_usdt["fiat_display_currency"]
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter()
rpc._fiat_converter = CryptoToFiatConverter({})
res = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert res["trade_count"] == 0
@ -495,7 +495,7 @@ def test_rpc_balance_handle_error(default_conf, mocker):
# ETH will be skipped due to mocked Error below
mocker.patch.multiple(
"freqtrade.rpc.fiat_convert.CoinGeckoAPI",
"freqtrade.rpc.fiat_convert.FtCoinGeckoApi",
get_price=MagicMock(return_value={"bitcoin": {"usd": 15000.0}}),
)
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=15000.0)
@ -509,7 +509,7 @@ def test_rpc_balance_handle_error(default_conf, mocker):
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter()
rpc._fiat_converter = CryptoToFiatConverter({})
with pytest.raises(RPCException, match="Error getting current tickers."):
rpc._rpc_balance(default_conf["stake_currency"], default_conf["fiat_display_currency"])
@ -558,7 +558,7 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
]
mocker.patch.multiple(
"freqtrade.rpc.fiat_convert.CoinGeckoAPI",
"freqtrade.rpc.fiat_convert.FtCoinGeckoApi",
get_price=MagicMock(return_value={"bitcoin": {"usd": 1.2}}),
)
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=1.2)
@ -578,7 +578,7 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter()
rpc._fiat_converter = CryptoToFiatConverter({})
result = rpc._rpc_balance(
default_conf_usdt["stake_currency"], default_conf_usdt["fiat_display_currency"]

View File

@ -2564,10 +2564,14 @@ def test_send_msg_buy_notification_no_fiat(
("Short", "short_signal_01", 2.0),
],
)
@pytest.mark.parametrize("fiat", ["", None])
def test_send_msg_exit_notification_no_fiat(
default_conf, mocker, direction, enter_signal, leverage, time_machine
default_conf, mocker, direction, enter_signal, leverage, time_machine, fiat
) -> None:
del default_conf["fiat_display_currency"]
if fiat is None:
del default_conf["fiat_display_currency"]
else:
default_conf["fiat_display_currency"] = fiat
time_machine.move_to("2022-05-02 00:00:00 +00:00", tick=False)
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)

177
tests/setup.Tests.ps1 Normal file
View File

@ -0,0 +1,177 @@
Describe "Setup and Tests" {
BeforeAll {
# Setup variables
$SetupScriptPath = Join-Path $PSScriptRoot "..\setup.ps1"
$Global:LogFilePath = Join-Path $env:TEMP "script_log.txt"
# Check if the setup script exists
if (-Not (Test-Path -Path $SetupScriptPath)) {
Write-Host "Error: setup.ps1 script not found at path: $SetupScriptPath"
exit 1
}
# Mock main to prevent it from running
Mock Main {}
. $SetupScriptPath
}
Context "Write-Log Tests" -Tag "Unit" {
It "should write INFO level log" {
if (Test-Path $Global:LogFilePath){
Remove-Item $Global:LogFilePath -ErrorAction SilentlyContinue
}
Write-Log -Message "Test Info Message" -Level "INFO"
$Global:LogFilePath | Should -Exist
$LogContent = Get-Content $Global:LogFilePath
$LogContent | Should -Contain "INFO: Test Info Message"
}
It "should write ERROR level log" {
if (Test-Path $Global:LogFilePath){
Remove-Item $Global:LogFilePath -ErrorAction SilentlyContinue
}
Write-Log -Message "Test Error Message" -Level "ERROR"
$Global:LogFilePath | Should -Exist
$LogContent = Get-Content $Global:LogFilePath
$LogContent | Should -Contain "ERROR: Test Error Message"
}
}
Describe "Get-UserSelection Tests" {
Context "Valid input" {
It "Should return the correct index for a valid single selection" {
$Options = @("Option1", "Option2", "Option3")
Mock Read-Host { return "B" }
$Result = Get-UserSelection -prompt "Select an option" -options $Options
$Result | Should -Be 1
}
It "Should return the correct index for a valid single selection" {
$Options = @("Option1", "Option2", "Option3")
Mock Read-Host { return "b" }
$Result = Get-UserSelection -prompt "Select an option" -options $Options
$Result | Should -Be 1
}
It "Should return the default choice when no input is provided" {
$Options = @("Option1", "Option2", "Option3")
Mock Read-Host { return "" }
$Result = Get-UserSelection -prompt "Select an option" -options $Options -defaultChoice "C"
$Result | Should -Be 2
}
}
Context "Invalid input" {
It "Should return -1 for an invalid letter selection" {
$Options = @("Option1", "Option2", "Option3")
Mock Read-Host { return "X" }
$Result = Get-UserSelection -prompt "Select an option" -options $Options
$Result | Should -Be -1
}
It "Should return -1 for a selection outside the valid range" {
$Options = @("Option1", "Option2", "Option3")
Mock Read-Host { return "D" }
$Result = Get-UserSelection -prompt "Select an option" -options $Options
$Result | Should -Be -1
}
It "Should return -1 for a non-letter input" {
$Options = @("Option1", "Option2", "Option3")
Mock Read-Host { return "1" }
$Result = Get-UserSelection -prompt "Select an option" -options $Options
$Result | Should -Be -1
}
It "Should return -1 for mixed valid and invalid input" {
Mock Read-Host { return "A,X,B,Y,C,Z" }
$Options = @("Option1", "Option2", "Option3")
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
$Indices | Should -Be -1
}
}
Context "Multiple selections" {
It "Should handle valid input correctly" {
Mock Read-Host { return "A, B, C" }
$Options = @("Option1", "Option2", "Option3")
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
$Indices | Should -Be @(0, 1, 2)
}
It "Should handle valid input without whitespace correctly" {
Mock Read-Host { return "A,B,C" }
$Options = @("Option1", "Option2", "Option3")
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
$Indices | Should -Be @(0, 1, 2)
}
It "Should return indices for selected options" {
Mock Read-Host { return "a,b" }
$Options = @("Option1", "Option2", "Option3")
$Indices = Get-UserSelection -prompt "Select options" -options $Options
$Indices | Should -Be @(0, 1)
}
It "Should return default choice if no input" {
Mock Read-Host { return "" }
$Options = @("Option1", "Option2", "Option3")
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "C"
$Indices | Should -Be @(2)
}
It "Should handle invalid input gracefully" {
Mock Read-Host { return "x,y,z" }
$Options = @("Option1", "Option2", "Option3")
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
$Indices | Should -Be -1
}
It "Should handle input without whitespace" {
Mock Read-Host { return "a,b,c" }
$Options = @("Option1", "Option2", "Option3")
$Indices = Get-UserSelection -prompt "Select options" -options $Options
$Indices | Should -Be @(0, 1, 2)
}
}
}
Describe "Exit-Script Tests" -Tag "Unit" {
BeforeEach {
Mock Write-Log {}
Mock Start-Process {}
Mock Read-Host { return "Y" }
}
It "should exit with the given exit code without waiting for key press" {
$ExitCode = Exit-Script -ExitCode 0 -isSubShell $true -waitForKeypress $false
$ExitCode | Should -Be 0
}
It "should prompt to open log file on error" {
Exit-Script -ExitCode 1 -isSubShell $true -waitForKeypress $false
Assert-MockCalled Read-Host -Exactly 1
Assert-MockCalled Start-Process -Exactly 1
}
}
Context 'Find-PythonExecutable' {
It 'Returns the first valid Python executable' {
Mock Test-PythonExecutable { $true } -ParameterFilter { $PythonExecutable -eq 'python' }
$Result = Find-PythonExecutable
$Result | Should -Be 'python'
}
It 'Returns null if no valid Python executable is found' {
Mock Test-PythonExecutable { $false }
$Result = Find-PythonExecutable
$Result | Should -Be $null
}
}
}

View File

@ -52,7 +52,7 @@ class HyperoptableStrategy(StrategyTestV3):
bot_loop_started = False
bot_started = False
def bot_loop_start(self):
def bot_loop_start(self, **kwargs):
self.bot_loop_started = True
def bot_start(self, **kwargs) -> None:

View File

@ -48,7 +48,7 @@ class HyperoptableStrategyV2(StrategyTestV2):
bot_loop_started = False
def bot_loop_start(self):
def bot_loop_start(self, **kwargs):
self.bot_loop_started = True
def bot_start(self, **kwargs) -> None:

View File

@ -659,6 +659,16 @@ def test_validate_default_conf(default_conf) -> None:
validate_config_schema(default_conf)
@pytest.mark.parametrize("fiat", ["EUR", "USD", "", None])
def test_validate_fiat_currency_options(default_conf, fiat) -> None:
# Validate via our validator - we allow setting defaults!
if fiat is not None:
default_conf["fiat_display_currency"] = fiat
else:
del default_conf["fiat_display_currency"]
validate_config_schema(default_conf)
def test_validate_max_open_trades(default_conf):
default_conf["max_open_trades"] = float("inf")
default_conf["stake_amount"] = "unlimited"