Merge branch 'freqtrade:develop' into develop

This commit is contained in:
Simon Waiblinger
2024-05-19 13:53:38 +02:00
committed by GitHub
458 changed files with 58326 additions and 41384 deletions
-22
View File
@@ -1,22 +0,0 @@
FROM freqtradeorg/freqtrade:develop_freqairl
USER root
# Install dependencies
COPY requirements-dev.txt /freqtrade/
RUN apt-get update \
&& apt-get -y install --no-install-recommends apt-utils dialog \
&& apt-get -y install --no-install-recommends git sudo vim build-essential \
&& apt-get clean \
&& mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \
&& echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \
&& echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \
&& chown ftuser:ftuser -R /home/ftuser/.local/ \
&& chown ftuser: -R /home/ftuser/
USER ftuser
RUN pip install --user autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir
# Empty the ENTRYPOINT to allow all commands
ENTRYPOINT []
+13 -11
View File
@@ -1,42 +1,44 @@
{ {
"name": "freqtrade Develop", "name": "freqtrade Develop",
"build": { "image": "ghcr.io/freqtrade/freqtrade-devcontainer:latest",
"dockerfile": "Dockerfile",
"context": ".."
},
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [ "forwardPorts": [
8080 8080
], ],
"mounts": [
"source=freqtrade-bashhistory,target=/home/ftuser/commandhistory,type=volume"
],
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/freqtrade,type=bind,consistency=cached", "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/freqtrade,type=bind,consistency=cached",
// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "ftuser", "remoteUser": "ftuser",
"onCreateCommand": "pip install --user -e .", "onCreateCommand": "pip install --user -e .",
"postCreateCommand": "freqtrade create-userdir --userdir user_data/", "postCreateCommand": "freqtrade create-userdir --userdir user_data/",
"workspaceFolder": "/workspaces/freqtrade", "workspaceFolder": "/workspaces/freqtrade",
"customizations": { "customizations": {
"vscode": {
"settings": { "settings": {
"terminal.integrated.shell.linux": "/bin/bash", "terminal.integrated.shell.linux": "/bin/bash",
"editor.insertSpaces": true, "editor.insertSpaces": true,
"files.trimTrailingWhitespace": true, "files.trimTrailingWhitespace": true,
"[markdown]": { "[markdown]": {
"files.trimTrailingWhitespace": false, "files.trimTrailingWhitespace": false
}, },
"python.pythonPath": "/usr/local/bin/python", "python.pythonPath": "/usr/local/bin/python",
"[python]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff"
}
}, },
// Add the IDs of extensions you want installed when the container is created. // Add the IDs of extensions you want installed when the container is created.
"extensions": [ "extensions": [
"ms-python.python", "ms-python.python",
"ms-python.vscode-pylance", "ms-python.vscode-pylance",
"charliermarsh.ruff",
"davidanson.vscode-markdownlint", "davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker", "ms-azuretools.vscode-docker",
"vscode-icons-team.vscode-icons", "vscode-icons-team.vscode-icons",
"github.vscode-github-actions",
], ],
} }
} }
}
+21
View File
@@ -0,0 +1,21 @@
FROM freqtradeorg/freqtrade:develop_freqairl
USER root
# Install dependencies
COPY requirements-dev.txt /freqtrade/
ARG USERNAME=ftuser
RUN apt-get update \
&& apt-get -y install --no-install-recommends apt-utils dialog git ssh vim build-essential zsh \
&& apt-get clean \
&& mkdir -p /home/${USERNAME}/.vscode-server /home/${USERNAME}/.vscode-server-insiders /home/${USERNAME}/commandhistory \
&& chown ${USERNAME}:${USERNAME} -R /home/${USERNAME}/.local/ \
&& chown ${USERNAME}: -R /home/${USERNAME}/
USER ftuser
RUN pip install --user autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir
# Empty the ENTRYPOINT to allow all commands
ENTRYPOINT []
+12
View File
@@ -0,0 +1,12 @@
{
"name": "freqtrade Dev container image builder",
"build": {
"dockerfile": "Dockerfile",
"context": "../../"
},
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
},
"ghcr.io/stuartleeks/dev-container-features/shell-history:0.0.3": {}
}
}
+12
View File
@@ -10,8 +10,20 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: weekly interval: weekly
time: "03:00"
timezone: "Etc/UTC"
open-pull-requests-limit: 15 open-pull-requests-limit: 15
target-branch: develop target-branch: develop
groups:
types:
patterns:
- "types-*"
pytest:
patterns:
- "pytest*"
mkdocs:
patterns:
- "mkdocs*"
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
@@ -0,0 +1,47 @@
name: Binance Leverage tiers update
on:
schedule:
- cron: "0 3 * * 4"
# on demand
workflow_dispatch:
permissions:
contents: read
jobs:
auto-update:
runs-on: ubuntu-latest
environment:
name: develop
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install ccxt
run: pip install ccxt
- name: Run leverage tier update
env:
CI_WEB_PROXY: ${{ secrets.CI_WEB_PROXY }}
FREQTRADE__EXCHANGE__KEY: ${{ secrets.BINANCE_EXCHANGE_KEY }}
FREQTRADE__EXCHANGE__SECRET: ${{ secrets.BINANCE_EXCHANGE_SECRET }}
run: python build_helpers/binance_update_lev_tiers.py
- uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.REPO_SCOPED_TOKEN }}
add-paths: freqtrade/exchange/binance_leverage_tiers.json
labels: |
Tech maintenance
Dependencies
branch: update/binance-leverage-tiers
title: Update Binance Leverage Tiers
commit-message: "chore: update pre-commit hooks"
committer: Freqtrade Bot <noreply@github.com>
body: Update binance leverage tiers.
delete-branch: true
+76 -34
View File
@@ -11,7 +11,7 @@ on:
types: [published] types: [published]
pull_request: pull_request:
schedule: schedule:
- cron: '0 5 * * 4' - cron: '0 3 * * 4'
concurrency: concurrency:
group: "${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}" group: "${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}"
@@ -19,7 +19,7 @@ concurrency:
permissions: permissions:
repository-projects: read repository-projects: read
jobs: jobs:
build_linux: build-linux:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@@ -60,11 +60,16 @@ jobs:
export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include export TA_INCLUDE_PATH=${HOME}/dependencies/include
pip install -r requirements-dev.txt pip install -r requirements-dev.txt
pip install -e ft_client/
pip install -e . pip install -e .
- name: Check for version alignment
run: |
python build_helpers/freqtrade_client_version_align.py
- name: Tests - name: Tests
run: | run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
- name: Coveralls - name: Coveralls
if: (runner.os == 'Linux' && matrix.python-version == '3.10' && matrix.os == 'ubuntu-22.04') if: (runner.os == 'Linux' && matrix.python-version == '3.10' && matrix.os == 'ubuntu-22.04')
@@ -106,7 +111,11 @@ jobs:
- name: Run Ruff - name: Run Ruff
run: | run: |
ruff check --output-format=github . ruff check --output-format=github
- name: Run Ruff format check
run: |
ruff format --check
- name: Mypy - name: Mypy
run: | run: |
@@ -124,8 +133,11 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ "macos-latest", "macos-13" ] os: [ "macos-12", "macos-13", "macos-14" ]
python-version: ["3.9", "3.10", "3.11", "3.12"] python-version: ["3.9", "3.10", "3.11", "3.12"]
exclude:
- os: "macos-14"
python-version: "3.9"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -154,7 +166,7 @@ jobs:
run: | run: |
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd .. cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
- name: Installation - macOS - name: Installation - macOS (Brew)
run: | run: |
# brew update # brew update
# TODO: Should be the brew upgrade # TODO: Should be the brew upgrade
@@ -177,11 +189,15 @@ jobs:
rm /usr/local/bin/python3.12-config || true rm /usr/local/bin/python3.12-config || true
brew install hdf5 c-blosc libomp brew install hdf5 c-blosc libomp
- name: Installation (python)
run: |
python -m pip install --upgrade pip wheel python -m pip install --upgrade pip wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include export TA_INCLUDE_PATH=${HOME}/dependencies/include
pip install -r requirements-dev.txt pip install -r requirements-dev.txt
pip install -e ft_client/
pip install -e . pip install -e .
- name: Tests - name: Tests
@@ -218,7 +234,11 @@ jobs:
- name: Run Ruff - name: Run Ruff
run: | run: |
ruff check --output-format=github . ruff check --output-format=github
- name: Run Ruff format check
run: |
ruff format --check
- name: Mypy - name: Mypy
run: | run: |
@@ -288,7 +308,11 @@ jobs:
- name: Run Ruff - name: Run Ruff
run: | run: |
ruff check --output-format=github . ruff check --output-format=github
- name: Run Ruff format check
run: |
ruff format --check
- name: Mypy - name: Mypy
run: | run: |
@@ -310,7 +334,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.10" python-version: "3.12"
- name: pre-commit dependencies - name: pre-commit dependencies
run: | run: |
@@ -324,8 +348,8 @@ jobs:
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: "3.10" python-version: "3.12"
- uses: pre-commit/action@v3.0.0 - uses: pre-commit/action@v3.0.1
docs-check: docs-check:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@@ -339,7 +363,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
- name: Documentation build - name: Documentation build
run: | run: |
@@ -356,7 +380,7 @@ jobs:
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
build_linux_online: build-linux-online:
# Run pytest with "live" checks # Run pytest with "live" checks
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
@@ -365,7 +389,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
- name: Cache_dependencies - name: Cache_dependencies
uses: actions/cache@v4 uses: actions/cache@v4
@@ -392,29 +416,30 @@ jobs:
export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include export TA_INCLUDE_PATH=${HOME}/dependencies/include
pip install -r requirements-dev.txt pip install -r requirements-dev.txt
pip install -e ft_client/
pip install -e . pip install -e .
- name: Tests incl. ccxt compatibility tests - name: Tests incl. ccxt compatibility tests
env: env:
CI_WEB_PROXY: http://152.67.78.211:13128 CI_WEB_PROXY: http://152.67.78.211:13128
run: | run: |
pytest --random-order --longrun --durations 20 -n auto --dist loadscope pytest --random-order --longrun --durations 20 -n auto
# Notify only once - when CI completes (and after deploy) in case it's successfull # Notify only once - when CI completes (and after deploy) in case it's successful
notify-complete: notify-complete:
needs: [ needs: [
build_linux, build-linux,
build-macos, build-macos,
build-windows, build-windows,
docs-check, docs-check,
mypy-version-check, mypy-version-check,
pre-commit, pre-commit,
build_linux_online build-linux-online
] ]
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
# Discord notification can't handle schedule events # Discord notification can't handle schedule events
if: (github.event_name != 'schedule') if: github.event_name != 'schedule' && github.repository == 'freqtrade/freqtrade'
permissions: permissions:
repository-projects: read repository-projects: read
steps: steps:
@@ -437,7 +462,7 @@ jobs:
build: build:
name: "Build" name: "Build"
needs: [ build_linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ] needs: [ build-linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
@@ -446,7 +471,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
- name: Build distribution - name: Build distribution
run: | run: |
@@ -461,6 +486,19 @@ jobs:
dist dist
retention-days: 10 retention-days: 10
- name: Build Client distribution
run: |
pip install -U build
python -m build --sdist --wheel ft_client
- name: Upload artifacts 📦
uses: actions/upload-artifact@v4
with:
name: freqtrade-client-build
path: |
ft_client/dist
retention-days: 10
deploy-pypi: deploy-pypi:
name: "Deploy to PyPI" name: "Deploy to PyPI"
needs: [ build ] needs: [ build ]
@@ -478,20 +516,22 @@ jobs:
- name: Download artifact 📦 - name: Download artifact 📦
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: freqtrade-build pattern: freqtrade*-build
path: dist path: dist
merge-multiple: true
- name: Publish to PyPI (Test) - name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.8.11 uses: pypa/gh-action-pypi-publish@v1.8.14
with: with:
repository-url: https://test.pypi.org/legacy/ repository-url: https://test.pypi.org/legacy/
- name: Publish to PyPI - name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.8.11 uses: pypa/gh-action-pypi-publish@v1.8.14
deploy-docker: deploy-docker:
needs: [ build_linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ] needs: [ build-linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade' if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
@@ -502,12 +542,13 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
- name: Extract branch name - name: Extract branch name
shell: bash id: extract-branch
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF##*/})" run: |
id: extract_branch echo "GITHUB_REF='${GITHUB_REF}'"
echo "branch=${GITHUB_REF##*/}" >> "$GITHUB_OUTPUT"
- name: Dockerhub login - name: Dockerhub login
env: env:
@@ -536,7 +577,7 @@ jobs:
- name: Build and test and push docker images - name: Build and test and push docker images
env: env:
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} BRANCH_NAME: ${{ steps.extract-branch.outputs.branch }}
run: | run: |
build_helpers/publish_docker_multi.sh build_helpers/publish_docker_multi.sh
@@ -553,9 +594,10 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Extract branch name - name: Extract branch name
shell: bash id: extract-branch
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF##*/})" run: |
id: extract_branch echo "GITHUB_REF='${GITHUB_REF}'"
echo "branch=${GITHUB_REF##*/}" >> "$GITHUB_OUTPUT"
- name: Dockerhub login - name: Dockerhub login
env: env:
@@ -566,7 +608,7 @@ jobs:
- name: Build and test and push docker images - name: Build and test and push docker images
env: env:
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} BRANCH_NAME: ${{ steps.extract-branch.outputs.branch }}
GHCR_USERNAME: ${{ github.actor }} GHCR_USERNAME: ${{ github.actor }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
+45
View File
@@ -0,0 +1,45 @@
name: Devcontainer Pre-Build
on:
workflow_dispatch:
schedule:
- cron: "0 3 * * 0"
# push:
# branches:
# - "master"
# tags:
# - "v*.*.*"
# pull_requests:
# branches:
# - "master"
concurrency:
group: "${{ github.workflow }}"
cancel-in-progress: true
permissions:
packages: write
jobs:
build-and-push:
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
@@ -0,0 +1,18 @@
name: Update Docker Hub Description
on:
push:
branches:
- stable
jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: freqtradeorg/freqtrade
@@ -1,17 +0,0 @@
name: Update Docker Hub Description
on:
push:
branches:
- stable
jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v3
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKERHUB_REPOSITORY: freqtradeorg/freqtrade
+3 -3
View File
@@ -1,7 +1,6 @@
name: Pre-commit auto-update name: Pre-commit auto-update
on: on:
# every day at midnight
schedule: schedule:
- cron: "0 3 * * 2" - cron: "0 3 * * 2"
# on demand # on demand
@@ -18,7 +17,7 @@ jobs:
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
- name: Install pre-commit - name: Install pre-commit
@@ -30,12 +29,13 @@ jobs:
- name: Run pre-commit - name: Run pre-commit
run: pre-commit run --all-files run: pre-commit run --all-files
- uses: peter-evans/create-pull-request@v5 - uses: peter-evans/create-pull-request@v6
with: with:
token: ${{ secrets.REPO_SCOPED_TOKEN }} token: ${{ secrets.REPO_SCOPED_TOKEN }}
add-paths: .pre-commit-config.yaml add-paths: .pre-commit-config.yaml
labels: | labels: |
Tech maintenance Tech maintenance
Dependencies
branch: update/pre-commit-hooks branch: update/pre-commit-hooks
title: Update pre-commit hooks title: Update pre-commit hooks
commit-message: "chore: update pre-commit hooks" commit-message: "chore: update pre-commit hooks"
+13 -6
View File
@@ -9,17 +9,17 @@ repos:
# stages: [push] # stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.8.0" rev: "v1.10.0"
hooks: hooks:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
additional_dependencies: additional_dependencies:
- types-cachetools==5.3.0.7 - types-cachetools==5.3.0.7
- types-filelock==3.2.7 - types-filelock==3.2.7
- types-requests==2.31.0.20240106 - types-requests==2.31.0.20240406
- types-tabulate==0.9.0.20240106 - types-tabulate==0.9.0.20240106
- types-python-dateutil==2.8.19.20240106 - types-python-dateutil==2.9.0.20240316
- SQLAlchemy==2.0.25 - SQLAlchemy==2.0.30
# stages: [push] # stages: [push]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
@@ -31,12 +31,12 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: 'v0.1.14' rev: 'v0.4.4'
hooks: hooks:
- id: ruff - id: ruff
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0 rev: v4.6.0
hooks: hooks:
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: | exclude: |
@@ -54,3 +54,10 @@ repos:
(?x)^( (?x)^(
.*\.md .*\.md
)$ )$
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
hooks:
- id: codespell
additional_dependencies:
- tomli
+11
View File
@@ -0,0 +1,11 @@
{
"recommendations": [
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker",
"vscode-icons-team.vscode-icons",
"github.vscode-github-actions",
]
}
+4 -4
View File
@@ -48,7 +48,7 @@ pytest tests/test_<file_name>.py::test_<method_name>
#### Run Ruff #### Run Ruff
```bash ```bash
ruff . ruff check .
``` ```
We receive a lot of code that fails the `ruff` checks. We receive a lot of code that fails the `ruff` checks.
@@ -72,12 +72,12 @@ you can manually run pre-commit with `pre-commit run -a`.
mypy freqtrade mypy freqtrade
``` ```
### 4. Ensure all imports are correct ### 4. Ensure formatting is correct
#### Run isort #### Run ruff
``` bash ``` bash
isort . ruff format .
``` ```
## (Core)-Committer Guide ## (Core)-Committer Guide
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.11.7-slim-bookworm as base FROM python:3.12.3-slim-bookworm as base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env python3
import json
import os
from pathlib import Path
import ccxt
key = os.environ.get("FREQTRADE__EXCHANGE__KEY")
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"}}
)
_ = exchange.load_markets()
lev_tiers = exchange.fetch_leverage_tiers()
# Assumes this is running in the root of the repository.
file = Path("freqtrade/exchange/binance_leverage_tiers.json")
json.dump(dict(sorted(lev_tiers.items())), file.open("w"), indent=2)
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env python3
from freqtrade import __version__ as ft_version
from freqtrade_client import __version__ as client_version
def main():
if ft_version != client_version:
print(f"Versions do not match: \nft: {ft_version} \nclient: {client_version}")
exit(1)
print(f"Versions match: ft: {ft_version}, client: {client_version}")
exit(0)
if __name__ == "__main__":
main()
+15 -13
View File
@@ -1,4 +1,4 @@
# File used in CI to ensure pre-commit dependencies are kept uptodate. # File used in CI to ensure pre-commit dependencies are kept up-to-date.
import sys import sys
from pathlib import Path from pathlib import Path
@@ -6,28 +6,30 @@ from pathlib import Path
import yaml import yaml
pre_commit_file = Path('.pre-commit-config.yaml') pre_commit_file = Path(".pre-commit-config.yaml")
require_dev = Path('requirements-dev.txt') require_dev = Path("requirements-dev.txt")
require = Path('requirements.txt') require = Path("requirements.txt")
with require_dev.open('r') as rfile: with require_dev.open("r") as rfile:
requirements = rfile.readlines() requirements = rfile.readlines()
with require.open('r') as rfile: with require.open("r") as rfile:
requirements.extend(rfile.readlines()) requirements.extend(rfile.readlines())
# Extract types only # Extract types only
type_reqs = [r.strip('\n') for r in requirements if r.startswith( type_reqs = [
'types-') or r.startswith('SQLAlchemy')] r.strip("\n") for r in requirements if r.startswith("types-") or r.startswith("SQLAlchemy")
]
with pre_commit_file.open('r') as file: with pre_commit_file.open("r") as file:
f = yaml.load(file, Loader=yaml.FullLoader) f = yaml.load(file, Loader=yaml.SafeLoader)
mypy_repo = [repo for repo in f['repos'] if repo['repo'] mypy_repo = [
== 'https://github.com/pre-commit/mirrors-mypy'] repo for repo in f["repos"] if repo["repo"] == "https://github.com/pre-commit/mirrors-mypy"
]
hooks = mypy_repo[0]['hooks'][0]['additional_dependencies'] hooks = mypy_repo[0]["hooks"][0]["additional_dependencies"]
errors = [] errors = []
for hook in hooks: for hook in hooks:
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.11.7-slim-bookworm as base FROM python:3.11.8-slim-bookworm as base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8
+3 -3
View File
@@ -36,7 +36,7 @@ freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4 5
``` ```
This command will read from the last backtesting results. The `--analysis-groups` option is This command will read from the last backtesting results. The `--analysis-groups` option is
used to specify the various tabular outputs showing the profit fo each group or trade, used to specify the various tabular outputs showing the profit of each group or trade,
ranging from the simplest (0) to the most detailed per pair, per buy and per sell tag (4): ranging from the simplest (0) to the most detailed per pair, per buy and per sell tag (4):
* 0: overall winrate and profit summary by enter_tag * 0: overall winrate and profit summary by enter_tag
@@ -109,12 +109,12 @@ automatically accessible by including them on the indicator-list, and these incl
- **open_date :** trade open datetime - **open_date :** trade open datetime
- **close_date :** trade close datetime - **close_date :** trade close datetime
- **min_rate :** minimum price seen throughout the position - **min_rate :** minimum price seen throughout the position
- **max_rate :** maxiumum price seen throughout the position - **max_rate :** maximum price seen throughout the position
- **open :** signal candle open price - **open :** signal candle open price
- **close :** signal candle close price - **close :** signal candle close price
- **high :** signal candle high price - **high :** signal candle high price
- **low :** signal candle low price - **low :** signal candle low price
- **volume :** signal candle volumne - **volume :** signal candle volume
- **profit_ratio :** trade profit ratio - **profit_ratio :** trade profit ratio
- **profit_abs :** absolute profit return of the trade - **profit_abs :** absolute profit return of the trade
Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+42 -41
View File
@@ -252,34 +252,34 @@ The most important in the backtesting is to understand the result.
A backtesting result will look like that: A backtesting result will look like that:
``` ```
========================================================= BACKTESTING REPORT ========================================================= ================================================ BACKTESTING REPORT =================================================
| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% | | Pair | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% |
|:---------|--------:|---------------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:| |:---------|--------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:|
| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 | | ADA/BTC | 35 | -0.11 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 |
| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 | | ARK/BTC | 11 | -0.41 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 |
| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 | | BTS/BTC | 32 | 0.31 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 |
| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 | | DASH/BTC | 13 | -0.08 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 |
| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 | | ENG/BTC | 18 | 1.36 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 |
| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 | | EOS/BTC | 36 | 0.08 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 |
| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 | | ETC/BTC | 26 | 0.37 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 |
| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 | | ETH/BTC | 33 | 0.30 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 |
| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 | | IOTA/BTC | 32 | 0.03 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 |
| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 | | LSK/BTC | 15 | 1.75 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 |
| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 | | LTC/BTC | 32 | -0.04 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 |
| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 | | NANO/BTC | 17 | 1.26 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 |
| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 | | NEO/BTC | 23 | 0.82 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 |
| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 | | REQ/BTC | 9 | 1.17 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 |
| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 | | XLM/BTC | 16 | 1.22 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 |
| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 | | XMR/BTC | 23 | -0.18 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 |
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | | XRP/BTC | 35 | 0.66 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | | ZEC/BTC | 22 | -0.46 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | | TOTAL | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
====================================================== LEFT OPEN TRADES REPORT ====================================================== ============================================= LEFT OPEN TRADES REPORT =============================================
| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | | Pair | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| |:---------|---------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | | ADA/BTC | 1 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | | LTC/BTC | 1 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | | TOTAL | 2 | 0.78 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
==================== EXIT REASON STATS ==================== ==================== EXIT REASON STATS ====================
| Exit Reason | Exits | Wins | Draws | Losses | | Exit Reason | Exits | Wins | Draws | Losses |
|:-------------------|--------:|------:|-------:|--------:| |:-------------------|--------:|------:|-------:|--------:|
@@ -358,7 +358,7 @@ here:
The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has
earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC. earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC.
The column `Avg Profit %` shows the average profit for all trades made while the column `Cum Profit %` sums up all the profits/losses. The column `Avg Profit %` shows the average profit for all trades made.
The column `Tot Profit %` shows instead the total profit % in relation to the starting balance. The column `Tot Profit %` shows instead the total profit % in relation to the starting balance.
In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`. In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`.
@@ -464,7 +464,7 @@ It contains some useful key metrics about performance of your strategy on backte
- `Profit factor`: profit / loss. - `Profit factor`: profit / loss.
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount. - `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. - `Total trade volume`: Volume generated on the exchange to reach the above profit.
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Tot Profit %`.
- `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade. - `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade.
- `Best day` / `Worst day`: Best and worst day based on daily profit. - `Best day` / `Worst day`: Best and worst day based on daily profit.
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
@@ -522,8 +522,8 @@ To save time, by default backtest will reuse a cached result from within the las
### Further backtest-result analysis ### Further backtest-result analysis
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file). 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](data-analysis.md#backtesting) backtesting section. 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.
## Assumptions made by backtesting ## Assumptions made by backtesting
@@ -531,12 +531,13 @@ Since backtesting lacks some detailed information about what happens within a ca
- Exchange [trading limits](#trading-limits-in-backtesting) are respected - Exchange [trading limits](#trading-limits-in-backtesting) are respected
- Entries happen at open-price - Entries happen at open-price
- All orders are filled at the requested price (no slippage, no unfilled orders) - All orders are filled at the requested price (no slippage) as long as the price is within the candle's high/low range
- Exit-signal exits happen at open-price of the consecutive candle - Exit-signal exits happen at open-price of the consecutive candle
- Exits don't free their trade slot for a new trade until the next candle
- Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open - Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open
- ROI - ROI
- exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%) - Exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%)
- exits are never "below the candle", so a ROI of 2% may result in a exit at 2.4% if low was at 2.4% profit - Exits are never "below the candle", so a ROI of 2% may result in a exit at 2.4% if low was at 2.4% profit
- ROI entries which came into effect on the triggering candle (e.g. `120: 0.02` for 1h candles, from `60: 0.05`) will use the candle's open as exit rate - ROI entries which came into effect on the triggering candle (e.g. `120: 0.02` for 1h candles, from `60: 0.05`) will use the candle's open as exit rate
- Force-exits caused by `<N>=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) - Force-exits caused by `<N>=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles)
- Stoploss exits happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price - Stoploss exits happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price
@@ -587,7 +588,7 @@ These precision values are based on current exchange limits (as described in the
## Improved backtest accuracy ## Improved backtest accuracy
One big limitation of backtesting is it's inability to know how prices moved intra-candle (was high before close, or viceversa?). One big limitation of backtesting is it's inability to know how prices moved intra-candle (was high before close, or vice-versa?).
So assuming you run backtesting with a 1h timeframe, there will be 4 prices for that candle (Open, High, Low, Close). So assuming you run backtesting with a 1h timeframe, there will be 4 prices for that candle (Open, High, Low, Close).
While backtesting does take some assumptions (read above) about this - this can never be perfect, and will always be biased in one way or the other. While backtesting does take some assumptions (read above) about this - this can never be perfect, and will always be biased in one way or the other.
@@ -629,11 +630,11 @@ There will be an additional table comparing win/losses of the different strategi
Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy. Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy.
``` ```
=========================================================== STRATEGY SUMMARY =========================================================================== ================================================== STRATEGY SUMMARY ===================================================================
| Strategy | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % | | Strategy | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % |
|:------------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:| |:------------|---------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:|
| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 | | Strategy1 | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 |
| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 | | Strategy2 | 1487 | -0.13 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 |
``` ```
## Next step ## Next step
+4 -1
View File
@@ -33,7 +33,6 @@ For spot pairs, naming will be `base/quote` (e.g. `ETH/USDT`).
For futures pairs, naming will be `base/quote:settle` (e.g. `ETH/USDT:USDT`). For futures pairs, naming will be `base/quote:settle` (e.g. `ETH/USDT:USDT`).
## Bot execution logic ## Bot execution logic
Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop. Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop.
@@ -50,6 +49,8 @@ By default, the bot loop runs every few seconds (`internals.process_throttle_sec
* Call `populate_indicators()` * Call `populate_indicators()`
* Call `populate_entry_trend()` * Call `populate_entry_trend()`
* Call `populate_exit_trend()` * Call `populate_exit_trend()`
* Update trades open order state from exchange.
* Call `order_filled()` strategy callback for filled orders.
* Check timeouts for open orders. * Check timeouts for open orders.
* Calls `check_entry_timeout()` strategy callback for open entry orders. * Calls `check_entry_timeout()` strategy callback for open entry orders.
* Calls `check_exit_timeout()` strategy callback for open exit orders. * Calls `check_exit_timeout()` strategy callback for open exit orders.
@@ -86,8 +87,10 @@ This loop will be repeated again and again until the bot is stopped.
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage. * In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
* Determine stake size by calling the `custom_stake_amount()` callback. * Determine stake size by calling the `custom_stake_amount()` callback.
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested. * Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
* Call `order_filled()` strategy callback for filled entry orders.
* Call `custom_stoploss()` and `custom_exit()` to find custom exit points. * Call `custom_stoploss()` and `custom_exit()` to find custom exit points.
* For exits based on exit-signal, custom-exit and partial exits: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle). * For exits based on exit-signal, custom-exit and partial exits: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
* Call `order_filled()` strategy callback for filled exit orders.
* Generate backtest report output * Generate backtest report output
!!! Note !!! Note
+20 -8
View File
@@ -14,7 +14,7 @@ You can specify a different configuration file used by the bot with the `-c/--co
If you used the [Quick start](docker_quickstart.md#docker-quick-start) method for installing If you used the [Quick start](docker_quickstart.md#docker-quick-start) method for installing
the bot, the installation script should have already created the default configuration file (`config.json`) for you. the bot, the installation script should have already created the default configuration file (`config.json`) for you.
If the default configuration file is not created we recommend to use `freqtrade new-config --config config.json` to generate a basic configuration file. If the default configuration file is not created we recommend to use `freqtrade new-config --config user_data/config.json` to generate a basic configuration file.
The Freqtrade configuration file is to be written in JSON format. The Freqtrade configuration file is to be written in JSON format.
@@ -49,6 +49,13 @@ FREQTRADE__EXCHANGE__SECRET=<yourExchangeSecret>
!!! Note !!! Note
Environment variables detected are logged at startup - so if you can't find why a value is not what you think it should be based on the configuration, make sure it's not loaded from an environment variable. Environment variables detected are logged at startup - so if you can't find why a value is not what you think it should be based on the configuration, make sure it's not loaded from an environment variable.
!!! Tip "Validate combined result"
You can use the [show-config subcommand](utils.md#show-config) to see the final, combined configuration.
??? Warning "Loading sequence"
Environment variables are loaded after the initial configuration. As such, you cannot provide the path to the configuration through environment variables. Please use `--config path/to/config.json` for that.
This also applies to user_dir to some degree. while the user directory can be set through environment variables - the configuration will **not** be loaded from that location.
### Multiple configuration files ### Multiple configuration files
Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream. Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream.
@@ -56,6 +63,9 @@ Multiple configuration files can be specified and used by the bot or the bot can
You can specify additional configuration files in `add_config_files`. Files specified in this parameter will be loaded and merged with the initial config file. The files are resolved relative to the initial configuration file. You can specify additional configuration files in `add_config_files`. Files specified in this parameter will be loaded and merged with the initial config file. The files are resolved relative to the initial configuration file.
This is similar to using multiple `--config` parameters, but simpler in usage as you don't have to specify all files for all commands. This is similar to using multiple `--config` parameters, but simpler in usage as you don't have to specify all files for all commands.
!!! Tip "Validate combined result"
You can use the [show-config subcommand](utils.md#show-config) to see the final, combined configuration.
!!! Tip "Use multiple configuration files to keep secrets secret" !!! Tip "Use multiple configuration files to keep secrets secret"
You can use a 2nd configuration file containing your secrets. That way you can share your "primary" configuration file, while still keeping your API keys for yourself. You can use a 2nd configuration file containing your secrets. That way you can share your "primary" configuration file, while still keeping your API keys for yourself.
The 2nd file should only specify what you intend to override. The 2nd file should only specify what you intend to override.
@@ -187,7 +197,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean | `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean
| `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `-1`.*<br> **Datatype:** Positive Integer or -1 | `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `-1`.*<br> **Datatype:** Positive Integer or -1
| | **Exchange** | | **Exchange**
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String | `exchange.name` | **Required.** Name of the exchange class to use. <br> **Datatype:** String
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
@@ -242,7 +252,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `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 | `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 | `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
| `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages. <br>*Defaults to `60` seconds.* <br> **Datatype:** Positive Integer or 0 | `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages. <br>*Defaults to `60` seconds.* <br> **Datatype:** Positive Integer or 0
| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean | `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](advanced-setup.md#configure-the-bot-running-as-a-systemd-service) for more details. <br> **Datatype:** Boolean
| `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> **Datatype:** ClassName | `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> **Datatype:** ClassName
| `strategy_path` | Adds an additional strategy lookup path (must be a directory). <br> **Datatype:** String | `strategy_path` | Adds an additional strategy lookup path (must be a directory). <br> **Datatype:** String
| `recursive_strategy_search` | Set to `true` to recursively search sub-directories inside `user_data/strategies` for a strategy. <br> **Datatype:** Boolean | `recursive_strategy_search` | Set to `true` to recursively search sub-directories inside `user_data/strategies` for a strategy. <br> **Datatype:** Boolean
@@ -326,6 +336,8 @@ You'd set `available_capital=5000` - granting each bot an initial capital of 500
The bot will then split this starting balance equally into `max_open_trades` buckets. The bot will then split this starting balance equally into `max_open_trades` buckets.
Profitable trades will result in increased stake-sizes for this bot - without affecting the stake-sizes of the other bot. Profitable trades will result in increased stake-sizes for this bot - without affecting the stake-sizes of the other bot.
Adjusting `available_capital` requires reloading the configuration to take effect. Adjusting the `available_capital` adds the difference between the previous `available_capital` and the new `available_capital`. Decreasing the available capital when trades are open doesn't exit the trades. The difference is returned to the wallet when the trades conclude. The outcome of this differs depending on the price movement between the adjustment and exiting the trades.
!!! Warning "Incompatible with `tradable_balance_ratio`" !!! Warning "Incompatible with `tradable_balance_ratio`"
Setting this option will replace any configuration of `tradable_balance_ratio`. Setting this option will replace any configuration of `tradable_balance_ratio`.
@@ -358,7 +370,7 @@ This setting works in combination with `max_open_trades`. The maximum capital en
For example, the bot will at most use (0.05 BTC x 3) = 0.15 BTC, assuming a configuration of `max_open_trades=3` and `stake_amount=0.05`. For example, the bot will at most use (0.05 BTC x 3) = 0.15 BTC, assuming a configuration of `max_open_trades=3` and `stake_amount=0.05`.
!!! Note !!! Note
This setting respects the [available balance configuration](#available-balance). This setting respects the [available balance configuration](#tradable-balance).
#### Dynamic stake amount #### Dynamic stake amount
@@ -503,13 +515,13 @@ Configuration:
Please carefully read the section [Market order pricing](#market-order-pricing) section when using market orders. Please carefully read the section [Market order pricing](#market-order-pricing) section when using market orders.
!!! Note "Stoploss on exchange" !!! Note "Stoploss on exchange"
`stoploss_on_exchange_interval` is not mandatory. Do not change its value if you are `order_types.stoploss_on_exchange_interval` is not mandatory. Do not change its value if you are
unsure of what you are doing. For more information about how stoploss works please unsure of what you are doing. For more information about how stoploss works please
refer to [the stoploss documentation](stoploss.md). refer to [the stoploss documentation](stoploss.md).
If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order. If `order_types.stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order.
!!! Warning "Warning: stoploss_on_exchange failures" !!! Warning "Warning: order_types.stoploss_on_exchange failures"
If stoploss on exchange creation fails for some reason, then an "emergency exit" is initiated. By default, this will exit the trade using a market order. The order-type for the emergency-exit can be changed by setting the `emergency_exit` value in the `order_types` dictionary - however, this is not advised. If stoploss on exchange creation fails for some reason, then an "emergency exit" is initiated. By default, this will exit the trade using a market order. The order-type for the emergency-exit can be changed by setting the `emergency_exit` value in the `order_types` dictionary - however, this is not advised.
### Understand order_time_in_force ### Understand order_time_in_force
@@ -535,7 +547,7 @@ is automatically cancelled by the exchange.
**PO (Post only):** **PO (Post only):**
Post only order. The order is either placed as a maker order, or it is canceled. Post only order. The order is either placed as a maker order, or it is canceled.
This means the order must be placed on orderbook for at at least time in an unfilled state. This means the order must be placed on orderbook for at least time in an unfilled state.
#### time_in_force config #### time_in_force config
+18 -14
View File
@@ -24,10 +24,10 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--days INT] [--new-pairs-days INT] [--days INT] [--new-pairs-days INT]
[--include-inactive-pairs] [--include-inactive-pairs]
[--timerange TIMERANGE] [--dl-trades] [--timerange TIMERANGE] [--dl-trades]
[--exchange EXCHANGE] [--convert] [--exchange EXCHANGE]
[-t TIMEFRAMES [TIMEFRAMES ...]] [--erase] [-t TIMEFRAMES [TIMEFRAMES ...]] [--erase]
[--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}] [--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
[--data-format-trades {json,jsongz,hdf5,feather}] [--data-format-trades {json,jsongz,hdf5,feather,parquet}]
[--trading-mode {spot,margin,futures}] [--trading-mode {spot,margin,futures}]
[--prepend] [--prepend]
@@ -48,6 +48,11 @@ options:
--dl-trades Download trades instead of OHLCV data. The bot will --dl-trades Download trades instead of OHLCV data. The bot will
resample trades to the desired timeframe as specified resample trades to the desired timeframe as specified
as --timeframes/-t. as --timeframes/-t.
--convert Convert downloaded trades to OHLCV data. Only
applicable in combination with `--dl-trades`. Will be
automatic for exchanges which don't have historic
OHLCV (e.g. Kraken). If not provided, use `trades-to-
ohlcv` to convert trades data to OHLCV data.
--exchange EXCHANGE Exchange name. Only valid if no config is provided. --exchange EXCHANGE Exchange name. Only valid if no config is provided.
-t TIMEFRAMES [TIMEFRAMES ...], --timeframes TIMEFRAMES [TIMEFRAMES ...] -t TIMEFRAMES [TIMEFRAMES ...], --timeframes TIMEFRAMES [TIMEFRAMES ...]
Specify which tickers to download. Space-separated Specify which tickers to download. Space-separated
@@ -57,7 +62,7 @@ options:
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet} --data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
Storage format for downloaded candle (OHLCV) data. Storage format for downloaded candle (OHLCV) data.
(default: `feather`). (default: `feather`).
--data-format-trades {json,jsongz,hdf5,feather} --data-format-trades {json,jsongz,hdf5,feather,parquet}
Storage format for downloaded trades data. (default: Storage format for downloaded trades data. (default:
`feather`). `feather`).
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures} --trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
@@ -471,15 +476,20 @@ ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
## Trades (tick) data ## Trades (tick) data
By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API. By default, `download-data` sub-command downloads Candles (OHLCV) data. Most exchanges also provide historic trade-data via their API.
This data can be useful if you need many different timeframes, since it is only downloaded once, and then resampled locally to the desired timeframes. This data can be useful if you need many different timeframes, since it is only downloaded once, and then resampled locally to the desired timeframes.
Since this data is large by default, the files use the feather file format by default. They are stored in your data-directory with the naming convention of `<pair>-trades.feather` (`ETH_BTC-trades.feather`). Incremental mode is also supported, as for historic OHLCV data, so downloading the data once per week with `--days 8` will create an incremental data-repository. Since this data is large by default, the files use the feather file format by default. They are stored in your data-directory with the naming convention of `<pair>-trades.feather` (`ETH_BTC-trades.feather`). Incremental mode is also supported, as for historic OHLCV data, so downloading the data once per week with `--days 8` will create an incremental data-repository.
To use this mode, simply add `--dl-trades` to your call. This will swap the download method to download trades, and resamples the data locally. To use this mode, simply add `--dl-trades` to your call. This will swap the download method to download trades.
If `--convert` is also provided, the resample step will happen automatically and overwrite eventually existing OHLCV data for the given pair/timeframe combinations.
!!! Warning "do not use" !!! Warning "Do not use"
You should not use this unless you're a kraken user. Most other exchanges provide OHLCV data with sufficient history. You should not use this unless you're a kraken user (Kraken does not provide historic OHLCV data).
Most other exchanges provide OHLCV data with sufficient history, so downloading multiple timeframes through that method will still proof to be a lot faster than downloading trades data.
!!! Note "Kraken user"
Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data.
Example call: Example call:
@@ -490,12 +500,6 @@ freqtrade download-data --exchange kraken --pairs XRP/EUR ETH/EUR --days 20 --dl
!!! Note !!! Note
While this method uses async calls, it will be slow, since it requires the result of the previous call to generate the next request to the exchange. While this method uses async calls, it will be slow, since it requires the result of the previous call to generate the next request to the exchange.
!!! Warning
The historic trades are not available during Freqtrade dry-run and live trade modes because all exchanges tested provide this data with a delay of few 100 candles, so it's not suitable for real-time trading.
!!! Note "Kraken user"
Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data.
## Next step ## Next step
Great, you now have backtest data downloaded, so you can now start [backtesting](backtesting.md) your strategy. Great, you now have some data downloaded, so you can now start [backtesting](backtesting.md) your strategy.
+6 -4
View File
@@ -83,7 +83,7 @@ Details will obviously vary between setups - but this should work to get you sta
``` json ``` json
{ {
"name": "freqtrade trade", "name": "freqtrade trade",
"type": "python", "type": "debugpy",
"request": "launch", "request": "launch",
"module": "freqtrade", "module": "freqtrade",
"console": "integratedTerminal", "console": "integratedTerminal",
@@ -129,6 +129,8 @@ Below is an outline of exception inheritance hierarchy:
+ FreqtradeException + FreqtradeException
| |
+---+ OperationalException +---+ OperationalException
| |
| +---+ ConfigurationError
| |
+---+ DependencyException +---+ DependencyException
| | | |
@@ -259,7 +261,7 @@ For that reason, they must implement the following methods:
The `until` portion should be calculated using the provided `calculate_lock_end()` method. The `until` portion should be calculated using the provided `calculate_lock_end()` method.
All Protections should use `"stop_duration"` / `"stop_duration_candles"` to define how long a a pair (or all pairs) should be locked. All Protections should use `"stop_duration"` / `"stop_duration_candles"` to define how long a pair (or all pairs) should be locked.
The content of this is made available as `self._stop_duration` to the each Protection. The content of this is made available as `self._stop_duration` to the each Protection.
If your protection requires a look-back period, please use `"lookback_period"` / `"lockback_period_candles"` to keep all protections aligned. If your protection requires a look-back period, please use `"lookback_period"` / `"lockback_period_candles"` to keep all protections aligned.
@@ -303,7 +305,7 @@ The `IProtection` parent class provides a helper method for this in `calculate_l
Most exchanges supported by CCXT should work out of the box. Most exchanges supported by CCXT should work out of the box.
To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`. To quickly test the public endpoints of an exchange, add a configuration for your exchange to `tests/exchange_online/conftest.py` and run these tests with `pytest --longrun tests/exchange_online/test_ccxt_compat.py`.
Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar). Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar).
Also try to use `freqtrade download-data` for an extended timerange (multiple months) and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded). Also try to use `freqtrade download-data` for an extended timerange (multiple months) and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded).
@@ -376,7 +378,7 @@ from pathlib import Path
exchange = ccxt.binance({ exchange = ccxt.binance({
'apiKey': '<apikey>', 'apiKey': '<apikey>',
'secret': '<secret>' 'secret': '<secret>',
'options': {'defaultType': 'swap'} 'options': {'defaultType': 'swap'}
}) })
_ = exchange.load_markets() _ = exchange.load_markets()
+1 -1
View File
@@ -137,7 +137,7 @@ $$ R = \frac{\text{average_profit}}{\text{average_loss}} = \frac{\mu_{win}}{\mu_
### Expectancy ### Expectancy
By combining the Win Rate $W$ and and the Risk Reward ratio $R$ to create an expectancy ratio $E$. A expectance ratio is the expected return of the investment made in a trade. We can compute the value of $E$ as follows: By combining the Win Rate $W$ and the Risk Reward ratio $R$ to create an expectancy ratio $E$. A expectance ratio is the expected return of the investment made in a trade. We can compute the value of $E$ as follows:
$$E = R * W - L$$ $$E = R * W - L$$
+3 -1
View File
@@ -68,6 +68,8 @@ Binance supports [time_in_force](configuration.md#understand-order_time_in_force
For Binance, it is suggested to add `"BNB/<STAKE>"` to your blacklist to avoid issues, unless you are willing to maintain enough extra `BNB` on the account or unless you're willing to disable using `BNB` for fees. For Binance, it is suggested to add `"BNB/<STAKE>"` to your blacklist to avoid issues, unless you are willing to maintain enough extra `BNB` on the account or unless you're willing to disable using `BNB` for fees.
Binance accounts may use `BNB` for fees, and if a trade happens to be on `BNB`, further trades may consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore. Binance accounts may use `BNB` for fees, and if a trade happens to be on `BNB`, further trades may consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore.
If not enough `BNB` is available to cover transaction fees, then fees will not be covered by `BNB` and no fee reduction will occur. Freqtrade will never buy BNB to cover for fees. BNB needs to be bought and monitored manually to this end.
### Binance sites ### Binance sites
Binance has been split into 2, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized. Binance has been split into 2, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized.
@@ -297,7 +299,7 @@ $ pip3 install web3
Most exchanges return current incomplete candle via their OHLCV/klines API interface. Most exchanges return current incomplete candle via their OHLCV/klines API interface.
By default, Freqtrade assumes that incomplete candle is fetched from the exchange and removes the last candle assuming it's the incomplete candle. By default, Freqtrade assumes that incomplete candle is fetched from the exchange and removes the last candle assuming it's the incomplete candle.
Whether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#Incomplete-candles) from the Contributor documentation. Whether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#incomplete-candles) from the Contributor documentation.
Due to the danger of repainting, Freqtrade does not allow you to use this incomplete candle. Due to the danger of repainting, Freqtrade does not allow you to use this incomplete candle.
+2 -2
View File
@@ -2,7 +2,7 @@
## Supported Markets ## Supported Markets
Freqtrade supports spot trading, as well as (isolated) futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an uptodate list of supported exchanges. Freqtrade supports spot trading, as well as (isolated) futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an up-to-date list of supported exchanges.
### Can my bot open short positions? ### Can my bot open short positions?
@@ -14,7 +14,7 @@ In spot markets, you can in some cases use leveraged spot tokens, which reflect
### Can my bot trade options or futures? ### Can my bot trade options or futures?
Futures trading is supported for selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an uptodate list of supported exchanges. Futures trading is supported for selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an up-to-date list of supported exchanges.
## Beginner Tips & Tricks ## Beginner Tips & Tricks
+85
View File
@@ -0,0 +1,85 @@
# FreqUI
Freqtrade provides a builtin webserver, which can serve [FreqUI](https://github.com/freqtrade/frequi), the freqtrade frontend.
By default, the UI is automatically installed as part of the installation (script, docker).
freqUI can also be manually installed by using the `freqtrade install-ui` command.
This same command can also be used to update freqUI to new new releases.
Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured API port (by default `http://127.0.0.1:8080`).
??? Note "Looking to contribute to freqUI?"
Developers should not use this method, but instead clone the corresponding use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI. A working installation of node will be required to build the frontend.
!!! tip "freqUI is not required to run freqtrade"
freqUI is an optional component of freqtrade, and is not required to run the bot.
It is a frontend that can be used to monitor the bot and to interact with it - but freqtrade itself will work perfectly fine without it.
## Configuration
FreqUI does not have it's own configuration file - but assumes a working setup for the [rest-api](rest-api.md) is available.
Please refer to the corresponding documentation page to get setup with freqUI
## UI
FreqUI is a modern, responsive web application that can be used to monitor and interact with your bot.
FreqUI provides a light, as well as a dark theme.
Themes can be easily switched via a prominent button at the top of the page.
The theme of the screenshots on this page will adapt to the selected documentation Theme, so to see the dark (or light) version, please switch the theme of the Documentation.
### Login
The below screenshot shows the login screen of freqUI.
![FreqUI - login](assets/frequi-login-CORS.png#only-dark)
![FreqUI - login](assets/frequi-login-CORS-light.png#only-light)
!!! Hint "CORS"
The Cors error shown in this screenshot is due to the fact that the UI is running on a different port than the API, and [CORS](#cors) has not been setup correctly yet.
### Trade view
The trade view allows you to visualize the trades that the bot is making and to interact with the bot.
On this page, you can also interact with the bot by starting and stopping it and - if configured - force trade entries and exits.
![FreqUI - trade view](assets/freqUI-trade-pane-dark.png#only-dark)
![FreqUI - trade view](assets/freqUI-trade-pane-light.png#only-light)
### Plot Configurator
FreqUI Plots can be configured either via a `plot_config` configuration object in the strategy (which can be loaded via "from strategy" button) or via the UI.
Multiple plot configurations can be created and switched at will - allowing for flexible, different views into your charts.
The plot configuration can be accessed via the "Plot Configurator" (Cog icon) button in the top right corner of the trade view.
![FreqUI - plot configuration](assets/freqUI-plot-configurator-dark.png#only-dark)
![FreqUI - plot configuration](assets/freqUI-plot-configurator-light.png#only-light)
### Settings
Several UI related settings can be changed by accessing the settings page.
Things you can change (among others):
* Timezone of the UI
* Visualization of open trades as part of the favicon (browser tab)
* Candle colors (up/down -> red/green)
* Enable / disable in-app notification types
![FreqUI - Settings view](assets/frequi-settings-dark.png#only-dark)
![FreqUI - Settings view](assets/frequi-settings-light.png#only-light)
## Backtesting
When freqtrade is started in [webserver mode](utils.md#webserver-mode) (freqtrade started with `freqtrade webserver`), the backtesting view becomes available.
This view allows you to backtest strategies and visualize the results.
You can also load and visualize previous backtest results, as well as compare the results with each other.
![FreqUI - Backtesting](assets/freqUI-backtesting-dark.png#only-dark)
![FreqUI - Backtesting](assets/freqUI-backtesting-light.png#only-light)
--8<-- "includes/cors.md"
+3
View File
@@ -32,6 +32,9 @@ FreqAI is configured through the typical [Freqtrade config file](configuration.m
A full example config is available in `config_examples/config_freqai.example.json`. A full example config is available in `config_examples/config_freqai.example.json`.
!!! Note
The `identifier` is commonly overlooked by newcomers, however, this value plays an important role in your configuration. This value is a unique ID that you choose to describe one of your runs. Keeping it the same allows you to maintain crash resilience as well as faster backtesting. As soon as you want to try a new run (new features, new model, etc.), you should change this value (or delete the `user_data/models/unique-id` folder. More details available in the [parameter table](freqai-parameter-table.md#feature-parameters).
## Building a FreqAI strategy ## Building a FreqAI strategy
The FreqAI strategy requires including the following lines of code in the standard [Freqtrade strategy](strategy-customization.md): The FreqAI strategy requires including the following lines of code in the standard [Freqtrade strategy](strategy-customization.md):
+16 -1
View File
@@ -235,7 +235,7 @@ By default, FreqAI builds a dynamic pipeline based on user congfiguration settin
Users are encouraged to customize the data pipeline to their needs by building their own data pipeline. This can be done by simply setting `dk.feature_pipeline` to their desired `Pipeline` object inside their `IFreqaiModel` `train()` function, or if they prefer not to touch the `train()` function, they can override `define_data_pipeline`/`define_label_pipeline` functions in their `IFreqaiModel`: Users are encouraged to customize the data pipeline to their needs by building their own data pipeline. This can be done by simply setting `dk.feature_pipeline` to their desired `Pipeline` object inside their `IFreqaiModel` `train()` function, or if they prefer not to touch the `train()` function, they can override `define_data_pipeline`/`define_label_pipeline` functions in their `IFreqaiModel`:
!!! note "More information available" !!! note "More information available"
FreqAI uses the the [`DataSieve`](https://github.com/emergentmethods/datasieve) pipeline, which follows the SKlearn pipeline API, but adds, among other features, coherence between the X, y, and sample_weight vector point removals, feature removal, feature name following. FreqAI uses the [`DataSieve`](https://github.com/emergentmethods/datasieve) pipeline, which follows the SKlearn pipeline API, but adds, among other features, coherence between the X, y, and sample_weight vector point removals, feature removal, feature name following.
```python ```python
from datasieve.transforms import SKLearnWrapper, DissimilarityIndex from datasieve.transforms import SKLearnWrapper, DissimilarityIndex
@@ -391,3 +391,18 @@ Given a number of data points $N$, and a distance $\varepsilon$, DBSCAN clusters
![dbscan](assets/freqai_dbscan.jpg) ![dbscan](assets/freqai_dbscan.jpg)
FreqAI uses `sklearn.cluster.DBSCAN` (details are available on scikit-learn's webpage [here](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html) (external website)) with `min_samples` ($N$) taken as 1/4 of the no. of time points (candles) in the feature set. `eps` ($\varepsilon$) is computed automatically as the elbow point in the *k-distance graph* computed from the nearest neighbors in the pairwise distances of all data points in the feature set. FreqAI uses `sklearn.cluster.DBSCAN` (details are available on scikit-learn's webpage [here](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html) (external website)) with `min_samples` ($N$) taken as 1/4 of the no. of time points (candles) in the feature set. `eps` ($\varepsilon$) is computed automatically as the elbow point in the *k-distance graph* computed from the nearest neighbors in the pairwise distances of all data points in the feature set.
### Data dimensionality reduction with Principal Component Analysis
You can reduce the dimensionality of your features by activating the principal_component_analysis in the config:
```json
"freqai": {
"feature_parameters" : {
"principal_component_analysis": true
}
}
```
This will perform PCA on the features and reduce their dimensionality so that the explained variance of the data set is >= 0.999. Reducing data dimensionality makes training the model faster and hence allows for more up-to-date models.
+4 -4
View File
@@ -31,12 +31,12 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](freqai-feature-engineering.md). <br> **Datatype:** Dictionary. | `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](freqai-feature-engineering.md). <br> **Datatype:** Dictionary.
| `include_timeframes` | A list of timeframes that all indicators in `feature_engineering_expand_*()` will be created for. The list is added as features to the base indicators dataset. <br> **Datatype:** List of timeframes (strings). | `include_timeframes` | A list of timeframes that all indicators in `feature_engineering_expand_*()` will be created for. The list is added as features to the base indicators dataset. <br> **Datatype:** List of timeframes (strings).
| `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `feature_engineering_expand_*()` during feature engineering (see details [here](freqai-feature-engineering.md)) will be created for each correlated coin. The correlated coins features are added to the base indicators dataset. <br> **Datatype:** List of assets (strings). | `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `feature_engineering_expand_*()` during feature engineering (see details [here](freqai-feature-engineering.md)) will be created for each correlated coin. The correlated coins features are added to the base indicators dataset. <br> **Datatype:** List of assets (strings).
| `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `feature_engineering_expand_all()` (see `templates/FreqaiExampleStrategy.py` for detailed usage). You can create custom labels and choose whether to make use of this parameter or not. <br> **Datatype:** Positive integer. | `label_period_candles` | Number of candles into the future that the labels are created for. This can be used in `set_freqai_targets()` (see `templates/FreqaiExampleStrategy.py` for detailed usage). This parameter is not necessarily required, you can create custom labels and choose whether to make use of this parameter or not. Please see `templates/FreqaiExampleStrategy.py` to see the example usage. <br> **Datatype:** Positive integer.
| `include_shifted_candles` | Add features from previous candles to subsequent candles with the intent of adding historical information. If used, FreqAI will duplicate and shift all features from the `include_shifted_candles` previous candles so that the information is available for the subsequent candle. <br> **Datatype:** Positive integer. | `include_shifted_candles` | Add features from previous candles to subsequent candles with the intent of adding historical information. If used, FreqAI will duplicate and shift all features from the `include_shifted_candles` previous candles so that the information is available for the subsequent candle. <br> **Datatype:** Positive integer.
| `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)). <br> **Datatype:** Positive float (typically < 1). | `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)). <br> **Datatype:** Positive float (typically < 1).
| `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `feature_engineering_*()` for indicator creation. FreqAI uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN. <br> **Datatype:** Positive integer. | `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `feature_engineering_*()` for indicator creation. FreqAI uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN. <br> **Datatype:** Positive integer.
| `indicator_periods_candles` | Time periods to calculate indicators for. The indicators are added to the base indicator dataset. <br> **Datatype:** List of positive integers. | `indicator_periods_candles` | Time periods to calculate indicators for. The indicators are added to the base indicator dataset. <br> **Datatype:** List of positive integers.
| `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](#reducing-data-dimensionality-with-principal-component-analysis) <br> **Datatype:** Boolean. <br> Default: `False`. | `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis) <br> **Datatype:** Boolean. <br> Default: `False`.
| `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features. Plot is stored in `user_data/models/<identifier>/sub-train-<COIN>_<timestamp>.html`. <br> **Datatype:** Integer. <br> Default: `0`. | `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features. Plot is stored in `user_data/models/<identifier>/sub-train-<COIN>_<timestamp>.html`. <br> **Datatype:** Integer. <br> Default: `0`.
| `DI_threshold` | Activates the use of the Dissimilarity Index for outlier detection when set to > 0. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Positive float (typically < 1). | `DI_threshold` | Activates the use of the Dissimilarity Index for outlier detection when set to > 0. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Positive float (typically < 1).
| `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training dataset, as well as from incoming data points. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Boolean. | `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training dataset, as well as from incoming data points. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Boolean.
@@ -55,7 +55,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| | **Data split parameters within the `freqai.data_split_parameters` sub dictionary** | | **Data split parameters within the `freqai.data_split_parameters` sub dictionary**
| `data_split_parameters` | Include any additional parameters available from scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). <br> **Datatype:** Dictionary. | `data_split_parameters` | Include any additional parameters available from scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). <br> **Datatype:** Dictionary.
| `test_size` | The fraction of data that should be used for testing instead of training. <br> **Datatype:** Positive float < 1. | `test_size` | The fraction of data that should be used for testing instead of training. <br> **Datatype:** Positive float < 1.
| `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`. <br> **Datatype:** Boolean. <br> Defaut: `False`. | `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`. <br> **Datatype:** Boolean. <br> Default: `False`.
### Model training parameters ### Model training parameters
@@ -75,7 +75,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `rl_config` | A dictionary containing the control parameters for a Reinforcement Learning model. <br> **Datatype:** Dictionary. | `rl_config` | A dictionary containing the control parameters for a Reinforcement Learning model. <br> **Datatype:** Dictionary.
| `train_cycles` | Training time steps will be set based on the `train_cycles * number of training data points. <br> **Datatype:** Integer. | `train_cycles` | Training time steps will be set based on the `train_cycles * number of training data points. <br> **Datatype:** Integer.
| `max_trade_duration_candles`| Guides the agent training to keep trades below desired length. Example usage shown in `prediction_models/ReinforcementLearner.py` within the customizable `calculate_reward()` function. <br> **Datatype:** int. | `max_trade_duration_candles`| Guides the agent training to keep trades below desired length. Example usage shown in `prediction_models/ReinforcementLearner.py` within the customizable `calculate_reward()` function. <br> **Datatype:** int.
| `model_type` | Model string from stable_baselines3 or SBcontrib. Available strings include: `'TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'PPO', 'A2C', 'DQN'`. User should ensure that `model_training_parameters` match those available to the corresponding stable_baselines3 model by visiting their documentaiton. [PPO doc](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html) (external website) <br> **Datatype:** string. | `model_type` | Model string from stable_baselines3 or SBcontrib. Available strings include: `'TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'PPO', 'A2C', 'DQN'`. User should ensure that `model_training_parameters` match those available to the corresponding stable_baselines3 model by visiting their documentation. [PPO doc](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html) (external website) <br> **Datatype:** string.
| `policy_type` | One of the available policy types from stable_baselines3 <br> **Datatype:** string. | `policy_type` | One of the available policy types from stable_baselines3 <br> **Datatype:** string.
| `max_training_drawdown_pct` | The maximum drawdown that the agent is allowed to experience during training. <br> **Datatype:** float. <br> Default: 0.8 | `max_training_drawdown_pct` | The maximum drawdown that the agent is allowed to experience during training. <br> **Datatype:** float. <br> Default: 0.8
| `cpu_count` | Number of threads/cpus to dedicate to the Reinforcement Learning training process (depending on if `ReinforcementLearning_multiproc` is selected or not). Recommended to leave this untouched, by default, this value is set to the total number of physical cores minus 1. <br> **Datatype:** int. | `cpu_count` | Number of threads/cpus to dedicate to the Reinforcement Learning training process (depending on if `ReinforcementLearning_multiproc` is selected or not). Recommended to leave this untouched, by default, this value is set to the total number of physical cores minus 1. <br> **Datatype:** int.
+1 -1
View File
@@ -142,7 +142,7 @@ Parameter details can be found [here](freqai-parameter-table.md), but in general
As you begin to modify the strategy and the prediction model, you will quickly realize some important differences between the Reinforcement Learner and the Regressors/Classifiers. Firstly, the strategy does not set a target value (no labels!). Instead, you set the `calculate_reward()` function inside the `MyRLEnv` class (see below). A default `calculate_reward()` is provided inside `prediction_models/ReinforcementLearner.py` to demonstrate the necessary building blocks for creating rewards, but this is *not* designed for production. Users *must* create their own custom reinforcement learning model class or use a pre-built one from outside the Freqtrade source code and save it to `user_data/freqaimodels`. It is inside the `calculate_reward()` where creative theories about the market can be expressed. For example, you can reward your agent when it makes a winning trade, and penalize the agent when it makes a losing trade. Or perhaps, you wish to reward the agent for entering trades, and penalize the agent for sitting in trades too long. Below we show examples of how these rewards are all calculated: As you begin to modify the strategy and the prediction model, you will quickly realize some important differences between the Reinforcement Learner and the Regressors/Classifiers. Firstly, the strategy does not set a target value (no labels!). Instead, you set the `calculate_reward()` function inside the `MyRLEnv` class (see below). A default `calculate_reward()` is provided inside `prediction_models/ReinforcementLearner.py` to demonstrate the necessary building blocks for creating rewards, but this is *not* designed for production. Users *must* create their own custom reinforcement learning model class or use a pre-built one from outside the Freqtrade source code and save it to `user_data/freqaimodels`. It is inside the `calculate_reward()` where creative theories about the market can be expressed. For example, you can reward your agent when it makes a winning trade, and penalize the agent when it makes a losing trade. Or perhaps, you wish to reward the agent for entering trades, and penalize the agent for sitting in trades too long. Below we show examples of how these rewards are all calculated:
!!! note "Hint" !!! note "Hint"
The best reward functions are ones that are continuously differentiable, and well scaled. In other words, adding a single large negative penalty to a rare event is not a good idea, and the neural net will not be able to learn that function. Instead, it is better to add a small negative penalty to a common event. This will help the agent learn faster. Not only this, but you can help improve the continuity of your rewards/penalties by having them scale with severity according to some linear/exponential functions. In other words, you'd slowly scale the penalty as the duration of the trade increases. This is better than a single large penalty occuring at a single point in time. The best reward functions are ones that are continuously differentiable, and well scaled. In other words, adding a single large negative penalty to a rare event is not a good idea, and the neural net will not be able to learn that function. Instead, it is better to add a small negative penalty to a common event. This will help the agent learn faster. Not only this, but you can help improve the continuity of your rewards/penalties by having them scale with severity according to some linear/exponential functions. In other words, you'd slowly scale the penalty as the duration of the trade increases. This is better than a single large penalty occurring at a single point in time.
```python ```python
from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
+3 -4
View File
@@ -14,8 +14,7 @@ To learn how to get data for the pairs and exchange you're interested in, head o
!!! Note !!! Note
Since 2021.4 release you no longer have to write a separate hyperopt class, but can configure the parameters directly in the strategy. Since 2021.4 release you no longer have to write a separate hyperopt class, but can configure the parameters directly in the strategy.
The legacy method is still supported, but it is no longer the recommended way of setting up hyperopt. The legacy method was supported up to 2021.8 and has been removed in 2021.9.
The legacy documentation is available at [Legacy Hyperopt](advanced-hyperopt.md#legacy-hyperopt).
## Install hyperopt dependencies ## Install hyperopt dependencies
@@ -765,7 +764,7 @@ Override the `roi_space()` method if you need components of the ROI tables to va
A sample for these methods can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces). A sample for these methods can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces).
!!! Note "Reduced search space" !!! Note "Reduced search space"
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#overriding-pre-defined-spaces) to change this to your needs.
### Understand Hyperopt Stoploss results ### Understand Hyperopt Stoploss results
@@ -807,7 +806,7 @@ If you have the `stoploss_space()` method in your custom hyperopt file, remove i
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces). Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces).
!!! Note "Reduced search space" !!! Note "Reduced search space"
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#overriding-pre-defined-spaces) to change this to your needs.
### Understand Hyperopt Trailing Stop results ### Understand Hyperopt Trailing Stop results
+43
View File
@@ -0,0 +1,43 @@
## CORS
This whole section is only necessary in cross-origin cases (where you multiple bot API's running on `localhost:8081`, `localhost:8082`, ...), and want to combine them into one FreqUI instance.
??? info "Technical explanation"
All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing.
Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems.
Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately.
Users can allow access from different origin URL's to the bot API via the `CORS_origins` configuration setting.
It consists of a list of allowed URL's that are allowed to consume resources from the bot's API.
Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary:
```jsonc
{
//...
"jwt_secret_key": "somethingrandom",
"CORS_origins": ["https://frequi.freqtrade.io"],
//...
}
```
In the following (pretty common) case, FreqUI is accessible on `http://localhost:8080/trade` (this is what you see in your navbar when navigating to freqUI).
![freqUI url](assets/frequi_url.png)
The correct configuration for this case is `http://localhost:8080` - the main part of the URL including the port.
```jsonc
{
//...
"jwt_secret_key": "somethingrandom",
"CORS_origins": ["http://localhost:8080"],
//...
}
```
!!! Tip "trailing Slash"
The trailing slash is not allowed in the `CORS_origins` configuration (e.g. `"http://localhots:8080/"`).
Such a configuration will not take effect, and the cors errors will remain.
!!! Note
We strongly recommend to also set `jwt_secret_key` to something random and known only to yourself to avoid unauthorized access to your bot.
+72 -4
View File
@@ -6,7 +6,7 @@ In your configuration, you can use Static Pairlist (defined by the [`StaticPairL
Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler. If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList` or `MarketCapPairList` as the starting Pairlist Handler.
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist. Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
@@ -24,6 +24,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
* [`VolumePairList`](#volume-pair-list) * [`VolumePairList`](#volume-pair-list)
* [`ProducerPairList`](#producerpairlist) * [`ProducerPairList`](#producerpairlist)
* [`RemotePairList`](#remotepairlist) * [`RemotePairList`](#remotepairlist)
* [`MarketCapPairList`](#marketcappairlist)
* [`AgeFilter`](#agefilter) * [`AgeFilter`](#agefilter)
* [`FullTradesFilter`](#fulltradesfilter) * [`FullTradesFilter`](#fulltradesfilter)
* [`OffsetFilter`](#offsetfilter) * [`OffsetFilter`](#offsetfilter)
@@ -67,7 +68,7 @@ When used in the leading position of the chain of Pairlist Handlers, the `pair_w
The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists. The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists.
Filtering instances (not the first position in the list) will not apply any cache and will always use up-to-date data. Filtering instances (not the first position in the list) will not apply any cache (beyond caching candles for the duration of the candle in advanced mode) and will always use up-to-date data.
`VolumePairList` is per default based on the ticker data from exchange, as reported by the ccxt library: `VolumePairList` is per default based on the ticker data from exchange, as reported by the ccxt library:
@@ -80,12 +81,14 @@ Filtering instances (not the first position in the list) will not apply any cach
"number_assets": 20, "number_assets": 20,
"sort_key": "quoteVolume", "sort_key": "quoteVolume",
"min_value": 0, "min_value": 0,
"max_value": 8000000,
"refresh_period": 1800 "refresh_period": 1800
} }
], ],
``` ```
You can define a minimum volume with `min_value` - which will filter out pairs with a volume lower than the specified value in the specified timerange. You can define a minimum volume with `min_value` - which will filter out pairs with a volume lower than the specified value in the specified timerange.
In addition to that, you can also define a maximum volume with `max_value` - which will filter out pairs with a volume higher than the specified value in the specified timerange.
##### VolumePairList Advanced mode ##### VolumePairList Advanced mode
@@ -192,14 +195,15 @@ The RemotePairList is defined in the pairlists section of the configuration sett
"refresh_period": 1800, "refresh_period": 1800,
"keep_pairlist_on_failure": true, "keep_pairlist_on_failure": true,
"read_timeout": 60, "read_timeout": 60,
"bearer_token": "my-bearer-token" "bearer_token": "my-bearer-token",
"save_to_file": "user_data/filename.json"
} }
] ]
``` ```
The optional `mode` option specifies if the pairlist should be used as a `blacklist` or as a `whitelist`. The default value is "whitelist". The optional `mode` option specifies if the pairlist should be used as a `blacklist` or as a `whitelist`. The default value is "whitelist".
The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append". The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append". The default value is "filter".
In "filter" mode, the retrieved pairlist is used as a filter. Only the pairs present in both the original pairlist and the retrieved pairlist are included in the final pairlist. Other pairs are filtered out. In "filter" mode, the retrieved pairlist is used as a filter. Only the pairs present in both the original pairlist and the retrieved pairlist are included in the final pairlist. Other pairs are filtered out.
@@ -207,6 +211,42 @@ In "append" mode, the retrieved pairlist is added to the original pairlist. All
The `pairlist_url` option specifies the URL of the remote server where the pairlist is located, or the path to a local file (if file:/// is prepended). This allows the user to use either a remote server or a local file as the source for the pairlist. The `pairlist_url` option specifies the URL of the remote server where the pairlist is located, or the path to a local file (if file:/// is prepended). This allows the user to use either a remote server or a local file as the source for the pairlist.
The `save_to_file` option, when provided with a valid filename, saves the processed pairlist to that file in JSON format. This option is optional, and by default, the pairlist is not saved to a file.
??? Example "Multi bot with shared pairlist example"
`save_to_file` can be used to save the pairlist to a file with Bot1:
```json
"pairlists": [
{
"method": "RemotePairList",
"mode": "whitelist",
"pairlist_url": "https://example.com/pairlist",
"number_assets": 10,
"refresh_period": 1800,
"keep_pairlist_on_failure": true,
"read_timeout": 60,
"save_to_file": "user_data/filename.json"
}
]
```
This saved pairlist file can be loaded by Bot2, or any additional bot with this configuration:
```json
"pairlists": [
{
"method": "RemotePairList",
"mode": "whitelist",
"pairlist_url": "file:///user_data/filename.json",
"number_assets": 10,
"refresh_period": 10,
"keep_pairlist_on_failure": true,
}
]
```
The user is responsible for providing a server or local file that returns a JSON object with the following structure: The user is responsible for providing a server or local file that returns a JSON object with the following structure:
```json ```json
@@ -227,6 +267,25 @@ The optional `bearer_token` will be included in the requests Authorization Heade
!!! Note !!! Note
In case of a server error the last received pairlist will be kept if `keep_pairlist_on_failure` is set to true, when set to false a empty pairlist is returned. In case of a server error the last received pairlist will be kept if `keep_pairlist_on_failure` is set to true, when set to false a empty pairlist is returned.
#### MarketCapPairList
`MarketCapPairList` employs sorting/filtering of pairs by their marketcap rank based of CoinGecko. It will only recognize coins up to the coin placed at rank 250. The returned pairlist will be sorted based of their marketcap ranks.
```json
"pairlists": [
{
"method": "MarketCapPairList",
"number_assets": 20,
"max_rank": 50,
"refresh_period": 86400
}
]
```
`number_assets` defines the maximum number of pairs returned by the pairlist. `max_rank` will determine the maximum rank used in creating/filtering the pairlist. It's expected that some coins within the top `max_rank` marketcap will not be included in the resulting pairlist since not all pairs will have active trading pairs in your preferred market/stake/exchange combination.
`refresh_period` setting defines the period (in seconds) at which the marketcap rank data will be refreshed. Defaults to 86,400s (1 day). The pairlist cache (`refresh_period`) is applicable on both generating pairlists (first position in the list) and filtering instances (not the first position in the list).
#### AgeFilter #### 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). 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).
@@ -312,6 +371,11 @@ As this Filter uses past performance of the bot, it'll have some startup-period
Filters low-value coins which would not allow setting stoplosses. Filters low-value coins which would not allow setting stoplosses.
Namely, pairs are blacklisted if a variance of one percent or more in the stop price would be caused by precision rounding on the exchange, i.e. `rounded(stop_price) <= rounded(stop_price * 0.99)`. The idea is to avoid coins with a value VERY close to their lower trading boundary, not allowing setting of proper stoploss.
!!! Tip "PerformanceFilter is pointless for futures trading"
The above does not apply to shorts. And for longs, in theory the trade will be liquidated first.
!!! Warning "Backtesting" !!! Warning "Backtesting"
`PrecisionFilter` does not support backtesting mode using multiple strategies. `PrecisionFilter` does not support backtesting mode using multiple strategies.
@@ -393,6 +457,8 @@ If the trading range over the last 10 days is <1% or >99%, remove the pair from
] ]
``` ```
Adding `"sort_direction": "asc"` or `"sort_direction": "desc"` enables sorting for this pairlist.
!!! Tip !!! Tip
This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit. This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit.
Additionally, it can also be used to automatically remove pairs with extreme high/low variance over a given amount of time. Additionally, it can also be used to automatically remove pairs with extreme high/low variance over a given amount of time.
@@ -420,6 +486,8 @@ If the volatility over the last 10 days is not in the range of 0.05-0.50, remove
] ]
``` ```
Adding `"sort_direction": "asc"` or `"sort_direction": "desc"` enables sorting mode for this pairlist.
### Full example of Pairlist Handlers ### Full example of Pairlist Handlers
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#pricefilter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value. The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#pricefilter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value.
+2 -2
View File
@@ -51,7 +51,7 @@ These requirements apply to both [Script Installation](#script-installation) and
### Install code ### Install code
We've included/collected install instructions for Ubuntu, MacOS, and Windows. These are guidelines and your success may vary with other distros. We've included/collected install instructions for Ubuntu, MacOS, and Windows. These are guidelines and your success may vary with other distros.
OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. OS Specific steps are listed first, the common section below is necessary for all systems.
!!! Note !!! Note
Python3.9 or higher and the corresponding pip are assumed to be available. Python3.9 or higher and the corresponding pip are assumed to be available.
@@ -286,7 +286,7 @@ cd freqtrade
#### Freqtrade install: Conda Environment #### Freqtrade install: Conda Environment
```bash ```bash
conda create --name freqtrade python=3.11 conda create --name freqtrade python=3.12
``` ```
!!! Note "Creating Conda Environment" !!! Note "Creating Conda Environment"
+1 -1
View File
@@ -17,7 +17,7 @@ If you already have an existing strategy, please read the [strategy migration gu
## Shorting ## Shorting
Shorting is not possible when trading with [`trading_mode`](#understand-tradingmode) set to `spot`. To short trade, `trading_mode` must be set to `margin`(currently unavailable) or [`futures`](#futures), with [`margin_mode`](#margin-mode) set to `cross`(currently unavailable) or [`isolated`](#isolated-margin-mode) Shorting is not possible when trading with [`trading_mode`](#leverage-trading-modes) set to `spot`. To short trade, `trading_mode` must be set to `margin`(currently unavailable) or [`futures`](#futures), with [`margin_mode`](#margin-mode) set to `cross`(currently unavailable) or [`isolated`](#isolated-margin-mode)
For a strategy to short, the strategy class must set the class variable `can_short = True` For a strategy to short, the strategy class must set the class variable `can_short = True`
+1 -1
View File
@@ -23,6 +23,7 @@ It also supports the lookahead-analysis of freqai strategies.
- `--max-open-trades` is forced to be at least equal to the number of pairs. - `--max-open-trades` is forced to be at least equal to the number of pairs.
- `--dry-run-wallet` is forced to be basically infinite (1 billion). - `--dry-run-wallet` is forced to be basically infinite (1 billion).
- `--stake-amount` is forced to be a static 10000 (10k). - `--stake-amount` is forced to be a static 10000 (10k).
- `--enable-protections` is forced to be off.
Those are set to avoid users accidentally generating false positives. Those are set to avoid users accidentally generating false positives.
@@ -40,7 +41,6 @@ usage: freqtrade lookahead-analysis [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--max-open-trades INT] [--max-open-trades INT]
[--stake-amount STAKE_AMOUNT] [--stake-amount STAKE_AMOUNT]
[--fee FLOAT] [-p PAIRS [PAIRS ...]] [--fee FLOAT] [-p PAIRS [PAIRS ...]]
[--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET] [--dry-run-wallet DRY_RUN_WALLET]
[--timeframe-detail TIMEFRAME_DETAIL] [--timeframe-detail TIMEFRAME_DETAIL]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
+5 -5
View File
@@ -1,6 +1,6 @@
markdown==3.5.2 markdown==3.6
mkdocs==1.5.3 mkdocs==1.6.0
mkdocs-material==9.5.4 mkdocs-material==9.5.22
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==10.7 pymdown-extensions==10.8.1
jinja2==3.1.3 jinja2==3.1.4
+32 -56
View File
@@ -1,16 +1,8 @@
# REST API & FreqUI # REST API
## FreqUI ## FreqUI
Freqtrade provides a builtin webserver, which can serve [FreqUI](https://github.com/freqtrade/frequi), the freqtrade UI. FreqUI now has it's own dedicated [documentation section](frequi.md) - please refer to that section for all information regarding the FreqUI.
By default, the UI is not included in the installation (except for docker images), and must be installed explicitly with `freqtrade install-ui`.
This same command can also be used to update freqUI, should there be a new release.
Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`).
!!! Note "developers"
Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI.
## Configuration ## Configuration
@@ -89,17 +81,20 @@ Make sure that the following 2 lines are available in your docker-compose file:
``` ```
!!! Danger "Security warning" !!! Danger "Security warning"
By using `8080:8080` in the docker port mapping, the API will be available to everyone connecting to the server under the correct port, so others may be able to control your bot. By using `"8080:8080"` (or `"0.0.0.0:8080:8080"`) in the docker port mapping, the API will be available to everyone connecting to the server under the correct port, so others may be able to control your bot.
This **may** be safe if you're running the bot in a secure environment (like your home network), but it's not recommended to expose the API to the internet.
## Rest API ## Rest API
### Consuming the API ### Consuming the API
You can consume the API by using the script `scripts/rest_client.py`. You can consume the API by using `freqtrade-client` (also available as `scripts/rest_client.py`).
The client script only requires the `requests` module, so Freqtrade does not need to be installed on the system. This command can be installed independent of the bot by using `pip install freqtrade-client`.
This module is designed to be lightweight, and only depends on the `requests` and `python-rapidjson` modules, skipping all heavy dependencies freqtrade otherwise needs.
``` bash ``` bash
python3 scripts/rest_client.py <command> [optional parameters] freqtrade-client <command> [optional parameters]
``` ```
By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour. By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour.
@@ -120,9 +115,27 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
``` ```
``` bash ``` bash
python3 scripts/rest_client.py --config rest_config.json <command> [optional parameters] freqtrade-client --config rest_config.json <command> [optional parameters]
``` ```
??? Note "Programmatic use"
The `freqtrade-client` package (installable independent of freqtrade) can be used in your own scripts to interact with the freqtrade API.
to do so, please use the following:
``` python
from freqtrade_client import FtRestClient
client = FtRestClient(server_url, username, password)
# Get the status of the bot
ping = client.ping()
print(ping)
# ...
```
For a full list of available commands, please refer to the list below.
### Available endpoints ### Available endpoints
| Command | Description | | Command | Description |
@@ -146,6 +159,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `mix_tags [pair]` | Shows profit statistics for each combinations of enter tag + exit reasons for given pair (or all pairs if pair isn't given). Pair is optional. | `mix_tags [pair]` | Shows profit statistics for each combinations of enter tag + exit reasons for given pair (or all pairs if pair isn't given). Pair is optional.
| `locks` | Displays currently locked pairs. | `locks` | Displays currently locked pairs.
| `delete_lock <lock_id>` | Deletes (disables) the lock by id. | `delete_lock <lock_id>` | Deletes (disables) the lock by id.
| `locks add <pair>, <until>, [side], [reason]` | Locks a pair until "until". (Until will be rounded up to the nearest timeframe).
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance. | `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
| `forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`). | `forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`). | `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).
@@ -176,7 +190,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
Possible commands can be listed from the rest-client script using the `help` command. Possible commands can be listed from the rest-client script using the `help` command.
``` bash ``` bash
python3 scripts/rest_client.py help freqtrade-client help
``` ```
``` output ``` output
@@ -433,7 +447,7 @@ To properly configure your reverse proxy (securely), please consult it's documen
- **Caddy**: Caddy v2 supports websockets out of the box, see the [documentation](https://caddyserver.com/docs/v2-upgrade#proxy) - **Caddy**: Caddy v2 supports websockets out of the box, see the [documentation](https://caddyserver.com/docs/v2-upgrade#proxy)
!!! Tip "SSL certificates" !!! Tip "SSL certificates"
You can use tools like certbot to setup ssl certificates to access your bot's UI through encrypted connection by using any fo the above reverse proxies. You can use tools like certbot to setup ssl certificates to access your bot's UI through encrypted connection by using any of the above reverse proxies.
While this will protect your data in transit, we do not recommend to run the freqtrade API outside of your private network (VPN, SSH tunnel). While this will protect your data in transit, we do not recommend to run the freqtrade API outside of your private network (VPN, SSH tunnel).
### OpenAPI interface ### OpenAPI interface
@@ -466,42 +480,4 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"} {"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"}
``` ```
### CORS --8<-- "includes/cors.md"
This whole section is only necessary in cross-origin cases (where you multiple bot API's running on `localhost:8081`, `localhost:8082`, ...), and want to combine them into one FreqUI instance.
??? info "Technical explanation"
All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing.
Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems.
Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately.
Users can allow access from different origin URL's to the bot API via the `CORS_origins` configuration setting.
It consists of a list of allowed URL's that are allowed to consume resources from the bot's API.
Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary:
```jsonc
{
//...
"jwt_secret_key": "somethingrandom",
"CORS_origins": ["https://frequi.freqtrade.io"],
//...
}
```
In the following (pretty common) case, FreqUI is accessible on `http://localhost:8080/trade` (this is what you see in your navbar when navigating to freqUI).
![freqUI url](assets/frequi_url.png)
The correct configuration for this case is `http://localhost:8080` - the main part of the URL including the port.
```jsonc
{
//...
"jwt_secret_key": "somethingrandom",
"CORS_origins": ["http://localhost:8080"],
//...
}
```
!!! Note
We strongly recommend to also set `jwt_secret_key` to something random and known only to yourself to avoid unauthorized access to your bot.
+1 -1
View File
@@ -109,7 +109,7 @@ Freqtrade does not depend or install any additional database driver. Please refe
The following systems have been tested and are known to work with freqtrade: The following systems have been tested and are known to work with freqtrade:
* sqlite (default) * sqlite (default)
* PostgreSQL) * PostgreSQL
* MariaDB * MariaDB
!!! Warning !!! Warning
+2 -2
View File
@@ -158,7 +158,7 @@ You could also have a default stop loss when you are in the red with your buy (b
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. 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.
!!! Note !!! 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). 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).
Both values require `trailing_stop` to be set to true and `trailing_stop_positive` with a value. Both values require `trailing_stop` to be set to true and `trailing_stop_positive` with a value.
@@ -240,7 +240,7 @@ When using leverage, the same principle is applied - with stoploss defining the
Therefore, a stoploss of 10% on a 10x trade would trigger on a 1% price move. Therefore, a stoploss of 10% on a 10x trade would trigger on a 1% price move.
If your stake amount (own capital) was 100$ - this trade would be 1000$ at 10x (after leverage). If your stake amount (own capital) was 100$ - this trade would be 1000$ at 10x (after leverage).
If price moves 1% - you've lost 10$ of your own capital - therfore stoploss will trigger in this case. If price moves 1% - you've lost 10$ of your own capital - therefore stoploss will trigger in this case.
Make sure to be aware of this, and avoid using too tight stoploss (at 10x leverage, 10% risk may be too little to allow the trade to "breath" a little). Make sure to be aware of this, and avoid using too tight stoploss (at 10x leverage, 10% risk may be too little to allow the trade to "breath" a little).
+98 -3
View File
@@ -11,8 +11,103 @@ The call sequence of the methods described here is covered under [bot execution
!!! Tip !!! Tip
Start off with a strategy template containing all available callback methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` Start off with a strategy template containing all available callback methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced`
## Storing information ## Storing information (Persistent)
Freqtrade allows storing/retrieving user custom information associated with a specific trade in the database.
Using a trade object, information can be stored using `trade.set_custom_data(key='my_key', value=my_value)` and retrieved using `trade.get_custom_data(key='my_key')`. Each data entry is associated with a trade and a user supplied key (of type `string`). This means that this can only be used in callbacks that also provide a trade object.
For the data to be able to be stored within the database, freqtrade must serialized the data. This is done by converting the data to a JSON formatted string.
Freqtrade will attempt to reverse this action on retrieval, so from a strategy perspective, this should not be relevant.
```python
from freqtrade.persistence import Trade
from datetime import timedelta
class AwesomeStrategy(IStrategy):
def bot_loop_start(self, **kwargs) -> None:
for trade in Trade.get_open_order_trades():
fills = trade.select_filled_orders(trade.entry_side)
if trade.pair == 'ETH/USDT':
trade_entry_type = trade.get_custom_data(key='entry_type')
if trade_entry_type is None:
trade_entry_type = 'breakout' if 'entry_1' in trade.enter_tag else 'dip'
elif fills > 1:
trade_entry_type = 'buy_up'
trade.set_custom_data(key='entry_type', value=trade_entry_type)
return super().bot_loop_start(**kwargs)
def adjust_entry_price(self, trade: Trade, order: Optional[Order], pair: str,
current_time: datetime, proposed_rate: float, current_order_rate: float,
entry_tag: Optional[str], side: str, **kwargs) -> float:
# Limit 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'
and entry_tag == 'long_sma200'
and side == 'long'
and (current_time - timedelta(minutes=10)) > trade.open_date_utc
and order.filled == 0.0
):
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
current_candle = dataframe.iloc[-1].squeeze()
# store information about entry adjustment
existing_count = trade.get_custom_data('num_entry_adjustments', default=0)
if not existing_count:
existing_count = 1
else:
existing_count += 1
trade.set_custom_data(key='num_entry_adjustments', value=existing_count)
# adjust order price
return current_candle['sma_200']
# default: maintain existing order
return current_order_rate
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs):
entry_adjustment_count = trade.get_custom_data(key='num_entry_adjustments')
trade_entry_type = trade.get_custom_data(key='entry_type')
if entry_adjustment_count is None:
if current_profit > 0.01 and (current_time - timedelta(minutes=100) > trade.open_date_utc):
return True, 'exit_1'
else
if entry_adjustment_count > 0 and if current_profit > 0.05:
return True, 'exit_2'
if trade_entry_type == 'breakout' and current_profit > 0.1:
return True, 'exit_3
return False, None
```
The above is a simple example - there are simpler ways to retrieve trade data like entry-adjustments.
!!! Note
It is recommended that simple data types are used `[bool, int, float, str]` to ensure no issues when serializing the data that needs to be stored.
Storing big junks of data may lead to unintended side-effects, like a database becoming big (and as a consequence, also slow).
!!! Warning "Non-serializable data"
If supplied data cannot be serialized a warning is logged and the entry for the specified `key` will contain `None` as data.
??? Note "All attributes"
custom-data has the following accessors through the Trade object (assumed as `trade` below):
* `trade.get_custom_data(key='something', default=0)` - Returns the actual value given in the type provided.
* `trade.get_custom_data_entry(key='something')` - Returns the entry - including metadata. The value is accessible via `.value` property.
* `trade.set_custom_data(key='something', value={'some': 'value'})` - set or update the corresponding key for this trade. Value must be serializable - and we recommend to keep the stored data relatively small.
"value" can be any type (both in setting and receiving) - but must be json serializable.
## Storing information (Non-Persistent)
!!! Warning "Deprecated"
This method of storing information is deprecated and we do advise against using non-persistent storage.
Please use [Persistent Storage](#storing-information-persistent) instead.
It's content has therefore been collapsed.
??? Abstract "Storing information"
Storing information can be accomplished by creating a new dictionary within the strategy class. Storing information can be accomplished by creating a new dictionary within the strategy class.
The name of the variable can be chosen at will, but should be prefixed with `custom_` to avoid naming collisions with predefined strategy variables. The name of the variable can be chosen at will, but should be prefixed with `custom_` to avoid naming collisions with predefined strategy variables.
@@ -114,7 +209,7 @@ def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_r
## Exit tag ## Exit tag
Similar to [Buy Tagging](#buy-tag), you can also specify a sell tag. Similar to [Entry Tagging](#enter-tag), you can also specify an exit tag.
``` python ``` python
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@@ -231,4 +326,4 @@ for val in self.buy_ema_short.range:
dataframe = pd.concat(frames, axis=1) dataframe = pd.concat(frames, axis=1)
``` ```
Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existant. Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existent.
+49 -9
View File
@@ -19,6 +19,7 @@ Currently available callbacks:
* [`adjust_trade_position()`](#adjust-trade-position) * [`adjust_trade_position()`](#adjust-trade-position)
* [`adjust_entry_price()`](#adjust-entry-price) * [`adjust_entry_price()`](#adjust-entry-price)
* [`leverage()`](#leverage-callback) * [`leverage()`](#leverage-callback)
* [`order_filled()`](#order-filled-callback)
!!! Tip "Callback calling sequence" !!! Tip "Callback calling sequence"
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic) You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
@@ -166,7 +167,7 @@ During backtesting, `current_rate` (and `current_profit`) are provided against t
The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price. The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price.
Returning None will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss. Returning None will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss.
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)). Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchangefreqtrade)).
!!! Note "Use of dates" !!! Note "Use of dates"
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support. All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
@@ -331,7 +332,7 @@ class AwesomeStrategy(IStrategy):
**kwargs) -> Optional[float]: **kwargs) -> Optional[float]:
if current_profit < 0.04: if current_profit < 0.04:
return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss return None # return None to keep using the initial stoploss
# After reaching the desired offset, allow the stoploss to trail by half the profit # After reaching the desired offset, allow the stoploss to trail by half the profit
desired_stoploss = current_profit / 2 desired_stoploss = current_profit / 2
@@ -449,7 +450,7 @@ Stoploss values returned from `custom_stoploss()` must specify a percentage rela
``` ```
Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation. Full examples can be found in the [Custom stoploss](strategy-callbacks.md#custom-stoploss) section of the Documentation.
!!! Note !!! Note
Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings. Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings.
@@ -767,6 +768,7 @@ This callback is **not** called when there is an open order (either buy or sell)
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. `adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade.
Adjustment orders can be assigned with a tag by returning a 2 element Tuple, with the first element being the adjustment amount, and the 2nd element the tag (e.g. `return 250, 'increase_favorable_conditions'`).
Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage. Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage.
@@ -782,7 +784,7 @@ Additional entries are ignored once you have reached the maximum amount of extra
### Decrease position ### Decrease position
The strategy is expected to return a negative stake_amount (in stake currency) for a partial exit. The strategy is expected to return a negative stake_amount (in stake currency) for a partial exit.
Returning the full owned stake at that point (based on the current price) (`-(trade.amount / trade.leverage) * current_exit_rate`) results in a full exit. Returning the full owned stake at that point (`-trade.stake_amount`) results in a full exit.
Returning a value more than the above (so remaining stake_amount would become negative) will result in the bot ignoring the signal. Returning a value more than the above (so remaining stake_amount would become negative) will result in the bot ignoring the signal.
!!! Note "About stake size" !!! Note "About stake size"
@@ -790,7 +792,7 @@ Returning a value more than the above (so remaining stake_amount would become ne
If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that. If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that.
Using 'unlimited' stake amount with DCA orders requires you to also implement the `custom_stake_amount()` callback to avoid allocating all funds to the initial order. Using 'unlimited' stake amount with DCA orders requires you to also implement the `custom_stake_amount()` callback to avoid allocating all funds to the initial order.
!!! Warning !!! Warning "Stoploss calculation"
Stoploss is still calculated from the initial opening price, not averaged price. Stoploss is still calculated from the initial opening price, not averaged price.
Regular stoploss rules still apply (cannot move down). Regular stoploss rules still apply (cannot move down).
@@ -800,8 +802,14 @@ Returning a value more than the above (so remaining stake_amount would become ne
During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected. During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected.
This can also cause deviating results between live and backtesting, since backtesting can adjust the trade only once per candle, whereas live could adjust the trade multiple times per candle. This can also cause deviating results between live and backtesting, since backtesting can adjust the trade only once per candle, whereas live could adjust the trade multiple times per candle.
!!! 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.
Each of the orders will be attached to the trade object for the duration of the trade - hence increasing memory usage.
Trades with long duration and 10s or even 100ds of position adjustments are therefore not recommended, and should be closed at regular intervals to not affect performance.
``` python ``` python
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from typing import Optional, Tuple, Union
class DigDeeperStrategy(IStrategy): class DigDeeperStrategy(IStrategy):
@@ -833,7 +841,8 @@ class DigDeeperStrategy(IStrategy):
min_stake: Optional[float], max_stake: float, min_stake: Optional[float], max_stake: float,
current_entry_rate: float, current_exit_rate: float, current_entry_rate: float, current_exit_rate: float,
current_entry_profit: float, current_exit_profit: float, current_entry_profit: float, current_exit_profit: float,
**kwargs) -> Optional[float]: **kwargs
) -> Union[Optional[float], Tuple[Optional[float], Optional[str]]]:
""" """
Custom trade adjustment logic, returning the stake amount that a trade should be Custom trade adjustment logic, returning the stake amount that a trade should be
increased or decreased. increased or decreased.
@@ -859,11 +868,12 @@ class DigDeeperStrategy(IStrategy):
:return float: Stake amount to adjust your trade, :return float: Stake amount to adjust your trade,
Positive values to increase position, Negative values to decrease position. Positive values to increase position, Negative values to decrease position.
Return None for no action. Return None for no action.
Optionally, return a tuple with a 2nd element with an order reason
""" """
if current_profit > 0.05 and trade.nr_of_successful_exits == 0: if current_profit > 0.05 and trade.nr_of_successful_exits == 0:
# Take half of the profit at +5% # Take half of the profit at +5%
return -(trade.stake_amount / 2) return -(trade.stake_amount / 2), 'half_profit_5%'
if current_profit > -0.05: if current_profit > -0.05:
return None return None
@@ -891,7 +901,7 @@ class DigDeeperStrategy(IStrategy):
stake_amount = filled_entries[0].stake_amount stake_amount = filled_entries[0].stake_amount
# This then calculates current safety order size # This then calculates current safety order size
stake_amount = stake_amount * (1 + (count_of_entries * 0.25)) stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
return stake_amount return stake_amount, '1/3rd_increase'
except Exception as exception: except Exception as exception:
return None return None
@@ -939,7 +949,7 @@ If the cancellation of the original order fails, then the order will not be repl
```python ```python
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from datetime import timedelta from datetime import timedelta, datetime
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@@ -1014,3 +1024,33 @@ class AwesomeStrategy(IStrategy):
All profit calculations include leverage. Stoploss / ROI also include leverage in their calculation. All profit calculations include leverage. Stoploss / ROI also include leverage in their calculation.
Defining a stoploss of 10% at 10x leverage would trigger the stoploss with a 1% move to the downside. Defining a stoploss of 10% at 10x leverage would trigger the stoploss with a 1% move to the downside.
## Order filled Callback
The `order_filled()` callback may be used to perform specific actions based on the current trade state after an order is filled.
It will be called independent of the order type (entry, exit, stoploss or position adjustment).
Assuming that your strategy needs to store the high value of the candle at trade entry, this is possible with this callback as the following example show.
``` python
class AwesomeStrategy(IStrategy):
def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None:
"""
Called right after an order fills.
Will be called for all order types (entry, exit, stoploss, position adjustment).
:param pair: Pair for trade
:param trade: trade object.
:param order: Order object.
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
# Obtain pair dataframe (just to show how to access it)
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side):
trade.set_custom_data(key='entry_candle_high', value=last_candle['high'])
return None
```
+4 -4
View File
@@ -405,7 +405,7 @@ The metadata-dict (available for `populate_entry_trend`, `populate_exit_trend`,
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`. Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
The Metadata-dict should not be modified and does not persist information across multiple calls. The Metadata-dict should not be modified and does not persist information across multiple calls.
Instead, have a look at the [Storing information](strategy-advanced.md#Storing-information) section. Instead, have a look at the [Storing information](strategy-advanced.md#storing-information-persistent) section.
## Strategy file loading ## Strategy file loading
@@ -551,8 +551,8 @@ for more information.
# Define BTC/STAKE informative pair. A custom formatter may be specified for formatting # Define BTC/STAKE informative pair. A custom formatter may be specified for formatting
# column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom # column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom
# formatting. Available in populate_indicators and other methods as 'rsi_upper'. # formatting. Available in populate_indicators and other methods as 'rsi_upper_1h'.
@informative('1h', 'BTC/{stake}', '{column}') @informative('1h', 'BTC/{stake}', '{column}_{timeframe}')
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14) dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
return dataframe return dataframe
@@ -776,7 +776,7 @@ The orderbook structure is aligned with the order structure from [ccxt](https://
Therefore, using `ob['bids'][0][0]` as demonstrated above will result in using the best bid price. `ob['bids'][0][1]` would look at the amount at this orderbook position. Therefore, using `ob['bids'][0][0]` as demonstrated above will result in using the best bid price. `ob['bids'][0][1]` would look at the amount at this orderbook position.
!!! Warning "Warning about backtesting" !!! Warning "Warning about backtesting"
The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used, as the method will return uptodate values. The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used, as the method will return up-to-date values.
### *ticker(pair)* ### *ticker(pair)*
+1 -1
View File
@@ -19,7 +19,7 @@ from pathlib import Path
project_root = "somedir/freqtrade" project_root = "somedir/freqtrade"
i=0 i=0
try: try:
os.chdirdir(project_root) os.chdir(project_root)
assert Path('LICENSE').is_file() assert Path('LICENSE').is_file()
except: except:
while i<4 and (not Path('LICENSE').is_file()): while i<4 and (not Path('LICENSE').is_file()):
+3 -2
View File
@@ -53,13 +53,13 @@ You can use bots in telegram groups by just adding them to the group. You can fi
} }
``` ```
For the Freqtrade configuration, you can then use the the full value (including `-` if it's there) as string: For the Freqtrade configuration, you can then use the full value (including `-` if it's there) as string:
```json ```json
"chat_id": "-1001332619709" "chat_id": "-1001332619709"
``` ```
!!! Warning "Using telegram groups" !!! Warning "Using telegram groups"
When using telegram groups, you're giving every member of the telegram group access to your freqtrade bot and to all commands possible via telegram. Please make sure that you can trust everyone in the telegram group to avoid unpleasent surprises. When using telegram groups, you're giving every member of the telegram group access to your freqtrade bot and to all commands possible via telegram. Please make sure that you can trust everyone in the telegram group to avoid unpleasant surprises.
## Control telegram noise ## Control telegram noise
@@ -181,6 +181,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/locks` | Show currently locked pairs. | `/locks` | Show currently locked pairs.
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id). | `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
| `/marketdir [long | short | even | none]` | Updates the user managed variable that represents the current market direction. If no direction is provided, the currently set direction will be displayed. | `/marketdir [long | short | even | none]` | Updates the user managed variable that represents the current market direction. If no direction is provided, the currently set direction will be displayed.
| `/list_custom_data <trade_id> [key]` | List custom_data for Trade ID & Key combination. If no Key is supplied it will list all key-value pairs found for that Trade ID.
| **Modify Trade states** | | **Modify Trade states** |
| `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`). | `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`). | `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`).
+2 -2
View File
@@ -126,7 +126,7 @@ An `Order` object will always be tied to it's corresponding [`Trade`](#trade-obj
### Order - Available attributes ### Order - Available attributes
an Order object is typically attached to a trade. an Order object is typically attached to a trade.
Most properties here can be None as they are dependant on the exchange response. Most properties here can be None as they are dependent on the exchange response.
| Attribute | DataType | Description | | Attribute | DataType | Description |
|------------|-------------|-------------| |------------|-------------|-------------|
@@ -141,7 +141,7 @@ Most properties here can be None as they are dependant on the exchange response.
`amount` | float | Amount in base currency `amount` | float | Amount in base currency
`filled` | float | Filled amount (in base currency) `filled` | float | Filled amount (in base currency)
`remaining` | float | Remaining amount `remaining` | float | Remaining amount
`cost` | float | Cost of the order - usually average * filled (*Exchange dependant on futures, may contain the cost with or without leverage and may be in contracts.*) `cost` | float | Cost of the order - usually average * filled (*Exchange dependent on futures, may contain the cost with or without leverage and may be in contracts.*)
`stake_amount` | float | Stake amount used for this order. *Added in 2023.7.* `stake_amount` | float | Stake amount used for this order. *Added in 2023.7.*
`order_date` | datetime | Order creation date **use `order_date_utc` instead** `order_date` | datetime | Order creation date **use `order_date_utc` instead**
`order_date_utc` | datetime | Order creation date (in UTC) `order_date_utc` | datetime | Order creation date (in UTC)
+1 -1
View File
@@ -6,7 +6,7 @@ To update your freqtrade installation, please use one of the below methods, corr
Breaking changes / changed behavior will be documented in the changelog that is posted alongside every release. Breaking changes / changed behavior will be documented in the changelog that is posted alongside every release.
For the develop branch, please follow PR's to avoid being surprised by changes. For the develop branch, please follow PR's to avoid being surprised by changes.
## docker ## Docker
!!! Note "Legacy installations using the `master` image" !!! Note "Legacy installations using the `master` image"
We're switching from master to stable for the release Images - please adjust your docker-file and replace `freqtradeorg/freqtrade:master` with `freqtradeorg/freqtrade:stable` We're switching from master to stable for the release Images - please adjust your docker-file and replace `freqtradeorg/freqtrade:master` with `freqtradeorg/freqtrade:stable`
+79 -194
View File
@@ -54,7 +54,7 @@ optional arguments:
### Create config examples ### Create config examples
``` ```
$ freqtrade new-config --config config_binance.json $ freqtrade new-config --config user_data/config_binance.json
? Do you want to enable Dry-run (simulated trades)? Yes ? Do you want to enable Dry-run (simulated trades)? Yes
? Please insert your stake currency: BTC ? Please insert your stake currency: BTC
@@ -66,6 +66,53 @@ $ freqtrade new-config --config config_binance.json
? Do you want to enable Telegram? No ? Do you want to enable Telegram? No
``` ```
## Show config
Show configuration file (with sensitive values redacted by default).
Especially useful with [split configuration files](configuration.md#multiple-configuration-files) or [environment variables](configuration.md#environment-variables), where this command will show the merged configuration.
![Show config output](assets/show-config-output.png)
```
usage: freqtrade show-config [-h] [--userdir PATH] [-c PATH]
[--show-sensitive]
options:
-h, --help show this help message and exit
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
-c PATH, --config PATH
Specify configuration file (default:
`userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
--show-sensitive Show secrets in the output.
```
``` output
Your combined configuration is:
{
"exit_pricing": {
"price_side": "other",
"use_order_book": true,
"order_book_top": 1
},
"stake_currency": "USDT",
"exchange": {
"name": "binance",
"key": "REDACTED",
"secret": "REDACTED",
"ccxt_config": {},
"ccxt_async_config": {},
}
// ...
}
```
!!! Warning "Sharing information provided by this command"
We try to remove all known sensitive information from the default output (without `--show-sensitive`).
Yet, please do double-check for sensitive values in your output to make sure you're not accidentally exposing some private info.
## Create new strategy ## Create new strategy
Creates a new strategy from a template similar to SampleStrategy. Creates a new strategy from a template similar to SampleStrategy.
@@ -219,207 +266,49 @@ optional arguments:
-a, --all Print all exchanges known to the ccxt library. -a, --all Print all exchanges known to the ccxt library.
``` ```
* Example: see exchanges available for the bot: Example: see exchanges available for the bot:
``` ```
$ freqtrade list-exchanges $ freqtrade list-exchanges
Exchanges available for Freqtrade: Exchanges available for Freqtrade:
Exchange name Valid reason Exchange name Supported Markets Reason
--------------- ------- -------------------------------------------- ------------------ ----------- ---------------------- ------------------------------------------------------------------------
aax True binance Official spot, isolated futures
ascendex True missing opt: fetchMyTrades bitmart Official spot
bequant True bybit spot, isolated futures
bibox True gate Official spot, isolated futures
bigone True htx Official spot
binance True huobi spot
binanceus True kraken Official spot
bitbank True missing opt: fetchTickers okx Official spot, isolated futures
bitcoincom True
bitfinex True
bitforex True missing opt: fetchMyTrades, fetchTickers
bitget True
bithumb True missing opt: fetchMyTrades
bitkk True missing opt: fetchMyTrades
bitmart True
bitmax True missing opt: fetchMyTrades
bitpanda True
bitvavo True
bitz True missing opt: fetchMyTrades
btcalpha True missing opt: fetchTicker, fetchTickers
btcmarkets True missing opt: fetchTickers
buda True missing opt: fetchMyTrades, fetchTickers
bw True missing opt: fetchMyTrades, fetchL2OrderBook
bybit True
bytetrade True
cdax True
cex True missing opt: fetchMyTrades
coinbaseprime True missing opt: fetchTickers
coinbasepro True missing opt: fetchTickers
coinex True
crex24 True
deribit True
digifinex True
equos True missing opt: fetchTicker, fetchTickers
eterbase True
fcoin True missing opt: fetchMyTrades, fetchTickers
fcoinjp True missing opt: fetchMyTrades, fetchTickers
gateio True
gemini True
gopax True
hbtc True
hitbtc True
huobijp True
huobipro True
idex True
kraken True
kucoin True
lbank True missing opt: fetchMyTrades
mercado True missing opt: fetchTickers
ndax True missing opt: fetchTickers
novadax True
okcoin True
okex True
probit True
qtrade True
stex True
timex True
upbit True missing opt: fetchMyTrades
vcc True
zb True missing opt: fetchMyTrades
``` ```
!!! info ""
Output reduced for clarity - supported and available exchanges may change over time.
!!! Note "missing opt exchanges" !!! Note "missing opt exchanges"
Values with "missing opt:" might need special configuration (e.g. using orderbook if `fetchTickers` is missing) - but should in theory work (although we cannot guarantee they will). Values with "missing opt:" might need special configuration (e.g. using orderbook if `fetchTickers` is missing) - but should in theory work (although we cannot guarantee they will).
* Example: see all exchanges supported by the ccxt library (including 'bad' ones, i.e. those that are known to not work with Freqtrade): Example: see all exchanges supported by the ccxt library (including 'bad' ones, i.e. those that are known to not work with Freqtrade)
``` ```
$ freqtrade list-exchanges -a $ freqtrade list-exchanges -a
All exchanges supported by the ccxt library: All exchanges supported by the ccxt library:
Exchange name Valid reason Exchange name Valid Supported Markets Reason
------------------ ------- --------------------------------------------------------------------------------------- ------------------ ------- ----------- ---------------------- ---------------------------------------------------------------------------------
aax True binance True Official spot, isolated futures
aofex False missing: fetchOrder bitflyer False spot missing: fetchOrder. missing opt: fetchTickers.
ascendex True missing opt: fetchMyTrades bitmart True Official spot
bequant True bybit True spot, isolated futures
bibox True gate True Official spot, isolated futures
bigone True htx True Official spot
binance True kraken True Official spot
binanceus True okx True Official spot, isolated futures
bit2c False missing: fetchOrder, fetchOHLCV
bitbank True missing opt: fetchTickers
bitbay False missing: fetchOrder
bitcoincom True
bitfinex True
bitfinex2 False missing: fetchOrder
bitflyer False missing: fetchOrder, fetchOHLCV
bitforex True missing opt: fetchMyTrades, fetchTickers
bitget True
bithumb True missing opt: fetchMyTrades
bitkk True missing opt: fetchMyTrades
bitmart True
bitmax True missing opt: fetchMyTrades
bitmex False Various reasons.
bitpanda True
bitso False missing: fetchOHLCV
bitstamp True missing opt: fetchTickers
bitstamp1 False missing: fetchOrder, fetchOHLCV
bitvavo True
bitz True missing opt: fetchMyTrades
bl3p False missing: fetchOrder, fetchOHLCV
bleutrade False missing: fetchOrder
braziliex False missing: fetchOHLCV
btcalpha True missing opt: fetchTicker, fetchTickers
btcbox False missing: fetchOHLCV
btcmarkets True missing opt: fetchTickers
btctradeua False missing: fetchOrder, fetchOHLCV
btcturk False missing: fetchOrder
buda True missing opt: fetchMyTrades, fetchTickers
bw True missing opt: fetchMyTrades, fetchL2OrderBook
bybit True
bytetrade True
cdax True
cex True missing opt: fetchMyTrades
chilebit False missing: fetchOrder, fetchOHLCV
coinbase False missing: fetchOrder, cancelOrder, createOrder, fetchOHLCV
coinbaseprime True missing opt: fetchTickers
coinbasepro True missing opt: fetchTickers
coincheck False missing: fetchOrder, fetchOHLCV
coinegg False missing: fetchOHLCV
coinex True
coinfalcon False missing: fetchOHLCV
coinfloor False missing: fetchOrder, fetchOHLCV
coingi False missing: fetchOrder, fetchOHLCV
coinmarketcap False missing: fetchOrder, cancelOrder, createOrder, fetchBalance, fetchOHLCV
coinmate False missing: fetchOHLCV
coinone False missing: fetchOHLCV
coinspot False missing: fetchOrder, cancelOrder, fetchOHLCV
crex24 True
currencycom False missing: fetchOrder
delta False missing: fetchOrder
deribit True
digifinex True
equos True missing opt: fetchTicker, fetchTickers
eterbase True
exmo False missing: fetchOrder
exx False missing: fetchOHLCV
fcoin True missing opt: fetchMyTrades, fetchTickers
fcoinjp True missing opt: fetchMyTrades, fetchTickers
flowbtc False missing: fetchOrder, fetchOHLCV
foxbit False missing: fetchOrder, fetchOHLCV
gateio True
gemini True
gopax True
hbtc True
hitbtc True
hollaex False missing: fetchOrder
huobijp True
huobipro True
idex True
independentreserve False missing: fetchOHLCV
indodax False missing: fetchOHLCV
itbit False missing: fetchOHLCV
kraken True
kucoin True
kuna False missing: fetchOHLCV
lakebtc False missing: fetchOrder, fetchOHLCV
latoken False missing: fetchOrder, fetchOHLCV
lbank True missing opt: fetchMyTrades
liquid False missing: fetchOHLCV
luno False missing: fetchOHLCV
lykke False missing: fetchOHLCV
mercado True missing opt: fetchTickers
mixcoins False missing: fetchOrder, fetchOHLCV
ndax True missing opt: fetchTickers
novadax True
oceanex False missing: fetchOHLCV
okcoin True
okex True
paymium False missing: fetchOrder, fetchOHLCV
phemex False Does not provide history.
poloniex False missing: fetchOrder
probit True
qtrade True
rightbtc False missing: fetchOrder
ripio False missing: fetchOHLCV
southxchange False missing: fetchOrder, fetchOHLCV
stex True
surbitcoin False missing: fetchOrder, fetchOHLCV
therock False missing: fetchOHLCV
tidebit False missing: fetchOrder
tidex False missing: fetchOHLCV
timex True
upbit True missing opt: fetchMyTrades
vbtc False missing: fetchOrder, fetchOHLCV
vcc True
wavesexchange False missing: fetchOrder
whitebit False missing: fetchOrder, cancelOrder, createOrder, fetchBalance
xbtce False missing: fetchOrder, fetchOHLCV
xena False missing: fetchOrder
yobit False missing: fetchOHLCV
zaif False missing: fetchOrder, fetchOHLCV
zb True missing opt: fetchMyTrades
``` ```
!!! info ""
Reduced output - supported and available exchanges may change over time.
## List Timeframes ## List Timeframes
Use the `list-timeframes` subcommand to see the list of timeframes available for the exchange. Use the `list-timeframes` subcommand to see the list of timeframes available for the exchange.
@@ -990,11 +879,7 @@ options:
-h, --help show this help message and exit -h, --help show this help message and exit
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...] --strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
Provide a space-separated list of strategies to Provide a space-separated list of strategies to
backtest. Please note that timeframe needs to be set be converted.
either in config or via command line. When using this
together with `--export trades`, the strategy-name is
injected into the filename (so `backtest-data.json`
becomes `backtest-data-SampleStrategy.json`
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
+1 -1
View File
@@ -65,7 +65,7 @@ You can set the POST body format to Form-Encoded (default), JSON-Encoded, or raw
The result would be a POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel. The result would be a POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel.
When using the Form-Encoded or JSON-Encoded configuration you can configure any number of payload values, and both the key and value will be ouput in the POST request. However, when using the raw data format you can only configure one value and it **must** be named `"data"`. In this instance the data key will not be output in the POST request, only the value. For example: When using the Form-Encoded or JSON-Encoded configuration you can configure any number of payload values, and both the key and value will be output in the POST request. However, when using the raw data format you can only configure one value and it **must** be named `"data"`. In this instance the data key will not be output in the POST request, only the value. For example:
```json ```json
"webhook": { "webhook": {
+1 -1
View File
@@ -24,7 +24,7 @@ git clone https://github.com/freqtrade/freqtrade.git
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows). Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows).
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.9, 3.10 and 3.11) and for 64bit Windows. As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.9, 3.10, 3.11 and 3.12) and for 64bit Windows.
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade. These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
Other versions must be downloaded from the above link. Other versions must be downloaded from the above link.
+18 -6
View File
@@ -1,21 +1,33 @@
"""Freqtrade bot""" """Freqtrade bot"""
__version__ = '2024.1-dev'
if 'dev' in __version__: __version__ = "2024.5-dev"
if "dev" in __version__:
from pathlib import Path from pathlib import Path
try: try:
import subprocess import subprocess
freqtrade_basedir = Path(__file__).parent freqtrade_basedir = Path(__file__).parent
__version__ = __version__ + '-' + subprocess.check_output( __version__ = (
['git', 'log', '--format="%h"', '-n 1'], __version__
stderr=subprocess.DEVNULL, cwd=freqtrade_basedir).decode("utf-8").rstrip().strip('"') + "-"
+ subprocess.check_output(
["git", "log", '--format="%h"', "-n 1"],
stderr=subprocess.DEVNULL,
cwd=freqtrade_basedir,
)
.decode("utf-8")
.rstrip()
.strip('"')
)
except Exception: # pragma: no cover except Exception: # pragma: no cover
# git not available, ignore # git not available, ignore
try: try:
# Try Fallback to freqtrade_commit file (created by CI while building docker image) # Try Fallback to freqtrade_commit file (created by CI while building docker image)
versionfile = Path('./freqtrade_commit') versionfile = Path("./freqtrade_commit")
if versionfile.is_file(): if versionfile.is_file():
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}" __version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
except Exception: except Exception:
+1 -1
View File
@@ -9,5 +9,5 @@ To launch Freqtrade as a module
from freqtrade import main from freqtrade import main
if __name__ == '__main__': if __name__ == "__main__":
main.main() main.main()
+28 -11
View File
@@ -6,22 +6,39 @@ Contains all start-commands, subcommands and CLI Interface creation.
Note: Be careful with file-scoped imports in these subfiles. Note: Be careful with file-scoped imports in these subfiles.
as they are parsed on startup, nothing containing optional modules should be loaded. as they are parsed on startup, nothing containing optional modules should be loaded.
""" """
from freqtrade.commands.analyze_commands import start_analysis_entries_exits from freqtrade.commands.analyze_commands import start_analysis_entries_exits
from freqtrade.commands.arguments import Arguments from freqtrade.commands.arguments import Arguments
from freqtrade.commands.build_config_commands import start_new_config from freqtrade.commands.build_config_commands import start_new_config, start_show_config
from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades, from freqtrade.commands.data_commands import (
start_download_data, start_list_data) start_convert_data,
start_convert_trades,
start_download_data,
start_list_data,
)
from freqtrade.commands.db_commands import start_convert_db from freqtrade.commands.db_commands import start_convert_db
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui, from freqtrade.commands.deploy_commands import (
start_new_strategy) start_create_userdir,
start_install_ui,
start_new_strategy,
)
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_freqAI_models, from freqtrade.commands.list_commands import (
start_list_markets, start_list_strategies, start_list_exchanges,
start_list_timeframes, start_show_trades) start_list_freqAI_models,
from freqtrade.commands.optimize_commands import (start_backtesting, start_backtesting_show, start_list_markets,
start_edge, start_hyperopt, start_list_strategies,
start_list_timeframes,
start_show_trades,
)
from freqtrade.commands.optimize_commands import (
start_backtesting,
start_backtesting_show,
start_edge,
start_hyperopt,
start_lookahead_analysis, start_lookahead_analysis,
start_recursive_analysis) start_recursive_analysis,
)
from freqtrade.commands.pairlist_commands import start_test_pairlist from freqtrade.commands.pairlist_commands import start_test_pairlist
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
from freqtrade.commands.strategy_utils_commands import start_strategy_update from freqtrade.commands.strategy_utils_commands import start_strategy_update
+11 -11
View File
@@ -4,7 +4,7 @@ from typing import Any, Dict
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import ConfigurationError, OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -20,25 +20,25 @@ def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[s
config = setup_utils_configuration(args, method) config = setup_utils_configuration(args, method)
no_unlimited_runmodes = { no_unlimited_runmodes = {
RunMode.BACKTEST: 'backtesting', RunMode.BACKTEST: "backtesting",
} }
if method in no_unlimited_runmodes.keys(): if method in no_unlimited_runmodes.keys():
from freqtrade.data.btanalysis import get_latest_backtest_filename from freqtrade.data.btanalysis import get_latest_backtest_filename
if 'exportfilename' in config: if "exportfilename" in config:
if config['exportfilename'].is_dir(): if config["exportfilename"].is_dir():
btfile = Path(get_latest_backtest_filename(config['exportfilename'])) btfile = Path(get_latest_backtest_filename(config["exportfilename"]))
signals_file = f"{config['exportfilename']}/{btfile.stem}_signals.pkl" signals_file = f"{config['exportfilename']}/{btfile.stem}_signals.pkl"
else: else:
if config['exportfilename'].exists(): if config["exportfilename"].exists():
btfile = Path(config['exportfilename']) btfile = Path(config["exportfilename"])
signals_file = f"{btfile.parent}/{btfile.stem}_signals.pkl" signals_file = f"{btfile.parent}/{btfile.stem}_signals.pkl"
else: else:
raise OperationalException(f"{config['exportfilename']} does not exist.") raise ConfigurationError(f"{config['exportfilename']} does not exist.")
else: else:
raise OperationalException('exportfilename not in config.') raise ConfigurationError("exportfilename not in config.")
if (not Path(signals_file).exists()): if not Path(signals_file).exists():
raise OperationalException( raise OperationalException(
f"Cannot find latest backtest signals file: {signals_file}." f"Cannot find latest backtest signals file: {signals_file}."
"Run backtesting with `--export signals`." "Run backtesting with `--export signals`."
@@ -58,6 +58,6 @@ def start_analysis_entries_exits(args: Dict[str, Any]) -> None:
# Initialize configuration # Initialize configuration
config = setup_analyze_configuration(args, RunMode.BACKTEST) config = setup_analyze_configuration(args, RunMode.BACKTEST)
logger.info('Starting freqtrade in analysis mode') logger.info("Starting freqtrade in analysis mode")
process_entry_exit_reasons(config) process_entry_exit_reasons(config)
+294 -144
View File
@@ -1,6 +1,7 @@
""" """
This module contains the argument manager class This module contains the argument manager class
""" """
import argparse import argparse
from functools import partial from functools import partial
from pathlib import Path from pathlib import Path
@@ -12,35 +13,72 @@ from freqtrade.constants import DEFAULT_CONFIG
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"] ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"]
ARGS_STRATEGY = ["strategy", "strategy_path", "recursive_strategy_search", "freqaimodel", ARGS_STRATEGY = [
"freqaimodel_path"] "strategy",
"strategy_path",
"recursive_strategy_search",
"freqaimodel",
"freqaimodel_path",
]
ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"] ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"]
ARGS_WEBSERVER: List[str] = [] ARGS_WEBSERVER: List[str] = []
ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", ARGS_COMMON_OPTIMIZE = [
"max_open_trades", "stake_amount", "fee", "pairs"] "timeframe",
"timerange",
"dataformat_ohlcv",
"max_open_trades",
"stake_amount",
"fee",
"pairs",
]
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + [
"enable_protections", "dry_run_wallet", "timeframe_detail", "position_stacking",
"strategy_list", "export", "exportfilename", "use_max_market_positions",
"backtest_breakdown", "backtest_cache", "enable_protections",
"freqai_backtest_live_models"] "dry_run_wallet",
"timeframe_detail",
"strategy_list",
"export",
"exportfilename",
"backtest_breakdown",
"backtest_cache",
"freqai_backtest_live_models",
]
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + [
"position_stacking", "use_max_market_positions", "hyperopt",
"enable_protections", "dry_run_wallet", "timeframe_detail", "hyperopt_path",
"epochs", "spaces", "print_all", "position_stacking",
"print_colorized", "print_json", "hyperopt_jobs", "use_max_market_positions",
"hyperopt_random_state", "hyperopt_min_trades", "enable_protections",
"hyperopt_loss", "disableparamexport", "dry_run_wallet",
"hyperopt_ignore_missing_space", "analyze_per_epoch"] "timeframe_detail",
"epochs",
"spaces",
"print_all",
"print_colorized",
"print_json",
"hyperopt_jobs",
"hyperopt_random_state",
"hyperopt_min_trades",
"hyperopt_loss",
"disableparamexport",
"hyperopt_ignore_missing_space",
"analyze_per_epoch",
]
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized", ARGS_LIST_STRATEGIES = [
"recursive_strategy_search"] "strategy_path",
"print_one_column",
"print_colorized",
"recursive_strategy_search",
]
ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column", "print_colorized"] ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column", "print_colorized"]
@@ -52,16 +90,32 @@ ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", ARGS_LIST_PAIRS = [
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all", "exchange",
"trading_mode"] "print_list",
"list_pairs_print_json",
"print_one_column",
"print_csv",
"base_currencies",
"quote_currencies",
"list_pairs_all",
"trading_mode",
]
ARGS_TEST_PAIRLIST = ["user_data_dir", "verbosity", "config", "quote_currencies", ARGS_TEST_PAIRLIST = [
"print_one_column", "list_pairs_print_json", "exchange"] "user_data_dir",
"verbosity",
"config",
"quote_currencies",
"print_one_column",
"list_pairs_print_json",
"exchange",
]
ARGS_CREATE_USERDIR = ["user_data_dir", "reset"] ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]
ARGS_BUILD_CONFIG = ["config"] ARGS_BUILD_CONFIG = ["config"]
ARGS_SHOW_CONFIG = ["user_data_dir", "config", "show_sensitive"]
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"] ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
@@ -69,21 +123,59 @@ ARGS_CONVERT_DATA_TRADES = ["pairs", "format_from_trades", "format_to", "erase",
ARGS_CONVERT_DATA = ["pairs", "format_from", "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", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"] ARGS_CONVERT_TRADES = [
"pairs",
"timeframes",
"exchange",
"dataformat_ohlcv",
"dataformat_trades",
"trading_mode",
]
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs", "trading_mode", "show_timerange"] ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs", "trading_mode", "show_timerange"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "include_inactive", ARGS_DOWNLOAD_DATA = [
"timerange", "download_trades", "exchange", "timeframes", "pairs",
"erase", "dataformat_ohlcv", "dataformat_trades", "trading_mode", "pairs_file",
"prepend_data"] "days",
"new_pairs_days",
"include_inactive",
"timerange",
"download_trades",
"convert_trades",
"exchange",
"timeframes",
"erase",
"dataformat_ohlcv",
"dataformat_trades",
"trading_mode",
"prepend_data",
]
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", ARGS_PLOT_DATAFRAME = [
"db_url", "trade_source", "export", "exportfilename", "pairs",
"timerange", "timeframe", "no_trades"] "indicators1",
"indicators2",
"plot_limit",
"db_url",
"trade_source",
"export",
"exportfilename",
"timerange",
"timeframe",
"no_trades",
]
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", ARGS_PLOT_PROFIT = [
"trade_source", "timeframe", "plot_auto_open", ] "pairs",
"timerange",
"export",
"exportfilename",
"db_url",
"trade_source",
"timeframe",
"plot_auto_open",
]
ARGS_CONVERT_DB = ["db_url", "db_url_from"] ARGS_CONVERT_DB = ["db_url", "db_url_from"]
@@ -91,35 +183,75 @@ ARGS_INSTALL_UI = ["erase_ui_only", "ui_version"]
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"] ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]
ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", ARGS_HYPEROPT_LIST = [
"hyperopt_list_min_trades", "hyperopt_list_max_trades", "hyperopt_list_best",
"hyperopt_list_min_avg_time", "hyperopt_list_max_avg_time", "hyperopt_list_profitable",
"hyperopt_list_min_avg_profit", "hyperopt_list_max_avg_profit", "hyperopt_list_min_trades",
"hyperopt_list_min_total_profit", "hyperopt_list_max_total_profit", "hyperopt_list_max_trades",
"hyperopt_list_min_objective", "hyperopt_list_max_objective", "hyperopt_list_min_avg_time",
"print_colorized", "print_json", "hyperopt_list_no_details", "hyperopt_list_max_avg_time",
"hyperoptexportfilename", "export_csv"] "hyperopt_list_min_avg_profit",
"hyperopt_list_max_avg_profit",
"hyperopt_list_min_total_profit",
"hyperopt_list_max_total_profit",
"hyperopt_list_min_objective",
"hyperopt_list_max_objective",
"print_colorized",
"print_json",
"hyperopt_list_no_details",
"hyperoptexportfilename",
"export_csv",
]
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index", ARGS_HYPEROPT_SHOW = [
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header", "hyperopt_list_best",
"disableparamexport", "backtest_breakdown"] "hyperopt_list_profitable",
"hyperopt_show_index",
"print_json",
"hyperoptexportfilename",
"hyperopt_show_no_header",
"disableparamexport",
"backtest_breakdown",
]
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list", ARGS_ANALYZE_ENTRIES_EXITS = [
"exit_reason_list", "indicator_list", "timerange", "exportfilename",
"analysis_rejected", "analysis_to_csv", "analysis_csv_path"] "analysis_groups",
"enter_reason_list",
"exit_reason_list",
"indicator_list",
"timerange",
"analysis_rejected",
"analysis_to_csv",
"analysis_csv_path",
]
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", NO_CONF_REQURIED = [
"list-markets", "list-pairs", "list-strategies", "list-freqaimodels", "convert-data",
"list-data", "hyperopt-list", "hyperopt-show", "backtest-filter", "convert-trade-data",
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv", "download-data",
"strategy-updater"] "list-timeframes",
"list-markets",
"list-pairs",
"list-strategies",
"list-freqaimodels",
"list-data",
"hyperopt-list",
"hyperopt-show",
"backtest-filter",
"plot-dataframe",
"plot-profit",
"show-trades",
"trades-to-ohlcv",
"strategy-updater",
]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"] NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_search"] ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_search"]
ARGS_LOOKAHEAD_ANALYSIS = [ ARGS_LOOKAHEAD_ANALYSIS = [
a for a in ARGS_BACKTEST if a not in ("position_stacking", "use_max_market_positions", 'cache') a for a in ARGS_BACKTEST if a not in ("position_stacking", "use_max_market_positions", "cache")
] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"] ] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"]
ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"] ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"]
@@ -154,14 +286,14 @@ class Arguments:
# Workaround issue in argparse with action='append' and default value # Workaround issue in argparse with action='append' and default value
# (see https://bugs.python.org/issue16399) # (see https://bugs.python.org/issue16399)
# Allow no-config for certain commands (like downloading / plotting) # Allow no-config for certain commands (like downloading / plotting)
if ('config' in parsed_arg and parsed_arg.config is None): if "config" in parsed_arg and parsed_arg.config is None:
conf_required = ('command' in parsed_arg and parsed_arg.command in NO_CONF_REQURIED) conf_required = "command" in parsed_arg and parsed_arg.command in NO_CONF_REQURIED
if 'user_data_dir' in parsed_arg and parsed_arg.user_data_dir is not None: if "user_data_dir" in parsed_arg and parsed_arg.user_data_dir is not None:
user_dir = parsed_arg.user_data_dir user_dir = parsed_arg.user_data_dir
else: else:
# Default case # Default case
user_dir = 'user_data' user_dir = "user_data"
# Try loading from "user_data/config.json" # Try loading from "user_data/config.json"
cfgfile = Path(user_dir) / DEFAULT_CONFIG cfgfile = Path(user_dir) / DEFAULT_CONFIG
if cfgfile.is_file(): if cfgfile.is_file():
@@ -175,7 +307,6 @@ class Arguments:
return parsed_arg return parsed_arg
def _build_args(self, optionlist, parser): def _build_args(self, optionlist, parser):
for val in optionlist: for val in optionlist:
opt = AVAILABLE_CLI_OPTIONS[val] opt = AVAILABLE_CLI_OPTIONS[val]
parser.add_argument(*opt.cli, dest=val, **opt.kwargs) parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
@@ -195,24 +326,47 @@ class Arguments:
self._build_args(optionlist=ARGS_STRATEGY, parser=strategy_group) self._build_args(optionlist=ARGS_STRATEGY, parser=strategy_group)
# Build main command # Build main command
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') self.parser = argparse.ArgumentParser(
self._build_args(optionlist=['version'], parser=self.parser) prog="freqtrade", description="Free, open source crypto trading bot"
)
self._build_args(optionlist=["version"], parser=self.parser)
from freqtrade.commands import (start_analysis_entries_exits, start_backtesting, from freqtrade.commands import (
start_backtesting_show, start_convert_data, start_analysis_entries_exits,
start_convert_db, start_convert_trades, start_backtesting,
start_create_userdir, start_download_data, start_edge, start_backtesting_show,
start_hyperopt, start_hyperopt_list, start_hyperopt_show, start_convert_data,
start_install_ui, start_list_data, start_list_exchanges, start_convert_db,
start_list_freqAI_models, start_list_markets, start_convert_trades,
start_list_strategies, start_list_timeframes, start_create_userdir,
start_lookahead_analysis, start_new_config, start_download_data,
start_new_strategy, start_plot_dataframe, start_plot_profit, start_edge,
start_recursive_analysis, start_show_trades, start_hyperopt,
start_strategy_update, start_test_pairlist, start_trading, start_hyperopt_list,
start_webserver) start_hyperopt_show,
start_install_ui,
start_list_data,
start_list_exchanges,
start_list_freqAI_models,
start_list_markets,
start_list_strategies,
start_list_timeframes,
start_lookahead_analysis,
start_new_config,
start_new_strategy,
start_plot_dataframe,
start_plot_profit,
start_recursive_analysis,
start_show_config,
start_show_trades,
start_strategy_update,
start_test_pairlist,
start_trading,
start_webserver,
)
subparsers = self.parser.add_subparsers(dest='command', subparsers = self.parser.add_subparsers(
dest="command",
# Use custom message when no subhandler is added # Use custom message when no subhandler is added
# shown from `main.py` # shown from `main.py`
# required=True # required=True
@@ -220,16 +374,14 @@ class Arguments:
# Add trade subcommand # Add trade subcommand
trade_cmd = subparsers.add_parser( trade_cmd = subparsers.add_parser(
'trade', "trade", help="Trade module.", parents=[_common_parser, _strategy_parser]
help='Trade module.',
parents=[_common_parser, _strategy_parser]
) )
trade_cmd.set_defaults(func=start_trading) trade_cmd.set_defaults(func=start_trading)
self._build_args(optionlist=ARGS_TRADE, parser=trade_cmd) self._build_args(optionlist=ARGS_TRADE, parser=trade_cmd)
# add create-userdir subcommand # add create-userdir subcommand
create_userdir_cmd = subparsers.add_parser( create_userdir_cmd = subparsers.add_parser(
'create-userdir', "create-userdir",
help="Create user-data directory.", help="Create user-data directory.",
) )
create_userdir_cmd.set_defaults(func=start_create_userdir) create_userdir_cmd.set_defaults(func=start_create_userdir)
@@ -237,15 +389,23 @@ class Arguments:
# add new-config subcommand # add new-config subcommand
build_config_cmd = subparsers.add_parser( build_config_cmd = subparsers.add_parser(
'new-config', "new-config",
help="Create new config", help="Create new config",
) )
build_config_cmd.set_defaults(func=start_new_config) build_config_cmd.set_defaults(func=start_new_config)
self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd) self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd)
# add show-config subcommand
show_config_cmd = subparsers.add_parser(
"show-config",
help="Show resolved config",
)
show_config_cmd.set_defaults(func=start_show_config)
self._build_args(optionlist=ARGS_SHOW_CONFIG, parser=show_config_cmd)
# add new-strategy subcommand # add new-strategy subcommand
build_strategy_cmd = subparsers.add_parser( build_strategy_cmd = subparsers.add_parser(
'new-strategy', "new-strategy",
help="Create new strategy", help="Create new strategy",
) )
build_strategy_cmd.set_defaults(func=start_new_strategy) build_strategy_cmd.set_defaults(func=start_new_strategy)
@@ -253,8 +413,8 @@ class Arguments:
# Add download-data subcommand # Add download-data subcommand
download_data_cmd = subparsers.add_parser( download_data_cmd = subparsers.add_parser(
'download-data', "download-data",
help='Download backtesting data.', help="Download backtesting data.",
parents=[_common_parser], parents=[_common_parser],
) )
download_data_cmd.set_defaults(func=start_download_data) download_data_cmd.set_defaults(func=start_download_data)
@@ -262,8 +422,8 @@ class Arguments:
# Add convert-data subcommand # Add convert-data subcommand
convert_data_cmd = subparsers.add_parser( convert_data_cmd = subparsers.add_parser(
'convert-data', "convert-data",
help='Convert candle (OHLCV) data from one format to another.', help="Convert candle (OHLCV) data from one format to another.",
parents=[_common_parser], parents=[_common_parser],
) )
convert_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=True)) convert_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=True))
@@ -271,8 +431,8 @@ class Arguments:
# Add convert-trade-data subcommand # Add convert-trade-data subcommand
convert_trade_data_cmd = subparsers.add_parser( convert_trade_data_cmd = subparsers.add_parser(
'convert-trade-data', "convert-trade-data",
help='Convert trade data from one format to another.', help="Convert trade data from one format to another.",
parents=[_common_parser], parents=[_common_parser],
) )
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False)) convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
@@ -280,8 +440,8 @@ class Arguments:
# Add trades-to-ohlcv subcommand # Add trades-to-ohlcv subcommand
convert_trade_data_cmd = subparsers.add_parser( convert_trade_data_cmd = subparsers.add_parser(
'trades-to-ohlcv', "trades-to-ohlcv",
help='Convert trade data to OHLCV data.', help="Convert trade data to OHLCV data.",
parents=[_common_parser], parents=[_common_parser],
) )
convert_trade_data_cmd.set_defaults(func=start_convert_trades) convert_trade_data_cmd.set_defaults(func=start_convert_trades)
@@ -289,8 +449,8 @@ class Arguments:
# Add list-data subcommand # Add list-data subcommand
list_data_cmd = subparsers.add_parser( list_data_cmd = subparsers.add_parser(
'list-data', "list-data",
help='List downloaded data.', help="List downloaded data.",
parents=[_common_parser], parents=[_common_parser],
) )
list_data_cmd.set_defaults(func=start_list_data) list_data_cmd.set_defaults(func=start_list_data)
@@ -298,17 +458,15 @@ class Arguments:
# Add backtesting subcommand # Add backtesting subcommand
backtesting_cmd = subparsers.add_parser( backtesting_cmd = subparsers.add_parser(
'backtesting', "backtesting", help="Backtesting module.", parents=[_common_parser, _strategy_parser]
help='Backtesting module.',
parents=[_common_parser, _strategy_parser]
) )
backtesting_cmd.set_defaults(func=start_backtesting) backtesting_cmd.set_defaults(func=start_backtesting)
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
# Add backtesting-show subcommand # Add backtesting-show subcommand
backtesting_show_cmd = subparsers.add_parser( backtesting_show_cmd = subparsers.add_parser(
'backtesting-show', "backtesting-show",
help='Show past Backtest results', help="Show past Backtest results",
parents=[_common_parser], parents=[_common_parser],
) )
backtesting_show_cmd.set_defaults(func=start_backtesting_show) backtesting_show_cmd.set_defaults(func=start_backtesting_show)
@@ -316,26 +474,22 @@ class Arguments:
# Add backtesting analysis subcommand # Add backtesting analysis subcommand
analysis_cmd = subparsers.add_parser( analysis_cmd = subparsers.add_parser(
'backtesting-analysis', "backtesting-analysis", help="Backtest Analysis module.", parents=[_common_parser]
help='Backtest Analysis module.',
parents=[_common_parser]
) )
analysis_cmd.set_defaults(func=start_analysis_entries_exits) analysis_cmd.set_defaults(func=start_analysis_entries_exits)
self._build_args(optionlist=ARGS_ANALYZE_ENTRIES_EXITS, parser=analysis_cmd) self._build_args(optionlist=ARGS_ANALYZE_ENTRIES_EXITS, parser=analysis_cmd)
# Add edge subcommand # Add edge subcommand
edge_cmd = subparsers.add_parser( edge_cmd = subparsers.add_parser(
'edge', "edge", help="Edge module.", parents=[_common_parser, _strategy_parser]
help='Edge module.',
parents=[_common_parser, _strategy_parser]
) )
edge_cmd.set_defaults(func=start_edge) edge_cmd.set_defaults(func=start_edge)
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd) self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
# Add hyperopt subcommand # Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser( hyperopt_cmd = subparsers.add_parser(
'hyperopt', "hyperopt",
help='Hyperopt module.', help="Hyperopt module.",
parents=[_common_parser, _strategy_parser], parents=[_common_parser, _strategy_parser],
) )
hyperopt_cmd.set_defaults(func=start_hyperopt) hyperopt_cmd.set_defaults(func=start_hyperopt)
@@ -343,8 +497,8 @@ class Arguments:
# Add hyperopt-list subcommand # Add hyperopt-list subcommand
hyperopt_list_cmd = subparsers.add_parser( hyperopt_list_cmd = subparsers.add_parser(
'hyperopt-list', "hyperopt-list",
help='List Hyperopt results', help="List Hyperopt results",
parents=[_common_parser], parents=[_common_parser],
) )
hyperopt_list_cmd.set_defaults(func=start_hyperopt_list) hyperopt_list_cmd.set_defaults(func=start_hyperopt_list)
@@ -352,8 +506,8 @@ class Arguments:
# Add hyperopt-show subcommand # Add hyperopt-show subcommand
hyperopt_show_cmd = subparsers.add_parser( hyperopt_show_cmd = subparsers.add_parser(
'hyperopt-show', "hyperopt-show",
help='Show details of Hyperopt results', help="Show details of Hyperopt results",
parents=[_common_parser], parents=[_common_parser],
) )
hyperopt_show_cmd.set_defaults(func=start_hyperopt_show) hyperopt_show_cmd.set_defaults(func=start_hyperopt_show)
@@ -361,8 +515,8 @@ class Arguments:
# Add list-exchanges subcommand # Add list-exchanges subcommand
list_exchanges_cmd = subparsers.add_parser( list_exchanges_cmd = subparsers.add_parser(
'list-exchanges', "list-exchanges",
help='Print available exchanges.', help="Print available exchanges.",
parents=[_common_parser], parents=[_common_parser],
) )
list_exchanges_cmd.set_defaults(func=start_list_exchanges) list_exchanges_cmd.set_defaults(func=start_list_exchanges)
@@ -370,8 +524,8 @@ class Arguments:
# Add list-markets subcommand # Add list-markets subcommand
list_markets_cmd = subparsers.add_parser( list_markets_cmd = subparsers.add_parser(
'list-markets', "list-markets",
help='Print markets on exchange.', help="Print markets on exchange.",
parents=[_common_parser], parents=[_common_parser],
) )
list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False)) list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False))
@@ -379,8 +533,8 @@ class Arguments:
# Add list-pairs subcommand # Add list-pairs subcommand
list_pairs_cmd = subparsers.add_parser( list_pairs_cmd = subparsers.add_parser(
'list-pairs', "list-pairs",
help='Print pairs on exchange.', help="Print pairs on exchange.",
parents=[_common_parser], parents=[_common_parser],
) )
list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True)) list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True))
@@ -388,8 +542,8 @@ class Arguments:
# Add list-strategies subcommand # Add list-strategies subcommand
list_strategies_cmd = subparsers.add_parser( list_strategies_cmd = subparsers.add_parser(
'list-strategies', "list-strategies",
help='Print available strategies.', help="Print available strategies.",
parents=[_common_parser], parents=[_common_parser],
) )
list_strategies_cmd.set_defaults(func=start_list_strategies) list_strategies_cmd.set_defaults(func=start_list_strategies)
@@ -397,8 +551,8 @@ class Arguments:
# Add list-freqAI Models subcommand # Add list-freqAI Models subcommand
list_freqaimodels_cmd = subparsers.add_parser( list_freqaimodels_cmd = subparsers.add_parser(
'list-freqaimodels', "list-freqaimodels",
help='Print available freqAI models.', help="Print available freqAI models.",
parents=[_common_parser], parents=[_common_parser],
) )
list_freqaimodels_cmd.set_defaults(func=start_list_freqAI_models) list_freqaimodels_cmd.set_defaults(func=start_list_freqAI_models)
@@ -406,8 +560,8 @@ class Arguments:
# Add list-timeframes subcommand # Add list-timeframes subcommand
list_timeframes_cmd = subparsers.add_parser( list_timeframes_cmd = subparsers.add_parser(
'list-timeframes', "list-timeframes",
help='Print available timeframes for the exchange.', help="Print available timeframes for the exchange.",
parents=[_common_parser], parents=[_common_parser],
) )
list_timeframes_cmd.set_defaults(func=start_list_timeframes) list_timeframes_cmd.set_defaults(func=start_list_timeframes)
@@ -415,8 +569,8 @@ class Arguments:
# Add show-trades subcommand # Add show-trades subcommand
show_trades = subparsers.add_parser( show_trades = subparsers.add_parser(
'show-trades', "show-trades",
help='Show trades.', help="Show trades.",
parents=[_common_parser], parents=[_common_parser],
) )
show_trades.set_defaults(func=start_show_trades) show_trades.set_defaults(func=start_show_trades)
@@ -424,8 +578,8 @@ class Arguments:
# Add test-pairlist subcommand # Add test-pairlist subcommand
test_pairlist_cmd = subparsers.add_parser( test_pairlist_cmd = subparsers.add_parser(
'test-pairlist', "test-pairlist",
help='Test your pairlist configuration.', help="Test your pairlist configuration.",
) )
test_pairlist_cmd.set_defaults(func=start_test_pairlist) test_pairlist_cmd.set_defaults(func=start_test_pairlist)
self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd) self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd)
@@ -440,16 +594,16 @@ class Arguments:
# Add install-ui subcommand # Add install-ui subcommand
install_ui_cmd = subparsers.add_parser( install_ui_cmd = subparsers.add_parser(
'install-ui', "install-ui",
help='Install FreqUI', help="Install FreqUI",
) )
install_ui_cmd.set_defaults(func=start_install_ui) install_ui_cmd.set_defaults(func=start_install_ui)
self._build_args(optionlist=ARGS_INSTALL_UI, parser=install_ui_cmd) self._build_args(optionlist=ARGS_INSTALL_UI, parser=install_ui_cmd)
# Add Plotting subcommand # Add Plotting subcommand
plot_dataframe_cmd = subparsers.add_parser( plot_dataframe_cmd = subparsers.add_parser(
'plot-dataframe', "plot-dataframe",
help='Plot candles with indicators.', help="Plot candles with indicators.",
parents=[_common_parser, _strategy_parser], parents=[_common_parser, _strategy_parser],
) )
plot_dataframe_cmd.set_defaults(func=start_plot_dataframe) plot_dataframe_cmd.set_defaults(func=start_plot_dataframe)
@@ -457,8 +611,8 @@ class Arguments:
# Plot profit # Plot profit
plot_profit_cmd = subparsers.add_parser( plot_profit_cmd = subparsers.add_parser(
'plot-profit', "plot-profit",
help='Generate plot showing profits.', help="Generate plot showing profits.",
parents=[_common_parser, _strategy_parser], parents=[_common_parser, _strategy_parser],
) )
plot_profit_cmd.set_defaults(func=start_plot_profit) plot_profit_cmd.set_defaults(func=start_plot_profit)
@@ -466,40 +620,36 @@ class Arguments:
# Add webserver subcommand # Add webserver subcommand
webserver_cmd = subparsers.add_parser( webserver_cmd = subparsers.add_parser(
'webserver', "webserver", help="Webserver module.", parents=[_common_parser]
help='Webserver module.',
parents=[_common_parser]
) )
webserver_cmd.set_defaults(func=start_webserver) webserver_cmd.set_defaults(func=start_webserver)
self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd) self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd)
# Add strategy_updater subcommand # Add strategy_updater subcommand
strategy_updater_cmd = subparsers.add_parser( strategy_updater_cmd = subparsers.add_parser(
'strategy-updater', "strategy-updater",
help='updates outdated strategy files to the current version', help="updates outdated strategy files to the current version",
parents=[_common_parser] parents=[_common_parser],
) )
strategy_updater_cmd.set_defaults(func=start_strategy_update) strategy_updater_cmd.set_defaults(func=start_strategy_update)
self._build_args(optionlist=ARGS_STRATEGY_UPDATER, parser=strategy_updater_cmd) self._build_args(optionlist=ARGS_STRATEGY_UPDATER, parser=strategy_updater_cmd)
# Add lookahead_analysis subcommand # Add lookahead_analysis subcommand
lookahead_analayis_cmd = subparsers.add_parser( lookahead_analayis_cmd = subparsers.add_parser(
'lookahead-analysis', "lookahead-analysis",
help="Check for potential look ahead bias.", help="Check for potential look ahead bias.",
parents=[_common_parser, _strategy_parser] parents=[_common_parser, _strategy_parser],
) )
lookahead_analayis_cmd.set_defaults(func=start_lookahead_analysis) lookahead_analayis_cmd.set_defaults(func=start_lookahead_analysis)
self._build_args(optionlist=ARGS_LOOKAHEAD_ANALYSIS, self._build_args(optionlist=ARGS_LOOKAHEAD_ANALYSIS, parser=lookahead_analayis_cmd)
parser=lookahead_analayis_cmd)
# Add recursive_analysis subcommand # Add recursive_analysis subcommand
recursive_analayis_cmd = subparsers.add_parser( recursive_analayis_cmd = subparsers.add_parser(
'recursive-analysis', "recursive-analysis",
help="Check for potential recursive formula issue.", help="Check for potential recursive formula issue.",
parents=[_common_parser, _strategy_parser] parents=[_common_parser, _strategy_parser],
) )
recursive_analayis_cmd.set_defaults(func=start_recursive_analysis) recursive_analayis_cmd.set_defaults(func=start_recursive_analysis)
self._build_args(optionlist=ARGS_RECURSIVE_ANALYSIS, self._build_args(optionlist=ARGS_RECURSIVE_ANALYSIS, parser=recursive_analayis_cmd)
parser=recursive_analayis_cmd)
+59 -43
View File
@@ -5,9 +5,12 @@ from typing import Any, Dict, List
from questionary import Separator, prompt from questionary import Separator, prompt
from freqtrade.configuration import sanitize_config
from freqtrade.configuration.config_setup import setup_utils_configuration
from freqtrade.configuration.detect_environment import running_in_docker from freqtrade.configuration.detect_environment import running_in_docker
from freqtrade.configuration.directory_operations import chown_user_directory from freqtrade.configuration.directory_operations import chown_user_directory
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges
from freqtrade.util import render_template from freqtrade.util import render_template
@@ -42,7 +45,7 @@ def ask_user_overwrite(config_path: Path) -> bool:
}, },
] ]
answers = prompt(questions) answers = prompt(questions)
return answers['overwrite'] return answers["overwrite"]
def ask_user_config() -> Dict[str, Any]: def ask_user_config() -> Dict[str, Any]:
@@ -62,7 +65,7 @@ def ask_user_config() -> Dict[str, Any]:
"type": "text", "type": "text",
"name": "stake_currency", "name": "stake_currency",
"message": "Please insert your stake currency:", "message": "Please insert your stake currency:",
"default": 'USDT', "default": "USDT",
}, },
{ {
"type": "text", "type": "text",
@@ -70,36 +73,35 @@ def ask_user_config() -> Dict[str, Any]:
"message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):", "message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
"default": "unlimited", "default": "unlimited",
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val), "validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"' "filter": lambda val: (
if val == UNLIMITED_STAKE_AMOUNT '"' + UNLIMITED_STAKE_AMOUNT + '"' if val == UNLIMITED_STAKE_AMOUNT else val
else val ),
}, },
{ {
"type": "text", "type": "text",
"name": "max_open_trades", "name": "max_open_trades",
"message": "Please insert max_open_trades (Integer or -1 for unlimited open trades):", "message": "Please insert max_open_trades (Integer or -1 for unlimited open trades):",
"default": "3", "default": "3",
"validate": lambda val: validate_is_int(val) "validate": lambda val: validate_is_int(val),
}, },
{ {
"type": "select", "type": "select",
"name": "timeframe_in_config", "name": "timeframe_in_config",
"message": "Time", "message": "Time",
"choices": ["Have the strategy define timeframe.", "Override in configuration."] "choices": ["Have the strategy define timeframe.", "Override in configuration."],
}, },
{ {
"type": "text", "type": "text",
"name": "timeframe", "name": "timeframe",
"message": "Please insert your desired timeframe (e.g. 5m):", "message": "Please insert your desired timeframe (e.g. 5m):",
"default": "5m", "default": "5m",
"when": lambda x: x["timeframe_in_config"] == 'Override in configuration.' "when": lambda x: x["timeframe_in_config"] == "Override in configuration.",
}, },
{ {
"type": "text", "type": "text",
"name": "fiat_display_currency", "name": "fiat_display_currency",
"message": "Please insert your display Currency (for reporting):", "message": "Please insert your display Currency (for reporting):",
"default": 'USD', "default": "USD",
}, },
{ {
"type": "select", "type": "select",
@@ -122,33 +124,33 @@ def ask_user_config() -> Dict[str, Any]:
"name": "trading_mode", "name": "trading_mode",
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?", "message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
"default": False, "default": False,
"filter": lambda val: 'futures' if val else 'spot', "filter": lambda val: "futures" if val else "spot",
"when": lambda x: x["exchange_name"] in ['binance', 'gate', 'okx'], "when": lambda x: x["exchange_name"] in ["binance", "gate", "okx"],
}, },
{ {
"type": "autocomplete", "type": "autocomplete",
"name": "exchange_name", "name": "exchange_name",
"message": "Type your exchange name (Must be supported by ccxt)", "message": "Type your exchange name (Must be supported by ccxt)",
"choices": available_exchanges(), "choices": available_exchanges(),
"when": lambda x: x["exchange_name"] == 'other' "when": lambda x: x["exchange_name"] == "other",
}, },
{ {
"type": "password", "type": "password",
"name": "exchange_key", "name": "exchange_key",
"message": "Insert Exchange Key", "message": "Insert Exchange Key",
"when": lambda x: not x['dry_run'] "when": lambda x: not x["dry_run"],
}, },
{ {
"type": "password", "type": "password",
"name": "exchange_secret", "name": "exchange_secret",
"message": "Insert Exchange Secret", "message": "Insert Exchange Secret",
"when": lambda x: not x['dry_run'] "when": lambda x: not x["dry_run"],
}, },
{ {
"type": "password", "type": "password",
"name": "exchange_key_password", "name": "exchange_key_password",
"message": "Insert Exchange API Key password", "message": "Insert Exchange API Key password",
"when": lambda x: not x['dry_run'] and x['exchange_name'] in ('kucoin', 'okx') "when": lambda x: not x["dry_run"] and x["exchange_name"] in ("kucoin", "okx"),
}, },
{ {
"type": "confirm", "type": "confirm",
@@ -160,13 +162,13 @@ def ask_user_config() -> Dict[str, Any]:
"type": "password", "type": "password",
"name": "telegram_token", "name": "telegram_token",
"message": "Insert Telegram token", "message": "Insert Telegram token",
"when": lambda x: x['telegram'] "when": lambda x: x["telegram"],
}, },
{ {
"type": "password", "type": "password",
"name": "telegram_chat_id", "name": "telegram_chat_id",
"message": "Insert Telegram chat id", "message": "Insert Telegram chat id",
"when": lambda x: x['telegram'] "when": lambda x: x["telegram"],
}, },
{ {
"type": "confirm", "type": "confirm",
@@ -177,23 +179,25 @@ def ask_user_config() -> Dict[str, Any]:
{ {
"type": "text", "type": "text",
"name": "api_server_listen_addr", "name": "api_server_listen_addr",
"message": ("Insert Api server Listen Address (0.0.0.0 for docker, " "message": (
"otherwise best left untouched)"), "Insert Api server Listen Address (0.0.0.0 for docker, "
"otherwise best left untouched)"
),
"default": "127.0.0.1" if not running_in_docker() else "0.0.0.0", "default": "127.0.0.1" if not running_in_docker() else "0.0.0.0",
"when": lambda x: x['api_server'] "when": lambda x: x["api_server"],
}, },
{ {
"type": "text", "type": "text",
"name": "api_server_username", "name": "api_server_username",
"message": "Insert api-server username", "message": "Insert api-server username",
"default": "freqtrader", "default": "freqtrader",
"when": lambda x: x['api_server'] "when": lambda x: x["api_server"],
}, },
{ {
"type": "password", "type": "password",
"name": "api_server_password", "name": "api_server_password",
"message": "Insert api-server password", "message": "Insert api-server password",
"when": lambda x: x['api_server'] "when": lambda x: x["api_server"],
}, },
] ]
answers = prompt(questions) answers = prompt(questions)
@@ -202,15 +206,11 @@ def ask_user_config() -> Dict[str, Any]:
# Interrupted questionary sessions return an empty dict. # Interrupted questionary sessions return an empty dict.
raise OperationalException("User interrupted interactive questions.") raise OperationalException("User interrupted interactive questions.")
# Ensure default is set for non-futures exchanges # Ensure default is set for non-futures exchanges
answers['trading_mode'] = answers.get('trading_mode', "spot") answers["trading_mode"] = answers.get("trading_mode", "spot")
answers['margin_mode'] = ( answers["margin_mode"] = "isolated" if answers.get("trading_mode") == "futures" else ""
'isolated'
if answers.get('trading_mode') == 'futures'
else ''
)
# Force JWT token to be a random string # Force JWT token to be a random string
answers['api_server_jwt_key'] = secrets.token_hex() answers["api_server_jwt_key"] = secrets.token_hex()
answers['api_server_ws_token'] = secrets.token_urlsafe(25) answers["api_server_ws_token"] = secrets.token_urlsafe(25)
return answers return answers
@@ -222,26 +222,26 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
:param selections: Dict containing selections taken by the user. :param selections: Dict containing selections taken by the user.
""" """
from jinja2.exceptions import TemplateNotFound from jinja2.exceptions import TemplateNotFound
try: try:
exchange_template = MAP_EXCHANGE_CHILDCLASS.get( exchange_template = MAP_EXCHANGE_CHILDCLASS.get(
selections['exchange_name'], selections['exchange_name']) selections["exchange_name"], selections["exchange_name"]
)
selections['exchange'] = render_template( selections["exchange"] = render_template(
templatefile=f"subtemplates/exchange_{exchange_template}.j2", templatefile=f"subtemplates/exchange_{exchange_template}.j2", arguments=selections
arguments=selections
) )
except TemplateNotFound: except TemplateNotFound:
selections['exchange'] = render_template( selections["exchange"] = render_template(
templatefile="subtemplates/exchange_generic.j2", templatefile="subtemplates/exchange_generic.j2", arguments=selections
arguments=selections
) )
config_text = render_template(templatefile='base_config.json.j2', config_text = render_template(templatefile="base_config.json.j2", arguments=selections)
arguments=selections)
logger.info(f"Writing config to `{config_path}`.") logger.info(f"Writing config to `{config_path}`.")
logger.info( logger.info(
"Please make sure to check the configuration contents and adjust settings to your needs.") "Please make sure to check the configuration contents and adjust settings to your needs."
)
config_path.write_text(config_text) config_path.write_text(config_text)
@@ -252,7 +252,7 @@ def start_new_config(args: Dict[str, Any]) -> None:
Asking the user questions to fill out the template accordingly. Asking the user questions to fill out the template accordingly.
""" """
config_path = Path(args['config'][0]) config_path = Path(args["config"][0])
chown_user_directory(config_path.parent) chown_user_directory(config_path.parent)
if config_path.exists(): if config_path.exists():
overwrite = ask_user_overwrite(config_path) overwrite = ask_user_overwrite(config_path)
@@ -261,6 +261,22 @@ def start_new_config(args: Dict[str, Any]) -> None:
else: else:
raise OperationalException( raise OperationalException(
f"Configuration file `{config_path}` already exists. " f"Configuration file `{config_path}` already exists. "
"Please delete it or use a different configuration file name.") "Please delete it or use a different configuration file name."
)
selections = ask_user_config() selections = ask_user_config()
deploy_new_config(config_path, selections) deploy_new_config(config_path, selections)
def start_show_config(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE, set_dry=False)
# TODO: Sanitize from sensitive info before printing
print("Your combined configuration is:")
config_sanitized = sanitize_config(
config["original_config"], show_sensitive=args.get("show_sensitive", False)
)
from rich import print_json
print_json(data=config_sanitized)
File diff suppressed because it is too large Load Diff
+86 -49
View File
@@ -5,12 +5,16 @@ from typing import Any, Dict
from freqtrade.configuration import TimeRange, setup_utils_configuration from freqtrade.configuration import TimeRange, setup_utils_configuration
from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Config from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Config
from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_format, from freqtrade.data.converter import (
convert_trades_to_ohlcv) convert_ohlcv_format,
convert_trades_format,
convert_trades_to_ohlcv,
)
from freqtrade.data.history import download_data_main from freqtrade.data.history import download_data_main
from freqtrade.enums import RunMode, TradingMode from freqtrade.enums import CandleType, RunMode, TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import ConfigurationError
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
from freqtrade.util.migrations import migrate_data from freqtrade.util.migrations import migrate_data
@@ -19,14 +23,17 @@ logger = logging.getLogger(__name__)
def _check_data_config_download_sanity(config: Config) -> None: def _check_data_config_download_sanity(config: Config) -> None:
if 'days' in config and 'timerange' in config: if "days" in config and "timerange" in config:
raise OperationalException("--days and --timerange are mutually exclusive. " raise ConfigurationError(
"You can only specify one or the other.") "--days and --timerange are mutually exclusive. "
"You can only specify one or the other."
)
if 'pairs' not in config: if "pairs" not in config:
raise OperationalException( raise ConfigurationError(
"Downloading data requires a list of pairs. " "Downloading data requires a list of pairs. "
"Please check the documentation on how to configure this.") "Please check the documentation on how to configure this."
)
def start_download_data(args: Dict[str, Any]) -> None: def start_download_data(args: Dict[str, Any]) -> None:
@@ -45,30 +52,41 @@ def start_download_data(args: Dict[str, Any]) -> None:
def start_convert_trades(args: Dict[str, Any]) -> None: def start_convert_trades(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
timerange = TimeRange() timerange = TimeRange()
# Remove stake-currency to skip checks which are not relevant for datadownload # Remove stake-currency to skip checks which are not relevant for datadownload
config['stake_currency'] = '' config["stake_currency"] = ""
if 'timeframes' not in config: if "timeframes" not in config:
config['timeframes'] = DL_DATA_TIMEFRAMES config["timeframes"] = DL_DATA_TIMEFRAMES
# Init exchange # Init exchange
exchange = ExchangeResolver.load_exchange(config, validate=False) exchange = ExchangeResolver.load_exchange(config, validate=False)
# Manual validations of relevant settings # Manual validations of relevant settings
for timeframe in config['timeframes']: for timeframe in config["timeframes"]:
exchange.validate_timeframes(timeframe) exchange.validate_timeframes(timeframe)
available_pairs = [
p
for p in exchange.get_markets(
tradable_only=True, active_only=not config.get("include_inactive")
).keys()
]
expanded_pairs = dynamic_expand_pairlist(config, available_pairs)
# Convert downloaded trade data to different timeframes # Convert downloaded trade data to different timeframes
convert_trades_to_ohlcv( convert_trades_to_ohlcv(
pairs=config.get('pairs', []), timeframes=config['timeframes'], pairs=expanded_pairs,
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), timeframes=config["timeframes"],
data_format_ohlcv=config['dataformat_ohlcv'], datadir=config["datadir"],
data_format_trades=config['dataformat_trades'], timerange=timerange,
erase=bool(config.get("erase")),
data_format_ohlcv=config["dataformat_ohlcv"],
data_format_trades=config["dataformat_trades"],
candle_type=config.get("candle_type_def", CandleType.SPOT),
) )
@@ -79,14 +97,19 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if ohlcv: if ohlcv:
migrate_data(config) migrate_data(config)
convert_ohlcv_format(config, convert_ohlcv_format(
convert_from=args['format_from'], config,
convert_to=args['format_to'], convert_from=args["format_from"],
erase=args['erase']) convert_to=args["format_to"],
erase=args["erase"],
)
else: else:
convert_trades_format(config, convert_trades_format(
convert_from=args['format_from_trades'], convert_to=args['format_to'], config,
erase=args['erase']) convert_from=args["format_from_trades"],
convert_to=args["format_to"],
erase=args["erase"],
)
def start_list_data(args: Dict[str, Any]) -> None: def start_list_data(args: Dict[str, Any]) -> None:
@@ -98,46 +121,60 @@ def start_list_data(args: Dict[str, Any]) -> None:
from tabulate import tabulate from tabulate import tabulate
from freqtrade.data.history.idatahandler import get_datahandler from freqtrade.data.history import get_datahandler
dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv'])
dhc = get_datahandler(config["datadir"], config["dataformat_ohlcv"])
paircombs = dhc.ohlcv_get_available_data( paircombs = dhc.ohlcv_get_available_data(
config['datadir'], config["datadir"], config.get("trading_mode", TradingMode.SPOT)
config.get('trading_mode', TradingMode.SPOT)
) )
if args['pairs']: if args["pairs"]:
paircombs = [comb for comb in paircombs if comb[0] in args['pairs']] paircombs = [comb for comb in paircombs if comb[0] in args["pairs"]]
print(f"Found {len(paircombs)} pair / timeframe combinations.") print(f"Found {len(paircombs)} pair / timeframe combinations.")
if not config.get('show_timerange'): if not config.get("show_timerange"):
groupedpair = defaultdict(list) groupedpair = defaultdict(list)
for pair, timeframe, candle_type in sorted( for pair, timeframe, candle_type in sorted(
paircombs, paircombs, key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])
): ):
groupedpair[(pair, candle_type)].append(timeframe) groupedpair[(pair, candle_type)].append(timeframe)
if groupedpair: if groupedpair:
print(tabulate([ print(
(pair, ', '.join(timeframes), candle_type) tabulate(
[
(pair, ", ".join(timeframes), candle_type)
for (pair, candle_type), timeframes in groupedpair.items() for (pair, candle_type), timeframes in groupedpair.items()
], ],
headers=("Pair", "Timeframe", "Type"), headers=("Pair", "Timeframe", "Type"),
tablefmt='psql', stralign='right')) tablefmt="psql",
stralign="right",
)
)
else: else:
paircombs1 = [( paircombs1 = [
pair, timeframe, candle_type, (pair, timeframe, candle_type, *dhc.ohlcv_data_min_max(pair, timeframe, candle_type))
*dhc.ohlcv_data_min_max(pair, timeframe, candle_type) for pair, timeframe, candle_type in paircombs
) for pair, timeframe, candle_type in paircombs] ]
print(tabulate([ print(
(pair, timeframe, candle_type, tabulate(
[
(
pair,
timeframe,
candle_type,
start.strftime(DATETIME_PRINT_FORMAT), start.strftime(DATETIME_PRINT_FORMAT),
end.strftime(DATETIME_PRINT_FORMAT), length) end.strftime(DATETIME_PRINT_FORMAT),
length,
)
for pair, timeframe, candle_type, start, end, length in sorted( for pair, timeframe, candle_type, start, end, length in sorted(
paircombs1, paircombs1, key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])) )
], ],
headers=("Pair", "Timeframe", "Type", 'From', 'To', 'Candles'), headers=("Pair", "Timeframe", "Type", "From", "To", "Candles"),
tablefmt='psql', stralign='right')) tablefmt="psql",
stralign="right",
)
)
+6 -4
View File
@@ -19,9 +19,9 @@ def start_convert_db(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
init_db(config['db_url']) init_db(config["db_url"])
session_target = Trade.session session_target = Trade.session
init_db(config['db_url_from']) init_db(config["db_url_from"])
logger.info("Starting db migration.") logger.info("Starting db migration.")
trade_count = 0 trade_count = 0
@@ -47,9 +47,11 @@ def start_convert_db(args: Dict[str, Any]) -> None:
max_order_id = session_target.scalar(select(func.max(Order.id))) max_order_id = session_target.scalar(select(func.max(Order.id)))
max_pairlock_id = session_target.scalar(select(func.max(PairLock.id))) max_pairlock_id = session_target.scalar(select(func.max(PairLock.id)))
set_sequence_ids(session_target.get_bind(), set_sequence_ids(
session_target.get_bind(),
trade_id=max_trade_id, trade_id=max_trade_id,
order_id=max_order_id, order_id=max_order_id,
pairlock_id=max_pairlock_id) pairlock_id=max_pairlock_id,
)
logger.info(f"Migrated {trade_count} Trades, and {pairlock_count} Pairlocks.") logger.info(f"Migrated {trade_count} Trades, and {pairlock_count} Pairlocks.")
+40 -35
View File
@@ -9,13 +9,17 @@ from freqtrade.configuration import setup_utils_configuration
from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir
from freqtrade.constants import USERPATH_STRATEGIES from freqtrade.constants import USERPATH_STRATEGIES
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import ConfigurationError, OperationalException
from freqtrade.util import render_template, render_template_with_fallback from freqtrade.util import render_template, render_template_with_fallback
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Timeout for requests
req_timeout = 30
def start_create_userdir(args: Dict[str, Any]) -> None: def start_create_userdir(args: Dict[str, Any]) -> None:
""" """
Create "user_data" directory to contain user data strategies, hyperopt, ...) Create "user_data" directory to contain user data strategies, hyperopt, ...)
@@ -34,7 +38,7 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
""" """
Deploy new strategy from template to strategy_path Deploy new strategy from template to strategy_path
""" """
fallback = 'full' fallback = "full"
attributes = render_template_with_fallback( attributes = render_template_with_fallback(
templatefile=f"strategy_subtemplates/strategy_attributes_{subtemplate}.j2", templatefile=f"strategy_subtemplates/strategy_attributes_{subtemplate}.j2",
templatefallbackfile=f"strategy_subtemplates/strategy_attributes_{fallback}.j2", templatefallbackfile=f"strategy_subtemplates/strategy_attributes_{fallback}.j2",
@@ -60,44 +64,46 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
templatefallbackfile="strategy_subtemplates/strategy_methods_empty.j2", templatefallbackfile="strategy_subtemplates/strategy_methods_empty.j2",
) )
strategy_text = render_template(templatefile='base_strategy.py.j2', strategy_text = render_template(
arguments={"strategy": strategy_name, templatefile="base_strategy.py.j2",
arguments={
"strategy": strategy_name,
"attributes": attributes, "attributes": attributes,
"indicators": indicators, "indicators": indicators,
"buy_trend": buy_trend, "buy_trend": buy_trend,
"sell_trend": sell_trend, "sell_trend": sell_trend,
"plot_config": plot_config, "plot_config": plot_config,
"additional_methods": additional_methods, "additional_methods": additional_methods,
}) },
)
logger.info(f"Writing strategy to `{strategy_path}`.") logger.info(f"Writing strategy to `{strategy_path}`.")
strategy_path.write_text(strategy_text) strategy_path.write_text(strategy_text)
def start_new_strategy(args: Dict[str, Any]) -> None: def start_new_strategy(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if "strategy" in args and args["strategy"]: if "strategy" in args and args["strategy"]:
new_path = config["user_data_dir"] / USERPATH_STRATEGIES / (args["strategy"] + ".py")
new_path = config['user_data_dir'] / USERPATH_STRATEGIES / (args['strategy'] + '.py')
if new_path.exists(): if new_path.exists():
raise OperationalException(f"`{new_path}` already exists. " raise OperationalException(
"Please choose another Strategy Name.") f"`{new_path}` already exists. Please choose another Strategy Name."
)
deploy_new_strategy(args['strategy'], new_path, args['template']) deploy_new_strategy(args["strategy"], new_path, args["template"])
else: else:
raise OperationalException("`new-strategy` requires --strategy to be set.") raise ConfigurationError("`new-strategy` requires --strategy to be set.")
def clean_ui_subdir(directory: Path): def clean_ui_subdir(directory: Path):
if directory.is_dir(): if directory.is_dir():
logger.info("Removing UI directory content.") logger.info("Removing UI directory content.")
for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root for p in reversed(list(directory.glob("**/*"))): # iterate contents from leaves to root
if p.name in ('.gitkeep', 'fallback_file.html'): if p.name in (".gitkeep", "fallback_file.html"):
continue continue
if p.is_file(): if p.is_file():
p.unlink() p.unlink()
@@ -106,11 +112,11 @@ def clean_ui_subdir(directory: Path):
def read_ui_version(dest_folder: Path) -> Optional[str]: def read_ui_version(dest_folder: Path) -> Optional[str]:
file = dest_folder / '.uiversion' file = dest_folder / ".uiversion"
if not file.is_file(): if not file.is_file():
return None return None
with file.open('r') as f: with file.open("r") as f:
return f.read() return f.read()
@@ -119,7 +125,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
from zipfile import ZipFile from zipfile import ZipFile
logger.info(f"Downloading {dl_url}") logger.info(f"Downloading {dl_url}")
resp = requests.get(dl_url).content resp = requests.get(dl_url, timeout=req_timeout).content
dest_folder.mkdir(parents=True, exist_ok=True) dest_folder.mkdir(parents=True, exist_ok=True)
with ZipFile(BytesIO(resp)) as zf: with ZipFile(BytesIO(resp)) as zf:
for fn in zf.filelist: for fn in zf.filelist:
@@ -129,55 +135,54 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
destfile.mkdir(exist_ok=True) destfile.mkdir(exist_ok=True)
else: else:
destfile.write_bytes(x.read()) destfile.write_bytes(x.read())
with (dest_folder / '.uiversion').open('w') as f: with (dest_folder / ".uiversion").open("w") as f:
f.write(version) f.write(version)
def get_ui_download_url(version: Optional[str] = None) -> Tuple[str, str]: def get_ui_download_url(version: Optional[str] = None) -> Tuple[str, str]:
base_url = 'https://api.github.com/repos/freqtrade/frequi/' base_url = "https://api.github.com/repos/freqtrade/frequi/"
# Get base UI Repo path # Get base UI Repo path
resp = requests.get(f"{base_url}releases") resp = requests.get(f"{base_url}releases", timeout=req_timeout)
resp.raise_for_status() resp.raise_for_status()
r = resp.json() r = resp.json()
if version: if version:
tmp = [x for x in r if x['name'] == version] tmp = [x for x in r if x["name"] == version]
if tmp: if tmp:
latest_version = tmp[0]['name'] latest_version = tmp[0]["name"]
assets = tmp[0].get('assets', []) assets = tmp[0].get("assets", [])
else: else:
raise ValueError("UI-Version not found.") raise ValueError("UI-Version not found.")
else: else:
latest_version = r[0]['name'] latest_version = r[0]["name"]
assets = r[0].get('assets', []) assets = r[0].get("assets", [])
dl_url = '' dl_url = ""
if assets and len(assets) > 0: if assets and len(assets) > 0:
dl_url = assets[0]['browser_download_url'] dl_url = assets[0]["browser_download_url"]
# URL not found - try assets url # URL not found - try assets url
if not dl_url: if not dl_url:
assets = r[0]['assets_url'] assets = r[0]["assets_url"]
resp = requests.get(assets) resp = requests.get(assets, timeout=req_timeout)
r = resp.json() r = resp.json()
dl_url = r[0]['browser_download_url'] dl_url = r[0]["browser_download_url"]
return dl_url, latest_version return dl_url, latest_version
def start_install_ui(args: Dict[str, Any]) -> None: def start_install_ui(args: Dict[str, Any]) -> None:
dest_folder = Path(__file__).parents[1] / "rpc/api_server/ui/installed/"
dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui/installed/'
# First make sure the assets are removed. # 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"))
curr_version = read_ui_version(dest_folder) curr_version = read_ui_version(dest_folder)
if curr_version == latest_version and not args.get('erase_ui_only'): if curr_version == latest_version and not args.get("erase_ui_only"):
logger.info(f"UI already up-to-date, FreqUI Version {curr_version}.") logger.info(f"UI already up-to-date, FreqUI Version {curr_version}.")
return return
clean_ui_subdir(dest_folder) clean_ui_subdir(dest_folder)
if args.get('erase_ui_only'): if args.get("erase_ui_only"):
logger.info("Erased UI directory content. Not downloading new version.") logger.info("Erased UI directory content. Not downloading new version.")
else: else:
# Download a new version # Download a new version
+40 -28
View File
@@ -22,15 +22,15 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
print_colorized = config.get('print_colorized', False) print_colorized = config.get("print_colorized", False)
print_json = config.get('print_json', False) print_json = config.get("print_json", False)
export_csv = config.get('export_csv') export_csv = config.get("export_csv")
no_details = config.get('hyperopt_list_no_details', False) no_details = config.get("hyperopt_list_no_details", False)
no_header = False no_header = False
results_file = get_latest_hyperopt_file( results_file = get_latest_hyperopt_file(
config['user_data_dir'] / 'hyperopt_results', config["user_data_dir"] / "hyperopt_results", config.get("hyperoptexportfilename")
config.get('hyperoptexportfilename')) )
# Previous evaluations # Previous evaluations
epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config) epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
@@ -40,21 +40,26 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
if not export_csv: if not export_csv:
try: try:
print(HyperoptTools.get_result_table(config, epochs, total_epochs, print(
not config.get('hyperopt_list_best', False), HyperoptTools.get_result_table(
print_colorized, 0)) config,
epochs,
total_epochs,
not config.get("hyperopt_list_best", False),
print_colorized,
0,
)
)
except KeyboardInterrupt: except KeyboardInterrupt:
print('User interrupted..') print("User interrupted..")
if epochs and not no_details: if epochs and not no_details:
sorted_epochs = sorted(epochs, key=itemgetter('loss')) sorted_epochs = sorted(epochs, key=itemgetter("loss"))
results = sorted_epochs[0] results = sorted_epochs[0]
HyperoptTools.show_epoch_details(results, total_epochs, print_json, no_header) HyperoptTools.show_epoch_details(results, total_epochs, print_json, no_header)
if epochs and export_csv: if epochs and export_csv:
HyperoptTools.export_csv_file( HyperoptTools.export_csv_file(config, epochs, export_csv)
config, epochs, export_csv
)
def start_hyperopt_show(args: Dict[str, Any]) -> None: def start_hyperopt_show(args: Dict[str, Any]) -> None:
@@ -65,13 +70,13 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
print_json = config.get('print_json', False) print_json = config.get("print_json", False)
no_header = config.get('hyperopt_show_no_header', False) no_header = config.get("hyperopt_show_no_header", False)
results_file = get_latest_hyperopt_file( results_file = get_latest_hyperopt_file(
config['user_data_dir'] / 'hyperopt_results', config["user_data_dir"] / "hyperopt_results", config.get("hyperoptexportfilename")
config.get('hyperoptexportfilename')) )
n = config.get('hyperopt_show_index', -1) n = config.get("hyperopt_show_index", -1)
# Previous evaluations # Previous evaluations
epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config) epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
@@ -80,10 +85,12 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
if n > filtered_epochs: if n > filtered_epochs:
raise OperationalException( raise OperationalException(
f"The index of the epoch to show should be less than {filtered_epochs + 1}.") f"The index of the epoch to show should be less than {filtered_epochs + 1}."
)
if n < -filtered_epochs: if n < -filtered_epochs:
raise OperationalException( raise OperationalException(
f"The index of the epoch to show should be greater than {-filtered_epochs - 1}.") f"The index of the epoch to show should be greater than {-filtered_epochs - 1}."
)
# Translate epoch index from human-readable format to pythonic # Translate epoch index from human-readable format to pythonic
if n > 0: if n > 0:
@@ -92,13 +99,18 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
if epochs: if epochs:
val = epochs[n] val = epochs[n]
metrics = val['results_metrics'] metrics = val["results_metrics"]
if 'strategy_name' in metrics: if "strategy_name" in metrics:
strategy_name = metrics['strategy_name'] strategy_name = metrics["strategy_name"]
show_backtest_result(strategy_name, metrics, show_backtest_result(
metrics['stake_currency'], config.get('backtest_breakdown', [])) strategy_name,
metrics,
metrics["stake_currency"],
config.get("backtest_breakdown", []),
)
HyperoptTools.try_export_params(config, strategy_name, val) HyperoptTools.try_export_params(config, strategy_name, val)
HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header, HyperoptTools.show_epoch_details(
header_str="Epoch details") val, total_epochs, print_json, no_header, header_str="Epoch details"
)
+144 -99
View File
@@ -10,7 +10,7 @@ from tabulate import tabulate
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import ConfigurationError, OperationalException
from freqtrade.exchange import list_available_exchanges, market_is_active from freqtrade.exchange import list_available_exchanges, market_is_active
from freqtrade.misc import parse_db_uri_for_logging, plural from freqtrade.misc import parse_db_uri_for_logging, plural
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
@@ -26,42 +26,47 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
:param args: Cli args from Arguments() :param args: Cli args from Arguments()
:return: None :return: None
""" """
exchanges = list_available_exchanges(args['list_exchanges_all']) exchanges = list_available_exchanges(args["list_exchanges_all"])
if args['print_one_column']: if args["print_one_column"]:
print('\n'.join([e['name'] for e in exchanges])) print("\n".join([e["name"] for e in exchanges]))
else: else:
headers = { headers = {
'name': 'Exchange name', "name": "Exchange name",
'supported': 'Supported', "supported": "Supported",
'trade_modes': 'Markets', "trade_modes": "Markets",
'comment': 'Reason', "comment": "Reason",
} }
headers.update({'valid': 'Valid'} if args['list_exchanges_all'] else {}) headers.update({"valid": "Valid"} if args["list_exchanges_all"] else {})
def build_entry(exchange: ValidExchangesType, valid: bool): def build_entry(exchange: ValidExchangesType, valid: bool):
valid_entry = {'valid': exchange['valid']} if valid else {} valid_entry = {"valid": exchange["valid"]} if valid else {}
result: Dict[str, Union[str, bool]] = { result: Dict[str, Union[str, bool]] = {
'name': exchange['name'], "name": exchange["name"],
**valid_entry, **valid_entry,
'supported': 'Official' if exchange['supported'] else '', "supported": "Official" if exchange["supported"] else "",
'trade_modes': ', '.join( "trade_modes": ", ".join(
(f"{a['margin_mode']} " if a['margin_mode'] else '') + a['trading_mode'] (f"{a['margin_mode']} " if a["margin_mode"] else "") + a["trading_mode"]
for a in exchange['trade_modes'] for a in exchange["trade_modes"]
), ),
'comment': exchange['comment'], "comment": exchange["comment"],
} }
return result return result
if args['list_exchanges_all']: if args["list_exchanges_all"]:
print("All exchanges supported by the ccxt library:") print("All exchanges supported by the ccxt library:")
exchanges = [build_entry(e, True) for e in exchanges] exchanges = [build_entry(e, True) for e in exchanges]
else: else:
print("Exchanges available for Freqtrade:") print("Exchanges available for Freqtrade:")
exchanges = [build_entry(e, False) for e in exchanges if e['valid'] is not False] exchanges = [build_entry(e, False) for e in exchanges if e["valid"] is not False]
print(tabulate(exchanges, headers=headers, )) print(
tabulate(
exchanges,
headers=headers,
)
)
def _print_objs_tabular(objs: List, print_colorized: bool) -> None: def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
@@ -71,26 +76,35 @@ def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
yellow = Fore.YELLOW yellow = Fore.YELLOW
reset = Style.RESET_ALL reset = Style.RESET_ALL
else: else:
red = '' red = ""
yellow = '' yellow = ""
reset = '' reset = ""
names = [s['name'] for s in objs] names = [s["name"] for s in objs]
objs_to_print = [{ objs_to_print = [
'name': s['name'] if s['name'] else "--", {
'location': s['location_rel'], "name": s["name"] if s["name"] else "--",
'status': (red + "LOAD FAILED" + reset if s['class'] is None "location": s["location_rel"],
else "OK" if names.count(s['name']) == 1 "status": (
else yellow + "DUPLICATE NAME" + reset) red + "LOAD FAILED" + reset
} for s in objs] if s["class"] is None
else "OK"
if names.count(s["name"]) == 1
else yellow + "DUPLICATE NAME" + reset
),
}
for s in objs
]
for idx, s in enumerate(objs): for idx, s in enumerate(objs):
if 'hyperoptable' in s: if "hyperoptable" in s:
objs_to_print[idx].update({ objs_to_print[idx].update(
'hyperoptable': "Yes" if s['hyperoptable']['count'] > 0 else "No", {
'buy-Params': len(s['hyperoptable'].get('buy', [])), "hyperoptable": "Yes" if s["hyperoptable"]["count"] > 0 else "No",
'sell-Params': len(s['hyperoptable'].get('sell', [])), "buy-Params": len(s["hyperoptable"].get("buy", [])),
}) "sell-Params": len(s["hyperoptable"].get("sell", [])),
print(tabulate(objs_to_print, headers='keys', tablefmt='psql', stralign='right')) }
)
print(tabulate(objs_to_print, headers="keys", tablefmt="psql", stralign="right"))
def start_list_strategies(args: Dict[str, Any]) -> None: def start_list_strategies(args: Dict[str, Any]) -> None:
@@ -100,19 +114,20 @@ def start_list_strategies(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
strategy_objs = StrategyResolver.search_all_objects( strategy_objs = StrategyResolver.search_all_objects(
config, not args['print_one_column'], config.get('recursive_strategy_search', False)) config, not args["print_one_column"], config.get("recursive_strategy_search", False)
)
# Sort alphabetically # Sort alphabetically
strategy_objs = sorted(strategy_objs, key=lambda x: x['name']) strategy_objs = sorted(strategy_objs, key=lambda x: x["name"])
for obj in strategy_objs: for obj in strategy_objs:
if obj['class']: if obj["class"]:
obj['hyperoptable'] = obj['class'].detect_all_parameters() obj["hyperoptable"] = obj["class"].detect_all_parameters()
else: else:
obj['hyperoptable'] = {'count': 0} obj["hyperoptable"] = {"count": 0}
if args['print_one_column']: if args["print_one_column"]:
print('\n'.join([s['name'] for s in strategy_objs])) print("\n".join([s["name"] for s in strategy_objs]))
else: else:
_print_objs_tabular(strategy_objs, config.get('print_colorized', False)) _print_objs_tabular(strategy_objs, config.get("print_colorized", False))
def start_list_freqAI_models(args: Dict[str, Any]) -> None: def start_list_freqAI_models(args: Dict[str, Any]) -> None:
@@ -121,13 +136,14 @@ def start_list_freqAI_models(args: Dict[str, Any]) -> None:
""" """
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
model_objs = FreqaiModelResolver.search_all_objects(config, not args['print_one_column'])
model_objs = FreqaiModelResolver.search_all_objects(config, not args["print_one_column"])
# Sort alphabetically # Sort alphabetically
model_objs = sorted(model_objs, key=lambda x: x['name']) model_objs = sorted(model_objs, key=lambda x: x["name"])
if args['print_one_column']: if args["print_one_column"]:
print('\n'.join([s['name'] for s in model_objs])) print("\n".join([s["name"] for s in model_objs]))
else: else:
_print_objs_tabular(model_objs, config.get('print_colorized', False)) _print_objs_tabular(model_objs, config.get("print_colorized", False))
def start_list_timeframes(args: Dict[str, Any]) -> None: def start_list_timeframes(args: Dict[str, Any]) -> None:
@@ -136,16 +152,18 @@ def start_list_timeframes(args: Dict[str, Any]) -> None:
""" """
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
# Do not use timeframe set in the config # Do not use timeframe set in the config
config['timeframe'] = None config["timeframe"] = None
# Init exchange # Init exchange
exchange = ExchangeResolver.load_exchange(config, validate=False) exchange = ExchangeResolver.load_exchange(config, validate=False)
if args['print_one_column']: if args["print_one_column"]:
print('\n'.join(exchange.timeframes)) print("\n".join(exchange.timeframes))
else: else:
print(f"Timeframes available for the exchange `{exchange.name}`: " print(
f"{', '.join(exchange.timeframes)}") f"Timeframes available for the exchange `{exchange.name}`: "
f"{', '.join(exchange.timeframes)}"
)
def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
@@ -161,51 +179,75 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
exchange = ExchangeResolver.load_exchange(config, validate=False) exchange = ExchangeResolver.load_exchange(config, validate=False)
# By default only active pairs/markets are to be shown # By default only active pairs/markets are to be shown
active_only = not args.get('list_pairs_all', False) active_only = not args.get("list_pairs_all", False)
base_currencies = args.get('base_currencies', []) base_currencies = args.get("base_currencies", [])
quote_currencies = args.get('quote_currencies', []) quote_currencies = args.get("quote_currencies", [])
try: try:
pairs = exchange.get_markets(base_currencies=base_currencies, pairs = exchange.get_markets(
base_currencies=base_currencies,
quote_currencies=quote_currencies, quote_currencies=quote_currencies,
tradable_only=pairs_only, tradable_only=pairs_only,
active_only=active_only) active_only=active_only,
)
# Sort the pairs/markets by symbol # Sort the pairs/markets by symbol
pairs = dict(sorted(pairs.items())) pairs = dict(sorted(pairs.items()))
except Exception as e: except Exception as e:
raise OperationalException(f"Cannot get markets. Reason: {e}") from e raise OperationalException(f"Cannot get markets. Reason: {e}") from e
else: else:
summary_str = ((f"Exchange {exchange.name} has {len(pairs)} ") + summary_str = (
("active " if active_only else "") + (f"Exchange {exchange.name} has {len(pairs)} ")
(plural(len(pairs), "pair" if pairs_only else "market")) + + ("active " if active_only else "")
(f" with {', '.join(base_currencies)} as base " + (plural(len(pairs), "pair" if pairs_only else "market"))
+ (
f" with {', '.join(base_currencies)} as base "
f"{plural(len(base_currencies), 'currency', 'currencies')}" f"{plural(len(base_currencies), 'currency', 'currencies')}"
if base_currencies else "") + if base_currencies
(" and" if base_currencies and quote_currencies else "") + else ""
(f" with {', '.join(quote_currencies)} as quote " )
+ (" and" if base_currencies and quote_currencies else "")
+ (
f" with {', '.join(quote_currencies)} as quote "
f"{plural(len(quote_currencies), 'currency', 'currencies')}" f"{plural(len(quote_currencies), 'currency', 'currencies')}"
if quote_currencies else "")) if quote_currencies
else ""
)
)
headers = ["Id", "Symbol", "Base", "Quote", "Active", headers = [
"Spot", "Margin", "Future", "Leverage"] "Id",
"Symbol",
"Base",
"Quote",
"Active",
"Spot",
"Margin",
"Future",
"Leverage",
]
tabular_data = [{ tabular_data = [
'Id': v['id'], {
'Symbol': v['symbol'], "Id": v["id"],
'Base': v['base'], "Symbol": v["symbol"],
'Quote': v['quote'], "Base": v["base"],
'Active': market_is_active(v), "Quote": v["quote"],
'Spot': 'Spot' if exchange.market_is_spot(v) else '', "Active": market_is_active(v),
'Margin': 'Margin' if exchange.market_is_margin(v) else '', "Spot": "Spot" if exchange.market_is_spot(v) else "",
'Future': 'Future' if exchange.market_is_future(v) else '', "Margin": "Margin" if exchange.market_is_margin(v) else "",
'Leverage': exchange.get_max_leverage(v['symbol'], 20) "Future": "Future" if exchange.market_is_future(v) else "",
} for _, v in pairs.items()] "Leverage": exchange.get_max_leverage(v["symbol"], 20),
}
for _, v in pairs.items()
]
if (args.get('print_one_column', False) or if (
args.get('list_pairs_print_json', False) or args.get("print_one_column", False)
args.get('print_csv', 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 # Print summary string in the log in case of machine-readable
# regular formats. # regular formats.
logger.info(f"{summary_str}.") logger.info(f"{summary_str}.")
@@ -215,24 +257,26 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
print() print()
if pairs: if pairs:
if args.get('print_list', False): if args.get("print_list", False):
# print data as a list, with human-readable summary # print data as a list, with human-readable summary
print(f"{summary_str}: {', '.join(pairs.keys())}.") print(f"{summary_str}: {', '.join(pairs.keys())}.")
elif args.get('print_one_column', False): elif args.get("print_one_column", False):
print('\n'.join(pairs.keys())) print("\n".join(pairs.keys()))
elif args.get('list_pairs_print_json', False): elif args.get("list_pairs_print_json", False):
print(rapidjson.dumps(list(pairs.keys()), default=str)) print(rapidjson.dumps(list(pairs.keys()), default=str))
elif args.get('print_csv', False): elif args.get("print_csv", False):
writer = csv.DictWriter(sys.stdout, fieldnames=headers) writer = csv.DictWriter(sys.stdout, fieldnames=headers)
writer.writeheader() writer.writeheader()
writer.writerows(tabular_data) writer.writerows(tabular_data)
else: else:
# print data as a table, with the human-readable summary # print data as a table, with the human-readable summary
print(f"{summary_str}:") print(f"{summary_str}:")
print(tabulate(tabular_data, headers='keys', tablefmt='psql', stralign='right')) print(tabulate(tabular_data, headers="keys", tablefmt="psql", stralign="right"))
elif not (args.get('print_one_column', False) or elif not (
args.get('list_pairs_print_json', False) or args.get("print_one_column", False)
args.get('print_csv', False)): or args.get("list_pairs_print_json", False)
or args.get("print_csv", False)
):
print(f"{summary_str}.") print(f"{summary_str}.")
@@ -243,21 +287,22 @@ def start_show_trades(args: Dict[str, Any]) -> None:
import json import json
from freqtrade.persistence import Trade, init_db from freqtrade.persistence import Trade, init_db
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if 'db_url' not in config: if "db_url" not in config:
raise OperationalException("--db-url is required for this command.") raise ConfigurationError("--db-url is required for this command.")
logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"') logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
init_db(config['db_url']) init_db(config["db_url"])
tfilter = [] tfilter = []
if config.get('trade_ids'): if config.get("trade_ids"):
tfilter.append(Trade.id.in_(config['trade_ids'])) tfilter.append(Trade.id.in_(config["trade_ids"]))
trades = Trade.get_trades(tfilter).all() trades = Trade.get_trades(tfilter).all()
logger.info(f"Printing {len(trades)} Trades: ") logger.info(f"Printing {len(trades)} Trades: ")
if config.get('print_json', False): if config.get("print_json", False):
print(json.dumps([trade.to_json() for trade in trades], indent=4)) print(json.dumps([trade.to_json() for trade in trades], indent=4))
else: else:
for trade in trades: for trade in trades:
+23 -19
View File
@@ -4,7 +4,7 @@ from typing import Any, Dict
from freqtrade import constants from freqtrade import constants
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import ConfigurationError, OperationalException
from freqtrade.util import fmt_coin from freqtrade.util import fmt_coin
@@ -21,17 +21,19 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
config = setup_utils_configuration(args, method) config = setup_utils_configuration(args, method)
no_unlimited_runmodes = { no_unlimited_runmodes = {
RunMode.BACKTEST: 'backtesting', RunMode.BACKTEST: "backtesting",
RunMode.HYPEROPT: 'hyperoptimization', RunMode.HYPEROPT: "hyperoptimization",
} }
if method in no_unlimited_runmodes.keys(): if method in no_unlimited_runmodes.keys():
wallet_size = config['dry_run_wallet'] * config['tradable_balance_ratio'] wallet_size = config["dry_run_wallet"] * config["tradable_balance_ratio"]
# tradable_balance_ratio # tradable_balance_ratio
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT if (
and config['stake_amount'] > wallet_size): config["stake_amount"] != constants.UNLIMITED_STAKE_AMOUNT
wallet = fmt_coin(wallet_size, config['stake_currency']) and config["stake_amount"] > wallet_size
stake = fmt_coin(config['stake_amount'], config['stake_currency']) ):
raise OperationalException( wallet = fmt_coin(wallet_size, config["stake_currency"])
stake = fmt_coin(config["stake_amount"], config["stake_currency"])
raise ConfigurationError(
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. " f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`." f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
) )
@@ -51,7 +53,7 @@ def start_backtesting(args: Dict[str, Any]) -> None:
# Initialize configuration # Initialize configuration
config = setup_optimize_configuration(args, RunMode.BACKTEST) config = setup_optimize_configuration(args, RunMode.BACKTEST)
logger.info('Starting freqtrade in Backtesting mode') logger.info("Starting freqtrade in Backtesting mode")
# Initialize backtesting object # Initialize backtesting object
backtesting = Backtesting(config) backtesting = Backtesting(config)
@@ -68,7 +70,7 @@ def start_backtesting_show(args: Dict[str, Any]) -> None:
from freqtrade.data.btanalysis import load_backtest_stats from freqtrade.data.btanalysis import load_backtest_stats
from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist
results = load_backtest_stats(config['exportfilename']) results = load_backtest_stats(config["exportfilename"])
show_backtest_results(config, results) show_backtest_results(config, results)
show_sorted_pairlist(config, results) show_sorted_pairlist(config, results)
@@ -87,20 +89,20 @@ def start_hyperopt(args: Dict[str, Any]) -> None:
from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt import Hyperopt
except ImportError as e: except ImportError as e:
raise OperationalException( raise OperationalException(
f"{e}. Please ensure that the hyperopt dependencies are installed.") from e f"{e}. Please ensure that the hyperopt dependencies are installed."
) from e
# Initialize configuration # Initialize configuration
config = setup_optimize_configuration(args, RunMode.HYPEROPT) config = setup_optimize_configuration(args, RunMode.HYPEROPT)
logger.info('Starting freqtrade in Hyperopt mode') logger.info("Starting freqtrade in Hyperopt mode")
lock = FileLock(Hyperopt.get_lock_filename(config)) lock = FileLock(Hyperopt.get_lock_filename(config))
try: try:
with lock.acquire(timeout=1): with lock.acquire(timeout=1):
# Remove noisy log messages # Remove noisy log messages
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING) logging.getLogger("hyperopt.tpe").setLevel(logging.WARNING)
logging.getLogger('filelock').setLevel(logging.WARNING) logging.getLogger("filelock").setLevel(logging.WARNING)
# Initialize backtesting object # Initialize backtesting object
hyperopt = Hyperopt(config) hyperopt = Hyperopt(config)
@@ -108,9 +110,11 @@ def start_hyperopt(args: Dict[str, Any]) -> None:
except Timeout: except Timeout:
logger.info("Another running instance of freqtrade Hyperopt detected.") logger.info("Another running instance of freqtrade Hyperopt detected.")
logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. " logger.info(
"Simultaneous execution of multiple Hyperopt commands is not supported. "
"Hyperopt module is resource hungry. Please run your Hyperopt sequentially " "Hyperopt module is resource hungry. Please run your Hyperopt sequentially "
"or on separate machines.") "or on separate machines."
)
logger.info("Quitting now.") logger.info("Quitting now.")
# TODO: return False here in order to help freqtrade to exit # TODO: return False here in order to help freqtrade to exit
# with non-zero exit code... # with non-zero exit code...
@@ -127,7 +131,7 @@ def start_edge(args: Dict[str, Any]) -> None:
# Initialize configuration # Initialize configuration
config = setup_optimize_configuration(args, RunMode.EDGE) config = setup_optimize_configuration(args, RunMode.EDGE)
logger.info('Starting freqtrade in Edge mode') logger.info("Starting freqtrade in Edge mode")
# Initialize Edge object # Initialize Edge object
edge_cli = EdgeCli(config) edge_cli = EdgeCli(config)
+8 -7
View File
@@ -17,28 +17,29 @@ def start_test_pairlist(args: Dict[str, Any]) -> None:
""" """
from freqtrade.persistence import FtNoDBContext from freqtrade.persistence import FtNoDBContext
from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.pairlistmanager import PairListManager
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
exchange = ExchangeResolver.load_exchange(config, validate=False) exchange = ExchangeResolver.load_exchange(config, validate=False)
quote_currencies = args.get('quote_currencies') quote_currencies = args.get("quote_currencies")
if not quote_currencies: if not quote_currencies:
quote_currencies = [config.get('stake_currency')] quote_currencies = [config.get("stake_currency")]
results = {} results = {}
with FtNoDBContext(): with FtNoDBContext():
for curr in quote_currencies: for curr in quote_currencies:
config['stake_currency'] = curr config["stake_currency"] = curr
pairlists = PairListManager(exchange, config) pairlists = PairListManager(exchange, config)
pairlists.refresh_pairlist() pairlists.refresh_pairlist()
results[curr] = pairlists.whitelist results[curr] = pairlists.whitelist
for curr, pairlist in results.items(): for curr, pairlist in results.items():
if not args.get('print_one_column', False) and not args.get('list_pairs_print_json', False): if not args.get("print_one_column", False) and not args.get("list_pairs_print_json", False):
print(f"Pairs for {curr}: ") print(f"Pairs for {curr}: ")
if args.get('print_one_column', False): if args.get("print_one_column", False):
print('\n'.join(pairlist)) print("\n".join(pairlist))
elif args.get('list_pairs_print_json', False): elif args.get("list_pairs_print_json", False):
print(rapidjson.dumps(list(pairlist), default=str)) print(rapidjson.dumps(list(pairlist), default=str))
else: else:
print(pairlist) print(pairlist)
+7 -4
View File
@@ -2,14 +2,15 @@ from typing import Any, Dict
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import ConfigurationError
def validate_plot_args(args: Dict[str, Any]) -> None: def validate_plot_args(args: Dict[str, Any]) -> None:
if not args.get('datadir') and not args.get('config'): if not args.get("datadir") and not args.get("config"):
raise OperationalException( raise ConfigurationError(
"You need to specify either `--datadir` or `--config` " "You need to specify either `--datadir` or `--config` "
"for plot-profit and plot-dataframe.") "for plot-profit and plot-dataframe."
)
def start_plot_dataframe(args: Dict[str, Any]) -> None: def start_plot_dataframe(args: Dict[str, Any]) -> None:
@@ -18,6 +19,7 @@ def start_plot_dataframe(args: Dict[str, Any]) -> None:
""" """
# Import here to avoid errors if plot-dependencies are not installed. # Import here to avoid errors if plot-dependencies are not installed.
from freqtrade.plot.plotting import load_and_plot_trades from freqtrade.plot.plotting import load_and_plot_trades
validate_plot_args(args) validate_plot_args(args)
config = setup_utils_configuration(args, RunMode.PLOT) config = setup_utils_configuration(args, RunMode.PLOT)
@@ -30,6 +32,7 @@ def start_plot_profit(args: Dict[str, Any]) -> None:
""" """
# Import here to avoid errors if plot-dependencies are not installed. # Import here to avoid errors if plot-dependencies are not installed.
from freqtrade.plot.plotting import plot_profit from freqtrade.plot.plotting import plot_profit
validate_plot_args(args) validate_plot_args(args)
config = setup_utils_configuration(args, RunMode.PLOT) config = setup_utils_configuration(args, RunMode.PLOT)
@@ -26,13 +26,15 @@ def start_strategy_update(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
strategy_objs = StrategyResolver.search_all_objects( strategy_objs = StrategyResolver.search_all_objects(
config, enum_failed=False, recursive=config.get('recursive_strategy_search', False)) config, enum_failed=False, recursive=config.get("recursive_strategy_search", False)
)
filtered_strategy_objs = [] filtered_strategy_objs = []
if args['strategy_list']: if args["strategy_list"]:
filtered_strategy_objs = [ filtered_strategy_objs = [
strategy_obj for strategy_obj in strategy_objs strategy_obj
if strategy_obj['name'] in args['strategy_list'] for strategy_obj in strategy_objs
if strategy_obj["name"] in args["strategy_list"]
] ]
else: else:
@@ -41,8 +43,8 @@ def start_strategy_update(args: Dict[str, Any]) -> None:
processed_locations = set() processed_locations = set()
for strategy_obj in filtered_strategy_objs: for strategy_obj in filtered_strategy_objs:
if strategy_obj['location'] not in processed_locations: if strategy_obj["location"] not in processed_locations:
processed_locations.add(strategy_obj['location']) processed_locations.add(strategy_obj["location"])
start_conversion(strategy_obj, config) start_conversion(strategy_obj, config)
-5
View File
@@ -23,11 +23,6 @@ def start_trading(args: Dict[str, Any]) -> int:
signal.signal(signal.SIGTERM, term_handler) signal.signal(signal.SIGTERM, term_handler)
worker = Worker(args) worker = Worker(args)
worker.run() worker.run()
except Exception as e:
logger.error(str(e))
logger.exception("Fatal exception!")
except (KeyboardInterrupt):
logger.info('SIGINT received, aborting ...')
finally: finally:
if worker: if worker:
logger.info("worker found ... calling exit") logger.info("worker found ... calling exit")
+1
View File
@@ -1,5 +1,6 @@
# flake8: noqa: F401 # flake8: noqa: F401
from freqtrade.configuration.config_secrets import sanitize_config
from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.configuration.config_setup import setup_utils_configuration
from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.configuration.config_validation import validate_config_consistency
from freqtrade.configuration.configuration import Configuration from freqtrade.configuration.configuration import Configuration
+36
View File
@@ -0,0 +1,36 @@
from copy import deepcopy
from freqtrade.constants import Config
def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
"""
Remove sensitive information from the config.
:param config: Configuration
:param show_sensitive: Show sensitive information
:return: Configuration
"""
if show_sensitive:
return config
keys_to_remove = [
"exchange.key",
"exchange.secret",
"exchange.password",
"exchange.uid",
"telegram.token",
"telegram.chat_id",
"discord.webhook_url",
"api_server.password",
]
config = deepcopy(config)
for key in keys_to_remove:
if "." in key:
nested_keys = key.split(".")
nested_config = config
for nested_key in nested_keys[:-1]:
nested_config = nested_config.get(nested_key, {})
nested_config[nested_keys[-1]] = "REDACTED"
else:
config[key] = "REDACTED"
return config
+5 -2
View File
@@ -10,7 +10,9 @@ from .configuration import Configuration
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str, Any]: def setup_utils_configuration(
args: Dict[str, Any], method: RunMode, *, set_dry: bool = True
) -> Dict[str, Any]:
""" """
Prepare the configuration for utils subcommands Prepare the configuration for utils subcommands
:param args: Cli args from Arguments() :param args: Cli args from Arguments()
@@ -21,7 +23,8 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str
config = configuration.get_config() config = configuration.get_config()
# Ensure these modes are using Dry-run # Ensure these modes are using Dry-run
config['dry_run'] = True if set_dry:
config["dry_run"] = True
validate_config_consistency(config, preliminary=True) validate_config_consistency(config, preliminary=True)
return config return config
+192 -170
View File
@@ -9,7 +9,7 @@ from jsonschema.exceptions import ValidationError, best_match
from freqtrade import constants from freqtrade import constants
from freqtrade.configuration.deprecated_settings import process_deprecated_setting from freqtrade.configuration.deprecated_settings import process_deprecated_setting
from freqtrade.enums import RunMode, TradingMode from freqtrade.enums import RunMode, TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import ConfigurationError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -20,18 +20,16 @@ def _extend_validator(validator_class):
Extended validator for the Freqtrade configuration JSON Schema. Extended validator for the Freqtrade configuration JSON Schema.
Currently it only handles defaults for subschemas. Currently it only handles defaults for subschemas.
""" """
validate_properties = validator_class.VALIDATORS['properties'] validate_properties = validator_class.VALIDATORS["properties"]
def set_defaults(validator, properties, instance, schema): def set_defaults(validator, properties, instance, schema):
for prop, subschema in properties.items(): for prop, subschema in properties.items():
if 'default' in subschema: if "default" in subschema:
instance.setdefault(prop, subschema['default']) instance.setdefault(prop, subschema["default"])
yield from validate_properties(validator, properties, instance, schema) yield from validate_properties(validator, properties, instance, schema)
return validators.extend( return validators.extend(validator_class, {"properties": set_defaults})
validator_class, {'properties': set_defaults}
)
FreqtradeValidator = _extend_validator(Draft4Validator) FreqtradeValidator = _extend_validator(Draft4Validator)
@@ -44,27 +42,23 @@ def validate_config_schema(conf: Dict[str, Any], preliminary: bool = False) -> D
:return: Returns the config if valid, otherwise throw an exception :return: Returns the config if valid, otherwise throw an exception
""" """
conf_schema = deepcopy(constants.CONF_SCHEMA) conf_schema = deepcopy(constants.CONF_SCHEMA)
if conf.get('runmode', RunMode.OTHER) in (RunMode.DRY_RUN, RunMode.LIVE): if conf.get("runmode", RunMode.OTHER) in (RunMode.DRY_RUN, RunMode.LIVE):
conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED conf_schema["required"] = constants.SCHEMA_TRADE_REQUIRED
elif conf.get('runmode', RunMode.OTHER) in (RunMode.BACKTEST, RunMode.HYPEROPT): elif conf.get("runmode", RunMode.OTHER) in (RunMode.BACKTEST, RunMode.HYPEROPT):
if preliminary: if preliminary:
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED conf_schema["required"] = constants.SCHEMA_BACKTEST_REQUIRED
else: else:
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED_FINAL conf_schema["required"] = constants.SCHEMA_BACKTEST_REQUIRED_FINAL
elif conf.get('runmode', RunMode.OTHER) == RunMode.WEBSERVER: elif conf.get("runmode", RunMode.OTHER) == RunMode.WEBSERVER:
conf_schema['required'] = constants.SCHEMA_MINIMAL_WEBSERVER conf_schema["required"] = constants.SCHEMA_MINIMAL_WEBSERVER
else: else:
conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED conf_schema["required"] = constants.SCHEMA_MINIMAL_REQUIRED
try: try:
FreqtradeValidator(conf_schema).validate(conf) FreqtradeValidator(conf_schema).validate(conf)
return conf return conf
except ValidationError as e: except ValidationError as e:
logger.critical( logger.critical(f"Invalid configuration. Reason: {e}")
f"Invalid configuration. Reason: {e}" raise ValidationError(best_match(Draft4Validator(conf_schema).iter_errors(conf)).message)
)
raise ValidationError(
best_match(Draft4Validator(conf_schema).iter_errors(conf)).message
)
def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = False) -> None: def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = False) -> None:
@@ -73,7 +67,7 @@ def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = Fal
Should be ran after loading both configuration and strategy, Should be ran after loading both configuration and strategy,
since strategies can set certain configuration settings too. since strategies can set certain configuration settings too.
:param conf: Config in JSON format :param conf: Config in JSON format
:return: Returns None if everything is ok, otherwise throw an OperationalException :return: Returns None if everything is ok, otherwise throw an ConfigurationError
""" """
# validating trailing stoploss # validating trailing stoploss
@@ -91,19 +85,21 @@ def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = Fal
validate_migrated_strategy_settings(conf) validate_migrated_strategy_settings(conf)
# validate configuration before returning # validate configuration before returning
logger.info('Validating configuration ...') logger.info("Validating configuration ...")
validate_config_schema(conf, preliminary=preliminary) validate_config_schema(conf, preliminary=preliminary)
def _validate_unlimited_amount(conf: Dict[str, Any]) -> None: def _validate_unlimited_amount(conf: Dict[str, Any]) -> None:
""" """
If edge is disabled, either max_open_trades or stake_amount need to be set. If edge is disabled, either max_open_trades or stake_amount need to be set.
:raise: OperationalException if config validation failed :raise: ConfigurationError if config validation failed
""" """
if (not conf.get('edge', {}).get('enabled') if (
and conf.get('max_open_trades') == float('inf') not conf.get("edge", {}).get("enabled")
and conf.get('stake_amount') == constants.UNLIMITED_STAKE_AMOUNT): and conf.get("max_open_trades") == float("inf")
raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.") and conf.get("stake_amount") == constants.UNLIMITED_STAKE_AMOUNT
):
raise ConfigurationError("`max_open_trades` and `stake_amount` cannot both be unlimited.")
def _validate_price_config(conf: Dict[str, Any]) -> None: def _validate_price_config(conf: Dict[str, Any]) -> None:
@@ -111,45 +107,47 @@ def _validate_price_config(conf: Dict[str, Any]) -> None:
When using market orders, price sides must be using the "other" side of the price When using market orders, price sides must be using the "other" side of the price
""" """
# TODO: The below could be an enforced setting when using market orders # TODO: The below could be an enforced setting when using market orders
if (conf.get('order_types', {}).get('entry') == 'market' if conf.get("order_types", {}).get("entry") == "market" and conf.get("entry_pricing", {}).get(
and conf.get('entry_pricing', {}).get('price_side') not in ('ask', 'other')): "price_side"
raise OperationalException( ) not in ("ask", "other"):
'Market entry orders require entry_pricing.price_side = "other".') raise ConfigurationError('Market entry orders require entry_pricing.price_side = "other".')
if (conf.get('order_types', {}).get('exit') == 'market' if conf.get("order_types", {}).get("exit") == "market" and conf.get("exit_pricing", {}).get(
and conf.get('exit_pricing', {}).get('price_side') not in ('bid', 'other')): "price_side"
raise OperationalException('Market exit orders require exit_pricing.price_side = "other".') ) not in ("bid", "other"):
raise ConfigurationError('Market exit orders require exit_pricing.price_side = "other".')
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
if conf.get("stoploss") == 0.0:
if conf.get('stoploss') == 0.0: raise ConfigurationError(
raise OperationalException( "The config stoploss needs to be different from 0 to avoid problems with sell orders."
'The config stoploss needs to be different from 0 to avoid problems with sell orders.'
) )
# Skip if trailing stoploss is not activated # Skip if trailing stoploss is not activated
if not conf.get('trailing_stop', False): if not conf.get("trailing_stop", False):
return return
tsl_positive = float(conf.get('trailing_stop_positive', 0)) tsl_positive = float(conf.get("trailing_stop_positive", 0))
tsl_offset = float(conf.get('trailing_stop_positive_offset', 0)) tsl_offset = float(conf.get("trailing_stop_positive_offset", 0))
tsl_only_offset = conf.get('trailing_only_offset_is_reached', False) tsl_only_offset = conf.get("trailing_only_offset_is_reached", False)
if tsl_only_offset: if tsl_only_offset:
if tsl_positive == 0.0: if tsl_positive == 0.0:
raise OperationalException( raise ConfigurationError(
'The config trailing_only_offset_is_reached needs ' "The config trailing_only_offset_is_reached needs "
'trailing_stop_positive_offset to be more than 0 in your config.') "trailing_stop_positive_offset to be more than 0 in your config."
)
if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive: if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive:
raise OperationalException( raise ConfigurationError(
'The config trailing_stop_positive_offset needs ' "The config trailing_stop_positive_offset needs "
'to be greater than trailing_stop_positive in your config.') "to be greater than trailing_stop_positive in your config."
)
# Fetch again without default # Fetch again without default
if 'trailing_stop_positive' in conf and float(conf['trailing_stop_positive']) == 0.0: if "trailing_stop_positive" in conf and float(conf["trailing_stop_positive"]) == 0.0:
raise OperationalException( raise ConfigurationError(
'The config trailing_stop_positive needs to be different from 0 ' "The config trailing_stop_positive needs to be different from 0 "
'to avoid problems with sell orders.' "to avoid problems with sell orders."
) )
@@ -158,11 +156,11 @@ def _validate_edge(conf: Dict[str, Any]) -> None:
Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists. Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists.
""" """
if not conf.get('edge', {}).get('enabled'): if not conf.get("edge", {}).get("enabled"):
return return
if not conf.get('use_exit_signal', True): if not conf.get("use_exit_signal", True):
raise OperationalException( raise ConfigurationError(
"Edge requires `use_exit_signal` to be True, otherwise no sells will happen." "Edge requires `use_exit_signal` to be True, otherwise no sells will happen."
) )
@@ -171,14 +169,21 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None:
""" """
Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does. Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does.
""" """
if conf.get('runmode', RunMode.OTHER) in [RunMode.OTHER, RunMode.PLOT, if conf.get("runmode", RunMode.OTHER) in [
RunMode.UTIL_NO_EXCHANGE, RunMode.UTIL_EXCHANGE]: RunMode.OTHER,
RunMode.PLOT,
RunMode.UTIL_NO_EXCHANGE,
RunMode.UTIL_EXCHANGE,
]:
return return
for pl in conf.get('pairlists', [{'method': 'StaticPairList'}]): for pl in conf.get("pairlists", [{"method": "StaticPairList"}]):
if (isinstance(pl, dict) and pl.get('method') == 'StaticPairList' if (
and not conf.get('exchange', {}).get('pair_whitelist')): isinstance(pl, dict)
raise OperationalException("StaticPairList requires pair_whitelist to be set.") and pl.get("method") == "StaticPairList"
and not conf.get("exchange", {}).get("pair_whitelist")
):
raise ConfigurationError("StaticPairList requires pair_whitelist to be set.")
def _validate_protections(conf: Dict[str, Any]) -> None: def _validate_protections(conf: Dict[str, Any]) -> None:
@@ -186,33 +191,33 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
Validate protection configuration validity Validate protection configuration validity
""" """
for prot in conf.get('protections', []): for prot in conf.get("protections", []):
if ('stop_duration' in prot and 'stop_duration_candles' in prot): if "stop_duration" in prot and "stop_duration_candles" in prot:
raise OperationalException( raise ConfigurationError(
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n" "Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
f"Please fix the protection {prot.get('method')}" f"Please fix the protection {prot.get('method')}"
) )
if ('lookback_period' in prot and 'lookback_period_candles' in prot): if "lookback_period" in prot and "lookback_period_candles" in prot:
raise OperationalException( raise ConfigurationError(
"Protections must specify either `lookback_period` or `lookback_period_candles`.\n" "Protections must specify either `lookback_period` or `lookback_period_candles`.\n"
f"Please fix the protection {prot.get('method')}" f"Please fix the protection {prot.get('method')}"
) )
def _validate_ask_orderbook(conf: Dict[str, Any]) -> None: def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
ask_strategy = conf.get('exit_pricing', {}) ask_strategy = conf.get("exit_pricing", {})
ob_min = ask_strategy.get('order_book_min') ob_min = ask_strategy.get("order_book_min")
ob_max = ask_strategy.get('order_book_max') ob_max = ask_strategy.get("order_book_max")
if ob_min is not None and ob_max is not None and ask_strategy.get('use_order_book'): if ob_min is not None and ob_max is not None and ask_strategy.get("use_order_book"):
if ob_min != ob_max: if ob_min != ob_max:
raise OperationalException( raise ConfigurationError(
"Using order_book_max != order_book_min in exit_pricing is no longer supported." "Using order_book_max != order_book_min in exit_pricing is no longer supported."
"Please pick one value and use `order_book_top` in the future." "Please pick one value and use `order_book_top` in the future."
) )
else: else:
# Move value to order_book_top # Move value to order_book_top
ask_strategy['order_book_top'] = ob_min ask_strategy["order_book_top"] = ob_min
logger.warning( logger.warning(
"DEPRECATED: " "DEPRECATED: "
"Please use `order_book_top` instead of `order_book_min` and `order_book_max` " "Please use `order_book_top` instead of `order_book_min` and `order_book_max` "
@@ -221,7 +226,6 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None: def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
_validate_time_in_force(conf) _validate_time_in_force(conf)
_validate_order_types(conf) _validate_order_types(conf)
_validate_unfilledtimeout(conf) _validate_unfilledtimeout(conf)
@@ -230,119 +234,129 @@ def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
def _validate_time_in_force(conf: Dict[str, Any]) -> None: def _validate_time_in_force(conf: Dict[str, Any]) -> None:
time_in_force = conf.get("order_time_in_force", {})
time_in_force = conf.get('order_time_in_force', {}) if "buy" in time_in_force or "sell" in time_in_force:
if 'buy' in time_in_force or 'sell' in time_in_force: if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: raise ConfigurationError(
raise OperationalException( "Please migrate your time_in_force settings to use 'entry' and 'exit'."
"Please migrate your time_in_force settings to use 'entry' and 'exit'.") )
else: else:
logger.warning( logger.warning(
"DEPRECATED: Using 'buy' and 'sell' for time_in_force is deprecated." "DEPRECATED: Using 'buy' and 'sell' for time_in_force is deprecated."
"Please migrate your time_in_force settings to use 'entry' and 'exit'." "Please migrate your time_in_force settings to use 'entry' and 'exit'."
) )
process_deprecated_setting( process_deprecated_setting(
conf, 'order_time_in_force', 'buy', 'order_time_in_force', 'entry') conf, "order_time_in_force", "buy", "order_time_in_force", "entry"
)
process_deprecated_setting( process_deprecated_setting(
conf, 'order_time_in_force', 'sell', 'order_time_in_force', 'exit') conf, "order_time_in_force", "sell", "order_time_in_force", "exit"
)
def _validate_order_types(conf: Dict[str, Any]) -> None: def _validate_order_types(conf: Dict[str, Any]) -> None:
order_types = conf.get("order_types", {})
order_types = conf.get('order_types', {}) old_order_types = [
old_order_types = ['buy', 'sell', 'emergencysell', 'forcebuy', "buy",
'forcesell', 'emergencyexit', 'forceexit', 'forceentry'] "sell",
"emergencysell",
"forcebuy",
"forcesell",
"emergencyexit",
"forceexit",
"forceentry",
]
if any(x in order_types for x in old_order_types): if any(x in order_types for x in old_order_types):
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
raise OperationalException( raise ConfigurationError(
"Please migrate your order_types settings to use the new wording.") "Please migrate your order_types settings to use the new wording."
)
else: else:
logger.warning( logger.warning(
"DEPRECATED: Using 'buy' and 'sell' for order_types is deprecated." "DEPRECATED: Using 'buy' and 'sell' for order_types is deprecated."
"Please migrate your order_types settings to use 'entry' and 'exit' wording." "Please migrate your order_types settings to use 'entry' and 'exit' wording."
) )
for o, n in [ for o, n in [
('buy', 'entry'), ("buy", "entry"),
('sell', 'exit'), ("sell", "exit"),
('emergencysell', 'emergency_exit'), ("emergencysell", "emergency_exit"),
('forcesell', 'force_exit'), ("forcesell", "force_exit"),
('forcebuy', 'force_entry'), ("forcebuy", "force_entry"),
('emergencyexit', 'emergency_exit'), ("emergencyexit", "emergency_exit"),
('forceexit', 'force_exit'), ("forceexit", "force_exit"),
('forceentry', 'force_entry'), ("forceentry", "force_entry"),
]: ]:
process_deprecated_setting(conf, "order_types", o, "order_types", n)
process_deprecated_setting(conf, 'order_types', o, 'order_types', n)
def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None: def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None:
unfilledtimeout = conf.get('unfilledtimeout', {}) unfilledtimeout = conf.get("unfilledtimeout", {})
if any(x in unfilledtimeout for x in ['buy', 'sell']): if any(x in unfilledtimeout for x in ["buy", "sell"]):
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
raise OperationalException( raise ConfigurationError(
"Please migrate your unfilledtimeout settings to use the new wording.") "Please migrate your unfilledtimeout settings to use the new wording."
)
else: else:
logger.warning( logger.warning(
"DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is deprecated." "DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is deprecated."
"Please migrate your unfilledtimeout settings to use 'entry' and 'exit' wording." "Please migrate your unfilledtimeout settings to use 'entry' and 'exit' wording."
) )
for o, n in [ for o, n in [
('buy', 'entry'), ("buy", "entry"),
('sell', 'exit'), ("sell", "exit"),
]: ]:
process_deprecated_setting(conf, "unfilledtimeout", o, "unfilledtimeout", n)
process_deprecated_setting(conf, 'unfilledtimeout', o, 'unfilledtimeout', n)
def _validate_pricing_rules(conf: Dict[str, Any]) -> None: def _validate_pricing_rules(conf: Dict[str, Any]) -> None:
if conf.get("ask_strategy") or conf.get("bid_strategy"):
if conf.get('ask_strategy') or conf.get('bid_strategy'): if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: raise ConfigurationError("Please migrate your pricing settings to use the new wording.")
raise OperationalException(
"Please migrate your pricing settings to use the new wording.")
else: else:
logger.warning( logger.warning(
"DEPRECATED: Using 'ask_strategy' and 'bid_strategy' is deprecated." "DEPRECATED: Using 'ask_strategy' and 'bid_strategy' is deprecated."
"Please migrate your settings to use 'entry_pricing' and 'exit_pricing'." "Please migrate your settings to use 'entry_pricing' and 'exit_pricing'."
) )
conf['entry_pricing'] = {} conf["entry_pricing"] = {}
for obj in list(conf.get('bid_strategy', {}).keys()): for obj in list(conf.get("bid_strategy", {}).keys()):
if obj == 'ask_last_balance': if obj == "ask_last_balance":
process_deprecated_setting(conf, 'bid_strategy', obj, process_deprecated_setting(
'entry_pricing', 'price_last_balance') conf, "bid_strategy", obj, "entry_pricing", "price_last_balance"
)
else: else:
process_deprecated_setting(conf, 'bid_strategy', obj, 'entry_pricing', obj) process_deprecated_setting(conf, "bid_strategy", obj, "entry_pricing", obj)
del conf['bid_strategy'] del conf["bid_strategy"]
conf['exit_pricing'] = {} conf["exit_pricing"] = {}
for obj in list(conf.get('ask_strategy', {}).keys()): for obj in list(conf.get("ask_strategy", {}).keys()):
if obj == 'bid_last_balance': if obj == "bid_last_balance":
process_deprecated_setting(conf, 'ask_strategy', obj, process_deprecated_setting(
'exit_pricing', 'price_last_balance') conf, "ask_strategy", obj, "exit_pricing", "price_last_balance"
)
else: else:
process_deprecated_setting(conf, 'ask_strategy', obj, 'exit_pricing', obj) process_deprecated_setting(conf, "ask_strategy", obj, "exit_pricing", obj)
del conf['ask_strategy'] del conf["ask_strategy"]
def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None: def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None:
freqai_enabled = conf.get('freqai', {}).get('enabled', False) freqai_enabled = conf.get("freqai", {}).get("enabled", False)
analyze_per_epoch = conf.get('analyze_per_epoch', False) analyze_per_epoch = conf.get("analyze_per_epoch", False)
if analyze_per_epoch and freqai_enabled: if analyze_per_epoch and freqai_enabled:
raise OperationalException( raise ConfigurationError(
'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.') "Using analyze-per-epoch parameter is not supported with a FreqAI strategy."
)
def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool) -> None: def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool) -> None:
freqai_enabled = conf.get('freqai', {}).get('enabled', False) freqai_enabled = conf.get("freqai", {}).get("enabled", False)
if freqai_enabled: if freqai_enabled:
main_tf = conf.get('timeframe', '5m') main_tf = conf.get("timeframe", "5m")
freqai_include_timeframes = conf.get('freqai', {}).get('feature_parameters', {} freqai_include_timeframes = (
).get('include_timeframes', []) conf.get("freqai", {}).get("feature_parameters", {}).get("include_timeframes", [])
)
from freqtrade.exchange import timeframe_to_seconds from freqtrade.exchange import timeframe_to_seconds
main_tf_s = timeframe_to_seconds(main_tf) main_tf_s = timeframe_to_seconds(main_tf)
offending_lines = [] offending_lines = []
for tf in freqai_include_timeframes: for tf in freqai_include_timeframes:
@@ -350,59 +364,67 @@ def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool)
if tf_s < main_tf_s: if tf_s < main_tf_s:
offending_lines.append(tf) offending_lines.append(tf)
if offending_lines: if offending_lines:
raise OperationalException( raise ConfigurationError(
f"Main timeframe of {main_tf} must be smaller or equal to FreqAI " f"Main timeframe of {main_tf} must be smaller or equal to FreqAI "
f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}") f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}"
)
# Ensure that the base timeframe is included in the include_timeframes list # Ensure that the base timeframe is included in the include_timeframes list
if not preliminary and main_tf not in freqai_include_timeframes: if not preliminary and main_tf not in freqai_include_timeframes:
feature_parameters = conf.get('freqai', {}).get('feature_parameters', {}) feature_parameters = conf.get("freqai", {}).get("feature_parameters", {})
include_timeframes = [main_tf] + freqai_include_timeframes include_timeframes = [main_tf] + freqai_include_timeframes
conf.get('freqai', {}).get('feature_parameters', {}) \ conf.get("freqai", {}).get("feature_parameters", {}).update(
.update({**feature_parameters, 'include_timeframes': include_timeframes}) {**feature_parameters, "include_timeframes": include_timeframes}
)
def _validate_freqai_backtest(conf: Dict[str, Any]) -> None: def _validate_freqai_backtest(conf: Dict[str, Any]) -> None:
if conf.get('runmode', RunMode.OTHER) == RunMode.BACKTEST: if conf.get("runmode", RunMode.OTHER) == RunMode.BACKTEST:
freqai_enabled = conf.get('freqai', {}).get('enabled', False) freqai_enabled = conf.get("freqai", {}).get("enabled", False)
timerange = conf.get('timerange') timerange = conf.get("timerange")
freqai_backtest_live_models = conf.get('freqai_backtest_live_models', False) freqai_backtest_live_models = conf.get("freqai_backtest_live_models", False)
if freqai_backtest_live_models and freqai_enabled and timerange: if freqai_backtest_live_models and freqai_enabled and timerange:
raise OperationalException( raise ConfigurationError(
'Using timerange parameter is not supported with ' "Using timerange parameter is not supported with "
'--freqai-backtest-live-models parameter.') "--freqai-backtest-live-models parameter."
)
if freqai_backtest_live_models and not freqai_enabled: if freqai_backtest_live_models and not freqai_enabled:
raise OperationalException( raise ConfigurationError(
'Using --freqai-backtest-live-models parameter is only ' "Using --freqai-backtest-live-models parameter is only "
'supported with a FreqAI strategy.') "supported with a FreqAI strategy."
)
if freqai_enabled and not freqai_backtest_live_models and not timerange: if freqai_enabled and not freqai_backtest_live_models and not timerange:
raise OperationalException( raise ConfigurationError(
'Please pass --timerange if you intend to use FreqAI for backtesting.') "Please pass --timerange if you intend to use FreqAI for backtesting."
)
def _validate_consumers(conf: Dict[str, Any]) -> None: def _validate_consumers(conf: Dict[str, Any]) -> None:
emc_conf = conf.get('external_message_consumer', {}) emc_conf = conf.get("external_message_consumer", {})
if emc_conf.get('enabled', False): if emc_conf.get("enabled", False):
if len(emc_conf.get('producers', [])) < 1: if len(emc_conf.get("producers", [])) < 1:
raise OperationalException("You must specify at least 1 Producer to connect to.") raise ConfigurationError("You must specify at least 1 Producer to connect to.")
producer_names = [p['name'] for p in emc_conf.get('producers', [])] producer_names = [p["name"] for p in emc_conf.get("producers", [])]
duplicates = [item for item, count in Counter(producer_names).items() if count > 1] duplicates = [item for item, count in Counter(producer_names).items() if count > 1]
if duplicates: if duplicates:
raise OperationalException( raise ConfigurationError(
f"Producer names must be unique. Duplicate: {', '.join(duplicates)}") f"Producer names must be unique. Duplicate: {', '.join(duplicates)}"
if conf.get('process_only_new_candles', True): )
if conf.get("process_only_new_candles", True):
# Warning here or require it? # Warning here or require it?
logger.warning("To receive best performance with external data, " logger.warning(
"please set `process_only_new_candles` to False") "To receive best performance with external data, "
"please set `process_only_new_candles` to False"
)
def _strategy_settings(conf: Dict[str, Any]) -> None: def _strategy_settings(conf: Dict[str, Any]) -> None:
process_deprecated_setting(conf, None, "use_sell_signal", None, "use_exit_signal")
process_deprecated_setting(conf, None, 'use_sell_signal', None, 'use_exit_signal') process_deprecated_setting(conf, None, "sell_profit_only", None, "exit_profit_only")
process_deprecated_setting(conf, None, 'sell_profit_only', None, 'exit_profit_only') process_deprecated_setting(conf, None, "sell_profit_offset", None, "exit_profit_offset")
process_deprecated_setting(conf, None, 'sell_profit_offset', None, 'exit_profit_offset') process_deprecated_setting(
process_deprecated_setting(conf, None, 'ignore_roi_if_buy_signal', conf, None, "ignore_roi_if_buy_signal", None, "ignore_roi_if_entry_signal"
None, 'ignore_roi_if_entry_signal') )
+246 -202
View File
@@ -1,6 +1,7 @@
""" """
This module contains the configuration class This module contains the configuration class
""" """
import logging import logging
import warnings import warnings
from copy import deepcopy from copy import deepcopy
@@ -13,7 +14,7 @@ from freqtrade.configuration.directory_operations import create_datadir, create_
from freqtrade.configuration.environment_vars import enironment_vars_to_dict from freqtrade.configuration.environment_vars import enironment_vars_to_dict
from freqtrade.configuration.load_config import load_file, load_from_files from freqtrade.configuration.load_config import load_file, load_from_files
from freqtrade.constants import Config from freqtrade.constants import Config
from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, CandleType, RunMode, TradingMode from freqtrade.enums import NON_UTIL_MODES, TRADE_MODES, CandleType, RunMode, TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.loggers import setup_logging from freqtrade.loggers import setup_logging
from freqtrade.misc import deep_merge_dicts, parse_db_uri_for_logging from freqtrade.misc import deep_merge_dicts, parse_db_uri_for_logging
@@ -56,7 +57,7 @@ class Configuration:
:return: configuration dictionary :return: configuration dictionary
""" """
# Keep this method as staticmethod, so it can be used from interactive environments # Keep this method as staticmethod, so it can be used from interactive environments
c = Configuration({'config': files}, RunMode.OTHER) c = Configuration({"config": files}, RunMode.OTHER)
return c.get_config() return c.get_config()
def load_config(self) -> Dict[str, Any]: def load_config(self) -> Dict[str, Any]:
@@ -69,19 +70,20 @@ class Configuration:
# Load environment variables # Load environment variables
from freqtrade.commands.arguments import NO_CONF_ALLOWED from freqtrade.commands.arguments import NO_CONF_ALLOWED
if self.args.get('command') not in NO_CONF_ALLOWED:
if self.args.get("command") not in NO_CONF_ALLOWED:
env_data = enironment_vars_to_dict() env_data = enironment_vars_to_dict()
config = deep_merge_dicts(env_data, config) config = deep_merge_dicts(env_data, config)
# Normalize config # Normalize config
if 'internals' not in config: if "internals" not in config:
config['internals'] = {} config["internals"] = {}
if 'pairlists' not in config: if "pairlists" not in config:
config['pairlists'] = [] config["pairlists"] = []
# Keep a copy of the original configuration file # Keep a copy of the original configuration file
config['original_config'] = deepcopy(config) config["original_config"] = deepcopy(config)
self._process_logging_options(config) self._process_logging_options(config)
@@ -105,7 +107,7 @@ class Configuration:
from freqtrade.exchange.check_exchange import check_exchange from freqtrade.exchange.check_exchange import check_exchange
# Check if the exchange set by the user is supported # Check if the exchange set by the user is supported
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) check_exchange(config, config.get("experimental", {}).get("block_bad_exchanges", True))
self._resolve_pairs_list(config) self._resolve_pairs_list(config)
@@ -119,52 +121,56 @@ class Configuration:
the -v/--verbose, --logfile options the -v/--verbose, --logfile options
""" """
# Log level # Log level
config.update({'verbosity': self.args.get('verbosity', 0)}) config.update({"verbosity": self.args.get("verbosity", 0)})
if 'logfile' in self.args and self.args['logfile']: if "logfile" in self.args and self.args["logfile"]:
config.update({'logfile': self.args['logfile']}) config.update({"logfile": self.args["logfile"]})
setup_logging(config) setup_logging(config)
def _process_trading_options(self, config: Config) -> None: def _process_trading_options(self, config: Config) -> None:
if config['runmode'] not in TRADING_MODES: if config["runmode"] not in TRADE_MODES:
return return
if config.get('dry_run', False): if config.get("dry_run", False):
logger.info('Dry run is enabled') logger.info("Dry run is enabled")
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]: if config.get("db_url") in [None, constants.DEFAULT_DB_PROD_URL]:
# Default to in-memory db for dry_run if not specified # Default to in-memory db for dry_run if not specified
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL config["db_url"] = constants.DEFAULT_DB_DRYRUN_URL
else: else:
if not config.get('db_url'): if not config.get("db_url"):
config['db_url'] = constants.DEFAULT_DB_PROD_URL config["db_url"] = constants.DEFAULT_DB_PROD_URL
logger.info('Dry run is disabled') logger.info("Dry run is disabled")
logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"') logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
def _process_common_options(self, config: Config) -> None: def _process_common_options(self, config: Config) -> None:
# Set strategy if not specified in config and or if it's non default # Set strategy if not specified in config and or if it's non default
if self.args.get('strategy') or not config.get('strategy'): if self.args.get("strategy") or not config.get("strategy"):
config.update({'strategy': self.args.get('strategy')}) config.update({"strategy": self.args.get("strategy")})
self._args_to_config(config, argname='strategy_path', self._args_to_config(
logstring='Using additional Strategy lookup path: {}') config, argname="strategy_path", logstring="Using additional Strategy lookup path: {}"
)
if ('db_url' in self.args and self.args['db_url'] and if (
self.args['db_url'] != constants.DEFAULT_DB_PROD_URL): "db_url" in self.args
config.update({'db_url': self.args['db_url']}) and self.args["db_url"]
logger.info('Parameter --db-url detected ...') and self.args["db_url"] != constants.DEFAULT_DB_PROD_URL
):
config.update({"db_url": self.args["db_url"]})
logger.info("Parameter --db-url detected ...")
self._args_to_config(config, argname='db_url_from', self._args_to_config(
logstring='Parameter --db-url-from detected ...') config, argname="db_url_from", logstring="Parameter --db-url-from detected ..."
)
if config.get('force_entry_enable', False): if config.get("force_entry_enable", False):
logger.warning('`force_entry_enable` RPC message enabled.') logger.warning("`force_entry_enable` RPC message enabled.")
# Support for sd_notify # Support for sd_notify
if 'sd_notify' in self.args and self.args['sd_notify']: if "sd_notify" in self.args and self.args["sd_notify"]:
config['internals'].update({'sd_notify': True}) config["internals"].update({"sd_notify": True})
def _process_datadir_options(self, config: Config) -> None: def _process_datadir_options(self, config: Config) -> None:
""" """
@@ -172,239 +178,275 @@ class Configuration:
--user-data, --datadir --user-data, --datadir
""" """
# Check exchange parameter here - otherwise `datadir` might be wrong. # Check exchange parameter here - otherwise `datadir` might be wrong.
if 'exchange' in self.args and self.args['exchange']: if "exchange" in self.args and self.args["exchange"]:
config['exchange']['name'] = self.args['exchange'] config["exchange"]["name"] = self.args["exchange"]
logger.info(f"Using exchange {config['exchange']['name']}") logger.info(f"Using exchange {config['exchange']['name']}")
if 'pair_whitelist' not in config['exchange']: if "pair_whitelist" not in config["exchange"]:
config['exchange']['pair_whitelist'] = [] config["exchange"]["pair_whitelist"] = []
if 'user_data_dir' in self.args and self.args['user_data_dir']: if "user_data_dir" in self.args and self.args["user_data_dir"]:
config.update({'user_data_dir': self.args['user_data_dir']}) config.update({"user_data_dir": self.args["user_data_dir"]})
elif 'user_data_dir' not in config: elif "user_data_dir" not in config:
# Default to cwd/user_data (legacy option ...) # Default to cwd/user_data (legacy option ...)
config.update({'user_data_dir': str(Path.cwd() / 'user_data')}) config.update({"user_data_dir": str(Path.cwd() / "user_data")})
# reset to user_data_dir so this contains the absolute path. # reset to user_data_dir so this contains the absolute path.
config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False) config["user_data_dir"] = create_userdata_dir(config["user_data_dir"], create_dir=False)
logger.info('Using user-data directory: %s ...', config['user_data_dir']) logger.info("Using user-data directory: %s ...", config["user_data_dir"])
config.update({'datadir': create_datadir(config, self.args.get('datadir'))}) config.update({"datadir": create_datadir(config, self.args.get("datadir"))})
logger.info('Using data directory: %s ...', config.get('datadir')) logger.info("Using data directory: %s ...", config.get("datadir"))
if self.args.get('exportfilename'): if self.args.get("exportfilename"):
self._args_to_config(config, argname='exportfilename', self._args_to_config(
logstring='Storing backtest results to {} ...') config, argname="exportfilename", logstring="Storing backtest results to {} ..."
config['exportfilename'] = Path(config['exportfilename']) )
config["exportfilename"] = Path(config["exportfilename"])
else: else:
config['exportfilename'] = (config['user_data_dir'] config["exportfilename"] = config["user_data_dir"] / "backtest_results"
/ 'backtest_results')
if self.args.get("show_sensitive"):
logger.warning(
"Sensitive information will be shown in the upcoming output. "
"Please make sure to never share this output without redacting "
"the information yourself."
)
def _process_optimize_options(self, config: Config) -> None: def _process_optimize_options(self, config: Config) -> None:
# This will override the strategy configuration # This will override the strategy configuration
self._args_to_config(config, argname='timeframe', self._args_to_config(
logstring='Parameter -i/--timeframe detected ... ' config,
'Using timeframe: {} ...') argname="timeframe",
logstring="Parameter -i/--timeframe detected ... Using timeframe: {} ...",
self._args_to_config(config, argname='position_stacking', )
logstring='Parameter --enable-position-stacking detected ...')
self._args_to_config( self._args_to_config(
config, argname='enable_protections', config,
logstring='Parameter --enable-protections detected, enabling Protections. ...') argname="position_stacking",
logstring="Parameter --enable-position-stacking detected ...",
)
if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]: self._args_to_config(
config.update({'use_max_market_positions': False}) config,
logger.info('Parameter --disable-max-market-positions detected ...') argname="enable_protections",
logger.info('max_open_trades set to unlimited ...') logstring="Parameter --enable-protections detected, enabling Protections. ...",
elif 'max_open_trades' in self.args and self.args['max_open_trades']: )
config.update({'max_open_trades': self.args['max_open_trades']})
logger.info('Parameter --max-open-trades detected, ' if "use_max_market_positions" in self.args and not self.args["use_max_market_positions"]:
'overriding max_open_trades to: %s ...', config.get('max_open_trades')) config.update({"use_max_market_positions": False})
elif config['runmode'] in NON_UTIL_MODES: logger.info("Parameter --disable-max-market-positions detected ...")
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) logger.info("max_open_trades set to unlimited ...")
elif "max_open_trades" in self.args and self.args["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 ...",
config.get("max_open_trades"),
)
elif config["runmode"] in NON_UTIL_MODES:
logger.info("Using max_open_trades: %s ...", config.get("max_open_trades"))
# Setting max_open_trades to infinite if -1 # Setting max_open_trades to infinite if -1
if config.get('max_open_trades') == -1: if config.get("max_open_trades") == -1:
config['max_open_trades'] = float('inf') config["max_open_trades"] = float("inf")
if self.args.get('stake_amount'): if self.args.get("stake_amount"):
# Convert explicitly to float to support CLI argument for both unlimited and value # Convert explicitly to float to support CLI argument for both unlimited and value
try: try:
self.args['stake_amount'] = float(self.args['stake_amount']) self.args["stake_amount"] = float(self.args["stake_amount"])
except ValueError: except ValueError:
pass pass
configurations = [ configurations = [
('timeframe_detail', (
'Parameter --timeframe-detail detected, using {} for intra-candle backtesting ...'), "timeframe_detail",
('backtest_show_pair_list', 'Parameter --show-pair-list detected.'), "Parameter --timeframe-detail detected, using {} for intra-candle backtesting ...",
('stake_amount', ),
'Parameter --stake-amount detected, overriding stake_amount to: {} ...'), ("backtest_show_pair_list", "Parameter --show-pair-list detected."),
('dry_run_wallet', (
'Parameter --dry-run-wallet detected, overriding dry_run_wallet to: {} ...'), "stake_amount",
('fee', 'Parameter --fee detected, setting fee to: {} ...'), "Parameter --stake-amount detected, overriding stake_amount to: {} ...",
('timerange', 'Parameter --timerange detected: {} ...'), ),
(
"dry_run_wallet",
"Parameter --dry-run-wallet detected, overriding dry_run_wallet to: {} ...",
),
("fee", "Parameter --fee detected, setting fee to: {} ..."),
("timerange", "Parameter --timerange detected: {} ..."),
] ]
self._args_to_config_loop(config, configurations) self._args_to_config_loop(config, configurations)
self._process_datadir_options(config) self._process_datadir_options(config)
self._args_to_config(config, argname='strategy_list', self._args_to_config(
logstring='Using strategy list of {} strategies', logfun=len) config,
argname="strategy_list",
logstring="Using strategy list of {} strategies",
logfun=len,
)
configurations = [ configurations = [
('recursive_strategy_search', (
'Recursively searching for a strategy in the strategies folder.'), "recursive_strategy_search",
('timeframe', 'Overriding timeframe with Command line argument'), "Recursively searching for a strategy in the strategies folder.",
('export', 'Parameter --export detected: {} ...'), ),
('backtest_breakdown', 'Parameter --breakdown detected ...'), ("timeframe", "Overriding timeframe with Command line argument"),
('backtest_cache', 'Parameter --cache={} detected ...'), ("export", "Parameter --export detected: {} ..."),
('disableparamexport', 'Parameter --disableparamexport detected: {} ...'), ("backtest_breakdown", "Parameter --breakdown detected ..."),
('freqai_backtest_live_models', ("backtest_cache", "Parameter --cache={} detected ..."),
'Parameter --freqai-backtest-live-models detected ...'), ("disableparamexport", "Parameter --disableparamexport detected: {} ..."),
("freqai_backtest_live_models", "Parameter --freqai-backtest-live-models detected ..."),
] ]
self._args_to_config_loop(config, configurations) self._args_to_config_loop(config, configurations)
# Edge section: # Edge section:
if 'stoploss_range' in self.args and self.args["stoploss_range"]: if "stoploss_range" in self.args and self.args["stoploss_range"]:
txt_range = eval(self.args["stoploss_range"]) txt_range = eval(self.args["stoploss_range"])
config['edge'].update({'stoploss_range_min': txt_range[0]}) config["edge"].update({"stoploss_range_min": txt_range[0]})
config['edge'].update({'stoploss_range_max': txt_range[1]}) config["edge"].update({"stoploss_range_max": txt_range[1]})
config['edge'].update({'stoploss_range_step': txt_range[2]}) config["edge"].update({"stoploss_range_step": txt_range[2]})
logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"]) logger.info("Parameter --stoplosses detected: %s ...", self.args["stoploss_range"])
# Hyperopt section # Hyperopt section
configurations = [ configurations = [
('hyperopt', 'Using Hyperopt class name: {}'), ("hyperopt", "Using Hyperopt class name: {}"),
('hyperopt_path', 'Using additional Hyperopt lookup path: {}'), ("hyperopt_path", "Using additional Hyperopt lookup path: {}"),
('hyperoptexportfilename', 'Using hyperopt file: {}'), ("hyperoptexportfilename", "Using hyperopt file: {}"),
('lookahead_analysis_exportfilename', 'Saving lookahead analysis results into {} ...'), ("lookahead_analysis_exportfilename", "Saving lookahead analysis results into {} ..."),
('epochs', 'Parameter --epochs detected ... Will run Hyperopt with for {} epochs ...'), ("epochs", "Parameter --epochs detected ... Will run Hyperopt with for {} epochs ..."),
('spaces', 'Parameter -s/--spaces detected: {}'), ("spaces", "Parameter -s/--spaces detected: {}"),
('analyze_per_epoch', 'Parameter --analyze-per-epoch detected.'), ("analyze_per_epoch", "Parameter --analyze-per-epoch detected."),
('print_all', 'Parameter --print-all detected ...'), ("print_all", "Parameter --print-all detected ..."),
] ]
self._args_to_config_loop(config, configurations) self._args_to_config_loop(config, configurations)
if 'print_colorized' in self.args and not self.args["print_colorized"]: if "print_colorized" in self.args and not self.args["print_colorized"]:
logger.info('Parameter --no-color detected ...') logger.info("Parameter --no-color detected ...")
config.update({'print_colorized': False}) config.update({"print_colorized": False})
else: else:
config.update({'print_colorized': True}) config.update({"print_colorized": True})
configurations = [ configurations = [
('print_json', 'Parameter --print-json detected ...'), ("print_json", "Parameter --print-json detected ..."),
('export_csv', 'Parameter --export-csv detected: {}'), ("export_csv", "Parameter --export-csv detected: {}"),
('hyperopt_jobs', 'Parameter -j/--job-workers detected: {}'), ("hyperopt_jobs", "Parameter -j/--job-workers detected: {}"),
('hyperopt_random_state', 'Parameter --random-state detected: {}'), ("hyperopt_random_state", "Parameter --random-state detected: {}"),
('hyperopt_min_trades', 'Parameter --min-trades detected: {}'), ("hyperopt_min_trades", "Parameter --min-trades detected: {}"),
('hyperopt_loss', 'Using Hyperopt loss class name: {}'), ("hyperopt_loss", "Using Hyperopt loss class name: {}"),
('hyperopt_show_index', 'Parameter -n/--index detected: {}'), ("hyperopt_show_index", "Parameter -n/--index detected: {}"),
('hyperopt_list_best', 'Parameter --best detected: {}'), ("hyperopt_list_best", "Parameter --best detected: {}"),
('hyperopt_list_profitable', 'Parameter --profitable detected: {}'), ("hyperopt_list_profitable", "Parameter --profitable detected: {}"),
('hyperopt_list_min_trades', 'Parameter --min-trades detected: {}'), ("hyperopt_list_min_trades", "Parameter --min-trades detected: {}"),
('hyperopt_list_max_trades', 'Parameter --max-trades detected: {}'), ("hyperopt_list_max_trades", "Parameter --max-trades detected: {}"),
('hyperopt_list_min_avg_time', 'Parameter --min-avg-time detected: {}'), ("hyperopt_list_min_avg_time", "Parameter --min-avg-time detected: {}"),
('hyperopt_list_max_avg_time', 'Parameter --max-avg-time detected: {}'), ("hyperopt_list_max_avg_time", "Parameter --max-avg-time detected: {}"),
('hyperopt_list_min_avg_profit', 'Parameter --min-avg-profit detected: {}'), ("hyperopt_list_min_avg_profit", "Parameter --min-avg-profit detected: {}"),
('hyperopt_list_max_avg_profit', 'Parameter --max-avg-profit detected: {}'), ("hyperopt_list_max_avg_profit", "Parameter --max-avg-profit detected: {}"),
('hyperopt_list_min_total_profit', 'Parameter --min-total-profit detected: {}'), ("hyperopt_list_min_total_profit", "Parameter --min-total-profit detected: {}"),
('hyperopt_list_max_total_profit', 'Parameter --max-total-profit detected: {}'), ("hyperopt_list_max_total_profit", "Parameter --max-total-profit detected: {}"),
('hyperopt_list_min_objective', 'Parameter --min-objective detected: {}'), ("hyperopt_list_min_objective", "Parameter --min-objective detected: {}"),
('hyperopt_list_max_objective', 'Parameter --max-objective detected: {}'), ("hyperopt_list_max_objective", "Parameter --max-objective detected: {}"),
('hyperopt_list_no_details', 'Parameter --no-details detected: {}'), ("hyperopt_list_no_details", "Parameter --no-details detected: {}"),
('hyperopt_show_no_header', 'Parameter --no-header detected: {}'), ("hyperopt_show_no_header", "Parameter --no-header detected: {}"),
('hyperopt_ignore_missing_space', 'Paramter --ignore-missing-space detected: {}'), ("hyperopt_ignore_missing_space", "Parameter --ignore-missing-space detected: {}"),
] ]
self._args_to_config_loop(config, configurations) self._args_to_config_loop(config, configurations)
def _process_plot_options(self, config: Config) -> None: def _process_plot_options(self, config: Config) -> None:
configurations = [ configurations = [
('pairs', 'Using pairs {}'), ("pairs", "Using pairs {}"),
('indicators1', 'Using indicators1: {}'), ("indicators1", "Using indicators1: {}"),
('indicators2', 'Using indicators2: {}'), ("indicators2", "Using indicators2: {}"),
('trade_ids', 'Filtering on trade_ids: {}'), ("trade_ids", "Filtering on trade_ids: {}"),
('plot_limit', 'Limiting plot to: {}'), ("plot_limit", "Limiting plot to: {}"),
('plot_auto_open', 'Parameter --auto-open detected.'), ("plot_auto_open", "Parameter --auto-open detected."),
('trade_source', 'Using trades from: {}'), ("trade_source", "Using trades from: {}"),
('prepend_data', 'Prepend detected. Allowing data prepending.'), ("prepend_data", "Prepend detected. Allowing data prepending."),
('erase', 'Erase detected. Deleting existing data.'), ("erase", "Erase detected. Deleting existing data."),
('no_trades', 'Parameter --no-trades detected.'), ("no_trades", "Parameter --no-trades detected."),
('timeframes', 'timeframes --timeframes: {}'), ("timeframes", "timeframes --timeframes: {}"),
('days', 'Detected --days: {}'), ("days", "Detected --days: {}"),
('include_inactive', 'Detected --include-inactive-pairs: {}'), ("include_inactive", "Detected --include-inactive-pairs: {}"),
('download_trades', 'Detected --dl-trades: {}'), ("download_trades", "Detected --dl-trades: {}"),
('dataformat_ohlcv', 'Using "{}" to store OHLCV data.'), ("convert_trades", "Detected --convert: {} - Converting Trade data to OHCV {}"),
('dataformat_trades', 'Using "{}" to store trades data.'), ("dataformat_ohlcv", 'Using "{}" to store OHLCV data.'),
('show_timerange', 'Detected --show-timerange'), ("dataformat_trades", 'Using "{}" to store trades data.'),
("show_timerange", "Detected --show-timerange"),
] ]
self._args_to_config_loop(config, configurations) self._args_to_config_loop(config, configurations)
def _process_data_options(self, config: Config) -> None: def _process_data_options(self, config: Config) -> None:
self._args_to_config(config, argname='new_pairs_days', self._args_to_config(
logstring='Detected --new-pairs-days: {}') config, argname="new_pairs_days", logstring="Detected --new-pairs-days: {}"
self._args_to_config(config, argname='trading_mode', )
logstring='Detected --trading-mode: {}') self._args_to_config(
config['candle_type_def'] = CandleType.get_default( config, argname="trading_mode", logstring="Detected --trading-mode: {}"
config.get('trading_mode', 'spot') or 'spot') )
config['trading_mode'] = TradingMode(config.get('trading_mode', 'spot') or 'spot') config["candle_type_def"] = CandleType.get_default(
self._args_to_config(config, argname='candle_types', config.get("trading_mode", "spot") or "spot"
logstring='Detected --candle-types: {}') )
config["trading_mode"] = TradingMode(config.get("trading_mode", "spot") or "spot")
self._args_to_config(
config, argname="candle_types", logstring="Detected --candle-types: {}"
)
def _process_analyze_options(self, config: Config) -> None: def _process_analyze_options(self, config: Config) -> None:
configurations = [ configurations = [
('analysis_groups', 'Analysis reason groups: {}'), ("analysis_groups", "Analysis reason groups: {}"),
('enter_reason_list', 'Analysis enter tag list: {}'), ("enter_reason_list", "Analysis enter tag list: {}"),
('exit_reason_list', 'Analysis exit tag list: {}'), ("exit_reason_list", "Analysis exit tag list: {}"),
('indicator_list', 'Analysis indicator list: {}'), ("indicator_list", "Analysis indicator list: {}"),
('timerange', 'Filter trades by timerange: {}'), ("timerange", "Filter trades by timerange: {}"),
('analysis_rejected', 'Analyse rejected signals: {}'), ("analysis_rejected", "Analyse rejected signals: {}"),
('analysis_to_csv', 'Store analysis tables to CSV: {}'), ("analysis_to_csv", "Store analysis tables to CSV: {}"),
('analysis_csv_path', 'Path to store analysis CSVs: {}'), ("analysis_csv_path", "Path to store analysis CSVs: {}"),
# Lookahead analysis results # Lookahead analysis results
('targeted_trade_amount', 'Targeted Trade amount: {}'), ("targeted_trade_amount", "Targeted Trade amount: {}"),
('minimum_trade_amount', 'Minimum Trade amount: {}'), ("minimum_trade_amount", "Minimum Trade amount: {}"),
('lookahead_analysis_exportfilename', 'Path to store lookahead-analysis-results: {}'), ("lookahead_analysis_exportfilename", "Path to store lookahead-analysis-results: {}"),
('startup_candle', 'Startup candle to be used on recursive analysis: {}'), ("startup_candle", "Startup candle to be used on recursive analysis: {}"),
] ]
self._args_to_config_loop(config, configurations) self._args_to_config_loop(config, configurations)
def _args_to_config_loop(self, config, configurations: List[Tuple[str, str]]) -> None: def _args_to_config_loop(self, config, configurations: List[Tuple[str, str]]) -> None:
for argname, logstring in configurations: for argname, logstring in configurations:
self._args_to_config(config, argname=argname, logstring=logstring) self._args_to_config(config, argname=argname, logstring=logstring)
def _process_runmode(self, config: Config) -> None: def _process_runmode(self, config: Config) -> None:
self._args_to_config(
self._args_to_config(config, argname='dry_run', config,
logstring='Parameter --dry-run detected, ' argname="dry_run",
'overriding dry_run to: {} ...') logstring="Parameter --dry-run detected, overriding dry_run to: {} ...",
)
if not self.runmode: if not self.runmode:
# Handle real mode, infer dry/live from config # Handle real mode, infer dry/live from config
self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE self.runmode = RunMode.DRY_RUN if config.get("dry_run", True) else RunMode.LIVE
logger.info(f"Runmode set to {self.runmode.value}.") logger.info(f"Runmode set to {self.runmode.value}.")
config.update({'runmode': self.runmode}) config.update({"runmode": self.runmode})
def _process_freqai_options(self, config: Config) -> None: def _process_freqai_options(self, config: Config) -> None:
self._args_to_config(
config, argname="freqaimodel", logstring="Using freqaimodel class name: {}"
)
self._args_to_config(config, argname='freqaimodel', self._args_to_config(
logstring='Using freqaimodel class name: {}') config, argname="freqaimodel_path", logstring="Using freqaimodel path: {}"
)
self._args_to_config(config, argname='freqaimodel_path',
logstring='Using freqaimodel path: {}')
return return
def _args_to_config(self, config: Config, argname: str, def _args_to_config(
logstring: str, logfun: Optional[Callable] = None, self,
deprecated_msg: Optional[str] = None) -> None: config: Config,
argname: str,
logstring: str,
logfun: Optional[Callable] = None,
deprecated_msg: Optional[str] = None,
) -> None:
""" """
:param config: Configuration dictionary :param config: Configuration dictionary
:param argname: Argumentname in self.args - will be copied to config dict. :param argname: Argumentname in self.args - will be copied to config dict.
@@ -414,9 +456,11 @@ class Configuration:
sample: logfun=len (prints the length of the found sample: logfun=len (prints the length of the found
configuration instead of the content) configuration instead of the content)
""" """
if (argname in self.args and self.args[argname] is not None if (
and self.args[argname] is not False): argname in self.args
and self.args[argname] is not None
and self.args[argname] is not False
):
config.update({argname: self.args[argname]}) config.update({argname: self.args[argname]})
if logfun: if logfun:
logger.info(logstring.format(logfun(config[argname]))) logger.info(logstring.format(logfun(config[argname])))
@@ -435,7 +479,7 @@ class Configuration:
""" """
if "pairs" in config: if "pairs" in config:
config['exchange']['pair_whitelist'] = config['pairs'] config["exchange"]["pair_whitelist"] = config["pairs"]
return return
if "pairs_file" in self.args and self.args["pairs_file"]: if "pairs_file" in self.args and self.args["pairs_file"]:
@@ -445,19 +489,19 @@ class Configuration:
# or if pairs file is specified explicitly # or if pairs file is specified explicitly
if not pairs_file.exists(): if not pairs_file.exists():
raise OperationalException(f'No pairs file found with path "{pairs_file}".') raise OperationalException(f'No pairs file found with path "{pairs_file}".')
config['pairs'] = load_file(pairs_file) config["pairs"] = load_file(pairs_file)
if isinstance(config['pairs'], list): if isinstance(config["pairs"], list):
config['pairs'].sort() config["pairs"].sort()
return return
if 'config' in self.args and self.args['config']: if "config" in self.args and self.args["config"]:
logger.info("Using pairlist from configuration.") logger.info("Using pairlist from configuration.")
config['pairs'] = config.get('exchange', {}).get('pair_whitelist') config["pairs"] = config.get("exchange", {}).get("pair_whitelist")
else: else:
# Fall back to /dl_path/pairs.json # Fall back to /dl_path/pairs.json
pairs_file = config['datadir'] / 'pairs.json' pairs_file = config["datadir"] / "pairs.json"
if pairs_file.exists(): if pairs_file.exists():
logger.info(f'Reading pairs file "{pairs_file}".') logger.info(f'Reading pairs file "{pairs_file}".')
config['pairs'] = load_file(pairs_file) config["pairs"] = load_file(pairs_file)
if 'pairs' in config and isinstance(config['pairs'], list): if "pairs" in config and isinstance(config["pairs"], list):
config['pairs'].sort() config["pairs"].sort()
+96 -56
View File
@@ -6,15 +6,19 @@ import logging
from typing import Optional from typing import Optional
from freqtrade.constants import Config from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import ConfigurationError, OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def check_conflicting_settings(config: Config, def check_conflicting_settings(
section_old: Optional[str], name_old: str, config: Config,
section_new: Optional[str], name_new: str) -> None: section_old: Optional[str],
name_old: str,
section_new: Optional[str],
name_new: str,
) -> None:
section_new_config = config.get(section_new, {}) if section_new else config section_new_config = config.get(section_new, {}) if section_new else config
section_old_config = config.get(section_old, {}) if section_old else config section_old_config = config.get(section_old, {}) if section_old else config
if name_new in section_new_config and name_old in section_old_config: if name_new in section_new_config and name_old in section_old_config:
@@ -29,9 +33,9 @@ def check_conflicting_settings(config: Config,
) )
def process_removed_setting(config: Config, def process_removed_setting(
section1: str, name1: str, config: Config, section1: str, name1: str, section2: Optional[str], name2: str
section2: Optional[str], name2: str) -> None: ) -> None:
""" """
:param section1: Removed section :param section1: Removed section
:param name1: Removed setting name :param name1: Removed setting name
@@ -41,16 +45,19 @@ def process_removed_setting(config: Config,
section1_config = config.get(section1, {}) section1_config = config.get(section1, {})
if name1 in section1_config: if name1 in section1_config:
section_2 = f"{section2}.{name2}" if section2 else f"{name2}" section_2 = f"{section2}.{name2}" if section2 else f"{name2}"
raise OperationalException( raise ConfigurationError(
f"Setting `{section1}.{name1}` has been moved to `{section_2}. " f"Setting `{section1}.{name1}` has been moved to `{section_2}. "
f"Please delete it from your configuration and use the `{section_2}` " f"Please delete it from your configuration and use the `{section_2}` "
"setting instead." "setting instead."
) )
def process_deprecated_setting(config: Config, def process_deprecated_setting(
section_old: Optional[str], name_old: str, config: Config,
section_new: Optional[str], name_new: str section_old: Optional[str],
name_old: str,
section_new: Optional[str],
name_new: str,
) -> None: ) -> None:
check_conflicting_settings(config, section_old, name_old, section_new, name_new) check_conflicting_settings(config, section_old, name_old, section_new, name_new)
section_old_config = config.get(section_old, {}) if section_old else config section_old_config = config.get(section_old, {}) if section_old else config
@@ -71,70 +78,103 @@ def process_deprecated_setting(config: Config,
def process_temporary_deprecated_settings(config: Config) -> None: def process_temporary_deprecated_settings(config: Config) -> None:
# Kept for future deprecated / moved settings # Kept for future deprecated / moved settings
# check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal', # check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal',
# 'experimental', 'use_sell_signal') # 'experimental', 'use_sell_signal')
process_deprecated_setting(config, 'ask_strategy', 'ignore_buying_expired_candle_after', process_deprecated_setting(
None, 'ignore_buying_expired_candle_after') config,
"ask_strategy",
"ignore_buying_expired_candle_after",
None,
"ignore_buying_expired_candle_after",
)
process_deprecated_setting(config, None, 'forcebuy_enable', None, 'force_entry_enable') process_deprecated_setting(config, None, "forcebuy_enable", None, "force_entry_enable")
# New settings # New settings
if config.get('telegram'): if config.get("telegram"):
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell', process_deprecated_setting(
'notification_settings', 'exit') config["telegram"], "notification_settings", "sell", "notification_settings", "exit"
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell_fill', )
'notification_settings', 'exit_fill') process_deprecated_setting(
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell_cancel', config["telegram"],
'notification_settings', 'exit_cancel') "notification_settings",
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy', "sell_fill",
'notification_settings', 'entry') "notification_settings",
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy_fill', "exit_fill",
'notification_settings', 'entry_fill') )
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy_cancel', process_deprecated_setting(
'notification_settings', 'entry_cancel') config["telegram"],
if config.get('webhook'): "notification_settings",
process_deprecated_setting(config, 'webhook', 'webhookbuy', 'webhook', 'webhookentry') "sell_cancel",
process_deprecated_setting(config, 'webhook', 'webhookbuycancel', "notification_settings",
'webhook', 'webhookentrycancel') "exit_cancel",
process_deprecated_setting(config, 'webhook', 'webhookbuyfill', )
'webhook', 'webhookentryfill') process_deprecated_setting(
process_deprecated_setting(config, 'webhook', 'webhooksell', 'webhook', 'webhookexit') config["telegram"], "notification_settings", "buy", "notification_settings", "entry"
process_deprecated_setting(config, 'webhook', 'webhooksellcancel', )
'webhook', 'webhookexitcancel') process_deprecated_setting(
process_deprecated_setting(config, 'webhook', 'webhooksellfill', config["telegram"],
'webhook', 'webhookexitfill') "notification_settings",
"buy_fill",
"notification_settings",
"entry_fill",
)
process_deprecated_setting(
config["telegram"],
"notification_settings",
"buy_cancel",
"notification_settings",
"entry_cancel",
)
if config.get("webhook"):
process_deprecated_setting(config, "webhook", "webhookbuy", "webhook", "webhookentry")
process_deprecated_setting(
config, "webhook", "webhookbuycancel", "webhook", "webhookentrycancel"
)
process_deprecated_setting(
config, "webhook", "webhookbuyfill", "webhook", "webhookentryfill"
)
process_deprecated_setting(config, "webhook", "webhooksell", "webhook", "webhookexit")
process_deprecated_setting(
config, "webhook", "webhooksellcancel", "webhook", "webhookexitcancel"
)
process_deprecated_setting(
config, "webhook", "webhooksellfill", "webhook", "webhookexitfill"
)
# Legacy way - having them in experimental ... # Legacy way - having them in experimental ...
process_removed_setting(config, 'experimental', 'use_sell_signal', None, 'use_exit_signal') process_removed_setting(config, "experimental", "use_sell_signal", None, "use_exit_signal")
process_removed_setting(config, 'experimental', 'sell_profit_only', None, 'exit_profit_only') process_removed_setting(config, "experimental", "sell_profit_only", None, "exit_profit_only")
process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal', process_removed_setting(
None, 'ignore_roi_if_entry_signal') config, "experimental", "ignore_roi_if_buy_signal", None, "ignore_roi_if_entry_signal"
)
process_removed_setting(config, 'ask_strategy', 'use_sell_signal', None, 'use_exit_signal') process_removed_setting(config, "ask_strategy", "use_sell_signal", None, "use_exit_signal")
process_removed_setting(config, 'ask_strategy', 'sell_profit_only', None, 'exit_profit_only') process_removed_setting(config, "ask_strategy", "sell_profit_only", None, "exit_profit_only")
process_removed_setting(config, 'ask_strategy', 'sell_profit_offset', process_removed_setting(
None, 'exit_profit_offset') config, "ask_strategy", "sell_profit_offset", None, "exit_profit_offset"
process_removed_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal', )
None, 'ignore_roi_if_entry_signal') process_removed_setting(
if (config.get('edge', {}).get('enabled', False) config, "ask_strategy", "ignore_roi_if_buy_signal", None, "ignore_roi_if_entry_signal"
and 'capital_available_percentage' in config.get('edge', {})): )
raise OperationalException( if config.get("edge", {}).get(
"enabled", False
) and "capital_available_percentage" in config.get("edge", {}):
raise ConfigurationError(
"DEPRECATED: " "DEPRECATED: "
"Using 'edge.capital_available_percentage' has been deprecated in favor of " "Using 'edge.capital_available_percentage' has been deprecated in favor of "
"'tradable_balance_ratio'. Please migrate your configuration to " "'tradable_balance_ratio'. Please migrate your configuration to "
"'tradable_balance_ratio' and remove 'capital_available_percentage' " "'tradable_balance_ratio' and remove 'capital_available_percentage' "
"from the edge configuration." "from the edge configuration."
) )
if 'ticker_interval' in config: if "ticker_interval" in config:
raise ConfigurationError(
raise OperationalException(
"DEPRECATED: 'ticker_interval' detected. " "DEPRECATED: 'ticker_interval' detected. "
"Please use 'timeframe' instead of 'ticker_interval." "Please use 'timeframe' instead of 'ticker_interval."
) )
if 'protections' in config: if "protections" in config:
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.") logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
@@ -5,4 +5,4 @@ def running_in_docker() -> bool:
""" """
Check if we are running in a docker container Check if we are running in a docker container
""" """
return os.environ.get('FT_APP_ENV') == 'docker' return os.environ.get("FT_APP_ENV") == "docker"
+26 -11
View File
@@ -4,8 +4,14 @@ from pathlib import Path
from typing import Optional from typing import Optional
from freqtrade.configuration.detect_environment import running_in_docker from freqtrade.configuration.detect_environment import running_in_docker
from freqtrade.constants import (USER_DATA_FILES, USERPATH_FREQAIMODELS, USERPATH_HYPEROPTS, from freqtrade.constants import (
USERPATH_NOTEBOOKS, USERPATH_STRATEGIES, Config) USER_DATA_FILES,
USERPATH_FREQAIMODELS,
USERPATH_HYPEROPTS,
USERPATH_NOTEBOOKS,
USERPATH_STRATEGIES,
Config,
)
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
@@ -13,16 +19,15 @@ logger = logging.getLogger(__name__)
def create_datadir(config: Config, datadir: Optional[str] = None) -> Path: def create_datadir(config: Config, datadir: Optional[str] = None) -> Path:
folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data") folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data")
if not datadir: if not datadir:
# set datadir # set datadir
exchange_name = config.get('exchange', {}).get('name', '').lower() exchange_name = config.get("exchange", {}).get("name", "").lower()
folder = folder.joinpath(exchange_name) folder = folder.joinpath(exchange_name)
if not folder.is_dir(): if not folder.is_dir():
folder.mkdir(parents=True) folder.mkdir(parents=True)
logger.info(f'Created data directory: {datadir}') logger.info(f"Created data directory: {datadir}")
return folder return folder
@@ -34,8 +39,8 @@ def chown_user_directory(directory: Path) -> None:
if running_in_docker(): if running_in_docker():
try: try:
import subprocess import subprocess
subprocess.check_output(
['sudo', 'chown', '-R', 'ftuser:', str(directory.resolve())]) subprocess.check_output(["sudo", "chown", "-R", "ftuser:", str(directory.resolve())])
except Exception: except Exception:
logger.warning(f"Could not chown {directory}") logger.warning(f"Could not chown {directory}")
@@ -50,18 +55,28 @@ def create_userdata_dir(directory: str, create_dir: bool = False) -> Path:
:param create_dir: Create directory if it does not exist. :param create_dir: Create directory if it does not exist.
:return: Path object containing the directory :return: Path object containing the directory
""" """
sub_dirs = ["backtest_results", "data", USERPATH_HYPEROPTS, "hyperopt_results", "logs", sub_dirs = [
USERPATH_NOTEBOOKS, "plot", USERPATH_STRATEGIES, USERPATH_FREQAIMODELS] "backtest_results",
"data",
USERPATH_HYPEROPTS,
"hyperopt_results",
"logs",
USERPATH_NOTEBOOKS,
"plot",
USERPATH_STRATEGIES,
USERPATH_FREQAIMODELS,
]
folder = Path(directory) folder = Path(directory)
chown_user_directory(folder) chown_user_directory(folder)
if not folder.is_dir(): if not folder.is_dir():
if create_dir: if create_dir:
folder.mkdir(parents=True) folder.mkdir(parents=True)
logger.info(f'Created user-data directory: {folder}') logger.info(f"Created user-data directory: {folder}")
else: else:
raise OperationalException( raise OperationalException(
f"Directory `{folder}` does not exist. " f"Directory `{folder}` does not exist. "
"Please use `freqtrade create-userdir` to create a user directory") "Please use `freqtrade create-userdir` to create a user directory"
)
# Create required subdirectories # Create required subdirectories
for f in sub_dirs: for f in sub_dirs:
+12 -7
View File
@@ -16,9 +16,9 @@ def _get_var_typed(val):
try: try:
return float(val) return float(val)
except ValueError: except ValueError:
if val.lower() in ('t', 'true'): if val.lower() in ("t", "true"):
return True return True
elif val.lower() in ('f', 'false'): elif val.lower() in ("f", "false"):
return False return False
# keep as string # keep as string
return val return val
@@ -32,16 +32,21 @@ def _flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str
:param prefix: Prefix to consider (usually FREQTRADE__) :param prefix: Prefix to consider (usually FREQTRADE__)
:return: Nested dict based on available and relevant variables. :return: Nested dict based on available and relevant variables.
""" """
no_convert = ['CHAT_ID', 'PASSWORD'] no_convert = ["CHAT_ID", "PASSWORD"]
relevant_vars: Dict[str, Any] = {} relevant_vars: Dict[str, Any] = {}
for env_var, val in sorted(env_dict.items()): for env_var, val in sorted(env_dict.items()):
if env_var.startswith(prefix): if env_var.startswith(prefix):
logger.info(f"Loading variable '{env_var}'") logger.info(f"Loading variable '{env_var}'")
key = env_var.replace(prefix, '') key = env_var.replace(prefix, "")
for k in reversed(key.split('__')): for k in reversed(key.split("__")):
val = {k.lower(): _get_var_typed(val) val = {
if not isinstance(val, dict) and k not in no_convert else val} k.lower(): (
_get_var_typed(val)
if not isinstance(val, dict) and k not in no_convert
else val
)
}
relevant_vars = deep_merge_dicts(val, relevant_vars) relevant_vars = deep_merge_dicts(val, relevant_vars)
return relevant_vars return relevant_vars
+26 -22
View File
@@ -1,6 +1,7 @@
""" """
This module contain functions to load the configuration file This module contain functions to load the configuration file
""" """
import logging import logging
import re import re
import sys import sys
@@ -11,7 +12,7 @@ from typing import Any, Dict, List, Optional
import rapidjson import rapidjson
from freqtrade.constants import MINIMAL_CONFIG, Config from freqtrade.constants import MINIMAL_CONFIG, Config
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import ConfigurationError, OperationalException
from freqtrade.misc import deep_merge_dicts from freqtrade.misc import deep_merge_dicts
@@ -25,28 +26,28 @@ def log_config_error_range(path: str, errmsg: str) -> str:
""" """
Parses configuration file and prints range around error Parses configuration file and prints range around error
""" """
if path != '-': if path != "-":
offsetlist = re.findall(r'(?<=Parse\serror\sat\soffset\s)\d+', errmsg) offsetlist = re.findall(r"(?<=Parse\serror\sat\soffset\s)\d+", errmsg)
if offsetlist: if offsetlist:
offset = int(offsetlist[0]) offset = int(offsetlist[0])
text = Path(path).read_text() text = Path(path).read_text()
# Fetch an offset of 80 characters around the error line # Fetch an offset of 80 characters around the error line
subtext = text[offset - min(80, offset) : offset + 80] subtext = text[offset - min(80, offset) : offset + 80]
segments = subtext.split('\n') segments = subtext.split("\n")
if len(segments) > 3: if len(segments) > 3:
# Remove first and last lines, to avoid odd truncations # Remove first and last lines, to avoid odd truncations
return '\n'.join(segments[1:-1]) return "\n".join(segments[1:-1])
else: else:
return subtext return subtext
return '' return ""
def load_file(path: Path) -> Dict[str, Any]: def load_file(path: Path) -> Dict[str, Any]:
try: try:
with path.open('r') as file: with path.open("r") as file:
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE) config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
except FileNotFoundError: except FileNotFoundError:
raise OperationalException(f'File "{path}" not found!') raise OperationalException(f'File "{path}" not found!') from None
return config return config
@@ -58,40 +59,42 @@ def load_config_file(path: str) -> Dict[str, Any]:
""" """
try: try:
# Read config from stdin if requested in the options # Read config from stdin if requested in the options
with Path(path).open() if path != '-' else sys.stdin as file: with Path(path).open() if path != "-" else sys.stdin as file:
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE) config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
except FileNotFoundError: except FileNotFoundError:
raise OperationalException( raise OperationalException(
f'Config file "{path}" not found!' f'Config file "{path}" not found!'
' Please create a config file or check whether it exists.') " Please create a config file or check whether it exists."
) from None
except rapidjson.JSONDecodeError as e: except rapidjson.JSONDecodeError as e:
err_range = log_config_error_range(path, str(e)) err_range = log_config_error_range(path, str(e))
raise OperationalException( raise ConfigurationError(
f'{e}\n' f"{e}\nPlease verify the following segment of your configuration:\n{err_range}"
f'Please verify the following segment of your configuration:\n{err_range}' if err_range
if err_range else 'Please verify your configuration file for syntax errors.' else "Please verify your configuration file for syntax errors."
) )
return config return config
def load_from_files( def load_from_files(
files: List[str], base_path: Optional[Path] = None, level: int = 0) -> Dict[str, Any]: files: List[str], base_path: Optional[Path] = None, level: int = 0
) -> Dict[str, Any]:
""" """
Recursively load configuration files if specified. Recursively load configuration files if specified.
Sub-files are assumed to be relative to the initial config. Sub-files are assumed to be relative to the initial config.
""" """
config: Config = {} config: Config = {}
if level > 5: if level > 5:
raise OperationalException("Config loop detected.") raise ConfigurationError("Config loop detected.")
if not files: if not files:
return deepcopy(MINIMAL_CONFIG) return deepcopy(MINIMAL_CONFIG)
files_loaded = [] files_loaded = []
# We expect here a list of config filenames # We expect here a list of config filenames
for filename in files: for filename in files:
logger.info(f'Using config: {filename} ...') logger.info(f"Using config: {filename} ...")
if filename == '-': if filename == "-":
# Immediately load stdin and return # Immediately load stdin and return
return load_config_file(filename) return load_config_file(filename)
file = Path(filename) file = Path(filename)
@@ -100,10 +103,11 @@ def load_from_files(
file = base_path / file file = base_path / file
config_tmp = load_config_file(str(file)) config_tmp = load_config_file(str(file))
if 'add_config_files' in config_tmp: if "add_config_files" in config_tmp:
config_sub = load_from_files( config_sub = load_from_files(
config_tmp['add_config_files'], file.resolve().parent, level + 1) config_tmp["add_config_files"], file.resolve().parent, level + 1
files_loaded.extend(config_sub.get('config_files', [])) )
files_loaded.extend(config_sub.get("config_files", []))
config_tmp = deep_merge_dicts(config_tmp, config_sub) config_tmp = deep_merge_dicts(config_tmp, config_sub)
files_loaded.insert(0, str(file)) files_loaded.insert(0, str(file))
@@ -111,6 +115,6 @@ def load_from_files(
# Merge config options, overwriting prior values # Merge config options, overwriting prior values
config = deep_merge_dicts(config_tmp, config) config = deep_merge_dicts(config_tmp, config)
config['config_files'] = files_loaded config["config_files"] = files_loaded
return config return config
+55 -37
View File
@@ -1,6 +1,7 @@
""" """
This module contains the argument manager class This module contains the argument manager class
""" """
import logging import logging
import re import re
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -9,7 +10,7 @@ from typing import Optional
from typing_extensions import Self from typing_extensions import Self
from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import ConfigurationError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -22,9 +23,13 @@ class TimeRange:
if *type is None, don't use corresponding startvalue. if *type is None, don't use corresponding startvalue.
""" """
def __init__(self, starttype: Optional[str] = None, stoptype: Optional[str] = None, def __init__(
startts: int = 0, stopts: int = 0): self,
starttype: Optional[str] = None,
stoptype: Optional[str] = None,
startts: int = 0,
stopts: int = 0,
):
self.starttype: Optional[str] = starttype self.starttype: Optional[str] = starttype
self.stoptype: Optional[str] = stoptype self.stoptype: Optional[str] = stoptype
self.startts: int = startts self.startts: int = startts
@@ -48,12 +53,12 @@ class TimeRange:
Returns a string representation of the timerange as used by parse_timerange. Returns a string representation of the timerange as used by parse_timerange.
Follows the format yyyymmdd-yyyymmdd - leaving out the parts that are not set. Follows the format yyyymmdd-yyyymmdd - leaving out the parts that are not set.
""" """
start = '' start = ""
stop = '' stop = ""
if startdt := self.startdt: if startdt := self.startdt:
start = startdt.strftime('%Y%m%d') start = startdt.strftime("%Y%m%d")
if stopdt := self.stopdt: if stopdt := self.stopdt:
stop = stopdt.strftime('%Y%m%d') stop = stopdt.strftime("%Y%m%d")
return f"{start}-{stop}" return f"{start}-{stop}"
@property @property
@@ -61,7 +66,7 @@ class TimeRange:
""" """
Returns a string representation of the start date Returns a string representation of the start date
""" """
val = 'unbounded' val = "unbounded"
if (startdt := self.startdt) is not None: if (startdt := self.startdt) is not None:
val = startdt.strftime(DATETIME_PRINT_FORMAT) val = startdt.strftime(DATETIME_PRINT_FORMAT)
return val return val
@@ -71,15 +76,19 @@ class TimeRange:
""" """
Returns a string representation of the stop date Returns a string representation of the stop date
""" """
val = 'unbounded' val = "unbounded"
if (stopdt := self.stopdt) is not None: if (stopdt := self.stopdt) is not None:
val = stopdt.strftime(DATETIME_PRINT_FORMAT) val = stopdt.strftime(DATETIME_PRINT_FORMAT)
return val return val
def __eq__(self, other): def __eq__(self, other):
"""Override the default Equals behavior""" """Override the default Equals behavior"""
return (self.starttype == other.starttype and self.stoptype == other.stoptype return (
and self.startts == other.startts and self.stopts == other.stopts) self.starttype == other.starttype
and self.stoptype == other.stoptype
and self.startts == other.startts
and self.stopts == other.stopts
)
def subtract_start(self, seconds: int) -> None: def subtract_start(self, seconds: int) -> None:
""" """
@@ -90,8 +99,9 @@ class TimeRange:
if self.startts: if self.startts:
self.startts = self.startts - seconds self.startts = self.startts - seconds
def adjust_start_if_necessary(self, timeframe_secs: int, startup_candles: int, def adjust_start_if_necessary(
min_date: datetime) -> None: self, timeframe_secs: int, startup_candles: int, min_date: datetime
) -> None:
""" """
Adjust startts by <startup_candles> candles. Adjust startts by <startup_candles> candles.
Applies only if no startup-candles have been available. Applies only if no startup-candles have been available.
@@ -101,13 +111,13 @@ class TimeRange:
has to be moved has to be moved
:return: None (Modifies the object in place) :return: None (Modifies the object in place)
""" """
if (not self.starttype or (startup_candles if not self.starttype or (startup_candles and min_date.timestamp() >= self.startts):
and min_date.timestamp() >= self.startts)):
# If no startts was defined, or backtest-data starts at the defined backtest-date # If no startts was defined, or backtest-data starts at the defined backtest-date
logger.warning("Moving start-date by %s candles to account for startup time.", logger.warning(
startup_candles) "Moving start-date by %s candles to account for startup time.", startup_candles
)
self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles) self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles)
self.starttype = 'date' self.starttype = "date"
@classmethod @classmethod
def parse_timerange(cls, text: Optional[str]) -> Self: def parse_timerange(cls, text: Optional[str]) -> Self:
@@ -118,15 +128,16 @@ class TimeRange:
""" """
if not text: if not text:
return cls(None, None, 0, 0) return cls(None, None, 0, 0)
syntax = [(r'^-(\d{8})$', (None, 'date')), syntax = [
(r'^(\d{8})-$', ('date', None)), (r"^-(\d{8})$", (None, "date")),
(r'^(\d{8})-(\d{8})$', ('date', 'date')), (r"^(\d{8})-$", ("date", None)),
(r'^-(\d{10})$', (None, 'date')), (r"^(\d{8})-(\d{8})$", ("date", "date")),
(r'^(\d{10})-$', ('date', None)), (r"^-(\d{10})$", (None, "date")),
(r'^(\d{10})-(\d{10})$', ('date', 'date')), (r"^(\d{10})-$", ("date", None)),
(r'^-(\d{13})$', (None, 'date')), (r"^(\d{10})-(\d{10})$", ("date", "date")),
(r'^(\d{13})-$', ('date', None)), (r"^-(\d{13})$", (None, "date")),
(r'^(\d{13})-(\d{13})$', ('date', 'date')), (r"^(\d{13})-$", ("date", None)),
(r"^(\d{13})-(\d{13})$", ("date", "date")),
] ]
for rex, stype in syntax: for rex, stype in syntax:
# Apply the regular expression to text # Apply the regular expression to text
@@ -138,9 +149,12 @@ class TimeRange:
stop: int = 0 stop: int = 0
if stype[0]: if stype[0]:
starts = rvals[index] starts = rvals[index]
if stype[0] == 'date' and len(starts) == 8: if stype[0] == "date" and len(starts) == 8:
start = int(datetime.strptime(starts, '%Y%m%d').replace( start = int(
tzinfo=timezone.utc).timestamp()) datetime.strptime(starts, "%Y%m%d")
.replace(tzinfo=timezone.utc)
.timestamp()
)
elif len(starts) == 13: elif len(starts) == 13:
start = int(starts) // 1000 start = int(starts) // 1000
else: else:
@@ -148,15 +162,19 @@ class TimeRange:
index += 1 index += 1
if stype[1]: if stype[1]:
stops = rvals[index] stops = rvals[index]
if stype[1] == 'date' and len(stops) == 8: if stype[1] == "date" and len(stops) == 8:
stop = int(datetime.strptime(stops, '%Y%m%d').replace( stop = int(
tzinfo=timezone.utc).timestamp()) datetime.strptime(stops, "%Y%m%d")
.replace(tzinfo=timezone.utc)
.timestamp()
)
elif len(stops) == 13: elif len(stops) == 13:
stop = int(stops) // 1000 stop = int(stops) // 1000
else: else:
stop = int(stops) stop = int(stops)
if start > stop > 0: if start > stop > 0:
raise OperationalException( raise ConfigurationError(
f'Start date is after stop date for timerange "{text}"') f'Start date is after stop date for timerange "{text}"'
)
return cls(stype[0], stype[1], start, stop) return cls(stype[0], stype[1], start, stop)
raise OperationalException(f'Incorrect syntax for timerange "{text}"') raise ConfigurationError(f'Incorrect syntax for timerange "{text}"')
+524 -467
View File
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -3,6 +3,4 @@ Module to handle data operations for freqtrade
""" """
# limit what's imported when using `from freqtrade.data import *` # limit what's imported when using `from freqtrade.data import *`
__all__ = [ __all__ = ["converter"]
'converter'
]
+136 -86
View File
@@ -1,6 +1,7 @@
""" """
Helpers when analyzing backtest data Helpers when analyzing backtest data
""" """
import logging import logging
from copy import copy from copy import copy
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -11,7 +12,7 @@ import numpy as np
import pandas as pd import pandas as pd
from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import ConfigurationError, OperationalException
from freqtrade.misc import file_dump_json, json_load from freqtrade.misc import file_dump_json, json_load
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.persistence import LocalTrade, Trade, init_db
@@ -21,13 +22,34 @@ from freqtrade.types import BacktestHistoryEntryType, BacktestResultType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Newest format # Newest format
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'max_stake_amount', 'amount', BT_DATA_COLUMNS = [
'open_date', 'close_date', 'open_rate', 'close_rate', "pair",
'fee_open', 'fee_close', 'trade_duration', "stake_amount",
'profit_ratio', 'profit_abs', 'exit_reason', "max_stake_amount",
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', "amount",
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag', "open_date",
'leverage', 'is_short', 'open_timestamp', 'close_timestamp', 'orders' "close_date",
"open_rate",
"close_rate",
"fee_open",
"fee_close",
"trade_duration",
"profit_ratio",
"profit_abs",
"exit_reason",
"initial_stop_loss_abs",
"initial_stop_loss_ratio",
"stop_loss_abs",
"stop_loss_ratio",
"min_rate",
"max_rate",
"is_open",
"enter_tag",
"leverage",
"is_short",
"open_timestamp",
"close_timestamp",
"orders",
] ]
@@ -50,15 +72,16 @@ def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> s
if not filename.is_file(): if not filename.is_file():
raise ValueError( raise ValueError(
f"Directory '{directory}' does not seem to contain backtest statistics yet.") f"Directory '{directory}' does not seem to contain backtest statistics yet."
)
with filename.open() as file: with filename.open() as file:
data = json_load(file) data = json_load(file)
if f'latest_{variant}' not in data: if f"latest_{variant}" not in data:
raise ValueError(f"Invalid '{LAST_BT_RESULT_FN}' format.") raise ValueError(f"Invalid '{LAST_BT_RESULT_FN}' format.")
return data[f'latest_{variant}'] return data[f"latest_{variant}"]
def get_latest_backtest_filename(directory: Union[Path, str]) -> str: def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
@@ -71,7 +94,7 @@ def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
* `directory/.last_result.json` does not exist * `directory/.last_result.json` does not exist
* `directory/.last_result.json` has the wrong content * `directory/.last_result.json` has the wrong content
""" """
return get_latest_optimize_filename(directory, 'backtest') return get_latest_optimize_filename(directory, "backtest")
def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str: def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
@@ -85,14 +108,15 @@ def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
* `directory/.last_result.json` has the wrong content * `directory/.last_result.json` has the wrong content
""" """
try: try:
return get_latest_optimize_filename(directory, 'hyperopt') return get_latest_optimize_filename(directory, "hyperopt")
except ValueError: except ValueError:
# Return default (legacy) pickle filename # Return default (legacy) pickle filename
return 'hyperopt_results.pickle' return "hyperopt_results.pickle"
def get_latest_hyperopt_file( def get_latest_hyperopt_file(
directory: Union[Path, str], predef_filename: Optional[str] = None) -> Path: directory: Union[Path, str], predef_filename: Optional[str] = None
) -> Path:
""" """
Get latest hyperopt export based on '.last_result.json'. Get latest hyperopt export based on '.last_result.json'.
:param directory: Directory to search for last result :param directory: Directory to search for last result
@@ -106,8 +130,9 @@ def get_latest_hyperopt_file(
directory = Path(directory) directory = Path(directory)
if predef_filename: if predef_filename:
if Path(predef_filename).is_absolute(): if Path(predef_filename).is_absolute():
raise OperationalException( raise ConfigurationError(
"--hyperopt-filename expects only the filename, not an absolute path.") "--hyperopt-filename expects only the filename, not an absolute path."
)
return directory / predef_filename return directory / predef_filename
return directory / get_latest_hyperopt_filename(directory) return directory / get_latest_hyperopt_filename(directory)
@@ -126,7 +151,7 @@ def load_backtest_metadata(filename: Union[Path, str]) -> Dict[str, Any]:
except FileNotFoundError: except FileNotFoundError:
return {} return {}
except Exception as e: except Exception as e:
raise OperationalException('Unexpected error while loading backtest metadata.') from e raise OperationalException("Unexpected error while loading backtest metadata.") from e
def load_backtest_stats(filename: Union[Path, str]) -> BacktestResultType: def load_backtest_stats(filename: Union[Path, str]) -> BacktestResultType:
@@ -147,7 +172,7 @@ def load_backtest_stats(filename: Union[Path, str]) -> BacktestResultType:
# Legacy list format does not contain metadata. # Legacy list format does not contain metadata.
if isinstance(data, dict): if isinstance(data, dict):
data['metadata'] = load_backtest_metadata(filename) data["metadata"] = load_backtest_metadata(filename)
return data return data
@@ -159,38 +184,39 @@ def load_and_merge_backtest_result(strategy_name: str, filename: Path, results:
:param results: dict to merge the result to. :param results: dict to merge the result to.
""" """
bt_data = load_backtest_stats(filename) bt_data = load_backtest_stats(filename)
k: Literal['metadata', 'strategy'] k: Literal["metadata", "strategy"]
for k in ('metadata', 'strategy'): # type: ignore for k in ("metadata", "strategy"): # type: ignore
results[k][strategy_name] = bt_data[k][strategy_name] results[k][strategy_name] = bt_data[k][strategy_name]
results['metadata'][strategy_name]['filename'] = filename.stem results["metadata"][strategy_name]["filename"] = filename.stem
comparison = bt_data['strategy_comparison'] comparison = bt_data["strategy_comparison"]
for i in range(len(comparison)): for i in range(len(comparison)):
if comparison[i]['key'] == strategy_name: if comparison[i]["key"] == strategy_name:
results['strategy_comparison'].append(comparison[i]) results["strategy_comparison"].append(comparison[i])
break break
def _get_backtest_files(dirname: Path) -> List[Path]: def _get_backtest_files(dirname: Path) -> List[Path]:
# Weird glob expression here avoids including .meta.json files. # Weird glob expression here avoids including .meta.json files.
return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json')))) return list(reversed(sorted(dirname.glob("backtest-result-*-[0-9][0-9].json"))))
def _extract_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]: def _extract_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]:
metadata = load_backtest_metadata(filename) metadata = load_backtest_metadata(filename)
return [ return [
{ {
'filename': filename.stem, "filename": filename.stem,
'strategy': s, "strategy": s,
'run_id': v['run_id'], "run_id": v["run_id"],
'notes': v.get('notes', ''), "notes": v.get("notes", ""),
# Backtest "run" time # Backtest "run" time
'backtest_start_time': v['backtest_start_time'], "backtest_start_time": v["backtest_start_time"],
# Backtest timerange # Backtest timerange
'backtest_start_ts': v.get('backtest_start_ts', None), "backtest_start_ts": v.get("backtest_start_ts", None),
'backtest_end_ts': v.get('backtest_end_ts', None), "backtest_end_ts": v.get("backtest_end_ts", None),
'timeframe': v.get('timeframe', None), "timeframe": v.get("timeframe", None),
'timeframe_detail': v.get('timeframe_detail', None), "timeframe_detail": v.get("timeframe_detail", None),
} for s, v in metadata.items() }
for s, v in metadata.items()
] ]
@@ -218,7 +244,7 @@ def delete_backtest_result(file_abs: Path):
""" """
# *.meta.json # *.meta.json
logger.info(f"Deleting backtest result file: {file_abs.name}") logger.info(f"Deleting backtest result file: {file_abs.name}")
file_abs_meta = file_abs.with_suffix('.meta.json') file_abs_meta = file_abs.with_suffix(".meta.json")
file_abs.unlink() file_abs.unlink()
file_abs_meta.unlink() file_abs_meta.unlink()
@@ -238,8 +264,19 @@ def update_backtest_metadata(filename: Path, strategy: str, content: Dict[str, A
file_dump_json(get_backtest_metadata_filename(filename), metadata) file_dump_json(get_backtest_metadata_filename(filename), metadata)
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str], def get_backtest_market_change(filename: Path, include_ts: bool = True) -> pd.DataFrame:
min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]: """
Read backtest market change file.
"""
df = pd.read_feather(filename)
if include_ts:
df.loc[:, "__date_ts"] = df.loc[:, "date"].astype(np.int64) // 1000 // 1000
return df
def find_existing_backtest_stats(
dirname: Union[Path, str], run_ids: Dict[str, str], min_backtest_date: Optional[datetime] = None
) -> Dict[str, Any]:
""" """
Find existing backtest stats that match specified run IDs and load them. Find existing backtest stats that match specified run IDs and load them.
:param dirname: pathlib.Path object, or string pointing to the file. :param dirname: pathlib.Path object, or string pointing to the file.
@@ -251,9 +288,9 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
run_ids = copy(run_ids) run_ids = copy(run_ids)
dirname = Path(dirname) dirname = Path(dirname)
results: Dict[str, Any] = { results: Dict[str, Any] = {
'metadata': {}, "metadata": {},
'strategy': {}, "strategy": {},
'strategy_comparison': [], "strategy_comparison": [],
} }
for filename in _get_backtest_files(dirname): for filename in _get_backtest_files(dirname):
@@ -270,14 +307,14 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
continue continue
if min_backtest_date is not None: if min_backtest_date is not None:
backtest_date = strategy_metadata['backtest_start_time'] backtest_date = strategy_metadata["backtest_start_time"]
backtest_date = datetime.fromtimestamp(backtest_date, tz=timezone.utc) backtest_date = datetime.fromtimestamp(backtest_date, tz=timezone.utc)
if backtest_date < min_backtest_date: if backtest_date < min_backtest_date:
# Do not use a cached result for this strategy as first result is too old. # Do not use a cached result for this strategy as first result is too old.
del run_ids[strategy_name] del run_ids[strategy_name]
continue continue
if strategy_metadata['run_id'] == run_id: if strategy_metadata["run_id"] == run_id:
del run_ids[strategy_name] del run_ids[strategy_name]
load_and_merge_backtest_result(strategy_name, filename, results) load_and_merge_backtest_result(strategy_name, filename, results)
@@ -290,20 +327,20 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
""" """
Compatibility support for older backtest data. Compatibility support for older backtest data.
""" """
df['open_date'] = pd.to_datetime(df['open_date'], utc=True) df["open_date"] = pd.to_datetime(df["open_date"], utc=True)
df['close_date'] = pd.to_datetime(df['close_date'], utc=True) df["close_date"] = pd.to_datetime(df["close_date"], utc=True)
# Compatibility support for pre short Columns # Compatibility support for pre short Columns
if 'is_short' not in df.columns: if "is_short" not in df.columns:
df['is_short'] = False df["is_short"] = False
if 'leverage' not in df.columns: if "leverage" not in df.columns:
df['leverage'] = 1.0 df["leverage"] = 1.0
if 'enter_tag' not in df.columns: if "enter_tag" not in df.columns:
df['enter_tag'] = df['buy_tag'] df["enter_tag"] = df["buy_tag"]
df = df.drop(['buy_tag'], axis=1) df = df.drop(["buy_tag"], axis=1)
if 'max_stake_amount' not in df.columns: if "max_stake_amount" not in df.columns:
df['max_stake_amount'] = df['stake_amount'] df["max_stake_amount"] = df["stake_amount"]
if 'orders' not in df.columns: if "orders" not in df.columns:
df['orders'] = None df["orders"] = None
return df return df
@@ -319,23 +356,25 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
data = load_backtest_stats(filename) data = load_backtest_stats(filename)
if not isinstance(data, list): if not isinstance(data, list):
# new, nested format # new, nested format
if 'strategy' not in data: if "strategy" not in data:
raise ValueError("Unknown dataformat.") raise ValueError("Unknown dataformat.")
if not strategy: if not strategy:
if len(data['strategy']) == 1: if len(data["strategy"]) == 1:
strategy = list(data['strategy'].keys())[0] strategy = list(data["strategy"].keys())[0]
else: else:
raise ValueError("Detected backtest result with more than one strategy. " raise ValueError(
"Please specify a strategy.") "Detected backtest result with more than one strategy. "
"Please specify a strategy."
)
if strategy not in data['strategy']: if strategy not in data["strategy"]:
raise ValueError( raise ValueError(
f"Strategy {strategy} not available in the backtest result. " f"Strategy {strategy} not available in the backtest result. "
f"Available strategies are '{','.join(data['strategy'].keys())}'" f"Available strategies are '{','.join(data['strategy'].keys())}'"
) )
data = data['strategy'][strategy]['trades'] data = data["strategy"][strategy]["trades"]
df = pd.DataFrame(data) df = pd.DataFrame(data)
if not df.empty: if not df.empty:
df = _load_backtest_data_df_compatibility(df) df = _load_backtest_data_df_compatibility(df)
@@ -343,7 +382,8 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
else: else:
# old format - only with lists. # old format - only with lists.
raise OperationalException( raise OperationalException(
"Backtest-results with only trades data are no longer supported.") "Backtest-results with only trades data are no longer supported."
)
if not df.empty: if not df.empty:
df = df.sort_values("open_date").reset_index(drop=True) df = df.sort_values("open_date").reset_index(drop=True)
return df return df
@@ -358,23 +398,26 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF
:return: dataframe with open-counts per time-period in timeframe :return: dataframe with open-counts per time-period in timeframe
""" """
from freqtrade.exchange import timeframe_to_resample_freq from freqtrade.exchange import timeframe_to_resample_freq
timeframe_freq = timeframe_to_resample_freq(timeframe) timeframe_freq = timeframe_to_resample_freq(timeframe)
dates = [pd.Series(pd.date_range(row[1]['open_date'], row[1]['close_date'], dates = [
freq=timeframe_freq)) pd.Series(pd.date_range(row[1]["open_date"], row[1]["close_date"], freq=timeframe_freq))
for row in results[['open_date', 'close_date']].iterrows()] for row in results[["open_date", "close_date"]].iterrows()
]
deltas = [len(x) for x in dates] deltas = [len(x) for x in dates]
dates = pd.Series(pd.concat(dates).values, name='date') dates = pd.Series(pd.concat(dates).values, name="date")
df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns) df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns)
df2 = pd.concat([dates, df2], axis=1) df2 = pd.concat([dates, df2], axis=1)
df2 = df2.set_index('date') df2 = df2.set_index("date")
df_final = df2.resample(timeframe_freq)[['pair']].count() df_final = df2.resample(timeframe_freq)[["pair"]].count()
df_final = df_final.rename({'pair': 'open_trades'}, axis=1) df_final = df_final.rename({"pair": "open_trades"}, axis=1)
return df_final return df_final
def evaluate_result_multi(results: pd.DataFrame, timeframe: str, def evaluate_result_multi(
max_open_trades: IntOrInf) -> pd.DataFrame: results: pd.DataFrame, timeframe: str, max_open_trades: IntOrInf
) -> pd.DataFrame:
""" """
Find overlapping trades by expanding each trade once per period it was open Find overlapping trades by expanding each trade once per period it was open
and then counting overlaps and then counting overlaps
@@ -384,7 +427,7 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
:return: dataframe with open-counts per time-period in freq :return: dataframe with open-counts per time-period in freq
""" """
df_final = analyze_trade_parallelism(results, timeframe) df_final = analyze_trade_parallelism(results, timeframe)
return df_final[df_final['open_trades'] > max_open_trades] return df_final[df_final["open_trades"] > max_open_trades]
def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.DataFrame: def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.DataFrame:
@@ -395,9 +438,9 @@ def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.
""" """
df = pd.DataFrame.from_records([t.to_json(True) for t in trades], columns=BT_DATA_COLUMNS) df = pd.DataFrame.from_records([t.to_json(True) for t in trades], columns=BT_DATA_COLUMNS)
if len(df) > 0: if len(df) > 0:
df['close_date'] = pd.to_datetime(df['close_date'], utc=True) df["close_date"] = pd.to_datetime(df["close_date"], utc=True)
df['open_date'] = pd.to_datetime(df['open_date'], utc=True) df["open_date"] = pd.to_datetime(df["open_date"], utc=True)
df['close_rate'] = df['close_rate'].astype('float64') df["close_rate"] = df["close_rate"].astype("float64")
return df return df
@@ -419,8 +462,13 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF
return trades return trades
def load_trades(source: str, db_url: str, exportfilename: Path, def load_trades(
no_trades: bool = False, strategy: Optional[str] = None) -> pd.DataFrame: source: str,
db_url: str,
exportfilename: Path,
no_trades: bool = False,
strategy: Optional[str] = None,
) -> pd.DataFrame:
""" """
Based on configuration option 'trade_source': Based on configuration option 'trade_source':
* loads data from DB (using `db_url`) * loads data from DB (using `db_url`)
@@ -441,8 +489,9 @@ def load_trades(source: str, db_url: str, exportfilename: Path,
return load_backtest_data(exportfilename, strategy) return load_backtest_data(exportfilename, strategy)
def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame, def extract_trades_of_period(
date_index=False) -> pd.DataFrame: dataframe: pd.DataFrame, trades: pd.DataFrame, date_index=False
) -> pd.DataFrame:
""" """
Compare trades and backtested pair DataFrames to get trades performed on backtested period Compare trades and backtested pair DataFrames to get trades performed on backtested period
:return: the DataFrame of a trades of period :return: the DataFrame of a trades of period
@@ -451,8 +500,9 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame,
trades_start = dataframe.index[0] trades_start = dataframe.index[0]
trades_stop = dataframe.index[-1] trades_stop = dataframe.index[-1]
else: else:
trades_start = dataframe.iloc[0]['date'] trades_start = dataframe.iloc[0]["date"]
trades_stop = dataframe.iloc[-1]['date'] trades_stop = dataframe.iloc[-1]["date"]
trades = trades.loc[(trades['open_date'] >= trades_start) & trades = trades.loc[
(trades['close_date'] <= trades_stop)] (trades["open_date"] >= trades_start) & (trades["close_date"] <= trades_stop)
]
return trades return trades
+33 -23
View File
@@ -1,28 +1,38 @@
from freqtrade.data.converter.converter import (clean_ohlcv_dataframe, convert_ohlcv_format, from freqtrade.data.converter.converter import (
ohlcv_fill_up_missing_data, ohlcv_to_dataframe, clean_ohlcv_dataframe,
order_book_to_dataframe, reduce_dataframe_footprint, convert_ohlcv_format,
trim_dataframe, trim_dataframes) ohlcv_fill_up_missing_data,
from freqtrade.data.converter.trade_converter import (convert_trades_format, ohlcv_to_dataframe,
convert_trades_to_ohlcv, trades_convert_types, order_book_to_dataframe,
reduce_dataframe_footprint,
trim_dataframe,
trim_dataframes,
)
from freqtrade.data.converter.trade_converter import (
convert_trades_format,
convert_trades_to_ohlcv,
trades_convert_types,
trades_df_remove_duplicates, trades_df_remove_duplicates,
trades_dict_to_list, trades_list_to_df, trades_dict_to_list,
trades_to_ohlcv) trades_list_to_df,
trades_to_ohlcv,
)
__all__ = [ __all__ = [
'clean_ohlcv_dataframe', "clean_ohlcv_dataframe",
'convert_ohlcv_format', "convert_ohlcv_format",
'ohlcv_fill_up_missing_data', "ohlcv_fill_up_missing_data",
'ohlcv_to_dataframe', "ohlcv_to_dataframe",
'order_book_to_dataframe', "order_book_to_dataframe",
'reduce_dataframe_footprint', "reduce_dataframe_footprint",
'trim_dataframe', "trim_dataframe",
'trim_dataframes', "trim_dataframes",
'convert_trades_format', "convert_trades_format",
'convert_trades_to_ohlcv', "convert_trades_to_ohlcv",
'trades_convert_types', "trades_convert_types",
'trades_df_remove_duplicates', "trades_df_remove_duplicates",
'trades_dict_to_list', "trades_dict_to_list",
'trades_list_to_df', "trades_list_to_df",
'trades_to_ohlcv', "trades_to_ohlcv",
] ]
+108 -79
View File
@@ -1,6 +1,7 @@
""" """
Functions to convert data from one format to another Functions to convert data from one format to another
""" """
import logging import logging
from typing import Dict from typing import Dict
@@ -15,8 +16,14 @@ from freqtrade.enums import CandleType, TradingMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *, def ohlcv_to_dataframe(
fill_missing: bool = True, drop_incomplete: bool = True) -> DataFrame: ohlcv: list,
timeframe: str,
pair: str,
*,
fill_missing: bool = True,
drop_incomplete: bool = True,
) -> DataFrame:
""" """
Converts a list with candle (OHLCV) data (in format returned by ccxt.fetch_ohlcv) Converts a list with candle (OHLCV) data (in format returned by ccxt.fetch_ohlcv)
to a Dataframe to a Dataframe
@@ -32,20 +39,28 @@ def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *,
cols = DEFAULT_DATAFRAME_COLUMNS cols = DEFAULT_DATAFRAME_COLUMNS
df = DataFrame(ohlcv, columns=cols) df = DataFrame(ohlcv, columns=cols)
df['date'] = to_datetime(df['date'], unit='ms', utc=True) df["date"] = to_datetime(df["date"], unit="ms", utc=True)
# Some exchanges return int values for Volume and even for OHLC. # Some exchanges return int values for Volume and even for OHLC.
# Convert them since TA-LIB indicators used in the strategy assume floats # Convert them since TA-LIB indicators used in the strategy assume floats
# and fail with exception... # and fail with exception...
df = df.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', df = df.astype(
'volume': 'float'}) dtype={
return clean_ohlcv_dataframe(df, timeframe, pair, "open": "float",
fill_missing=fill_missing, "high": "float",
drop_incomplete=drop_incomplete) "low": "float",
"close": "float",
"volume": "float",
}
)
return clean_ohlcv_dataframe(
df, timeframe, pair, fill_missing=fill_missing, drop_incomplete=drop_incomplete
)
def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *, def clean_ohlcv_dataframe(
fill_missing: bool, drop_incomplete: bool) -> DataFrame: data: DataFrame, timeframe: str, pair: str, *, fill_missing: bool, drop_incomplete: bool
) -> DataFrame:
""" """
Cleanse a OHLCV dataframe by Cleanse a OHLCV dataframe by
* Grouping it by date (removes duplicate tics) * Grouping it by date (removes duplicate tics)
@@ -60,17 +75,19 @@ def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *,
:return: DataFrame :return: DataFrame
""" """
# group by index and aggregate results to eliminate duplicate ticks # group by index and aggregate results to eliminate duplicate ticks
data = data.groupby(by='date', as_index=False, sort=True).agg({ data = data.groupby(by="date", as_index=False, sort=True).agg(
'open': 'first', {
'high': 'max', "open": "first",
'low': 'min', "high": "max",
'close': 'last', "low": "min",
'volume': 'max', "close": "last",
}) "volume": "max",
}
)
# eliminate partial candle # eliminate partial candle
if drop_incomplete: if drop_incomplete:
data.drop(data.tail(1).index, inplace=True) data.drop(data.tail(1).index, inplace=True)
logger.debug('Dropping last candle') logger.debug("Dropping last candle")
if fill_missing: if fill_missing:
return ohlcv_fill_up_missing_data(data, timeframe, pair) return ohlcv_fill_up_missing_data(data, timeframe, pair)
@@ -81,37 +98,35 @@ def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *,
def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) -> DataFrame: def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) -> DataFrame:
""" """
Fills up missing data with 0 volume rows, Fills up missing data with 0 volume rows,
using the previous close as price for "open", "high" "low" and "close", volume is set to 0 using the previous close as price for "open", "high", "low" and "close", volume is set to 0
""" """
from freqtrade.exchange import timeframe_to_resample_freq from freqtrade.exchange import timeframe_to_resample_freq
ohlcv_dict = { ohlcv_dict = {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
}
resample_interval = timeframe_to_resample_freq(timeframe) resample_interval = timeframe_to_resample_freq(timeframe)
# Resample to create "NAN" values # Resample to create "NAN" values
df = dataframe.resample(resample_interval, on='date').agg(ohlcv_dict) df = dataframe.resample(resample_interval, on="date").agg(ohlcv_dict)
# Forwardfill close for missing columns # Forwardfill close for missing columns
df['close'] = df['close'].ffill() df["close"] = df["close"].ffill()
# Use close for "open, high, low" # Use close for "open, high, low"
df.loc[:, ['open', 'high', 'low']] = df[['open', 'high', 'low']].fillna( df.loc[:, ["open", "high", "low"]] = df[["open", "high", "low"]].fillna(
value={'open': df['close'], value={
'high': df['close'], "open": df["close"],
'low': df['close'], "high": df["close"],
}) "low": df["close"],
}
)
df.reset_index(inplace=True) df.reset_index(inplace=True)
len_before = len(dataframe) len_before = len(dataframe)
len_after = len(df) len_after = len(df)
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0 pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
if len_before != len_after: if len_before != len_after:
message = (f"Missing data fillup for {pair}, {timeframe}: " message = (
f"before: {len_before} - after: {len_after} - {pct_missing:.2%}") f"Missing data fillup for {pair}, {timeframe}: "
f"before: {len_before} - after: {len_after} - {pct_missing:.2%}"
)
if pct_missing > 0.01: if pct_missing > 0.01:
logger.info(message) logger.info(message)
else: else:
@@ -120,8 +135,9 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
return df return df
def trim_dataframe(df: DataFrame, timerange, *, df_date_col: str = 'date', def trim_dataframe(
startup_candles: int = 0) -> DataFrame: df: DataFrame, timerange, *, df_date_col: str = "date", startup_candles: int = 0
) -> DataFrame:
""" """
Trim dataframe based on given timerange Trim dataframe based on given timerange
:param df: Dataframe to trim :param df: Dataframe to trim
@@ -134,15 +150,16 @@ def trim_dataframe(df: DataFrame, timerange, *, df_date_col: str = 'date',
# Trim candles instead of timeframe in case of given startup_candle count # Trim candles instead of timeframe in case of given startup_candle count
df = df.iloc[startup_candles:, :] df = df.iloc[startup_candles:, :]
else: else:
if timerange.starttype == 'date': if timerange.starttype == "date":
df = df.loc[df[df_date_col] >= timerange.startdt, :] df = df.loc[df[df_date_col] >= timerange.startdt, :]
if timerange.stoptype == 'date': if timerange.stoptype == "date":
df = df.loc[df[df_date_col] <= timerange.stopdt, :] df = df.loc[df[df_date_col] <= timerange.stopdt, :]
return df return df
def trim_dataframes(preprocessed: Dict[str, DataFrame], timerange, def trim_dataframes(
startup_candles: int) -> Dict[str, DataFrame]: preprocessed: Dict[str, DataFrame], timerange, startup_candles: int
) -> Dict[str, DataFrame]:
""" """
Trim startup period from analyzed dataframes Trim startup period from analyzed dataframes
:param preprocessed: Dict of pair: dataframe :param preprocessed: Dict of pair: dataframe
@@ -157,8 +174,9 @@ def trim_dataframes(preprocessed: Dict[str, DataFrame], timerange,
if not trimed_df.empty: if not trimed_df.empty:
processed[pair] = trimed_df processed[pair] = trimed_df
else: else:
logger.warning(f'{pair} has no data left after adjusting for startup candles, ' logger.warning(
f'skipping.') f"{pair} has no data left after adjusting for startup candles, skipping."
)
return processed return processed
@@ -170,19 +188,28 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
b_sum b_size bids asks a_size a_sum b_sum b_size bids asks a_size a_sum
------------------------------------------------------------------- -------------------------------------------------------------------
""" """
cols = ['bids', 'b_size'] cols = ["bids", "b_size"]
bids_frame = DataFrame(bids, columns=cols) bids_frame = DataFrame(bids, columns=cols)
# add cumulative sum column # add cumulative sum column
bids_frame['b_sum'] = bids_frame['b_size'].cumsum() bids_frame["b_sum"] = bids_frame["b_size"].cumsum()
cols2 = ['asks', 'a_size'] cols2 = ["asks", "a_size"]
asks_frame = DataFrame(asks, columns=cols2) asks_frame = DataFrame(asks, columns=cols2)
# add cumulative sum column # add cumulative sum column
asks_frame['a_sum'] = asks_frame['a_size'].cumsum() asks_frame["a_sum"] = asks_frame["a_size"].cumsum()
frame = pd.concat([bids_frame['b_sum'], bids_frame['b_size'], bids_frame['bids'], frame = pd.concat(
asks_frame['asks'], asks_frame['a_size'], asks_frame['a_sum']], axis=1, [
keys=['b_sum', 'b_size', 'bids', 'asks', 'a_size', 'a_sum']) bids_frame["b_sum"],
bids_frame["b_size"],
bids_frame["bids"],
asks_frame["asks"],
asks_frame["a_size"],
asks_frame["a_sum"],
],
axis=1,
keys=["b_sum", "b_size", "bids", "asks", "a_size", "a_sum"],
)
# logger.info('order book %s', frame ) # logger.info('order book %s', frame )
return frame return frame
@@ -200,48 +227,52 @@ def convert_ohlcv_format(
:param convert_to: Target format :param convert_to: Target format
:param erase: Erase source data (does not apply if source and target format are identical) :param erase: Erase source data (does not apply if source and target format are identical)
""" """
from freqtrade.data.history.idatahandler import get_datahandler from freqtrade.data.history import get_datahandler
src = get_datahandler(config['datadir'], convert_from)
trg = get_datahandler(config['datadir'], convert_to) src = get_datahandler(config["datadir"], convert_from)
timeframes = config.get('timeframes', [config.get('timeframe')]) trg = get_datahandler(config["datadir"], convert_to)
timeframes = config.get("timeframes", [config.get("timeframe")])
logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}") logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}")
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', [ candle_types = [
c.value for c in CandleType])] CandleType.from_string(ct)
for ct in config.get("candle_types", [c.value for c in CandleType])
]
logger.info(candle_types) logger.info(candle_types)
paircombs = src.ohlcv_get_available_data(config['datadir'], TradingMode.SPOT) paircombs = src.ohlcv_get_available_data(config["datadir"], TradingMode.SPOT)
paircombs.extend(src.ohlcv_get_available_data(config['datadir'], TradingMode.FUTURES)) paircombs.extend(src.ohlcv_get_available_data(config["datadir"], TradingMode.FUTURES))
if 'pairs' in config: if "pairs" in config:
# Filter pairs # Filter pairs
paircombs = [comb for comb in paircombs if comb[0] in config['pairs']] paircombs = [comb for comb in paircombs if comb[0] in config["pairs"]]
if 'timeframes' in config: if "timeframes" in config:
paircombs = [comb for comb in paircombs if comb[1] in config['timeframes']] paircombs = [comb for comb in paircombs if comb[1] in config["timeframes"]]
paircombs = [comb for comb in paircombs if comb[2] in candle_types] paircombs = [comb for comb in paircombs if comb[2] in candle_types]
paircombs = sorted(paircombs, key=lambda x: (x[0], x[1], x[2].value)) paircombs = sorted(paircombs, key=lambda x: (x[0], x[1], x[2].value))
formatted_paircombs = '\n'.join([f"{pair}, {timeframe}, {candle_type}" formatted_paircombs = "\n".join(
for pair, timeframe, candle_type in paircombs]) [f"{pair}, {timeframe}, {candle_type}" for pair, timeframe, candle_type in paircombs]
)
logger.info(f"Converting candle (OHLCV) data for the following pair combinations:\n" logger.info(
f"{formatted_paircombs}") f"Converting candle (OHLCV) data for the following pair combinations:\n"
f"{formatted_paircombs}"
)
for pair, timeframe, candle_type in paircombs: for pair, timeframe, candle_type in paircombs:
data = src.ohlcv_load(pair=pair, timeframe=timeframe, data = src.ohlcv_load(
pair=pair,
timeframe=timeframe,
timerange=None, timerange=None,
fill_missing=False, fill_missing=False,
drop_incomplete=False, drop_incomplete=False,
startup_candles=0, startup_candles=0,
candle_type=candle_type) candle_type=candle_type,
)
logger.info(f"Converting {len(data)} {timeframe} {candle_type} candles for {pair}") logger.info(f"Converting {len(data)} {timeframe} {candle_type} candles for {pair}")
if len(data) > 0: if len(data) > 0:
trg.ohlcv_store( trg.ohlcv_store(pair=pair, timeframe=timeframe, data=data, candle_type=candle_type)
pair=pair,
timeframe=timeframe,
data=data,
candle_type=candle_type
)
if erase and convert_from != convert_to: if erase and convert_from != convert_to:
logger.info(f"Deleting source data for {pair} / {timeframe}") logger.info(f"Deleting source data for {pair} / {timeframe}")
src.ohlcv_purge(pair=pair, timeframe=timeframe, candle_type=candle_type) src.ohlcv_purge(pair=pair, timeframe=timeframe, candle_type=candle_type)
@@ -254,12 +285,11 @@ def reduce_dataframe_footprint(df: DataFrame) -> DataFrame:
:return: Dataframe converted to float/int 32s :return: Dataframe converted to float/int 32s
""" """
logger.debug(f"Memory usage of dataframe is " logger.debug(f"Memory usage of dataframe is {df.memory_usage().sum() / 1024**2:.2f} MB")
f"{df.memory_usage().sum() / 1024**2:.2f} MB")
df_dtypes = df.dtypes df_dtypes = df.dtypes
for column, dtype in df_dtypes.items(): for column, dtype in df_dtypes.items():
if column in ['open', 'high', 'low', 'close', 'volume']: if column in ["open", "high", "low", "close", "volume"]:
continue continue
if dtype == np.float64: if dtype == np.float64:
df_dtypes[column] = np.float32 df_dtypes[column] = np.float32
@@ -267,7 +297,6 @@ def reduce_dataframe_footprint(df: DataFrame) -> DataFrame:
df_dtypes[column] = np.int32 df_dtypes[column] = np.int32
df = df.astype(df_dtypes) df = df.astype(df_dtypes)
logger.debug(f"Memory usage after optimization is: " logger.debug(f"Memory usage after optimization is: {df.memory_usage().sum() / 1024**2:.2f} MB")
f"{df.memory_usage().sum() / 1024**2:.2f} MB")
return df return df

Some files were not shown because too many files have changed in this diff Show More