Merge branch 'develop' into feature/fetch-public-trades
This commit is contained in:
@@ -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
|
||||
|
||||
+21
-10
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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
|
||||
|
||||
+27
-2
@@ -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
|
||||
|
||||
|
||||
+19
-15
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2024.5-dev"
|
||||
__version__ = "2024.6-dev"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -142,6 +142,7 @@ ARGS_DOWNLOAD_DATA = [
|
||||
"include_inactive",
|
||||
"timerange",
|
||||
"download_trades",
|
||||
"convert_trades",
|
||||
"exchange",
|
||||
"timeframes",
|
||||
"erase",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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(
|
||||
|
||||
+22
-10
@@ -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
|
||||
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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
@@ -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"],
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -53,6 +53,7 @@ MAP_EXCHANGE_CHILDCLASS = {
|
||||
|
||||
SUPPORTED_EXCHANGES = [
|
||||
"binance",
|
||||
"bingx",
|
||||
"bitmart",
|
||||
"gate",
|
||||
"htx",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = [":"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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: (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,"
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
+12
-23
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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})
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
+4
-1
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+11
-9
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+2
-1000
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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": "",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user