Merge pull request #9607 from freqtrade/new_release

New release 2023.12
This commit is contained in:
Matthias
2023-12-30 08:49:40 +01:00
committed by GitHub
81 changed files with 3686 additions and 923 deletions
+87 -52
View File
@@ -31,7 +31,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@@ -44,7 +44,6 @@ jobs:
- name: pip cache (linux) - name: pip cache (linux)
uses: actions/cache@v3 uses: actions/cache@v3
if: runner.os == 'Linux'
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
@@ -55,7 +54,6 @@ jobs:
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd .. cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
- name: Installation - *nix - name: Installation - *nix
if: runner.os == 'Linux'
run: | run: |
python -m pip install --upgrade pip wheel python -m pip install --upgrade pip wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
@@ -122,18 +120,18 @@ jobs:
details: Freqtrade CI failed on ${{ matrix.os }} details: Freqtrade CI failed on ${{ matrix.os }}
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
build_macos: build-macos:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ macos-latest ] os: [ "macos-latest", "macos-13" ]
python-version: ["3.9", "3.10", "3.11"] python-version: ["3.9", "3.10", "3.11"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@@ -143,14 +141,13 @@ jobs:
id: cache id: cache
with: with:
path: ~/dependencies/ path: ~/dependencies/
key: ${{ runner.os }}-dependencies key: ${{ matrix.os }}-dependencies
- name: pip cache (macOS) - name: pip cache (macOS)
uses: actions/cache@v3 uses: actions/cache@v3
if: runner.os == 'macOS'
with: with:
path: ~/Library/Caches/pip path: ~/Library/Caches/pip
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip key: ${{ matrix.os }}-${{ matrix.python-version }}-pip
- name: TA binary *nix - name: TA binary *nix
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
@@ -158,7 +155,6 @@ jobs:
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd .. cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
- name: Installation - macOS - name: Installation - macOS
if: runner.os == 'macOS'
run: | run: |
# brew update # brew update
# TODO: Should be the brew upgrade # TODO: Should be the brew upgrade
@@ -166,16 +162,21 @@ jobs:
# https://github.com/actions/runner-images/issues/6817 # https://github.com/actions/runner-images/issues/6817
rm /usr/local/bin/2to3 || true rm /usr/local/bin/2to3 || true
rm /usr/local/bin/2to3-3.11 || true rm /usr/local/bin/2to3-3.11 || true
rm /usr/local/bin/2to3-3.12 || true
rm /usr/local/bin/idle3 || true rm /usr/local/bin/idle3 || true
rm /usr/local/bin/idle3.11 || true rm /usr/local/bin/idle3.11 || true
rm /usr/local/bin/idle3.12 || true
rm /usr/local/bin/pydoc3 || true rm /usr/local/bin/pydoc3 || true
rm /usr/local/bin/pydoc3.11 || true rm /usr/local/bin/pydoc3.11 || true
rm /usr/local/bin/pydoc3.12 || true
rm /usr/local/bin/python3 || true rm /usr/local/bin/python3 || true
rm /usr/local/bin/python3.11 || true rm /usr/local/bin/python3.11 || true
rm /usr/local/bin/python3.12 || true
rm /usr/local/bin/python3-config || true rm /usr/local/bin/python3-config || true
rm /usr/local/bin/python3.11-config || true rm /usr/local/bin/python3.11-config || true
rm /usr/local/bin/python3.12-config || true
brew install hdf5 c-blosc brew install hdf5 c-blosc libomp
python -m pip install --upgrade pip wheel python -m pip install --upgrade pip wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_LIBRARY_PATH=${HOME}/dependencies/lib
@@ -231,7 +232,7 @@ jobs:
details: Test Succeeded! details: Test Succeeded!
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
build_windows: build-windows:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@@ -243,7 +244,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@@ -301,13 +302,13 @@ jobs:
details: Test Failed details: Test Failed
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
mypy_version_check: mypy-version-check:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "3.10" python-version: "3.10"
@@ -321,12 +322,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v5
with: with:
python-version: "3.10" python-version: "3.10"
- uses: pre-commit/action@v3.0.0 - uses: pre-commit/action@v3.0.0
docs_check: docs-check:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -336,7 +337,7 @@ jobs:
./tests/test_docs.sh ./tests/test_docs.sh
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
@@ -362,9 +363,9 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "3.9" python-version: "3.11"
- name: Cache_dependencies - name: Cache_dependencies
uses: actions/cache@v3 uses: actions/cache@v3
@@ -375,7 +376,6 @@ jobs:
- name: pip cache (linux) - name: pip cache (linux)
uses: actions/cache@v3 uses: actions/cache@v3
if: runner.os == 'Linux'
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
@@ -386,7 +386,6 @@ jobs:
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd .. cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
- name: Installation - *nix - name: Installation - *nix
if: runner.os == 'Linux'
run: | run: |
python -m pip install --upgrade pip wheel python -m pip install --upgrade pip wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
@@ -399,17 +398,17 @@ jobs:
env: env:
CI_WEB_PROXY: http://152.67.78.211:13128 CI_WEB_PROXY: http://152.67.78.211:13128
run: | run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun pytest --random-order --longrun --durations 20 -n auto --dist loadscope
# Notify only once - when CI completes (and after deploy) in case it's successfull # Notify only once - when CI completes (and after deploy) in case it's successfull
notify-complete: notify-complete:
needs: [ needs: [
build_linux, build_linux,
build_macos, build-macos,
build_windows, build-windows,
docs_check, docs-check,
mypy_version_check, mypy-version-check,
pre-commit, pre-commit,
build_linux_online build_linux_online
] ]
@@ -436,8 +435,63 @@ jobs:
details: Test Completed! details: Test Completed!
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
deploy: build:
needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check, pre-commit ] name: "Build"
needs: [ build_linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Build distribution
run: |
pip install -U build
python -m build --sdist --wheel
- name: Upload artifacts 📦
uses: actions/upload-artifact@v4
with:
name: freqtrade-build
path: |
dist
retention-days: 10
deploy-pypi:
name: "Deploy to PyPI"
needs: [ build ]
runs-on: ubuntu-22.04
if: (github.event_name == 'release')
environment:
name: release
url: https://pypi.org/p/freqtrade
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Download artifact 📦
uses: actions/download-artifact@v4
with:
name: freqtrade-build
path: dist
- name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.8.11
with:
repository-url: https://test.pypi.org/legacy/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.8.11
deploy-docker:
needs: [ build_linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade' if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
@@ -446,7 +500,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
@@ -455,26 +509,6 @@ jobs:
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF##*/})" run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF##*/})"
id: extract_branch id: extract_branch
- name: Build distribution
run: |
pip install -U setuptools wheel
python setup.py sdist bdist_wheel
- name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.8.10
if: (github.event_name == 'release')
with:
user: __token__
password: ${{ secrets.pypi_test_password }}
repository_url: https://test.pypi.org/legacy/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.8.10
if: (github.event_name == 'release')
with:
user: __token__
password: ${{ secrets.pypi_password }}
- name: Dockerhub login - name: Dockerhub login
env: env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
@@ -506,10 +540,11 @@ jobs:
run: | run: |
build_helpers/publish_docker_multi.sh build_helpers/publish_docker_multi.sh
deploy_arm: deploy-arm:
name: "Deploy Docker"
permissions: permissions:
packages: write packages: write
needs: [ deploy ] needs: [ deploy-docker ]
# Only run on 64bit machines # Only run on 64bit machines
runs-on: [self-hosted, linux, ARM64] runs-on: [self-hosted, linux, ARM64]
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade' if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
+44
View File
@@ -0,0 +1,44 @@
name: Pre-commit auto-update
on:
# every day at midnight
schedule:
- cron: "0 3 * * 2"
# on demand
workflow_dispatch:
permissions:
contents: read
jobs:
auto-update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install pre-commit
run: pip install pre-commit
- name: Run auto-update
run: pre-commit autoupdate
- name: Run pre-commit
run: pre-commit run --all-files
- uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.REPO_SCOPED_TOKEN }}
add-paths: .pre-commit-config.yaml
labels: |
Tech maintenance
branch: update/pre-commit-hooks
title: Update pre-commit hooks
commit-message: "chore: update pre-commit hooks"
committer: Freqtrade Bot <noreply@github.com>
body: Update versions of pre-commit hooks to latest version.
delete-branch: true
-1
View File
@@ -111,7 +111,6 @@ target/
#exceptions #exceptions
!*.gitkeep !*.gitkeep
!config_examples/config_binance.example.json !config_examples/config_binance.example.json
!config_examples/config_bittrex.example.json
!config_examples/config_full.example.json !config_examples/config_full.example.json
!config_examples/config_kraken.example.json !config_examples/config_kraken.example.json
!config_examples/config_freqai.example.json !config_examples/config_freqai.example.json
+6 -5
View File
@@ -2,13 +2,14 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/pycqa/flake8 - repo: https://github.com/pycqa/flake8
rev: "6.0.0" rev: "6.1.0"
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [Flake8-pyproject]
# stages: [push] # stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.7.0" rev: "v1.8.0"
hooks: hooks:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
@@ -22,7 +23,7 @@ repos:
# stages: [push] # stages: [push]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
rev: "5.12.0" rev: "5.13.2"
hooks: hooks:
- id: isort - id: isort
name: isort (python) name: isort (python)
@@ -30,12 +31,12 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: 'v0.1.1' rev: 'v0.1.9'
hooks: hooks:
- id: ruff - id: ruff
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.5.0
hooks: hooks:
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: | exclude: |
+2
View File
@@ -5,3 +5,5 @@ recursive-include freqtrade/templates/ *.j2 *.ipynb
include freqtrade/exchange/binance_leverage_tiers.json include freqtrade/exchange/binance_leverage_tiers.json
include freqtrade/rpc/api_server/ui/fallback_file.html include freqtrade/rpc/api_server/ui/fallback_file.html
include freqtrade/rpc/api_server/ui/favicon.ico include freqtrade/rpc/api_server/ui/favicon.ico
prune tests
+2 -2
View File
@@ -618,13 +618,13 @@ To compare multiple strategies, a list of Strategies can be provided to backtest
This is limited to 1 timeframe value per run. However, data is only loaded once from disk so if you have multiple This is limited to 1 timeframe value per run. However, data is only loaded once from disk so if you have multiple
strategies you'd like to compare, this will give a nice runtime boost. strategies you'd like to compare, this will give a nice runtime boost.
All listed Strategies need to be in the same directory. All listed Strategies need to be in the same directory, unless also `--recursive-strategy-search` is specified, where sub-directories within the strategy directory are also considered.
``` bash ``` bash
freqtrade backtesting --timerange 20180401-20180410 --timeframe 5m --strategy-list Strategy001 Strategy002 --export trades freqtrade backtesting --timerange 20180401-20180410 --timeframe 5m --strategy-list Strategy001 Strategy002 --export trades
``` ```
This will save the results to `user_data/backtest_results/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename. This will save the results to `user_data/backtest_results/backtest-result-<datetime>.json`, including results for both `Strategy001` and `Strategy002`.
There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table). There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table).
Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy. Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy.
+1 -1
View File
@@ -321,7 +321,7 @@ For example, if you have 10 ETH available in your wallet on the exchange and `tr
To fully utilize compounding profits when using multiple bots on the same exchange account, you'll want to limit each bot to a certain starting balance. To fully utilize compounding profits when using multiple bots on the same exchange account, you'll want to limit each bot to a certain starting balance.
This can be accomplished by setting `available_capital` to the desired starting balance. This can be accomplished by setting `available_capital` to the desired starting balance.
Assuming your account has 10.000 USDT and you want to run 2 different strategies on this exchange. Assuming your account has 10000 USDT and you want to run 2 different strategies on this exchange.
You'd set `available_capital=5000` - granting each bot an initial capital of 5000 USDT. You'd set `available_capital=5000` - granting each bot an initial capital of 5000 USDT.
The bot will then split this starting balance equally into `max_open_trades` buckets. The bot will then split this starting balance equally into `max_open_trades` buckets.
Profitable trades will result in increased stake-sizes for this bot - without affecting the stake-sizes of the other bot. Profitable trades will result in increased stake-sizes for this bot - without affecting the stake-sizes of the other bot.
+4 -4
View File
@@ -419,6 +419,9 @@ This part of the documentation is aimed at maintainers, and shows how to create
### Create release branch ### Create release branch
!!! Note
Make sure that the `stable` branch is up-to-date!
First, pick a commit that's about one week old (to not include latest additions to releases). First, pick a commit that's about one week old (to not include latest additions to releases).
``` bash ``` bash
@@ -431,14 +434,11 @@ Determine if crucial bugfixes have been made between this commit and the current
* Merge the release branch (stable) into this branch. * Merge the release branch (stable) into this branch.
* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi. * Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi.
* Commit this part. * Commit this part.
* push that branch to the remote and create a PR against the stable branch. * Push that branch to the remote and create a PR against the **stable branch**.
* Update develop version to next version following the pattern `2019.8-dev`. * Update develop version to next version following the pattern `2019.8-dev`.
### Create changelog from git commits ### Create changelog from git commits
!!! Note
Make sure that the `stable` branch is up-to-date!
``` bash ``` bash
# Needs to be done before merging / pulling that branch. # Needs to be done before merging / pulling that branch.
git log --oneline --no-decorate --no-merges stable..new_release git log --oneline --no-decorate --no-merges stable..new_release
-42
View File
@@ -181,48 +181,6 @@ freqtrade download-data --exchange kraken --dl-trades -p BTC/EUR BCH/EUR
Please pay attention that rateLimit configuration entry holds delay in milliseconds between requests, NOT requests\sec rate. Please pay attention that rateLimit configuration entry holds delay in milliseconds between requests, NOT requests\sec rate.
So, in order to mitigate Kraken API "Rate limit exceeded" exception, this configuration should be increased, NOT decreased. So, in order to mitigate Kraken API "Rate limit exceeded" exception, this configuration should be increased, NOT decreased.
## Bittrex
### Order types
Bittrex does not support market orders. If you have a message at the bot startup about this, you should change order type values set in your configuration and/or in the strategy from `"market"` to `"limit"`. See some more details on this [here in the FAQ](faq.md#im-getting-the-exchange-bittrex-does-not-support-market-orders-message-and-cannot-run-my-strategy).
Bittrex also does not support `VolumePairlist` due to limited / split API constellation at the moment.
Please use `StaticPairlist`. Other pairlists (other than `VolumePairlist`) should not be affected.
### Volume pairlist
Bittrex does not support the direct usage of VolumePairList. This can however be worked around by using the advanced mode with `lookback_days: 1` (or more), which will emulate 24h volume.
Read more in the [pairlist documentation](plugins.md#volumepairlist-advanced-mode).
### Restricted markets
Bittrex split its exchange into US and International versions.
The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction.
If you have restricted pairs in your whitelist, you'll get a warning message in the log on Freqtrade startup for each restricted pair.
The warning message will look similar to the following:
``` output
[...] Message: bittrex {"success":false,"message":"RESTRICTED_MARKET","result":null,"explanation":null}"
```
If you're an "International" customer on the Bittrex exchange, then this warning will probably not impact you.
If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your whitelist.
You can get a list of restricted markets by using the following snippet:
``` python
import ccxt
ct = ccxt.bittrex()
lm = ct.load_markets()
res = [p for p, x in lm.items() if 'US' in x['info']['prohibitedIn']]
print(res)
```
## Kucoin ## Kucoin
Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows: Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
+1 -7
View File
@@ -128,15 +128,9 @@ This warning can point to one of the below problems:
* Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling. * Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling.
* API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges). * API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges).
### I'm getting the "RESTRICTED_MARKET" message in the log
Currently known to happen for US Bittrex users.
Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information.
### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy ### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy
As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex and Gate.io). As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Gate.io).
To fix this, redefine order types in the strategy to use "limit" instead of "market": To fix this, redefine order types in the strategy to use "limit" instead of "market":
+2 -2
View File
@@ -112,8 +112,8 @@ For convenience `lookback_days` can be specified, which will imply that 1d candl
!!! Warning "Performance implications when using lookback range" !!! Warning "Performance implications when using lookback range"
If used in first position in combination with lookback, the computation of the range based volume can be time and resource consuming, as it downloads candles for all tradable pairs. Hence it's highly advised to use the standard approach with `VolumeFilter` to narrow the pairlist down for further range volume calculation. If used in first position in combination with lookback, the computation of the range based volume can be time and resource consuming, as it downloads candles for all tradable pairs. Hence it's highly advised to use the standard approach with `VolumeFilter` to narrow the pairlist down for further range volume calculation.
??? Tip "Unsupported exchanges (Bittrex, Gemini)" ??? Tip "Unsupported exchanges"
On some exchanges (like Bittrex and Gemini), regular VolumePairList does not work as the api does not natively provide 24h volume. This can be worked around by using candle data to build the volume. On some exchanges (like Gemini), regular VolumePairList does not work as the api does not natively provide 24h volume. This can be worked around by using candle data to build the volume.
To roughly simulate 24h volume, you can use the following configuration. To roughly simulate 24h volume, you can use the following configuration.
Please note that These pairlists will only refresh once per day. Please note that These pairlists will only refresh once per day.
+1 -1
View File
@@ -1,6 +1,6 @@
markdown==3.5.1 markdown==3.5.1
mkdocs==1.5.3 mkdocs==1.5.3
mkdocs-material==9.4.14 mkdocs-material==9.5.3
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==10.5 pymdown-extensions==10.5
jinja2==3.1.2 jinja2==3.1.2
+1 -1
View File
@@ -489,7 +489,7 @@ The helper function `stoploss_from_absolute()` can be used to convert from an ab
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc) trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
candle = dataframe.iloc[-1].squeeze() candle = dataframe.iloc[-1].squeeze()
sign = 1 if trade.is_short else -1 side = 1 if trade.is_short else -1
return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2), return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2),
current_rate, is_short=trade.is_short, current_rate, is_short=trade.is_short,
leverage=trade.leverage) leverage=trade.leverage)
-2
View File
@@ -242,7 +242,6 @@ bitkk True missing opt: fetchMyTrades
bitmart True bitmart True
bitmax True missing opt: fetchMyTrades bitmax True missing opt: fetchMyTrades
bitpanda True bitpanda True
bittrex True
bitvavo True bitvavo True
bitz True missing opt: fetchMyTrades bitz True missing opt: fetchMyTrades
btcalpha True missing opt: fetchTicker, fetchTickers btcalpha True missing opt: fetchTicker, fetchTickers
@@ -324,7 +323,6 @@ bitpanda True
bitso False missing: fetchOHLCV bitso False missing: fetchOHLCV
bitstamp True missing opt: fetchTickers bitstamp True missing opt: fetchTickers
bitstamp1 False missing: fetchOrder, fetchOHLCV bitstamp1 False missing: fetchOrder, fetchOHLCV
bittrex True
bitvavo True bitvavo True
bitz True missing opt: fetchMyTrades bitz True missing opt: fetchMyTrades
bl3p False missing: fetchOrder, fetchOHLCV bl3p False missing: fetchOrder, fetchOHLCV
+1 -1
View File
@@ -1,5 +1,5 @@
""" Freqtrade bot """ """ Freqtrade bot """
__version__ = '2023.11' __version__ = '2023.12'
if 'dev' in __version__: if 'dev' in __version__:
from pathlib import Path from pathlib import Path
+4 -4
View File
@@ -67,7 +67,7 @@ def validate_config_schema(conf: Dict[str, Any], preliminary: bool = False) -> D
) )
def validate_config_consistency(conf: Dict[str, Any], preliminary: bool = False) -> None: def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = False) -> None:
""" """
Validate the configuration consistency. Validate the configuration consistency.
Should be ran after loading both configuration and strategy, Should be ran after loading both configuration and strategy,
@@ -86,7 +86,7 @@ def validate_config_consistency(conf: Dict[str, Any], preliminary: bool = False)
_validate_ask_orderbook(conf) _validate_ask_orderbook(conf)
_validate_freqai_hyperopt(conf) _validate_freqai_hyperopt(conf)
_validate_freqai_backtest(conf) _validate_freqai_backtest(conf)
_validate_freqai_include_timeframes(conf) _validate_freqai_include_timeframes(conf, preliminary=preliminary)
_validate_consumers(conf) _validate_consumers(conf)
validate_migrated_strategy_settings(conf) validate_migrated_strategy_settings(conf)
@@ -335,7 +335,7 @@ def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None:
'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.') 'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.')
def _validate_freqai_include_timeframes(conf: Dict[str, Any]) -> None: def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool) -> None:
freqai_enabled = conf.get('freqai', {}).get('enabled', False) freqai_enabled = conf.get('freqai', {}).get('enabled', False)
if freqai_enabled: if freqai_enabled:
main_tf = conf.get('timeframe', '5m') main_tf = conf.get('timeframe', '5m')
@@ -355,7 +355,7 @@ def _validate_freqai_include_timeframes(conf: Dict[str, Any]) -> None:
f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}") f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}")
# Ensure that the base timeframe is included in the include_timeframes list # Ensure that the base timeframe is included in the include_timeframes list
if main_tf not in freqai_include_timeframes: if not preliminary and main_tf not in freqai_include_timeframes:
feature_parameters = conf.get('freqai', {}).get('feature_parameters', {}) feature_parameters = conf.get('freqai', {}).get('feature_parameters', {})
include_timeframes = [main_tf] + freqai_include_timeframes include_timeframes = [main_tf] + freqai_include_timeframes
conf.get('freqai', {}).get('feature_parameters', {}) \ conf.get('freqai', {}).get('feature_parameters', {}) \
+4 -1
View File
@@ -326,7 +326,10 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
"Please specify a strategy.") "Please specify a strategy.")
if strategy not in data['strategy']: if strategy not in data['strategy']:
raise ValueError(f"Strategy {strategy} not available in the backtest result.") raise ValueError(
f"Strategy {strategy} not available in the backtest result. "
f"Available strategies are '{','.join(data['strategy'].keys())}'"
)
data = data['strategy'][strategy]['trades'] data = data['strategy'][strategy]['trades']
df = pd.DataFrame(data) df = pd.DataFrame(data)
+2 -2
View File
@@ -116,8 +116,8 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
len_after = len(df) len_after = len(df)
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0 pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
if len_before != len_after: if len_before != len_after:
message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}" message = (f"Missing data fillup for {pair}, {timeframe}: "
f" - {pct_missing:.2%}") f"before: {len_before} - after: {len_after} - {pct_missing:.2%}")
if pct_missing > 0.01: if pct_missing > 0.01:
logger.info(message) logger.info(message)
else: else:
-1
View File
@@ -6,7 +6,6 @@ from freqtrade.exchange.exchange import Exchange
from freqtrade.exchange.binance import Binance from freqtrade.exchange.binance import Binance
from freqtrade.exchange.bitmart import Bitmart from freqtrade.exchange.bitmart import Bitmart
from freqtrade.exchange.bitpanda import Bitpanda from freqtrade.exchange.bitpanda import Bitpanda
from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.bitvavo import Bitvavo from freqtrade.exchange.bitvavo import Bitvavo
from freqtrade.exchange.bybit import Bybit from freqtrade.exchange.bybit import Bybit
from freqtrade.exchange.coinbasepro import Coinbasepro from freqtrade.exchange.coinbasepro import Coinbasepro
File diff suppressed because it is too large Load Diff
-25
View File
@@ -1,25 +0,0 @@
""" Bittrex exchange subclass """
import logging
from typing import Dict
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__)
class Bittrex(Exchange):
"""
Bittrex exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
"""
_ft_has: Dict = {
"ohlcv_candle_limit_per_timeframe": {
'1m': 1440,
'5m': 288,
'1h': 744,
'1d': 365,
},
"l2_limit_range": [1, 25, 500],
}
+1
View File
@@ -29,6 +29,7 @@ class Bybit(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"ohlcv_candle_limit": 1000, "ohlcv_candle_limit": 1000,
"ohlcv_has_history": True, "ohlcv_has_history": True,
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
} }
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"ohlcv_has_history": True, "ohlcv_has_history": True,
+1
View File
@@ -330,6 +330,7 @@ class Exchange:
Exchange ohlcv candle limit Exchange ohlcv candle limit
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
TODO: this is most likely no longer needed since only bittrex needed this.
:param timeframe: Timeframe to check :param timeframe: Timeframe to check
:param candle_type: Candle-type :param candle_type: Candle-type
:param since_ms: Starting timestamp :param since_ms: Starting timestamp
+13 -6
View File
@@ -12,7 +12,6 @@ import numpy as np
import pandas as pd import pandas as pd
import psutil import psutil
import rapidjson import rapidjson
from joblib import dump, load
from joblib.externals import cloudpickle from joblib.externals import cloudpickle
from numpy.typing import NDArray from numpy.typing import NDArray
from pandas import DataFrame from pandas import DataFrame
@@ -285,6 +284,10 @@ class FreqaiDataDrawer:
new_pred["date_pred"] = dataframe["date"] new_pred["date_pred"] = dataframe["date"]
hist_preds = self.historic_predictions[pair].copy() hist_preds = self.historic_predictions[pair].copy()
# ensure both dataframes have the same date format so they can be merged
new_pred["date_pred"] = pd.to_datetime(new_pred["date_pred"])
hist_preds["date_pred"] = pd.to_datetime(hist_preds["date_pred"])
# find the closest common date between new_pred and historic predictions # find the closest common date between new_pred and historic predictions
# and cut off the new_pred dataframe at that date # and cut off the new_pred dataframe at that date
common_dates = pd.merge(new_pred, hist_preds, on="date_pred", how="inner") common_dates = pd.merge(new_pred, hist_preds, on="date_pred", how="inner")
@@ -295,7 +298,9 @@ class FreqaiDataDrawer:
"predictions. You likely left your FreqAI instance offline " "predictions. You likely left your FreqAI instance offline "
f"for more than {len(dataframe.index)} candles.") f"for more than {len(dataframe.index)} candles.")
df_concat = pd.concat([hist_preds, new_pred], ignore_index=True, keys=hist_preds.keys()) # reindex new_pred columns to match the historic predictions dataframe
new_pred_reindexed = new_pred.reindex(columns=hist_preds.columns)
df_concat = pd.concat([hist_preds, new_pred_reindexed], ignore_index=True)
# any missing values will get zeroed out so users can see the exact # any missing values will get zeroed out so users can see the exact
# downtime in FreqUI # downtime in FreqUI
@@ -318,9 +323,9 @@ class FreqaiDataDrawer:
index = self.historic_predictions[pair].index[-1:] index = self.historic_predictions[pair].index[-1:]
columns = self.historic_predictions[pair].columns columns = self.historic_predictions[pair].columns
nan_df = pd.DataFrame(np.nan, index=index, columns=columns) zeros_df = pd.DataFrame(np.zeros((1, len(columns))), index=index, columns=columns)
self.historic_predictions[pair] = pd.concat( self.historic_predictions[pair] = pd.concat(
[self.historic_predictions[pair], nan_df], ignore_index=True, axis=0) [self.historic_predictions[pair], zeros_df], ignore_index=True, axis=0)
df = self.historic_predictions[pair] df = self.historic_predictions[pair]
# model outputs and associated statistics # model outputs and associated statistics
@@ -471,7 +476,8 @@ class FreqaiDataDrawer:
# Save the trained model # Save the trained model
if self.model_type == 'joblib': if self.model_type == 'joblib':
dump(model, save_path / f"{dk.model_filename}_model.joblib") with (save_path / f"{dk.model_filename}_model.joblib").open("wb") as fp:
cloudpickle.dump(model, fp)
elif self.model_type == 'keras': elif self.model_type == 'keras':
model.save(save_path / f"{dk.model_filename}_model.h5") model.save(save_path / f"{dk.model_filename}_model.h5")
elif self.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]: elif self.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]:
@@ -558,7 +564,8 @@ class FreqaiDataDrawer:
if dk.live and coin in self.model_dictionary: if dk.live and coin in self.model_dictionary:
model = self.model_dictionary[coin] model = self.model_dictionary[coin]
elif self.model_type == 'joblib': elif self.model_type == 'joblib':
model = load(dk.data_path / f"{dk.model_filename}_model.joblib") with (dk.data_path / f"{dk.model_filename}_model.joblib").open("rb") as fp:
model = cloudpickle.load(fp)
elif 'stable_baselines' in self.model_type or 'sb3_contrib' == self.model_type: elif 'stable_baselines' in self.model_type or 'sb3_contrib' == self.model_type:
mod = importlib.import_module( mod = importlib.import_module(
self.model_type, self.freqai_info['rl_config']['model_type']) self.model_type, self.freqai_info['rl_config']['model_type'])
+1 -1
View File
@@ -244,7 +244,7 @@ class FreqaiDataKitchen:
f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points" f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points"
f" due to NaNs in populated dataset {len(unfiltered_df)}." f" due to NaNs in populated dataset {len(unfiltered_df)}."
) )
if len(unfiltered_df) == 0 and not self.live: if len(filtered_df) == 0 and not self.live:
raise OperationalException( raise OperationalException(
f"{self.pair}: all training data dropped due to NaNs. " f"{self.pair}: all training data dropped due to NaNs. "
"You likely did not download enough training data prior " "You likely did not download enough training data prior "
@@ -0,0 +1,82 @@
import logging
from typing import Any, Dict, Tuple
import numpy as np
import numpy.typing as npt
from pandas import DataFrame
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import LabelEncoder
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
logger = logging.getLogger(__name__)
class SKLearnRandomForestClassifier(BaseClassifierModel):
"""
User created prediction model. The class inherits IFreqaiModel, which
means it has full access to all Frequency AI functionality. Typically,
users would use this to override the common `fit()`, `train()`, or
`predict()` methods to add their custom data handling tools or change
various aspects of the training that cannot be configured via the
top level config.json file.
"""
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
"""
User sets up the training and test data to fit their desired model here
:param data_dictionary: the dictionary holding all data for train, test,
labels, weights
:param dk: The datakitchen object for the current coin/model
"""
X = data_dictionary["train_features"].to_numpy()
y = data_dictionary["train_labels"].to_numpy()[:, 0]
if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0:
eval_set = None
else:
test_features = data_dictionary["test_features"].to_numpy()
test_labels = data_dictionary["test_labels"].to_numpy()[:, 0]
eval_set = (test_features, test_labels)
if self.freqai_info.get("continual_learning", False):
logger.warning("Continual learning is not supported for "
"SKLearnRandomForestClassifier, ignoring.")
train_weights = data_dictionary["train_weights"]
model = RandomForestClassifier(**self.model_training_parameters)
model.fit(X=X, y=y, sample_weight=train_weights)
if eval_set:
logger.info("Score: %s", model.score(eval_set[0], eval_set[1]))
return model
def predict(
self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs
) -> Tuple[DataFrame, npt.NDArray[np.int_]]:
"""
Filter the prediction features data and predict with it.
:param unfiltered_df: Full dataframe for the current backtest period.
:return:
:pred_df: dataframe containing the predictions
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
data (NaNs) or felt uncertain about data (PCA and DI index)
"""
(pred_df, dk.do_predict) = super().predict(unfiltered_df, dk, **kwargs)
le = LabelEncoder()
label = dk.label_list[0]
labels_before = list(dk.data['labels_std'].keys())
labels_after = le.fit_transform(labels_before).tolist()
pred_df[label] = le.inverse_transform(pred_df[label])
pred_df = pred_df.rename(
columns={labels_after[i]: labels_before[i] for i in range(len(labels_before))})
return (pred_df, dk.do_predict)
@@ -45,7 +45,7 @@ class XGBoostRFRegressor(BaseRegressionModel):
model = XGBRFRegressor(**self.model_training_parameters) model = XGBRFRegressor(**self.model_training_parameters)
model.set_params(callbacks=[TBCallback(dk.data_path)], activate=self.activate_tensorboard) model.set_params(callbacks=[TBCallback(dk.data_path)])
model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set, model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set,
sample_weight_eval_set=eval_weights, xgb_model=xgb_model) sample_weight_eval_set=eval_weights, xgb_model=xgb_model)
# set the callbacks to empty so that we can serialize to disk later # set the callbacks to empty so that we can serialize to disk later
@@ -45,7 +45,7 @@ class XGBoostRegressor(BaseRegressionModel):
model = XGBRegressor(**self.model_training_parameters) model = XGBRegressor(**self.model_training_parameters)
model.set_params(callbacks=[TBCallback(dk.data_path)], activate=self.activate_tensorboard) model.set_params(callbacks=[TBCallback(dk.data_path)])
model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set, model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set,
sample_weight_eval_set=eval_weights, xgb_model=xgb_model) sample_weight_eval_set=eval_weights, xgb_model=xgb_model)
# set the callbacks to empty so that we can serialize to disk later # set the callbacks to empty so that we can serialize to disk later
+10 -9
View File
@@ -33,8 +33,8 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.rpc import RPCManager from freqtrade.rpc import RPCManager
from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
from freqtrade.rpc.rpc_types import (RPCBuyMsg, RPCCancelMsg, RPCProtectionMsg, RPCSellCancelMsg, from freqtrade.rpc.rpc_types import (ProfitLossStr, RPCCancelMsg, RPCEntryMsg, RPCExitCancelMsg,
RPCSellMsg) RPCExitMsg, RPCProtectionMsg)
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util import FtPrecise from freqtrade.util import FtPrecise
@@ -580,7 +580,8 @@ class FreqtradeBot(LoggingMixin):
else: else:
self.log_once(f"Pair {pair} is currently locked.", logger.info) self.log_once(f"Pair {pair} is currently locked.", logger.info)
return False return False
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) stake_amount = self.wallets.get_trade_stake_amount(
pair, self.config['max_open_trades'], self.edge)
bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {}) bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {})
if ((bid_check_dom.get('enabled', False)) and if ((bid_check_dom.get('enabled', False)) and
@@ -903,7 +904,7 @@ class FreqtradeBot(LoggingMixin):
# First cancelling stoploss on exchange ... # First cancelling stoploss on exchange ...
if trade.stoploss_order_id: if trade.stoploss_order_id:
try: try:
logger.info(f"Canceling stoploss on exchange for {trade}") logger.info(f"Cancelling stoploss on exchange for {trade}")
co = self.exchange.cancel_stoploss_order_with_result( co = self.exchange.cancel_stoploss_order_with_result(
trade.stoploss_order_id, trade.pair, trade.amount) trade.stoploss_order_id, trade.pair, trade.amount)
self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True) self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True)
@@ -1014,7 +1015,7 @@ class FreqtradeBot(LoggingMixin):
current_rate = self.exchange.get_rate( current_rate = self.exchange.get_rate(
trade.pair, side='entry', is_short=trade.is_short, refresh=False) trade.pair, side='entry', is_short=trade.is_short, refresh=False)
msg: RPCBuyMsg = { msg: RPCEntryMsg = {
'trade_id': trade.id, 'trade_id': trade.id,
'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY, 'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY,
'buy_tag': trade.enter_tag, 'buy_tag': trade.enter_tag,
@@ -1791,9 +1792,9 @@ class FreqtradeBot(LoggingMixin):
order_rate = trade.safe_close_rate order_rate = trade.safe_close_rate
profit = trade.calculate_profit(rate=order_rate) profit = trade.calculate_profit(rate=order_rate)
amount = trade.amount amount = trade.amount
gain = "profit" if profit.profit_ratio > 0 else "loss" gain: ProfitLossStr = "profit" if profit.profit_ratio > 0 else "loss"
msg: RPCSellMsg = { msg: RPCExitMsg = {
'type': (RPCMessageType.EXIT_FILL if fill 'type': (RPCMessageType.EXIT_FILL if fill
else RPCMessageType.EXIT), else RPCMessageType.EXIT),
'trade_id': trade.id, 'trade_id': trade.id,
@@ -1845,9 +1846,9 @@ class FreqtradeBot(LoggingMixin):
profit = trade.calculate_profit(rate=profit_rate) profit = trade.calculate_profit(rate=profit_rate)
current_rate = self.exchange.get_rate( current_rate = self.exchange.get_rate(
trade.pair, side='exit', is_short=trade.is_short, refresh=False) trade.pair, side='exit', is_short=trade.is_short, refresh=False)
gain = "profit" if profit.profit_ratio > 0 else "loss" gain: ProfitLossStr = "profit" if profit.profit_ratio > 0 else "loss"
msg: RPCSellCancelMsg = { msg: RPCExitCancelMsg = {
'type': RPCMessageType.EXIT_CANCEL, 'type': RPCMessageType.EXIT_CANCEL,
'trade_id': trade.id, 'trade_id': trade.id,
'exchange': trade.exchange.capitalize(), 'exchange': trade.exchange.capitalize(),
+31 -14
View File
@@ -276,11 +276,13 @@ class Backtesting:
else: else:
self.detail_data = {} self.detail_data = {}
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
self.funding_fee_timeframe: str = self.exchange.get_option('mark_ohlcv_timeframe')
self.funding_fee_timeframe_secs: int = timeframe_to_seconds(self.funding_fee_timeframe)
# Load additional futures data. # Load additional futures data.
funding_rates_dict = history.load_data( funding_rates_dict = history.load_data(
datadir=self.config['datadir'], datadir=self.config['datadir'],
pairs=self.pairlists.whitelist, pairs=self.pairlists.whitelist,
timeframe=self.exchange.get_option('mark_ohlcv_timeframe'), timeframe=self.funding_fee_timeframe,
timerange=self.timerange, timerange=self.timerange,
startup_candles=0, startup_candles=0,
fail_without_data=True, fail_without_data=True,
@@ -292,7 +294,7 @@ class Backtesting:
mark_rates_dict = history.load_data( mark_rates_dict = history.load_data(
datadir=self.config['datadir'], datadir=self.config['datadir'],
pairs=self.pairlists.whitelist, pairs=self.pairlists.whitelist,
timeframe=self.exchange.get_option('mark_ohlcv_timeframe'), timeframe=self.funding_fee_timeframe,
timerange=self.timerange, timerange=self.timerange,
startup_candles=0, startup_candles=0,
fail_without_data=True, fail_without_data=True,
@@ -597,6 +599,8 @@ class Backtesting:
""" """
if order and self._get_order_filled(order.ft_price, row): if order and self._get_order_filled(order.ft_price, row):
order.close_bt_order(current_date, trade) order.close_bt_order(current_date, trade)
self._run_funding_fees(trade, current_date, force=True)
if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount): if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount):
# trade is still open # trade is still open
trade.set_liquidation_price(self.exchange.get_liquidation_price( trade.set_liquidation_price(self.exchange.get_liquidation_price(
@@ -718,16 +722,7 @@ class Backtesting:
self, trade: LocalTrade, row: Tuple, current_time: datetime self, trade: LocalTrade, row: Tuple, current_time: datetime
) -> Optional[LocalTrade]: ) -> Optional[LocalTrade]:
if self.trading_mode == TradingMode.FUTURES: self._run_funding_fees(trade, current_time)
trade.set_funding_fees(
self.exchange.calculate_funding_fees(
self.futures_data[trade.pair],
amount=trade.amount,
is_short=trade.is_short,
open_date=trade.date_last_filled_utc,
close_date=current_time
)
)
# Check if we need to adjust our current positions # Check if we need to adjust our current positions
if self.strategy.position_adjustment_enable: if self.strategy.position_adjustment_enable:
@@ -746,6 +741,27 @@ class Backtesting:
return t return t
return None return None
def _run_funding_fees(self, trade: LocalTrade, current_time: datetime, force: bool = False):
"""
Calculate funding fees if necessary and add them to the trade.
"""
if self.trading_mode == TradingMode.FUTURES:
if (
force
or (current_time.timestamp() % self.funding_fee_timeframe_secs) == 0
):
# Funding fee interval.
trade.set_funding_fees(
self.exchange.calculate_funding_fees(
self.futures_data[trade.pair],
amount=trade.amount,
is_short=trade.is_short,
open_date=trade.date_last_filled_utc,
close_date=current_time
)
)
def get_valid_price_and_stake( def get_valid_price_and_stake(
self, pair: str, row: Tuple, propose_rate: float, stake_amount: float, self, pair: str, row: Tuple, propose_rate: float, stake_amount: float,
direction: LongShort, current_time: datetime, entry_tag: Optional[str], direction: LongShort, current_time: datetime, entry_tag: Optional[str],
@@ -775,7 +791,8 @@ class Backtesting:
leverage = trade.leverage if trade else 1.0 leverage = trade.leverage if trade else 1.0
if not pos_adjust: if not pos_adjust:
try: try:
stake_amount = self.wallets.get_trade_stake_amount(pair, None, update=False) stake_amount = self.wallets.get_trade_stake_amount(
pair, self.strategy.max_open_trades, update=False)
except DependencyException: except DependencyException:
return 0, 0, 0, 0 return 0, 0, 0, 0
@@ -957,7 +974,7 @@ class Backtesting:
def trade_slot_available(self, open_trade_count: int) -> bool: def trade_slot_available(self, open_trade_count: int) -> bool:
# Always allow trades when max_open_trades is enabled. # Always allow trades when max_open_trades is enabled.
max_open_trades: IntOrInf = self.config['max_open_trades'] max_open_trades: IntOrInf = self.strategy.max_open_trades
if max_open_trades <= 0 or open_trade_count < max_open_trades: if max_open_trades <= 0 or open_trade_count < max_open_trades:
return True return True
# Rejected trade # Rejected trade
+5 -1
View File
@@ -500,7 +500,7 @@ class Hyperopt:
while i < 5 and len(asked_non_tried) < n_points: while i < 5 and len(asked_non_tried) < n_points:
if i < 3: if i < 3:
self.opt.cache_ = {} self.opt.cache_ = {}
asked = unique_list(self.opt.ask(n_points=n_points * 5)) asked = unique_list(self.opt.ask(n_points=n_points * 5 if i > 0 else n_points))
is_random = [False for _ in range(len(asked))] is_random = [False for _ in range(len(asked))]
else: else:
asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5)) asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5))
@@ -637,6 +637,10 @@ class Hyperopt:
HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs, HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs,
self.print_json) self.print_json)
elif self.num_epochs_saved > 0:
print(
f"No good result found for given optimization function in {self.num_epochs_saved} "
f"{plural(self.num_epochs_saved, 'epoch')}.")
else: else:
# This is printed when Ctrl+C is pressed quickly, before first epochs have # This is printed when Ctrl+C is pressed quickly, before first epochs have
# a chance to be evaluated. # a chance to be evaluated.
+14 -14
View File
@@ -156,20 +156,20 @@ class Order(ModelBase):
if self.order_id != str(order['id']): if self.order_id != str(order['id']):
raise DependencyException("Order-id's don't match") raise DependencyException("Order-id's don't match")
self.status = order.get('status', self.status) self.status = safe_value_fallback(order, 'status', default_value=self.status)
self.symbol = order.get('symbol', self.symbol) self.symbol = safe_value_fallback(order, 'symbol', default_value=self.symbol)
self.order_type = order.get('type', self.order_type) self.order_type = safe_value_fallback(order, 'type', default_value=self.order_type)
self.side = order.get('side', self.side) self.side = safe_value_fallback(order, 'side', default_value=self.side)
self.price = order.get('price', self.price) self.price = safe_value_fallback(order, 'price', default_value=self.price)
self.amount = order.get('amount', self.amount) self.amount = safe_value_fallback(order, 'amount', default_value=self.amount)
self.filled = order.get('filled', self.filled) self.filled = safe_value_fallback(order, 'filled', default_value=self.filled)
self.average = order.get('average', self.average) self.average = safe_value_fallback(order, 'average', default_value=self.average)
self.remaining = order.get('remaining', self.remaining) self.remaining = safe_value_fallback(order, 'remaining', default_value=self.remaining)
self.cost = order.get('cost', self.cost) self.cost = safe_value_fallback(order, 'cost', default_value=self.cost)
self.stop_price = order.get('stopPrice', self.stop_price) self.stop_price = safe_value_fallback(order, 'stopPrice', default_value=self.stop_price)
order_date = safe_value_fallback(order, 'timestamp')
if 'timestamp' in order and order['timestamp'] is not None: if order_date:
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc) self.order_date = datetime.fromtimestamp(order_date / 1000, tz=timezone.utc)
self.ft_is_open = True self.ft_is_open = True
if self.status in NON_OPEN_EXCHANGE_STATES: if self.status in NON_OPEN_EXCHANGE_STATES:
+1
View File
@@ -471,6 +471,7 @@ class FreqAIModelListResponse(BaseModel):
class StrategyResponse(BaseModel): class StrategyResponse(BaseModel):
strategy: str strategy: str
code: str code: str
timeframe: Optional[str]
class AvailablePairs(BaseModel): class AvailablePairs(BaseModel):
+1
View File
@@ -350,6 +350,7 @@ def get_strategy(strategy: str, config=Depends(get_config)):
return { return {
'strategy': strategy_obj.get_strategy_name(), 'strategy': strategy_obj.get_strategy_name(),
'code': strategy_obj.__source__, 'code': strategy_obj.__source__,
'timeframe': getattr(strategy_obj, 'timeframe', None),
} }
+2
View File
@@ -15,6 +15,7 @@ class Discord(Webhook):
self.rpc = rpc self.rpc = rpc
self.strategy = config.get('strategy', '') self.strategy = config.get('strategy', '')
self.timeframe = config.get('timeframe', '') self.timeframe = config.get('timeframe', '')
self.bot_name = config.get('bot_name', '')
self._url = config['discord']['webhook_url'] self._url = config['discord']['webhook_url']
self._format = 'json' self._format = 'json'
@@ -36,6 +37,7 @@ class Discord(Webhook):
msg['strategy'] = self.strategy msg['strategy'] = self.strategy
msg['timeframe'] = self.timeframe msg['timeframe'] = self.timeframe
msg['bot_name'] = self.bot_name
color = 0x0000FF color = 0x0000FF
if msg['type'] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL): if msg['type'] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL):
profit_ratio = msg.get('profit_ratio') profit_ratio = msg.get('profit_ratio')
+1
View File
@@ -28,6 +28,7 @@ coingecko_mapping = {
'busd': 'binance-usd', 'busd': 'binance-usd',
'tusd': 'true-usd', 'tusd': 'true-usd',
'usdc': 'usd-coin', 'usdc': 'usd-coin',
'btc': 'bitcoin'
} }
+4 -3
View File
@@ -121,8 +121,8 @@ class RPC:
'stake_currency_decimals': decimals_per_coin(config['stake_currency']), 'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
'stake_amount': str(config['stake_amount']), 'stake_amount': str(config['stake_amount']),
'available_capital': config.get('available_capital'), 'available_capital': config.get('available_capital'),
'max_open_trades': (config['max_open_trades'] 'max_open_trades': (config.get('max_open_trades', 0)
if config['max_open_trades'] != float('inf') else -1), if config.get('max_open_trades', 0) != float('inf') else -1),
'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {}, 'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
'stoploss': config.get('stoploss'), 'stoploss': config.get('stoploss'),
'stoploss_on_exchange': config.get('order_types', 'stoploss_on_exchange': config.get('order_types',
@@ -914,7 +914,8 @@ class RPC:
if not stake_amount: if not stake_amount:
# gen stake amount # gen stake amount
stake_amount = self._freqtrade.wallets.get_trade_stake_amount(pair) stake_amount = self._freqtrade.wallets.get_trade_stake_amount(
pair, self._config['max_open_trades'])
# execute buy # execute buy
if not order_type: if not order_type:
+13 -10
View File
@@ -5,6 +5,9 @@ from freqtrade.constants import PairWithTimeframe
from freqtrade.enums import RPCMessageType from freqtrade.enums import RPCMessageType
ProfitLossStr = Literal["profit", "loss"]
class RPCSendMsgBase(TypedDict): class RPCSendMsgBase(TypedDict):
pass pass
# ty1pe: Literal[RPCMessageType] # ty1pe: Literal[RPCMessageType]
@@ -41,7 +44,7 @@ class RPCWhitelistMsg(RPCSendMsgBase):
data: List[str] data: List[str]
class __RPCBuyMsgBase(RPCSendMsgBase): class __RPCEntryExitMsgBase(RPCSendMsgBase):
trade_id: int trade_id: int
buy_tag: Optional[str] buy_tag: Optional[str]
enter_tag: Optional[str] enter_tag: Optional[str]
@@ -62,19 +65,19 @@ class __RPCBuyMsgBase(RPCSendMsgBase):
sub_trade: bool sub_trade: bool
class RPCBuyMsg(__RPCBuyMsgBase): class RPCEntryMsg(__RPCEntryExitMsgBase):
type: Literal[RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL] type: Literal[RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]
class RPCCancelMsg(__RPCBuyMsgBase): class RPCCancelMsg(__RPCEntryExitMsgBase):
type: Literal[RPCMessageType.ENTRY_CANCEL] type: Literal[RPCMessageType.ENTRY_CANCEL]
reason: str reason: str
class RPCSellMsg(__RPCBuyMsgBase): class RPCExitMsg(__RPCEntryExitMsgBase):
type: Literal[RPCMessageType.EXIT, RPCMessageType.EXIT_FILL] type: Literal[RPCMessageType.EXIT, RPCMessageType.EXIT_FILL]
cumulative_profit: float cumulative_profit: float
gain: str # Literal["profit", "loss"] gain: ProfitLossStr
close_rate: float close_rate: float
profit_amount: float profit_amount: float
profit_ratio: float profit_ratio: float
@@ -85,10 +88,10 @@ class RPCSellMsg(__RPCBuyMsgBase):
order_rate: Optional[float] order_rate: Optional[float]
class RPCSellCancelMsg(__RPCBuyMsgBase): class RPCExitCancelMsg(__RPCEntryExitMsgBase):
type: Literal[RPCMessageType.EXIT_CANCEL] type: Literal[RPCMessageType.EXIT_CANCEL]
reason: str reason: str
gain: str # Literal["profit", "loss"] gain: ProfitLossStr
profit_amount: float profit_amount: float
profit_ratio: float profit_ratio: float
sell_reason: Optional[str] sell_reason: Optional[str]
@@ -119,10 +122,10 @@ RPCSendMsg = Union[
RPCStrategyMsg, RPCStrategyMsg,
RPCProtectionMsg, RPCProtectionMsg,
RPCWhitelistMsg, RPCWhitelistMsg,
RPCBuyMsg, RPCEntryMsg,
RPCCancelMsg, RPCCancelMsg,
RPCSellMsg, RPCExitMsg,
RPCSellCancelMsg, RPCExitCancelMsg,
RPCAnalyzedDFMsg, RPCAnalyzedDFMsg,
RPCNewCandleMsg RPCNewCandleMsg
] ]
+1 -1
View File
@@ -84,7 +84,7 @@ class Webhook(RPCHandler):
valuedict = self._get_value_dict(msg) valuedict = self._get_value_dict(msg)
if not valuedict: if not valuedict:
logger.info("Message type '%s' not configured for webhooks", msg['type']) logger.debug("Message type '%s' not configured for webhooks", msg['type'])
return return
payload = {key: value.format(**msg) for (key, value) in valuedict.items()} payload = {key: value.format(**msg) for (key, value) in valuedict.items()}
+11 -8
View File
@@ -36,7 +36,7 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
:return: Merged dataframe :return: Merged dataframe
:raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe :raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe
""" """
informative = informative.copy()
minutes_inf = timeframe_to_minutes(timeframe_inf) minutes_inf = timeframe_to_minutes(timeframe_inf)
minutes = timeframe_to_minutes(timeframe) minutes = timeframe_to_minutes(timeframe)
if minutes == minutes_inf: if minutes == minutes_inf:
@@ -46,10 +46,16 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
# Subtract "small" timeframe so merging is not delayed by 1 small candle # Subtract "small" timeframe so merging is not delayed by 1 small candle
# Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073 # Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073
if not informative.empty: if not informative.empty:
informative['date_merge'] = ( if timeframe_inf == '1M':
informative[date_column] + pd.to_timedelta(minutes_inf, 'm') - informative['date_merge'] = (
pd.to_timedelta(minutes, 'm') (informative[date_column] + pd.offsets.MonthBegin(1))
) - pd.to_timedelta(minutes, 'm')
)
else:
informative['date_merge'] = (
informative[date_column] + pd.to_timedelta(minutes_inf, 'm') -
pd.to_timedelta(minutes, 'm')
)
else: else:
informative['date_merge'] = informative[date_column] informative['date_merge'] = informative[date_column]
else: else:
@@ -80,9 +86,6 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
right_on=date_merge, how='left') right_on=date_merge, how='left')
dataframe = dataframe.drop(date_merge, axis=1) dataframe = dataframe.drop(date_merge, axis=1)
# if ffill:
# dataframe = dataframe.ffill()
return dataframe return dataframe
+5 -18
View File
@@ -6,7 +6,7 @@ import talib.abstract as ta
from pandas import DataFrame from pandas import DataFrame
from technical import qtpylib from technical import qtpylib
from freqtrade.strategy import CategoricalParameter, IStrategy from freqtrade.strategy import IStrategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -45,11 +45,6 @@ class FreqaiExampleStrategy(IStrategy):
startup_candle_count: int = 40 startup_candle_count: int = 40
can_short = True can_short = True
std_dev_multiplier_buy = CategoricalParameter(
[0.75, 1, 1.25, 1.5, 1.75], default=1.25, space="buy", optimize=True)
std_dev_multiplier_sell = CategoricalParameter(
[0.75, 1, 1.25, 1.5, 1.75], space="sell", default=1.25, optimize=True)
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: Dict, **kwargs) -> DataFrame: metadata: Dict, **kwargs) -> DataFrame:
""" """
@@ -239,21 +234,13 @@ class FreqaiExampleStrategy(IStrategy):
dataframe = self.freqai.start(dataframe, metadata, self) dataframe = self.freqai.start(dataframe, metadata, self)
for val in self.std_dev_multiplier_buy.range:
dataframe[f'target_roi_{val}'] = (
dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * val
)
for val in self.std_dev_multiplier_sell.range:
dataframe[f'sell_roi_{val}'] = (
dataframe["&-s_close_mean"] - dataframe["&-s_close_std"] * val
)
return dataframe return dataframe
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
enter_long_conditions = [ enter_long_conditions = [
df["do_predict"] == 1, df["do_predict"] == 1,
df["&-s_close"] > df[f"target_roi_{self.std_dev_multiplier_buy.value}"], df["&-s_close"] > 0.01,
] ]
if enter_long_conditions: if enter_long_conditions:
@@ -263,7 +250,7 @@ class FreqaiExampleStrategy(IStrategy):
enter_short_conditions = [ enter_short_conditions = [
df["do_predict"] == 1, df["do_predict"] == 1,
df["&-s_close"] < df[f"sell_roi_{self.std_dev_multiplier_sell.value}"], df["&-s_close"] < -0.01,
] ]
if enter_short_conditions: if enter_short_conditions:
@@ -276,14 +263,14 @@ class FreqaiExampleStrategy(IStrategy):
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
exit_long_conditions = [ exit_long_conditions = [
df["do_predict"] == 1, df["do_predict"] == 1,
df["&-s_close"] < df[f"sell_roi_{self.std_dev_multiplier_sell.value}"] * 0.25, df["&-s_close"] < 0
] ]
if exit_long_conditions: if exit_long_conditions:
df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1 df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1
exit_short_conditions = [ exit_short_conditions = [
df["do_predict"] == 1, df["do_predict"] == 1,
df["&-s_close"] > df[f"target_roi_{self.std_dev_multiplier_buy.value}"] * 0.25, df["&-s_close"] > 0
] ]
if exit_short_conditions: if exit_short_conditions:
df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1 df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1
+1 -1
View File
@@ -39,7 +39,7 @@
}, },
{{ exchange | indent(4) }}, {{ exchange | indent(4) }},
"pairlists": [ "pairlists": [
{{ '{"method": "StaticPairList"}' if exchange_name == 'bittrex' else volume_pairlist }} {{ volume_pairlist }}
], ],
"telegram": { "telegram": {
"enabled": {{ telegram | lower }}, "enabled": {{ telegram | lower }},
+7 -6
View File
@@ -6,7 +6,7 @@ from copy import deepcopy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Dict, NamedTuple, Optional from typing import Dict, NamedTuple, Optional
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config, IntOrInf
from freqtrade.enums import RunMode, TradingMode from freqtrade.enums import RunMode, TradingMode
from freqtrade.exceptions import DependencyException from freqtrade.exceptions import DependencyException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@@ -262,15 +262,15 @@ class Wallets:
return min(self.get_total_stake_amount() - Trade.total_open_trades_stakes(), free) return min(self.get_total_stake_amount() - Trade.total_open_trades_stakes(), free)
def _calculate_unlimited_stake_amount(self, available_amount: float, def _calculate_unlimited_stake_amount(self, available_amount: float,
val_tied_up: float) -> float: val_tied_up: float, max_open_trades: IntOrInf) -> float:
""" """
Calculate stake amount for "unlimited" stake amount Calculate stake amount for "unlimited" stake amount
:return: 0 if max number of trades reached, else stake_amount to use. :return: 0 if max number of trades reached, else stake_amount to use.
""" """
if self._config['max_open_trades'] == 0: if max_open_trades == 0:
return 0 return 0
possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades'] possible_stake = (available_amount + val_tied_up) / max_open_trades
# Theoretical amount can be above available amount - therefore limit to available amount! # Theoretical amount can be above available amount - therefore limit to available amount!
return min(possible_stake, available_amount) return min(possible_stake, available_amount)
@@ -298,7 +298,8 @@ class Wallets:
return stake_amount return stake_amount
def get_trade_stake_amount(self, pair: str, edge=None, update: bool = True) -> float: def get_trade_stake_amount(
self, pair: str, max_open_trades: IntOrInf, edge=None, update: bool = True) -> float:
""" """
Calculate stake amount for the trade Calculate stake amount for the trade
:return: float: Stake amount :return: float: Stake amount
@@ -322,7 +323,7 @@ class Wallets:
stake_amount = self._config['stake_amount'] stake_amount = self._config['stake_amount']
if stake_amount == UNLIMITED_STAKE_AMOUNT: if stake_amount == UNLIMITED_STAKE_AMOUNT:
stake_amount = self._calculate_unlimited_stake_amount( stake_amount = self._calculate_unlimited_stake_amount(
available_amount, val_tied_up) available_amount, val_tied_up, max_open_trades)
return self._check_available_stake_amount(stake_amount, available_amount) return self._check_available_stake_amount(stake_amount, available_amount)
+65
View File
@@ -2,6 +2,55 @@
requires = ["setuptools >= 64.0.0", "wheel"] requires = ["setuptools >= 64.0.0", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project]
name = "freqtrade"
dynamic = ["version", "dependencies", "optional-dependencies"]
authors = [
{name = "Freqtrade Team"},
{name = "Freqtrade Team", email = "freqtrade@protonmail.com"},
]
description = "Freqtrade - Crypto Trading Bot"
readme = "README.md"
requires-python = ">=3.9"
license = {text = "GPLv3"}
# license = "GPLv3"
classifiers = [
"Environment :: Console",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Operating System :: MacOS",
"Operating System :: Unix",
"Topic :: Office/Business :: Financial :: Investment",
]
[project.urls]
Homepage = "https://github.com/freqtrade/freqtrade"
Documentation = "https://freqtrade.io"
"Bug Tracker" = "https://github.com/freqtrade/freqtrade/issues"
[project.scripts]
freqtrade = "freqtrade.main:main"
[tool.setuptools]
include-package-data = true
zip-safe = false
[tool.setuptools.packages.find]
where = ["."]
include = ["freqtrade*"]
exclude = ["tests", "tests.*"]
namespaces = true
[tool.setuptools.dynamic]
version = {attr = "freqtrade.__version__"}
[tool.black] [tool.black]
line-length = 100 line-length = 100
exclude = ''' exclude = '''
@@ -31,6 +80,7 @@ skip_glob = ["**/.env*", "**/env/*", "**/.venv/*", "**/docs/*", "**/user_data/*"
[tool.pytest.ini_options] [tool.pytest.ini_options]
asyncio_mode = "auto" asyncio_mode = "auto"
addopts = "--dist loadscope"
[tool.mypy] [tool.mypy]
ignore_missing_imports = true ignore_missing_imports = true
@@ -93,3 +143,18 @@ max-complexity = 12
[tool.ruff.per-file-ignores] [tool.ruff.per-file-ignores]
"tests/*" = ["S"] "tests/*" = ["S"]
[tool.flake8]
# Default from https://flake8.pycqa.org/en/latest/user/options.html#cmdoption-flake8-ignore
# minus E226
ignore = ["E121","E123","E126","E24","E704","W503","W504"]
max-line-length = 100
max-complexity = 12
exclude = [
".git",
"__pycache__",
".eggs",
"user_data",
".venv",
".env",
]
+6 -5
View File
@@ -7,20 +7,21 @@
-r docs/requirements-docs.txt -r docs/requirements-docs.txt
coveralls==3.3.1 coveralls==3.3.1
ruff==0.1.6 ruff==0.1.9
mypy==1.7.1 mypy==1.8.0
pre-commit==3.5.0 pre-commit==3.6.0
pytest==7.4.3 pytest==7.4.3
pytest-asyncio==0.21.1 pytest-asyncio==0.21.1
pytest-cov==4.1.0 pytest-cov==4.1.0
pytest-mock==3.12.0 pytest-mock==3.12.0
pytest-random-order==1.1.0 pytest-random-order==1.1.0
isort==5.12.0 pytest-xdist==3.5.0
isort==5.13.2
# For datetime mocking # For datetime mocking
time-machine==2.13.0 time-machine==2.13.0
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==7.11.0 nbconvert==7.13.1
# mypy types # mypy types
types-cachetools==5.3.0.7 types-cachetools==5.3.0.7
+1 -1
View File
@@ -2,7 +2,7 @@
-r requirements-freqai.txt -r requirements-freqai.txt
# Required for freqai-rl # Required for freqai-rl
torch==2.0.1 torch==2.1.2
#until these branches will be released we can use this #until these branches will be released we can use this
gymnasium==0.29.1 gymnasium==0.29.1
stable_baselines3==2.2.1 stable_baselines3==2.2.1
+3 -3
View File
@@ -3,10 +3,10 @@
-r requirements-plot.txt -r requirements-plot.txt
# Required for freqai # Required for freqai
scikit-learn==1.1.3 scikit-learn==1.3.2
joblib==1.3.2 joblib==1.3.2
catboost==1.2.2; 'arm' not in platform_machine catboost==1.2.2; 'arm' not in platform_machine
lightgbm==4.1.0 lightgbm==4.2.0
xgboost==2.0.2 xgboost==2.0.3
tensorboard==2.15.1 tensorboard==2.15.1
datasieve==0.1.7 datasieve==0.1.7
+2 -2
View File
@@ -3,6 +3,6 @@
# Required for hyperopt # Required for hyperopt
scipy==1.11.4 scipy==1.11.4
scikit-learn==1.1.3 scikit-learn==1.3.2
scikit-optimize==0.9.0 ft-scikit-optimize==0.9.2
filelock==3.13.1 filelock==3.13.1
+10 -10
View File
@@ -1,12 +1,12 @@
numpy==1.26.2 numpy==1.26.2
pandas==2.1.3 pandas==2.1.4
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==4.1.66 ccxt==4.1.98
cryptography==41.0.7 cryptography==41.0.7
aiohttp==3.9.1 aiohttp==3.9.1
SQLAlchemy==2.0.23 SQLAlchemy==2.0.23
python-telegram-bot==20.6 python-telegram-bot==20.7
# can't be hard-pinned due to telegram-bot pinning httpx with ~ # can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1 httpx>=0.24.1
arrow==1.3.0 arrow==1.3.0
@@ -15,20 +15,20 @@ requests==2.31.0
urllib3==2.1.0 urllib3==2.1.0
jsonschema==4.20.0 jsonschema==4.20.0
TA-Lib==0.4.28 TA-Lib==0.4.28
technical==1.4.0 technical==1.4.2
tabulate==0.9.0 tabulate==0.9.0
pycoingecko==3.1.0 pycoingecko==3.1.0
jinja2==3.1.2 jinja2==3.1.2
tables==3.9.1 tables==3.9.1
joblib==1.3.2 joblib==1.3.2
rich==13.7.0 rich==13.7.0
pyarrow==14.0.1; platform_machine != 'armv7l' pyarrow==14.0.2; platform_machine != 'armv7l'
# find first, C search in arrays # find first, C search in arrays
py_find_1st==1.1.6 py_find_1st==1.1.6
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.13 python-rapidjson==1.14
# Properly format api responses # Properly format api responses
orjson==3.9.10 orjson==3.9.10
@@ -36,12 +36,12 @@ orjson==3.9.10
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.104.1 fastapi==0.105.0
pydantic==2.5.2 pydantic==2.5.3
uvicorn==0.24.0.post1 uvicorn==0.25.0
pyjwt==2.8.0 pyjwt==2.8.0
aiofiles==23.2.1 aiofiles==23.2.1
psutil==5.9.6 psutil==5.9.7
# Support for colorized terminal output # Support for colorized terminal output
colorama==0.4.6 colorama==0.4.6
-53
View File
@@ -1,53 +0,0 @@
[metadata]
name = freqtrade
version = attr: freqtrade.__version__
author = Freqtrade Team
author_email = freqtrade@protonmail.com
description = Freqtrade - Crypto Trading Bot
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/freqtrade/freqtrade
project_urls =
Bug Tracker = https://github.com/freqtrade/freqtrade/issues
license = GPLv3
classifiers =
Environment :: Console
Intended Audience :: Science/Research
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Operating System :: MacOS
Operating System :: Unix
Topic :: Office/Business :: Financial :: Investment
[options]
zip_safe = False
include_package_data = True
tests_require =
pytest
pytest-asyncio
pytest-cov
pytest-mock
packages = find:
python_requires = >=3.9
[options.entry_points]
console_scripts =
freqtrade = freqtrade.main:main
[flake8]
# Default from https://flake8.pycqa.org/en/latest/user/options.html#cmdoption-flake8-ignore
# minus E226
ignore = E121,E123,E126,E24,E704,W503,W504
max-line-length = 100
max-complexity = 12
exclude =
.git,
__pycache__,
.eggs,
user_data,
.venv
.env
+3 -2
View File
@@ -5,8 +5,8 @@ from setuptools import setup
plot = ['plotly>=4.0'] plot = ['plotly>=4.0']
hyperopt = [ hyperopt = [
'scipy', 'scipy',
'scikit-learn<=1.1.3', 'scikit-learn',
'scikit-optimize>=0.7.0', 'ft-scikit-optimize>=0.9.2',
'filelock', 'filelock',
] ]
@@ -122,4 +122,5 @@ setup(
'freqai_rl': freqai_rl, 'freqai_rl': freqai_rl,
'all': all_extra, 'all': all_extra,
}, },
url="https://github.com/freqtrade/freqtrade",
) )
+1 -1
View File
@@ -30,7 +30,7 @@ def test_validate_is_int():
assert not validate_is_int('-ee') assert not validate_is_int('-ee')
@pytest.mark.parametrize('exchange', ['bittrex', 'binance', 'kraken']) @pytest.mark.parametrize('exchange', ['bybit', 'binance', 'kraken'])
def test_start_new_config(mocker, caplog, exchange): def test_start_new_config(mocker, caplog, exchange):
wt_mock = mocker.patch.object(Path, "write_text", MagicMock()) wt_mock = mocker.patch.object(Path, "write_text", MagicMock())
mocker.patch.object(Path, "exists", MagicMock(return_value=True)) mocker.patch.object(Path, "exists", MagicMock(return_value=True))
+53 -53
View File
@@ -32,7 +32,7 @@ from tests.conftest_trades import MOCK_TRADE_COUNT
def test_setup_utils_configuration(): def test_setup_utils_configuration():
args = [ args = [
'list-exchanges', '--config', 'config_examples/config_bittrex.example.json', 'list-exchanges', '--config', 'tests/testdata/testconfigs/main_test_config.json',
] ]
config = setup_utils_configuration(get_args(args), RunMode.OTHER) config = setup_utils_configuration(get_args(args), RunMode.OTHER)
@@ -49,7 +49,7 @@ def test_start_trading_fail(mocker, caplog):
exitmock = mocker.patch("freqtrade.worker.Worker.exit", MagicMock()) exitmock = mocker.patch("freqtrade.worker.Worker.exit", MagicMock())
args = [ args = [
'trade', 'trade',
'-c', 'config_examples/config_bittrex.example.json' '-c', 'tests/testdata/testconfigs/main_test_config.json'
] ]
start_trading(get_args(args)) start_trading(get_args(args))
assert exitmock.call_count == 1 assert exitmock.call_count == 1
@@ -68,7 +68,7 @@ def test_start_webserver(mocker, caplog):
args = [ args = [
'webserver', 'webserver',
'-c', 'config_examples/config_bittrex.example.json' '-c', 'tests/testdata/testconfigs/main_test_config.json'
] ]
start_webserver(get_args(args)) start_webserver(get_args(args))
assert api_server_mock.call_count == 1 assert api_server_mock.call_count == 1
@@ -84,7 +84,7 @@ def test_list_exchanges(capsys):
captured = capsys.readouterr() captured = capsys.readouterr()
assert re.match(r"Exchanges available for Freqtrade.*", captured.out) assert re.match(r"Exchanges available for Freqtrade.*", captured.out)
assert re.search(r".*binance.*", captured.out) assert re.search(r".*binance.*", captured.out)
assert re.search(r".*bittrex.*", captured.out) assert re.search(r".*bybit.*", captured.out)
# Test with --one-column # Test with --one-column
args = [ args = [
@@ -95,7 +95,7 @@ def test_list_exchanges(capsys):
start_list_exchanges(get_args(args)) start_list_exchanges(get_args(args))
captured = capsys.readouterr() captured = capsys.readouterr()
assert re.search(r"^binance$", captured.out, re.MULTILINE) assert re.search(r"^binance$", captured.out, re.MULTILINE)
assert re.search(r"^bittrex$", captured.out, re.MULTILINE) assert re.search(r"^bybit$", captured.out, re.MULTILINE)
# Test with --all # Test with --all
args = [ args = [
@@ -107,7 +107,7 @@ def test_list_exchanges(capsys):
captured = capsys.readouterr() captured = capsys.readouterr()
assert re.match(r"All exchanges supported by the ccxt library.*", captured.out) assert re.match(r"All exchanges supported by the ccxt library.*", captured.out)
assert re.search(r".*binance.*", captured.out) assert re.search(r".*binance.*", captured.out)
assert re.search(r".*bittrex.*", captured.out) assert re.search(r".*bingx.*", captured.out)
assert re.search(r".*bitmex.*", captured.out) assert re.search(r".*bitmex.*", captured.out)
# Test with --one-column --all # Test with --one-column --all
@@ -120,7 +120,7 @@ def test_list_exchanges(capsys):
start_list_exchanges(get_args(args)) start_list_exchanges(get_args(args))
captured = capsys.readouterr() captured = capsys.readouterr()
assert re.search(r"^binance$", captured.out, re.MULTILINE) assert re.search(r"^binance$", captured.out, re.MULTILINE)
assert re.search(r"^bittrex$", captured.out, re.MULTILINE) assert re.search(r"^bingx$", captured.out, re.MULTILINE)
assert re.search(r"^bitmex$", captured.out, re.MULTILINE) assert re.search(r"^bitmex$", captured.out, re.MULTILINE)
@@ -133,7 +133,7 @@ def test_list_timeframes(mocker, capsys):
'1h': 'hour', '1h': 'hour',
'1d': 'day', '1d': 'day',
} }
patch_exchange(mocker, api_mock=api_mock, id='bittrex') patch_exchange(mocker, api_mock=api_mock, id='bybit')
args = [ args = [
"list-timeframes", "list-timeframes",
] ]
@@ -143,25 +143,25 @@ def test_list_timeframes(mocker, capsys):
match=r"This command requires a configured exchange.*"): match=r"This command requires a configured exchange.*"):
start_list_timeframes(pargs) start_list_timeframes(pargs)
# Test with --config config_examples/config_bittrex.example.json # Test with --config tests/testdata/testconfigs/main_test_config.json
args = [ args = [
"list-timeframes", "list-timeframes",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
] ]
start_list_timeframes(get_args(args)) start_list_timeframes(get_args(args))
captured = capsys.readouterr() captured = capsys.readouterr()
assert re.match("Timeframes available for the exchange `Bittrex`: " assert re.match("Timeframes available for the exchange `Bybit`: "
"1m, 5m, 30m, 1h, 1d", "1m, 5m, 30m, 1h, 1d",
captured.out) captured.out)
# Test with --exchange bittrex # Test with --exchange bybit
args = [ args = [
"list-timeframes", "list-timeframes",
"--exchange", "bittrex", "--exchange", "bybit",
] ]
start_list_timeframes(get_args(args)) start_list_timeframes(get_args(args))
captured = capsys.readouterr() captured = capsys.readouterr()
assert re.match("Timeframes available for the exchange `Bittrex`: " assert re.match("Timeframes available for the exchange `Bybit`: "
"1m, 5m, 30m, 1h, 1d", "1m, 5m, 30m, 1h, 1d",
captured.out) captured.out)
@@ -190,7 +190,7 @@ def test_list_timeframes(mocker, capsys):
# Test with --one-column # Test with --one-column
args = [ args = [
"list-timeframes", "list-timeframes",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--one-column", "--one-column",
] ]
start_list_timeframes(get_args(args)) start_list_timeframes(get_args(args))
@@ -217,7 +217,7 @@ def test_list_timeframes(mocker, capsys):
def test_list_markets(mocker, markets_static, capsys): def test_list_markets(mocker, markets_static, capsys):
api_mock = MagicMock() api_mock = MagicMock()
patch_exchange(mocker, api_mock=api_mock, id='bittrex', mock_markets=markets_static) patch_exchange(mocker, api_mock=api_mock, id='binance', mock_markets=markets_static)
# Test with no --config # Test with no --config
args = [ args = [
@@ -229,15 +229,15 @@ def test_list_markets(mocker, markets_static, capsys):
match=r"This command requires a configured exchange.*"): match=r"This command requires a configured exchange.*"):
start_list_markets(pargs, False) start_list_markets(pargs, False)
# Test with --config config_examples/config_bittrex.example.json # Test with --config tests/testdata/testconfigs/main_test_config.json
args = [ args = [
"list-markets", "list-markets",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 12 active markets: " assert ("Exchange Binance has 12 active markets: "
"ADA/USDT:USDT, BLK/BTC, ETH/BTC, ETH/USDT, ETH/USDT:USDT, LTC/BTC, " "ADA/USDT:USDT, BLK/BTC, ETH/BTC, ETH/USDT, ETH/USDT:USDT, LTC/BTC, "
"LTC/ETH, LTC/USD, NEO/BTC, TKN/BTC, XLTCUSDT, XRP/BTC.\n" "LTC/ETH, LTC/USD, NEO/BTC, TKN/BTC, XLTCUSDT, XRP/BTC.\n"
in captured.out) in captured.out)
@@ -255,16 +255,16 @@ def test_list_markets(mocker, markets_static, capsys):
assert re.match("\nExchange Binance has 12 active markets:\n", assert re.match("\nExchange Binance has 12 active markets:\n",
captured.out) captured.out)
patch_exchange(mocker, api_mock=api_mock, id="bittrex", mock_markets=markets_static) patch_exchange(mocker, api_mock=api_mock, id="binance", mock_markets=markets_static)
# Test with --all: all markets # Test with --all: all markets
args = [ args = [
"list-markets", "--all", "list-markets", "--all",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 14 markets: " assert ("Exchange Binance has 14 markets: "
"ADA/USDT:USDT, BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, ETH/USDT:USDT, " "ADA/USDT:USDT, BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, ETH/USDT:USDT, "
"LTC/BTC, LTC/ETH, LTC/USD, LTC/USDT, NEO/BTC, TKN/BTC, XLTCUSDT, XRP/BTC.\n" "LTC/BTC, LTC/ETH, LTC/USD, LTC/USDT, NEO/BTC, TKN/BTC, XLTCUSDT, XRP/BTC.\n"
in captured.out) in captured.out)
@@ -272,24 +272,24 @@ def test_list_markets(mocker, markets_static, capsys):
# Test list-pairs subcommand: active pairs # Test list-pairs subcommand: active pairs
args = [ args = [
"list-pairs", "list-pairs",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), True) start_list_markets(get_args(args), True)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 9 active pairs: " assert ("Exchange Binance has 9 active pairs: "
"BLK/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, NEO/BTC, TKN/BTC, XRP/BTC.\n" "BLK/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, NEO/BTC, TKN/BTC, XRP/BTC.\n"
in captured.out) in captured.out)
# Test list-pairs subcommand with --all: all pairs # Test list-pairs subcommand with --all: all pairs
args = [ args = [
"list-pairs", "--all", "list-pairs", "--all",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), True) start_list_markets(get_args(args), True)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 11 pairs: " assert ("Exchange Binance has 11 pairs: "
"BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, LTC/USDT, NEO/BTC, " "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, LTC/USDT, NEO/BTC, "
"TKN/BTC, XRP/BTC.\n" "TKN/BTC, XRP/BTC.\n"
in captured.out) in captured.out)
@@ -297,133 +297,133 @@ def test_list_markets(mocker, markets_static, capsys):
# active markets, base=ETH, LTC # active markets, base=ETH, LTC
args = [ args = [
"list-markets", "list-markets",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--base", "ETH", "LTC", "--base", "ETH", "LTC",
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 7 active markets with ETH, LTC as base currencies: " assert ("Exchange Binance has 7 active markets with ETH, LTC as base currencies: "
"ETH/BTC, ETH/USDT, ETH/USDT:USDT, LTC/BTC, LTC/ETH, LTC/USD, XLTCUSDT.\n" "ETH/BTC, ETH/USDT, ETH/USDT:USDT, LTC/BTC, LTC/ETH, LTC/USD, XLTCUSDT.\n"
in captured.out) in captured.out)
# active markets, base=LTC # active markets, base=LTC
args = [ args = [
"list-markets", "list-markets",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--base", "LTC", "--base", "LTC",
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 4 active markets with LTC as base currency: " assert ("Exchange Binance has 4 active markets with LTC as base currency: "
"LTC/BTC, LTC/ETH, LTC/USD, XLTCUSDT.\n" "LTC/BTC, LTC/ETH, LTC/USD, XLTCUSDT.\n"
in captured.out) in captured.out)
# active markets, quote=USDT, USD # active markets, quote=USDT, USD
args = [ args = [
"list-markets", "list-markets",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--quote", "USDT", "USD", "--quote", "USDT", "USD",
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 5 active markets with USDT, USD as quote currencies: " assert ("Exchange Binance has 5 active markets with USDT, USD as quote currencies: "
"ADA/USDT:USDT, ETH/USDT, ETH/USDT:USDT, LTC/USD, XLTCUSDT.\n" "ADA/USDT:USDT, ETH/USDT, ETH/USDT:USDT, LTC/USD, XLTCUSDT.\n"
in captured.out) in captured.out)
# active markets, quote=USDT # active markets, quote=USDT
args = [ args = [
"list-markets", "list-markets",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--quote", "USDT", "--quote", "USDT",
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 4 active markets with USDT as quote currency: " assert ("Exchange Binance has 4 active markets with USDT as quote currency: "
"ADA/USDT:USDT, ETH/USDT, ETH/USDT:USDT, XLTCUSDT.\n" "ADA/USDT:USDT, ETH/USDT, ETH/USDT:USDT, XLTCUSDT.\n"
in captured.out) in captured.out)
# active markets, base=LTC, quote=USDT # active markets, base=LTC, quote=USDT
args = [ args = [
"list-markets", "list-markets",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--base", "LTC", "--quote", "USDT", "--base", "LTC", "--quote", "USDT",
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 1 active market with LTC as base currency and " assert ("Exchange Binance has 1 active market with LTC as base currency and "
"with USDT as quote currency: XLTCUSDT.\n" "with USDT as quote currency: XLTCUSDT.\n"
in captured.out) in captured.out)
# active pairs, base=LTC, quote=USDT # active pairs, base=LTC, quote=USDT
args = [ args = [
"list-pairs", "list-pairs",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--base", "LTC", "--quote", "USD", "--base", "LTC", "--quote", "USD",
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), True) start_list_markets(get_args(args), True)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 1 active pair with LTC as base currency and " assert ("Exchange Binance has 1 active pair with LTC as base currency and "
"with USD as quote currency: LTC/USD.\n" "with USD as quote currency: LTC/USD.\n"
in captured.out) in captured.out)
# active markets, base=LTC, quote=USDT, NONEXISTENT # active markets, base=LTC, quote=USDT, NONEXISTENT
args = [ args = [
"list-markets", "list-markets",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--base", "LTC", "--quote", "USDT", "NONEXISTENT", "--base", "LTC", "--quote", "USDT", "NONEXISTENT",
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 1 active market with LTC as base currency and " assert ("Exchange Binance has 1 active market with LTC as base currency and "
"with USDT, NONEXISTENT as quote currencies: XLTCUSDT.\n" "with USDT, NONEXISTENT as quote currencies: XLTCUSDT.\n"
in captured.out) in captured.out)
# active markets, base=LTC, quote=NONEXISTENT # active markets, base=LTC, quote=NONEXISTENT
args = [ args = [
"list-markets", "list-markets",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--base", "LTC", "--quote", "NONEXISTENT", "--base", "LTC", "--quote", "NONEXISTENT",
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 0 active markets with LTC as base currency and " assert ("Exchange Binance has 0 active markets with LTC as base currency and "
"with NONEXISTENT as quote currency.\n" "with NONEXISTENT as quote currency.\n"
in captured.out) in captured.out)
# Test tabular output # Test tabular output
args = [ args = [
"list-markets", "list-markets",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 12 active markets:\n" assert ("Exchange Binance has 12 active markets:\n"
in captured.out) in captured.out)
# Test tabular output, no markets found # Test tabular output, no markets found
args = [ args = [
"list-markets", "list-markets",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--base", "LTC", "--quote", "NONEXISTENT", "--base", "LTC", "--quote", "NONEXISTENT",
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 0 active markets with LTC as base currency and " assert ("Exchange Binance has 0 active markets with LTC as base currency and "
"with NONEXISTENT as quote currency.\n" "with NONEXISTENT as quote currency.\n"
in captured.out) in captured.out)
# Test --print-json # Test --print-json
args = [ args = [
"list-markets", "list-markets",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--print-json" "--print-json"
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
@@ -435,7 +435,7 @@ def test_list_markets(mocker, markets_static, capsys):
# Test --print-csv # Test --print-csv
args = [ args = [
"list-markets", "list-markets",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--print-csv" "--print-csv"
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
@@ -447,7 +447,7 @@ def test_list_markets(mocker, markets_static, capsys):
# Test --one-column # Test --one-column
args = [ args = [
"list-markets", "list-markets",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--one-column" "--one-column"
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
@@ -459,7 +459,7 @@ def test_list_markets(mocker, markets_static, capsys):
# Test --one-column # Test --one-column
args = [ args = [
"list-markets", "list-markets",
'--config', 'config_examples/config_bittrex.example.json', '--config', 'tests/testdata/testconfigs/main_test_config.json',
"--one-column" "--one-column"
] ]
with pytest.raises(OperationalException, match=r"Cannot get markets.*"): with pytest.raises(OperationalException, match=r"Cannot get markets.*"):
@@ -971,7 +971,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = [ args = [
'test-pairlist', 'test-pairlist',
'-c', 'config_examples/config_bittrex.example.json' '-c', 'tests/testdata/testconfigs/main_test_config.json'
] ]
start_test_pairlist(get_args(args)) start_test_pairlist(get_args(args))
@@ -985,7 +985,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
args = [ args = [
'test-pairlist', 'test-pairlist',
'-c', 'config_examples/config_bittrex.example.json', '-c', 'tests/testdata/testconfigs/main_test_config.json',
'--one-column', '--one-column',
] ]
start_test_pairlist(get_args(args)) start_test_pairlist(get_args(args))
@@ -994,7 +994,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
args = [ args = [
'test-pairlist', 'test-pairlist',
'-c', 'config_examples/config_bittrex.example.json', '-c', 'tests/testdata/testconfigs/main_test_config.json',
'--print-json', '--print-json',
] ]
start_test_pairlist(get_args(args)) start_test_pairlist(get_args(args))
+29 -3
View File
@@ -11,6 +11,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import pytest import pytest
from xdist.scheduler.loadscope import LoadScopeScheduling
from freqtrade import constants from freqtrade import constants
from freqtrade.commands import Arguments from freqtrade.commands import Arguments
@@ -56,6 +57,27 @@ def pytest_configure(config):
setattr(config.option, 'markexpr', 'not longrun') setattr(config.option, 'markexpr', 'not longrun')
class FixtureScheduler(LoadScopeScheduling):
# Based on the suggestion in
# https://github.com/pytest-dev/pytest-xdist/issues/18
def _split_scope(self, nodeid):
if 'exchange_online' in nodeid:
try:
# Extract exchange ID from nodeid
exchange_id = nodeid.split('[')[1].split('-')[0].rstrip(']')
return exchange_id
except Exception as e:
print(e)
pass
return nodeid
def pytest_xdist_make_scheduler(config, log):
return FixtureScheduler(config, log)
def log_has(line, logs): def log_has(line, logs):
"""Check if line is found on some caplog's message.""" """Check if line is found on some caplog's message."""
return any(line == message for message in logs.messages) return any(line == message for message in logs.messages)
@@ -87,11 +109,15 @@ def get_args(args):
def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'): def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'):
np.random.seed(42) np.random.seed(42)
tf_mins = timeframe_to_minutes(timeframe)
base = np.random.normal(20, 2, size=size) base = np.random.normal(20, 2, size=size)
if timeframe == '1M':
date = pd.date_range(start, periods=size, freq=f'{tf_mins}min', tz='UTC') date = pd.date_range(start, periods=size, freq='1MS', tz='UTC')
elif timeframe == '1w':
date = pd.date_range(start, periods=size, freq='1W-MON', tz='UTC')
else:
tf_mins = timeframe_to_minutes(timeframe)
date = pd.date_range(start, periods=size, freq=f'{tf_mins}min', tz='UTC')
df = pd.DataFrame({ df = pd.DataFrame({
'date': date, 'date': date,
'open': base, 'open': base,
+2 -2
View File
@@ -64,7 +64,7 @@ def test_ohlcv_fill_up_missing_data(testdatadir, caplog):
# Column names should not change # Column names should not change
assert (data.columns == data2.columns).all() assert (data.columns == data2.columns).all()
assert log_has_re(f"Missing data fillup for UNITTEST/BTC: before: " assert log_has_re(f"Missing data fillup for UNITTEST/BTC, 1m: before: "
f"{len(data)} - after: {len(data2)}.*", caplog) f"{len(data)} - after: {len(data2)}.*", caplog)
# Test fillup actually fixes invalid backtest data # Test fillup actually fixes invalid backtest data
@@ -128,7 +128,7 @@ def test_ohlcv_fill_up_missing_data2(caplog):
# Column names should not change # Column names should not change
assert (data.columns == data2.columns).all() assert (data.columns == data2.columns).all()
assert log_has_re(f"Missing data fillup for UNITTEST/BTC: before: " assert log_has_re(f"Missing data fillup for UNITTEST/BTC, {timeframe}: before: "
f"{len(data)} - after: {len(data2)}.*", caplog) f"{len(data)} - after: {len(data2)}.*", caplog)
+3 -3
View File
@@ -513,11 +513,11 @@ def test_gethandlerclass():
def test_get_datahandler(testdatadir): def test_get_datahandler(testdatadir):
dh = get_datahandler(testdatadir, 'json') dh = get_datahandler(testdatadir, 'json')
assert type(dh) == JsonDataHandler assert isinstance(dh, JsonDataHandler)
dh = get_datahandler(testdatadir, 'jsongz') dh = get_datahandler(testdatadir, 'jsongz')
assert type(dh) == JsonGzDataHandler assert isinstance(dh, JsonGzDataHandler)
dh1 = get_datahandler(testdatadir, 'jsongz', dh) dh1 = get_datahandler(testdatadir, 'jsongz', dh)
assert id(dh1) == id(dh) assert id(dh1) == id(dh)
dh = get_datahandler(testdatadir, 'hdf5') dh = get_datahandler(testdatadir, 'hdf5')
assert type(dh) == HDF5DataHandler assert isinstance(dh, HDF5DataHandler)
+86
View File
@@ -500,3 +500,89 @@ def test_dp__add_external_df(default_conf_usdt):
# 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00 # 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00
assert isinstance(res[1], int) assert isinstance(res[1], int)
assert res[1] == 0 assert res[1] == 0
def test_dp_get_required_startup(default_conf_usdt):
timeframe = '1h'
default_conf_usdt["timeframe"] = timeframe
dp = DataProvider(default_conf_usdt, None)
# No FreqAI config
assert dp.get_required_startup('5m', False) == 0
assert dp.get_required_startup('1h', False) == 0
assert dp.get_required_startup('1d', False) == 0
assert dp.get_required_startup('1d', True) == 0
assert dp.get_required_startup('1d') == 0
dp._config['startup_candle_count'] = 20
assert dp.get_required_startup('5m', False) == 20
assert dp.get_required_startup('5m', True) == 20
assert dp.get_required_startup('1h', False) == 20
assert dp.get_required_startup('1h') == 20
# With freqAI config
dp._config['freqai'] = {
'enabled': True,
'train_period_days': 20,
'feature_parameters': {
'indicator_periods_candles': [
5,
20,
]
}
}
assert dp.get_required_startup('5m', False) == 20
assert dp.get_required_startup('5m', True) == 5780
assert dp.get_required_startup('1h', False) == 20
assert dp.get_required_startup('1h', True) == 500
assert dp.get_required_startup('1d', False) == 20
assert dp.get_required_startup('1d', True) == 40
assert dp.get_required_startup('1d') == 40
# FreqAI kindof ignores startup_candle_count if it's below indicator_periods_candles
dp._config['startup_candle_count'] = 0
assert dp.get_required_startup('5m', False) == 20
assert dp.get_required_startup('5m', True) == 5780
assert dp.get_required_startup('1h', False) == 20
assert dp.get_required_startup('1h', True) == 500
assert dp.get_required_startup('1d', False) == 20
assert dp.get_required_startup('1d', True) == 40
assert dp.get_required_startup('1d') == 40
dp._config['freqai']['feature_parameters']['indicator_periods_candles'][1] = 50
assert dp.get_required_startup('5m', False) == 50
assert dp.get_required_startup('5m', True) == 5810
assert dp.get_required_startup('1h', False) == 50
assert dp.get_required_startup('1h', True) == 530
assert dp.get_required_startup('1d', False) == 50
assert dp.get_required_startup('1d', True) == 70
assert dp.get_required_startup('1d') == 70
# scenario from issue https://github.com/freqtrade/freqtrade/issues/9432
dp._config['freqai'] = {
'enabled': True,
'train_period_days': 180,
'feature_parameters': {
'indicator_periods_candles': [
10,
20,
]
}
}
dp._config['startup_candle_count'] = 40
assert dp.get_required_startup('5m', False) == 40
assert dp.get_required_startup('5m', True) == 51880
assert dp.get_required_startup('1h', False) == 40
assert dp.get_required_startup('1h', True) == 4360
assert dp.get_required_startup('1d', False) == 40
assert dp.get_required_startup('1d', True) == 220
assert dp.get_required_startup('1d') == 220
+54 -25
View File
@@ -13,7 +13,7 @@ from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError, from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError,
InsufficientFundsError, InvalidOrderException, InsufficientFundsError, InvalidOrderException,
OperationalException, PricingError, TemporaryError) OperationalException, PricingError, TemporaryError)
from freqtrade.exchange import (Binance, Bittrex, Exchange, Kraken, market_is_active, from freqtrade.exchange import (Binance, Bybit, Exchange, Kraken, market_is_active,
timeframe_to_prev_date) timeframe_to_prev_date)
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT, from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
calculate_backoff, remove_exchange_credentials) calculate_backoff, remove_exchange_credentials)
@@ -24,7 +24,7 @@ from tests.conftest import (EXMS, generate_test_data_raw, get_mock_coro, get_pat
# Make sure to always keep one exchange here which is NOT subclassed!! # Make sure to always keep one exchange here which is NOT subclassed!!
EXCHANGES = ['bittrex', 'binance', 'kraken', 'gate', 'kucoin', 'bybit', 'okx'] EXCHANGES = ['binance', 'kraken', 'gate', 'kucoin', 'bybit', 'okx']
get_entry_rate_data = [ get_entry_rate_data = [
('other', 20, 19, 10, 0.0, 20), # Full ask side ('other', 20, 19, 10, 0.0, 20), # Full ask side
@@ -228,10 +228,10 @@ def test_exchange_resolver(default_conf, mocker, caplog):
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
caplog.clear() caplog.clear()
default_conf['exchange']['name'] = 'Bittrex' default_conf['exchange']['name'] = 'Bybit'
exchange = ExchangeResolver.load_exchange(default_conf) exchange = ExchangeResolver.load_exchange(default_conf)
assert isinstance(exchange, Exchange) assert isinstance(exchange, Exchange)
assert isinstance(exchange, Bittrex) assert isinstance(exchange, Bybit)
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
caplog) caplog)
caplog.clear() caplog.clear()
@@ -263,8 +263,8 @@ def test_exchange_resolver(default_conf, mocker, caplog):
def test_validate_order_time_in_force(default_conf, mocker, caplog): def test_validate_order_time_in_force(default_conf, mocker, caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
# explicitly test bittrex, exchanges implementing other policies need separate tests # explicitly test bybit, exchanges implementing other policies need separate tests
ex = get_patched_exchange(mocker, default_conf, id="bittrex") ex = get_patched_exchange(mocker, default_conf, id="bybit")
tif = { tif = {
"buy": "gtc", "buy": "gtc",
"sell": "gtc", "sell": "gtc",
@@ -273,11 +273,14 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog):
ex.validate_order_time_in_force(tif) ex.validate_order_time_in_force(tif)
tif2 = { tif2 = {
"buy": "fok", "buy": "fok",
"sell": "ioc", "sell": "ioc22",
} }
with pytest.raises(OperationalException, match=r"Time in force.*not supported for .*"): with pytest.raises(OperationalException, match=r"Time in force.*not supported for .*"):
ex.validate_order_time_in_force(tif2) ex.validate_order_time_in_force(tif2)
tif2 = {
"buy": "fok",
"sell": "ioc",
}
# Patch to see if this will pass if the values are in the ft dict # Patch to see if this will pass if the values are in the ft dict
ex._ft_has.update({"order_time_in_force": ["GTC", "FOK", "IOC"]}) ex._ft_has.update({"order_time_in_force": ["GTC", "FOK", "IOC"]})
ex.validate_order_time_in_force(tif2) ex.validate_order_time_in_force(tif2)
@@ -915,7 +918,6 @@ def test_validate_ordertypes(default_conf, mocker):
mocker.patch(f'{EXMS}.validate_timeframes') mocker.patch(f'{EXMS}.validate_timeframes')
mocker.patch(f'{EXMS}.validate_stakecurrency') mocker.patch(f'{EXMS}.validate_stakecurrency')
mocker.patch(f'{EXMS}.validate_pricing') mocker.patch(f'{EXMS}.validate_pricing')
mocker.patch(f'{EXMS}.name', 'Bittrex')
default_conf['order_types'] = { default_conf['order_types'] = {
'entry': 'limit', 'entry': 'limit',
@@ -1977,6 +1979,34 @@ def test_fetch_ticker(default_conf, mocker, exchange_name):
exchange.fetch_ticker(pair='XRP/ETH') exchange.fetch_ticker(pair='XRP/ETH')
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test___now_is_time_to_refresh(default_conf, mocker, exchange_name, time_machine):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
pair = 'BTC/USDT'
candle_type = CandleType.SPOT
start_dt = datetime(2023, 12, 1, 0, 10, 0, tzinfo=timezone.utc)
time_machine.move_to(start_dt, tick=False)
assert (pair, '5m', candle_type) not in exchange._pairs_last_refresh_time
# not refreshed yet
assert exchange._now_is_time_to_refresh(pair, '5m', candle_type) is True
last_closed_candle = (start_dt - timedelta(minutes=5)).timestamp()
exchange._pairs_last_refresh_time[(pair, '5m', candle_type)] = last_closed_candle
# next candle not closed yet
time_machine.move_to(start_dt + timedelta(minutes=4, seconds=59), tick=False)
assert exchange._now_is_time_to_refresh(pair, '5m', candle_type) is False
# next candle closed
time_machine.move_to(start_dt + timedelta(minutes=5, seconds=0), tick=False)
assert exchange._now_is_time_to_refresh(pair, '5m', candle_type) is True
# 1 second later (last_refresh_time didn't change)
time_machine.move_to(start_dt + timedelta(minutes=5, seconds=1), tick=False)
assert exchange._now_is_time_to_refresh(pair, '5m', candle_type) is True
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
@pytest.mark.parametrize('candle_type', ['mark', '']) @pytest.mark.parametrize('candle_type', ['mark', ''])
def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_type): def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_type):
@@ -2738,7 +2768,6 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
assert res_ohlcv[9][4] == 0.07668 assert res_ohlcv[9][4] == 0.07668
assert res_ohlcv[9][5] == 16.65244264 assert res_ohlcv[9][5] == 16.65244264
# Bittrex use-case (real data from Bittrex)
# This OHLCV data is ordered ASC (oldest first, newest last) # This OHLCV data is ordered ASC (oldest first, newest last)
ohlcv = [ ohlcv = [
[1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924], [1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924],
@@ -3382,7 +3411,7 @@ def test_get_fee(default_conf, mocker, exchange_name):
def test_stoploss_order_unsupported_exchange(default_conf, mocker): def test_stoploss_order_unsupported_exchange(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id='bittrex') exchange = get_patched_exchange(mocker, default_conf, id='bitpanda')
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
exchange.create_stoploss( exchange.create_stoploss(
pair='ETH/BTC', pair='ETH/BTC',
@@ -3578,10 +3607,10 @@ def test_ohlcv_candle_limit(default_conf, mocker, exchange_name):
timeframes = ('1m', '5m', '1h') timeframes = ('1m', '5m', '1h')
expected = exchange._ft_has['ohlcv_candle_limit'] expected = exchange._ft_has['ohlcv_candle_limit']
for timeframe in timeframes: for timeframe in timeframes:
if 'ohlcv_candle_limit_per_timeframe' in exchange._ft_has: # if 'ohlcv_candle_limit_per_timeframe' in exchange._ft_has:
expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe] # expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe]
# This should only run for bittrex # This should only run for bittrex
assert exchange_name == 'bittrex' # assert exchange_name == 'bittrex'
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT) == expected assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT) == expected
@@ -3873,11 +3902,11 @@ def test_set_margin_mode(mocker, default_conf, margin_mode):
("kraken", TradingMode.SPOT, None, False), ("kraken", TradingMode.SPOT, None, False),
("kraken", TradingMode.MARGIN, MarginMode.ISOLATED, True), ("kraken", TradingMode.MARGIN, MarginMode.ISOLATED, True),
("kraken", TradingMode.FUTURES, MarginMode.ISOLATED, True), ("kraken", TradingMode.FUTURES, MarginMode.ISOLATED, True),
("bittrex", TradingMode.SPOT, None, False), ("bitmart", TradingMode.SPOT, None, False),
("bittrex", TradingMode.MARGIN, MarginMode.CROSS, True), ("bitmart", TradingMode.MARGIN, MarginMode.CROSS, True),
("bittrex", TradingMode.MARGIN, MarginMode.ISOLATED, True), ("bitmart", TradingMode.MARGIN, MarginMode.ISOLATED, True),
("bittrex", TradingMode.FUTURES, MarginMode.CROSS, True), ("bitmart", TradingMode.FUTURES, MarginMode.CROSS, True),
("bittrex", TradingMode.FUTURES, MarginMode.ISOLATED, True), ("bitmart", TradingMode.FUTURES, MarginMode.ISOLATED, True),
("gate", TradingMode.MARGIN, MarginMode.ISOLATED, True), ("gate", TradingMode.MARGIN, MarginMode.ISOLATED, True),
("okx", TradingMode.SPOT, None, False), ("okx", TradingMode.SPOT, None, False),
("okx", TradingMode.MARGIN, MarginMode.CROSS, True), ("okx", TradingMode.MARGIN, MarginMode.CROSS, True),
@@ -4494,10 +4523,10 @@ def test_amount_to_contract_precision(
@pytest.mark.parametrize('exchange_name,open_rate,is_short,trading_mode,margin_mode', [ @pytest.mark.parametrize('exchange_name,open_rate,is_short,trading_mode,margin_mode', [
# Bittrex # Bybit
('bittrex', 2.0, False, 'spot', None), ('bybit', 2.0, False, 'spot', None),
('bittrex', 2.0, False, 'spot', 'cross'), ('bybit', 2.0, False, 'spot', 'cross'),
('bittrex', 2.0, True, 'spot', 'isolated'), ('bybit', 2.0, True, 'spot', 'isolated'),
# Binance # Binance
('binance', 2.0, False, 'spot', None), ('binance', 2.0, False, 'spot', None),
('binance', 2.0, False, 'spot', 'cross'), ('binance', 2.0, False, 'spot', 'cross'),
@@ -4919,7 +4948,7 @@ def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers):
exchange.get_max_leverage("BTC/USDT:USDT", 1000000000.01) exchange.get_max_leverage("BTC/USDT:USDT", 1000000000.01)
@pytest.mark.parametrize("exchange_name", ['bittrex', 'binance', 'kraken', 'gate', 'okx', 'bybit']) @pytest.mark.parametrize("exchange_name", ['binance', 'kraken', 'gate', 'okx', 'bybit'])
def test__get_params(mocker, default_conf, exchange_name): def test__get_params(mocker, default_conf, exchange_name):
api_mock = MagicMock() api_mock = MagicMock()
mocker.patch(f'{EXMS}.exchange_has', return_value=True) mocker.patch(f'{EXMS}.exchange_has', return_value=True)
-8
View File
@@ -14,14 +14,6 @@ EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str]
# Exchanges that should be tested online # Exchanges that should be tested online
EXCHANGES = { EXCHANGES = {
'bittrex': {
'pair': 'BTC/USDT',
'stake_currency': 'USDT',
'hasQuoteVolume': False,
'timeframe': '1h',
'leverage_tiers_public': False,
'leverage_in_spot_market': False,
},
'binance': { 'binance': {
'pair': 'BTC/USDT', 'pair': 'BTC/USDT',
'stake_currency': 'USDT', 'stake_currency': 'USDT',
@@ -218,9 +218,6 @@ class TestCCXTExchange:
def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE): def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE):
exc, exchangename = exchange exc, exchangename = exchange
if exchangename in ('bittrex'):
# For some weired reason, this test returns random lengths for bittrex.
pytest.skip("Exchange doesn't provide stable ohlcv history")
if not exc._ft_has['ohlcv_has_history']: if not exc._ft_has['ohlcv_has_history']:
pytest.skip("Exchange does not support candle history") pytest.skip("Exchange does not support candle history")
+15
View File
@@ -20,6 +20,21 @@ def is_mac() -> bool:
return "Darwin" in machine return "Darwin" in machine
@pytest.fixture(autouse=True)
def patch_torch_initlogs(mocker) -> None:
if is_mac():
# Mock torch import completely
import sys
import types
module_name = 'torch'
mocked_module = types.ModuleType(module_name)
sys.modules[module_name] = mocked_module
else:
mocker.patch("torch._logging._init_logs")
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def freqai_conf(default_conf, tmp_path): def freqai_conf(default_conf, tmp_path):
freqaiconf = deepcopy(default_conf) freqaiconf = deepcopy(default_conf)
+1 -2
View File
@@ -10,9 +10,8 @@ from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from tests.conftest import get_patched_exchange from tests.conftest import get_patched_exchange
from tests.freqai.conftest import (get_patched_data_kitchen, get_patched_freqai_strategy, from tests.freqai.conftest import (get_patched_data_kitchen, get_patched_freqai_strategy, is_mac,
make_unfiltered_dataframe) make_unfiltered_dataframe)
from tests.freqai.test_freqai_interface import is_mac
@pytest.mark.parametrize( @pytest.mark.parametrize(
+1
View File
@@ -176,6 +176,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s
'CatboostClassifier', 'CatboostClassifier',
'XGBoostClassifier', 'XGBoostClassifier',
'XGBoostRFClassifier', 'XGBoostRFClassifier',
'SKLearnRandomForestClassifier',
'PyTorchMLPClassifier', 'PyTorchMLPClassifier',
]) ])
def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
+30 -8
View File
@@ -549,6 +549,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
default_conf_usdt['exchange']['pair_whitelist'] = ['.*'] default_conf_usdt['exchange']['pair_whitelist'] = ['.*']
backtesting = Backtesting(default_conf_usdt) backtesting = Backtesting(default_conf_usdt)
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
mocker.patch('freqtrade.optimize.backtesting.Backtesting._run_funding_fees')
pair = 'ETH/USDT:USDT' pair = 'ETH/USDT:USDT'
row = [ row = [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0),
@@ -851,9 +852,13 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
assert late_entry > 0 assert late_entry > 0
@pytest.mark.parametrize('use_detail', [True, False]) @pytest.mark.parametrize('use_detail,exp_funding_fee, exp_ff_updates', [
(True, -0.018054162, 11),
(False, -0.01780296, 5),
])
def test_backtest_one_detail_futures( def test_backtest_one_detail_futures(
default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None: default_conf_usdt, fee, mocker, testdatadir, use_detail, exp_funding_fee,
exp_ff_updates) -> None:
default_conf_usdt['use_exit_signal'] = False default_conf_usdt['use_exit_signal'] = False
default_conf_usdt['trading_mode'] = 'futures' default_conf_usdt['trading_mode'] = 'futures'
default_conf_usdt['margin_mode'] = 'isolated' default_conf_usdt['margin_mode'] = 'isolated'
@@ -882,6 +887,8 @@ def test_backtest_one_detail_futures(
default_conf_usdt['max_open_trades'] = 10 default_conf_usdt['max_open_trades'] = 10
backtesting = Backtesting(default_conf_usdt) backtesting = Backtesting(default_conf_usdt)
ff_spy = mocker.spy(backtesting.exchange, 'calculate_funding_fees')
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.populate_entry_trend = advise_entry backtesting.strategy.populate_entry_trend = advise_entry
backtesting.strategy.custom_entry_price = custom_entry_price backtesting.strategy.custom_entry_price = custom_entry_price
@@ -936,13 +943,22 @@ def test_backtest_one_detail_futures(
assert (round(ln2.iloc[0]["low"], 6) <= round( assert (round(ln2.iloc[0]["low"], 6) <= round(
t["close_rate"], 6) <= round(ln2.iloc[0]["high"], 6)) t["close_rate"], 6) <= round(ln2.iloc[0]["high"], 6))
assert -0.0181 < Trade.trades[1].funding_fees < -0.01 assert pytest.approx(Trade.trades[1].funding_fees) == exp_funding_fee
assert ff_spy.call_count == exp_ff_updates
# assert late_entry > 0 # assert late_entry > 0
@pytest.mark.parametrize('use_detail', [True, False]) @pytest.mark.parametrize('use_detail,entries,max_stake,ff_updates,expected_ff', [
(True, 50, 3000, 54, -1.18038144),
(False, 6, 360, 10, -0.14679994),
])
def test_backtest_one_detail_futures_funding_fees( def test_backtest_one_detail_futures_funding_fees(
default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None: default_conf_usdt, fee, mocker, testdatadir, use_detail, entries, max_stake,
ff_updates, expected_ff,
) -> None:
"""
Funding fees are expected to differ, as the maximum position size differs.
"""
default_conf_usdt['use_exit_signal'] = False default_conf_usdt['use_exit_signal'] = False
default_conf_usdt['trading_mode'] = 'futures' default_conf_usdt['trading_mode'] = 'futures'
default_conf_usdt['margin_mode'] = 'isolated' default_conf_usdt['margin_mode'] = 'isolated'
@@ -975,6 +991,7 @@ def test_backtest_one_detail_futures_funding_fees(
default_conf_usdt['max_open_trades'] = 1 default_conf_usdt['max_open_trades'] = 1
backtesting = Backtesting(default_conf_usdt) backtesting = Backtesting(default_conf_usdt)
ff_spy = mocker.spy(backtesting.exchange, 'calculate_funding_fees')
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.populate_entry_trend = advise_entry backtesting.strategy.populate_entry_trend = advise_entry
backtesting.strategy.adjust_trade_position = adjust_trade_position backtesting.strategy.adjust_trade_position = adjust_trade_position
@@ -1000,13 +1017,18 @@ def test_backtest_one_detail_futures_funding_fees(
assert len(results) == 1 assert len(results) == 1
assert 'orders' in results.columns assert 'orders' in results.columns
# funding_fees have been calculated for each funding-fee candle
# the trade is open for 26 hours - hence we expect the 8h fee to apply 4 times.
# Additional counts will happen due each successful entry, which needs to call this, too.
assert ff_spy.call_count == ff_updates
for t in Trade.trades: for t in Trade.trades:
# At least 4 adjustment orders # At least 6 adjustment orders
assert t.nr_of_successful_entries >= 6 assert t.nr_of_successful_entries == entries
# Funding fees will vary depending on the number of adjustment orders # Funding fees will vary depending on the number of adjustment orders
# That number is a lot higher with detail data. # That number is a lot higher with detail data.
assert -1.81 < t.funding_fees < -0.1 assert t.max_stake_amount == max_stake
assert pytest.approx(t.funding_fees) == expected_ff
def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) -> None:
@@ -104,6 +104,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
mocker.patch(f"{EXMS}.get_max_leverage", return_value=10) mocker.patch(f"{EXMS}.get_max_leverage", return_value=10)
mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0.1, 0.1)) mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0.1, 0.1))
mocker.patch('freqtrade.optimize.backtesting.Backtesting._run_funding_fees')
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({ default_conf.update({
+14 -4
View File
@@ -9,7 +9,7 @@ from sqlalchemy import select
from freqtrade.edge import PairInfo from freqtrade.edge import PairInfo
from freqtrade.enums import SignalDirection, State, TradingMode from freqtrade.enums import SignalDirection, State, TradingMode
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
from freqtrade.persistence import Trade from freqtrade.persistence import Order, Trade
from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.pairlock_middleware import PairLocks
from freqtrade.rpc import RPC, RPCException from freqtrade.rpc import RPC, RPCException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
@@ -355,8 +355,18 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
rpc._rpc_delete('200') rpc._rpc_delete('200')
trades = Trade.session.scalars(select(Trade)).all() trades = Trade.session.scalars(select(Trade)).all()
trades[1].stoploss_order_id = '1234' trades[2].stoploss_order_id = '102'
trades[2].stoploss_order_id = '1234' trades[2].orders.append(
Order(
ft_order_side='stoploss',
ft_pair=trades[2].pair,
ft_is_open=True,
ft_amount=trades[2].amount,
ft_price=trades[2].stop_loss,
order_id='102',
status='open',
)
)
assert len(trades) > 2 assert len(trades) > 2
res = rpc._rpc_delete('1') res = rpc._rpc_delete('1')
@@ -369,7 +379,7 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
cancel_mock.reset_mock() cancel_mock.reset_mock()
stoploss_mock.reset_mock() stoploss_mock.reset_mock()
res = rpc._rpc_delete('2') res = rpc._rpc_delete('5')
assert isinstance(res, dict) assert isinstance(res, dict)
assert stoploss_mock.call_count == 1 assert stoploss_mock.call_count == 1
assert res['cancel_order_count'] == 1 assert res['cancel_order_count'] == 1
+4 -3
View File
@@ -728,7 +728,6 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
ftbot.strategy.order_types['stoploss_on_exchange'] = True ftbot.strategy.order_types['stoploss_on_exchange'] = True
trades = Trade.session.scalars(select(Trade)).all() trades = Trade.session.scalars(select(Trade)).all()
trades[1].stoploss_order_id = '1234'
Trade.commit() Trade.commit()
assert len(trades) > 2 assert len(trades) > 2
@@ -745,9 +744,9 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
assert cancel_mock.call_count == 0 assert cancel_mock.call_count == 0
assert len(trades) - 1 == len(Trade.session.scalars(select(Trade)).all()) assert len(trades) - 1 == len(Trade.session.scalars(select(Trade)).all())
rc = client_delete(client, f"{BASE_URI}/trades/2") rc = client_delete(client, f"{BASE_URI}/trades/5")
assert_response(rc) assert_response(rc)
assert rc.json()['result_msg'] == 'Deleted trade 2. Closed 1 open orders.' assert rc.json()['result_msg'] == 'Deleted trade 5. Closed 1 open orders.'
assert len(trades) - 2 == len(Trade.session.scalars(select(Trade)).all()) assert len(trades) - 2 == len(Trade.session.scalars(select(Trade)).all())
assert stoploss_mock.call_count == 1 assert stoploss_mock.call_count == 1
@@ -1770,6 +1769,7 @@ def test_api_freqaimodels(botclient, tmp_path, mocker):
{'name': 'LightGBMRegressorMultiTarget'}, {'name': 'LightGBMRegressorMultiTarget'},
{'name': 'ReinforcementLearner'}, {'name': 'ReinforcementLearner'},
{'name': 'ReinforcementLearner_multiproc'}, {'name': 'ReinforcementLearner_multiproc'},
{'name': 'SKlearnRandomForestClassifier'},
{'name': 'XGBoostClassifier'}, {'name': 'XGBoostClassifier'},
{'name': 'XGBoostRFClassifier'}, {'name': 'XGBoostRFClassifier'},
{'name': 'XGBoostRFRegressor'}, {'name': 'XGBoostRFRegressor'},
@@ -1788,6 +1788,7 @@ def test_api_freqaimodels(botclient, tmp_path, mocker):
'LightGBMRegressorMultiTarget', 'LightGBMRegressorMultiTarget',
'ReinforcementLearner', 'ReinforcementLearner',
'ReinforcementLearner_multiproc', 'ReinforcementLearner_multiproc',
'SKlearnRandomForestClassifier',
'XGBoostClassifier', 'XGBoostClassifier',
'XGBoostRFClassifier', 'XGBoostRFClassifier',
'XGBoostRFRegressor', 'XGBoostRFRegressor',
-1
View File
@@ -109,7 +109,6 @@ def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None):
_start_thread=MagicMock(), _start_thread=MagicMock(),
) )
if not ftbot: if not ftbot:
mocker.patch('freqtrade.exchange.exchange.Exchange._init_async_loop')
ftbot = get_patched_freqtradebot(mocker, default_conf) ftbot = get_patched_freqtradebot(mocker, default_conf)
rpc = RPC(ftbot) rpc = RPC(ftbot)
telegram = Telegram(rpc, default_conf) telegram = Telegram(rpc, default_conf)
+2
View File
@@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring, C0103, protected-access # pragma pylint: disable=missing-docstring, C0103, protected-access
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import MagicMock from unittest.mock import MagicMock
@@ -331,6 +332,7 @@ def test_send_msg_webhook(default_conf, mocker):
def test_exception_send_msg(default_conf, mocker, caplog): def test_exception_send_msg(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG)
default_conf["webhook"] = get_webhook_dict() default_conf["webhook"] = get_webhook_dict()
del default_conf["webhook"]["entry"] del default_conf["webhook"]["entry"]
del default_conf["webhook"]["webhookentry"] del default_conf["webhook"]["webhookentry"]
+56
View File
@@ -12,9 +12,11 @@ from tests.conftest import generate_test_data, get_patched_exchange
def test_merge_informative_pair(): def test_merge_informative_pair():
data = generate_test_data('15m', 40) data = generate_test_data('15m', 40)
informative = generate_test_data('1h', 40) informative = generate_test_data('1h', 40)
cols_inf = list(informative.columns)
result = merge_informative_pair(data, informative, '15m', '1h', ffill=True) result = merge_informative_pair(data, informative, '15m', '1h', ffill=True)
assert isinstance(result, pd.DataFrame) assert isinstance(result, pd.DataFrame)
assert list(informative.columns) == cols_inf
assert len(result) == len(data) assert len(result) == len(data)
assert 'date' in result.columns assert 'date' in result.columns
assert result['date'].equals(data['date']) assert result['date'].equals(data['date'])
@@ -61,6 +63,60 @@ def test_merge_informative_pair():
assert result.iloc[8]['date_1h'] is pd.NaT assert result.iloc[8]['date_1h'] is pd.NaT
def test_merge_informative_pair_weekly():
# Covers roughly 2 months - until 2023-01-10
data = generate_test_data('1h', 1040, '2022-11-28')
informative = generate_test_data('1w', 40, '2022-11-01')
informative['day'] = informative['date'].dt.day_name()
result = merge_informative_pair(data, informative, '1h', '1w', ffill=True)
assert isinstance(result, pd.DataFrame)
# 2022-12-24 is a Saturday
candle1 = result.loc[(result['date'] == '2022-12-24T22:00:00.000Z')]
assert candle1.iloc[0]['date'] == pd.Timestamp('2022-12-24T22:00:00.000Z')
assert candle1.iloc[0]['date_1w'] == pd.Timestamp('2022-12-12T00:00:00.000Z')
candle2 = result.loc[(result['date'] == '2022-12-24T23:00:00.000Z')]
assert candle2.iloc[0]['date'] == pd.Timestamp('2022-12-24T23:00:00.000Z')
assert candle2.iloc[0]['date_1w'] == pd.Timestamp('2022-12-12T00:00:00.000Z')
# 2022-12-25 is a Sunday
candle3 = result.loc[(result['date'] == '2022-12-25T22:00:00.000Z')]
assert candle3.iloc[0]['date'] == pd.Timestamp('2022-12-25T22:00:00.000Z')
# Still old candle
assert candle3.iloc[0]['date_1w'] == pd.Timestamp('2022-12-12T00:00:00.000Z')
candle4 = result.loc[(result['date'] == '2022-12-25T23:00:00.000Z')]
assert candle4.iloc[0]['date'] == pd.Timestamp('2022-12-25T23:00:00.000Z')
assert candle4.iloc[0]['date_1w'] == pd.Timestamp('2022-12-19T00:00:00.000Z')
def test_merge_informative_pair_monthly():
# Covers roughly 2 months - until 2023-01-10
data = generate_test_data('1h', 1040, '2022-11-28')
informative = generate_test_data('1M', 40, '2022-01-01')
result = merge_informative_pair(data, informative, '1h', '1M', ffill=True)
assert isinstance(result, pd.DataFrame)
candle1 = result.loc[(result['date'] == '2022-12-31T22:00:00.000Z')]
assert candle1.iloc[0]['date'] == pd.Timestamp('2022-12-31T22:00:00.000Z')
assert candle1.iloc[0]['date_1M'] == pd.Timestamp('2022-11-01T00:00:00.000Z')
candle2 = result.loc[(result['date'] == '2022-12-31T23:00:00.000Z')]
assert candle2.iloc[0]['date'] == pd.Timestamp('2022-12-31T23:00:00.000Z')
assert candle2.iloc[0]['date_1M'] == pd.Timestamp('2022-12-01T00:00:00.000Z')
# Candle is empty, as the start-date did fail.
candle3 = result.loc[(result['date'] == '2022-11-30T22:00:00.000Z')]
assert candle3.iloc[0]['date'] == pd.Timestamp('2022-11-30T22:00:00.000Z')
assert candle3.iloc[0]['date_1M'] is pd.NaT
# First candle with 1M data merged.
candle4 = result.loc[(result['date'] == '2022-11-30T23:00:00.000Z')]
assert candle4.iloc[0]['date'] == pd.Timestamp('2022-11-30T23:00:00.000Z')
assert candle4.iloc[0]['date_1M'] == pd.Timestamp('2022-11-01T00:00:00.000Z')
def test_merge_informative_pair_same(): def test_merge_informative_pair_same():
data = generate_test_data('15m', 40) data = generate_test_data('15m', 40)
informative = generate_test_data('15m', 40) informative = generate_test_data('15m', 40)
+1 -1
View File
@@ -173,7 +173,7 @@ def test_download_data_options() -> None:
def test_plot_dataframe_options() -> None: def test_plot_dataframe_options() -> None:
args = [ args = [
'plot-dataframe', 'plot-dataframe',
'-c', 'config_examples/config_bittrex.example.json', '-c', 'tests/testdata/testconfigs/main_test_config.json',
'--indicators1', 'sma10', 'sma100', '--indicators1', 'sma10', 'sma100',
'--indicators2', 'macd', 'fastd', 'fastk', '--indicators2', 'macd', 'fastd', 'fastk',
'--plot-limit', '30', '--plot-limit', '30',
+8 -7
View File
@@ -146,7 +146,7 @@ def test_get_trade_stake_amount(default_conf_usdt, mocker) -> None:
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT') result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT', 1)
assert result == default_conf_usdt['stake_amount'] assert result == default_conf_usdt['stake_amount']
@@ -211,12 +211,12 @@ def test_check_available_stake_amount(
if expected[i] is not None: if expected[i] is not None:
limit_buy_order_usdt_open['id'] = str(i) limit_buy_order_usdt_open['id'] = str(i)
result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT') result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT', 1)
assert pytest.approx(result) == expected[i] assert pytest.approx(result) == expected[i]
freqtrade.execute_entry('ETH/USDT', result) freqtrade.execute_entry('ETH/USDT', result)
else: else:
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
freqtrade.wallets.get_trade_stake_amount('ETH/USDT') freqtrade.wallets.get_trade_stake_amount('ETH/USDT', 1)
def test_edge_called_in_process(mocker, edge_conf) -> None: def test_edge_called_in_process(mocker, edge_conf) -> None:
@@ -238,9 +238,9 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None:
freqtrade = FreqtradeBot(edge_conf) freqtrade = FreqtradeBot(edge_conf)
assert freqtrade.wallets.get_trade_stake_amount( assert freqtrade.wallets.get_trade_stake_amount(
'NEO/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.20 'NEO/BTC', 1, freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.20
assert freqtrade.wallets.get_trade_stake_amount( assert freqtrade.wallets.get_trade_stake_amount(
'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 'LTC/BTC', 1, freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21
@pytest.mark.parametrize('buy_price_mult,ignore_strat_sl', [ @pytest.mark.parametrize('buy_price_mult,ignore_strat_sl', [
@@ -420,7 +420,8 @@ def test_create_trade_minimal_amount(
else: else:
assert not freqtrade.create_trade('ETH/USDT') assert not freqtrade.create_trade('ETH/USDT')
if not max_open_trades: if not max_open_trades:
assert freqtrade.wallets.get_trade_stake_amount('ETH/USDT', freqtrade.edge) == 0 assert freqtrade.wallets.get_trade_stake_amount(
'ETH/USDT', default_conf_usdt['max_open_trades'], freqtrade.edge) == 0
@pytest.mark.parametrize('whitelist,positions', [ @pytest.mark.parametrize('whitelist,positions', [
@@ -3485,7 +3486,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'kraken', 'bittrex'], @pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'kraken', 'bybit'],
indirect=['limit_buy_order_canceled_empty']) indirect=['limit_buy_order_canceled_empty'])
def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_short, fee, def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_short, fee,
limit_buy_order_canceled_empty) -> None: limit_buy_order_canceled_empty) -> None:
+2 -2
View File
@@ -185,7 +185,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
trades = Trade.session.scalars(select(Trade)).all() trades = Trade.session.scalars(select(Trade)).all()
assert len(trades) == 4 assert len(trades) == 4
assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') == result1 assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC', 5) == result1
rpc._rpc_force_entry('TKN/BTC', None) rpc._rpc_force_entry('TKN/BTC', None)
@@ -205,7 +205,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
# One trade sold # One trade sold
assert len(trades) == 4 assert len(trades) == 4
# stake-amount should now be reduced, since one trade was sold at a loss. # stake-amount should now be reduced, since one trade was sold at a loss.
assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') < result1 assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC', 5) < result1
# Validate that balance of sold trade is not in dry-run balances anymore. # Validate that balance of sold trade is not in dry-run balances anymore.
bals2 = freqtrade.wallets.get_all_balances() bals2 = freqtrade.wallets.get_all_balances()
assert bals != bals2 assert bals != bals2
+7 -7
View File
@@ -63,9 +63,9 @@ def test_set_loggers_syslog():
setup_logging_pre() setup_logging_pre()
setup_logging(config) setup_logging(config)
assert len(logger.handlers) == 3 assert len(logger.handlers) == 3
assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler] assert [x for x in logger.handlers if isinstance(x, logging.handlers.SysLogHandler)]
assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler] assert [x for x in logger.handlers if isinstance(x, FTStdErrStreamHandler)]
assert [x for x in logger.handlers if type(x) == FTBufferingHandler] assert [x for x in logger.handlers if isinstance(x, FTBufferingHandler)]
# setting up logging again should NOT cause the loggers to be added a second time. # setting up logging again should NOT cause the loggers to be added a second time.
setup_logging(config) setup_logging(config)
assert len(logger.handlers) == 3 assert len(logger.handlers) == 3
@@ -86,9 +86,9 @@ def test_set_loggers_Filehandler(tmp_path):
setup_logging_pre() setup_logging_pre()
setup_logging(config) setup_logging(config)
assert len(logger.handlers) == 3 assert len(logger.handlers) == 3
assert [x for x in logger.handlers if type(x) == logging.handlers.RotatingFileHandler] assert [x for x in logger.handlers if isinstance(x, logging.handlers.RotatingFileHandler)]
assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler] assert [x for x in logger.handlers if isinstance(x, FTStdErrStreamHandler)]
assert [x for x in logger.handlers if type(x) == FTBufferingHandler] assert [x for x in logger.handlers if isinstance(x, FTBufferingHandler)]
# setting up logging again should NOT cause the loggers to be added a second time. # setting up logging again should NOT cause the loggers to be added a second time.
setup_logging(config) setup_logging(config)
assert len(logger.handlers) == 3 assert len(logger.handlers) == 3
@@ -112,7 +112,7 @@ def test_set_loggers_journald(mocker):
setup_logging(config) setup_logging(config)
assert len(logger.handlers) == 3 assert len(logger.handlers) == 3
assert [x for x in logger.handlers if type(x).__name__ == "JournaldLogHandler"] assert [x for x in logger.handlers if type(x).__name__ == "JournaldLogHandler"]
assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler] assert [x for x in logger.handlers if isinstance(x, FTStdErrStreamHandler)]
# reset handlers to not break pytest # reset handlers to not break pytest
logger.handlers = orig_handlers logger.handlers = orig_handlers
+10 -10
View File
@@ -67,12 +67,12 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = ['trade', '-c', 'config_examples/config_bittrex.example.json'] args = ['trade', '-c', 'tests/testdata/testconfigs/main_test_config.json']
# Test Main + the KeyboardInterrupt exception # Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(args) main(args)
assert log_has('Using config: config_examples/config_bittrex.example.json ...', caplog) assert log_has('Using config: tests/testdata/testconfigs/main_test_config.json ...', caplog)
assert log_has('Fatal exception!', caplog) assert log_has('Fatal exception!', caplog)
@@ -85,12 +85,12 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) mocker.patch('freqtrade.wallets.Wallets.update', MagicMock())
mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = ['trade', '-c', 'config_examples/config_bittrex.example.json'] args = ['trade', '-c', 'tests/testdata/testconfigs/main_test_config.json']
# Test Main + the KeyboardInterrupt exception # Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(args) main(args)
assert log_has('Using config: config_examples/config_bittrex.example.json ...', caplog) assert log_has('Using config: tests/testdata/testconfigs/main_test_config.json ...', caplog)
assert log_has('SIGINT received, aborting ...', caplog) assert log_has('SIGINT received, aborting ...', caplog)
@@ -106,12 +106,12 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = ['trade', '-c', 'config_examples/config_bittrex.example.json'] args = ['trade', '-c', 'tests/testdata/testconfigs/main_test_config.json']
# Test Main + the KeyboardInterrupt exception # Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(args) main(args)
assert log_has('Using config: config_examples/config_bittrex.example.json ...', caplog) assert log_has('Using config: tests/testdata/testconfigs/main_test_config.json ...', caplog)
assert log_has('Oh snap!', caplog) assert log_has('Oh snap!', caplog)
@@ -160,13 +160,13 @@ def test_main_reload_config(mocker, default_conf, caplog) -> None:
args = Arguments([ args = Arguments([
'trade', 'trade',
'-c', '-c',
'config_examples/config_bittrex.example.json' 'tests/testdata/testconfigs/main_test_config.json'
]).get_parsed_arg() ]).get_parsed_arg()
worker = Worker(args=args, config=default_conf) worker = Worker(args=args, config=default_conf)
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(['trade', '-c', 'config_examples/config_bittrex.example.json']) main(['trade', '-c', 'tests/testdata/testconfigs/main_test_config.json'])
assert log_has('Using config: config_examples/config_bittrex.example.json ...', caplog) assert log_has('Using config: tests/testdata/testconfigs/main_test_config.json ...', caplog)
assert worker_mock.call_count == 4 assert worker_mock.call_count == 4
assert reconfigure_mock.call_count == 1 assert reconfigure_mock.call_count == 1
assert isinstance(worker.freqtrade, FreqtradeBot) assert isinstance(worker.freqtrade, FreqtradeBot)
@@ -187,7 +187,7 @@ def test_reconfigure(mocker, default_conf) -> None:
args = Arguments([ args = Arguments([
'trade', 'trade',
'-c', '-c',
'config_examples/config_bittrex.example.json' 'tests/testdata/testconfigs/main_test_config.json'
]).get_parsed_arg() ]).get_parsed_arg()
worker = Worker(args=args, config=default_conf) worker = Worker(args=args, config=default_conf)
freqtrade = worker.freqtrade freqtrade = worker.freqtrade
+2 -2
View File
@@ -377,7 +377,7 @@ def test_start_plot_dataframe(mocker):
aup = mocker.patch("freqtrade.plot.plotting.load_and_plot_trades", MagicMock()) aup = mocker.patch("freqtrade.plot.plotting.load_and_plot_trades", MagicMock())
args = [ args = [
"plot-dataframe", "plot-dataframe",
"--config", "config_examples/config_bittrex.example.json", "--config", "tests/testdata/testconfigs/main_test_config.json",
"--pairs", "ETH/BTC" "--pairs", "ETH/BTC"
] ]
start_plot_dataframe(get_args(args)) start_plot_dataframe(get_args(args))
@@ -420,7 +420,7 @@ def test_start_plot_profit(mocker):
aup = mocker.patch("freqtrade.plot.plotting.plot_profit", MagicMock()) aup = mocker.patch("freqtrade.plot.plotting.plot_profit", MagicMock())
args = [ args = [
"plot-profit", "plot-profit",
"--config", "config_examples/config_bittrex.example.json", "--config", "tests/testdata/testconfigs/main_test_config.json",
"--pairs", "ETH/BTC" "--pairs", "ETH/BTC"
] ]
start_plot_profit(get_args(args)) start_plot_profit(get_args(args))
+6 -9
View File
@@ -121,7 +121,7 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
with pytest.raises(DependencyException, match=r'.*stake amount.*'): with pytest.raises(DependencyException, match=r'.*stake amount.*'):
freqtrade.wallets.get_trade_stake_amount('ETH/BTC') freqtrade.wallets.get_trade_stake_amount('ETH/BTC', 1)
@pytest.mark.parametrize("balance_ratio,capital,result1,result2", [ @pytest.mark.parametrize("balance_ratio,capital,result1,result2", [
@@ -148,7 +148,6 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT
conf['dry_run_wallet'] = 100 conf['dry_run_wallet'] = 100
conf['max_open_trades'] = 2
conf['tradable_balance_ratio'] = balance_ratio conf['tradable_balance_ratio'] = balance_ratio
if capital is not None: if capital is not None:
conf['available_capital'] = capital conf['available_capital'] = capital
@@ -156,30 +155,28 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r
freqtrade = get_patched_freqtradebot(mocker, conf) freqtrade = get_patched_freqtradebot(mocker, conf)
# no open trades, order amount should be 'balance / max_open_trades' # no open trades, order amount should be 'balance / max_open_trades'
result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT') result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT', 2)
assert result == result1 assert result == result1
# create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)'
freqtrade.execute_entry('ETH/USDT', result) freqtrade.execute_entry('ETH/USDT', result)
result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT') result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT', 2)
assert result == result1 assert result == result1
# create 2 trades, order amount should be None # create 2 trades, order amount should be None
freqtrade.execute_entry('LTC/BTC', result) freqtrade.execute_entry('LTC/BTC', result)
result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT') result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', 2)
assert result == 0 assert result == 0
freqtrade.config['max_open_trades'] = 3
freqtrade.config['dry_run_wallet'] = 200 freqtrade.config['dry_run_wallet'] = 200
freqtrade.wallets.start_cap = 200 freqtrade.wallets.start_cap = 200
result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT') result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', 3)
assert round(result, 4) == round(result2, 4) assert round(result, 4) == round(result2, 4)
# set max_open_trades = None, so do not trade # set max_open_trades = None, so do not trade
freqtrade.config['max_open_trades'] = 0 result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT', 0)
result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT')
assert result == 0 assert result == 0
@@ -29,7 +29,7 @@
"order_book_top": 1 "order_book_top": 1
}, },
"exchange": { "exchange": {
"name": "bittrex", "name": "binance",
"key": "your_exchange_key", "key": "your_exchange_key",
"secret": "your_exchange_secret", "secret": "your_exchange_secret",
"ccxt_config": {}, "ccxt_config": {},