mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 00:23:07 +00:00
Merge branch 'develop' into feat/plot_annotations
This commit is contained in:
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -12,7 +12,7 @@ Have you searched for similar issues before posting it?
|
||||
If you have discovered a bug in the bot, please [search the issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue).
|
||||
If it hasn't been reported, please create a new issue.
|
||||
|
||||
Please do not use bug reports to request new features.
|
||||
Please do not use the bug report template to request new features.
|
||||
-->
|
||||
|
||||
## Describe your environment
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/question.md
vendored
7
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -8,9 +8,12 @@ assignees: ''
|
||||
---
|
||||
<!--
|
||||
Have you searched for similar issues before posting it?
|
||||
Did you have a VERY good look at the [documentation](https://www.freqtrade.io/en/latest/) and are sure that the question is not explained there
|
||||
Did you have a VERY good look at the [documentation](https://www.freqtrade.io/) and are sure that the question is not explained there
|
||||
|
||||
Please do not use the question template to report bugs or to request new features.
|
||||
|
||||
Has your strategy or configuration been generated by an AI model, and is now not working?
|
||||
Please consult the documentation. We'll close such issues and point to the documentation.
|
||||
-->
|
||||
|
||||
## Describe your environment
|
||||
@@ -22,4 +25,4 @@ Please do not use the question template to report bugs or to request new feature
|
||||
|
||||
## Your question
|
||||
|
||||
*Ask the question you have not been able to find an answer in the [Documentation](https://www.freqtrade.io/en/latest/)*
|
||||
*Ask the question you have not been able to find an answer in the [Documentation](https://www.freqtrade.io/)*
|
||||
|
||||
@@ -16,6 +16,8 @@ jobs:
|
||||
name: develop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -32,7 +34,7 @@ jobs:
|
||||
run: python build_helpers/binance_update_lev_tiers.py
|
||||
|
||||
|
||||
- uses: peter-evans/create-pull-request@v7
|
||||
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||
add-paths: freqtrade/exchange/binance_leverage_tiers.json
|
||||
@@ -42,6 +44,7 @@ jobs:
|
||||
branch: update/binance-leverage-tiers
|
||||
title: Update Binance Leverage Tiers
|
||||
commit-message: "chore: update pre-commit hooks"
|
||||
committer: Freqtrade Bot <noreply@github.com>
|
||||
committer: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
||||
author: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
||||
body: Update binance leverage tiers.
|
||||
delete-branch: true
|
||||
|
||||
76
.github/workflows/ci.yml
vendored
76
.github/workflows/ci.yml
vendored
@@ -24,11 +24,13 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ "ubuntu-20.04", "ubuntu-22.04", "ubuntu-24.04" ]
|
||||
os: [ "ubuntu-22.04", "ubuntu-24.04" ]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -36,8 +38,9 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache-dependency-glob: "requirements**.txt"
|
||||
@@ -71,17 +74,17 @@ jobs:
|
||||
python build_helpers/freqtrade_client_version_align.py
|
||||
|
||||
- name: Tests
|
||||
if: (!(runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-22.04'))
|
||||
if: (!(runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04'))
|
||||
run: |
|
||||
pytest --random-order
|
||||
|
||||
- name: Tests with Coveralls
|
||||
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-22.04')
|
||||
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
|
||||
run: |
|
||||
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
|
||||
|
||||
- name: Coveralls
|
||||
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-22.04')
|
||||
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
|
||||
env:
|
||||
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||
@@ -137,11 +140,12 @@ jobs:
|
||||
ruff format --check
|
||||
|
||||
- name: Mypy
|
||||
if: matrix.os == 'ubuntu-24.04'
|
||||
run: |
|
||||
mypy freqtrade scripts tests
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@v1
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
severity: error
|
||||
@@ -157,6 +161,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -165,8 +171,9 @@ jobs:
|
||||
check-latest: true
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache-dependency-glob: "requirements**.txt"
|
||||
@@ -260,11 +267,12 @@ jobs:
|
||||
ruff format --check
|
||||
|
||||
- name: Mypy
|
||||
if: matrix.os == 'macos-15'
|
||||
run: |
|
||||
mypy freqtrade scripts
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@v1
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
severity: info
|
||||
@@ -281,6 +289,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -288,8 +298,9 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache-dependency-glob: "requirements**.txt"
|
||||
@@ -355,7 +366,7 @@ jobs:
|
||||
shell: powershell
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@v1
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
severity: error
|
||||
@@ -366,6 +377,8 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -381,16 +394,20 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
|
||||
|
||||
docs-check:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Documentation syntax
|
||||
run: |
|
||||
@@ -407,7 +424,7 @@ jobs:
|
||||
mkdocs build
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@v1
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
severity: error
|
||||
@@ -417,9 +434,11 @@ jobs:
|
||||
|
||||
build-linux-online:
|
||||
# Run pytest with "live" checks
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -427,8 +446,9 @@ jobs:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
cache-dependency-glob: "requirements**.txt"
|
||||
@@ -485,14 +505,14 @@ jobs:
|
||||
|
||||
- name: Check user permission
|
||||
id: check
|
||||
uses: scherermichael-oss/action-has-permission@1.0.6
|
||||
uses: scherermichael-oss/action-has-permission@136e061bfe093832d87f090dd768e14e27a740d3 # 1.0.6
|
||||
with:
|
||||
required-permission: write
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@v1
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
if: always() && steps.check.outputs.has-permission && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
severity: info
|
||||
@@ -506,6 +526,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -551,6 +573,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download artifact 📦
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -560,7 +584,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Publish to PyPI (Test)
|
||||
uses: pypa/gh-action-pypi-publish@v1.12.4
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
|
||||
@@ -578,6 +602,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download artifact 📦
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -587,7 +613,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.12.4
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
|
||||
|
||||
deploy-docker:
|
||||
@@ -598,6 +624,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -626,14 +654,16 @@ jobs:
|
||||
docker version -f '{{.Server.Experimental}}'
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 #v3.10.0
|
||||
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
run: echo ${PLATFORMS}
|
||||
env:
|
||||
PLATFORMS: ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
- name: Build and test and push docker images
|
||||
env:
|
||||
@@ -652,6 +682,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Extract branch name
|
||||
id: extract-branch
|
||||
@@ -675,7 +707,7 @@ jobs:
|
||||
build_helpers/publish_docker_arm64.sh
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@v1
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) && (github.event_name != 'schedule')
|
||||
with:
|
||||
severity: info
|
||||
|
||||
10
.github/workflows/deploy-docs.yml
vendored
10
.github/workflows/deploy-docs.yml
vendored
@@ -20,6 +20,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -43,12 +45,16 @@ jobs:
|
||||
- name: Build and push Mike
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: |
|
||||
mike deploy ${{ github.ref_name }} latest --push --update-aliases
|
||||
mike deploy ${REF_NAME} latest --push --update-aliases
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
|
||||
- name: Build and push Mike - Release
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
run: |
|
||||
mike deploy ${{ github.ref_name }} stable --push --update-aliases
|
||||
mike deploy ${REF_NAME} stable --push --update-aliases
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
|
||||
- name: Show mike versions
|
||||
run: |
|
||||
|
||||
39
.github/workflows/devcontainer-build.yml
vendored
39
.github/workflows/devcontainer-build.yml
vendored
@@ -17,29 +17,26 @@ concurrency:
|
||||
group: "${{ github.workflow }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
permissions:
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Pre-build dev container image
|
||||
uses: devcontainers/ci@v0.3
|
||||
with:
|
||||
subFolder: .github
|
||||
imageName: ghcr.io/${{ github.repository }}-devcontainer
|
||||
cacheFrom: ghcr.io/${{ github.repository }}-devcontainer
|
||||
push: always
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Pre-build dev container image
|
||||
uses: devcontainers/ci@8bf61b26e9c3a98f69cb6ce2f88d24ff59b785c6 # v0.3.19
|
||||
with:
|
||||
subFolder: .github
|
||||
imageName: ghcr.io/${{ github.repository }}-devcontainer
|
||||
cacheFrom: ghcr.io/${{ github.repository }}-devcontainer
|
||||
push: always
|
||||
|
||||
7
.github/workflows/docker-update-readme.yml
vendored
7
.github/workflows/docker-update-readme.yml
vendored
@@ -4,14 +4,19 @@ on:
|
||||
branches:
|
||||
- stable
|
||||
|
||||
# disable permissions for all of the available permissions
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
dockerHubDescription:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
7
.github/workflows/pre-commit-update.yml
vendored
7
.github/workflows/pre-commit-update.yml
vendored
@@ -14,6 +14,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -26,7 +28,7 @@ jobs:
|
||||
- name: Run auto-update
|
||||
run: pre-commit autoupdate
|
||||
|
||||
- uses: peter-evans/create-pull-request@v7
|
||||
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||
add-paths: .pre-commit-config.yaml
|
||||
@@ -36,6 +38,7 @@ jobs:
|
||||
branch: update/pre-commit-hooks
|
||||
title: Update pre-commit hooks
|
||||
commit-message: "chore: update pre-commit hooks"
|
||||
committer: Freqtrade Bot <noreply@github.com>
|
||||
committer: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
||||
author: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
||||
body: Update versions of pre-commit hooks to latest version.
|
||||
delete-branch: true
|
||||
|
||||
@@ -1,29 +1,41 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
|
||||
- repo: local
|
||||
# Keep json schema in sync with the config schema
|
||||
# This will write the files - and fail pre-commit if a file has been changed.
|
||||
hooks:
|
||||
- id: Extract config json schema
|
||||
name: extract-config-json-schema
|
||||
entry: "python build_helpers/extract_config_json_schema.py"
|
||||
language: python
|
||||
pass_filenames: false
|
||||
additional_dependencies: ["python-rapidjson", "jsonschema"]
|
||||
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: "7.1.1"
|
||||
rev: "7.2.0"
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [Flake8-pyproject]
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.14.1"
|
||||
rev: "v1.15.0"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==5.5.0.20240820
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.32.0.20241016
|
||||
- types-requests==2.32.0.20250328
|
||||
- types-tabulate==0.9.0.20241207
|
||||
- types-python-dateutil==2.9.0.20241206
|
||||
- SQLAlchemy==2.0.37
|
||||
- SQLAlchemy==2.0.40
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: "6.0.0"
|
||||
rev: "6.0.1"
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
@@ -31,7 +43,7 @@ repos:
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.9.4'
|
||||
rev: 'v0.11.7'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
@@ -57,7 +69,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/stefmolin/exif-stripper
|
||||
rev: 0.6.1
|
||||
rev: 0.6.2
|
||||
hooks:
|
||||
- id: strip-exif
|
||||
|
||||
@@ -67,3 +79,9 @@ repos:
|
||||
- id: codespell
|
||||
additional_dependencies:
|
||||
- tomli
|
||||
|
||||
# Ensure github actions remain safe
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.6.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12.8-slim-bookworm as base
|
||||
FROM python:3.12.10-slim-bookworm as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 
|
||||
|
||||
[](https://github.com/freqtrade/freqtrade/actions/)
|
||||
[](https://github.com/freqtrade/freqtrade/actions/)
|
||||
[](https://doi.org/10.21105/joss.04864)
|
||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||
[](https://www.freqtrade.io)
|
||||
|
||||
@@ -12,7 +12,12 @@ secret = os.environ.get("FREQTRADE__EXCHANGE__SECRET")
|
||||
proxy = os.environ.get("CI_WEB_PROXY")
|
||||
|
||||
exchange = ccxt.binance(
|
||||
{"apiKey": key, "secret": secret, "httpsProxy": proxy, "options": {"defaultType": "swap"}}
|
||||
{
|
||||
"apiKey": key,
|
||||
"secret": secret,
|
||||
"httpsProxy": proxy,
|
||||
"options": {"defaultType": "swap"},
|
||||
}
|
||||
)
|
||||
_ = exchange.load_markets()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import subprocess
|
||||
import subprocess # noqa: S404, RUF100
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,23 @@ from pathlib import Path
|
||||
|
||||
import rapidjson
|
||||
|
||||
from freqtrade.configuration.config_schema import CONF_SCHEMA
|
||||
|
||||
|
||||
def extract_config_json_schema():
|
||||
try:
|
||||
# Try to import from the installed package
|
||||
from freqtrade.config_schema import CONF_SCHEMA
|
||||
except ImportError:
|
||||
# If freqtrade is not installed, add the parent directory to sys.path
|
||||
# to import directly from the source
|
||||
import sys
|
||||
|
||||
script_dir = Path(__file__).parent
|
||||
freqtrade_dir = script_dir.parent
|
||||
sys.path.insert(0, str(freqtrade_dir))
|
||||
|
||||
# Now try to import from the source
|
||||
from freqtrade.config_schema import CONF_SCHEMA
|
||||
|
||||
schema_filename = Path(__file__).parent / "schema.json"
|
||||
with schema_filename.open("w") as f:
|
||||
rapidjson.dump(CONF_SCHEMA, f, indent=2)
|
||||
|
||||
Binary file not shown.
@@ -257,7 +257,8 @@
|
||||
"enum": [
|
||||
"day",
|
||||
"week",
|
||||
"month"
|
||||
"month",
|
||||
"year"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -541,6 +542,10 @@
|
||||
"description": "Edge configuration.",
|
||||
"$ref": "#/definitions/edge"
|
||||
},
|
||||
"log_config": {
|
||||
"description": "Logging configuration.",
|
||||
"$ref": "#/definitions/logging"
|
||||
},
|
||||
"freqai": {
|
||||
"description": "FreqAI configuration.",
|
||||
"$ref": "#/definitions/freqai"
|
||||
@@ -612,6 +617,14 @@
|
||||
"description": "Telegram topic ID - only applicable for group chats",
|
||||
"type": "string"
|
||||
},
|
||||
"authorized_users": {
|
||||
"description": "Authorized users for the bot.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"allow_custom_messages": {
|
||||
"description": "Allow sending custom messages from the Strategy.",
|
||||
"type": "boolean",
|
||||
@@ -1019,6 +1032,7 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"running",
|
||||
"paused",
|
||||
"stopped"
|
||||
]
|
||||
},
|
||||
@@ -1272,6 +1286,30 @@
|
||||
"allowed_risk"
|
||||
]
|
||||
},
|
||||
"logging": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "number",
|
||||
"const": 1
|
||||
},
|
||||
"formatters": {
|
||||
"type": "object"
|
||||
},
|
||||
"handlers": {
|
||||
"type": "object"
|
||||
},
|
||||
"root": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"version",
|
||||
"formatters",
|
||||
"handlers",
|
||||
"root"
|
||||
]
|
||||
},
|
||||
"external_message_consumer": {
|
||||
"description": "Configuration for external message consumer.",
|
||||
"type": "object",
|
||||
@@ -1366,10 +1404,10 @@
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"keras": {
|
||||
"description": "Use Keras for model training.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
"identifier": {
|
||||
"description": "A unique ID for the current model. Must be changed when modifying features.",
|
||||
"type": "string",
|
||||
"default": "example"
|
||||
},
|
||||
"write_metrics_to_disk": {
|
||||
"description": "Write metrics to disk?",
|
||||
@@ -1399,16 +1437,49 @@
|
||||
"type": "number",
|
||||
"default": 7
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique ID for the current model. Must be changed when modifying features.",
|
||||
"type": "string",
|
||||
"default": "example"
|
||||
"live_retrain_hours": {
|
||||
"description": "Frequency of retraining during dry/live runs.",
|
||||
"type": "number",
|
||||
"default": 0
|
||||
},
|
||||
"expiration_hours": {
|
||||
"description": "Avoid making predictions if a model is more than `expiration_hours` old. Defaults to 0 (no expiration).",
|
||||
"type": "number",
|
||||
"default": 0
|
||||
},
|
||||
"save_backtest_models": {
|
||||
"description": "Save models to disk when running backtesting.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"fit_live_predictions_candles": {
|
||||
"description": "Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset.",
|
||||
"type": "integer"
|
||||
},
|
||||
"data_kitchen_thread_count": {
|
||||
"description": "Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.).",
|
||||
"type": "integer"
|
||||
},
|
||||
"activate_tensorboard": {
|
||||
"description": "Indicate whether or not to activate tensorboard",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"wait_for_training_iteration_on_reload": {
|
||||
"description": "Wait for the next training iteration to complete after /reload or ctrl+c.",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"continual_learning": {
|
||||
"description": "Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"keras": {
|
||||
"description": "Use Keras for model training.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"feature_parameters": {
|
||||
"description": "The parameters used to engineer the feature set",
|
||||
"type": "object",
|
||||
@@ -1445,6 +1516,14 @@
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"indicator_periods_candles": {
|
||||
"description": "Time periods to calculate indicators for. The indicators are added to the base indicator dataset.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
"use_SVM_to_remove_outliers": {
|
||||
"description": "Use SVM to remove outliers from the features.",
|
||||
"type": "boolean",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11.11-slim-bookworm as base
|
||||
FROM python:3.11.12-slim-bookworm as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
@@ -34,7 +34,7 @@ COPY build_helpers/* /tmp/
|
||||
# Install dependencies
|
||||
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
||||
USER ftuser
|
||||
RUN pip install --user --no-cache-dir numpy \
|
||||
RUN pip install --user --no-cache-dir "numpy<2" \
|
||||
&& pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib \
|
||||
&& pip install --user --no-cache-dir -r requirements.txt
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ sudo loginctl enable-linger "$USER"
|
||||
If you run the bot as a service, you can use systemd service manager as a software watchdog monitoring freqtrade bot
|
||||
state and restarting it in the case of failures. If the `internals.sd_notify` parameter is set to true in the
|
||||
configuration or the `--sd-notify` command line option is used, the bot will send keep-alive ping messages to systemd
|
||||
using the sd_notify (systemd notifications) protocol and will also tell systemd its current state (Running or Stopped)
|
||||
using the sd_notify (systemd notifications) protocol and will also tell systemd its current state (Running, Paused or Stopped)
|
||||
when it changes.
|
||||
|
||||
The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd
|
||||
@@ -188,30 +188,113 @@ as the watchdog.
|
||||
|
||||
## Advanced Logging
|
||||
|
||||
Freqtrade uses the default logging module provided by python.
|
||||
Python allows for extensive [logging configuration](https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig) in this regard - way more than what can be covered here.
|
||||
|
||||
Default logging format (coloured terminal output) is set up by default if no `log_config` is provided in your freqtrade configuration.
|
||||
Using `--logfile logfile.log` will enable the RotatingFileHandler.
|
||||
|
||||
If you're not content with the log format, or with the default settings provided for the RotatingFileHandler, you can customize logging to your liking by adding the `log_config` configuration to your freqtrade configuration file(s).
|
||||
|
||||
The default configuration looks roughly like the below, with the file handler being provided but not enabled as the `filename` is commented out.
|
||||
Uncomment this line and supply a valid path/filename to enable it.
|
||||
|
||||
``` json hl_lines="5-7 13-16 27"
|
||||
{
|
||||
"log_config": {
|
||||
"version": 1,
|
||||
"formatters": {
|
||||
"basic": {
|
||||
"format": "%(message)s"
|
||||
},
|
||||
"standard": {
|
||||
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "freqtrade.loggers.ft_rich_handler.FtRichHandler",
|
||||
"formatter": "basic"
|
||||
},
|
||||
"file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"formatter": "standard",
|
||||
// "filename": "someRandomLogFile.log",
|
||||
"maxBytes": 10485760,
|
||||
"backupCount": 10
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"handlers": [
|
||||
"console",
|
||||
// "file"
|
||||
],
|
||||
"level": "INFO",
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! Note "highlighted lines"
|
||||
Highlighted lines in the above code-block define the Rich handler and belong together.
|
||||
The formatter "standard" and "file" will belong to the FileHandler.
|
||||
|
||||
Each handler must use one of the defined formatters (by name), its class must be available, and must be a valid logging class.
|
||||
To actually use a handler, it must be in the "handlers" section inside the "root" segment.
|
||||
If this section is left out, freqtrade will provide no output (in the non-configured handler, anyway).
|
||||
|
||||
!!! Tip "Explicit log configuration"
|
||||
We recommend to extract the logging configuration from your main freqtrade configuration file, and provide it to your bot via [multiple configuration files](configuration.md#multiple-configuration-files) functionality. This will avoid unnecessary code duplication.
|
||||
|
||||
---
|
||||
|
||||
On many Linux systems the bot can be configured to send its log messages to `syslog` or `journald` system services. Logging to a remote `syslog` server is also available on Windows. The special values for the `--logfile` command line option can be used for this.
|
||||
|
||||
### Logging to syslog
|
||||
|
||||
To send Freqtrade log messages to a local or remote `syslog` service use the `--logfile` command line option with the value in the following format:
|
||||
To send Freqtrade log messages to a local or remote `syslog` service use the `"log_config"` setup option to configure logging.
|
||||
|
||||
* `--logfile syslog:<syslog_address>` -- send log messages to `syslog` service using the `<syslog_address>` as the syslog address.
|
||||
``` json
|
||||
{
|
||||
// ...
|
||||
"log_config": {
|
||||
"version": 1,
|
||||
"formatters": {
|
||||
"syslog_fmt": {
|
||||
"format": "%(name)s - %(levelname)s - %(message)s"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
// Other handlers?
|
||||
"syslog": {
|
||||
"class": "logging.handlers.SysLogHandler",
|
||||
"formatter": "syslog_fmt",
|
||||
// Use one of the other options above as address instead?
|
||||
"address": "/dev/log"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"handlers": [
|
||||
// other handlers
|
||||
"syslog",
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
The syslog address can be either a Unix domain socket (socket filename) or a UDP socket specification, consisting of IP address and UDP port, separated by the `:` character.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
So, the following are the examples of possible usages:
|
||||
[Additional log-handlers](#advanced-logging) may need to be configured to for example also have log output in the console.
|
||||
|
||||
* `--logfile syslog:/dev/log` -- log to syslog (rsyslog) using the `/dev/log` socket, suitable for most systems.
|
||||
* `--logfile syslog` -- same as above, the shortcut for `/dev/log`.
|
||||
* `--logfile syslog:/var/run/syslog` -- log to syslog (rsyslog) using the `/var/run/syslog` socket. Use this on MacOS.
|
||||
* `--logfile syslog:localhost:514` -- log to local syslog using UDP socket, if it listens on port 514.
|
||||
* `--logfile syslog:<ip>:514` -- log to remote syslog at IP address and port 514. This may be used on Windows for remote logging to an external syslog server.
|
||||
#### Syslog usage
|
||||
|
||||
Log messages are send to `syslog` with the `user` facility. So you can see them with the following commands:
|
||||
|
||||
* `tail -f /var/log/user`, or
|
||||
* `tail -f /var/log/user`, or
|
||||
* install a comprehensive graphical viewer (for instance, 'Log File Viewer' for Ubuntu).
|
||||
|
||||
On many systems `syslog` (`rsyslog`) fetches data from `journald` (and vice versa), so both `--logfile syslog` or `--logfile journald` can be used and the messages be viewed with both `journalctl` and a syslog viewer utility. You can combine this in any way which suites you better.
|
||||
On many systems `syslog` (`rsyslog`) fetches data from `journald` (and vice versa), so both syslog or journald can be used and the messages be viewed with both `journalctl` and a syslog viewer utility. You can combine this in any way which suites you better.
|
||||
|
||||
For `rsyslog` the messages from the bot can be redirected into a separate dedicated log file. To achieve this, add
|
||||
|
||||
@@ -228,13 +311,69 @@ For `syslog` (`rsyslog`), the reduction mode can be switched on. This will reduc
|
||||
$RepeatedMsgReduction on
|
||||
```
|
||||
|
||||
#### Syslog addressing
|
||||
|
||||
The syslog address can be either a Unix domain socket (socket filename) or a UDP socket specification, consisting of IP address and UDP port, separated by the `:` character.
|
||||
|
||||
|
||||
So, the following are the examples of possible addresses:
|
||||
|
||||
* `"address": "/dev/log"` -- log to syslog (rsyslog) using the `/dev/log` socket, suitable for most systems.
|
||||
* `"address": "/var/run/syslog"` -- log to syslog (rsyslog) using the `/var/run/syslog` socket. Use this on MacOS.
|
||||
* `"address": "localhost:514"` -- log to local syslog using UDP socket, if it listens on port 514.
|
||||
* `"address": "<ip>:514"` -- log to remote syslog at IP address and port 514. This may be used on Windows for remote logging to an external syslog server.
|
||||
|
||||
|
||||
??? Info "Deprecated - configure syslog via command line"
|
||||
|
||||
`--logfile syslog:<syslog_address>` -- send log messages to `syslog` service using the `<syslog_address>` as the syslog address.
|
||||
|
||||
The syslog address can be either a Unix domain socket (socket filename) or a UDP socket specification, consisting of IP address and UDP port, separated by the `:` character.
|
||||
|
||||
So, the following are the examples of possible usages:
|
||||
|
||||
* `--logfile syslog:/dev/log` -- log to syslog (rsyslog) using the `/dev/log` socket, suitable for most systems.
|
||||
* `--logfile syslog` -- same as above, the shortcut for `/dev/log`.
|
||||
* `--logfile syslog:/var/run/syslog` -- log to syslog (rsyslog) using the `/var/run/syslog` socket. Use this on MacOS.
|
||||
* `--logfile syslog:localhost:514` -- log to local syslog using UDP socket, if it listens on port 514.
|
||||
* `--logfile syslog:<ip>:514` -- log to remote syslog at IP address and port 514. This may be used on Windows for remote logging to an external syslog server.
|
||||
|
||||
### Logging to journald
|
||||
|
||||
This needs the `cysystemd` python package installed as dependency (`pip install cysystemd`), which is not available on Windows. Hence, the whole journald logging functionality is not available for a bot running on Windows.
|
||||
|
||||
To send Freqtrade log messages to `journald` system service use the `--logfile` command line option with the value in the following format:
|
||||
To send Freqtrade log messages to `journald` system service, add the following configuration snippet to your configuration.
|
||||
|
||||
* `--logfile journald` -- send log messages to `journald`.
|
||||
``` json
|
||||
{
|
||||
// ...
|
||||
"log_config": {
|
||||
"version": 1,
|
||||
"formatters": {
|
||||
"journald_fmt": {
|
||||
"format": "%(name)s - %(levelname)s - %(message)s"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
// Other handlers?
|
||||
"journald": {
|
||||
"class": "cysystemd.journal.JournaldLogHandler",
|
||||
"formatter": "journald_fmt",
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"handlers": [
|
||||
// ..
|
||||
"journald",
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[Additional log-handlers](#advanced-logging) may need to be configured to for example also have log output in the console.
|
||||
|
||||
Log messages are send to `journald` with the `user` facility. So you can see them with the following commands:
|
||||
|
||||
@@ -244,3 +383,51 @@ Log messages are send to `journald` with the `user` facility. So you can see the
|
||||
There are many other options in the `journalctl` utility to filter the messages, see manual pages for this utility.
|
||||
|
||||
On many systems `syslog` (`rsyslog`) fetches data from `journald` (and vice versa), so both `--logfile syslog` or `--logfile journald` can be used and the messages be viewed with both `journalctl` and a syslog viewer utility. You can combine this in any way which suites you better.
|
||||
|
||||
??? Info "Deprecated - configure journald via command line"
|
||||
To send Freqtrade log messages to `journald` system service use the `--logfile` command line option with the value in the following format:
|
||||
|
||||
`--logfile journald` -- send log messages to `journald`.
|
||||
|
||||
### Log format as JSON
|
||||
|
||||
You can also configure the default output stream to use JSON format instead.
|
||||
The "fmt_dict" attribute defines the keys for the json output - as well as the [python logging LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes).
|
||||
|
||||
The below configuration will change the default output to JSON. The same formatter could however also be used in combination with the `RotatingFileHandler`.
|
||||
We recommend to keep one format in human readable form.
|
||||
|
||||
``` json
|
||||
{
|
||||
// ...
|
||||
"log_config": {
|
||||
"version": 1,
|
||||
"formatters": {
|
||||
"json": {
|
||||
"()": "freqtrade.loggers.json_formatter.JsonFormatter",
|
||||
"fmt_dict": {
|
||||
"timestamp": "asctime",
|
||||
"level": "levelname",
|
||||
"logger": "name",
|
||||
"message": "message"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
// Other handlers?
|
||||
"jsonStream": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "json"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"handlers": [
|
||||
// ..
|
||||
"jsonStream",
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -209,6 +209,7 @@ A backtesting result will look like that:
|
||||
| Sortino | 1.88 |
|
||||
| Sharpe | 2.97 |
|
||||
| Calmar | 6.29 |
|
||||
| SQN | 2.45 |
|
||||
| Profit factor | 1.11 |
|
||||
| Expectancy (Ratio) | -0.15 (-0.05) |
|
||||
| Avg. stake amount | 0.001 BTC |
|
||||
@@ -315,6 +316,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
| Sortino | 1.88 |
|
||||
| Sharpe | 2.97 |
|
||||
| Calmar | 6.29 |
|
||||
| SQN | 2.45 |
|
||||
| Profit factor | 1.11 |
|
||||
| Expectancy (Ratio) | -0.15 (-0.05) |
|
||||
| Avg. stake amount | 0.001 BTC |
|
||||
@@ -368,6 +370,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
- `Sortino`: Annualized Sortino ratio.
|
||||
- `Sharpe`: Annualized Sharpe ratio.
|
||||
- `Calmar`: Annualized Calmar ratio.
|
||||
- `SQN`: System Quality Number (SQN) - by Van Tharp.
|
||||
- `Profit factor`: profit / loss.
|
||||
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
||||
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
||||
@@ -432,6 +435,20 @@ To save time, by default backtest will reuse a cached result from within the las
|
||||
To further analyze your backtest results, freqtrade will export the trades to file by default.
|
||||
You can then load the trades to perform further analysis as shown in the [data analysis](strategy_analysis_example.md#load-backtest-results-to-pandas-dataframe) backtesting section.
|
||||
|
||||
### Backtest output file
|
||||
|
||||
The output file freqtrade produces is a zip file containing the following files:
|
||||
|
||||
- The backtest report in json format
|
||||
- the market change data in feather format
|
||||
- a copy of the strategy file
|
||||
- a copy of the strategy parameters (if a parameter file was used)
|
||||
- a sanitized copy of the config file
|
||||
|
||||
This will ensure results are reproducible - under the assumption that the same data is available.
|
||||
|
||||
Only the strategy file and the config file are included in the zip file, eventual dependencies are not included.
|
||||
|
||||
## Assumptions made by backtesting
|
||||
|
||||
Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions:
|
||||
|
||||
@@ -54,11 +54,13 @@ By default, the bot loop runs every few seconds (`internals.process_throttle_sec
|
||||
* Check timeouts for open orders.
|
||||
* Calls `check_entry_timeout()` strategy callback for open entry orders.
|
||||
* Calls `check_exit_timeout()` strategy callback for open exit orders.
|
||||
* Calls `adjust_entry_price()` strategy callback for open entry orders.
|
||||
* Calls `adjust_order_price()` strategy callback for open orders.
|
||||
* Calls `adjust_entry_price()` strategy callback for open entry orders. *only called when `adjust_order_price()` is not implemented*
|
||||
* Calls `adjust_exit_price()` strategy callback for open exit orders. *only called when `adjust_order_price()` is not implemented*
|
||||
* Verifies existing positions and eventually places exit orders.
|
||||
* Considers stoploss, ROI and exit-signal, `custom_exit()` and `custom_stoploss()`.
|
||||
* Determine exit-price based on `exit_pricing` configuration setting or by using the `custom_exit_price()` callback.
|
||||
* Before a exit order is placed, `confirm_trade_exit()` strategy callback is called.
|
||||
* Before an exit order is placed, `confirm_trade_exit()` strategy callback is called.
|
||||
* Check position adjustments for open trades if enabled by calling `adjust_trade_position()` and place additional order if required.
|
||||
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
||||
* Verifies entry signal trying to enter new positions.
|
||||
@@ -80,7 +82,9 @@ This loop will be repeated again and again until the bot is stopped.
|
||||
* Loops per candle simulating entry and exit points.
|
||||
* Calls `bot_loop_start()` strategy callback.
|
||||
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
|
||||
* Calls `adjust_entry_price()` strategy callback for open entry orders.
|
||||
* Calls `adjust_order_price()` strategy callback for open orders.
|
||||
* Calls `adjust_entry_price()` strategy callback for open entry orders. *only called when `adjust_order_price()` is not implemented!*
|
||||
* Calls `adjust_exit_price()` strategy callback for open exit orders. *only called when `adjust_order_price()` is not implemented!*
|
||||
* Check for trade entry signals (`enter_long` / `enter_short` columns).
|
||||
* Confirm trade entry / exits (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
||||
* Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle).
|
||||
|
||||
@@ -59,7 +59,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
usage: freqtrade backtesting-show [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[-c PATH] [-d PATH] [--userdir PATH]
|
||||
[--export-filename PATH] [--show-pair-list]
|
||||
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
@@ -11,8 +11,9 @@ options:
|
||||
`--export` to be set as well. Example: `--export-filen
|
||||
ame=user_data/backtest_results/backtest_today.json`
|
||||
--show-pair-list Show backtesting pairlist sorted by profit.
|
||||
--breakdown {day,week,month} [{day,week,month} ...]
|
||||
Show backtesting breakdown per [day, week, month].
|
||||
--breakdown {day,week,month,year} [{day,week,month,year} ...]
|
||||
Show backtesting breakdown per [day, week, month,
|
||||
year].
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
@@ -29,7 +30,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ usage: freqtrade backtesting [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||
[--export {none,trades,signals}]
|
||||
[--export-filename PATH]
|
||||
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
|
||||
[--cache {none,day,week,month}]
|
||||
[--freqai-backtest-live-models]
|
||||
|
||||
@@ -65,8 +65,9 @@ options:
|
||||
Use this filename for backtest results.Requires
|
||||
`--export` to be set as well. Example: `--export-filen
|
||||
ame=user_data/backtest_results/backtest_today.json`
|
||||
--breakdown {day,week,month} [{day,week,month} ...]
|
||||
Show backtesting breakdown per [day, week, month].
|
||||
--breakdown {day,week,month,year} [{day,week,month,year} ...]
|
||||
Show backtesting breakdown per [day, week, month,
|
||||
year].
|
||||
--cache {none,day,week,month}
|
||||
Load a cached backtest result no older than specified
|
||||
age (default: day).
|
||||
@@ -88,7 +89,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -45,7 +45,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -34,7 +34,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -63,7 +63,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -55,7 +55,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ usage: freqtrade hyperopt-show [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[--profitable] [-n INT] [--print-json]
|
||||
[--hyperopt-filename FILENAME] [--no-header]
|
||||
[--disable-param-export]
|
||||
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
@@ -18,8 +18,9 @@ options:
|
||||
--no-header Do not print epoch details header.
|
||||
--disable-param-export
|
||||
Disable automatic hyperopt parameter export.
|
||||
--breakdown {day,week,month} [{day,week,month} ...]
|
||||
Show backtesting breakdown per [day, week, month].
|
||||
--breakdown {day,week,month,year} [{day,week,month,year} ...]
|
||||
Show backtesting breakdown per [day, week, month,
|
||||
year].
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
@@ -36,7 +37,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ options:
|
||||
SortinoHyperOptLoss, SortinoHyperOptLossDaily,
|
||||
CalmarHyperOptLoss, MaxDrawDownHyperOptLoss,
|
||||
MaxDrawDownRelativeHyperOptLoss,
|
||||
MaxDrawDownPerPairHyperOptLoss,
|
||||
ProfitDrawDownHyperOptLoss, MultiMetricHyperOptLoss
|
||||
--disable-param-export
|
||||
Disable automatic hyperopt parameter export.
|
||||
@@ -102,7 +103,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
```
|
||||
usage: freqtrade install-ui [-h] [--erase] [--ui-version UI_VERSION]
|
||||
usage: freqtrade install-ui [-h] [--erase] [--prerelease]
|
||||
[--ui-version UI_VERSION]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--erase Clean UI folder, don't download new version.
|
||||
--prerelease Install the latest pre-release version of FreqUI. This
|
||||
is not recommended for production use.
|
||||
--ui-version UI_VERSION
|
||||
Specify a specific version of FreqUI to install. Not
|
||||
specifying this installs the latest version.
|
||||
|
||||
@@ -41,7 +41,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -27,7 +27,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -89,7 +89,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -63,7 +63,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -49,7 +49,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -41,7 +41,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -30,7 +30,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -34,7 +34,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -41,7 +41,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@ Common arguments:
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
Path to the base directory of the exchange with
|
||||
historical backtesting data. To see futures data, use
|
||||
trading-mode additionally.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled). <br> *Defaults to `true`.*<br> **Datatype:** Boolean
|
||||
| `exit_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to exit. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Exit](#exit-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
|
||||
| | **TODO**
|
||||
| | **Order/Signal handling**
|
||||
| `use_exit_signal` | Use exit signals produced by the strategy in addition to the `minimal_roi`. <br>Setting this to false disables the usage of `"exit_long"` and `"exit_short"` columns. Has no influence on other exit methods (Stoploss, ROI, callbacks). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| `exit_profit_only` | Wait until the bot reaches `exit_profit_offset` before taking an exit decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `exit_profit_offset` | Exit-signal is only active above this value. Only active in combination with `exit_profit_only=True`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio)
|
||||
@@ -266,7 +266,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `bot_name` | Name of the bot. Passed via API to a client - can be shown to distinguish / name bots.<br> *Defaults to `freqtrade`*<br> **Datatype:** String
|
||||
| `external_message_consumer` | Enable [Producer/Consumer mode](producer-consumer.md) for more details. <br> **Datatype:** Dict
|
||||
| | **Other**
|
||||
| `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command. <br>*Defaults to `stopped`.* <br> **Datatype:** Enum, either `stopped` or `running`
|
||||
| `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command. <br>*Defaults to `stopped`.* <br> **Datatype:** Enum, either `running`, `paused` or `stopped`
|
||||
| `force_entry_enable` | Enables the RPC Commands to force a Trade entry. More information below. <br> **Datatype:** Boolean
|
||||
| `disable_dataframe_checks` | Disable checking the OHLCV dataframe returned from the strategy methods for correctness. Only use when intentionally changing the dataframe and understand what you are doing. [Strategy Override](#parameters-in-the-strategy).<br> *Defaults to `False`*. <br> **Datatype:** Boolean
|
||||
| `internals.process_throttle_secs` | Set the process throttle, or minimum loop duration for one bot iteration loop. Value in second. <br>*Defaults to `5` seconds.* <br> **Datatype:** Positive Integer
|
||||
@@ -281,7 +281,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `add_config_files` | Additional config files. These files will be loaded and merged with the current config file. The files are resolved relative to the initial file.<br> *Defaults to `[]`*. <br> **Datatype:** List of strings
|
||||
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `feather`*. <br> **Datatype:** String
|
||||
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `feather`*. <br> **Datatype:** String
|
||||
| `reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage (and decreasing train/inference timing in FreqAI). (Currently only affects FreqAI use-cases) <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
| `reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage (and decreasing train/inference timing backtesting/hyperopt and in FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
| `log_config` | Dictionary containing the log config for python logging. [more info](advanced-setup.md#advanced-logging) <br> **Datatype:** dict. <br> Default: `FtRichHandler`
|
||||
|
||||
### Parameters in the strategy
|
||||
|
||||
|
||||
@@ -88,3 +88,8 @@ Setting protections from the configuration via `"protections": [],` has been rem
|
||||
Using hdf5 as data storage has been deprecated in 2024.12 and was removed in 2025.1. We recommend switching to the feather data format.
|
||||
|
||||
Please use the [`convert-data` subcommand](data-download.md#sub-command-convert-data) to convert your existing data to one of the supported formats before updating.
|
||||
|
||||
## Configuring advanced logging via config
|
||||
|
||||
Configuring syslog and journald via `--logfile systemd` and `--logfile journald` respectively has been deprecated in 2025.3.
|
||||
Please use configuration based [log setup](advanced-setup.md#advanced-logging) instead.
|
||||
|
||||
@@ -363,6 +363,10 @@ Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer
|
||||
* Create a different software wallet, only transfer the funds you want to trade with to that wallet, and use that wallet to trade on Hyperliquid.
|
||||
* If you have funds you don't want to use for trading (after making a profit for example), transfer them back to your hardware wallet.
|
||||
|
||||
### Historic Hyperliquid data
|
||||
|
||||
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
|
||||
|
||||
## All exchanges
|
||||
|
||||
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
||||
|
||||
@@ -258,6 +258,8 @@ freqtrade trade --config config_examples/config_freqai.example.json --strategy F
|
||||
We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file.
|
||||
This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available.
|
||||
|
||||
PyTorch dropped support for macOS x64 (intel based Apple devices) in version 2.3. Subsequently, freqtrade also dropped support for PyTorch on this platform.
|
||||
|
||||
### Structure
|
||||
|
||||
#### Model
|
||||
|
||||
@@ -471,6 +471,7 @@ Currently, the following loss functions are builtin:
|
||||
* `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation.
|
||||
* `MaxDrawDownHyperOptLoss` - Optimizes Maximum absolute drawdown.
|
||||
* `MaxDrawDownRelativeHyperOptLoss` - Optimizes both maximum absolute drawdown while also adjusting for maximum relative drawdown.
|
||||
* `MaxDrawDownPerPairHyperOptLoss` - Calculates the profit/drawdown ratio per pair and returns the worst result as objective, forcing hyperopt to optimize the parameters for all pairs in the pairlist. This way, we prevent one or more pairs with good results from inflating the metrics, while the pairs with poor results are not represented and therefore not optimized.
|
||||
* `CalmarHyperOptLoss` - Optimizes Calmar Ratio calculated on trade returns relative to max drawdown.
|
||||
* `ProfitDrawDownHyperOptLoss` - Optimizes by max Profit & min Drawdown objective. `DRAWDOWN_MULT` variable within the hyperoptloss file can be adjusted to be stricter or more flexible on drawdown purposes.
|
||||
* `MultiMetricHyperOptLoss` - Optimizes by several key metrics to achieve balanced performance. The primary focus is on maximizing Profit and minimizing Drawdown, while also considering additional metrics such as Profit Factor, Expectancy Ratio and Winrate. Moreover, it applies a penalty for epochs with a low number of trades, encouraging strategies with adequate trade frequency.
|
||||
|
||||
@@ -44,9 +44,24 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
|
||||
|
||||
By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. The pairlist also supports wildcards (in regex-style) - so `.*/BTC` will include all pairs with BTC as a stake.
|
||||
|
||||
It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.
|
||||
It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`, which in the below example, will trade BTC/USDT and ETH/USDT - and will prevent BNB/USDT trading.
|
||||
|
||||
Both `pair_*list` parameters support regex - so values like `.*/USDT` would enable trading all pairs that are not in the blacklist.
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "...",
|
||||
// ...
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT",
|
||||
"ETH/USDT",
|
||||
// ...
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/USDT",
|
||||
// ...
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{"method": "StaticPairList"}
|
||||
],
|
||||
@@ -377,6 +392,9 @@ If an incorrect category string is chosen, the plugin will print the available c
|
||||
!!! Warning "Many categories"
|
||||
Each added category corresponds to one API call to CoinGecko. The more categories you add, the longer the pairlist generation will take, potentially causing rate limit issues.
|
||||
|
||||
!!! Danger "Duplicate symbols in coingecko"
|
||||
Coingecko often has duplicate symbols, where the same symbol is used for different coins. Freqtrade will use the symbol as is and try to search for it on the exchange. If the symbol exists - it will be used. Freqtrade will however not check if the _intended_ symbol is the one coingecko meant. This can sometimes lead to unexpected results, especially on low volume coins or with meme coin categories.
|
||||
|
||||
#### AgeFilter
|
||||
|
||||
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).
|
||||
|
||||
@@ -3,7 +3,7 @@ This section will highlight a few projects from members of the community.
|
||||
The projects below are for the most part not maintained by the freqtrade , therefore use your own caution before using them.
|
||||
|
||||
- [Example freqtrade strategies](https://github.com/freqtrade/freqtrade-strategies/)
|
||||
- [FrequentHippo - Grafana dashboard with dry/live runs and backtests](http://frequenthippo.ddns.net:3000/) (by hippocritical).
|
||||
- [FrequentHippo - Statistics of dry/live runs and backtests](http://frequenthippo.ddns.net) (by hippocritical).
|
||||
- [Online pairlist generator](https://remotepairlist.com/) (by Blood4rc).
|
||||
- [Freqtrade Backtesting Project](https://strat.ninja/) (by Blood4rc).
|
||||
- [Freqtrade analysis notebook](https://github.com/froggleston/freqtrade_analysis_notebook) (by Froggleston).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||

|
||||
|
||||
[](https://github.com/freqtrade/freqtrade/actions/)
|
||||
[](https://github.com/freqtrade/freqtrade/actions/)
|
||||
[](https://doi.org/10.21105/joss.04864)
|
||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||
[](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
markdown==3.7
|
||||
markdown==3.8
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.6.3
|
||||
mkdocs-material==9.6.12
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.14.3
|
||||
jinja2==3.1.5
|
||||
pymdown-extensions==10.15
|
||||
jinja2==3.1.6
|
||||
mike==2.1.3
|
||||
|
||||
@@ -268,6 +268,9 @@ show_config
|
||||
start
|
||||
Start the bot if it's in the stopped state.
|
||||
|
||||
pause
|
||||
Pause the bot if it's in the running state. If triggered on stopped state will handle open positions.
|
||||
|
||||
stats
|
||||
Return the stats report (durations, sell-reasons).
|
||||
|
||||
@@ -302,6 +305,19 @@ trades
|
||||
:param limit: Limits trades to the X last trades. Max 500 trades.
|
||||
:param offset: Offset by this amount of trades.
|
||||
|
||||
list_open_trades_custom_data
|
||||
Return a dict containing open trades custom-datas
|
||||
|
||||
:param key: str, optional - Key of the custom-data
|
||||
:param limit: Limits trades to X trades.
|
||||
:param offset: Offset by this amount of trades.
|
||||
|
||||
list_custom_data
|
||||
Return a dict containing custom-datas of a specified trade
|
||||
|
||||
:param trade_id: int - ID of the trade
|
||||
:param key: str, optional - Key of the custom-data
|
||||
|
||||
version
|
||||
Return the version of the bot.
|
||||
|
||||
@@ -320,6 +336,7 @@ All endpoints in the below table need to be prefixed with the base URL of the AP
|
||||
|-----------|--------|--------------------------|
|
||||
| `/ping` | GET | Simple command testing the API Readiness - requires no authentication.
|
||||
| `/start` | POST | Starts the trader.
|
||||
| `/pause` | POST | Pause the trader. Gracefully handle open trades according to their rules. Do not enter new positions.
|
||||
| `/stop` | POST | Stops the trader.
|
||||
| `/stopbuy` | POST | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||
| `/reload_config` | POST | Reloads the configuration file.
|
||||
|
||||
@@ -154,10 +154,10 @@ For example, simplified math:
|
||||
|
||||
In summary: The stoploss will be adjusted to be always be -10% of the highest observed price.
|
||||
|
||||
### Trailing stop loss, custom positive loss
|
||||
### Trailing stop loss, different positive loss
|
||||
|
||||
You could also have a default stop loss when you are in the red with your buy (buy - fee), but once you hit a positive result (or an offset you define) the system will utilize a new stop loss, which can have a different value.
|
||||
For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used.
|
||||
You could also have a default stop loss when you are in the red with your buy (buy - fee), but once you hit a positive result (or an offset you define) the system will utilize a new stop loss, with a different value.
|
||||
For example, your default stop loss is -10%, but once you have reached profitability (example 0.1%) a different trailing stoploss will be used.
|
||||
|
||||
!!! Note
|
||||
If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refer to next section with [offset enabled](#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset).
|
||||
@@ -208,7 +208,9 @@ Before this, `stoploss` is used for the trailing stoploss.
|
||||
|
||||
You can also keep a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns.
|
||||
|
||||
If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`.
|
||||
If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss` and is not trailing.
|
||||
Leaving this value as `trailing_only_offset_is_reached=False` will allow the trailing stoploss to start trailing as soon as the asset price increases above the initial entry price.
|
||||
|
||||
This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset.
|
||||
|
||||
Configuration (offset is buy-price + 3%):
|
||||
|
||||
@@ -79,6 +79,8 @@ import talib.abstract as ta
|
||||
|
||||
class MyStrategy(IStrategy):
|
||||
|
||||
timeframe = '15m'
|
||||
|
||||
# set the initial stoploss to -10%
|
||||
stoploss = -0.10
|
||||
|
||||
@@ -163,18 +165,23 @@ If there is any significant difference, verify that your entry and exit signals
|
||||
|
||||
## Controlling or monitoring a running bot
|
||||
|
||||
Once your bot is running in dry or live mode, Freqtrade has five mechanisms to control or monitor a running bot:
|
||||
Once your bot is running in dry or live mode, Freqtrade has six mechanisms to control or monitor a running bot:
|
||||
|
||||
- **[FreqUI](freq-ui.md)**: The easiest to get started with, FreqUI is a web interface to see and control current activity of your bot.
|
||||
- **[Telegram](telegram-usage.md)**: On mobile devices, Telegram integration is available to get alerts about your bot activity and to control certain aspects.
|
||||
- **[FTUI](https://github.com/freqtrade/ftui)**: FTUI is a terminal (command line) interface to Freqtrade, and allows monitoring of a running bot only.
|
||||
- **[REST API](rest-api.md)**: The REST API allows programmers to develop their own tools to interact with a Freqtrade bot.
|
||||
- **[freqtrade-client](rest-api.md#consuming-the-api)**: A python implementation of the REST API, making it easy to make requests and consume bot responses from your python apps or the command line.
|
||||
- **[REST API endpoints](rest-api.md#available-endpoints)**: The REST API allows programmers to develop their own tools to interact with a Freqtrade bot.
|
||||
- **[Webhooks](webhook-config.md)**: Freqtrade can send information to other services, e.g. discord, by webhooks.
|
||||
|
||||
### Logs
|
||||
|
||||
Freqtrade generates extensive debugging logs to help you understand what's happening. Please familiarise yourself with the information and error messages you might see in your bot logs.
|
||||
|
||||
Logging by default occurs on standard out (the command line). If you want to write out to a file instead, many freqtrade commands, including the `trade` command, accept the `--logfile` option to write to a file.
|
||||
|
||||
Check the [FAQ](faq.md#how-do-i-search-the-bot-logs-for-something) for examples.
|
||||
|
||||
## Final Thoughts
|
||||
|
||||
Algo trading is difficult, and most public strategies are not good performers due to the time and effort to make a strategy work profitably in multiple scenarios.
|
||||
|
||||
@@ -758,7 +758,7 @@ For performance reasons, it's disabled by default and freqtrade will show a warn
|
||||
|
||||
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
|
||||
|
||||
This callback is also called when there is an open order (either buy or sell) waiting for execution - and will cancel the existing open order to place a new order if the amount, price or direction is different.
|
||||
This callback is also called when there is an open order (either buy or sell) waiting for execution - and will cancel the existing open order to place a new order if the amount, price or direction is different. Also partially filled orders will be canceled, and will be replaced with the new amount as returned by the callback.
|
||||
|
||||
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
||||
|
||||
@@ -770,9 +770,10 @@ Modifications to leverage are not possible, and the stake-amount returned is ass
|
||||
The combined stake currently allocated to the position is held in `trade.stake_amount`. Therefore `trade.stake_amount` will always be updated on every additional entry and partial exit made through `adjust_trade_position()`.
|
||||
|
||||
!!! Danger "Loose Logic"
|
||||
On dry and live run, this function will be called every `throttle_process_secs` (default to 5s). If you have a loose logic, for example your logic for extra entry is only to check RSI of last candle is below 30, then when such condition fulfilled, your bot will do extra re-entry every 5 secs until either it run out of money, it hit the `max_position_adjustment` limit, or a new candle with RSI more than 30 arrived.
|
||||
On dry and live run, this function will be called every `throttle_process_secs` (default to 5s). If you have a loose logic, (e.g. increase position if RSI of the last candle is below 30), your bot will do extra re-entry every 5 secs until you either it run out of money, hit the `max_position_adjustment` limit, or a new candle with RSI more than 30 arrived.
|
||||
|
||||
Same thing also can happen with partial exit. So be sure to have a strict logic and/or check for the last filled order.
|
||||
Same thing also can happen with partial exit.
|
||||
So be sure to have a strict logic and/or check for the last filled order and if an order is already open.
|
||||
|
||||
!!! Warning "Performance with many position adjustments"
|
||||
Position adjustments can be a good approach to increase a strategy's output - but it can also have drawbacks if using this feature extensively.
|
||||
@@ -876,6 +877,9 @@ class DigDeeperStrategy(IStrategy):
|
||||
Return None for no action.
|
||||
Optionally, return a tuple with a 2nd element with an order reason
|
||||
"""
|
||||
if trade.has_open_orders:
|
||||
# Only act if no orders are open
|
||||
return
|
||||
|
||||
if current_profit > 0.05 and trade.nr_of_successful_exits == 0:
|
||||
# Take half of the profit at +5%
|
||||
@@ -934,28 +938,25 @@ class DigDeeperStrategy(IStrategy):
|
||||
|
||||
The total profit for this trade was 950$ on a 3350$ investment (`100@8$ + 100@9$ + 150@11$`). As such - the final relative profit is 28.35% (`950 / 3350`).
|
||||
|
||||
## Adjust Entry Price
|
||||
## Adjust order Price
|
||||
|
||||
The `adjust_entry_price()` callback may be used by strategy developer to refresh/replace limit orders upon arrival of new candles.
|
||||
The `adjust_order_price()` callback may be used by strategy developer to refresh/replace limit orders upon arrival of new candles.
|
||||
This callback is called once every iteration unless the order has been (re)placed within the current candle - limiting the maximum (re)placement of each order to once per candle.
|
||||
This also means that the first call will be at the start of the next candle after the initial order was placed.
|
||||
|
||||
Be aware that `custom_entry_price()` is still the one dictating initial entry limit order price target at the time of entry trigger.
|
||||
Be aware that `custom_entry_price()`/`custom_exit_price()` is still the one dictating initial limit order price target at the time of the signal.
|
||||
|
||||
Orders can be cancelled out of this callback by returning `None`.
|
||||
|
||||
Returning `current_order_rate` will keep the order on the exchange "as is".
|
||||
Returning any other price will cancel the existing order, and replace it with a new order.
|
||||
|
||||
The trade open-date (`trade.open_date_utc`) will remain at the time of the very first order placed.
|
||||
Please make sure to be aware of this - and eventually adjust your logic in other callbacks to account for this, and use the date of the first filled order instead.
|
||||
|
||||
If the cancellation of the original order fails, then the order will not be replaced - though the order will most likely have been canceled on exchange. Having this happen on initial entries will result in the deletion of the order, while on position adjustment orders, it'll result in the trade size remaining as is.
|
||||
If the order has been partially filled, the order will not be replaced. You can however use [`adjust_trade_position()`](#adjust-trade-position) to adjust the trade size to the full, expected position size, should this be necessary / desired.
|
||||
If the order has been partially filled, the order will not be replaced. You can however use [`adjust_trade_position()`](#adjust-trade-position) to adjust the trade size to the expected position size, should this be necessary / desired.
|
||||
|
||||
!!! Warning "Regular timeout"
|
||||
Entry `unfilledtimeout` mechanism (as well as `check_entry_timeout()`) takes precedence over this.
|
||||
Entry Orders that are cancelled via the above methods will not have this callback called. Be sure to update timeout values to match your expectations.
|
||||
Entry `unfilledtimeout` mechanism (as well as `check_entry_timeout()`/`check_exit_timeout()`) takes precedence over this callback.
|
||||
Orders that are cancelled via the above methods will not have this callback called. Be sure to update timeout values to match your expectations.
|
||||
|
||||
```python
|
||||
# Default imports
|
||||
@@ -964,14 +965,26 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def adjust_entry_price(self, trade: Trade, order: Order | None, pair: str,
|
||||
current_time: datetime, proposed_rate: float, current_order_rate: float,
|
||||
entry_tag: str | None, side: str, **kwargs) -> float:
|
||||
def adjust_order_price(
|
||||
self,
|
||||
trade: Trade,
|
||||
order: Order | None,
|
||||
pair: str,
|
||||
current_time: datetime,
|
||||
proposed_rate: float,
|
||||
current_order_rate: float,
|
||||
entry_tag: str | None,
|
||||
side: str,
|
||||
is_entry: bool,
|
||||
**kwargs,
|
||||
) -> float | None:
|
||||
"""
|
||||
Entry price re-adjustment logic, returning the user desired limit price.
|
||||
Exit and entry order price re-adjustment logic, returning the user desired limit price.
|
||||
This only executes when a order was already placed, still open (unfilled fully or partially)
|
||||
and not timed out on subsequent candles after entry trigger.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
|
||||
|
||||
When not implemented by a strategy, returns current_order_rate as default.
|
||||
If current_order_rate is returned then the existing order is maintained.
|
||||
If None is returned then order gets canceled but not replaced by a new one.
|
||||
@@ -983,14 +996,16 @@ class AwesomeStrategy(IStrategy):
|
||||
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
|
||||
:param current_order_rate: Rate of the existing order in place.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: "long" or "short" - indicating the direction of the proposed trade
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param is_entry: True if the order is an entry order, False if it's an exit order.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New entry price value if provided
|
||||
|
||||
:return float or None: New entry price value if provided
|
||||
"""
|
||||
# Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
|
||||
|
||||
# Limit entry orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
|
||||
if (
|
||||
pair == "BTC/USDT"
|
||||
is_entry
|
||||
and pair == "BTC/USDT"
|
||||
and entry_tag == "long_sma200"
|
||||
and side == "long"
|
||||
and (current_time - timedelta(minutes=10)) <= trade.open_date_utc
|
||||
@@ -1007,6 +1022,26 @@ class AwesomeStrategy(IStrategy):
|
||||
return current_order_rate
|
||||
```
|
||||
|
||||
!!! danger "Incompatibility with `adjust_*_price()`"
|
||||
If you have both `adjust_order_price()` and `adjust_entry_price()`/`adjust_exit_price()` implemented, only `adjust_order_price()` will be used.
|
||||
If you need to adjust entry/exit prices, you can either implement the logic in `adjust_order_price()`, or use the split `adjust_entry_price()` / `adjust_exit_price()` callbacks, but not both.
|
||||
Mixing these is not supported and will raise an error during bot startup.
|
||||
|
||||
### Adjust Entry Price
|
||||
|
||||
The `adjust_entry_price()` callback may be used by strategy developer to refresh/replace entry limit orders upon arrival.
|
||||
It's a sub-set of `adjust_order_price()` and is called only for entry orders.
|
||||
All remaining behavior is identical to `adjust_order_price()`.
|
||||
|
||||
The trade open-date (`trade.open_date_utc`) will remain at the time of the very first order placed.
|
||||
Please make sure to be aware of this - and eventually adjust your logic in other callbacks to account for this, and use the date of the first filled order instead.
|
||||
|
||||
### Adjust Exit Price
|
||||
|
||||
The `adjust_exit_price()` callback may be used by strategy developer to refresh/replace exit limit orders upon arrival.
|
||||
It's a sub-set of `adjust_order_price()` and is called only for exit orders.
|
||||
All remaining behavior is identical to `adjust_order_price()`.
|
||||
|
||||
## Leverage Callback
|
||||
|
||||
When trading in markets that allow leverage, this method must return the desired Leverage (Defaults to 1 -> No leverage).
|
||||
|
||||
@@ -513,7 +513,7 @@ By default, freqtrade will attempt to load strategies from all `.py` files withi
|
||||
Assuming your strategy is called `AwesomeStrategy`, stored in the file `user_data/strategies/AwesomeStrategy.py`, then you can start freqtrade in dry (or live, depending on your configuration) mode with:
|
||||
|
||||
```bash
|
||||
freqtrade trade --strategy AwesomeStrategy`
|
||||
freqtrade trade --strategy AwesomeStrategy
|
||||
```
|
||||
|
||||
Note that we're using the class name, not the file name.
|
||||
@@ -1122,6 +1122,7 @@ The following list contains some common patterns which should be avoided to prev
|
||||
- don't use `.iloc[-1]` or any other absolute position in the dataframe within `populate_` functions, as this will be different between dry-run and backtesting. Absolute `iloc` indexing is safe to use in callbacks however - see [Strategy Callbacks](strategy-callbacks.md).
|
||||
- don't use functions that use all dataframe or column values, e.g. `dataframe['mean_volume'] = dataframe['volume'].mean()`. As backtesting uses the full dataframe, at any point in the dataframe, the `'mean_volume'` series would include data from the future. Use rolling() calculations instead, e.g. `dataframe['volume'].rolling(<window>).mean()`.
|
||||
- don't use `.resample('1h')`. This uses the left border of the period interval, so moves data from an hour boundary to the start of the hour. Use `.resample('1h', label='right')` instead.
|
||||
- don't use `.merge()` to combine longer timeframes onto shorter ones. Instead, use the [informative pair](#informative-pairs) helpers. (A plain merge can implicitly cause a lookahead bias as date refers to open date, not close date).
|
||||
|
||||
!!! Tip "Identifying problems"
|
||||
You should always use the two helper commands [lookahead-analysis](lookahead-analysis.md) and [recursive-analysis](recursive-analysis.md), which can each help you figure out problems with your strategy in different ways.
|
||||
|
||||
@@ -81,6 +81,19 @@ Without this, the bot will always respond to the general channel in the group if
|
||||
|
||||
Similar to the group-id - you can use `/tg_info` from the topic/thread to get the correct topic-id.
|
||||
|
||||
#### Authorized users
|
||||
|
||||
For groups, it can be useful to limit who can send commands to the bot.
|
||||
|
||||
If `"authorized_users": []` is present and empty, no user will be allowed to control the bot.
|
||||
In the below example, only the user with the id "1234567" is allowed to control the bot - all other users will only be able to receive messages.
|
||||
|
||||
```json
|
||||
"chat_id": "-1001332619709",
|
||||
"topic_id": "3",
|
||||
"authorized_users": ["1234567"]
|
||||
```
|
||||
|
||||
## Control telegram noise
|
||||
|
||||
Freqtrade provides means to control the verbosity of your telegram bot.
|
||||
@@ -175,7 +188,7 @@ You can create your own keyboard in `config.json`:
|
||||
!!! Note "Supported Commands"
|
||||
Only the following commands are allowed. Command arguments are not supported!
|
||||
|
||||
`/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`, `/marketdir`
|
||||
`/start`, `/pause`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`, `/marketdir`
|
||||
|
||||
## Telegram commands
|
||||
|
||||
@@ -187,8 +200,8 @@ official commands. You can ask at any moment for help with `/help`.
|
||||
|----------|-------------|
|
||||
| **System commands**
|
||||
| `/start` | Starts the trader
|
||||
| `/pause | /stopentry | /stopbuy` | Pause the trader. Gracefully handle open trades according to their rules. Do not enter new positions.
|
||||
| `/stop` | Stops the trader
|
||||
| `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||
| `/reload_config` | Reloads the configuration file
|
||||
| `/show_config` | Shows part of the current configuration with relevant settings to operation
|
||||
| `/logs [limit]` | Show last log messages.
|
||||
@@ -237,25 +250,27 @@ Below, example of Telegram message you will receive for each command.
|
||||
|
||||
> **Status:** `running`
|
||||
|
||||
### /pause | /stopentry | /stopbuy
|
||||
|
||||
> **Status:** `paused, no more entries will occur from now. Run /start to enable entries.`
|
||||
|
||||
Prevents the bot from opening new trades by changing the state to `paused`.
|
||||
Open trades will continue to be managed according to their regular rules (ROI/exit signals, stop-loss, etc.).
|
||||
Note that position adjustment remains active, but only on the exit side — meaning that when the bot is `paused`, it can only reduce the position size of open trades.
|
||||
|
||||
After this, give the bot time to close off open trades (can be checked via `/status table`).
|
||||
Once all positions are closed, run `/stop` to completely stop the bot.
|
||||
|
||||
Use `/start` to resume the bot to the `running` state, allowing it to open new positions.
|
||||
|
||||
!!! Warning
|
||||
The pause/stopentry signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
|
||||
|
||||
### /stop
|
||||
|
||||
> `Stopping trader ...`
|
||||
> **Status:** `stopped`
|
||||
|
||||
### /stopbuy
|
||||
|
||||
> **status:** `Setting max_open_trades to 0. Run /reload_config to reset.`
|
||||
|
||||
Prevents the bot from opening new trades by temporarily setting "max_open_trades" to 0. Open trades will be handled via their regular rules (ROI / Sell-signal, stoploss, ...).
|
||||
|
||||
After this, give the bot time to close off open trades (can be checked via `/status table`).
|
||||
Once all positions are sold, run `/stop` to completely stop the bot.
|
||||
|
||||
`/reload_config` resets "max_open_trades" to the value set in the configuration and resets this command.
|
||||
|
||||
!!! Warning
|
||||
The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
|
||||
|
||||
### /status
|
||||
|
||||
For each open trade, the bot will send you the following message.
|
||||
|
||||
@@ -25,6 +25,7 @@ The following attributes / properties are available for each individual trade -
|
||||
| `close_date_utc` | datetime | Timestamp when trade was closed - in UTC. |
|
||||
| `close_profit` | float | Relative profit at the time of trade closure. `0.01` == 1% |
|
||||
| `close_profit_abs` | float | Absolute profit (in stake currency) at the time of trade closure. |
|
||||
| `realized_profit` | float | Absolute already realized profit (in stake currency) while the trade is still open. |
|
||||
| `leverage` | float | Leverage used for this trade - defaults to 1.0 in spot markets. |
|
||||
| `enter_tag` | string | Tag provided on entry via the `enter_tag` column in the dataframe. |
|
||||
| `is_short` | boolean | True for short trades, False otherwise. |
|
||||
@@ -35,6 +36,7 @@ The following attributes / properties are available for each individual trade -
|
||||
| `trade_direction` | "long" / "short" | Trade direction in text - long or short. |
|
||||
| `nr_of_successful_entries` | int | Number of successful (filled) entry orders. |
|
||||
| `nr_of_successful_exits` | int | Number of successful (filled) exit orders. |
|
||||
| `has_open_orders` | boolean | Has the trade open orders (excluding stoploss orders). |
|
||||
|
||||
## Class methods
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2025.2-dev"
|
||||
__version__ = "2025.5-dev"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import subprocess # noqa: S404
|
||||
import subprocess # noqa: S404, RUF100
|
||||
|
||||
freqtrade_basedir = Path(__file__).parent
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ ARGS_COMMON = [
|
||||
"user_data_dir",
|
||||
]
|
||||
|
||||
ARGS_MAIN = ["version_main"]
|
||||
|
||||
ARGS_STRATEGY = [
|
||||
"strategy",
|
||||
"strategy_path",
|
||||
@@ -43,7 +45,8 @@ ARGS_COMMON_OPTIMIZE = [
|
||||
"pairs",
|
||||
]
|
||||
|
||||
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + [
|
||||
ARGS_BACKTEST = [
|
||||
*ARGS_COMMON_OPTIMIZE,
|
||||
"position_stacking",
|
||||
"enable_protections",
|
||||
"dry_run_wallet",
|
||||
@@ -56,7 +59,8 @@ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + [
|
||||
"freqai_backtest_live_models",
|
||||
]
|
||||
|
||||
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + [
|
||||
ARGS_HYPEROPT = [
|
||||
*ARGS_COMMON_OPTIMIZE,
|
||||
"hyperopt",
|
||||
"hyperopt_path",
|
||||
"position_stacking",
|
||||
@@ -76,7 +80,7 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + [
|
||||
"analyze_per_epoch",
|
||||
]
|
||||
|
||||
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
||||
ARGS_EDGE = [*ARGS_COMMON_OPTIMIZE, "stoploss_range"]
|
||||
|
||||
ARGS_LIST_STRATEGIES = [
|
||||
"strategy_path",
|
||||
@@ -125,7 +129,7 @@ ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "strategy_path", "template"]
|
||||
|
||||
ARGS_CONVERT_DATA_TRADES = ["pairs", "format_from_trades", "format_to", "erase", "exchange"]
|
||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"]
|
||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode", "candle_types"]
|
||||
ARGS_CONVERT_DATA_OHLCV = [*ARGS_CONVERT_DATA, "timeframes", "trading_mode", "candle_types"]
|
||||
|
||||
ARGS_CONVERT_TRADES = [
|
||||
"pairs",
|
||||
@@ -191,7 +195,7 @@ ARGS_PLOT_PROFIT = [
|
||||
|
||||
ARGS_CONVERT_DB = ["db_url", "db_url_from"]
|
||||
|
||||
ARGS_INSTALL_UI = ["erase_ui_only", "ui_version"]
|
||||
ARGS_INSTALL_UI = ["erase_ui_only", "ui_prerelease", "ui_version"]
|
||||
|
||||
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]
|
||||
|
||||
@@ -347,7 +351,7 @@ class Arguments:
|
||||
self.parser = ArgumentParser(
|
||||
prog="freqtrade", description="Free, open source crypto trading bot"
|
||||
)
|
||||
self._build_args(optionlist=["version_main"], parser=self.parser)
|
||||
self._build_args(optionlist=ARGS_MAIN, parser=self.parser)
|
||||
|
||||
from freqtrade.commands import (
|
||||
start_analysis_entries_exits,
|
||||
|
||||
@@ -83,7 +83,8 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"-d",
|
||||
"--datadir",
|
||||
"--data-dir",
|
||||
help="Path to directory with historical backtesting data.",
|
||||
help="Path to the base directory of the exchange with historical backtesting data. "
|
||||
"To see futures data, use trading-mode additionally.",
|
||||
metavar="PATH",
|
||||
),
|
||||
"user_data_dir": Arg(
|
||||
@@ -224,7 +225,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"backtest_breakdown": Arg(
|
||||
"--breakdown",
|
||||
help="Show backtesting breakdown per [day, week, month].",
|
||||
help="Show backtesting breakdown per [day, week, month, year].",
|
||||
nargs="+",
|
||||
choices=constants.BACKTEST_BREAKDOWNS,
|
||||
),
|
||||
@@ -463,7 +464,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"format_from_trades": Arg(
|
||||
"--format-from",
|
||||
help="Source format for data conversion.",
|
||||
choices=constants.AVAILABLE_DATAHANDLERS + ["kraken_csv"],
|
||||
choices=[*constants.AVAILABLE_DATAHANDLERS, "kraken_csv"],
|
||||
required=True,
|
||||
),
|
||||
"format_from": Arg(
|
||||
@@ -527,6 +528,15 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
type=str,
|
||||
),
|
||||
"ui_prerelease": Arg(
|
||||
"--prerelease",
|
||||
help=(
|
||||
"Install the latest pre-release version of FreqUI. "
|
||||
"This is not recommended for production use."
|
||||
),
|
||||
action="store_true",
|
||||
default=False,
|
||||
),
|
||||
# Templating options
|
||||
"template": Arg(
|
||||
"--template",
|
||||
|
||||
@@ -23,8 +23,8 @@ def start_create_userdir(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir
|
||||
|
||||
if "user_data_dir" in args and args["user_data_dir"]:
|
||||
userdir = create_userdata_dir(args["user_data_dir"], create_dir=True)
|
||||
if user_data_dir := args.get("user_data_dir"):
|
||||
userdir = create_userdata_dir(user_data_dir, create_dir=True)
|
||||
copy_sample_files(userdir, overwrite=args["reset"])
|
||||
else:
|
||||
logger.warning("`create-userdir` requires --userdir to be set.")
|
||||
@@ -85,22 +85,22 @@ def start_new_strategy(args: dict[str, Any]) -> None:
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
if "strategy" in args and args["strategy"]:
|
||||
if "strategy_path" in args and args["strategy_path"]:
|
||||
strategy_dir = Path(args["strategy_path"])
|
||||
if strategy := args.get("strategy"):
|
||||
if strategy_path := args.get("strategy_path"):
|
||||
strategy_dir = Path(strategy_path)
|
||||
else:
|
||||
strategy_dir = config["user_data_dir"] / USERPATH_STRATEGIES
|
||||
if not strategy_dir.is_dir():
|
||||
logger.info(f"Creating strategy directory {strategy_dir}")
|
||||
strategy_dir.mkdir(parents=True)
|
||||
new_path = strategy_dir / (args["strategy"] + ".py")
|
||||
new_path = strategy_dir / (strategy + ".py")
|
||||
|
||||
if new_path.exists():
|
||||
raise OperationalException(
|
||||
f"`{new_path}` already exists. Please choose another Strategy Name."
|
||||
)
|
||||
|
||||
deploy_new_strategy(args["strategy"], new_path, args["template"])
|
||||
deploy_new_strategy(strategy, new_path, args["template"])
|
||||
|
||||
else:
|
||||
raise ConfigurationError("`new-strategy` requires --strategy to be set.")
|
||||
@@ -116,7 +116,9 @@ def start_install_ui(args: dict[str, Any]) -> None:
|
||||
|
||||
dest_folder = Path(__file__).parents[1] / "rpc/api_server/ui/installed/"
|
||||
# First make sure the assets are removed.
|
||||
dl_url, latest_version = get_ui_download_url(args.get("ui_version"))
|
||||
dl_url, latest_version = get_ui_download_url(
|
||||
args.get("ui_version"), args.get("ui_prerelease", False)
|
||||
)
|
||||
|
||||
curr_version = read_ui_version(dest_folder)
|
||||
if curr_version == latest_version and not args.get("erase_ui_only"):
|
||||
|
||||
@@ -51,7 +51,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
|
||||
f.write(version)
|
||||
|
||||
|
||||
def get_ui_download_url(version: str | None = None) -> tuple[str, str]:
|
||||
def get_ui_download_url(version: str | None, prerelease: bool) -> tuple[str, str]:
|
||||
base_url = "https://api.github.com/repos/freqtrade/frequi/"
|
||||
# Get base UI Repo path
|
||||
|
||||
@@ -61,14 +61,18 @@ def get_ui_download_url(version: str | None = None) -> tuple[str, str]:
|
||||
|
||||
if version:
|
||||
tmp = [x for x in r if x["name"] == version]
|
||||
if tmp:
|
||||
latest_version = tmp[0]["name"]
|
||||
assets = tmp[0].get("assets", [])
|
||||
else:
|
||||
raise ValueError("UI-Version not found.")
|
||||
else:
|
||||
latest_version = r[0]["name"]
|
||||
assets = r[0].get("assets", [])
|
||||
tmp = [x for x in r if prerelease or not x.get("prerelease")]
|
||||
|
||||
if tmp:
|
||||
# Ensure we have the latest version
|
||||
if version is None:
|
||||
tmp.sort(key=lambda x: x["created_at"], reverse=True)
|
||||
latest_version = tmp[0]["name"]
|
||||
assets = tmp[0].get("assets", [])
|
||||
else:
|
||||
raise ValueError("UI-Version not found.")
|
||||
|
||||
dl_url = ""
|
||||
if assets and len(assets) > 0:
|
||||
dl_url = assets[0]["browser_download_url"]
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import Any
|
||||
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import ConfigurationError, OperationalException
|
||||
from freqtrade.ft_types import ValidExchangesType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -17,11 +16,12 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from freqtrade.exchange import list_available_exchanges
|
||||
from freqtrade.ft_types import ValidExchangesType
|
||||
from freqtrade.loggers.rich_console import get_rich_console
|
||||
|
||||
available_exchanges: list[ValidExchangesType] = list_available_exchanges(
|
||||
args["list_exchanges_all"]
|
||||
@@ -77,15 +77,16 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
|
||||
)
|
||||
# table.add_row(*[exchange[header] for header in headers])
|
||||
|
||||
console = Console()
|
||||
console = get_rich_console()
|
||||
console.print(table)
|
||||
|
||||
|
||||
def _print_objs_tabular(objs: list, print_colorized: bool) -> None:
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from freqtrade.loggers.rich_console import get_rich_console
|
||||
|
||||
names = [s["name"] for s in objs]
|
||||
objs_to_print: list[dict[str, Text | str]] = [
|
||||
{
|
||||
@@ -118,10 +119,7 @@ def _print_objs_tabular(objs: list, print_colorized: bool) -> None:
|
||||
for row in objs_to_print:
|
||||
table.add_row(*[row[header] for header in objs_to_print[0].keys()])
|
||||
|
||||
console = Console(
|
||||
color_system="auto" if print_colorized else None,
|
||||
width=200 if "pytest" in sys.modules else None,
|
||||
)
|
||||
console = get_rich_console(color_system="auto" if print_colorized else None)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@@ -219,7 +217,7 @@ def start_list_markets(args: dict[str, Any], pairs_only: bool = False) -> None:
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.exchange import market_is_active
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.misc import plural, safe_value_fallback
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
from freqtrade.util import print_rich_table
|
||||
|
||||
@@ -246,88 +244,99 @@ def start_list_markets(args: dict[str, Any], pairs_only: bool = False) -> None:
|
||||
except Exception as e:
|
||||
raise OperationalException(f"Cannot get markets. Reason: {e}") from e
|
||||
|
||||
else:
|
||||
summary_str = (
|
||||
(f"Exchange {exchange.name} has {len(pairs)} ")
|
||||
+ ("active " if active_only else "")
|
||||
+ (plural(len(pairs), "pair" if pairs_only else "market"))
|
||||
+ (
|
||||
f" with {', '.join(base_currencies)} as base "
|
||||
f"{plural(len(base_currencies), 'currency', 'currencies')}"
|
||||
if base_currencies
|
||||
else ""
|
||||
)
|
||||
+ (" and" if base_currencies and quote_currencies else "")
|
||||
+ (
|
||||
f" with {', '.join(quote_currencies)} as quote "
|
||||
f"{plural(len(quote_currencies), 'currency', 'currencies')}"
|
||||
if quote_currencies
|
||||
else ""
|
||||
)
|
||||
tickers = exchange.get_tickers()
|
||||
|
||||
summary_str = (
|
||||
(f"Exchange {exchange.name} has {len(pairs)} ")
|
||||
+ ("active " if active_only else "")
|
||||
+ (plural(len(pairs), "pair" if pairs_only else "market"))
|
||||
+ (
|
||||
f" with {', '.join(base_currencies)} as base "
|
||||
f"{plural(len(base_currencies), 'currency', 'currencies')}"
|
||||
if base_currencies
|
||||
else ""
|
||||
)
|
||||
+ (" and" if base_currencies and quote_currencies else "")
|
||||
+ (
|
||||
f" with {', '.join(quote_currencies)} as quote "
|
||||
f"{plural(len(quote_currencies), 'currency', 'currencies')}"
|
||||
if quote_currencies
|
||||
else ""
|
||||
)
|
||||
)
|
||||
|
||||
headers = [
|
||||
"Id",
|
||||
"Symbol",
|
||||
"Base",
|
||||
"Quote",
|
||||
"Active",
|
||||
"Spot",
|
||||
"Margin",
|
||||
"Future",
|
||||
"Leverage",
|
||||
]
|
||||
headers = [
|
||||
"Id",
|
||||
"Symbol",
|
||||
"Base",
|
||||
"Quote",
|
||||
"Active",
|
||||
"Spot",
|
||||
"Margin",
|
||||
"Future",
|
||||
"Leverage",
|
||||
"Min Stake",
|
||||
]
|
||||
|
||||
tabular_data = [
|
||||
{
|
||||
"Id": v["id"],
|
||||
"Symbol": v["symbol"],
|
||||
"Base": v["base"],
|
||||
"Quote": v["quote"],
|
||||
"Active": market_is_active(v),
|
||||
"Spot": "Spot" if exchange.market_is_spot(v) else "",
|
||||
"Margin": "Margin" if exchange.market_is_margin(v) else "",
|
||||
"Future": "Future" if exchange.market_is_future(v) else "",
|
||||
"Leverage": exchange.get_max_leverage(v["symbol"], 20),
|
||||
}
|
||||
for _, v in pairs.items()
|
||||
]
|
||||
tabular_data = [
|
||||
{
|
||||
"Id": v["id"],
|
||||
"Symbol": v["symbol"],
|
||||
"Base": v["base"],
|
||||
"Quote": v["quote"],
|
||||
"Active": market_is_active(v),
|
||||
"Spot": "Spot" if exchange.market_is_spot(v) else "",
|
||||
"Margin": "Margin" if exchange.market_is_margin(v) else "",
|
||||
"Future": "Future" if exchange.market_is_future(v) else "",
|
||||
"Leverage": exchange.get_max_leverage(v["symbol"], 20),
|
||||
"Min Stake": round(
|
||||
exchange.get_min_pair_stake_amount(
|
||||
v["symbol"],
|
||||
safe_value_fallback(tickers.get(v["symbol"], {}), "last", "ask", 0.0),
|
||||
0.0,
|
||||
)
|
||||
or 0.0,
|
||||
8,
|
||||
),
|
||||
}
|
||||
for _, v in pairs.items()
|
||||
]
|
||||
|
||||
if (
|
||||
args.get("print_one_column", False)
|
||||
or args.get("list_pairs_print_json", False)
|
||||
or args.get("print_csv", False)
|
||||
):
|
||||
# Print summary string in the log in case of machine-readable
|
||||
# regular formats.
|
||||
logger.info(f"{summary_str}.")
|
||||
if (
|
||||
args.get("print_one_column", False)
|
||||
or args.get("list_pairs_print_json", False)
|
||||
or args.get("print_csv", False)
|
||||
):
|
||||
# Print summary string in the log in case of machine-readable
|
||||
# regular formats.
|
||||
logger.info(f"{summary_str}.")
|
||||
else:
|
||||
# Print empty string separating leading logs and output in case of
|
||||
# human-readable formats.
|
||||
print()
|
||||
|
||||
if pairs:
|
||||
if args.get("print_list", False):
|
||||
# print data as a list, with human-readable summary
|
||||
print(f"{summary_str}: {', '.join(pairs.keys())}.")
|
||||
elif args.get("print_one_column", False):
|
||||
print("\n".join(pairs.keys()))
|
||||
elif args.get("list_pairs_print_json", False):
|
||||
import rapidjson
|
||||
|
||||
print(rapidjson.dumps(list(pairs.keys()), default=str))
|
||||
elif args.get("print_csv", False):
|
||||
writer = csv.DictWriter(sys.stdout, fieldnames=headers)
|
||||
writer.writeheader()
|
||||
writer.writerows(tabular_data)
|
||||
else:
|
||||
# Print empty string separating leading logs and output in case of
|
||||
# human-readable formats.
|
||||
print()
|
||||
|
||||
if pairs:
|
||||
if args.get("print_list", False):
|
||||
# print data as a list, with human-readable summary
|
||||
print(f"{summary_str}: {', '.join(pairs.keys())}.")
|
||||
elif args.get("print_one_column", False):
|
||||
print("\n".join(pairs.keys()))
|
||||
elif args.get("list_pairs_print_json", False):
|
||||
import rapidjson
|
||||
|
||||
print(rapidjson.dumps(list(pairs.keys()), default=str))
|
||||
elif args.get("print_csv", False):
|
||||
writer = csv.DictWriter(sys.stdout, fieldnames=headers)
|
||||
writer.writeheader()
|
||||
writer.writerows(tabular_data)
|
||||
else:
|
||||
print_rich_table(tabular_data, headers, summary_str)
|
||||
elif not (
|
||||
args.get("print_one_column", False)
|
||||
or args.get("list_pairs_print_json", False)
|
||||
or args.get("print_csv", False)
|
||||
):
|
||||
print(f"{summary_str}.")
|
||||
print_rich_table(tabular_data, headers, summary_str)
|
||||
elif not (
|
||||
args.get("print_one_column", False)
|
||||
or args.get("list_pairs_print_json", False)
|
||||
or args.get("print_csv", False)
|
||||
):
|
||||
print(f"{summary_str}.")
|
||||
|
||||
|
||||
def start_show_trades(args: dict[str, Any]) -> None:
|
||||
|
||||
4
freqtrade/config_schema/__init__.py
Normal file
4
freqtrade/config_schema/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from freqtrade.config_schema.config_schema import CONF_SCHEMA
|
||||
|
||||
|
||||
__all__ = ["CONF_SCHEMA"]
|
||||
@@ -425,6 +425,10 @@ CONF_SCHEMA = {
|
||||
"description": "Edge configuration.",
|
||||
"$ref": "#/definitions/edge",
|
||||
},
|
||||
"log_config": {
|
||||
"description": "Logging configuration.",
|
||||
"$ref": "#/definitions/logging",
|
||||
},
|
||||
"freqai": {
|
||||
"description": "FreqAI configuration.",
|
||||
"$ref": "#/definitions/freqai",
|
||||
@@ -471,6 +475,12 @@ CONF_SCHEMA = {
|
||||
"description": "Telegram topic ID - only applicable for group chats",
|
||||
"type": "string",
|
||||
},
|
||||
"authorized_users": {
|
||||
"description": "Authorized users for the bot.",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": True,
|
||||
},
|
||||
"allow_custom_messages": {
|
||||
"description": "Allow sending custom messages from the Strategy.",
|
||||
"type": "boolean",
|
||||
@@ -679,7 +689,7 @@ CONF_SCHEMA = {
|
||||
"initial_state": {
|
||||
"description": "Initial state of the system.",
|
||||
"type": "string",
|
||||
"enum": ["running", "stopped"],
|
||||
"enum": ["running", "paused", "stopped"],
|
||||
},
|
||||
"force_entry_enable": {
|
||||
"description": "Force enable entry.",
|
||||
@@ -877,6 +887,28 @@ CONF_SCHEMA = {
|
||||
},
|
||||
"required": ["process_throttle_secs", "allowed_risk"],
|
||||
},
|
||||
"logging": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {"type": "number", "const": 1},
|
||||
"formatters": {
|
||||
"type": "object",
|
||||
# In theory the below, but can be more flexible
|
||||
# based on logging.config documentation
|
||||
# "additionalProperties": {
|
||||
# "type": "object",
|
||||
# "properties": {
|
||||
# "format": {"type": "string"},
|
||||
# "datefmt": {"type": "string"},
|
||||
# },
|
||||
# "required": ["format"],
|
||||
# },
|
||||
},
|
||||
"handlers": {"type": "object"},
|
||||
"root": {"type": "object"},
|
||||
},
|
||||
"required": ["version", "formatters", "handlers", "root"],
|
||||
},
|
||||
"external_message_consumer": {
|
||||
"description": "Configuration for external message consumer.",
|
||||
"type": "object",
|
||||
@@ -965,10 +997,13 @@ CONF_SCHEMA = {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
},
|
||||
"keras": {
|
||||
"description": "Use Keras for model training.",
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"identifier": {
|
||||
"description": (
|
||||
"A unique ID for the current model. "
|
||||
"Must be changed when modifying features."
|
||||
),
|
||||
"type": "string",
|
||||
"default": "example",
|
||||
},
|
||||
"write_metrics_to_disk": {
|
||||
"description": "Write metrics to disk?",
|
||||
@@ -1000,13 +1035,42 @@ CONF_SCHEMA = {
|
||||
"type": "number",
|
||||
"default": 7,
|
||||
},
|
||||
"identifier": {
|
||||
"live_retrain_hours": {
|
||||
"description": "Frequency of retraining during dry/live runs.",
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
},
|
||||
"expiration_hours": {
|
||||
"description": (
|
||||
"A unique ID for the current model. "
|
||||
"Must be changed when modifying features."
|
||||
"Avoid making predictions if a model is more than `expiration_hours` "
|
||||
"old. Defaults to 0 (no expiration)."
|
||||
),
|
||||
"type": "string",
|
||||
"default": "example",
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
},
|
||||
"save_backtest_models": {
|
||||
"description": "Save models to disk when running backtesting.",
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
},
|
||||
"fit_live_predictions_candles": {
|
||||
"description": (
|
||||
"Number of historical candles to use for computing target (label) "
|
||||
"statistics from prediction data, instead of from the training dataset."
|
||||
),
|
||||
"type": "integer",
|
||||
},
|
||||
"data_kitchen_thread_count": {
|
||||
"description": (
|
||||
"Designate the number of threads you want to use for data processing "
|
||||
"(outlier methods, normalization, etc.)."
|
||||
),
|
||||
"type": "integer",
|
||||
},
|
||||
"activate_tensorboard": {
|
||||
"description": "Indicate whether or not to activate tensorboard",
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
},
|
||||
"wait_for_training_iteration_on_reload": {
|
||||
"description": (
|
||||
@@ -1015,6 +1079,20 @@ CONF_SCHEMA = {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
},
|
||||
"continual_learning": {
|
||||
"description": (
|
||||
"Use the final state of the most recently trained model "
|
||||
"as starting point for the new model, allowing for "
|
||||
"incremental learning."
|
||||
),
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
},
|
||||
"keras": {
|
||||
"description": "Use Keras for model training.",
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
},
|
||||
"feature_parameters": {
|
||||
"description": "The parameters used to engineer the feature set",
|
||||
"type": "object",
|
||||
@@ -1068,6 +1146,14 @@ CONF_SCHEMA = {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
},
|
||||
"indicator_periods_candles": {
|
||||
"description": (
|
||||
"Time periods to calculate indicators for. "
|
||||
"The indicators are added to the base indicator dataset."
|
||||
),
|
||||
"type": "array",
|
||||
"items": {"type": "number", "minimum": 1},
|
||||
},
|
||||
"use_SVM_to_remove_outliers": {
|
||||
"description": "Use SVM to remove outliers from the features.",
|
||||
"type": "boolean",
|
||||
@@ -1260,7 +1346,8 @@ SCHEMA_BACKTEST_REQUIRED = [
|
||||
"dataformat_ohlcv",
|
||||
"dataformat_trades",
|
||||
]
|
||||
SCHEMA_BACKTEST_REQUIRED_FINAL = SCHEMA_BACKTEST_REQUIRED + [
|
||||
SCHEMA_BACKTEST_REQUIRED_FINAL = [
|
||||
*SCHEMA_BACKTEST_REQUIRED,
|
||||
"stoploss",
|
||||
"minimal_roi",
|
||||
"max_open_trades",
|
||||
@@ -1272,6 +1359,4 @@ SCHEMA_MINIMAL_REQUIRED = [
|
||||
"dataformat_ohlcv",
|
||||
"dataformat_trades",
|
||||
]
|
||||
SCHEMA_MINIMAL_WEBSERVER = SCHEMA_MINIMAL_REQUIRED + [
|
||||
"api_server",
|
||||
]
|
||||
SCHEMA_MINIMAL_WEBSERVER = [*SCHEMA_MINIMAL_REQUIRED, "api_server"]
|
||||
@@ -6,7 +6,7 @@ from typing import Any
|
||||
from jsonschema import Draft4Validator, validators
|
||||
from jsonschema.exceptions import ValidationError, best_match
|
||||
|
||||
from freqtrade.configuration.config_schema import (
|
||||
from freqtrade.config_schema.config_schema import (
|
||||
CONF_SCHEMA,
|
||||
SCHEMA_BACKTEST_REQUIRED,
|
||||
SCHEMA_BACKTEST_REQUIRED_FINAL,
|
||||
@@ -361,7 +361,7 @@ def _validate_freqai_include_timeframes(conf: dict[str, Any], preliminary: bool)
|
||||
# Ensure that the base timeframe is included in the include_timeframes list
|
||||
if not preliminary and main_tf not in freqai_include_timeframes:
|
||||
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", {}).update(
|
||||
{**feature_parameters, "include_timeframes": include_timeframes}
|
||||
)
|
||||
|
||||
@@ -135,7 +135,7 @@ class Configuration:
|
||||
{"verbosity": safe_value_fallback(self.args, "verbosity", default_value=0)}
|
||||
)
|
||||
|
||||
if "logfile" in self.args and self.args["logfile"]:
|
||||
if self.args.get("logfile"):
|
||||
config.update({"logfile": self.args["logfile"]})
|
||||
|
||||
if "print_colorized" in self.args and not self.args["print_colorized"]:
|
||||
@@ -187,7 +187,7 @@ class Configuration:
|
||||
logger.warning("`force_entry_enable` RPC message enabled.")
|
||||
|
||||
# Support for sd_notify
|
||||
if "sd_notify" in self.args and self.args["sd_notify"]:
|
||||
if self.args.get("sd_notify"):
|
||||
config["internals"].update({"sd_notify": True})
|
||||
|
||||
def _process_datadir_options(self, config: Config) -> None:
|
||||
@@ -196,14 +196,14 @@ class Configuration:
|
||||
--user-data, --datadir
|
||||
"""
|
||||
# Check exchange parameter here - otherwise `datadir` might be wrong.
|
||||
if "exchange" in self.args and self.args["exchange"]:
|
||||
if self.args.get("exchange"):
|
||||
config["exchange"]["name"] = self.args["exchange"]
|
||||
logger.info(f"Using exchange {config['exchange']['name']}")
|
||||
|
||||
if "pair_whitelist" not in config["exchange"]:
|
||||
config["exchange"]["pair_whitelist"] = []
|
||||
|
||||
if "user_data_dir" in self.args and self.args["user_data_dir"]:
|
||||
if self.args.get("user_data_dir"):
|
||||
config.update({"user_data_dir": self.args["user_data_dir"]})
|
||||
elif "user_data_dir" not in config:
|
||||
# Default to cwd/user_data (legacy option ...)
|
||||
@@ -251,7 +251,7 @@ class Configuration:
|
||||
logstring="Parameter --enable-protections detected, enabling Protections. ...",
|
||||
)
|
||||
|
||||
if "max_open_trades" in self.args and self.args["max_open_trades"]:
|
||||
if self.args.get("max_open_trades"):
|
||||
config.update({"max_open_trades": self.args["max_open_trades"]})
|
||||
logger.info(
|
||||
"Parameter --max-open-trades detected, overriding max_open_trades to: %s ...",
|
||||
@@ -314,7 +314,7 @@ class Configuration:
|
||||
self._args_to_config_loop(config, configurations)
|
||||
|
||||
# Edge section:
|
||||
if "stoploss_range" in self.args and self.args["stoploss_range"]:
|
||||
if self.args.get("stoploss_range"):
|
||||
txt_range = ast.literal_eval(self.args["stoploss_range"])
|
||||
config["edge"].update({"stoploss_range_min": txt_range[0]})
|
||||
config["edge"].update({"stoploss_range_max": txt_range[1]})
|
||||
@@ -493,7 +493,7 @@ class Configuration:
|
||||
config["exchange"]["pair_whitelist"] = config["pairs"]
|
||||
return
|
||||
|
||||
if "pairs_file" in self.args and self.args["pairs_file"]:
|
||||
if self.args.get("pairs_file"):
|
||||
pairs_file = Path(self.args["pairs_file"])
|
||||
logger.info(f'Reading pairs file "{pairs_file}".')
|
||||
# Download pairs from the pairs file if no config is specified
|
||||
@@ -505,7 +505,7 @@ class Configuration:
|
||||
config["pairs"].sort()
|
||||
return
|
||||
|
||||
if "config" in self.args and self.args["config"]:
|
||||
if self.args.get("config"):
|
||||
logger.info("Using pairlist from configuration.")
|
||||
config["pairs"] = config.get("exchange", {}).get("pair_whitelist")
|
||||
else:
|
||||
|
||||
@@ -37,7 +37,7 @@ def chown_user_directory(directory: Path) -> None:
|
||||
"""
|
||||
if running_in_docker():
|
||||
try:
|
||||
import subprocess # noqa: S404
|
||||
import subprocess # noqa: S404, RUF100
|
||||
|
||||
subprocess.check_output(["sudo", "chown", "-R", "ftuser:", str(directory.resolve())])
|
||||
except Exception:
|
||||
|
||||
@@ -37,6 +37,7 @@ HYPEROPT_LOSS_BUILTIN = [
|
||||
"CalmarHyperOptLoss",
|
||||
"MaxDrawDownHyperOptLoss",
|
||||
"MaxDrawDownRelativeHyperOptLoss",
|
||||
"MaxDrawDownPerPairHyperOptLoss",
|
||||
"ProfitDrawDownHyperOptLoss",
|
||||
"MultiMetricHyperOptLoss",
|
||||
]
|
||||
@@ -59,7 +60,7 @@ AVAILABLE_PAIRLISTS = [
|
||||
"VolatilityFilter",
|
||||
]
|
||||
AVAILABLE_DATAHANDLERS = ["json", "jsongz", "feather", "parquet"]
|
||||
BACKTEST_BREAKDOWNS = ["day", "week", "month"]
|
||||
BACKTEST_BREAKDOWNS = ["day", "week", "month", "year"]
|
||||
BACKTEST_CACHE_AGE = ["none", "day", "week", "month"]
|
||||
BACKTEST_CACHE_DEFAULT = "day"
|
||||
DRY_RUN_WALLET = 1000
|
||||
@@ -70,6 +71,19 @@ DEFAULT_DATAFRAME_COLUMNS = ["date", "open", "high", "low", "close", "volume"]
|
||||
# it has wide consequences for stored trades files
|
||||
DEFAULT_TRADES_COLUMNS = ["timestamp", "id", "type", "side", "price", "amount", "cost"]
|
||||
DEFAULT_ORDERFLOW_COLUMNS = ["level", "bid", "ask", "delta"]
|
||||
ORDERFLOW_ADDED_COLUMNS = [
|
||||
"trades",
|
||||
"orderflow",
|
||||
"imbalances",
|
||||
"stacked_imbalances_bid",
|
||||
"stacked_imbalances_ask",
|
||||
"max_delta",
|
||||
"min_delta",
|
||||
"bid",
|
||||
"ask",
|
||||
"delta",
|
||||
"total_trades",
|
||||
]
|
||||
TRADES_DTYPES = {
|
||||
"timestamp": "int64",
|
||||
"id": "str",
|
||||
@@ -99,7 +113,7 @@ DL_DATA_TIMEFRAMES = ["1m", "5m"]
|
||||
ENV_VAR_PREFIX = "FREQTRADE__"
|
||||
|
||||
CANCELED_EXCHANGE_STATES = ("cancelled", "canceled", "expired", "rejected")
|
||||
NON_OPEN_EXCHANGE_STATES = CANCELED_EXCHANGE_STATES + ("closed",)
|
||||
NON_OPEN_EXCHANGE_STATES = (*CANCELED_EXCHANGE_STATES, "closed")
|
||||
|
||||
# Define decimals per coin for outputs
|
||||
# Only used for outputs.
|
||||
|
||||
31
freqtrade/data/btanalysis/__init__.py
Normal file
31
freqtrade/data/btanalysis/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# flake8: noqa: F401
|
||||
from .bt_fileutils import (
|
||||
BT_DATA_COLUMNS,
|
||||
delete_backtest_result,
|
||||
extract_trades_of_period,
|
||||
find_existing_backtest_stats,
|
||||
get_backtest_market_change,
|
||||
get_backtest_result,
|
||||
get_backtest_resultlist,
|
||||
get_latest_backtest_filename,
|
||||
get_latest_hyperopt_file,
|
||||
get_latest_hyperopt_filename,
|
||||
get_latest_optimize_filename,
|
||||
load_and_merge_backtest_result,
|
||||
load_backtest_analysis_data,
|
||||
load_backtest_data,
|
||||
load_backtest_metadata,
|
||||
load_backtest_stats,
|
||||
load_exit_signal_candles,
|
||||
load_file_from_zip,
|
||||
load_rejected_signals,
|
||||
load_signal_candles,
|
||||
load_trades,
|
||||
load_trades_from_db,
|
||||
trade_list_to_dataframe,
|
||||
update_backtest_metadata,
|
||||
)
|
||||
from .trade_parallelism import (
|
||||
analyze_trade_parallelism,
|
||||
evaluate_result_multi,
|
||||
)
|
||||
@@ -13,7 +13,7 @@ from typing import Any, Literal
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf
|
||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||
from freqtrade.exceptions import ConfigurationError, OperationalException
|
||||
from freqtrade.ft_types import BacktestHistoryEntryType, BacktestResultType
|
||||
from freqtrade.misc import file_dump_json, json_load
|
||||
@@ -376,7 +376,7 @@ def load_backtest_data(filename: Path | str, strategy: str | None = None) -> pd.
|
||||
|
||||
if not strategy:
|
||||
if len(data["strategy"]) == 1:
|
||||
strategy = list(data["strategy"].keys())[0]
|
||||
strategy = next(iter(data["strategy"].keys()))
|
||||
else:
|
||||
raise ValueError(
|
||||
"Detected backtest result with more than one strategy. "
|
||||
@@ -491,55 +491,6 @@ def load_exit_signal_candles(backtest_dir: Path) -> dict[str, dict[str, pd.DataF
|
||||
return load_backtest_analysis_data(backtest_dir, "exited")
|
||||
|
||||
|
||||
def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataFrame:
|
||||
"""
|
||||
Find overlapping trades by expanding each trade once per period it was open
|
||||
and then counting overlaps.
|
||||
:param results: Results Dataframe - can be loaded
|
||||
:param timeframe: Timeframe used for backtest
|
||||
:return: dataframe with open-counts per time-period in timeframe
|
||||
"""
|
||||
from freqtrade.exchange import timeframe_to_resample_freq
|
||||
|
||||
timeframe_freq = timeframe_to_resample_freq(timeframe)
|
||||
dates = [
|
||||
pd.Series(
|
||||
pd.date_range(
|
||||
row[1]["open_date"],
|
||||
row[1]["close_date"],
|
||||
freq=timeframe_freq,
|
||||
# Exclude right boundary - the date is the candle open date.
|
||||
inclusive="left",
|
||||
)
|
||||
)
|
||||
for row in results[["open_date", "close_date"]].iterrows()
|
||||
]
|
||||
deltas = [len(x) for x in dates]
|
||||
dates = pd.Series(pd.concat(dates).values, name="date")
|
||||
df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns)
|
||||
|
||||
df2 = pd.concat([dates, df2], axis=1)
|
||||
df2 = df2.set_index("date")
|
||||
df_final = df2.resample(timeframe_freq)[["pair"]].count()
|
||||
df_final = df_final.rename({"pair": "open_trades"}, axis=1)
|
||||
return df_final
|
||||
|
||||
|
||||
def evaluate_result_multi(
|
||||
results: pd.DataFrame, timeframe: str, max_open_trades: IntOrInf
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Find overlapping trades by expanding each trade once per period it was open
|
||||
and then counting overlaps
|
||||
:param results: Results Dataframe - can be loaded
|
||||
:param timeframe: Frequency used for the backtest
|
||||
:param max_open_trades: parameter max_open_trades used during backtest run
|
||||
:return: dataframe with open-counts per time-period in freq
|
||||
"""
|
||||
df_final = analyze_trade_parallelism(results, timeframe)
|
||||
return df_final[df_final["open_trades"] > max_open_trades]
|
||||
|
||||
|
||||
def trade_list_to_dataframe(trades: list[Trade] | list[LocalTrade]) -> pd.DataFrame:
|
||||
"""
|
||||
Convert list of Trade objects to pandas Dataframe
|
||||
60
freqtrade/data/btanalysis/trade_parallelism.py
Normal file
60
freqtrade/data/btanalysis/trade_parallelism.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.constants import IntOrInf
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def analyze_trade_parallelism(trades: pd.DataFrame, timeframe: str) -> pd.DataFrame:
|
||||
"""
|
||||
Find overlapping trades by expanding each trade once per period it was open
|
||||
and then counting overlaps.
|
||||
:param trades: Trades Dataframe - can be loaded from backtest, or created
|
||||
via trade_list_to_dataframe
|
||||
:param timeframe: Timeframe used for backtest
|
||||
:return: dataframe with open-counts per time-period in timeframe
|
||||
"""
|
||||
from freqtrade.exchange import timeframe_to_resample_freq
|
||||
|
||||
timeframe_freq = timeframe_to_resample_freq(timeframe)
|
||||
dates = [
|
||||
pd.Series(
|
||||
pd.date_range(
|
||||
row[1]["open_date"],
|
||||
row[1]["close_date"],
|
||||
freq=timeframe_freq,
|
||||
# Exclude right boundary - the date is the candle open date.
|
||||
inclusive="left",
|
||||
)
|
||||
)
|
||||
for row in trades[["open_date", "close_date"]].iterrows()
|
||||
]
|
||||
deltas = [len(x) for x in dates]
|
||||
dates = pd.Series(pd.concat(dates).values, name="date")
|
||||
df2 = pd.DataFrame(np.repeat(trades.values, deltas, axis=0), columns=trades.columns)
|
||||
|
||||
df2 = pd.concat([dates, df2], axis=1)
|
||||
df2 = df2.set_index("date")
|
||||
df_final = df2.resample(timeframe_freq)[["pair"]].count()
|
||||
df_final = df_final.rename({"pair": "open_trades"}, axis=1)
|
||||
return df_final
|
||||
|
||||
|
||||
def evaluate_result_multi(
|
||||
trades: pd.DataFrame, timeframe: str, max_open_trades: IntOrInf
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Find overlapping trades by expanding each trade once per period it was open
|
||||
and then counting overlaps
|
||||
:param trades: Trades Dataframe - can be loaded from backtest, or created
|
||||
via trade_list_to_dataframe
|
||||
:param timeframe: Frequency used for the backtest
|
||||
:param max_open_trades: parameter max_open_trades used during backtest run
|
||||
:return: dataframe with open-counts per time-period in freq
|
||||
"""
|
||||
df_final = analyze_trade_parallelism(trades, timeframe)
|
||||
return df_final[df_final["open_trades"] > max_open_trades]
|
||||
@@ -9,26 +9,12 @@ from datetime import datetime
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.constants import DEFAULT_ORDERFLOW_COLUMNS, Config
|
||||
from freqtrade.constants import DEFAULT_ORDERFLOW_COLUMNS, ORDERFLOW_ADDED_COLUMNS, Config
|
||||
from freqtrade.exceptions import DependencyException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ORDERFLOW_ADDED_COLUMNS = [
|
||||
"trades",
|
||||
"orderflow",
|
||||
"imbalances",
|
||||
"stacked_imbalances_bid",
|
||||
"stacked_imbalances_ask",
|
||||
"max_delta",
|
||||
"min_delta",
|
||||
"bid",
|
||||
"ask",
|
||||
"delta",
|
||||
"total_trades",
|
||||
]
|
||||
|
||||
|
||||
def _init_dataframe_with_trades_columns(dataframe: pd.DataFrame):
|
||||
"""
|
||||
|
||||
@@ -49,7 +49,7 @@ class DataProvider:
|
||||
self._pairlists = pairlists
|
||||
self.__rpc = rpc
|
||||
self.__cached_pairs: dict[PairWithTimeframe, tuple[DataFrame, datetime]] = {}
|
||||
self.__slice_index: int | None = None
|
||||
self.__slice_index: dict[str, int] = {}
|
||||
self.__slice_date: datetime | None = None
|
||||
|
||||
self.__cached_pairs_backtesting: dict[PairWithTimeframe, DataFrame] = {}
|
||||
@@ -69,13 +69,13 @@ class DataProvider:
|
||||
self.producers = self._config.get("external_message_consumer", {}).get("producers", [])
|
||||
self.external_data_enabled = len(self.producers) > 0
|
||||
|
||||
def _set_dataframe_max_index(self, limit_index: int):
|
||||
def _set_dataframe_max_index(self, pair: str, limit_index: int):
|
||||
"""
|
||||
Limit analyzed dataframe to max specified index.
|
||||
Only relevant in backtesting.
|
||||
:param limit_index: dataframe index.
|
||||
"""
|
||||
self.__slice_index = limit_index
|
||||
self.__slice_index[pair] = limit_index
|
||||
|
||||
def _set_dataframe_max_date(self, limit_date: datetime):
|
||||
"""
|
||||
@@ -393,9 +393,10 @@ class DataProvider:
|
||||
df, date = self.__cached_pairs[pair_key]
|
||||
else:
|
||||
df, date = self.__cached_pairs[pair_key]
|
||||
if self.__slice_index is not None:
|
||||
max_index = self.__slice_index
|
||||
if (max_index := self.__slice_index.get(pair)) is not None:
|
||||
df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES) : max_index]
|
||||
else:
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
||||
return df, date
|
||||
else:
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
||||
@@ -430,7 +431,7 @@ class DataProvider:
|
||||
# Don't reset backtesting pairs -
|
||||
# otherwise they're reloaded each time during hyperopt due to with analyze_per_epoch
|
||||
# self.__cached_pairs_backtesting = {}
|
||||
self.__slice_index = 0
|
||||
self.__slice_index = {}
|
||||
|
||||
# Exchange functions
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ def _merge_dfs(
|
||||
):
|
||||
merge_on = ["pair", "open_date"]
|
||||
signal_wide_indicators = list(set(available_inds) - set(BT_DATA_COLUMNS))
|
||||
columns_to_keep = merge_on + ["enter_reason", "exit_reason"]
|
||||
columns_to_keep = [*merge_on, "enter_reason", "exit_reason"]
|
||||
|
||||
if exit_df is None or exit_df.empty or entry_only is True:
|
||||
return entry_df[columns_to_keep + available_inds]
|
||||
|
||||
@@ -116,7 +116,7 @@ def load_data(
|
||||
result[pair] = hist
|
||||
else:
|
||||
if candle_type is CandleType.FUNDING_RATE and user_futures_funding_rate is not None:
|
||||
logger.warn(f"{pair} using user specified [{user_futures_funding_rate}]")
|
||||
logger.warning(f"{pair} using user specified [{user_futures_funding_rate}]")
|
||||
elif candle_type not in (CandleType.SPOT, CandleType.FUTURES):
|
||||
result[pair] = DataFrame(columns=["date", "open", "close", "high", "low", "volume"])
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ import pandas as pd
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def calculate_market_change(data: dict[str, pd.DataFrame], column: str = "close") -> float:
|
||||
def calculate_market_change(
|
||||
data: dict[str, pd.DataFrame], column: str = "close", min_date: datetime | None = None
|
||||
) -> float:
|
||||
"""
|
||||
Calculate market change based on "column".
|
||||
Calculation is done by taking the first non-null and the last non-null element of each column
|
||||
@@ -19,14 +21,24 @@ def calculate_market_change(data: dict[str, pd.DataFrame], column: str = "close"
|
||||
|
||||
:param data: Dict of Dataframes, dict key should be pair.
|
||||
:param column: Column in the original dataframes to use
|
||||
:param min_date: Minimum date to consider for calculations. Market change should only be
|
||||
calculated for data actually backtested, excluding startup periods.
|
||||
:return:
|
||||
"""
|
||||
tmp_means = []
|
||||
for pair, df in data.items():
|
||||
start = df[column].dropna().iloc[0]
|
||||
end = df[column].dropna().iloc[-1]
|
||||
df1 = df
|
||||
if min_date is not None:
|
||||
df1 = df1[df1["date"] >= min_date]
|
||||
if df1.empty:
|
||||
logger.warning(f"Pair {pair} has no data after {min_date}.")
|
||||
continue
|
||||
start = df1[column].dropna().iloc[0]
|
||||
end = df1[column].dropna().iloc[-1]
|
||||
tmp_means.append((end - start) / start)
|
||||
|
||||
if not tmp_means:
|
||||
return 0.0
|
||||
return float(np.mean(tmp_means))
|
||||
|
||||
|
||||
@@ -118,7 +130,7 @@ def _calc_drawdown_series(
|
||||
) -> pd.DataFrame:
|
||||
max_drawdown_df = pd.DataFrame()
|
||||
max_drawdown_df["cumulative"] = profit_results[value_col].cumsum()
|
||||
max_drawdown_df["high_value"] = max_drawdown_df["cumulative"].cummax()
|
||||
max_drawdown_df["high_value"] = np.maximum(0, max_drawdown_df["cumulative"].cummax())
|
||||
max_drawdown_df["drawdown"] = max_drawdown_df["cumulative"] - max_drawdown_df["high_value"]
|
||||
max_drawdown_df["date"] = profit_results.loc[:, date_col]
|
||||
if starting_balance:
|
||||
@@ -201,13 +213,11 @@ def calculate_max_drawdown(
|
||||
if relative
|
||||
else max_drawdown_df["drawdown"].idxmin()
|
||||
)
|
||||
if idxmin == 0:
|
||||
raise ValueError("No losing trade, therefore no drawdown.")
|
||||
high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]["high_value"].idxmax(), date_col]
|
||||
|
||||
high_idx = max_drawdown_df.iloc[: idxmin + 1]["high_value"].idxmax()
|
||||
high_date = profit_results.loc[high_idx, date_col]
|
||||
low_date = profit_results.loc[idxmin, date_col]
|
||||
high_val = max_drawdown_df.loc[
|
||||
max_drawdown_df.iloc[:idxmin]["high_value"].idxmax(), "cumulative"
|
||||
]
|
||||
high_val = max_drawdown_df.loc[high_idx, "cumulative"]
|
||||
low_val = max_drawdown_df.loc[idxmin, "cumulative"]
|
||||
max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"]
|
||||
|
||||
@@ -375,3 +385,32 @@ def calculate_calmar(
|
||||
|
||||
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||
return calmar_ratio
|
||||
|
||||
|
||||
def calculate_sqn(trades: pd.DataFrame, starting_balance: float) -> float:
|
||||
"""
|
||||
Calculate System Quality Number (SQN) - Van K. Tharp.
|
||||
SQN measures systematic trading quality and takes into account both
|
||||
the number of trades and their standard deviation.
|
||||
|
||||
:param trades: DataFrame containing trades (requires column profit_abs)
|
||||
:param starting_balance: Starting balance of the trading system
|
||||
:return: SQN value
|
||||
"""
|
||||
if len(trades) == 0:
|
||||
return 0.0
|
||||
|
||||
total_profit = trades["profit_abs"] / starting_balance
|
||||
number_of_trades = len(trades)
|
||||
|
||||
# Calculate average trade and standard deviation
|
||||
average_profits = total_profit.mean()
|
||||
profits_std = total_profit.std()
|
||||
|
||||
if profits_std != 0 and not np.isnan(profits_std):
|
||||
sqn = math.sqrt(number_of_trades) * (average_profits / profits_std)
|
||||
else:
|
||||
# Define negative SQN to indicate this is NOT optimal
|
||||
sqn = -100.0
|
||||
|
||||
return round(sqn, 4)
|
||||
|
||||
@@ -7,8 +7,9 @@ class State(Enum):
|
||||
"""
|
||||
|
||||
RUNNING = 1
|
||||
STOPPED = 2
|
||||
RELOAD_CONFIG = 3
|
||||
PAUSED = 2
|
||||
STOPPED = 3
|
||||
RELOAD_CONFIG = 4
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
|
||||
@@ -11,7 +11,11 @@ from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
||||
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.binance_public_data import concat_safe, download_archive_ohlcv
|
||||
from freqtrade.exchange.binance_public_data import (
|
||||
concat_safe,
|
||||
download_archive_ohlcv,
|
||||
download_archive_trades,
|
||||
)
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_types import FtHas, Tickers
|
||||
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_msecs
|
||||
@@ -139,7 +143,7 @@ class Binance(Exchange):
|
||||
Does not work for other exchanges, which don't return the earliest data when called with "0"
|
||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||
"""
|
||||
if is_new_pair:
|
||||
if is_new_pair and candle_type in (CandleType.SPOT, CandleType.FUTURES, CandleType.MARK):
|
||||
with self._loop_lock:
|
||||
x = self.loop.run_until_complete(
|
||||
self._async_get_candle_history(pair, timeframe, candle_type, 0)
|
||||
@@ -270,12 +274,12 @@ class Binance(Exchange):
|
||||
def dry_run_liquidation_price(
|
||||
self,
|
||||
pair: str,
|
||||
open_rate: float, # Entry price of position
|
||||
open_rate: float,
|
||||
is_short: bool,
|
||||
amount: float,
|
||||
stake_amount: float,
|
||||
leverage: float,
|
||||
wallet_balance: float, # Or margin balance
|
||||
wallet_balance: float,
|
||||
open_trades: list,
|
||||
) -> float | None:
|
||||
"""
|
||||
@@ -289,8 +293,6 @@ class Binance(Exchange):
|
||||
:param amount: Absolute value of position size incl. leverage (in base currency)
|
||||
:param stake_amount: Stake amount - Collateral in settle currency.
|
||||
:param leverage: Leverage used for this position.
|
||||
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
|
||||
:param margin_mode: Either ISOLATED or CROSS
|
||||
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||
Cross-Margin Mode: crossWalletBalance
|
||||
Isolated-Margin Mode: isolatedWalletBalance
|
||||
@@ -379,3 +381,48 @@ class Binance(Exchange):
|
||||
if not t:
|
||||
return [], "0"
|
||||
return t, from_id
|
||||
|
||||
async def _async_get_trade_history_id(
|
||||
self, pair: str, until: int, since: int, from_id: str | None = None
|
||||
) -> tuple[str, list[list]]:
|
||||
logger.info(f"Fetching trades from Binance, {from_id=}, {since=}, {until=}")
|
||||
|
||||
if not self._config["exchange"].get("only_from_ccxt", False):
|
||||
if from_id is None or not since:
|
||||
trades = await self._api_async.fetch_trades(
|
||||
pair,
|
||||
params={
|
||||
self._trades_pagination_arg: "0",
|
||||
},
|
||||
limit=5,
|
||||
)
|
||||
listing_date: int = trades[0]["timestamp"]
|
||||
since = max(since, listing_date)
|
||||
|
||||
_, res = await download_archive_trades(
|
||||
CandleType.SPOT,
|
||||
pair,
|
||||
since_ms=since,
|
||||
until_ms=until,
|
||||
markets=self.markets,
|
||||
)
|
||||
|
||||
if not res:
|
||||
end_time = since
|
||||
end_id = from_id
|
||||
else:
|
||||
end_time = res[-1][0]
|
||||
end_id = res[-1][1]
|
||||
|
||||
if end_time and end_time >= until:
|
||||
return pair, res
|
||||
else:
|
||||
_, res2 = await super()._async_get_trade_history_id(
|
||||
pair, until=until, since=end_time, from_id=end_id
|
||||
)
|
||||
res.extend(res2)
|
||||
return pair, res
|
||||
|
||||
return await super()._async_get_trade_history_id(
|
||||
pair, until=until, since=since, from_id=from_id
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
"""
|
||||
Fetch daily-archived OHLCV data from https://data.binance.vision/
|
||||
Documentation can be found in https://github.com/binance/binance-public-data
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -10,9 +11,11 @@ from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import DEFAULT_TRADES_COLUMNS
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.misc import chunks
|
||||
from freqtrade.util.datetime_helpers import dt_from_ts, dt_now
|
||||
@@ -157,8 +160,8 @@ async def _download_archive_ohlcv(
|
||||
return concat_safe(dfs)
|
||||
else:
|
||||
dfs.append(None)
|
||||
except BaseException as e:
|
||||
logger.warning(f"An exception raised: : {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"An exception raised: {e}")
|
||||
# Directly return the existing data, do not allow the gap within the data
|
||||
await cancel_and_await_tasks(tasks[tasks.index(task) + 1 :])
|
||||
return concat_safe(dfs)
|
||||
@@ -212,6 +215,20 @@ def binance_vision_ohlcv_zip_url(
|
||||
return url
|
||||
|
||||
|
||||
def binance_vision_trades_zip_url(symbol: str, candle_type: CandleType, date: date) -> str:
|
||||
"""
|
||||
example urls:
|
||||
https://data.binance.vision/data/spot/daily/aggTrades/BTCUSDT/BTCUSDT-aggTrades-2023-10-27.zip
|
||||
https://data.binance.vision/data/futures/um/daily/aggTrades/BTCUSDT/BTCUSDT-aggTrades-2023-10-27.zip
|
||||
"""
|
||||
asset_type_url_segment = candle_type_to_url_segment(candle_type)
|
||||
url = (
|
||||
f"https://data.binance.vision/data/{asset_type_url_segment}/daily/aggTrades/{symbol}"
|
||||
f"/{symbol}-aggTrades-{date.strftime('%Y-%m-%d')}.zip"
|
||||
)
|
||||
return url
|
||||
|
||||
|
||||
async def get_daily_ohlcv(
|
||||
symbol: str,
|
||||
timeframe: str,
|
||||
@@ -268,7 +285,11 @@ async def get_daily_ohlcv(
|
||||
names=["date", "open", "high", "low", "close", "volume"],
|
||||
header=header,
|
||||
)
|
||||
df["date"] = pd.to_datetime(df["date"], unit="ms", utc=True)
|
||||
df["date"] = pd.to_datetime(
|
||||
np.where(df["date"] > 1e13, df["date"] // 1000, df["date"]),
|
||||
unit="ms",
|
||||
utc=True,
|
||||
)
|
||||
return df
|
||||
elif resp.status == 404:
|
||||
logger.debug(f"Failed to download {url}")
|
||||
@@ -280,3 +301,203 @@ async def get_daily_ohlcv(
|
||||
if isinstance(e, Http404) or retry > retry_count:
|
||||
logger.debug(f"Failed to get data from {url}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def download_archive_trades(
|
||||
candle_type: CandleType,
|
||||
pair: str,
|
||||
*,
|
||||
since_ms: int,
|
||||
until_ms: int | None,
|
||||
markets: dict[str, Any],
|
||||
stop_on_404: bool = True,
|
||||
) -> tuple[str, list[list]]:
|
||||
try:
|
||||
symbol = markets[pair]["id"]
|
||||
|
||||
last_available_date = dt_now() - timedelta(days=2)
|
||||
|
||||
start = dt_from_ts(since_ms)
|
||||
end = dt_from_ts(until_ms) if until_ms else dt_now()
|
||||
end = min(end, last_available_date)
|
||||
if start >= end:
|
||||
return pair, []
|
||||
result_list = await _download_archive_trades(
|
||||
symbol, pair, candle_type, start, end, stop_on_404
|
||||
)
|
||||
return pair, result_list
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"An exception occurred during fast trades download from Binance, falling back to "
|
||||
"the slower REST API, this can take a lot more time.",
|
||||
exc_info=e,
|
||||
)
|
||||
return pair, []
|
||||
|
||||
|
||||
def parse_trades_from_zip(csvf):
|
||||
# https://github.com/binance/binance-public-data/issues/283
|
||||
first_byte = csvf.read(1)[0]
|
||||
if chr(first_byte).isdigit():
|
||||
# spot
|
||||
header = None
|
||||
names = [
|
||||
"id",
|
||||
"price",
|
||||
"amount",
|
||||
"first_trade_id",
|
||||
"last_trade_id",
|
||||
"timestamp",
|
||||
"is_buyer_maker",
|
||||
"is_best_match",
|
||||
]
|
||||
else:
|
||||
# futures
|
||||
header = 0
|
||||
names = [
|
||||
"id",
|
||||
"price",
|
||||
"amount",
|
||||
"first_trade_id",
|
||||
"last_trade_id",
|
||||
"timestamp",
|
||||
"is_buyer_maker",
|
||||
]
|
||||
csvf.seek(0)
|
||||
|
||||
df = pd.read_csv(
|
||||
csvf,
|
||||
names=names,
|
||||
header=header,
|
||||
)
|
||||
df.loc[:, "cost"] = df["price"] * df["amount"]
|
||||
# Side is reversed intentionally
|
||||
# based on ccxt parseTrade logic.
|
||||
df.loc[:, "side"] = np.where(df["is_buyer_maker"], "sell", "buy")
|
||||
df.loc[:, "type"] = None
|
||||
# Convert timestamp to ms
|
||||
df.loc[:, "timestamp"] = np.where(
|
||||
df["timestamp"] > 1e13,
|
||||
df["timestamp"] // 1000,
|
||||
df["timestamp"],
|
||||
)
|
||||
return df.loc[:, DEFAULT_TRADES_COLUMNS].to_records(index=False).tolist()
|
||||
|
||||
|
||||
async def get_daily_trades(
|
||||
symbol: str,
|
||||
candle_type: CandleType,
|
||||
date: date,
|
||||
session: aiohttp.ClientSession,
|
||||
retry_count: int = 3,
|
||||
retry_delay: float = 0.0,
|
||||
) -> list[list]:
|
||||
"""
|
||||
Get daily OHLCV from https://data.binance.vision
|
||||
See https://github.com/binance/binance-public-data
|
||||
|
||||
:symbol: binance symbol name, e.g. BTCUSDT
|
||||
:candle_type: SPOT or FUTURES
|
||||
:date: the returned DataFrame will cover the entire day of `date` in UTC
|
||||
:session: an aiohttp.ClientSession instance
|
||||
:retry_count: times to retry before returning the exceptions
|
||||
:retry_delay: the time to wait before every retry
|
||||
:return: a list containing trades in DEFAULT_TRADES_COLUMNS format
|
||||
"""
|
||||
|
||||
url = binance_vision_trades_zip_url(symbol, candle_type, date)
|
||||
|
||||
logger.debug(f"download trades data from binance: {url}")
|
||||
|
||||
retry = 0
|
||||
while True:
|
||||
if retry > 0:
|
||||
sleep_secs = retry * retry_delay
|
||||
logger.debug(
|
||||
f"[{retry}/{retry_count}] retry to download {url} after {sleep_secs} seconds"
|
||||
)
|
||||
await asyncio.sleep(sleep_secs)
|
||||
try:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status == 200:
|
||||
content = await resp.read()
|
||||
logger.debug(f"Successfully downloaded {url}")
|
||||
with zipfile.ZipFile(BytesIO(content)) as zipf:
|
||||
with zipf.open(zipf.namelist()[0]) as csvf:
|
||||
return parse_trades_from_zip(csvf)
|
||||
elif resp.status == 404:
|
||||
logger.debug(f"Failed to download {url}")
|
||||
raise Http404(f"404: {url}", date, url)
|
||||
else:
|
||||
raise BadHttpStatus(f"{resp.status} - {resp.reason}")
|
||||
except Exception as e:
|
||||
logger.info("download Daily_trades raised: %s", e)
|
||||
retry += 1
|
||||
if isinstance(e, Http404) or retry > retry_count:
|
||||
logger.debug(f"Failed to get data from {url}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def _download_archive_trades(
|
||||
symbol: str,
|
||||
pair: str,
|
||||
candle_type: CandleType,
|
||||
start: date,
|
||||
end: date,
|
||||
stop_on_404: bool,
|
||||
) -> list[list]:
|
||||
# daily dataframes, `None` indicates missing data in that day (when `stop_on_404` is False)
|
||||
results: list[list] = []
|
||||
# the current day being processing, starting at 1.
|
||||
current_day = 0
|
||||
|
||||
connector = aiohttp.TCPConnector(limit=100)
|
||||
async with aiohttp.ClientSession(connector=connector, trust_env=True) as session:
|
||||
# the HTTP connections has been throttled by TCPConnector
|
||||
for dates in chunks(list(date_range(start, end)), 30):
|
||||
tasks = [
|
||||
asyncio.create_task(get_daily_trades(symbol, candle_type, date, session))
|
||||
for date in dates
|
||||
]
|
||||
for task in tasks:
|
||||
current_day += 1
|
||||
try:
|
||||
result = await task
|
||||
except Http404 as e:
|
||||
if stop_on_404:
|
||||
logger.debug(f"Failed to download {e.url} due to 404.")
|
||||
|
||||
# A 404 error on the first day indicates missing data
|
||||
# on https://data.binance.vision, we provide the warning and the advice.
|
||||
# https://github.com/freqtrade/freqtrade/blob/acc53065e5fa7ab5197073276306dc9dc3adbfa3/tests/exchange_online/test_binance_compare_ohlcv.py#L7
|
||||
if current_day == 1:
|
||||
logger.warning(
|
||||
f"Fast download is unavailable due to missing data: "
|
||||
f"{e.url}. Falling back to the slower REST API, "
|
||||
"which may take more time."
|
||||
)
|
||||
if pair in ["BTC/USDT:USDT", "ETH/USDT:USDT", "BCH/USDT:USDT"]:
|
||||
logger.warning(
|
||||
f"To avoid the delay, you can first download {pair} using "
|
||||
"`--timerange <start date>-20200101`, and then download the "
|
||||
"remaining data with `--timerange 20200101-<end date>`."
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Binance fast download for {pair} stopped at {e.date} due to "
|
||||
f"missing data: {e.url}, falling back to rest API for the "
|
||||
"remaining data, this can take more time."
|
||||
)
|
||||
await cancel_and_await_tasks(tasks[tasks.index(task) + 1 :])
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.warning(f"An exception raised: {e}")
|
||||
# Directly return the existing data, do not allow the gap within the data
|
||||
await cancel_and_await_tasks(tasks[tasks.index(task) + 1 :])
|
||||
return results
|
||||
else:
|
||||
# Happy case
|
||||
results.extend(result)
|
||||
|
||||
return results
|
||||
|
||||
@@ -166,15 +166,16 @@ class Bybit(Exchange):
|
||||
PERPETUAL:
|
||||
bybit:
|
||||
https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
|
||||
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#b
|
||||
|
||||
Long:
|
||||
Liquidation Price = (
|
||||
Entry Price * (1 - Initial Margin Rate + Maintenance Margin Rate)
|
||||
- Extra Margin Added/ Contract)
|
||||
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||
- (Extra Margin Added/Contract Quantity))
|
||||
Short:
|
||||
Liquidation Price = (
|
||||
Entry Price * (1 + Initial Margin Rate - Maintenance Margin Rate)
|
||||
+ Extra Margin Added/ Contract)
|
||||
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||
+ (Extra Margin Added/Contract Quantity))
|
||||
|
||||
Implementation Note: Extra margin is currently not used.
|
||||
|
||||
@@ -184,8 +185,6 @@ class Bybit(Exchange):
|
||||
:param amount: Absolute value of position size incl. leverage (in base currency)
|
||||
:param stake_amount: Stake amount - Collateral in settle currency.
|
||||
:param leverage: Leverage used for this position.
|
||||
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
|
||||
:param margin_mode: Either ISOLATED or CROSS
|
||||
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||
Cross-Margin Mode: crossWalletBalance
|
||||
Isolated-Margin Mode: isolatedWalletBalance
|
||||
@@ -198,13 +197,16 @@ class Bybit(Exchange):
|
||||
if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
|
||||
if market["inverse"]:
|
||||
raise OperationalException("Freqtrade does not yet support inverse contracts")
|
||||
initial_margin_rate = 1 / leverage
|
||||
position_value = amount * open_rate
|
||||
initial_margin = position_value / leverage
|
||||
maintenance_margin = position_value * mm_ratio
|
||||
margin_diff_per_contract = (initial_margin - maintenance_margin) / amount
|
||||
|
||||
# See docstring - ignores extra margin!
|
||||
if is_short:
|
||||
return open_rate * (1 + initial_margin_rate - mm_ratio)
|
||||
return open_rate + margin_diff_per_contract
|
||||
else:
|
||||
return open_rate * (1 - initial_margin_rate + mm_ratio)
|
||||
return open_rate - margin_diff_per_contract
|
||||
|
||||
else:
|
||||
raise OperationalException(
|
||||
|
||||
@@ -148,6 +148,7 @@ class Exchange:
|
||||
"trades_has_history": False,
|
||||
"l2_limit_range": None,
|
||||
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
|
||||
"l2_limit_upper": None, # Upper limit for L2 limit
|
||||
"mark_ohlcv_price": "mark",
|
||||
"mark_ohlcv_timeframe": "8h",
|
||||
"funding_fee_timeframe": "8h",
|
||||
@@ -960,7 +961,7 @@ class Exchange:
|
||||
return 1 / pow(10, precision)
|
||||
|
||||
def get_min_pair_stake_amount(
|
||||
self, pair: str, price: float, stoploss: float, leverage: float | None = 1.0
|
||||
self, pair: str, price: float, stoploss: float, leverage: float = 1.0
|
||||
) -> float | None:
|
||||
return self._get_stake_amount_limit(pair, price, stoploss, "min", leverage)
|
||||
|
||||
@@ -979,7 +980,7 @@ class Exchange:
|
||||
price: float,
|
||||
stoploss: float,
|
||||
limit: Literal["min", "max"],
|
||||
leverage: float | None = 1.0,
|
||||
leverage: float = 1.0,
|
||||
) -> float | None:
|
||||
isMin = limit == "min"
|
||||
|
||||
@@ -988,6 +989,8 @@ class Exchange:
|
||||
except KeyError:
|
||||
raise ValueError(f"Can't get market information for symbol {pair}")
|
||||
|
||||
stake_limits = []
|
||||
limits = market["limits"]
|
||||
if isMin:
|
||||
# reserve some percent defined in config (5% default) + stoploss
|
||||
margin_reserve: float = 1.0 + self._config.get(
|
||||
@@ -997,11 +1000,12 @@ class Exchange:
|
||||
# it should not be more than 50%
|
||||
stoploss_reserve = max(min(stoploss_reserve, 1.5), 1)
|
||||
else:
|
||||
# is_max
|
||||
margin_reserve = 1.0
|
||||
stoploss_reserve = 1.0
|
||||
if max_from_tiers := self._get_max_notional_from_tiers(pair, leverage=leverage):
|
||||
stake_limits.append(max_from_tiers)
|
||||
|
||||
stake_limits = []
|
||||
limits = market["limits"]
|
||||
if limits["cost"][limit] is not None:
|
||||
stake_limits.append(
|
||||
self._contracts_to_amount(pair, limits["cost"][limit]) * stoploss_reserve
|
||||
@@ -1365,8 +1369,8 @@ class Exchange:
|
||||
ordertype = available_order_Types[user_order_type]
|
||||
else:
|
||||
# Otherwise pick only one available
|
||||
ordertype = list(available_order_Types.values())[0]
|
||||
user_order_type = list(available_order_Types.keys())[0]
|
||||
ordertype = next(iter(available_order_Types.values()))
|
||||
user_order_type = next(iter(available_order_Types.keys()))
|
||||
return ordertype, user_order_type
|
||||
|
||||
def _get_stop_limit_rate(self, stop_price: float, order_types: dict, side: str) -> float:
|
||||
@@ -1955,14 +1959,18 @@ class Exchange:
|
||||
|
||||
@staticmethod
|
||||
def get_next_limit_in_list(
|
||||
limit: int, limit_range: list[int] | None, range_required: bool = True
|
||||
limit: int,
|
||||
limit_range: list[int] | None,
|
||||
range_required: bool = True,
|
||||
upper_limit: int | None = None,
|
||||
):
|
||||
"""
|
||||
Get next greater value in the list.
|
||||
Used by fetch_l2_order_book if the api only supports a limited range
|
||||
if both limit_range and upper_limit is provided, limit_range wins.
|
||||
"""
|
||||
if not limit_range:
|
||||
return limit
|
||||
return min(limit, upper_limit) if upper_limit else limit
|
||||
|
||||
result = min([x for x in limit_range if limit <= x] + [max(limit_range)])
|
||||
if not range_required and limit > result:
|
||||
@@ -1979,7 +1987,10 @@ class Exchange:
|
||||
{'asks': [price, volume], 'bids': [price, volume]}
|
||||
"""
|
||||
limit1 = self.get_next_limit_in_list(
|
||||
limit, self._ft_has["l2_limit_range"], self._ft_has["l2_limit_range_required"]
|
||||
limit,
|
||||
self._ft_has["l2_limit_range"],
|
||||
self._ft_has["l2_limit_range_required"],
|
||||
self._ft_has["l2_limit_upper"],
|
||||
)
|
||||
try:
|
||||
return self._api.fetch_l2_order_book(pair, limit1)
|
||||
@@ -2351,6 +2362,7 @@ class Exchange:
|
||||
since_ms=since_ms,
|
||||
until_ms=until_ms,
|
||||
candle_type=candle_type,
|
||||
raise_=True,
|
||||
)
|
||||
)
|
||||
logger.debug(f"Downloaded data for {pair} from ccxt with length {len(data)}.")
|
||||
@@ -2391,7 +2403,7 @@ class Exchange:
|
||||
if isinstance(res, BaseException):
|
||||
logger.warning(f"Async code raised an exception: {repr(res)}")
|
||||
if raise_:
|
||||
raise
|
||||
raise res
|
||||
continue
|
||||
else:
|
||||
# Deconstruct tuple if it's not an exception
|
||||
@@ -2440,8 +2452,8 @@ class Exchange:
|
||||
|
||||
return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_ts)
|
||||
logger.info(
|
||||
f"Failed to reuse watch {pair}, {timeframe}, {candle_ts < last_refresh_time},"
|
||||
f" {candle_ts}, {last_refresh_time}, "
|
||||
f"Couldn't reuse watch for {pair}, {timeframe}, falling back to REST api. "
|
||||
f"{candle_ts < last_refresh_time}, {candle_ts}, {last_refresh_time}, "
|
||||
f"{format_ms_time(candle_ts)}, {format_ms_time(last_refresh_time)} "
|
||||
)
|
||||
|
||||
@@ -2789,7 +2801,7 @@ class Exchange:
|
||||
pair, timeframe, candle_type = pairwt
|
||||
since_ms = None
|
||||
new_ticks: list = []
|
||||
all_stored_ticks_df = DataFrame(columns=DEFAULT_TRADES_COLUMNS + ["date"])
|
||||
all_stored_ticks_df = DataFrame(columns=[*DEFAULT_TRADES_COLUMNS, "date"])
|
||||
first_candle_ms = self.needed_candle_for_trades_ms(timeframe, candle_type)
|
||||
# refresh, if
|
||||
# a. not in _trades
|
||||
@@ -2834,7 +2846,7 @@ class Exchange:
|
||||
else:
|
||||
# Skip cache, it's too old
|
||||
all_stored_ticks_df = DataFrame(
|
||||
columns=DEFAULT_TRADES_COLUMNS + ["date"]
|
||||
columns=[*DEFAULT_TRADES_COLUMNS, "date"]
|
||||
)
|
||||
|
||||
# from_id overrules with exchange set to id paginate
|
||||
@@ -3352,42 +3364,22 @@ class Exchange:
|
||||
pair_tiers = self._leverage_tiers[pair]
|
||||
|
||||
if stake_amount == 0:
|
||||
return self._leverage_tiers[pair][0]["maxLeverage"] # Max lev for lowest amount
|
||||
return pair_tiers[0]["maxLeverage"] # Max lev for lowest amount
|
||||
|
||||
for tier_index in range(len(pair_tiers)):
|
||||
tier = pair_tiers[tier_index]
|
||||
lev = tier["maxLeverage"]
|
||||
# Find the appropriate tier based on stake_amount
|
||||
prior_max_lev = None
|
||||
for tier in pair_tiers:
|
||||
min_stake = tier["minNotional"] / (prior_max_lev or tier["maxLeverage"])
|
||||
max_stake = tier["maxNotional"] / tier["maxLeverage"]
|
||||
prior_max_lev = tier["maxLeverage"]
|
||||
# Adjust notional by leverage to do a proper comparison
|
||||
if min_stake <= stake_amount <= max_stake:
|
||||
return tier["maxLeverage"]
|
||||
|
||||
if tier_index < len(pair_tiers) - 1:
|
||||
next_tier = pair_tiers[tier_index + 1]
|
||||
next_floor = next_tier["minNotional"] / next_tier["maxLeverage"]
|
||||
if next_floor > stake_amount: # Next tier min too high for stake amount
|
||||
return min((tier["maxNotional"] / stake_amount), lev)
|
||||
#
|
||||
# With the two leverage tiers below,
|
||||
# - a stake amount of 150 would mean a max leverage of (10000 / 150) = 66.66
|
||||
# - stakes below 133.33 = max_lev of 75
|
||||
# - stakes between 133.33-200 = max_lev of 10000/stake = 50.01-74.99
|
||||
# - stakes from 200 + 1000 = max_lev of 50
|
||||
#
|
||||
# {
|
||||
# "min": 0, # stake = 0.0
|
||||
# "max": 10000, # max_stake@75 = 10000/75 = 133.33333333333334
|
||||
# "lev": 75,
|
||||
# },
|
||||
# {
|
||||
# "min": 10000, # stake = 200.0
|
||||
# "max": 50000, # max_stake@50 = 50000/50 = 1000.0
|
||||
# "lev": 50,
|
||||
# }
|
||||
#
|
||||
|
||||
else: # if on the last tier
|
||||
if stake_amount > tier["maxNotional"]:
|
||||
# If stake is > than max tradeable amount
|
||||
raise InvalidOrderException(f"Amount {stake_amount} too high for {pair}")
|
||||
else:
|
||||
return tier["maxLeverage"]
|
||||
# else: # if on the last tier
|
||||
if stake_amount > max_stake:
|
||||
# If stake is > than max tradeable amount
|
||||
raise InvalidOrderException(f"Amount {stake_amount} too high for {pair}")
|
||||
|
||||
raise OperationalException(
|
||||
"Looped through all tiers without finding a max leverage. Should never be reached"
|
||||
@@ -3402,6 +3394,23 @@ class Exchange:
|
||||
else:
|
||||
return 1.0
|
||||
|
||||
def _get_max_notional_from_tiers(self, pair: str, leverage: float) -> float | None:
|
||||
"""
|
||||
get max_notional from leverage_tiers
|
||||
:param pair: The base/quote currency pair being traded
|
||||
:param leverage: The leverage to be used
|
||||
:return: The maximum notional value for the given leverage or None if not found
|
||||
"""
|
||||
if self.trading_mode != TradingMode.FUTURES:
|
||||
return None
|
||||
if pair not in self._leverage_tiers:
|
||||
return None
|
||||
pair_tiers = self._leverage_tiers[pair]
|
||||
for tier in reversed(pair_tiers):
|
||||
if leverage <= tier["maxLeverage"]:
|
||||
return tier["maxNotional"]
|
||||
return None
|
||||
|
||||
@retrier
|
||||
def _set_leverage(
|
||||
self,
|
||||
@@ -3687,12 +3696,12 @@ class Exchange:
|
||||
def dry_run_liquidation_price(
|
||||
self,
|
||||
pair: str,
|
||||
open_rate: float, # Entry price of position
|
||||
open_rate: float,
|
||||
is_short: bool,
|
||||
amount: float,
|
||||
stake_amount: float,
|
||||
leverage: float,
|
||||
wallet_balance: float, # Or margin balance
|
||||
wallet_balance: float,
|
||||
open_trades: list,
|
||||
) -> float | None:
|
||||
"""
|
||||
@@ -3713,8 +3722,6 @@ class Exchange:
|
||||
:param amount: Absolute value of position size incl. leverage (in base currency)
|
||||
:param stake_amount: Stake amount - Collateral in settle currency.
|
||||
:param leverage: Leverage used for this position.
|
||||
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
|
||||
:param margin_mode: Either ISOLATED or CROSS
|
||||
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||
Cross-Margin Mode: crossWalletBalance
|
||||
Isolated-Margin Mode: isolatedWalletBalance
|
||||
|
||||
@@ -37,6 +37,7 @@ class FtHas(TypedDict, total=False):
|
||||
# Orderbook
|
||||
l2_limit_range: list[int] | None
|
||||
l2_limit_range_required: bool
|
||||
l2_limit_upper: int | None
|
||||
# Futures
|
||||
ccxt_futures_name: str # usually swap
|
||||
mark_ohlcv_price: str
|
||||
@@ -44,6 +45,7 @@ class FtHas(TypedDict, total=False):
|
||||
funding_fee_timeframe: str
|
||||
funding_fee_candle_limit: int
|
||||
floor_leverage: bool
|
||||
uses_leverage_tiers: bool
|
||||
needs_trading_fees: bool
|
||||
order_props_in_contracts: list[Literal["amount", "cost", "filled", "remaining"]]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Exchange support utils
|
||||
|
||||
import inspect
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import ceil, floor
|
||||
from math import ceil, floor, isnan
|
||||
from typing import Any
|
||||
|
||||
import ccxt
|
||||
@@ -305,7 +305,7 @@ def price_to_precision(
|
||||
:param rounding_mode: rounding mode to use. Defaults to ROUND
|
||||
:return: price rounded up to the precision the Exchange accepts
|
||||
"""
|
||||
if price_precision is not None and precisionMode is not None:
|
||||
if price_precision is not None and precisionMode is not None and not isnan(price):
|
||||
if rounding_mode not in (ROUND_UP, ROUND_DOWN):
|
||||
# Use CCXT code where possible.
|
||||
return float(
|
||||
|
||||
@@ -84,6 +84,7 @@ class ExchangeWS:
|
||||
Remove history for a pair/timeframe combination from ccxt cache
|
||||
"""
|
||||
self._ccxt_object.ohlcvs.get(paircomb[0], {}).pop(paircomb[1], None)
|
||||
self.klines_last_refresh.pop(paircomb, None)
|
||||
|
||||
@retrier(retries=3)
|
||||
def ohlcvs(self, pair: str, timeframe: str) -> list[list]:
|
||||
@@ -138,6 +139,15 @@ class ExchangeWS:
|
||||
)
|
||||
)
|
||||
|
||||
async def _unwatch_ohlcv(self, pair: str, timeframe: str, candle_type: CandleType) -> None:
|
||||
try:
|
||||
await self._ccxt_object.un_watch_ohlcv_for_symbols([[pair, timeframe]])
|
||||
except ccxt.NotSupported as e:
|
||||
logger.debug("un_watch_ohlcv_for_symbols not supported: %s", e)
|
||||
pass
|
||||
except Exception:
|
||||
logger.exception("Exception in _unwatch_ohlcv")
|
||||
|
||||
def _continuous_stopped(
|
||||
self, task: asyncio.Task, pair: str, timeframe: str, candle_type: CandleType
|
||||
):
|
||||
@@ -150,6 +160,10 @@ class ExchangeWS:
|
||||
result = str(result1)
|
||||
|
||||
logger.info(f"{pair}, {timeframe}, {candle_type} - Task finished - {result}")
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._unwatch_ohlcv(pair, timeframe, candle_type), loop=self._loop
|
||||
)
|
||||
|
||||
self._klines_scheduled.discard((pair, timeframe, candle_type))
|
||||
self._pop_history((pair, timeframe, candle_type))
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ class Gate(Exchange):
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
"stop_price_param": "stopPrice",
|
||||
"stop_price_prop": "stopPrice",
|
||||
"l2_limit_upper": 1000,
|
||||
"marketOrderRequiresPrice": True,
|
||||
"trades_has_history": False, # Endpoint would support this - but ccxt doesn't.
|
||||
}
|
||||
@@ -44,6 +45,7 @@ class Gate(Exchange):
|
||||
"marketOrderRequiresPrice": False,
|
||||
"funding_fee_candle_limit": 90,
|
||||
"stop_price_type_field": "price_type",
|
||||
"l2_limit_upper": 300,
|
||||
"stop_price_type_value_mapping": {
|
||||
PriceType.LAST: 0,
|
||||
PriceType.MARK: 1,
|
||||
|
||||
@@ -35,6 +35,7 @@ class Hyperliquid(Exchange):
|
||||
"stop_price_prop": "stopPrice",
|
||||
"funding_fee_timeframe": "1h",
|
||||
"funding_fee_candle_limit": 500,
|
||||
"uses_leverage_tiers": False,
|
||||
}
|
||||
|
||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||
|
||||
@@ -69,7 +69,7 @@ class Kraken(Exchange):
|
||||
consolidated: CcxtBalances = {}
|
||||
for currency, balance in balances.items():
|
||||
base_currency = currency[:-2] if currency.endswith(".F") else currency
|
||||
base_currency = self._api.commonCurrencies.get(base_currency, base_currency)
|
||||
|
||||
if base_currency in consolidated:
|
||||
consolidated[base_currency]["free"] += balance["free"]
|
||||
consolidated[base_currency]["used"] += balance["used"]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user