mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 02:12:01 +00:00
Merge branch 'develop' into feature/fetch-public-trades
This commit is contained in:
commit
50bf770351
|
@ -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
|
||||
|
|
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
|
@ -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 }}
|
||||
|
|
5
.github/workflows/pre-commit-update.yml
vendored
5
.github/workflows/pre-commit-update.yml
vendored
|
@ -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.
BIN
build_helpers/TA_Lib-0.4.30-cp310-cp310-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.30-cp310-cp310-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.30-cp311-cp311-linux_armv7l.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.30-cp311-cp311-linux_armv7l.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.30-cp311-cp311-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.30-cp311-cp311-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.30-cp312-cp312-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.30-cp312-cp312-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.30-cp39-cp39-linux_armv7l.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.30-cp39-cp39-linux_armv7l.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.30-cp39-cp39-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.30-cp39-cp39-win_amd64.whl
Normal file
Binary file not shown.
Binary file not shown.
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
26
freqtrade/util/coin_gecko.py
Normal file
26
freqtrade/util/coin_gecko.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from pycoingecko import CoinGeckoAPI
|
||||
|
||||
|
||||
class FtCoinGeckoApi(CoinGeckoAPI):
|
||||
"""
|
||||
Simple wrapper around pycoingecko's api to support Demo API keys.
|
||||
|
||||
"""
|
||||
|
||||
__API_URL_BASE = "https://api.coingecko.com/api/v3/"
|
||||
__PRO_API_URL_BASE = "https://pro-api.coingecko.com/api/v3/"
|
||||
_api_key: str = ""
|
||||
|
||||
def __init__(self, api_key: str = "", *, is_demo=True, retries=5):
|
||||
super().__init__(retries=retries)
|
||||
# Doint' pass api_key to parent, instead set the header on the session directly
|
||||
self._api_key = api_key
|
||||
|
||||
if api_key and not is_demo:
|
||||
self.api_base_url = self.__PRO_API_URL_BASE
|
||||
self.session.params.update({"x_cg_pro_api_key": api_key})
|
||||
else:
|
||||
# Use demo api key
|
||||
self.api_base_url = self.__API_URL_BASE
|
||||
if api_key:
|
||||
self.session.params.update({"x_cg_demo_api_key": api_key})
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
numpy==1.26.4
|
||||
pandas==2.2.2
|
||||
bottleneck==1.3.8
|
||||
numexpr==2.10.0
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==4.3.21
|
||||
ccxt==4.3.38
|
||||
cryptography==42.0.7
|
||||
aiohttp==3.9.5
|
||||
SQLAlchemy==2.0.30
|
||||
python-telegram-bot==21.1.1
|
||||
python-telegram-bot==21.2
|
||||
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
||||
httpx>=0.24.1
|
||||
humanize==4.9.0
|
||||
cachetools==5.3.3
|
||||
requests==2.31.0
|
||||
requests==2.32.3
|
||||
urllib3==2.2.1
|
||||
jsonschema==4.22.0
|
||||
TA-Lib==0.4.28
|
||||
TA-Lib==0.4.30
|
||||
technical==1.4.3
|
||||
tabulate==0.9.0
|
||||
pycoingecko==3.1.0
|
||||
|
@ -22,13 +24,13 @@ jinja2==3.1.4
|
|||
tables==3.9.1
|
||||
joblib==1.4.2
|
||||
rich==13.7.1
|
||||
pyarrow==16.0.0; platform_machine != 'armv7l'
|
||||
pyarrow==16.1.0; platform_machine != 'armv7l'
|
||||
|
||||
# find first, C search in arrays
|
||||
py_find_1st==1.1.6
|
||||
|
||||
# Load ticker files 30% faster
|
||||
python-rapidjson==1.16
|
||||
python-rapidjson==1.17
|
||||
# Properly format api responses
|
||||
orjson==3.10.3
|
||||
|
||||
|
@ -37,8 +39,8 @@ sdnotify==0.3.2
|
|||
|
||||
# API Server
|
||||
fastapi==0.111.0
|
||||
pydantic==2.7.1
|
||||
uvicorn==0.29.0
|
||||
pydantic==2.7.2
|
||||
uvicorn==0.30.1
|
||||
pyjwt==2.8.0
|
||||
aiofiles==23.2.1
|
||||
psutil==5.9.8
|
||||
|
@ -53,7 +55,7 @@ python-dateutil==2.9.0.post0
|
|||
pytz==2024.1
|
||||
|
||||
#Futures
|
||||
schedule==1.2.1
|
||||
schedule==1.2.2
|
||||
|
||||
#WS Messages
|
||||
websockets==12.0
|
||||
|
|
285
setup.ps1
Normal file
285
setup.ps1
Normal file
|
@ -0,0 +1,285 @@
|
|||
Clear-Host
|
||||
|
||||
$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
$Global:LogFilePath = Join-Path $env:TEMP "script_log_$Timestamp.txt"
|
||||
|
||||
$RequirementFiles = @("requirements.txt", "requirements-dev.txt", "requirements-hyperopt.txt", "requirements-freqai.txt", "requirements-freqai-rl.txt", "requirements-plot.txt")
|
||||
$VenvName = ".venv"
|
||||
$VenvDir = Join-Path $PSScriptRoot $VenvName
|
||||
|
||||
function Write-Log {
|
||||
param (
|
||||
[string]$Message,
|
||||
[string]$Level = 'INFO'
|
||||
)
|
||||
|
||||
if (-not (Test-Path -Path $LogFilePath)) {
|
||||
New-Item -ItemType File -Path $LogFilePath -Force | Out-Null
|
||||
}
|
||||
|
||||
switch ($Level) {
|
||||
'INFO' { Write-Host $Message -ForegroundColor Green }
|
||||
'WARNING' { Write-Host $Message -ForegroundColor Yellow }
|
||||
'ERROR' { Write-Host $Message -ForegroundColor Red }
|
||||
'PROMPT' { Write-Host $Message -ForegroundColor Cyan }
|
||||
}
|
||||
|
||||
"${Level}: $Message" | Out-File $LogFilePath -Append
|
||||
}
|
||||
|
||||
function Get-UserSelection {
|
||||
param (
|
||||
[string]$Prompt,
|
||||
[string[]]$Options,
|
||||
[string]$DefaultChoice = 'A',
|
||||
[bool]$AllowMultipleSelections = $true
|
||||
)
|
||||
|
||||
Write-Log "$Prompt`n" -Level 'PROMPT'
|
||||
for ($I = 0; $I -lt $Options.Length; $I++) {
|
||||
Write-Log "$([char](65 + $I)). $($Options[$I])" -Level 'PROMPT'
|
||||
}
|
||||
|
||||
if ($AllowMultipleSelections) {
|
||||
Write-Log "`nSelect one or more options by typing the corresponding letters, separated by commas." -Level 'PROMPT'
|
||||
}
|
||||
else {
|
||||
Write-Log "`nSelect an option by typing the corresponding letter." -Level 'PROMPT'
|
||||
}
|
||||
|
||||
[string]$UserInput = Read-Host
|
||||
if ([string]::IsNullOrEmpty($UserInput)) {
|
||||
$UserInput = $DefaultChoice
|
||||
}
|
||||
$UserInput = $UserInput.ToUpper()
|
||||
|
||||
if ($AllowMultipleSelections) {
|
||||
$Selections = $UserInput.Split(',') | ForEach-Object { $_.Trim() }
|
||||
$SelectedIndices = @()
|
||||
foreach ($Selection in $Selections) {
|
||||
if ($Selection -match '^[A-Z]$') {
|
||||
$Index = [int][char]$Selection - [int][char]'A'
|
||||
if ($Index -ge 0 -and $Index -lt $Options.Length) {
|
||||
$SelectedIndices += $Index
|
||||
}
|
||||
else {
|
||||
Write-Log "Invalid input: $Selection. Please enter letters within the valid range of options." -Level 'ERROR'
|
||||
return -1
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Log "Invalid input: $Selection. Please enter a letter between A and Z." -Level 'ERROR'
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return $SelectedIndices
|
||||
}
|
||||
else {
|
||||
if ($UserInput -match '^[A-Z]$') {
|
||||
$SelectedIndex = [int][char]$UserInput - [int][char]'A'
|
||||
if ($SelectedIndex -ge 0 -and $SelectedIndex -lt $Options.Length) {
|
||||
return $SelectedIndex
|
||||
}
|
||||
else {
|
||||
Write-Log "Invalid input: $UserInput. Please enter a letter within the valid range of options." -Level 'ERROR'
|
||||
return -1
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Log "Invalid input: $UserInput. Please enter a letter between A and Z." -Level 'ERROR'
|
||||
return -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Exit-Script {
|
||||
param (
|
||||
[int]$ExitCode,
|
||||
[bool]$WaitForKeypress = $true
|
||||
)
|
||||
|
||||
if ($ExitCode -ne 0) {
|
||||
Write-Log "Script failed. Would you like to open the log file? (Y/N)" -Level 'PROMPT'
|
||||
$openLog = Read-Host
|
||||
if ($openLog -eq 'Y' -or $openLog -eq 'y') {
|
||||
Start-Process notepad.exe -ArgumentList $LogFilePath
|
||||
}
|
||||
}
|
||||
elseif ($WaitForKeypress) {
|
||||
Write-Log "Press any key to exit..."
|
||||
$host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") | Out-Null
|
||||
}
|
||||
|
||||
return $ExitCode
|
||||
}
|
||||
|
||||
function Test-PythonExecutable {
|
||||
param(
|
||||
[string]$PythonExecutable
|
||||
)
|
||||
|
||||
$DeactivateVenv = Join-Path $VenvDir "Scripts\Deactivate.bat"
|
||||
if (Test-Path $DeactivateVenv) {
|
||||
Write-Host "Deactivating virtual environment..." 2>&1 | Out-File $LogFilePath -Append
|
||||
& $DeactivateVenv
|
||||
Write-Host "Virtual environment deactivated." 2>&1 | Out-File $LogFilePath -Append
|
||||
}
|
||||
else {
|
||||
Write-Host "Deactivation script not found: $DeactivateVenv" 2>&1 | Out-File $LogFilePath -Append
|
||||
}
|
||||
|
||||
$PythonCmd = Get-Command $PythonExecutable -ErrorAction SilentlyContinue
|
||||
if ($PythonCmd) {
|
||||
$VersionOutput = & $PythonCmd.Source --version 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$Version = $VersionOutput | Select-String -Pattern "Python (\d+\.\d+\.\d+)" | ForEach-Object { $_.Matches.Groups[1].Value }
|
||||
Write-Log "Python version $Version found using executable '$PythonExecutable'."
|
||||
return $true
|
||||
}
|
||||
else {
|
||||
Write-Log "Python executable '$PythonExecutable' not working correctly." -Level 'ERROR'
|
||||
return $false
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Log "Python executable '$PythonExecutable' not found." -Level 'ERROR'
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Find-PythonExecutable {
|
||||
$PythonExecutables = @(
|
||||
"python",
|
||||
"python3.12",
|
||||
"python3.11",
|
||||
"python3.10",
|
||||
"python3.9",
|
||||
"python3",
|
||||
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python312\python.exe",
|
||||
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python311\python.exe",
|
||||
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python310\python.exe",
|
||||
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python39\python.exe",
|
||||
"C:\Python312\python.exe",
|
||||
"C:\Python311\python.exe",
|
||||
"C:\Python310\python.exe",
|
||||
"C:\Python39\python.exe"
|
||||
)
|
||||
|
||||
|
||||
foreach ($Executable in $PythonExecutables) {
|
||||
if (Test-PythonExecutable -PythonExecutable $Executable) {
|
||||
return $Executable
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
function Main {
|
||||
"Starting the operations..." | Out-File $LogFilePath -Append
|
||||
"Current directory: $(Get-Location)" | Out-File $LogFilePath -Append
|
||||
|
||||
# Exit on lower versions than Python 3.9 or when Python executable not found
|
||||
$PythonExecutable = Find-PythonExecutable
|
||||
if ($null -eq $PythonExecutable) {
|
||||
Write-Log "No suitable Python executable found. Please ensure that Python 3.9 or higher is installed and available in the system PATH." -Level 'ERROR'
|
||||
Exit 1
|
||||
}
|
||||
|
||||
# Define the path to the Python executable in the virtual environment
|
||||
$ActivateVenv = "$VenvDir\Scripts\Activate.ps1"
|
||||
|
||||
# Check if the virtual environment exists, if not, create it
|
||||
if (-Not (Test-Path $ActivateVenv)) {
|
||||
Write-Log "Virtual environment not found. Creating virtual environment..." -Level 'ERROR'
|
||||
& $PythonExecutable -m venv $VenvName 2>&1 | Out-File $LogFilePath -Append
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Log "Failed to create virtual environment." -Level 'ERROR'
|
||||
Exit-Script -exitCode 1
|
||||
}
|
||||
else {
|
||||
Write-Log "Virtual environment created."
|
||||
}
|
||||
}
|
||||
|
||||
# Activate the virtual environment and check if it was successful
|
||||
Write-Log "Virtual environment found. Activating virtual environment..."
|
||||
& $ActivateVenv 2>&1 | Out-File $LogFilePath -Append
|
||||
# Check if virtual environment is activated
|
||||
if ($env:VIRTUAL_ENV) {
|
||||
Write-Log "Virtual environment is activated at: $($env:VIRTUAL_ENV)"
|
||||
}
|
||||
else {
|
||||
Write-Log "Failed to activate virtual environment." -Level 'ERROR'
|
||||
Exit-Script -exitCode 1
|
||||
}
|
||||
|
||||
# Ensure pip
|
||||
python -m ensurepip --default-pip 2>&1 | Out-File $LogFilePath -Append
|
||||
|
||||
# Pull latest updates only if the repository state is not dirty
|
||||
Write-Log "Checking if the repository is clean..."
|
||||
$Status = & "git" status --porcelain
|
||||
if ($Status) {
|
||||
Write-Log "Changes in local git repository. Skipping git pull."
|
||||
}
|
||||
else {
|
||||
Write-Log "Pulling latest updates..."
|
||||
& "git" pull 2>&1 | Out-File $LogFilePath -Append
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Log "Failed to pull updates from Git." -Level 'ERROR'
|
||||
Exit-Script -exitCode 1
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path "$VenvDir\Lib\site-packages\talib")) {
|
||||
# Install TA-Lib using the virtual environment's pip
|
||||
Write-Log "Installing TA-Lib using virtual environment's pip..."
|
||||
python -m pip install --find-links=build_helpers\ --prefer-binary TA-Lib 2>&1 | Out-File $LogFilePath -Append
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Log "Failed to install TA-Lib." -Level 'ERROR'
|
||||
Exit-Script -exitCode 1
|
||||
}
|
||||
}
|
||||
|
||||
# Present options for requirement files
|
||||
$SelectedIndices = Get-UserSelection -prompt "Select which requirement files to install:" -options $RequirementFiles -defaultChoice 'A'
|
||||
|
||||
# Cache the selected requirement files
|
||||
$SelectedRequirementFiles = @()
|
||||
$PipInstallArguments = @()
|
||||
foreach ($Index in $SelectedIndices) {
|
||||
$RelativePath = $RequirementFiles[$Index]
|
||||
if (Test-Path $RelativePath) {
|
||||
$SelectedRequirementFiles += $RelativePath
|
||||
$PipInstallArguments += "-r", $RelativePath # Add each flag and path as separate elements
|
||||
}
|
||||
else {
|
||||
Write-Log "Requirement file not found: $RelativePath" -Level 'ERROR'
|
||||
Exit-Script -exitCode 1
|
||||
}
|
||||
}
|
||||
if ($PipInstallArguments.Count -ne 0) {
|
||||
& pip install @PipInstallArguments # Use array splatting to pass arguments correctly
|
||||
}
|
||||
|
||||
# Install freqtrade from setup using the virtual environment's Python
|
||||
Write-Log "Installing freqtrade from setup..."
|
||||
pip install -e . 2>&1 | Out-File $LogFilePath -Append
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Log "Failed to install freqtrade." -Level 'ERROR'
|
||||
Exit-Script -exitCode 1
|
||||
}
|
||||
|
||||
Write-Log "Installing freqUI..."
|
||||
python freqtrade install-ui 2>&1 | Out-File $LogFilePath -Append
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Log "Failed to install freqUI." -Level 'ERROR'
|
||||
Exit-Script -exitCode 1
|
||||
}
|
||||
|
||||
Write-Log "Installation/Update complete!"
|
||||
Exit-Script -exitCode 0
|
||||
}
|
||||
|
||||
# Call the Main function
|
||||
Main
|
2
setup.py
2
setup.py
|
@ -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",
|
||||
|
|
4
setup.sh
4
setup.sh
|
@ -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,
|
||||
|
|
1002
tests/conftest.py
1002
tests/conftest.py
File diff suppressed because it is too large
Load Diff
1000
tests/conftest_hyperopt.py
Normal file
1000
tests/conftest_hyperopt.py
Normal file
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)
|
||||
|
||||
|
|
177
tests/setup.Tests.ps1
Normal file
177
tests/setup.Tests.ps1
Normal file
|
@ -0,0 +1,177 @@
|
|||
|
||||
Describe "Setup and Tests" {
|
||||
BeforeAll {
|
||||
# Setup variables
|
||||
$SetupScriptPath = Join-Path $PSScriptRoot "..\setup.ps1"
|
||||
$Global:LogFilePath = Join-Path $env:TEMP "script_log.txt"
|
||||
|
||||
# Check if the setup script exists
|
||||
if (-Not (Test-Path -Path $SetupScriptPath)) {
|
||||
Write-Host "Error: setup.ps1 script not found at path: $SetupScriptPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Mock main to prevent it from running
|
||||
Mock Main {}
|
||||
|
||||
. $SetupScriptPath
|
||||
}
|
||||
|
||||
Context "Write-Log Tests" -Tag "Unit" {
|
||||
It "should write INFO level log" {
|
||||
if (Test-Path $Global:LogFilePath){
|
||||
Remove-Item $Global:LogFilePath -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Log -Message "Test Info Message" -Level "INFO"
|
||||
$Global:LogFilePath | Should -Exist
|
||||
|
||||
$LogContent = Get-Content $Global:LogFilePath
|
||||
$LogContent | Should -Contain "INFO: Test Info Message"
|
||||
}
|
||||
|
||||
It "should write ERROR level log" {
|
||||
if (Test-Path $Global:LogFilePath){
|
||||
Remove-Item $Global:LogFilePath -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Log -Message "Test Error Message" -Level "ERROR"
|
||||
$Global:LogFilePath | Should -Exist
|
||||
|
||||
$LogContent = Get-Content $Global:LogFilePath
|
||||
$LogContent | Should -Contain "ERROR: Test Error Message"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Get-UserSelection Tests" {
|
||||
Context "Valid input" {
|
||||
It "Should return the correct index for a valid single selection" {
|
||||
$Options = @("Option1", "Option2", "Option3")
|
||||
Mock Read-Host { return "B" }
|
||||
$Result = Get-UserSelection -prompt "Select an option" -options $Options
|
||||
$Result | Should -Be 1
|
||||
}
|
||||
|
||||
It "Should return the correct index for a valid single selection" {
|
||||
$Options = @("Option1", "Option2", "Option3")
|
||||
Mock Read-Host { return "b" }
|
||||
$Result = Get-UserSelection -prompt "Select an option" -options $Options
|
||||
$Result | Should -Be 1
|
||||
}
|
||||
|
||||
It "Should return the default choice when no input is provided" {
|
||||
$Options = @("Option1", "Option2", "Option3")
|
||||
Mock Read-Host { return "" }
|
||||
$Result = Get-UserSelection -prompt "Select an option" -options $Options -defaultChoice "C"
|
||||
$Result | Should -Be 2
|
||||
}
|
||||
}
|
||||
|
||||
Context "Invalid input" {
|
||||
It "Should return -1 for an invalid letter selection" {
|
||||
$Options = @("Option1", "Option2", "Option3")
|
||||
Mock Read-Host { return "X" }
|
||||
$Result = Get-UserSelection -prompt "Select an option" -options $Options
|
||||
$Result | Should -Be -1
|
||||
}
|
||||
|
||||
It "Should return -1 for a selection outside the valid range" {
|
||||
$Options = @("Option1", "Option2", "Option3")
|
||||
Mock Read-Host { return "D" }
|
||||
$Result = Get-UserSelection -prompt "Select an option" -options $Options
|
||||
$Result | Should -Be -1
|
||||
}
|
||||
|
||||
It "Should return -1 for a non-letter input" {
|
||||
$Options = @("Option1", "Option2", "Option3")
|
||||
Mock Read-Host { return "1" }
|
||||
$Result = Get-UserSelection -prompt "Select an option" -options $Options
|
||||
$Result | Should -Be -1
|
||||
}
|
||||
|
||||
It "Should return -1 for mixed valid and invalid input" {
|
||||
Mock Read-Host { return "A,X,B,Y,C,Z" }
|
||||
$Options = @("Option1", "Option2", "Option3")
|
||||
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
|
||||
$Indices | Should -Be -1
|
||||
}
|
||||
}
|
||||
|
||||
Context "Multiple selections" {
|
||||
It "Should handle valid input correctly" {
|
||||
Mock Read-Host { return "A, B, C" }
|
||||
$Options = @("Option1", "Option2", "Option3")
|
||||
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
|
||||
$Indices | Should -Be @(0, 1, 2)
|
||||
}
|
||||
|
||||
It "Should handle valid input without whitespace correctly" {
|
||||
Mock Read-Host { return "A,B,C" }
|
||||
$Options = @("Option1", "Option2", "Option3")
|
||||
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
|
||||
$Indices | Should -Be @(0, 1, 2)
|
||||
}
|
||||
|
||||
It "Should return indices for selected options" {
|
||||
Mock Read-Host { return "a,b" }
|
||||
$Options = @("Option1", "Option2", "Option3")
|
||||
$Indices = Get-UserSelection -prompt "Select options" -options $Options
|
||||
$Indices | Should -Be @(0, 1)
|
||||
}
|
||||
|
||||
It "Should return default choice if no input" {
|
||||
Mock Read-Host { return "" }
|
||||
$Options = @("Option1", "Option2", "Option3")
|
||||
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "C"
|
||||
$Indices | Should -Be @(2)
|
||||
}
|
||||
|
||||
It "Should handle invalid input gracefully" {
|
||||
Mock Read-Host { return "x,y,z" }
|
||||
$Options = @("Option1", "Option2", "Option3")
|
||||
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
|
||||
$Indices | Should -Be -1
|
||||
}
|
||||
|
||||
It "Should handle input without whitespace" {
|
||||
Mock Read-Host { return "a,b,c" }
|
||||
$Options = @("Option1", "Option2", "Option3")
|
||||
$Indices = Get-UserSelection -prompt "Select options" -options $Options
|
||||
$Indices | Should -Be @(0, 1, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Exit-Script Tests" -Tag "Unit" {
|
||||
BeforeEach {
|
||||
Mock Write-Log {}
|
||||
Mock Start-Process {}
|
||||
Mock Read-Host { return "Y" }
|
||||
}
|
||||
|
||||
It "should exit with the given exit code without waiting for key press" {
|
||||
$ExitCode = Exit-Script -ExitCode 0 -isSubShell $true -waitForKeypress $false
|
||||
$ExitCode | Should -Be 0
|
||||
}
|
||||
|
||||
It "should prompt to open log file on error" {
|
||||
Exit-Script -ExitCode 1 -isSubShell $true -waitForKeypress $false
|
||||
Assert-MockCalled Read-Host -Exactly 1
|
||||
Assert-MockCalled Start-Process -Exactly 1
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Find-PythonExecutable' {
|
||||
It 'Returns the first valid Python executable' {
|
||||
Mock Test-PythonExecutable { $true } -ParameterFilter { $PythonExecutable -eq 'python' }
|
||||
$Result = Find-PythonExecutable
|
||||
$Result | Should -Be 'python'
|
||||
}
|
||||
|
||||
It 'Returns null if no valid Python executable is found' {
|
||||
Mock Test-PythonExecutable { $false }
|
||||
$Result = Find-PythonExecutable
|
||||
$Result | Should -Be $null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue
Block a user