Merge branch 'develop' of https://github.com/simwai/freqtrade into develop
This commit is contained in:
+87
-52
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -2,27 +2,28 @@
|
|||||||
# 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
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.3.0.7
|
- types-cachetools==5.3.0.7
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.31.0.10
|
- types-requests==2.31.0.20240106
|
||||||
- types-tabulate==0.9.0.3
|
- types-tabulate==0.9.0.20240106
|
||||||
- types-python-dateutil==2.8.19.14
|
- types-python-dateutil==2.8.19.20240106
|
||||||
- SQLAlchemy==2.0.23
|
- SQLAlchemy==2.0.23
|
||||||
# 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: |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -52,7 +52,7 @@
|
|||||||
"train_period_days": 15,
|
"train_period_days": 15,
|
||||||
"backtest_period_days": 7,
|
"backtest_period_days": 7,
|
||||||
"live_retrain_hours": 0,
|
"live_retrain_hours": 0,
|
||||||
"identifier": "uniqe-id",
|
"identifier": "unique-id",
|
||||||
"feature_parameters": {
|
"feature_parameters": {
|
||||||
"include_timeframes": [
|
"include_timeframes": [
|
||||||
"3m",
|
"3m",
|
||||||
|
|||||||
+2
-2
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -572,9 +572,11 @@ In addition to fiat currencies, a range of crypto currencies is supported.
|
|||||||
The valid values are:
|
The valid values are:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
|
"BTC", "ETH", "XRP", "LTC", "BCH", "BNB"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Removing `fiat_display_currency` completely from the configuration will skip initializing coingecko, and will not show any FIAT currency conversion. This has no importance for the correct functioning of the bot.
|
||||||
|
|
||||||
## Using Dry-run mode
|
## Using Dry-run mode
|
||||||
|
|
||||||
We recommend starting the bot in the Dry-run mode to see how your bot will
|
We recommend starting the bot in the Dry-run mode to see how your bot will
|
||||||
|
|||||||
+2
-42
@@ -127,6 +127,8 @@ Freqtrade will not attempt to change these settings.
|
|||||||
|
|
||||||
## Kraken
|
## Kraken
|
||||||
|
|
||||||
|
Kraken supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "IOC" (immediate-or-cancel) and "PO" (Post only) settings.
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
Kraken supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
Kraken supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||||
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
||||||
@@ -181,48 +183,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
@@ -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":
|
||||||
|
|
||||||
|
|||||||
@@ -41,11 +41,11 @@ FreqAI stores new model files after each successful training. These files become
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
"freqai": {
|
"freqai": {
|
||||||
"purge_old_models": true,
|
"purge_old_models": 4,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This will automatically purge all models older than the two most recently trained ones to save disk space.
|
This will automatically purge all models older than the four most recently trained ones to save disk space. Inputing "0" will never purge any models.
|
||||||
|
|
||||||
## Backtesting
|
## Backtesting
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ Backtesting mode requires [downloading the necessary data](#downloading-data-to-
|
|||||||
This way, you can return to using any model you wish by simply specifying the `identifier`.
|
This way, you can return to using any model you wish by simply specifying the `identifier`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Backtesting calls `set_freqai_targets()` one time for each backtest window (where the number of windows is the full backtest timerange divided by the `backtest_period_days` parameter). Doing this means that the targets simulate dry/live behavior without look ahead bias. However, the definition of the features in `feature_engineering_*()` is performed once on the entire backtest timerange. This means that you should be sure that features do look-ahead into the future.
|
Backtesting calls `set_freqai_targets()` one time for each backtest window (where the number of windows is the full backtest timerange divided by the `backtest_period_days` parameter). Doing this means that the targets simulate dry/live behavior without look ahead bias. However, the definition of the features in `feature_engineering_*()` is performed once on the entire training timerange. This means that you should be sure that features do not look-ahead into the future.
|
||||||
More details about look-ahead bias can be found in [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies).
|
More details about look-ahead bias can be found in [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -114,6 +114,11 @@ Here we compile some external materials that provide deeper looks into various c
|
|||||||
- [Real-time head-to-head: Adaptive modeling of financial market data using XGBoost and CatBoost](https://emergentmethods.medium.com/real-time-head-to-head-adaptive-modeling-of-financial-market-data-using-xgboost-and-catboost-995a115a7495)
|
- [Real-time head-to-head: Adaptive modeling of financial market data using XGBoost and CatBoost](https://emergentmethods.medium.com/real-time-head-to-head-adaptive-modeling-of-financial-market-data-using-xgboost-and-catboost-995a115a7495)
|
||||||
- [FreqAI - from price to prediction](https://emergentmethods.medium.com/freqai-from-price-to-prediction-6fadac18b665)
|
- [FreqAI - from price to prediction](https://emergentmethods.medium.com/freqai-from-price-to-prediction-6fadac18b665)
|
||||||
|
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
You can find support for FreqAI in a variety of places, including the [Freqtrade discord](https://discord.gg/Jd8JYeWHc4), the dedicated [FreqAI discord](https://discord.gg/7AMWACmbjT), and in [github issues](https://github.com/freqtrade/freqtrade/issues).
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
FreqAI is developed by a group of individuals who all contribute specific skillsets to the project.
|
FreqAI is developed by a group of individuals who all contribute specific skillsets to the project.
|
||||||
|
|||||||
@@ -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,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.7
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -760,22 +760,31 @@ The `position_adjustment_enable` strategy property enables the usage of `adjust_
|
|||||||
For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled.
|
For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled.
|
||||||
`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging) or to increase or decrease positions.
|
`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging) or to increase or decrease positions.
|
||||||
|
|
||||||
`max_entry_position_adjustment` property is used to limit the number of additional entries per trade (on top of the first entry order) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment entries.
|
|
||||||
|
|
||||||
The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional entry order should be made (position is increased -> buy order for long trades, sell order for short trades).
|
|
||||||
If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored.
|
|
||||||
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
|
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
|
||||||
|
|
||||||
This callback is **not** called when there is an open order (either buy or sell) waiting for execution.
|
This callback is **not** called when there is an open order (either buy or sell) waiting for execution.
|
||||||
|
|
||||||
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
||||||
|
|
||||||
Additional entries are ignored once you have reached the maximum amount of extra entries that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits.
|
|
||||||
|
|
||||||
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade.
|
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade.
|
||||||
|
|
||||||
Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage.
|
Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage.
|
||||||
|
|
||||||
|
### Increase position
|
||||||
|
|
||||||
|
The strategy is expected to return a positive **stake_amount** (in stake currency) between `min_stake` and `max_stake` if and when an additional entry order should be made (position is increased -> buy order for long trades, sell order for short trades).
|
||||||
|
|
||||||
|
If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored.
|
||||||
|
`max_entry_position_adjustment` property is used to limit the number of additional entries per trade (on top of the first entry order) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment entries.
|
||||||
|
|
||||||
|
Additional entries are ignored once you have reached the maximum amount of extra entries that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits.
|
||||||
|
|
||||||
|
### Decrease position
|
||||||
|
|
||||||
|
The strategy is expected to return a negative stake_amount (in stake currency) for a partial exit.
|
||||||
|
Returning the full owned stake at that point (based on the current price) (`-(trade.amount / trade.leverage) * current_exit_rate`) results in a full exit.
|
||||||
|
Returning a value more than the above (so remaining stake_amount would become negative) will result in the bot ignoring the signal.
|
||||||
|
|
||||||
!!! Note "About stake size"
|
!!! Note "About stake size"
|
||||||
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
|
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
|
||||||
If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that.
|
If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that.
|
||||||
|
|||||||
@@ -367,6 +367,11 @@ class AwesomeStrategy(IStrategy):
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
??? info "Orders that don't fill immediately"
|
||||||
|
`minimal_roi` will take the `trade.open_date` as reference, which is the time the trade was initialized / the first order for this trade was placed.
|
||||||
|
This will also hold true for limit orders that don't fill immediately (usually in combination with "off-spot" prices through `custom_entry_price()`), as well as for cases where the initial order is replaced through `adjust_entry_price()`.
|
||||||
|
The time used will still be from the initial `trade.open_date` (when the initial order was first placed), not from the newly placed order date.
|
||||||
|
|
||||||
### Stoploss
|
### Stoploss
|
||||||
|
|
||||||
Setting a stoploss is highly recommended to protect your capital from strong moves against you.
|
Setting a stoploss is highly recommended to protect your capital from strong moves against you.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ Possible parameters are:
|
|||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `base_currency`
|
* `base_currency`
|
||||||
|
* `quote_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `current_rate`
|
* `current_rate`
|
||||||
@@ -155,6 +156,7 @@ Possible parameters are:
|
|||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `base_currency`
|
* `base_currency`
|
||||||
|
* `quote_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `current_rate`
|
* `current_rate`
|
||||||
@@ -176,6 +178,7 @@ Possible parameters are:
|
|||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `base_currency`
|
* `base_currency`
|
||||||
|
* `quote_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `current_rate`
|
* `current_rate`
|
||||||
@@ -199,6 +202,7 @@ Possible parameters are:
|
|||||||
* `profit_ratio`
|
* `profit_ratio`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `base_currency`
|
* `base_currency`
|
||||||
|
* `quote_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `exit_reason`
|
* `exit_reason`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
@@ -224,6 +228,7 @@ Possible parameters are:
|
|||||||
* `profit_ratio`
|
* `profit_ratio`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `base_currency`
|
* `base_currency`
|
||||||
|
* `quote_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `exit_reason`
|
* `exit_reason`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
@@ -249,6 +254,7 @@ Possible parameters are:
|
|||||||
* `profit_ratio`
|
* `profit_ratio`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `base_currency`
|
* `base_currency`
|
||||||
|
* `quote_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `exit_reason`
|
* `exit_reason`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2023.12-dev'
|
__version__ = '2024.1-dev'
|
||||||
|
|
||||||
if 'dev' in __version__:
|
if 'dev' in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from freqtrade.enums import RunMode, TradingMode
|
|||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
from freqtrade.resolvers import ExchangeResolver
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
from freqtrade.util.migrations import migrate_data
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -78,7 +78,7 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
|||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
if ohlcv:
|
if ohlcv:
|
||||||
migrate_binance_futures_data(config)
|
migrate_data(config)
|
||||||
convert_ohlcv_format(config,
|
convert_ohlcv_format(config,
|
||||||
convert_from=args['format_from'],
|
convert_from=args['format_from'],
|
||||||
convert_to=args['format_to'],
|
convert_to=args['format_to'],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from freqtrade import constants
|
|||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import round_coin_value
|
from freqtrade.util import fmt_coin
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -29,8 +29,8 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
|
|||||||
# tradable_balance_ratio
|
# tradable_balance_ratio
|
||||||
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
|
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
|
||||||
and config['stake_amount'] > wallet_size):
|
and config['stake_amount'] > wallet_size):
|
||||||
wallet = round_coin_value(wallet_size, config['stake_currency'])
|
wallet = fmt_coin(wallet_size, config['stake_currency'])
|
||||||
stake = round_coin_value(config['stake_amount'], config['stake_currency'])
|
stake = fmt_coin(config['stake_amount'], config['stake_currency'])
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
|
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
|
||||||
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
|
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
|
||||||
|
|||||||
@@ -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', {}) \
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ SUPPORTED_FIAT = [
|
|||||||
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
||||||
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
||||||
"RUB", "UAH", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR",
|
"RUB", "UAH", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR",
|
||||||
"USD", "BTC", "ETH", "XRP", "LTC", "BCH"
|
"USD", "BTC", "ETH", "XRP", "LTC", "BCH", "BNB"
|
||||||
]
|
]
|
||||||
|
|
||||||
MINIMAL_CONFIG = {
|
MINIMAL_CONFIG = {
|
||||||
|
|||||||
@@ -175,36 +175,40 @@ def _get_backtest_files(dirname: Path) -> List[Path]:
|
|||||||
return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))))
|
return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))))
|
||||||
|
|
||||||
|
|
||||||
def get_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]:
|
def _extract_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]:
|
||||||
"""
|
metadata = load_backtest_metadata(filename)
|
||||||
Get backtest result read from metadata file
|
|
||||||
"""
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'filename': filename.stem,
|
'filename': filename.stem,
|
||||||
'strategy': s,
|
'strategy': s,
|
||||||
'notes': v.get('notes', ''),
|
|
||||||
'run_id': v['run_id'],
|
'run_id': v['run_id'],
|
||||||
|
'notes': v.get('notes', ''),
|
||||||
|
# Backtest "run" time
|
||||||
'backtest_start_time': v['backtest_start_time'],
|
'backtest_start_time': v['backtest_start_time'],
|
||||||
} for s, v in load_backtest_metadata(filename).items()
|
# Backtest timerange
|
||||||
|
'backtest_start_ts': v.get('backtest_start_ts', None),
|
||||||
|
'backtest_end_ts': v.get('backtest_end_ts', None),
|
||||||
|
'timeframe': v.get('timeframe', None),
|
||||||
|
'timeframe_detail': v.get('timeframe_detail', None),
|
||||||
|
} for s, v in metadata.items()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]:
|
||||||
|
"""
|
||||||
|
Get backtest result read from metadata file
|
||||||
|
"""
|
||||||
|
return _extract_backtest_result(filename)
|
||||||
|
|
||||||
|
|
||||||
def get_backtest_resultlist(dirname: Path) -> List[BacktestHistoryEntryType]:
|
def get_backtest_resultlist(dirname: Path) -> List[BacktestHistoryEntryType]:
|
||||||
"""
|
"""
|
||||||
Get list of backtest results read from metadata files
|
Get list of backtest results read from metadata files
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
{
|
result
|
||||||
'filename': filename.stem,
|
|
||||||
'strategy': s,
|
|
||||||
'run_id': v['run_id'],
|
|
||||||
'notes': v.get('notes', ''),
|
|
||||||
'backtest_start_time': v['backtest_start_time'],
|
|
||||||
}
|
|
||||||
for filename in _get_backtest_files(dirname)
|
for filename in _get_backtest_files(dirname)
|
||||||
for s, v in load_backtest_metadata(filename).items()
|
for result in _extract_backtest_result(filename)
|
||||||
if v
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -326,7 +330,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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -311,11 +311,13 @@ class DataProvider:
|
|||||||
timerange = TimeRange.parse_timerange(None if self._config.get(
|
timerange = TimeRange.parse_timerange(None if self._config.get(
|
||||||
'timerange') is None else str(self._config.get('timerange')))
|
'timerange') is None else str(self._config.get('timerange')))
|
||||||
|
|
||||||
# It is not necessary to add the training candles, as they
|
startup_candles = self.get_required_startup(str(timeframe))
|
||||||
# were already added at the beginning of the backtest.
|
|
||||||
startup_candles = self.get_required_startup(str(timeframe), False)
|
|
||||||
tf_seconds = timeframe_to_seconds(str(timeframe))
|
tf_seconds = timeframe_to_seconds(str(timeframe))
|
||||||
timerange.subtract_start(tf_seconds * startup_candles)
|
timerange.subtract_start(tf_seconds * startup_candles)
|
||||||
|
|
||||||
|
logger.info(f"Loading data for {pair} {timeframe} "
|
||||||
|
f"from {timerange.start_fmt} to {timerange.stop_fmt}")
|
||||||
|
|
||||||
self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
|
self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
@@ -327,7 +329,7 @@ class DataProvider:
|
|||||||
)
|
)
|
||||||
return self.__cached_pairs_backtesting[saved_pair].copy()
|
return self.__cached_pairs_backtesting[saved_pair].copy()
|
||||||
|
|
||||||
def get_required_startup(self, timeframe: str, add_train_candles: bool = True) -> int:
|
def get_required_startup(self, timeframe: str) -> int:
|
||||||
freqai_config = self._config.get('freqai', {})
|
freqai_config = self._config.get('freqai', {})
|
||||||
if not freqai_config.get('enabled', False):
|
if not freqai_config.get('enabled', False):
|
||||||
return self._config.get('startup_candle_count', 0)
|
return self._config.get('startup_candle_count', 0)
|
||||||
@@ -337,12 +339,11 @@ class DataProvider:
|
|||||||
# make sure the startupcandles is at least the set maximum indicator periods
|
# make sure the startupcandles is at least the set maximum indicator periods
|
||||||
self._config['startup_candle_count'] = max(startup_candles, max(indicator_periods))
|
self._config['startup_candle_count'] = max(startup_candles, max(indicator_periods))
|
||||||
tf_seconds = timeframe_to_seconds(timeframe)
|
tf_seconds = timeframe_to_seconds(timeframe)
|
||||||
train_candles = 0
|
train_candles = freqai_config['train_period_days'] * 86400 / tf_seconds
|
||||||
if add_train_candles:
|
|
||||||
train_candles = freqai_config['train_period_days'] * 86400 / tf_seconds
|
|
||||||
total_candles = int(self._config['startup_candle_count'] + train_candles)
|
total_candles = int(self._config['startup_candle_count'] + train_candles)
|
||||||
logger.info(f'Increasing startup_candle_count for freqai to {total_candles}')
|
logger.info(
|
||||||
return total_candles
|
f'Increasing startup_candle_count for freqai on {timeframe} to {total_candles}')
|
||||||
|
return total_candles
|
||||||
|
|
||||||
def get_pair_dataframe(
|
def get_pair_dataframe(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from pandas import DataFrame, concat
|
|||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS,
|
from freqtrade.constants import (DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS,
|
||||||
DL_DATA_TIMEFRAMES, Config)
|
DL_DATA_TIMEFRAMES, DOCS_LINK, Config)
|
||||||
from freqtrade.data.converter import (clean_ohlcv_dataframe, convert_trades_to_ohlcv,
|
from freqtrade.data.converter import (clean_ohlcv_dataframe, convert_trades_to_ohlcv,
|
||||||
ohlcv_to_dataframe, trades_df_remove_duplicates,
|
ohlcv_to_dataframe, trades_df_remove_duplicates,
|
||||||
trades_list_to_df)
|
trades_list_to_df)
|
||||||
@@ -18,8 +18,8 @@ from freqtrade.exceptions import OperationalException
|
|||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||||
from freqtrade.util import dt_ts, format_ms_time
|
from freqtrade.util import dt_ts, format_ms_time
|
||||||
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
|
||||||
from freqtrade.util.datetime_helpers import dt_now
|
from freqtrade.util.datetime_helpers import dt_now
|
||||||
|
from freqtrade.util.migrations import migrate_data
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -311,15 +311,19 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
|||||||
# Predefined candletype (and timeframe) depending on exchange
|
# Predefined candletype (and timeframe) depending on exchange
|
||||||
# Downloads what is necessary to backtest based on futures data.
|
# Downloads what is necessary to backtest based on futures data.
|
||||||
tf_mark = exchange.get_option('mark_ohlcv_timeframe')
|
tf_mark = exchange.get_option('mark_ohlcv_timeframe')
|
||||||
|
tf_funding_rate = exchange.get_option('funding_fee_timeframe')
|
||||||
|
|
||||||
fr_candle_type = CandleType.from_string(exchange.get_option('mark_ohlcv_price'))
|
fr_candle_type = CandleType.from_string(exchange.get_option('mark_ohlcv_price'))
|
||||||
# All exchanges need FundingRate for futures trading.
|
# All exchanges need FundingRate for futures trading.
|
||||||
# The timeframe is aligned to the mark-price timeframe.
|
# The timeframe is aligned to the mark-price timeframe.
|
||||||
for funding_candle_type in (CandleType.FUNDING_RATE, fr_candle_type):
|
combs = ((CandleType.FUNDING_RATE, tf_funding_rate), (fr_candle_type, tf_mark))
|
||||||
|
for candle_type_f, tf in combs:
|
||||||
|
logger.debug(f'Downloading pair {pair}, {candle_type_f}, interval {tf}.')
|
||||||
_download_pair_history(pair=pair, process=process,
|
_download_pair_history(pair=pair, process=process,
|
||||||
datadir=datadir, exchange=exchange,
|
datadir=datadir, exchange=exchange,
|
||||||
timerange=timerange, data_handler=data_handler,
|
timerange=timerange, data_handler=data_handler,
|
||||||
timeframe=str(tf_mark), new_pairs_days=new_pairs_days,
|
timeframe=str(tf), new_pairs_days=new_pairs_days,
|
||||||
candle_type=funding_candle_type,
|
candle_type=candle_type_f,
|
||||||
erase=erase, prepend=prepend)
|
erase=erase, prepend=prepend)
|
||||||
|
|
||||||
return pairs_not_available
|
return pairs_not_available
|
||||||
@@ -500,6 +504,12 @@ def download_data_main(config: Config) -> None:
|
|||||||
logger.info(f"About to download pairs: {expanded_pairs}, "
|
logger.info(f"About to download pairs: {expanded_pairs}, "
|
||||||
f"intervals: {config['timeframes']} to {config['datadir']}")
|
f"intervals: {config['timeframes']} to {config['datadir']}")
|
||||||
|
|
||||||
|
if len(expanded_pairs) == 0:
|
||||||
|
logger.warning(
|
||||||
|
"No pairs available for download. "
|
||||||
|
"Please make sure you're using the correct Pair naming for your selected trade mode. \n"
|
||||||
|
f"More info: {DOCS_LINK}/bot-basics/#pair-naming")
|
||||||
|
|
||||||
for timeframe in config['timeframes']:
|
for timeframe in config['timeframes']:
|
||||||
exchange.validate_timeframes(timeframe)
|
exchange.validate_timeframes(timeframe)
|
||||||
|
|
||||||
@@ -527,7 +537,7 @@ def download_data_main(config: Config) -> None:
|
|||||||
"Please use `--dl-trades` instead for this exchange "
|
"Please use `--dl-trades` instead for this exchange "
|
||||||
"(will unfortunately take a long time)."
|
"(will unfortunately take a long time)."
|
||||||
)
|
)
|
||||||
migrate_binance_futures_data(config)
|
migrate_data(config, exchange)
|
||||||
pairs_not_available = refresh_backtest_ohlcv_data(
|
pairs_not_available = refresh_backtest_ohlcv_data(
|
||||||
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||||
datadir=config['datadir'], timerange=timerange,
|
datadir=config['datadir'], timerange=timerange,
|
||||||
|
|||||||
@@ -403,6 +403,34 @@ class IDataHandler(ABC):
|
|||||||
return
|
return
|
||||||
file_old.rename(file_new)
|
file_old.rename(file_new)
|
||||||
|
|
||||||
|
def fix_funding_fee_timeframe(self, ff_timeframe: str):
|
||||||
|
"""
|
||||||
|
Temporary method to migrate data from old funding fee timeframe to the correct timeframe
|
||||||
|
Applies to bybit and okx, where funding-fee and mark candles have different timeframes.
|
||||||
|
"""
|
||||||
|
paircombs = self.ohlcv_get_available_data(self._datadir, TradingMode.FUTURES)
|
||||||
|
funding_rate_combs = [
|
||||||
|
f for f in paircombs if f[2] == CandleType.FUNDING_RATE and f[1] != ff_timeframe
|
||||||
|
]
|
||||||
|
|
||||||
|
if funding_rate_combs:
|
||||||
|
logger.warning(
|
||||||
|
f'Migrating {len(funding_rate_combs)} funding fees to correct timeframe.')
|
||||||
|
|
||||||
|
for pair, timeframe, candletype in funding_rate_combs:
|
||||||
|
old_name = self._pair_data_filename(self._datadir, pair, timeframe, candletype)
|
||||||
|
new_name = self._pair_data_filename(self._datadir, pair, ff_timeframe, candletype)
|
||||||
|
|
||||||
|
if not Path(old_name).exists():
|
||||||
|
logger.warning(f'{old_name} does not exist, skipping.')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if Path(new_name).exists():
|
||||||
|
logger.warning(f'{new_name} already exists, Removing.')
|
||||||
|
Path(new_name).unlink()
|
||||||
|
|
||||||
|
Path(old_name).rename(new_name)
|
||||||
|
|
||||||
|
|
||||||
def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
|
def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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
@@ -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],
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class Exchange:
|
|||||||
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
|
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
|
||||||
"mark_ohlcv_price": "mark",
|
"mark_ohlcv_price": "mark",
|
||||||
"mark_ohlcv_timeframe": "8h",
|
"mark_ohlcv_timeframe": "8h",
|
||||||
|
"funding_fee_timeframe": "8h",
|
||||||
"ccxt_futures_name": "swap",
|
"ccxt_futures_name": "swap",
|
||||||
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
||||||
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
||||||
@@ -121,11 +122,12 @@ class Exchange:
|
|||||||
# Cache for 10 minutes ...
|
# Cache for 10 minutes ...
|
||||||
self._cache_lock = Lock()
|
self._cache_lock = Lock()
|
||||||
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10)
|
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10)
|
||||||
# Cache values for 1800 to avoid frequent polling of the exchange for prices
|
# Cache values for 300 to avoid frequent polling of the exchange for prices
|
||||||
# Caching only applies to RPC methods, so prices for open trades are still
|
# Caching only applies to RPC methods, so prices for open trades are still
|
||||||
# refreshed once every iteration.
|
# refreshed once every iteration.
|
||||||
self._exit_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
|
# Shouldn't be too high either, as it'll freeze UI updates in case of open orders.
|
||||||
self._entry_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
|
self._exit_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=300)
|
||||||
|
self._entry_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=300)
|
||||||
|
|
||||||
# Holds candles
|
# Holds candles
|
||||||
self._klines: Dict[PairWithTimeframe, DataFrame] = {}
|
self._klines: Dict[PairWithTimeframe, DataFrame] = {}
|
||||||
@@ -319,10 +321,11 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _log_exchange_response(self, endpoint, response) -> None:
|
def _log_exchange_response(self, endpoint: str, response, *, add_info=None) -> None:
|
||||||
""" Log exchange responses """
|
""" Log exchange responses """
|
||||||
if self.log_responses:
|
if self.log_responses:
|
||||||
logger.info(f"API {endpoint}: {response}")
|
add_info_str = "" if add_info is None else f" {add_info}: "
|
||||||
|
logger.info(f"API {endpoint}: {add_info_str}{response}")
|
||||||
|
|
||||||
def ohlcv_candle_limit(
|
def ohlcv_candle_limit(
|
||||||
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
|
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
|
||||||
@@ -330,6 +333,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
|
||||||
@@ -1383,7 +1387,7 @@ class Exchange:
|
|||||||
order = self.fetch_stoploss_order(order_id, pair)
|
order = self.fetch_stoploss_order(order_id, pair)
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.warning(f"Could not fetch cancelled stoploss order {order_id}.")
|
logger.warning(f"Could not fetch cancelled stoploss order {order_id}.")
|
||||||
order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
|
order = {'id': order_id, 'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
|
||||||
|
|
||||||
return order
|
return order
|
||||||
|
|
||||||
@@ -2413,6 +2417,8 @@ class Exchange:
|
|||||||
symbol=pair,
|
symbol=pair,
|
||||||
since=since
|
since=since
|
||||||
)
|
)
|
||||||
|
self._log_exchange_response('funding_history', funding_history,
|
||||||
|
add_info=f"pair: {pair}, since: {since}")
|
||||||
return sum(fee['amount'] for fee in funding_history)
|
return sum(fee['amount'] for fee in funding_history)
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
@@ -2729,17 +2735,16 @@ class Exchange:
|
|||||||
# Only really relevant for trades very close to the full hour
|
# Only really relevant for trades very close to the full hour
|
||||||
open_date = timeframe_to_prev_date('1h', open_date)
|
open_date = timeframe_to_prev_date('1h', open_date)
|
||||||
timeframe = self._ft_has['mark_ohlcv_timeframe']
|
timeframe = self._ft_has['mark_ohlcv_timeframe']
|
||||||
timeframe_ff = self._ft_has.get('funding_fee_timeframe',
|
timeframe_ff = self._ft_has['funding_fee_timeframe']
|
||||||
self._ft_has['mark_ohlcv_timeframe'])
|
mark_price_type = CandleType.from_string(self._ft_has["mark_ohlcv_price"])
|
||||||
|
|
||||||
if not close_date:
|
if not close_date:
|
||||||
close_date = datetime.now(timezone.utc)
|
close_date = datetime.now(timezone.utc)
|
||||||
since_ms = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000
|
since_ms = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000
|
||||||
|
|
||||||
mark_comb: PairWithTimeframe = (
|
mark_comb: PairWithTimeframe = (pair, timeframe, mark_price_type)
|
||||||
pair, timeframe, CandleType.from_string(self._ft_has["mark_ohlcv_price"]))
|
|
||||||
|
|
||||||
funding_comb: PairWithTimeframe = (pair, timeframe_ff, CandleType.FUNDING_RATE)
|
funding_comb: PairWithTimeframe = (pair, timeframe_ff, CandleType.FUNDING_RATE)
|
||||||
|
|
||||||
candle_histories = self.refresh_latest_ohlcv(
|
candle_histories = self.refresh_latest_ohlcv(
|
||||||
[mark_comb, funding_comb],
|
[mark_comb, funding_comb],
|
||||||
since_ms=since_ms,
|
since_ms=since_ms,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class Kraken(Exchange):
|
|||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
"stop_price_param": "stopPrice",
|
"stop_price_param": "stopPrice",
|
||||||
"stop_price_prop": "stopPrice",
|
"stop_price_prop": "stopPrice",
|
||||||
|
"order_time_in_force": ["GTC", "IOC", "PO"],
|
||||||
"ohlcv_candle_limit": 720,
|
"ohlcv_candle_limit": 720,
|
||||||
"ohlcv_has_history": False,
|
"ohlcv_has_history": False,
|
||||||
"trades_pagination": "id",
|
"trades_pagination": "id",
|
||||||
@@ -187,6 +188,9 @@ class Kraken(Exchange):
|
|||||||
)
|
)
|
||||||
if leverage > 1.0:
|
if leverage > 1.0:
|
||||||
params['leverage'] = round(leverage)
|
params['leverage'] = round(leverage)
|
||||||
|
if time_in_force == 'PO':
|
||||||
|
params.pop('timeInForce', None)
|
||||||
|
params['postOnly'] = True
|
||||||
return params
|
return params
|
||||||
|
|
||||||
def calculate_funding_fees(
|
def calculate_funding_fees(
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ class Okx(Exchange):
|
|||||||
f'StoplossOrder not found (pair: {pair} id: {order_id}).')
|
f'StoplossOrder not found (pair: {pair} id: {order_id}).')
|
||||||
|
|
||||||
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
|
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
|
||||||
if order['type'] == 'stop':
|
if order.get('type', '') == 'stop':
|
||||||
return safe_value_fallback2(order, order, 'id_stop', 'id')
|
return safe_value_fallback2(order, order, 'id_stop', 'id')
|
||||||
return order['id']
|
return order['id']
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from joblib import Parallel
|
|
||||||
from sklearn.base import is_classifier
|
from sklearn.base import is_classifier
|
||||||
from sklearn.multioutput import MultiOutputClassifier, _fit_estimator
|
from sklearn.multioutput import MultiOutputClassifier, _fit_estimator
|
||||||
from sklearn.utils.fixes import delayed
|
|
||||||
from sklearn.utils.multiclass import check_classification_targets
|
from sklearn.utils.multiclass import check_classification_targets
|
||||||
|
from sklearn.utils.parallel import Parallel, delayed
|
||||||
from sklearn.utils.validation import has_fit_parameter
|
from sklearn.utils.validation import has_fit_parameter
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from joblib import Parallel
|
|
||||||
from sklearn.multioutput import MultiOutputRegressor, _fit_estimator
|
from sklearn.multioutput import MultiOutputRegressor, _fit_estimator
|
||||||
from sklearn.utils.fixes import delayed
|
from sklearn.utils.parallel import Parallel, delayed
|
||||||
from sklearn.utils.validation import has_fit_parameter
|
from sklearn.utils.validation import has_fit_parameter
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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'])
|
||||||
|
|||||||
@@ -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 "
|
||||||
@@ -709,6 +709,8 @@ class FreqaiDataKitchen:
|
|||||||
pair, tf, strategy, corr_dataframes, base_dataframes, is_corr_pairs)
|
pair, tf, strategy, corr_dataframes, base_dataframes, is_corr_pairs)
|
||||||
informative_copy = informative_df.copy()
|
informative_copy = informative_df.copy()
|
||||||
|
|
||||||
|
logger.debug(f"Populating features for {pair} {tf}")
|
||||||
|
|
||||||
for t in self.freqai_config["feature_parameters"]["indicator_periods_candles"]:
|
for t in self.freqai_config["feature_parameters"]["indicator_periods_candles"]:
|
||||||
df_features = strategy.feature_engineering_expand_all(
|
df_features = strategy.feature_engineering_expand_all(
|
||||||
informative_copy.copy(), t, metadata=metadata)
|
informative_copy.copy(), t, metadata=metadata)
|
||||||
@@ -788,6 +790,7 @@ class FreqaiDataKitchen:
|
|||||||
|
|
||||||
if not prediction_dataframe.empty:
|
if not prediction_dataframe.empty:
|
||||||
dataframe = prediction_dataframe.copy()
|
dataframe = prediction_dataframe.copy()
|
||||||
|
base_dataframes[self.config["timeframe"]] = dataframe.copy()
|
||||||
else:
|
else:
|
||||||
dataframe = base_dataframes[self.config["timeframe"]].copy()
|
dataframe = base_dataframes[self.config["timeframe"]].copy()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+34
-36
@@ -18,8 +18,8 @@ from freqtrade.constants import BuySell, Config, EntryExecuteMode, ExchangeConfi
|
|||||||
from freqtrade.data.converter import order_book_to_dataframe
|
from freqtrade.data.converter import order_book_to_dataframe
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, RunMode, SignalDirection,
|
from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, SignalDirection, State,
|
||||||
State, TradingMode)
|
TradingMode)
|
||||||
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
||||||
InvalidOrderException, PricingError)
|
InvalidOrderException, PricingError)
|
||||||
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, remove_exchange_credentials,
|
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, remove_exchange_credentials,
|
||||||
@@ -33,12 +33,12 @@ 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
|
||||||
from freqtrade.util.binance_mig import migrate_binance_futures_names
|
from freqtrade.util.migrations import migrate_binance_futures_names
|
||||||
from freqtrade.wallets import Wallets
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
|
|
||||||
@@ -83,6 +83,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
PairLocks.timeframe = self.config['timeframe']
|
PairLocks.timeframe = self.config['timeframe']
|
||||||
|
|
||||||
self.pairlists = PairListManager(self.exchange, self.config)
|
self.pairlists = PairListManager(self.exchange, self.config)
|
||||||
|
self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT)
|
||||||
|
self.last_process: Optional[datetime] = None
|
||||||
|
|
||||||
# RPC runs in separate threads, can start handling external commands just after
|
# RPC runs in separate threads, can start handling external commands just after
|
||||||
# initialization, even before Freqtradebot has a chance to start its throttling,
|
# initialization, even before Freqtradebot has a chance to start its throttling,
|
||||||
@@ -119,8 +121,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self._exit_lock = Lock()
|
self._exit_lock = Lock()
|
||||||
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
||||||
|
|
||||||
self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT)
|
|
||||||
|
|
||||||
self._schedule = Scheduler()
|
self._schedule = Scheduler()
|
||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
@@ -135,7 +135,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
for minutes in [1, 31]:
|
for minutes in [1, 31]:
|
||||||
t = str(time(time_slot, minutes, 2))
|
t = str(time(time_slot, minutes, 2))
|
||||||
self._schedule.every().day.at(t).do(update)
|
self._schedule.every().day.at(t).do(update)
|
||||||
self.last_process: Optional[datetime] = None
|
|
||||||
|
|
||||||
self.strategy.ft_bot_start()
|
self.strategy.ft_bot_start()
|
||||||
# Initialize protections AFTER bot start - otherwise parameters are not loaded.
|
# Initialize protections AFTER bot start - otherwise parameters are not loaded.
|
||||||
@@ -580,7 +579,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
|
||||||
@@ -672,20 +672,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
amount = self.exchange.amount_to_contract_precision(
|
amount = self.exchange.amount_to_contract_precision(
|
||||||
trade.pair,
|
trade.pair,
|
||||||
abs(float(FtPrecise(stake_amount * trade.leverage) / FtPrecise(current_exit_rate))))
|
abs(float(FtPrecise(stake_amount * trade.leverage) / FtPrecise(current_exit_rate))))
|
||||||
if amount > trade.amount:
|
|
||||||
# This is currently ineffective as remaining would become < min tradable
|
|
||||||
# Fixing this would require checking for 0.0 there -
|
|
||||||
# if we decide that this callback is allowed to "fully exit"
|
|
||||||
logger.info(
|
|
||||||
f"Adjusting amount to trade.amount as it is higher. {amount} > {trade.amount}")
|
|
||||||
amount = trade.amount
|
|
||||||
|
|
||||||
if amount == 0.0:
|
if amount == 0.0:
|
||||||
logger.info("Amount to exit is 0.0 due to exchange limits - not exiting.")
|
logger.info("Amount to exit is 0.0 due to exchange limits - not exiting.")
|
||||||
return
|
return
|
||||||
|
|
||||||
remaining = (trade.amount - amount) * current_exit_rate
|
remaining = (trade.amount - amount) * current_exit_rate
|
||||||
if min_exit_stake and remaining < min_exit_stake:
|
if min_exit_stake and remaining != 0 and remaining < min_exit_stake:
|
||||||
logger.info(f"Remaining amount of {remaining} would be smaller "
|
logger.info(f"Remaining amount of {remaining} would be smaller "
|
||||||
f"than the minimum of {min_exit_stake}.")
|
f"than the minimum of {min_exit_stake}.")
|
||||||
return
|
return
|
||||||
@@ -903,7 +896,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)
|
||||||
@@ -1009,12 +1002,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if open_rate is None:
|
if open_rate is None:
|
||||||
open_rate = trade.open_rate
|
open_rate = trade.open_rate
|
||||||
|
|
||||||
current_rate = trade.open_rate_requested
|
current_rate = self.exchange.get_rate(
|
||||||
if self.dataprovider.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
|
||||||
current_rate = self.exchange.get_rate(
|
|
||||||
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,
|
||||||
@@ -1029,6 +1020,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'stake_amount': trade.stake_amount,
|
'stake_amount': trade.stake_amount,
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||||
|
'quote_currency': self.exchange.get_pair_quote_currency(trade.pair),
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||||
'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount),
|
'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount),
|
||||||
'open_date': trade.open_date_utc or datetime.now(timezone.utc),
|
'open_date': trade.open_date_utc or datetime.now(timezone.utc),
|
||||||
@@ -1062,6 +1054,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'open_rate': trade.open_rate,
|
'open_rate': trade.open_rate,
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||||
|
'quote_currency': self.exchange.get_pair_quote_currency(trade.pair),
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||||
'amount': trade.amount,
|
'amount': trade.amount,
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
@@ -1347,9 +1340,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
not_closed = order['status'] == 'open' or fully_cancelled
|
not_closed = order['status'] == 'open' or fully_cancelled
|
||||||
|
|
||||||
if not_closed:
|
if not_closed:
|
||||||
if fully_cancelled or (
|
if (
|
||||||
open_order and self.strategy.ft_check_timed_out(
|
fully_cancelled or (
|
||||||
trade, open_order, datetime.now(timezone.utc)
|
open_order and self.strategy.ft_check_timed_out(
|
||||||
|
trade, open_order, datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
self.handle_cancel_order(
|
self.handle_cancel_order(
|
||||||
@@ -1791,9 +1786,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,
|
||||||
@@ -1809,20 +1804,22 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'open_rate': trade.open_rate,
|
'open_rate': trade.open_rate,
|
||||||
'close_rate': order_rate,
|
'close_rate': order_rate,
|
||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'profit_amount': profit.profit_abs if fill else profit.total_profit,
|
'profit_amount': profit.profit_abs,
|
||||||
'profit_ratio': profit.profit_ratio,
|
'profit_ratio': profit.profit_ratio,
|
||||||
'buy_tag': trade.enter_tag,
|
'buy_tag': trade.enter_tag,
|
||||||
'enter_tag': trade.enter_tag,
|
'enter_tag': trade.enter_tag,
|
||||||
'sell_reason': trade.exit_reason, # Deprecated
|
|
||||||
'exit_reason': trade.exit_reason,
|
'exit_reason': trade.exit_reason,
|
||||||
'open_date': trade.open_date_utc,
|
'open_date': trade.open_date_utc,
|
||||||
'close_date': trade.close_date_utc or datetime.now(timezone.utc),
|
'close_date': trade.close_date_utc or datetime.now(timezone.utc),
|
||||||
'stake_amount': trade.stake_amount,
|
'stake_amount': trade.stake_amount,
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||||
|
'quote_currency': self.exchange.get_pair_quote_currency(trade.pair),
|
||||||
'fiat_currency': self.config.get('fiat_display_currency'),
|
'fiat_currency': self.config.get('fiat_display_currency'),
|
||||||
'sub_trade': sub_trade,
|
'sub_trade': sub_trade,
|
||||||
'cumulative_profit': trade.realized_profit,
|
'cumulative_profit': trade.realized_profit,
|
||||||
|
'final_profit_ratio': trade.close_profit if not trade.is_open else None,
|
||||||
|
'is_final_exit': trade.is_open is False,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Send the message
|
# Send the message
|
||||||
@@ -1845,9 +1842,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(),
|
||||||
@@ -1864,12 +1861,12 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'profit_ratio': profit.profit_ratio,
|
'profit_ratio': profit.profit_ratio,
|
||||||
'buy_tag': trade.enter_tag,
|
'buy_tag': trade.enter_tag,
|
||||||
'enter_tag': trade.enter_tag,
|
'enter_tag': trade.enter_tag,
|
||||||
'sell_reason': trade.exit_reason, # Deprecated
|
|
||||||
'exit_reason': trade.exit_reason,
|
'exit_reason': trade.exit_reason,
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
'close_date': trade.close_date or datetime.now(timezone.utc),
|
'close_date': trade.close_date or datetime.now(timezone.utc),
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||||
|
'quote_currency': self.exchange.get_pair_quote_currency(trade.pair),
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||||
'reason': reason,
|
'reason': reason,
|
||||||
'sub_trade': sub_trade,
|
'sub_trade': sub_trade,
|
||||||
@@ -1977,15 +1974,16 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool):
|
self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool):
|
||||||
"""send "fill" notifications"""
|
"""send "fill" notifications"""
|
||||||
|
|
||||||
sub_trade = not isclose(order.safe_amount_after_fee,
|
|
||||||
trade.amount, abs_tol=constants.MATH_CLOSE_PREC)
|
|
||||||
if order.ft_order_side == trade.exit_side:
|
if order.ft_order_side == trade.exit_side:
|
||||||
# Exit notification
|
# Exit notification
|
||||||
if send_msg and not stoploss_order and order.order_id not in trade.open_orders_ids:
|
if send_msg and not stoploss_order and order.order_id not in trade.open_orders_ids:
|
||||||
self._notify_exit(trade, '', fill=True, sub_trade=sub_trade, order=order)
|
self._notify_exit(trade, order.order_type, fill=True,
|
||||||
|
sub_trade=trade.is_open, order=order)
|
||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
self.handle_protections(trade.pair, trade.trade_direction)
|
self.handle_protections(trade.pair, trade.trade_direction)
|
||||||
elif send_msg and order.order_id not in trade.open_orders_ids and not stoploss_order:
|
elif send_msg and order.order_id not in trade.open_orders_ids and not stoploss_order:
|
||||||
|
sub_trade = not isclose(order.safe_amount_after_fee,
|
||||||
|
trade.amount, abs_tol=constants.MATH_CLOSE_PREC)
|
||||||
# Enter fill
|
# Enter fill
|
||||||
self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade)
|
self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade)
|
||||||
|
|
||||||
|
|||||||
@@ -11,41 +11,12 @@ from urllib.parse import urlparse
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import rapidjson
|
import rapidjson
|
||||||
|
|
||||||
from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN
|
|
||||||
from freqtrade.enums import SignalTagType, SignalType
|
from freqtrade.enums import SignalTagType, SignalType
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def decimals_per_coin(coin: str):
|
|
||||||
"""
|
|
||||||
Helper method getting decimal amount for this coin
|
|
||||||
example usage: f".{decimals_per_coin('USD')}f"
|
|
||||||
:param coin: Which coin are we printing the price / value for
|
|
||||||
"""
|
|
||||||
return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK)
|
|
||||||
|
|
||||||
|
|
||||||
def round_coin_value(
|
|
||||||
value: float, coin: str, show_coin_name=True, keep_trailing_zeros=False) -> str:
|
|
||||||
"""
|
|
||||||
Get price value for this coin
|
|
||||||
:param value: Value to be printed
|
|
||||||
:param coin: Which coin are we printing the price / value for
|
|
||||||
:param show_coin_name: Return string in format: "222.22 USDT" or "222.22"
|
|
||||||
:param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2"
|
|
||||||
:return: Formatted / rounded value (with or without coin name)
|
|
||||||
"""
|
|
||||||
val = f"{value:.{decimals_per_coin(coin)}f}"
|
|
||||||
if not keep_trailing_zeros:
|
|
||||||
val = val.rstrip('0').rstrip('.')
|
|
||||||
if show_coin_name:
|
|
||||||
val = f"{val} {coin}"
|
|
||||||
|
|
||||||
return val
|
|
||||||
|
|
||||||
|
|
||||||
def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = True) -> None:
|
def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = True) -> None:
|
||||||
"""
|
"""
|
||||||
Dump JSON data into a file
|
Dump JSON data into a file
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
|||||||
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.types import BacktestResultType, get_BacktestResultType_default
|
from freqtrade.types import BacktestResultType, get_BacktestResultType_default
|
||||||
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
from freqtrade.util.migrations import migrate_data
|
||||||
from freqtrade.wallets import Wallets
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
|
|
||||||
@@ -145,19 +145,20 @@ class Backtesting:
|
|||||||
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
||||||
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
|
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
|
||||||
|
|
||||||
if self.config.get('freqai', {}).get('enabled', False):
|
|
||||||
# For FreqAI, increase the required_startup to includes the training data
|
|
||||||
self.required_startup = self.dataprovider.get_required_startup(self.timeframe)
|
|
||||||
|
|
||||||
# Add maximum startup candle count to configuration for informative pairs support
|
# Add maximum startup candle count to configuration for informative pairs support
|
||||||
self.config['startup_candle_count'] = self.required_startup
|
self.config['startup_candle_count'] = self.required_startup
|
||||||
|
|
||||||
|
if self.config.get('freqai', {}).get('enabled', False):
|
||||||
|
# For FreqAI, increase the required_startup to includes the training data
|
||||||
|
# This value should NOT be written to startup_candle_count
|
||||||
|
self.required_startup = self.dataprovider.get_required_startup(self.timeframe)
|
||||||
|
|
||||||
self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
|
self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
|
||||||
# strategies which define "can_short=True" will fail to load in Spot mode.
|
# strategies which define "can_short=True" will fail to load in Spot mode.
|
||||||
self._can_short = self.trading_mode != TradingMode.SPOT
|
self._can_short = self.trading_mode != TradingMode.SPOT
|
||||||
self._position_stacking: bool = self.config.get('position_stacking', False)
|
self._position_stacking: bool = self.config.get('position_stacking', False)
|
||||||
self.enable_protections: bool = self.config.get('enable_protections', False)
|
self.enable_protections: bool = self.config.get('enable_protections', False)
|
||||||
migrate_binance_futures_data(config)
|
migrate_data(config, self.exchange)
|
||||||
|
|
||||||
self.init_backtest()
|
self.init_backtest()
|
||||||
|
|
||||||
@@ -239,7 +240,7 @@ class Backtesting:
|
|||||||
pairs=self.pairlists.whitelist,
|
pairs=self.pairlists.whitelist,
|
||||||
timeframe=self.timeframe,
|
timeframe=self.timeframe,
|
||||||
timerange=self.timerange,
|
timerange=self.timerange,
|
||||||
startup_candles=self.config['startup_candle_count'],
|
startup_candles=self.required_startup,
|
||||||
fail_without_data=True,
|
fail_without_data=True,
|
||||||
data_format=self.config['dataformat_ohlcv'],
|
data_format=self.config['dataformat_ohlcv'],
|
||||||
candle_type=self.config.get('candle_type_def', CandleType.SPOT)
|
candle_type=self.config.get('candle_type_def', CandleType.SPOT)
|
||||||
@@ -276,11 +277,15 @@ 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('funding_fee_timeframe')
|
||||||
|
self.funding_fee_timeframe_secs: int = timeframe_to_seconds(self.funding_fee_timeframe)
|
||||||
|
mark_timeframe: str = self.exchange.get_option('mark_ohlcv_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 +297,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=mark_timeframe,
|
||||||
timerange=self.timerange,
|
timerange=self.timerange,
|
||||||
startup_candles=0,
|
startup_candles=0,
|
||||||
fail_without_data=True,
|
fail_without_data=True,
|
||||||
@@ -528,7 +533,7 @@ class Backtesting:
|
|||||||
def _get_adjust_trade_entry_for_candle(
|
def _get_adjust_trade_entry_for_candle(
|
||||||
self, trade: LocalTrade, row: Tuple, current_time: datetime
|
self, trade: LocalTrade, row: Tuple, current_time: datetime
|
||||||
) -> LocalTrade:
|
) -> LocalTrade:
|
||||||
current_rate = row[OPEN_IDX]
|
current_rate: float = row[OPEN_IDX]
|
||||||
current_profit = trade.calc_profit_ratio(current_rate)
|
current_profit = trade.calc_profit_ratio(current_rate)
|
||||||
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1)
|
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1)
|
||||||
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
|
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
|
||||||
@@ -561,11 +566,8 @@ class Backtesting:
|
|||||||
self.precision_mode, trade.contract_size)
|
self.precision_mode, trade.contract_size)
|
||||||
if amount == 0.0:
|
if amount == 0.0:
|
||||||
return trade
|
return trade
|
||||||
if amount > trade.amount:
|
|
||||||
# This is currently ineffective as remaining would become < min tradable
|
|
||||||
amount = trade.amount
|
|
||||||
remaining = (trade.amount - amount) * current_rate
|
remaining = (trade.amount - amount) * current_rate
|
||||||
if remaining < min_stake:
|
if min_stake and remaining != 0 and remaining < min_stake:
|
||||||
# Remaining stake is too low to be sold.
|
# Remaining stake is too low to be sold.
|
||||||
return trade
|
return trade
|
||||||
exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT)
|
exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,9 +14,10 @@ from pandas import isna, json_normalize
|
|||||||
from freqtrade.constants import FTHYPT_FILEVERSION, Config
|
from freqtrade.constants import FTHYPT_FILEVERSION, Config
|
||||||
from freqtrade.enums import HyperoptState
|
from freqtrade.enums import HyperoptState
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2
|
from freqtrade.misc import deep_merge_dicts, round_dict, safe_value_fallback2
|
||||||
from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs
|
from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs
|
||||||
from freqtrade.optimize.optimize_reports import generate_wins_draws_losses
|
from freqtrade.optimize.optimize_reports import generate_wins_draws_losses
|
||||||
|
from freqtrade.util import fmt_coin
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -405,7 +406,7 @@ class HyperoptTools:
|
|||||||
|
|
||||||
trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply(
|
trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply(
|
||||||
lambda x: "{} {}".format(
|
lambda x: "{} {}".format(
|
||||||
round_coin_value(x['max_drawdown_abs'], stake_currency, keep_trailing_zeros=True),
|
fmt_coin(x['max_drawdown_abs'], stake_currency, keep_trailing_zeros=True),
|
||||||
(f"({x['max_drawdown_account']:,.2%})"
|
(f"({x['max_drawdown_account']:,.2%})"
|
||||||
if has_account_drawdown
|
if has_account_drawdown
|
||||||
else f"({x['max_drawdown']:,.2%})"
|
else f"({x['max_drawdown']:,.2%})"
|
||||||
@@ -420,7 +421,7 @@ class HyperoptTools:
|
|||||||
|
|
||||||
trials['Profit'] = trials.apply(
|
trials['Profit'] = trials.apply(
|
||||||
lambda x: '{} {}'.format(
|
lambda x: '{} {}'.format(
|
||||||
round_coin_value(x['Total profit'], stake_currency, keep_trailing_zeros=True),
|
fmt_coin(x['Total profit'], stake_currency, keep_trailing_zeros=True),
|
||||||
f"({x['Profit']:,.2%})".rjust(10, ' ')
|
f"({x['Profit']:,.2%})".rjust(10, ' ')
|
||||||
).rjust(25 + len(stake_currency))
|
).rjust(25 + len(stake_currency))
|
||||||
if x['Total profit'] != 0.0 else '--'.rjust(25 + len(stake_currency)),
|
if x['Total profit'] != 0.0 else '--'.rjust(25 + len(stake_currency)),
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ from typing import Any, Dict, List
|
|||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config
|
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config
|
||||||
from freqtrade.misc import decimals_per_coin, round_coin_value
|
|
||||||
from freqtrade.optimize.optimize_reports.optimize_reports import generate_periodic_breakdown_stats
|
from freqtrade.optimize.optimize_reports.optimize_reports import generate_periodic_breakdown_stats
|
||||||
from freqtrade.types import BacktestResultType
|
from freqtrade.types import BacktestResultType
|
||||||
|
from freqtrade.util import decimals_per_coin, fmt_coin
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -63,7 +63,7 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st
|
|||||||
def text_table_exit_reason(exit_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
|
def text_table_exit_reason(exit_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||||
"""
|
"""
|
||||||
Generate small table outlining Backtest results
|
Generate small table outlining Backtest results
|
||||||
:param sell_reason_stats: Exit reason metrics
|
:param exit_reason_stats: Exit reason metrics
|
||||||
:param stake_currency: Stakecurrency used
|
:param stake_currency: Stakecurrency used
|
||||||
:return: pretty printed table with tabulate as string
|
:return: pretty printed table with tabulate as string
|
||||||
"""
|
"""
|
||||||
@@ -81,7 +81,7 @@ def text_table_exit_reason(exit_reason_stats: List[Dict[str, Any]], stake_curren
|
|||||||
t.get('exit_reason', t.get('sell_reason')), t['trades'],
|
t.get('exit_reason', t.get('sell_reason')), t['trades'],
|
||||||
generate_wins_draws_losses(t['wins'], t['draws'], t['losses']),
|
generate_wins_draws_losses(t['wins'], t['draws'], t['losses']),
|
||||||
t['profit_mean_pct'], t['profit_sum_pct'],
|
t['profit_mean_pct'], t['profit_sum_pct'],
|
||||||
round_coin_value(t['profit_total_abs'], stake_currency, False),
|
fmt_coin(t['profit_total_abs'], stake_currency, False),
|
||||||
t['profit_total_pct'],
|
t['profit_total_pct'],
|
||||||
] for t in exit_reason_stats]
|
] for t in exit_reason_stats]
|
||||||
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
||||||
@@ -134,7 +134,7 @@ def text_table_periodic_breakdown(days_breakdown_stats: List[Dict[str, Any]],
|
|||||||
'Losses',
|
'Losses',
|
||||||
]
|
]
|
||||||
output = [[
|
output = [[
|
||||||
d['date'], round_coin_value(d['profit_abs'], stake_currency, False),
|
d['date'], fmt_coin(d['profit_abs'], stake_currency, False),
|
||||||
d['wins'], d['draws'], d['loses'],
|
d['wins'], d['draws'], d['loses'],
|
||||||
] for d in days_breakdown_stats]
|
] for d in days_breakdown_stats]
|
||||||
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
||||||
@@ -187,10 +187,10 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
f"{strat_results.get('trade_count_short', 0)}"),
|
f"{strat_results.get('trade_count_short', 0)}"),
|
||||||
('Total profit Long %', f"{strat_results['profit_total_long']:.2%}"),
|
('Total profit Long %', f"{strat_results['profit_total_long']:.2%}"),
|
||||||
('Total profit Short %', f"{strat_results['profit_total_short']:.2%}"),
|
('Total profit Short %', f"{strat_results['profit_total_short']:.2%}"),
|
||||||
('Absolute profit Long', round_coin_value(strat_results['profit_total_long_abs'],
|
('Absolute profit Long', fmt_coin(strat_results['profit_total_long_abs'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Absolute profit Short', round_coin_value(strat_results['profit_total_short_abs'],
|
('Absolute profit Short', fmt_coin(strat_results['profit_total_short_abs'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
] if strat_results.get('trade_count_short', 0) > 0 else []
|
] if strat_results.get('trade_count_short', 0) > 0 else []
|
||||||
|
|
||||||
drawdown_metrics = []
|
drawdown_metrics = []
|
||||||
@@ -203,12 +203,12 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
('Absolute Drawdown (Account)', f"{strat_results['max_drawdown_account']:.2%}")
|
('Absolute Drawdown (Account)', f"{strat_results['max_drawdown_account']:.2%}")
|
||||||
if 'max_drawdown_account' in strat_results else (
|
if 'max_drawdown_account' in strat_results else (
|
||||||
'Drawdown', f"{strat_results['max_drawdown']:.2%}"),
|
'Drawdown', f"{strat_results['max_drawdown']:.2%}"),
|
||||||
('Absolute Drawdown', round_coin_value(strat_results['max_drawdown_abs'],
|
('Absolute Drawdown', fmt_coin(strat_results['max_drawdown_abs'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Drawdown high', round_coin_value(strat_results['max_drawdown_high'],
|
('Drawdown high', fmt_coin(strat_results['max_drawdown_high'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Drawdown low', round_coin_value(strat_results['max_drawdown_low'],
|
('Drawdown low', fmt_coin(strat_results['max_drawdown_low'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Drawdown Start', strat_results['drawdown_start']),
|
('Drawdown Start', strat_results['drawdown_start']),
|
||||||
('Drawdown End', strat_results['drawdown_end']),
|
('Drawdown End', strat_results['drawdown_end']),
|
||||||
])
|
])
|
||||||
@@ -230,12 +230,12 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
('Total/Daily Avg Trades',
|
('Total/Daily Avg Trades',
|
||||||
f"{strat_results['total_trades']} / {strat_results['trades_per_day']}"),
|
f"{strat_results['total_trades']} / {strat_results['trades_per_day']}"),
|
||||||
|
|
||||||
('Starting balance', round_coin_value(strat_results['starting_balance'],
|
('Starting balance', fmt_coin(strat_results['starting_balance'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Final balance', round_coin_value(strat_results['final_balance'],
|
('Final balance', fmt_coin(strat_results['final_balance'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
|
('Absolute profit ', fmt_coin(strat_results['profit_total_abs'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Total profit %', f"{strat_results['profit_total']:.2%}"),
|
('Total profit %', f"{strat_results['profit_total']:.2%}"),
|
||||||
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
|
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
|
||||||
('Sortino', f"{strat_results['sortino']:.2f}" if 'sortino' in strat_results else 'N/A'),
|
('Sortino', f"{strat_results['sortino']:.2f}" if 'sortino' in strat_results else 'N/A'),
|
||||||
@@ -249,10 +249,10 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
('Trades per day', strat_results['trades_per_day']),
|
('Trades per day', strat_results['trades_per_day']),
|
||||||
('Avg. daily profit %',
|
('Avg. daily profit %',
|
||||||
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
|
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
|
||||||
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
|
('Avg. stake amount', fmt_coin(strat_results['avg_stake_amount'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Total trade volume', round_coin_value(strat_results['total_volume'],
|
('Total trade volume', fmt_coin(strat_results['total_volume'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
*short_metrics,
|
*short_metrics,
|
||||||
('', ''), # Empty line to improve readability
|
('', ''), # Empty line to improve readability
|
||||||
('Best Pair', f"{strat_results['best_pair']['key']} "
|
('Best Pair', f"{strat_results['best_pair']['key']} "
|
||||||
@@ -263,10 +263,10 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
('Worst trade', f"{worst_trade['pair']} "
|
('Worst trade', f"{worst_trade['pair']} "
|
||||||
f"{worst_trade['profit_ratio']:.2%}"),
|
f"{worst_trade['profit_ratio']:.2%}"),
|
||||||
|
|
||||||
('Best day', round_coin_value(strat_results['backtest_best_day_abs'],
|
('Best day', fmt_coin(strat_results['backtest_best_day_abs'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Worst day', round_coin_value(strat_results['backtest_worst_day_abs'],
|
('Worst day', fmt_coin(strat_results['backtest_worst_day_abs'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Days win/draw/lose', f"{strat_results['winning_days']} / "
|
('Days win/draw/lose', f"{strat_results['winning_days']} / "
|
||||||
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
|
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
|
||||||
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
|
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
|
||||||
@@ -281,10 +281,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
*entry_adjustment_metrics,
|
*entry_adjustment_metrics,
|
||||||
('', ''), # Empty line to improve readability
|
('', ''), # Empty line to improve readability
|
||||||
|
|
||||||
('Min balance', round_coin_value(strat_results['csum_min'],
|
('Min balance', fmt_coin(strat_results['csum_min'], strat_results['stake_currency'])),
|
||||||
strat_results['stake_currency'])),
|
('Max balance', fmt_coin(strat_results['csum_max'], strat_results['stake_currency'])),
|
||||||
('Max balance', round_coin_value(strat_results['csum_max'],
|
|
||||||
strat_results['stake_currency'])),
|
|
||||||
|
|
||||||
*drawdown_metrics,
|
*drawdown_metrics,
|
||||||
('Market change', f"{strat_results['market_change']:.2%}"),
|
('Market change', f"{strat_results['market_change']:.2%}"),
|
||||||
@@ -292,9 +290,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
|
|
||||||
return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl")
|
return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl")
|
||||||
else:
|
else:
|
||||||
start_balance = round_coin_value(strat_results['starting_balance'],
|
start_balance = fmt_coin(strat_results['starting_balance'], strat_results['stake_currency'])
|
||||||
strat_results['stake_currency'])
|
stake_amount = fmt_coin(
|
||||||
stake_amount = round_coin_value(
|
|
||||||
strat_results['stake_amount'], strat_results['stake_currency']
|
strat_results['stake_amount'], strat_results['stake_currency']
|
||||||
) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
|
) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
|
||||||
|
|
||||||
@@ -322,24 +319,20 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
|||||||
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
if (results.get('results_per_enter_tag') is not None
|
if (results.get('results_per_enter_tag') is not None):
|
||||||
or results.get('results_per_buy_tag') is not None):
|
table = text_table_tags("enter_tag", results['results_per_enter_tag'], stake_currency)
|
||||||
# results_per_buy_tag is deprecated and should be removed 2 versions after short golive.
|
|
||||||
table = text_table_tags(
|
|
||||||
"enter_tag",
|
|
||||||
results.get('results_per_enter_tag', results.get('results_per_buy_tag')),
|
|
||||||
stake_currency=stake_currency)
|
|
||||||
|
|
||||||
if isinstance(table, str) and len(table) > 0:
|
if isinstance(table, str) and len(table) > 0:
|
||||||
print(' ENTER TAG STATS '.center(len(table.splitlines()[0]), '='))
|
print(' ENTER TAG STATS '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
exit_reasons = results.get('exit_reason_summary', results.get('sell_reason_summary'))
|
exit_reasons = results.get('exit_reason_summary')
|
||||||
table = text_table_exit_reason(exit_reason_stats=exit_reasons,
|
if exit_reasons:
|
||||||
stake_currency=stake_currency)
|
table = text_table_exit_reason(exit_reason_stats=exit_reasons,
|
||||||
if isinstance(table, str) and len(table) > 0:
|
stake_currency=stake_currency)
|
||||||
print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '='))
|
if isinstance(table, str) and len(table) > 0:
|
||||||
print(table)
|
print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '='))
|
||||||
|
print(table)
|
||||||
|
|
||||||
for period in backtest_breakdown:
|
for period in backtest_breakdown:
|
||||||
if period in results.get('periodic_breakdown', {}):
|
if period in results.get('periodic_breakdown', {}):
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, IntO
|
|||||||
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
|
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
|
||||||
calculate_expectancy, calculate_market_change,
|
calculate_expectancy, calculate_market_change,
|
||||||
calculate_max_drawdown, calculate_sharpe, calculate_sortino)
|
calculate_max_drawdown, calculate_sharpe, calculate_sortino)
|
||||||
from freqtrade.misc import decimals_per_coin, round_coin_value
|
|
||||||
from freqtrade.types import BacktestResultType
|
from freqtrade.types import BacktestResultType
|
||||||
|
from freqtrade.util import decimals_per_coin, fmt_coin
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -203,7 +203,7 @@ def generate_strategy_comparison(bt_stats: Dict) -> List[Dict]:
|
|||||||
# Update "key" to strategy (results_per_pair has it as "Total").
|
# Update "key" to strategy (results_per_pair has it as "Total").
|
||||||
tabular_data[-1]['key'] = strategy
|
tabular_data[-1]['key'] = strategy
|
||||||
tabular_data[-1]['max_drawdown_account'] = result['max_drawdown_account']
|
tabular_data[-1]['max_drawdown_account'] = result['max_drawdown_account']
|
||||||
tabular_data[-1]['max_drawdown_abs'] = round_coin_value(
|
tabular_data[-1]['max_drawdown_abs'] = fmt_coin(
|
||||||
result['max_drawdown_abs'], result['stake_currency'], False)
|
result['max_drawdown_abs'], result['stake_currency'], False)
|
||||||
return tabular_data
|
return tabular_data
|
||||||
|
|
||||||
@@ -561,6 +561,10 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
|
|||||||
metadata[strategy] = {
|
metadata[strategy] = {
|
||||||
'run_id': content['run_id'],
|
'run_id': content['run_id'],
|
||||||
'backtest_start_time': content['backtest_start_time'],
|
'backtest_start_time': content['backtest_start_time'],
|
||||||
|
'timeframe': content['config']['timeframe'],
|
||||||
|
'timeframe_detail': content['config'].get('timeframe_detail', None),
|
||||||
|
'backtest_start_ts': int(min_date.timestamp()),
|
||||||
|
'backtest_end_ts': int(max_date.timestamp()),
|
||||||
}
|
}
|
||||||
result['strategy'][strategy] = strat_stats
|
result['strategy'][strategy] = strat_stats
|
||||||
|
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ class Order(ModelBase):
|
|||||||
|
|
||||||
return (f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, "
|
return (f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, "
|
||||||
f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, "
|
f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, "
|
||||||
f"status={self.status}, date={self.order_date:{DATETIME_PRINT_FORMAT}})")
|
f"status={self.status}, date={self.order_date_utc:{DATETIME_PRINT_FORMAT}})")
|
||||||
|
|
||||||
def update_from_ccxt_object(self, order):
|
def update_from_ccxt_object(self, order):
|
||||||
"""
|
"""
|
||||||
@@ -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:
|
||||||
@@ -542,7 +542,9 @@ class LocalTrade:
|
|||||||
f"{self.trading_mode.value} trading requires param interest_rate on trades")
|
f"{self.trading_mode.value} trading requires param interest_rate on trades")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
|
open_since = (
|
||||||
|
self.open_date_utc.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
|
f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
|
||||||
@@ -1603,7 +1605,7 @@ class Trade(ModelBase, LocalTrade):
|
|||||||
:return: unsorted query object
|
:return: unsorted query object
|
||||||
"""
|
"""
|
||||||
query = Trade.get_trades_query(trade_filter, include_orders)
|
query = Trade.get_trades_query(trade_filter, include_orders)
|
||||||
# this sholud remain split. if use_db is False, session is not available and the above will
|
# this should remain split. if use_db is False, session is not available and the above will
|
||||||
# raise an exception.
|
# raise an exception.
|
||||||
return Trade.session.scalars(query)
|
return Trade.session.scalars(query)
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
@@ -537,6 +538,10 @@ class BacktestHistoryEntry(BaseModel):
|
|||||||
run_id: str
|
run_id: str
|
||||||
backtest_start_time: int
|
backtest_start_time: int
|
||||||
notes: Optional[str] = ''
|
notes: Optional[str] = ''
|
||||||
|
backtest_start_ts: Optional[int] = None
|
||||||
|
backtest_end_ts: Optional[int] = None
|
||||||
|
timeframe: Optional[str] = None
|
||||||
|
timeframe_detail: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class BacktestMetadataUpdate(BaseModel):
|
class BacktestMetadataUpdate(BaseModel):
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ from freqtrade.exceptions import ExchangeError, PricingError
|
|||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.loggers import bufferHandler
|
from freqtrade.loggers import bufferHandler
|
||||||
from freqtrade.misc import decimals_per_coin
|
|
||||||
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade
|
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade
|
||||||
from freqtrade.persistence.models import PairLock
|
from freqtrade.persistence.models import PairLock
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||||
from freqtrade.rpc.rpc_types import RPCSendMsg
|
from freqtrade.rpc.rpc_types import RPCSendMsg
|
||||||
from freqtrade.util import dt_humanize, dt_now, dt_ts_def, format_date, shorten_date
|
from freqtrade.util import (decimals_per_coin, dt_humanize, dt_now, dt_ts_def, format_date,
|
||||||
|
shorten_date)
|
||||||
from freqtrade.wallets import PositionWallet, Wallet
|
from freqtrade.wallets import PositionWallet, Wallet
|
||||||
|
|
||||||
|
|
||||||
@@ -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:
|
||||||
|
|||||||
+19
-12
@@ -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,13 +44,14 @@ 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]
|
||||||
exchange: str
|
exchange: str
|
||||||
pair: str
|
pair: str
|
||||||
base_currency: str
|
base_currency: str
|
||||||
|
quote_currency: str
|
||||||
leverage: Optional[float]
|
leverage: Optional[float]
|
||||||
direction: str
|
direction: str
|
||||||
limit: float
|
limit: float
|
||||||
@@ -62,36 +66,36 @@ 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
|
||||||
sell_reason: Optional[str]
|
|
||||||
exit_reason: Optional[str]
|
exit_reason: Optional[str]
|
||||||
close_date: datetime
|
close_date: datetime
|
||||||
# current_rate: Optional[float]
|
# current_rate: Optional[float]
|
||||||
order_rate: Optional[float]
|
order_rate: Optional[float]
|
||||||
|
final_profit_ratio: Optional[float]
|
||||||
|
is_final_exit: bool
|
||||||
|
|
||||||
|
|
||||||
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]
|
|
||||||
exit_reason: Optional[str]
|
exit_reason: Optional[str]
|
||||||
close_date: datetime
|
close_date: datetime
|
||||||
|
|
||||||
@@ -114,15 +118,18 @@ class RPCNewCandleMsg(RPCSendMsgBase):
|
|||||||
data: PairWithTimeframe
|
data: PairWithTimeframe
|
||||||
|
|
||||||
|
|
||||||
|
RPCOrderMsg = Union[RPCEntryMsg, RPCExitMsg, RPCExitCancelMsg, RPCCancelMsg]
|
||||||
|
|
||||||
|
|
||||||
RPCSendMsg = Union[
|
RPCSendMsg = Union[
|
||||||
RPCStatusMsg,
|
RPCStatusMsg,
|
||||||
RPCStrategyMsg,
|
RPCStrategyMsg,
|
||||||
RPCProtectionMsg,
|
RPCProtectionMsg,
|
||||||
RPCWhitelistMsg,
|
RPCWhitelistMsg,
|
||||||
RPCBuyMsg,
|
RPCEntryMsg,
|
||||||
RPCCancelMsg,
|
RPCCancelMsg,
|
||||||
RPCSellMsg,
|
RPCExitMsg,
|
||||||
RPCSellCancelMsg,
|
RPCExitCancelMsg,
|
||||||
RPCAnalyzedDFMsg,
|
RPCAnalyzedDFMsg,
|
||||||
RPCNewCandleMsg
|
RPCNewCandleMsg
|
||||||
]
|
]
|
||||||
|
|||||||
+159
-132
@@ -10,12 +10,12 @@ import re
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from functools import partial
|
from functools import partial, wraps
|
||||||
from html import escape
|
from html import escape
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from math import isnan
|
from math import isnan
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import Any, Callable, Coroutine, Dict, List, Optional, Union
|
from typing import Any, Callable, Coroutine, Dict, List, Literal, Optional, Union
|
||||||
|
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton,
|
from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton,
|
||||||
@@ -29,11 +29,11 @@ from freqtrade.__init__ import __version__
|
|||||||
from freqtrade.constants import DUST_PER_COIN, Config
|
from freqtrade.constants import DUST_PER_COIN, Config
|
||||||
from freqtrade.enums import MarketDirection, RPCMessageType, SignalDirection, TradingMode
|
from freqtrade.enums import MarketDirection, RPCMessageType, SignalDirection, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import chunks, plural, round_coin_value
|
from freqtrade.misc import chunks, plural
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import RPC, RPCException, RPCHandler
|
from freqtrade.rpc import RPC, RPCException, RPCHandler
|
||||||
from freqtrade.rpc.rpc_types import RPCSendMsg
|
from freqtrade.rpc.rpc_types import RPCEntryMsg, RPCExitMsg, RPCOrderMsg, RPCSendMsg
|
||||||
from freqtrade.util import dt_humanize
|
from freqtrade.util import dt_humanize, fmt_coin, round_value
|
||||||
|
|
||||||
|
|
||||||
MAX_MESSAGE_LENGTH = MessageLimit.MAX_TEXT_LENGTH
|
MAX_MESSAGE_LENGTH = MessageLimit.MAX_TEXT_LENGTH
|
||||||
@@ -44,6 +44,23 @@ logger = logging.getLogger(__name__)
|
|||||||
logger.debug('Included module rpc.telegram ...')
|
logger.debug('Included module rpc.telegram ...')
|
||||||
|
|
||||||
|
|
||||||
|
def safe_async_db(func: Callable[..., Any]):
|
||||||
|
"""
|
||||||
|
Decorator to safely handle sessions when switching async context
|
||||||
|
:param func: function to decorate
|
||||||
|
:return: decorated function
|
||||||
|
"""
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
""" Decorator logic """
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
Trade.session.remove()
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TimeunitMappings:
|
class TimeunitMappings:
|
||||||
header: str
|
header: str
|
||||||
@@ -61,6 +78,7 @@ def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]):
|
|||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@wraps(command_handler)
|
||||||
async def wrapper(self, *args, **kwargs):
|
async def wrapper(self, *args, **kwargs):
|
||||||
""" Decorator logic """
|
""" Decorator logic """
|
||||||
update = kwargs.get('update') or args[0]
|
update = kwargs.get('update') or args[0]
|
||||||
@@ -286,7 +304,7 @@ class Telegram(RPCHandler):
|
|||||||
asyncio.run_coroutine_threadsafe(self._cleanup_telegram(), self._loop)
|
asyncio.run_coroutine_threadsafe(self._cleanup_telegram(), self._loop)
|
||||||
self._thread.join()
|
self._thread.join()
|
||||||
|
|
||||||
def _exchange_from_msg(self, msg: Dict[str, Any]) -> str:
|
def _exchange_from_msg(self, msg: RPCOrderMsg) -> str:
|
||||||
"""
|
"""
|
||||||
Extracts the exchange name from the given message.
|
Extracts the exchange name from the given message.
|
||||||
:param msg: The message to extract the exchange name from.
|
:param msg: The message to extract the exchange name from.
|
||||||
@@ -310,164 +328,172 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
|
def _format_entry_msg(self, msg: RPCEntryMsg) -> str:
|
||||||
if self._rpc._fiat_converter:
|
|
||||||
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
|
|
||||||
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
|
||||||
else:
|
|
||||||
msg['stake_amount_fiat'] = 0
|
|
||||||
is_fill = msg['type'] in [RPCMessageType.ENTRY_FILL]
|
is_fill = msg['type'] in [RPCMessageType.ENTRY_FILL]
|
||||||
emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}'
|
emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}'
|
||||||
|
|
||||||
entry_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['direction'] == 'Long'
|
terminology = {
|
||||||
else {'enter': 'Short', 'entered': 'Shorted'})
|
'1_enter': 'New Trade',
|
||||||
|
'1_entered': 'New Trade filled',
|
||||||
|
'x_enter': 'Increasing position',
|
||||||
|
'x_entered': 'Position increase filled',
|
||||||
|
}
|
||||||
|
|
||||||
|
key = f"{'x' if msg['sub_trade'] else '1'}_{'entered' if is_fill else 'enter'}"
|
||||||
|
wording = terminology[key]
|
||||||
|
|
||||||
message = (
|
message = (
|
||||||
f"{emoji} *{self._exchange_from_msg(msg)}:*"
|
f"{emoji} *{self._exchange_from_msg(msg)}:*"
|
||||||
f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}"
|
f" {wording} (#{msg['trade_id']})\n"
|
||||||
f" (#{msg['trade_id']})\n"
|
f"*Pair:* `{msg['pair']}`\n"
|
||||||
)
|
)
|
||||||
message += self._add_analyzed_candle(msg['pair'])
|
message += self._add_analyzed_candle(msg['pair'])
|
||||||
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else ""
|
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else ""
|
||||||
message += f"*Amount:* `{msg['amount']:.8f}`\n"
|
message += f"*Amount:* `{round_value(msg['amount'], 8)}`\n"
|
||||||
|
message += f"*Direction:* `{msg['direction']}"
|
||||||
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
|
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
|
||||||
message += f"*Leverage:* `{msg['leverage']}`\n"
|
message += f" ({msg['leverage']:.1g}x)"
|
||||||
|
message += "`\n"
|
||||||
|
message += f"*Open Rate:* `{fmt_coin(msg['open_rate'], msg['quote_currency'])}`\n"
|
||||||
|
if msg['type'] == RPCMessageType.ENTRY and msg['current_rate']:
|
||||||
|
message += f"*Current Rate:* `{fmt_coin(msg['current_rate'], msg['quote_currency'])}`\n"
|
||||||
|
|
||||||
if msg['type'] in [RPCMessageType.ENTRY_FILL]:
|
profit_fiat_extra = self.__format_profit_fiat(msg, 'stake_amount') # type: ignore
|
||||||
message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
|
total = fmt_coin(msg['stake_amount'], msg['quote_currency'])
|
||||||
elif msg['type'] in [RPCMessageType.ENTRY]:
|
|
||||||
message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n"\
|
|
||||||
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
|
||||||
|
|
||||||
message += f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
|
message += f"*{'New ' if msg['sub_trade'] else ''}Total:* `{total}{profit_fiat_extra}`"
|
||||||
|
|
||||||
if msg.get('fiat_currency'):
|
|
||||||
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
|
||||||
|
|
||||||
message += ")`"
|
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def _format_exit_msg(self, msg: Dict[str, Any]) -> str:
|
def _format_exit_msg(self, msg: RPCExitMsg) -> str:
|
||||||
msg['amount'] = round(msg['amount'], 8)
|
duration = msg['close_date'].replace(
|
||||||
msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2)
|
|
||||||
msg['duration'] = msg['close_date'].replace(
|
|
||||||
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
||||||
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
duration_min = duration.total_seconds() / 60
|
||||||
|
|
||||||
msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None
|
leverage_text = (f" ({msg['leverage']:.1g}x)"
|
||||||
msg['emoji'] = self._get_sell_emoji(msg)
|
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0
|
||||||
msg['leverage_text'] = (f"*Leverage:* `{msg['leverage']:.1f}`\n"
|
else "")
|
||||||
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0
|
|
||||||
else "")
|
|
||||||
|
|
||||||
# Check if all sell properties are available.
|
profit_fiat_extra = self.__format_profit_fiat(msg, 'profit_amount')
|
||||||
# This might not be the case if the message origin is triggered by /forceexit
|
|
||||||
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
|
profit_extra = (
|
||||||
and self._rpc._fiat_converter):
|
f" ({msg['gain']}: {fmt_coin(msg['profit_amount'], msg['quote_currency'])}"
|
||||||
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
|
f"{profit_fiat_extra})")
|
||||||
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
|
||||||
msg['profit_extra'] = f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}"
|
|
||||||
else:
|
|
||||||
msg['profit_extra'] = ''
|
|
||||||
msg['profit_extra'] = (
|
|
||||||
f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
|
|
||||||
f"{msg['profit_extra']})")
|
|
||||||
|
|
||||||
is_fill = msg['type'] == RPCMessageType.EXIT_FILL
|
is_fill = msg['type'] == RPCMessageType.EXIT_FILL
|
||||||
is_sub_trade = msg.get('sub_trade')
|
is_sub_trade = msg.get('sub_trade')
|
||||||
is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit')
|
is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit')
|
||||||
profit_prefix = ('Sub ' if is_sub_profit else 'Cumulative ') if is_sub_trade else ''
|
is_final_exit = msg.get('is_final_exit', False) and is_sub_profit
|
||||||
|
profit_prefix = 'Sub ' if is_sub_trade else ''
|
||||||
cp_extra = ''
|
cp_extra = ''
|
||||||
exit_wording = 'Exited' if is_fill else 'Exiting'
|
exit_wording = 'Exited' if is_fill else 'Exiting'
|
||||||
if is_sub_profit and is_sub_trade:
|
if is_sub_trade or is_final_exit:
|
||||||
if self._rpc._fiat_converter:
|
cp_fiat = self.__format_profit_fiat(msg, 'cumulative_profit')
|
||||||
cp_fiat = self._rpc._fiat_converter.convert_amount(
|
|
||||||
msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency'])
|
|
||||||
cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}"
|
|
||||||
exit_wording = f"Partially {exit_wording.lower()}"
|
|
||||||
cp_extra = (
|
|
||||||
f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} "
|
|
||||||
f"{msg['stake_currency']}{cp_extra}`)\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
if is_final_exit:
|
||||||
|
profit_prefix = 'Sub '
|
||||||
|
cp_extra = (
|
||||||
|
f"*Final Profit:* `{msg['final_profit_ratio']:.2%} "
|
||||||
|
f"({msg['cumulative_profit']:.8f} {msg['quote_currency']}{cp_fiat})`\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
exit_wording = f"Partially {exit_wording.lower()}"
|
||||||
|
if msg['cumulative_profit']:
|
||||||
|
cp_extra = (
|
||||||
|
f"*Cumulative Profit:* `"
|
||||||
|
f"{fmt_coin(msg['cumulative_profit'], msg['stake_currency'])}{cp_fiat}`\n"
|
||||||
|
)
|
||||||
|
enter_tag = f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else ""
|
||||||
message = (
|
message = (
|
||||||
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
|
f"{self._get_exit_emoji(msg)} *{self._exchange_from_msg(msg)}:* "
|
||||||
f"{exit_wording} {msg['pair']} (#{msg['trade_id']})\n"
|
f"{exit_wording} {msg['pair']} (#{msg['trade_id']})\n"
|
||||||
f"{self._add_analyzed_candle(msg['pair'])}"
|
f"{self._add_analyzed_candle(msg['pair'])}"
|
||||||
f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* "
|
f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* "
|
||||||
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
f"`{msg['profit_ratio']:.2%}{profit_extra}`\n"
|
||||||
f"{cp_extra}"
|
f"{cp_extra}"
|
||||||
f"*Enter Tag:* `{msg['enter_tag']}`\n"
|
f"{enter_tag}"
|
||||||
f"*Exit Reason:* `{msg['exit_reason']}`\n"
|
f"*Exit Reason:* `{msg['exit_reason']}`\n"
|
||||||
f"*Direction:* `{msg['direction']}`\n"
|
f"*Direction:* `{msg['direction']}"
|
||||||
f"{msg['leverage_text']}"
|
f"{leverage_text}`\n"
|
||||||
f"*Amount:* `{msg['amount']:.8f}`\n"
|
f"*Amount:* `{round_value(msg['amount'], 8)}`\n"
|
||||||
f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
|
f"*Open Rate:* `{fmt_coin(msg['open_rate'], msg['quote_currency'])}`\n"
|
||||||
)
|
)
|
||||||
if msg['type'] == RPCMessageType.EXIT:
|
if msg['type'] == RPCMessageType.EXIT and msg['current_rate']:
|
||||||
message += f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
message += f"*Current Rate:* `{fmt_coin(msg['current_rate'], msg['quote_currency'])}`\n"
|
||||||
if msg['order_rate']:
|
if msg['order_rate']:
|
||||||
message += f"*Exit Rate:* `{msg['order_rate']:.8f}`"
|
message += f"*Exit Rate:* `{fmt_coin(msg['order_rate'], msg['quote_currency'])}`"
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
||||||
message += f"*Exit Rate:* `{msg['close_rate']:.8f}`"
|
message += f"*Exit Rate:* `{fmt_coin(msg['close_rate'], msg['quote_currency'])}`"
|
||||||
|
|
||||||
if is_sub_trade:
|
if is_sub_trade:
|
||||||
if self._rpc._fiat_converter:
|
stake_amount_fiat = self.__format_profit_fiat(msg, 'stake_amount')
|
||||||
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
|
|
||||||
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
|
||||||
else:
|
|
||||||
msg['stake_amount_fiat'] = 0
|
|
||||||
rem = round_coin_value(msg['stake_amount'], msg['stake_currency'])
|
|
||||||
message += f"\n*Remaining:* `({rem}"
|
|
||||||
|
|
||||||
if msg.get('fiat_currency', None):
|
rem = fmt_coin(msg['stake_amount'], msg['quote_currency'])
|
||||||
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
message += f"\n*Remaining:* `{rem}{stake_amount_fiat}`"
|
||||||
|
|
||||||
message += ")`"
|
|
||||||
else:
|
else:
|
||||||
message += f"\n*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`"
|
message += f"\n*Duration:* `{duration} ({duration_min:.1f} min)`"
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> Optional[str]:
|
def __format_profit_fiat(
|
||||||
if msg_type in [RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]:
|
self,
|
||||||
|
msg: RPCExitMsg,
|
||||||
|
key: Literal['stake_amount', 'profit_amount', 'cumulative_profit']
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Format Fiat currency to append to regular profit output
|
||||||
|
"""
|
||||||
|
profit_fiat_extra = ''
|
||||||
|
if self._rpc._fiat_converter and (fiat_currency := msg.get('fiat_currency')):
|
||||||
|
profit_fiat = self._rpc._fiat_converter.convert_amount(
|
||||||
|
msg[key], msg['stake_currency'], fiat_currency)
|
||||||
|
profit_fiat_extra = f" / {profit_fiat:.3f} {fiat_currency}"
|
||||||
|
return profit_fiat_extra
|
||||||
|
|
||||||
|
def compose_message(self, msg: RPCSendMsg) -> Optional[str]:
|
||||||
|
if msg['type'] == RPCMessageType.ENTRY or msg['type'] == RPCMessageType.ENTRY_FILL:
|
||||||
message = self._format_entry_msg(msg)
|
message = self._format_entry_msg(msg)
|
||||||
|
|
||||||
elif msg_type in [RPCMessageType.EXIT, RPCMessageType.EXIT_FILL]:
|
elif msg['type'] == RPCMessageType.EXIT or msg['type'] == RPCMessageType.EXIT_FILL:
|
||||||
message = self._format_exit_msg(msg)
|
message = self._format_exit_msg(msg)
|
||||||
|
|
||||||
elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL):
|
elif (
|
||||||
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit'
|
msg['type'] == RPCMessageType.ENTRY_CANCEL
|
||||||
|
or msg['type'] == RPCMessageType.EXIT_CANCEL
|
||||||
|
):
|
||||||
|
message_side = 'enter' if msg['type'] == RPCMessageType.ENTRY_CANCEL else 'exit'
|
||||||
message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* "
|
message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* "
|
||||||
f"Cancelling {'partial ' if msg.get('sub_trade') else ''}"
|
f"Cancelling {'partial ' if msg.get('sub_trade') else ''}"
|
||||||
f"{msg['message_side']} Order for {msg['pair']} "
|
f"{message_side} Order for {msg['pair']} "
|
||||||
f"(#{msg['trade_id']}). Reason: {msg['reason']}.")
|
f"(#{msg['trade_id']}). Reason: {msg['reason']}.")
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
|
elif msg['type'] == RPCMessageType.PROTECTION_TRIGGER:
|
||||||
message = (
|
message = (
|
||||||
f"*Protection* triggered due to {msg['reason']}. "
|
f"*Protection* triggered due to {msg['reason']}. "
|
||||||
f"`{msg['pair']}` will be locked until `{msg['lock_end_time']}`."
|
f"`{msg['pair']}` will be locked until `{msg['lock_end_time']}`."
|
||||||
)
|
)
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
|
elif msg['type'] == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
|
||||||
message = (
|
message = (
|
||||||
f"*Protection* triggered due to {msg['reason']}. "
|
f"*Protection* triggered due to {msg['reason']}. "
|
||||||
f"*All pairs* will be locked until `{msg['lock_end_time']}`."
|
f"*All pairs* will be locked until `{msg['lock_end_time']}`."
|
||||||
)
|
)
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.STATUS:
|
elif msg['type'] == RPCMessageType.STATUS:
|
||||||
message = f"*Status:* `{msg['status']}`"
|
message = f"*Status:* `{msg['status']}`"
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.WARNING:
|
elif msg['type'] == RPCMessageType.WARNING:
|
||||||
message = f"\N{WARNING SIGN} *Warning:* `{msg['status']}`"
|
message = f"\N{WARNING SIGN} *Warning:* `{msg['status']}`"
|
||||||
elif msg_type == RPCMessageType.EXCEPTION:
|
elif msg['type'] == RPCMessageType.EXCEPTION:
|
||||||
# Errors will contain exceptions, which are wrapped in tripple ticks.
|
# Errors will contain exceptions, which are wrapped in tripple ticks.
|
||||||
message = f"\N{WARNING SIGN} *ERROR:* \n {msg['status']}"
|
message = f"\N{WARNING SIGN} *ERROR:* \n {msg['status']}"
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.STARTUP:
|
elif msg['type'] == RPCMessageType.STARTUP:
|
||||||
message = f"{msg['status']}"
|
message = f"{msg['status']}"
|
||||||
elif msg_type == RPCMessageType.STRATEGY_MSG:
|
elif msg['type'] == RPCMessageType.STRATEGY_MSG:
|
||||||
message = f"{msg['msg']}"
|
message = f"{msg['msg']}"
|
||||||
else:
|
else:
|
||||||
logger.debug("Unknown message type: %s", msg_type)
|
logger.debug("Unknown message type: %s", msg['type'])
|
||||||
return None
|
return None
|
||||||
return message
|
return message
|
||||||
|
|
||||||
@@ -495,20 +521,20 @@ class Telegram(RPCHandler):
|
|||||||
# Notification disabled
|
# Notification disabled
|
||||||
return
|
return
|
||||||
|
|
||||||
message = self.compose_message(deepcopy(msg), msg_type) # type: ignore
|
message = self.compose_message(deepcopy(msg))
|
||||||
if message:
|
if message:
|
||||||
asyncio.run_coroutine_threadsafe(
|
asyncio.run_coroutine_threadsafe(
|
||||||
self._send_msg(message, disable_notification=(noti == 'silent')),
|
self._send_msg(message, disable_notification=(noti == 'silent')),
|
||||||
self._loop)
|
self._loop)
|
||||||
|
|
||||||
def _get_sell_emoji(self, msg):
|
def _get_exit_emoji(self, msg):
|
||||||
"""
|
"""
|
||||||
Get emoji for sell-side
|
Get emoji for exit-messages
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if float(msg['profit_percent']) >= 5.0:
|
if float(msg['profit_ratio']) >= 0.05:
|
||||||
return "\N{ROCKET}"
|
return "\N{ROCKET}"
|
||||||
elif float(msg['profit_percent']) >= 0.0:
|
elif float(msg['profit_ratio']) >= 0.0:
|
||||||
return "\N{EIGHT SPOKED ASTERISK}"
|
return "\N{EIGHT SPOKED ASTERISK}"
|
||||||
elif msg['exit_reason'] == "stop_loss":
|
elif msg['exit_reason'] == "stop_loss":
|
||||||
return "\N{WARNING SIGN}"
|
return "\N{WARNING SIGN}"
|
||||||
@@ -537,7 +563,7 @@ class Telegram(RPCHandler):
|
|||||||
if order_nr == 1:
|
if order_nr == 1:
|
||||||
lines.append(
|
lines.append(
|
||||||
f"*Amount:* {cur_entry_amount:.8g} "
|
f"*Amount:* {cur_entry_amount:.8g} "
|
||||||
f"({round_coin_value(order['cost'], quote_currency)})"
|
f"({fmt_coin(order['cost'], quote_currency)})"
|
||||||
)
|
)
|
||||||
lines.append(f"*Average Price:* {cur_entry_average:.8g}")
|
lines.append(f"*Average Price:* {cur_entry_average:.8g}")
|
||||||
else:
|
else:
|
||||||
@@ -547,7 +573,7 @@ class Telegram(RPCHandler):
|
|||||||
lines.append("({})".format(dt_humanize(order["order_filled_date"],
|
lines.append("({})".format(dt_humanize(order["order_filled_date"],
|
||||||
granularity=["day", "hour", "minute"])))
|
granularity=["day", "hour", "minute"])))
|
||||||
lines.append(f"*Amount:* {cur_entry_amount:.8g} "
|
lines.append(f"*Amount:* {cur_entry_amount:.8g} "
|
||||||
f"({round_coin_value(order['cost'], quote_currency)})")
|
f"({fmt_coin(order['cost'], quote_currency)})")
|
||||||
lines.append(f"*Average {wording} Price:* {cur_entry_average:.8g} "
|
lines.append(f"*Average {wording} Price:* {cur_entry_average:.8g} "
|
||||||
f"({price_to_1st_entry:.2%} from 1st entry rate)")
|
f"({price_to_1st_entry:.2%} from 1st entry rate)")
|
||||||
lines.append(f"*Order Filled:* {order['order_filled_date']}")
|
lines.append(f"*Order Filled:* {order['order_filled_date']}")
|
||||||
@@ -633,12 +659,12 @@ class Telegram(RPCHandler):
|
|||||||
r['num_exits'] = len([o for o in r['orders'] if not o['ft_is_entry']
|
r['num_exits'] = len([o for o in r['orders'] if not o['ft_is_entry']
|
||||||
and not o['ft_order_side'] == 'stoploss'])
|
and not o['ft_order_side'] == 'stoploss'])
|
||||||
r['exit_reason'] = r.get('exit_reason', "")
|
r['exit_reason'] = r.get('exit_reason', "")
|
||||||
r['stake_amount_r'] = round_coin_value(r['stake_amount'], r['quote_currency'])
|
r['stake_amount_r'] = fmt_coin(r['stake_amount'], r['quote_currency'])
|
||||||
r['max_stake_amount_r'] = round_coin_value(
|
r['max_stake_amount_r'] = fmt_coin(
|
||||||
r['max_stake_amount'] or r['stake_amount'], r['quote_currency'])
|
r['max_stake_amount'] or r['stake_amount'], r['quote_currency'])
|
||||||
r['profit_abs_r'] = round_coin_value(r['profit_abs'], r['quote_currency'])
|
r['profit_abs_r'] = fmt_coin(r['profit_abs'], r['quote_currency'])
|
||||||
r['realized_profit_r'] = round_coin_value(r['realized_profit'], r['quote_currency'])
|
r['realized_profit_r'] = fmt_coin(r['realized_profit'], r['quote_currency'])
|
||||||
r['total_profit_abs_r'] = round_coin_value(
|
r['total_profit_abs_r'] = fmt_coin(
|
||||||
r['total_profit_abs'], r['quote_currency'])
|
r['total_profit_abs'], r['quote_currency'])
|
||||||
lines = [
|
lines = [
|
||||||
"*Trade ID:* `{trade_id}`" +
|
"*Trade ID:* `{trade_id}`" +
|
||||||
@@ -781,7 +807,7 @@ class Telegram(RPCHandler):
|
|||||||
)
|
)
|
||||||
stats_tab = tabulate(
|
stats_tab = tabulate(
|
||||||
[[f"{period['date']:{val.dateformat}} ({period['trade_count']})",
|
[[f"{period['date']:{val.dateformat}} ({period['trade_count']})",
|
||||||
f"{round_coin_value(period['abs_profit'], stats['stake_currency'])}",
|
f"{fmt_coin(period['abs_profit'], stats['stake_currency'])}",
|
||||||
f"{period['fiat_value']:.2f} {stats['fiat_display_currency']}",
|
f"{period['fiat_value']:.2f} {stats['fiat_display_currency']}",
|
||||||
f"{period['rel_profit']:.2%}",
|
f"{period['rel_profit']:.2%}",
|
||||||
] for period in stats['data']],
|
] for period in stats['data']],
|
||||||
@@ -883,19 +909,19 @@ class Telegram(RPCHandler):
|
|||||||
# Message to display
|
# Message to display
|
||||||
if stats['closed_trade_count'] > 0:
|
if stats['closed_trade_count'] > 0:
|
||||||
markdown_msg = ("*ROI:* Closed trades\n"
|
markdown_msg = ("*ROI:* Closed trades\n"
|
||||||
f"∙ `{round_coin_value(profit_closed_coin, stake_cur)} "
|
f"∙ `{fmt_coin(profit_closed_coin, stake_cur)} "
|
||||||
f"({profit_closed_ratio_mean:.2%}) "
|
f"({profit_closed_ratio_mean:.2%}) "
|
||||||
f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
||||||
f"∙ `{round_coin_value(profit_closed_fiat, fiat_disp_cur)}`\n")
|
f"∙ `{fmt_coin(profit_closed_fiat, fiat_disp_cur)}`\n")
|
||||||
else:
|
else:
|
||||||
markdown_msg = "`No closed trade` \n"
|
markdown_msg = "`No closed trade` \n"
|
||||||
|
|
||||||
markdown_msg += (
|
markdown_msg += (
|
||||||
f"*ROI:* All trades\n"
|
f"*ROI:* All trades\n"
|
||||||
f"∙ `{round_coin_value(profit_all_coin, stake_cur)} "
|
f"∙ `{fmt_coin(profit_all_coin, stake_cur)} "
|
||||||
f"({profit_all_ratio_mean:.2%}) "
|
f"({profit_all_ratio_mean:.2%}) "
|
||||||
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
||||||
f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n"
|
f"∙ `{fmt_coin(profit_all_fiat, fiat_disp_cur)}`\n"
|
||||||
f"*Total Trade Count:* `{trade_count}`\n"
|
f"*Total Trade Count:* `{trade_count}`\n"
|
||||||
f"*Bot started:* `{stats['bot_start_date']}`\n"
|
f"*Bot started:* `{stats['bot_start_date']}`\n"
|
||||||
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
|
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
|
||||||
@@ -909,14 +935,14 @@ class Telegram(RPCHandler):
|
|||||||
markdown_msg += (
|
markdown_msg += (
|
||||||
f"\n*Avg. Duration:* `{avg_duration}`\n"
|
f"\n*Avg. Duration:* `{avg_duration}`\n"
|
||||||
f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`\n"
|
f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`\n"
|
||||||
f"*Trading volume:* `{round_coin_value(stats['trading_volume'], stake_cur)}`\n"
|
f"*Trading volume:* `{fmt_coin(stats['trading_volume'], stake_cur)}`\n"
|
||||||
f"*Profit factor:* `{stats['profit_factor']:.2f}`\n"
|
f"*Profit factor:* `{stats['profit_factor']:.2f}`\n"
|
||||||
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
|
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
|
||||||
f"({round_coin_value(stats['max_drawdown_abs'], stake_cur)})`\n"
|
f"({fmt_coin(stats['max_drawdown_abs'], stake_cur)})`\n"
|
||||||
f" from `{stats['max_drawdown_start']} "
|
f" from `{stats['max_drawdown_start']} "
|
||||||
f"({round_coin_value(stats['drawdown_high'], stake_cur)})`\n"
|
f"({fmt_coin(stats['drawdown_high'], stake_cur)})`\n"
|
||||||
f" to `{stats['max_drawdown_end']} "
|
f" to `{stats['max_drawdown_end']} "
|
||||||
f"({round_coin_value(stats['drawdown_low'], stake_cur)})`\n"
|
f"({fmt_coin(stats['drawdown_low'], stake_cur)})`\n"
|
||||||
)
|
)
|
||||||
await self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
|
await self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
@@ -984,9 +1010,9 @@ class Telegram(RPCHandler):
|
|||||||
output = ''
|
output = ''
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
output += "*Warning:* Simulated balances in Dry Mode.\n"
|
output += "*Warning:* Simulated balances in Dry Mode.\n"
|
||||||
starting_cap = round_coin_value(result['starting_capital'], self._config['stake_currency'])
|
starting_cap = fmt_coin(result['starting_capital'], self._config['stake_currency'])
|
||||||
output += f"Starting capital: `{starting_cap}`"
|
output += f"Starting capital: `{starting_cap}`"
|
||||||
starting_cap_fiat = round_coin_value(
|
starting_cap_fiat = fmt_coin(
|
||||||
result['starting_capital_fiat'], self._config['fiat_display_currency']
|
result['starting_capital_fiat'], self._config['fiat_display_currency']
|
||||||
) if result['starting_capital_fiat'] > 0 else ''
|
) if result['starting_capital_fiat'] > 0 else ''
|
||||||
output += (f" `, {starting_cap_fiat}`.\n"
|
output += (f" `, {starting_cap_fiat}`.\n"
|
||||||
@@ -1006,9 +1032,9 @@ class Telegram(RPCHandler):
|
|||||||
f"\t`{curr['side']}: {curr['position']:.8f}`\n"
|
f"\t`{curr['side']}: {curr['position']:.8f}`\n"
|
||||||
f"\t`Leverage: {curr['leverage']:.1f}`\n"
|
f"\t`Leverage: {curr['leverage']:.1f}`\n"
|
||||||
f"\t`Est. {curr['stake']}: "
|
f"\t`Est. {curr['stake']}: "
|
||||||
f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
|
f"{fmt_coin(curr['est_stake'], curr['stake'], False)}`\n")
|
||||||
else:
|
else:
|
||||||
est_stake = round_coin_value(
|
est_stake = fmt_coin(
|
||||||
curr['est_stake' if full_result else 'est_stake_bot'], curr['stake'], False)
|
curr['est_stake' if full_result else 'est_stake_bot'], curr['stake'], False)
|
||||||
|
|
||||||
curr_output = (
|
curr_output = (
|
||||||
@@ -1036,13 +1062,13 @@ class Telegram(RPCHandler):
|
|||||||
f"{plural(total_dust_currencies, 'Currency', 'Currencies')} "
|
f"{plural(total_dust_currencies, 'Currency', 'Currencies')} "
|
||||||
f"(< {balance_dust_level} {result['stake']}):*\n"
|
f"(< {balance_dust_level} {result['stake']}):*\n"
|
||||||
f"\t`Est. {result['stake']}: "
|
f"\t`Est. {result['stake']}: "
|
||||||
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
|
f"{fmt_coin(total_dust_balance, result['stake'], False)}`\n")
|
||||||
tc = result['trade_count'] > 0
|
tc = result['trade_count'] > 0
|
||||||
stake_improve = f" `({result['starting_capital_ratio']:.2%})`" if tc else ''
|
stake_improve = f" `({result['starting_capital_ratio']:.2%})`" if tc else ''
|
||||||
fiat_val = f" `({result['starting_capital_fiat_ratio']:.2%})`" if tc else ''
|
fiat_val = f" `({result['starting_capital_fiat_ratio']:.2%})`" if tc else ''
|
||||||
value = round_coin_value(
|
value = fmt_coin(
|
||||||
result['value' if full_result else 'value_bot'], result['symbol'], False)
|
result['value' if full_result else 'value_bot'], result['symbol'], False)
|
||||||
total_stake = round_coin_value(
|
total_stake = fmt_coin(
|
||||||
result['total' if full_result else 'total_bot'], result['stake'], False)
|
result['total' if full_result else 'total_bot'], result['stake'], False)
|
||||||
output += (
|
output += (
|
||||||
f"\n*Estimated Value{' (Bot managed assets only)' if not full_result else ''}*:\n"
|
f"\n*Estimated Value{' (Bot managed assets only)' if not full_result else ''}*:\n"
|
||||||
@@ -1150,7 +1176,7 @@ class Telegram(RPCHandler):
|
|||||||
try:
|
try:
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
# Workaround to avoid nested loops
|
# Workaround to avoid nested loops
|
||||||
await loop.run_in_executor(None, self._rpc._rpc_force_exit, trade_id)
|
await loop.run_in_executor(None, safe_async_db(self._rpc._rpc_force_exit), trade_id)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
await self._send_msg(str(e))
|
await self._send_msg(str(e))
|
||||||
|
|
||||||
@@ -1176,6 +1202,7 @@ class Telegram(RPCHandler):
|
|||||||
async def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
|
async def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
|
||||||
if pair != 'cancel':
|
if pair != 'cancel':
|
||||||
try:
|
try:
|
||||||
|
@safe_async_db
|
||||||
def _force_enter():
|
def _force_enter():
|
||||||
self._rpc._rpc_force_entry(pair, price, order_side=order_side)
|
self._rpc._rpc_force_entry(pair, price, order_side=order_side)
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
@@ -1320,7 +1347,7 @@ class Telegram(RPCHandler):
|
|||||||
for i, trade in enumerate(trades):
|
for i, trade in enumerate(trades):
|
||||||
stat_line = (
|
stat_line = (
|
||||||
f"{i+1}.\t <code>{trade['pair']}\t"
|
f"{i+1}.\t <code>{trade['pair']}\t"
|
||||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} "
|
||||||
f"({trade['profit_ratio']:.2%}) "
|
f"({trade['profit_ratio']:.2%}) "
|
||||||
f"({trade['count']})</code>\n")
|
f"({trade['count']})</code>\n")
|
||||||
|
|
||||||
@@ -1352,7 +1379,7 @@ class Telegram(RPCHandler):
|
|||||||
for i, trade in enumerate(trades):
|
for i, trade in enumerate(trades):
|
||||||
stat_line = (
|
stat_line = (
|
||||||
f"{i+1}.\t <code>{trade['enter_tag']}\t"
|
f"{i+1}.\t <code>{trade['enter_tag']}\t"
|
||||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} "
|
||||||
f"({trade['profit_ratio']:.2%}) "
|
f"({trade['profit_ratio']:.2%}) "
|
||||||
f"({trade['count']})</code>\n")
|
f"({trade['count']})</code>\n")
|
||||||
|
|
||||||
@@ -1384,7 +1411,7 @@ class Telegram(RPCHandler):
|
|||||||
for i, trade in enumerate(trades):
|
for i, trade in enumerate(trades):
|
||||||
stat_line = (
|
stat_line = (
|
||||||
f"{i+1}.\t <code>{trade['exit_reason']}\t"
|
f"{i+1}.\t <code>{trade['exit_reason']}\t"
|
||||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} "
|
||||||
f"({trade['profit_ratio']:.2%}) "
|
f"({trade['profit_ratio']:.2%}) "
|
||||||
f"({trade['count']})</code>\n")
|
f"({trade['count']})</code>\n")
|
||||||
|
|
||||||
@@ -1416,7 +1443,7 @@ class Telegram(RPCHandler):
|
|||||||
for i, trade in enumerate(trades):
|
for i, trade in enumerate(trades):
|
||||||
stat_line = (
|
stat_line = (
|
||||||
f"{i+1}.\t <code>{trade['mix_tag']}\t"
|
f"{i+1}.\t <code>{trade['mix_tag']}\t"
|
||||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} "
|
||||||
f"({trade['profit_ratio']:.2%}) "
|
f"({trade['profit_ratio']:.2%}) "
|
||||||
f"({trade['count']})</code>\n")
|
f"({trade['count']})</code>\n")
|
||||||
|
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class FreqaiExampleHybridStrategy(IStrategy):
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"purge_old_models": 2,
|
"purge_old_models": 2,
|
||||||
"train_period_days": 15,
|
"train_period_days": 15,
|
||||||
"identifier": "uniqe-id",
|
"identifier": "unique-id",
|
||||||
"feature_parameters": {
|
"feature_parameters": {
|
||||||
"include_timeframes": [
|
"include_timeframes": [
|
||||||
"3m",
|
"3m",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
@@ -26,3 +26,7 @@ class BacktestHistoryEntryType(BacktestMetadataType):
|
|||||||
filename: str
|
filename: str
|
||||||
strategy: str
|
strategy: str
|
||||||
notes: str
|
notes: str
|
||||||
|
backtest_start_ts: Optional[int]
|
||||||
|
backtest_end_ts: Optional[int]
|
||||||
|
timeframe: Optional[str]
|
||||||
|
timeframe_detail: Optional[str]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from freqtrade.util.datetime_helpers import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts,
|
from freqtrade.util.datetime_helpers import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts,
|
||||||
dt_ts_def, dt_utc, format_date, format_ms_time,
|
dt_ts_def, dt_utc, format_date, format_ms_time,
|
||||||
shorten_date)
|
shorten_date)
|
||||||
|
from freqtrade.util.formatters import decimals_per_coin, fmt_coin, round_value
|
||||||
from freqtrade.util.ft_precise import FtPrecise
|
from freqtrade.util.ft_precise import FtPrecise
|
||||||
from freqtrade.util.periodic_cache import PeriodicCache
|
from freqtrade.util.periodic_cache import PeriodicCache
|
||||||
from freqtrade.util.template_renderer import render_template, render_template_with_fallback # noqa
|
from freqtrade.util.template_renderer import render_template, render_template_with_fallback # noqa
|
||||||
@@ -19,4 +20,7 @@ __all__ = [
|
|||||||
'FtPrecise',
|
'FtPrecise',
|
||||||
'PeriodicCache',
|
'PeriodicCache',
|
||||||
'shorten_date',
|
'shorten_date',
|
||||||
|
'decimals_per_coin',
|
||||||
|
'round_value',
|
||||||
|
'fmt_coin',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN
|
||||||
|
|
||||||
|
|
||||||
|
def decimals_per_coin(coin: str):
|
||||||
|
"""
|
||||||
|
Helper method getting decimal amount for this coin
|
||||||
|
example usage: f".{decimals_per_coin('USD')}f"
|
||||||
|
:param coin: Which coin are we printing the price / value for
|
||||||
|
"""
|
||||||
|
return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK)
|
||||||
|
|
||||||
|
|
||||||
|
def round_value(value: float, decimals: int, keep_trailing_zeros=False) -> str:
|
||||||
|
"""
|
||||||
|
Round value to given decimals
|
||||||
|
:param value: Value to be rounded
|
||||||
|
:param decimals: Number of decimals to round to
|
||||||
|
:param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2"
|
||||||
|
:return: Rounded value as string
|
||||||
|
"""
|
||||||
|
val = f"{value:.{decimals}f}"
|
||||||
|
if not keep_trailing_zeros:
|
||||||
|
val = val.rstrip('0').rstrip('.')
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_coin(
|
||||||
|
value: float, coin: str, show_coin_name=True, keep_trailing_zeros=False) -> str:
|
||||||
|
"""
|
||||||
|
Format price value for this coin
|
||||||
|
:param value: Value to be printed
|
||||||
|
:param coin: Which coin are we printing the price / value for
|
||||||
|
:param show_coin_name: Return string in format: "222.22 USDT" or "222.22"
|
||||||
|
:param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2"
|
||||||
|
:return: Formatted / rounded value (with or without coin name)
|
||||||
|
"""
|
||||||
|
val = f"{value:.{decimals_per_coin(coin)}f}"
|
||||||
|
val = round_value(value, decimals_per_coin(coin), keep_trailing_zeros)
|
||||||
|
if show_coin_name:
|
||||||
|
val = f"{val} {coin}"
|
||||||
|
|
||||||
|
return val
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.util.migrations.binance_mig import migrate_binance_futures_names # noqa F401
|
||||||
|
from freqtrade.util.migrations.binance_mig import migrate_binance_futures_data
|
||||||
|
from freqtrade.util.migrations.funding_rate_mig import migrate_funding_fee_timeframe
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_data(config, exchange: Optional[Exchange] = None):
|
||||||
|
migrate_binance_futures_data(config)
|
||||||
|
|
||||||
|
migrate_funding_fee_timeframe(config, exchange)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from freqtrade.constants import Config
|
||||||
|
from freqtrade.data.history.idatahandler import get_datahandler
|
||||||
|
from freqtrade.enums import TradingMode
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_funding_fee_timeframe(config: Config, exchange: Optional[Exchange]):
|
||||||
|
if (
|
||||||
|
config.get('trading_mode', TradingMode.SPOT) != TradingMode.FUTURES
|
||||||
|
):
|
||||||
|
# only act on futures
|
||||||
|
return
|
||||||
|
|
||||||
|
if not exchange:
|
||||||
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
|
|
||||||
|
ff_timeframe = exchange.get_option('funding_fee_timeframe')
|
||||||
|
|
||||||
|
dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv'])
|
||||||
|
dhc.fix_funding_fee_timeframe(ff_timeframe)
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]
|
||||||
|
|||||||
+10
-9
@@ -7,24 +7,25 @@
|
|||||||
-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.11
|
||||||
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.4
|
||||||
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.14.0
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.3.0.7
|
types-cachetools==5.3.0.7
|
||||||
types-filelock==3.2.7
|
types-filelock==3.2.7
|
||||||
types-requests==2.31.0.10
|
types-requests==2.31.0.20240106
|
||||||
types-tabulate==0.9.0.3
|
types-tabulate==0.9.0.20240106
|
||||||
types-python-dateutil==2.8.19.14
|
types-python-dateutil==2.8.19.20240106
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+12
-12
@@ -1,12 +1,12 @@
|
|||||||
numpy==1.26.2
|
numpy==1.26.3
|
||||||
pandas==2.1.3
|
pandas==2.1.4
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==4.1.66
|
ccxt==4.2.9
|
||||||
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.108.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
|
||||||
@@ -58,5 +58,5 @@ schedule==1.2.1
|
|||||||
websockets==12.0
|
websockets==12.0
|
||||||
janus==1.0.0
|
janus==1.0.0
|
||||||
|
|
||||||
ast-comments==1.2.0
|
ast-comments==1.2.1
|
||||||
packaging==23.2
|
packaging==23.2
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
+30
-3
@@ -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,
|
||||||
@@ -527,6 +553,7 @@ def get_default_conf(testdatadir):
|
|||||||
"internals": {},
|
"internals": {},
|
||||||
"export": "none",
|
"export": "none",
|
||||||
"dataformat_ohlcv": "feather",
|
"dataformat_ohlcv": "feather",
|
||||||
|
"runmode": "dry_run",
|
||||||
"candle_type_def": CandleType.SPOT,
|
"candle_type_def": CandleType.SPOT,
|
||||||
}
|
}
|
||||||
return configuration
|
return configuration
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -500,3 +500,62 @@ 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') == 0
|
||||||
|
assert dp.get_required_startup('1h') == 0
|
||||||
|
assert dp.get_required_startup('1d') == 0
|
||||||
|
|
||||||
|
dp._config['startup_candle_count'] = 20
|
||||||
|
assert dp.get_required_startup('5m') == 20
|
||||||
|
assert dp.get_required_startup('1h') == 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') == 5780
|
||||||
|
assert dp.get_required_startup('1h') == 500
|
||||||
|
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') == 5780
|
||||||
|
assert dp.get_required_startup('1h') == 500
|
||||||
|
assert dp.get_required_startup('1d') == 40
|
||||||
|
|
||||||
|
dp._config['freqai']['feature_parameters']['indicator_periods_candles'][1] = 50
|
||||||
|
assert dp.get_required_startup('5m') == 5810
|
||||||
|
assert dp.get_required_startup('1h') == 530
|
||||||
|
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') == 51880
|
||||||
|
assert dp.get_required_startup('1h') == 4360
|
||||||
|
assert dp.get_required_startup('1d') == 220
|
||||||
|
|||||||
@@ -508,8 +508,9 @@ def test_refresh_backtest_ohlcv_data(
|
|||||||
|
|
||||||
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||||
mocker.patch.object(Path, "unlink", MagicMock())
|
mocker.patch.object(Path, "unlink", MagicMock())
|
||||||
|
default_conf['trading_mode'] = trademode
|
||||||
|
|
||||||
ex = get_patched_exchange(mocker, default_conf)
|
ex = get_patched_exchange(mocker, default_conf, id='bybit')
|
||||||
timerange = TimeRange.parse_timerange("20190101-20190102")
|
timerange = TimeRange.parse_timerange("20190101-20190102")
|
||||||
refresh_backtest_ohlcv_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC"],
|
refresh_backtest_ohlcv_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC"],
|
||||||
timeframes=["1m", "5m"], datadir=testdatadir,
|
timeframes=["1m", "5m"], datadir=testdatadir,
|
||||||
@@ -521,6 +522,9 @@ def test_refresh_backtest_ohlcv_data(
|
|||||||
assert dl_mock.call_args[1]['timerange'].starttype == 'date'
|
assert dl_mock.call_args[1]['timerange'].starttype == 'date'
|
||||||
|
|
||||||
assert log_has_re(r"Downloading pair ETH/BTC, .* interval 1m\.", caplog)
|
assert log_has_re(r"Downloading pair ETH/BTC, .* interval 1m\.", caplog)
|
||||||
|
if trademode == 'futures':
|
||||||
|
assert log_has_re(r"Downloading pair ETH/BTC, funding_rate, interval 8h\.", caplog)
|
||||||
|
assert log_has_re(r"Downloading pair ETH/BTC, mark, interval 4h\.", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
|
def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -55,7 +55,7 @@ get_entry_rate_data = [
|
|||||||
('bid', 6, 5, None, 0, 5), # last not available - uses bid
|
('bid', 6, 5, None, 0, 5), # last not available - uses bid
|
||||||
]
|
]
|
||||||
|
|
||||||
get_sell_rate_data = [
|
get_exit_rate_data = [
|
||||||
('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side
|
('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side
|
||||||
('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side
|
('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side
|
||||||
('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat
|
('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat
|
||||||
@@ -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):
|
||||||
@@ -2482,8 +2512,10 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name):
|
|||||||
|
|
||||||
@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", get_entry_rate_data)
|
@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", get_entry_rate_data)
|
||||||
def test_get_entry_rate(mocker, default_conf, caplog, side, ask, bid,
|
def test_get_entry_rate(mocker, default_conf, caplog, side, ask, bid,
|
||||||
last, last_ab, expected) -> None:
|
last, last_ab, expected, time_machine) -> None:
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
|
start_dt = datetime(2023, 12, 1, 0, 10, 0, tzinfo=timezone.utc)
|
||||||
|
time_machine.move_to(start_dt, tick=False)
|
||||||
if last_ab is None:
|
if last_ab is None:
|
||||||
del default_conf['entry_pricing']['price_last_balance']
|
del default_conf['entry_pricing']['price_last_balance']
|
||||||
else:
|
else:
|
||||||
@@ -2491,39 +2523,65 @@ def test_get_entry_rate(mocker, default_conf, caplog, side, ask, bid,
|
|||||||
default_conf['entry_pricing']['price_side'] = side
|
default_conf['entry_pricing']['price_side'] = side
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': ask, 'last': last, 'bid': bid})
|
mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': ask, 'last': last, 'bid': bid})
|
||||||
|
log_msg = "Using cached entry rate for ETH/BTC."
|
||||||
|
|
||||||
assert exchange.get_rate('ETH/BTC', side="entry", is_short=False, refresh=True) == expected
|
assert exchange.get_rate('ETH/BTC', side="entry", is_short=False, refresh=True) == expected
|
||||||
assert not log_has("Using cached entry rate for ETH/BTC.", caplog)
|
assert not log_has(log_msg, caplog)
|
||||||
|
|
||||||
|
time_machine.move_to(start_dt + timedelta(minutes=4), tick=False)
|
||||||
|
# Running a 2nd time without Refresh!
|
||||||
|
caplog.clear()
|
||||||
assert exchange.get_rate('ETH/BTC', side="entry", is_short=False, refresh=False) == expected
|
assert exchange.get_rate('ETH/BTC', side="entry", is_short=False, refresh=False) == expected
|
||||||
assert log_has("Using cached entry rate for ETH/BTC.", caplog)
|
assert log_has(log_msg, caplog)
|
||||||
|
|
||||||
|
time_machine.move_to(start_dt + timedelta(minutes=6), tick=False)
|
||||||
|
# Running a 2nd time - forces refresh due to ttl timeout
|
||||||
|
caplog.clear()
|
||||||
|
assert exchange.get_rate('ETH/BTC', side="entry", is_short=False, refresh=False) == expected
|
||||||
|
assert not log_has(log_msg, caplog)
|
||||||
|
|
||||||
# Running a 2nd time with Refresh on!
|
# Running a 2nd time with Refresh on!
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
assert exchange.get_rate('ETH/BTC', side="entry", is_short=False, refresh=True) == expected
|
assert exchange.get_rate('ETH/BTC', side="entry", is_short=False, refresh=True) == expected
|
||||||
assert not log_has("Using cached entry rate for ETH/BTC.", caplog)
|
assert not log_has(log_msg, caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', get_sell_rate_data)
|
@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', get_exit_rate_data)
|
||||||
def test_get_exit_rate(default_conf, mocker, caplog, side, bid, ask,
|
def test_get_exit_rate(default_conf, mocker, caplog, side, bid, ask,
|
||||||
last, last_ab, expected) -> None:
|
last, last_ab, expected, time_machine) -> None:
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
|
start_dt = datetime(2023, 12, 1, 0, 10, 0, tzinfo=timezone.utc)
|
||||||
|
time_machine.move_to(start_dt, tick=False)
|
||||||
|
|
||||||
default_conf['exit_pricing']['price_side'] = side
|
default_conf['exit_pricing']['price_side'] = side
|
||||||
if last_ab is not None:
|
if last_ab is not None:
|
||||||
default_conf['exit_pricing']['price_last_balance'] = last_ab
|
default_conf['exit_pricing']['price_last_balance'] = last_ab
|
||||||
mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': ask, 'bid': bid, 'last': last})
|
mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': ask, 'bid': bid, 'last': last})
|
||||||
pair = "ETH/BTC"
|
pair = "ETH/BTC"
|
||||||
|
log_msg = "Using cached exit rate for ETH/BTC."
|
||||||
|
|
||||||
# Test regular mode
|
# Test regular mode
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
rate = exchange.get_rate(pair, side="exit", is_short=False, refresh=True)
|
rate = exchange.get_rate(pair, side="exit", is_short=False, refresh=True)
|
||||||
assert not log_has("Using cached exit rate for ETH/BTC.", caplog)
|
assert not log_has(log_msg, caplog)
|
||||||
assert isinstance(rate, float)
|
assert isinstance(rate, float)
|
||||||
assert rate == expected
|
assert rate == expected
|
||||||
# Use caching
|
# Use caching
|
||||||
rate = exchange.get_rate(pair, side="exit", is_short=False, refresh=False)
|
caplog.clear()
|
||||||
assert rate == expected
|
assert exchange.get_rate(pair, side="exit", is_short=False, refresh=False) == expected
|
||||||
assert log_has("Using cached exit rate for ETH/BTC.", caplog)
|
assert log_has(log_msg, caplog)
|
||||||
|
|
||||||
|
time_machine.move_to(start_dt + timedelta(minutes=4), tick=False)
|
||||||
|
# Caching still active - TTL didn't expire
|
||||||
|
caplog.clear()
|
||||||
|
assert exchange.get_rate(pair, side="exit", is_short=False, refresh=False) == expected
|
||||||
|
assert log_has(log_msg, caplog)
|
||||||
|
|
||||||
|
time_machine.move_to(start_dt + timedelta(minutes=6), tick=False)
|
||||||
|
# Caching expired - refresh forced
|
||||||
|
caplog.clear()
|
||||||
|
assert exchange.get_rate(pair, side="exit", is_short=False, refresh=False) == expected
|
||||||
|
assert not log_has(log_msg, caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("entry,is_short,side,ask,bid,last,last_ab,expected", [
|
@pytest.mark.parametrize("entry,is_short,side,ask,bid,last,last_ab,expected", [
|
||||||
@@ -2619,9 +2677,9 @@ def test_get_exit_rate_exception(default_conf, mocker, is_short):
|
|||||||
@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", get_entry_rate_data)
|
@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", get_entry_rate_data)
|
||||||
@pytest.mark.parametrize("side2", ['bid', 'ask'])
|
@pytest.mark.parametrize("side2", ['bid', 'ask'])
|
||||||
@pytest.mark.parametrize("use_order_book", [True, False])
|
@pytest.mark.parametrize("use_order_book", [True, False])
|
||||||
def test_get_rates_testing_buy(mocker, default_conf, caplog, side, ask, bid,
|
def test_get_rates_testing_entry(mocker, default_conf, caplog, side, ask, bid,
|
||||||
last, last_ab, expected,
|
last, last_ab, expected,
|
||||||
side2, use_order_book, order_book_l2) -> None:
|
side2, use_order_book, order_book_l2) -> None:
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
if last_ab is None:
|
if last_ab is None:
|
||||||
del default_conf['entry_pricing']['price_last_balance']
|
del default_conf['entry_pricing']['price_last_balance']
|
||||||
@@ -2655,10 +2713,10 @@ def test_get_rates_testing_buy(mocker, default_conf, caplog, side, ask, bid,
|
|||||||
assert api_mock.fetch_ticker.call_count == 1
|
assert api_mock.fetch_ticker.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', get_sell_rate_data)
|
@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', get_exit_rate_data)
|
||||||
@pytest.mark.parametrize("side2", ['bid', 'ask'])
|
@pytest.mark.parametrize("side2", ['bid', 'ask'])
|
||||||
@pytest.mark.parametrize("use_order_book", [True, False])
|
@pytest.mark.parametrize("use_order_book", [True, False])
|
||||||
def test_get_rates_testing_sell(default_conf, mocker, caplog, side, bid, ask,
|
def test_get_rates_testing_exit(default_conf, mocker, caplog, side, bid, ask,
|
||||||
last, last_ab, expected,
|
last, last_ab, expected,
|
||||||
side2, use_order_book, order_book_l2) -> None:
|
side2, use_order_book, order_book_l2) -> None:
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
@@ -2738,7 +2796,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],
|
||||||
@@ -3165,7 +3222,7 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
|
|||||||
mocker.patch(f'{mock_prefix}.fetch_stoploss_order', side_effect=exc)
|
mocker.patch(f'{mock_prefix}.fetch_stoploss_order', side_effect=exc)
|
||||||
co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
|
co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
|
||||||
assert co['amount'] == 555
|
assert co['amount'] == 555
|
||||||
assert co == {'fee': {}, 'status': 'canceled', 'amount': 555, 'info': {}}
|
assert co == {'id': '_', 'fee': {}, 'status': 'canceled', 'amount': 555, 'info': {}}
|
||||||
|
|
||||||
with pytest.raises(InvalidOrderException):
|
with pytest.raises(InvalidOrderException):
|
||||||
exc = InvalidOrderException("Did not find order")
|
exc = InvalidOrderException("Did not find order")
|
||||||
@@ -3382,7 +3439,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 +3635,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 +3930,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 +4551,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 +4976,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)
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ STOPLOSS_ORDERTYPE = 'stop-loss'
|
|||||||
STOPLOSS_LIMIT_ORDERTYPE = 'stop-loss-limit'
|
STOPLOSS_LIMIT_ORDERTYPE = 'stop-loss-limit'
|
||||||
|
|
||||||
|
|
||||||
def test_buy_kraken_trading_agreement(default_conf, mocker):
|
@pytest.mark.parametrize("order_type,time_in_force,expected_params", [
|
||||||
|
('limit', 'ioc', {'timeInForce': 'IOC', 'trading_agreement': 'agree'}),
|
||||||
|
('limit', 'PO', {'postOnly': True, 'trading_agreement': 'agree'}),
|
||||||
|
('market', None, {'trading_agreement': 'agree'})
|
||||||
|
])
|
||||||
|
def test_kraken_trading_agreement(default_conf, mocker, order_type, time_in_force, expected_params):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
order_id = f'test_prod_{order_type}_{randint(0, 10 ** 6)}'
|
||||||
order_type = 'limit'
|
|
||||||
time_in_force = 'ioc'
|
|
||||||
api_mock.options = {}
|
api_mock.options = {}
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
@@ -49,41 +52,9 @@ def test_buy_kraken_trading_agreement(default_conf, mocker):
|
|||||||
assert api_mock.create_order.call_args[0][1] == order_type
|
assert api_mock.create_order.call_args[0][1] == order_type
|
||||||
assert api_mock.create_order.call_args[0][2] == 'buy'
|
assert api_mock.create_order.call_args[0][2] == 'buy'
|
||||||
assert api_mock.create_order.call_args[0][3] == 1
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
assert api_mock.create_order.call_args[0][4] == 200
|
assert api_mock.create_order.call_args[0][4] == (200 if order_type == 'limit' else None)
|
||||||
assert api_mock.create_order.call_args[0][5] == {'timeInForce': 'IOC',
|
|
||||||
'trading_agreement': 'agree'}
|
|
||||||
|
|
||||||
|
assert api_mock.create_order.call_args[0][5] == expected_params
|
||||||
def test_sell_kraken_trading_agreement(default_conf, mocker):
|
|
||||||
api_mock = MagicMock()
|
|
||||||
order_id = f'test_prod_sell_{randint(0, 10 ** 6)}'
|
|
||||||
order_type = 'market'
|
|
||||||
api_mock.options = {}
|
|
||||||
api_mock.create_order = MagicMock(return_value={
|
|
||||||
'id': order_id,
|
|
||||||
'symbol': 'ETH/BTC',
|
|
||||||
'info': {
|
|
||||||
'foo': 'bar'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
default_conf['dry_run'] = False
|
|
||||||
|
|
||||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
|
||||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
|
||||||
|
|
||||||
order = exchange.create_order(pair='ETH/BTC', ordertype=order_type,
|
|
||||||
side="sell", amount=1, rate=200, leverage=1.0)
|
|
||||||
|
|
||||||
assert 'id' in order
|
|
||||||
assert 'info' in order
|
|
||||||
assert order['id'] == order_id
|
|
||||||
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
|
||||||
assert api_mock.create_order.call_args[0][1] == order_type
|
|
||||||
assert api_mock.create_order.call_args[0][2] == 'sell'
|
|
||||||
assert api_mock.create_order.call_args[0][3] == 1
|
|
||||||
assert api_mock.create_order.call_args[0][4] is None
|
|
||||||
assert api_mock.create_order.call_args[0][5] == {'trading_agreement': 'agree'}
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_balances_prod(default_conf, mocker):
|
def test_get_balances_prod(default_conf, mocker):
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -39,7 +54,7 @@ def freqai_conf(default_conf, tmp_path):
|
|||||||
"backtest_period_days": 10,
|
"backtest_period_days": 10,
|
||||||
"live_retrain_hours": 0,
|
"live_retrain_hours": 0,
|
||||||
"expiration_hours": 1,
|
"expiration_hours": 1,
|
||||||
"identifier": "uniqe-id100",
|
"identifier": "unique-id100",
|
||||||
"live_trained_timestamp": 0,
|
"live_trained_timestamp": 0,
|
||||||
"data_kitchen_thread_count": 2,
|
"data_kitchen_thread_count": 2,
|
||||||
"activate_tensorboard": False,
|
"activate_tensorboard": False,
|
||||||
|
|||||||
@@ -6,11 +6,17 @@ from unittest.mock import PropertyMock
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.commands.optimize_commands import setup_optimize_configuration
|
from freqtrade.commands.optimize_commands import setup_optimize_configuration
|
||||||
|
from freqtrade.configuration.timerange import TimeRange
|
||||||
|
from freqtrade.data import history
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
|
from freqtrade.enums.candletype import CandleType
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has_re, patch_exchange,
|
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, get_patched_exchange, log_has_re,
|
||||||
patched_configuration_load_config_file)
|
patch_exchange, patched_configuration_load_config_file)
|
||||||
|
from tests.freqai.conftest import get_patched_freqai_strategy
|
||||||
|
|
||||||
|
|
||||||
def test_freqai_backtest_start_backtest_list(freqai_conf, mocker, testdatadir, caplog):
|
def test_freqai_backtest_start_backtest_list(freqai_conf, mocker, testdatadir, caplog):
|
||||||
@@ -40,7 +46,16 @@ def test_freqai_backtest_start_backtest_list(freqai_conf, mocker, testdatadir, c
|
|||||||
Backtesting.cleanup()
|
Backtesting.cleanup()
|
||||||
|
|
||||||
|
|
||||||
def test_freqai_backtest_load_data(freqai_conf, mocker, caplog):
|
@pytest.mark.parametrize(
|
||||||
|
"timeframe, expected_startup_candle_count",
|
||||||
|
[
|
||||||
|
("5m", 876),
|
||||||
|
("15m", 492),
|
||||||
|
("1d", 302),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_freqai_backtest_load_data(freqai_conf, mocker, caplog,
|
||||||
|
timeframe, expected_startup_candle_count):
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
@@ -48,10 +63,14 @@ def test_freqai_backtest_load_data(freqai_conf, mocker, caplog):
|
|||||||
PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT']))
|
PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT']))
|
||||||
mocker.patch('freqtrade.optimize.backtesting.history.load_data')
|
mocker.patch('freqtrade.optimize.backtesting.history.load_data')
|
||||||
mocker.patch('freqtrade.optimize.backtesting.history.get_timerange', return_value=(now, now))
|
mocker.patch('freqtrade.optimize.backtesting.history.get_timerange', return_value=(now, now))
|
||||||
|
freqai_conf['timeframe'] = timeframe
|
||||||
|
freqai_conf.get('freqai', {}).get('feature_parameters', {}).update({'include_timeframes': []})
|
||||||
backtesting = Backtesting(deepcopy(freqai_conf))
|
backtesting = Backtesting(deepcopy(freqai_conf))
|
||||||
backtesting.load_bt_data()
|
backtesting.load_bt_data()
|
||||||
|
|
||||||
assert log_has_re('Increasing startup_candle_count for freqai to.*', caplog)
|
assert log_has_re(f'Increasing startup_candle_count for freqai on {timeframe} '
|
||||||
|
f'to {expected_startup_candle_count}', caplog)
|
||||||
|
assert history.load_data.call_args[1]['startup_candles'] == expected_startup_candle_count
|
||||||
|
|
||||||
Backtesting.cleanup()
|
Backtesting.cleanup()
|
||||||
|
|
||||||
@@ -85,3 +104,35 @@ def test_freqai_backtest_live_models_model_not_found(freqai_conf, mocker, testda
|
|||||||
Backtesting(bt_config)
|
Backtesting(bt_config)
|
||||||
|
|
||||||
Backtesting.cleanup()
|
Backtesting.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def test_freqai_backtest_consistent_timerange(mocker, freqai_conf):
|
||||||
|
freqai_conf['runmode'] = 'backtest'
|
||||||
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||||
|
PropertyMock(return_value=['XRP/USDT:USDT']))
|
||||||
|
|
||||||
|
gbs = mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats')
|
||||||
|
|
||||||
|
freqai_conf['candle_type_def'] = CandleType.FUTURES
|
||||||
|
freqai_conf.get('exchange', {}).update({'pair_whitelist': ['XRP/USDT:USDT']})
|
||||||
|
freqai_conf.get('freqai', {}).get('feature_parameters', {}).update(
|
||||||
|
{'include_timeframes': ['5m', '1h'], 'include_corr_pairlist': []})
|
||||||
|
freqai_conf['timerange'] = '20211120-20211121'
|
||||||
|
|
||||||
|
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||||
|
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||||
|
|
||||||
|
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||||
|
strategy.freqai_info = freqai_conf.get("freqai", {})
|
||||||
|
freqai = strategy.freqai
|
||||||
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
|
||||||
|
timerange = TimeRange.parse_timerange("20211115-20211122")
|
||||||
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
|
|
||||||
|
backtesting = Backtesting(deepcopy(freqai_conf))
|
||||||
|
backtesting.start()
|
||||||
|
|
||||||
|
gbs.call_args[1]['min_date'] == datetime(2021, 11, 20, 0, 0, tzinfo=timezone.utc)
|
||||||
|
gbs.call_args[1]['max_date'] == datetime(2021, 11, 21, 0, 0, tzinfo=timezone.utc)
|
||||||
|
Backtesting.cleanup()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from tests.freqai.conftest import get_patched_freqai_strategy
|
|||||||
|
|
||||||
|
|
||||||
def test_update_historic_data(mocker, freqai_conf):
|
def test_update_historic_data(mocker, freqai_conf):
|
||||||
|
freqai_conf['runmode'] = 'backtest'
|
||||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from datetime import datetime, timedelta, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
@@ -10,9 +11,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(
|
||||||
@@ -136,3 +136,64 @@ def test_get_full_model_path(mocker, freqai_conf, model):
|
|||||||
|
|
||||||
model_path = freqai.dk.get_full_models_path(freqai_conf)
|
model_path = freqai.dk.get_full_models_path(freqai_conf)
|
||||||
assert model_path.is_dir() is True
|
assert model_path.is_dir() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_pair_data_for_features_with_prealoaded_data(mocker, freqai_conf):
|
||||||
|
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||||
|
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||||
|
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||||
|
strategy.freqai_info = freqai_conf.get("freqai", {})
|
||||||
|
freqai = strategy.freqai
|
||||||
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||||
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
|
|
||||||
|
_, base_df = freqai.dd.get_base_and_corr_dataframes(timerange, "LTC/BTC", freqai.dk)
|
||||||
|
df = freqai.dk.get_pair_data_for_features("LTC/BTC", "5m", strategy, base_dataframes=base_df)
|
||||||
|
|
||||||
|
assert df is base_df["5m"]
|
||||||
|
assert not df.empty
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_pair_data_for_features_without_preloaded_data(mocker, freqai_conf):
|
||||||
|
freqai_conf.update({"timerange": "20180115-20180130"})
|
||||||
|
freqai_conf['runmode'] = 'backtest'
|
||||||
|
|
||||||
|
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||||
|
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||||
|
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||||
|
strategy.freqai_info = freqai_conf.get("freqai", {})
|
||||||
|
freqai = strategy.freqai
|
||||||
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||||
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
|
|
||||||
|
base_df = {'5m': pd.DataFrame()}
|
||||||
|
df = freqai.dk.get_pair_data_for_features("LTC/BTC", "5m", strategy, base_dataframes=base_df)
|
||||||
|
|
||||||
|
assert df is not base_df["5m"]
|
||||||
|
assert not df.empty
|
||||||
|
assert df.iloc[0]['date'].strftime("%Y-%m-%d %H:%M:%S") == "2018-01-11 23:00:00"
|
||||||
|
assert df.iloc[-1]['date'].strftime("%Y-%m-%d %H:%M:%S") == "2018-01-30 00:00:00"
|
||||||
|
|
||||||
|
|
||||||
|
def test_populate_features(mocker, freqai_conf):
|
||||||
|
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||||
|
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||||
|
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||||
|
strategy.freqai_info = freqai_conf.get("freqai", {})
|
||||||
|
freqai = strategy.freqai
|
||||||
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
timerange = TimeRange.parse_timerange("20180115-20180130")
|
||||||
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
|
|
||||||
|
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(timerange, "LTC/BTC", freqai.dk)
|
||||||
|
mocker.patch.object(strategy, 'feature_engineering_expand_all', return_value=base_df["5m"])
|
||||||
|
df = freqai.dk.populate_features(base_df["5m"], "LTC/BTC", strategy,
|
||||||
|
base_dataframes=base_df, corr_dataframes=corr_df)
|
||||||
|
|
||||||
|
strategy.feature_engineering_expand_all.assert_called_once()
|
||||||
|
pd.testing.assert_frame_equal(base_df["5m"],
|
||||||
|
strategy.feature_engineering_expand_all.call_args[0][0])
|
||||||
|
|
||||||
|
assert df.iloc[0]['date'].strftime("%Y-%m-%d %H:%M:%S") == "2018-01-15 00:00:00"
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ from tests.freqai.conftest import (get_patched_freqai_strategy, is_mac, make_rl_
|
|||||||
mock_pytorch_mlp_model_training_parameters)
|
mock_pytorch_mlp_model_training_parameters)
|
||||||
|
|
||||||
|
|
||||||
def is_py11() -> bool:
|
def is_py12() -> bool:
|
||||||
return sys.version_info >= (3, 11)
|
return sys.version_info >= (3, 12)
|
||||||
|
|
||||||
|
|
||||||
def is_arm() -> bool:
|
def is_arm() -> bool:
|
||||||
@@ -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):
|
||||||
@@ -298,8 +299,11 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog)
|
|||||||
|
|
||||||
def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
|
def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
|
||||||
freqai_conf.update({"timerange": "20180120-20180124"})
|
freqai_conf.update({"timerange": "20180120-20180124"})
|
||||||
freqai_conf.get("freqai", {}).update({"backtest_period_days": 0.5})
|
freqai_conf['runmode'] = 'backtest'
|
||||||
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
|
freqai_conf.get("freqai", {}).update({
|
||||||
|
"backtest_period_days": 0.5,
|
||||||
|
"save_backtest_models": True,
|
||||||
|
})
|
||||||
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
|
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
|
||||||
{"indicator_periods_candles": [2]})
|
{"indicator_periods_candles": [2]})
|
||||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||||
@@ -326,6 +330,7 @@ def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
|
|||||||
|
|
||||||
def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
||||||
freqai_conf.update({"timerange": "20180120-20180130"})
|
freqai_conf.update({"timerange": "20180120-20180130"})
|
||||||
|
freqai_conf['runmode'] = 'backtest'
|
||||||
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
|
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
|
||||||
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
|
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
|
||||||
{"indicator_periods_candles": [2]})
|
{"indicator_periods_candles": [2]})
|
||||||
@@ -389,6 +394,7 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
|||||||
|
|
||||||
|
|
||||||
def test_backtesting_fit_live_predictions(mocker, freqai_conf, caplog):
|
def test_backtesting_fit_live_predictions(mocker, freqai_conf, caplog):
|
||||||
|
freqai_conf['runmode'] = 'backtest'
|
||||||
freqai_conf.get("freqai", {}).update({"fit_live_predictions_candles": 10})
|
freqai_conf.get("freqai", {}).update({"fit_live_predictions_candles": 10})
|
||||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||||
@@ -522,8 +528,8 @@ def test_get_state_info(mocker, freqai_conf, dp_exists, caplog, tickers):
|
|||||||
|
|
||||||
if is_mac():
|
if is_mac():
|
||||||
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
|
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
|
||||||
if is_py11():
|
if is_py12():
|
||||||
pytest.skip("Reinforcement learning currently not available on python 3.11.")
|
pytest.skip("Reinforcement learning currently not available on python 3.12.")
|
||||||
|
|
||||||
freqai_conf.update({"freqaimodel": "ReinforcementLearner"})
|
freqai_conf.update({"freqaimodel": "ReinforcementLearner"})
|
||||||
freqai_conf.update({"timerange": "20180110-20180130"})
|
freqai_conf.update({"timerange": "20180110-20180130"})
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -1110,6 +1132,7 @@ def test_processed(default_conf, mocker, testdatadir) -> None:
|
|||||||
def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
default_conf['use_exit_signal'] = False
|
default_conf['use_exit_signal'] = False
|
||||||
default_conf['max_open_trades'] = 10
|
default_conf['max_open_trades'] = 10
|
||||||
|
default_conf['runmode'] = 'backtest'
|
||||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=100000)
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=100000)
|
||||||
@@ -1276,6 +1299,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
|
|||||||
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_fee', fee)
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||||
default_conf['max_open_trades'] = 10
|
default_conf['max_open_trades'] = 10
|
||||||
|
default_conf['runmode'] = 'backtest'
|
||||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
|
||||||
pair='UNITTEST/BTC', datadir=testdatadir)
|
pair='UNITTEST/BTC', datadir=testdatadir)
|
||||||
default_conf['timeframe'] = '1m'
|
default_conf['timeframe'] = '1m'
|
||||||
@@ -1320,6 +1344,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
|||||||
dataframe['exit_short'] = 0
|
dataframe['exit_short'] = 0
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
default_conf['runmode'] = 'backtest'
|
||||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
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_fee', fee)
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user