mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge branch 'develop' into v3_fixes
This commit is contained in:
commit
afd3a32883
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [xmatthias]
|
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -9,7 +9,7 @@ assignees: ''
|
||||||
<!--
|
<!--
|
||||||
Have you searched for similar issues before posting it?
|
Have you searched for similar issues before posting it?
|
||||||
|
|
||||||
If you have discovered a bug in the bot, please [search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue).
|
If you have discovered a bug in the bot, please [search the issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue).
|
||||||
If it hasn't been reported, please create a new issue.
|
If it hasn't been reported, please create a new issue.
|
||||||
|
|
||||||
Please do not use bug reports to request new features.
|
Please do not use bug reports to request new features.
|
||||||
|
|
2
.github/ISSUE_TEMPLATE/question.md
vendored
2
.github/ISSUE_TEMPLATE/question.md
vendored
|
@ -22,4 +22,4 @@ Please do not use the question template to report bugs or to request new feature
|
||||||
|
|
||||||
## Your question
|
## Your question
|
||||||
|
|
||||||
*Ask the question you have not been able to find an answer in our [Documentation](https://www.freqtrade.io/en/latest/)*
|
*Ask the question you have not been able to find an answer in the [Documentation](https://www.freqtrade.io/en/latest/)*
|
||||||
|
|
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
|
@ -5,9 +5,17 @@ updates:
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
- package-ecosystem: pip
|
- package-ecosystem: pip
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
target-branch: develop
|
target-branch: develop
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
target-branch: develop
|
||||||
|
|
135
.github/workflows/ci.yml
vendored
135
.github/workflows/ci.yml
vendored
|
@ -3,9 +3,9 @@ name: Freqtrade CI
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
|
||||||
- stable
|
- stable
|
||||||
- develop
|
- develop
|
||||||
|
- ci/*
|
||||||
tags:
|
tags:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
@ -20,26 +20,26 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-18.04, ubuntu-20.04 ]
|
os: [ ubuntu-18.04, ubuntu-20.04 ]
|
||||||
python-version: [3.7, 3.8, 3.9]
|
python-version: ["3.8", "3.9", "3.10"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Cache_dependencies
|
- name: Cache_dependencies
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: ~/dependencies/
|
path: ~/dependencies/
|
||||||
key: ${{ runner.os }}-dependencies
|
key: ${{ runner.os }}-dependencies
|
||||||
|
|
||||||
- name: pip cache (linux)
|
- name: pip cache (linux)
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
if: startsWith(matrix.os, 'ubuntu')
|
if: runner.os == 'Linux'
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||||
|
@ -50,8 +50,9 @@ jobs:
|
||||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||||
|
|
||||||
- name: Installation - *nix
|
- name: Installation - *nix
|
||||||
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
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
|
||||||
|
@ -69,7 +70,7 @@ jobs:
|
||||||
if: matrix.python-version == '3.9'
|
if: matrix.python-version == '3.9'
|
||||||
|
|
||||||
- name: Coveralls
|
- name: Coveralls
|
||||||
if: (startsWith(matrix.os, 'ubuntu-20') && matrix.python-version == '3.8')
|
if: (runner.os == 'Linux' && matrix.python-version == '3.8')
|
||||||
env:
|
env:
|
||||||
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
||||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||||
|
@ -101,42 +102,39 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
mypy freqtrade scripts
|
mypy freqtrade scripts
|
||||||
|
|
||||||
- name: Slack Notification
|
- name: Discord notification
|
||||||
uses: lazy-actions/slatify@v3.0.0
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
type: ${{ job.status }}
|
severity: error
|
||||||
job_name: '*Freqtrade CI ${{ matrix.os }}*'
|
details: Freqtrade CI failed on ${{ matrix.os }}
|
||||||
mention: 'here'
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
mention_if: 'failure'
|
|
||||||
channel: '#notifications'
|
|
||||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
|
||||||
|
|
||||||
build_macos:
|
build_macos:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos-latest ]
|
os: [ macos-latest ]
|
||||||
python-version: [3.7, 3.8, 3.9]
|
python-version: ["3.8", "3.9", "3.10"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Cache_dependencies
|
- name: Cache_dependencies
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: ~/dependencies/
|
path: ~/dependencies/
|
||||||
key: ${{ runner.os }}-dependencies
|
key: ${{ runner.os }}-dependencies
|
||||||
|
|
||||||
- name: pip cache (macOS)
|
- name: pip cache (macOS)
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
if: startsWith(matrix.os, 'macOS')
|
if: runner.os == 'macOS'
|
||||||
with:
|
with:
|
||||||
path: ~/Library/Caches/pip
|
path: ~/Library/Caches/pip
|
||||||
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||||
|
@ -147,10 +145,11 @@ jobs:
|
||||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||||
|
|
||||||
- name: Installation - macOS
|
- name: Installation - macOS
|
||||||
|
if: runner.os == 'macOS'
|
||||||
run: |
|
run: |
|
||||||
brew update
|
brew update
|
||||||
brew install hdf5 c-blosc
|
brew install hdf5 c-blosc
|
||||||
python -m pip install --upgrade pip
|
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
|
||||||
|
@ -162,7 +161,7 @@ jobs:
|
||||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
||||||
|
|
||||||
- name: Coveralls
|
- name: Coveralls
|
||||||
if: (startsWith(matrix.os, 'ubuntu-20') && matrix.python-version == '3.8')
|
if: (runner.os == 'Linux' && matrix.python-version == '3.8')
|
||||||
env:
|
env:
|
||||||
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
||||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||||
|
@ -194,17 +193,13 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
mypy freqtrade scripts
|
mypy freqtrade scripts
|
||||||
|
|
||||||
- name: Slack Notification
|
- name: Discord notification
|
||||||
uses: lazy-actions/slatify@v3.0.0
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
type: ${{ job.status }}
|
severity: info
|
||||||
job_name: '*Freqtrade CI ${{ matrix.os }}*'
|
details: Test Succeeded!
|
||||||
mention: 'here'
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
mention_if: 'failure'
|
|
||||||
channel: '#notifications'
|
|
||||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
|
||||||
|
|
||||||
|
|
||||||
build_windows:
|
build_windows:
|
||||||
|
|
||||||
|
@ -212,19 +207,18 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ windows-latest ]
|
os: [ windows-latest ]
|
||||||
python-version: [3.7, 3.8]
|
python-version: ["3.8", "3.9", "3.10"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Pip cache (Windows)
|
- name: Pip cache (Windows)
|
||||||
uses: actions/cache@preview
|
uses: actions/cache@v3
|
||||||
if: startsWith(runner.os, 'Windows')
|
|
||||||
with:
|
with:
|
||||||
path: ~\AppData\Local\pip\Cache
|
path: ~\AppData\Local\pip\Cache
|
||||||
key: ${{ matrix.os }}-${{ matrix.python-version }}-pip
|
key: ${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||||
|
@ -257,28 +251,25 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
mypy freqtrade scripts
|
mypy freqtrade scripts
|
||||||
|
|
||||||
- name: Slack Notification
|
- name: Discord notification
|
||||||
uses: lazy-actions/slatify@v3.0.0
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
type: ${{ job.status }}
|
severity: error
|
||||||
job_name: '*Freqtrade CI windows*'
|
details: Test Failed
|
||||||
mention: 'here'
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
mention_if: 'failure'
|
|
||||||
channel: '#notifications'
|
|
||||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
|
||||||
|
|
||||||
docs_check:
|
docs_check:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Documentation syntax
|
- name: Documentation syntax
|
||||||
run: |
|
run: |
|
||||||
./tests/test_docs.sh
|
./tests/test_docs.sh
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
|
|
||||||
|
@ -288,14 +279,13 @@ jobs:
|
||||||
pip install mkdocs
|
pip install mkdocs
|
||||||
mkdocs build
|
mkdocs build
|
||||||
|
|
||||||
- name: Slack Notification
|
- name: Discord notification
|
||||||
uses: lazy-actions/slatify@v3.0.0
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
type: ${{ job.status }}
|
severity: error
|
||||||
job_name: '*Freqtrade Docs*'
|
details: Freqtrade doc test failed!
|
||||||
channel: '#notifications'
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
|
||||||
|
|
||||||
cleanup-prior-runs:
|
cleanup-prior-runs:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
@ -306,7 +296,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
|
||||||
# Notify on slack only once - when CI completes (and after deploy) in case it's successfull
|
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
||||||
notify-complete:
|
notify-complete:
|
||||||
needs: [ build_linux, build_macos, build_windows, docs_check ]
|
needs: [ build_linux, build_macos, build_windows, docs_check ]
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
@ -320,14 +310,13 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Slack Notification
|
- name: Discord notification
|
||||||
uses: lazy-actions/slatify@v3.0.0
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
if: always() && steps.check.outputs.has-permission && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: always() && steps.check.outputs.has-permission && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
type: ${{ job.status }}
|
severity: info
|
||||||
job_name: '*Freqtrade CI*'
|
details: Test Completed!
|
||||||
channel: '#notifications'
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: [ build_linux, build_macos, build_windows, docs_check ]
|
needs: [ build_linux, build_macos, build_windows, docs_check ]
|
||||||
|
@ -336,10 +325,10 @@ jobs:
|
||||||
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'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
|
|
||||||
|
@ -385,7 +374,7 @@ jobs:
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: crazy-max/ghaction-docker-buildx@v1
|
uses: crazy-max/ghaction-docker-buildx@v3.3.1
|
||||||
with:
|
with:
|
||||||
buildx-version: latest
|
buildx-version: latest
|
||||||
qemu-version: latest
|
qemu-version: latest
|
||||||
|
@ -400,17 +389,13 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
build_helpers/publish_docker_multi.sh
|
build_helpers/publish_docker_multi.sh
|
||||||
|
|
||||||
|
- name: Discord notification
|
||||||
- name: Slack Notification
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
uses: lazy-actions/slatify@v3.0.0
|
|
||||||
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
type: ${{ job.status }}
|
severity: info
|
||||||
job_name: '*Freqtrade CI Deploy*'
|
details: Deploy Succeeded!
|
||||||
mention: 'here'
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
mention_if: 'failure'
|
|
||||||
channel: '#notifications'
|
|
||||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
|
||||||
|
|
||||||
|
|
||||||
deploy_arm:
|
deploy_arm:
|
||||||
|
@ -420,7 +405,7 @@ jobs:
|
||||||
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'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Extract branch name
|
- name: Extract branch name
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
4
.github/workflows/docker_update_readme.yml
vendored
4
.github/workflows/docker_update_readme.yml
vendored
|
@ -8,9 +8,9 @@ jobs:
|
||||||
dockerHubDescription:
|
dockerHubDescription:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v3
|
||||||
- name: Docker Hub Description
|
- name: Docker Hub Description
|
||||||
uses: peter-evans/dockerhub-description@v2.1.0
|
uses: peter-evans/dockerhub-description@v3
|
||||||
env:
|
env:
|
||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,6 +1,8 @@
|
||||||
# Freqtrade rules
|
# Freqtrade rules
|
||||||
config*.json
|
config*.json
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
*.sqlite-shm
|
||||||
|
*.sqlite-wal
|
||||||
logfile.txt
|
logfile.txt
|
||||||
user_data/*
|
user_data/*
|
||||||
!user_data/strategy/sample_strategy.py
|
!user_data/strategy/sample_strategy.py
|
||||||
|
@ -10,6 +12,9 @@ freqtrade-plot.html
|
||||||
freqtrade-profit-plot.html
|
freqtrade-profit-plot.html
|
||||||
freqtrade/rpc/api_server/ui/*
|
freqtrade/rpc/api_server/ui/*
|
||||||
|
|
||||||
|
# Macos related
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|
21
.pre-commit-config.yaml
Normal file
21
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pycqa/flake8
|
||||||
|
rev: '4.0.1'
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
# stages: [push]
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
|
rev: 'v0.942'
|
||||||
|
hooks:
|
||||||
|
- id: mypy
|
||||||
|
# stages: [push]
|
||||||
|
|
||||||
|
- repo: https://github.com/pycqa/isort
|
||||||
|
rev: '5.10.1'
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
name: isort (python)
|
||||||
|
# stages: [push]
|
55
.travis.yml
55
.travis.yml
|
@ -1,55 +0,0 @@
|
||||||
os:
|
|
||||||
- linux
|
|
||||||
dist: bionic
|
|
||||||
language: python
|
|
||||||
python:
|
|
||||||
- 3.8
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
- IMAGE_NAME=freqtradeorg/freqtrade
|
|
||||||
install:
|
|
||||||
- cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies; cd ..
|
|
||||||
- export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
|
||||||
- export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
|
||||||
- export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
|
||||||
- pip install -r requirements-dev.txt
|
|
||||||
- pip install -e .
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
include:
|
|
||||||
- stage: tests
|
|
||||||
script:
|
|
||||||
- pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
|
||||||
# Allow failure for coveralls
|
|
||||||
# - coveralls || true
|
|
||||||
name: pytest
|
|
||||||
- script:
|
|
||||||
- cp config_examples/config_bittrex.example.json config.json
|
|
||||||
- freqtrade create-userdir --userdir user_data
|
|
||||||
- freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
|
|
||||||
name: backtest
|
|
||||||
- script:
|
|
||||||
- cp config_examples/config_bittrex.example.json config.json
|
|
||||||
- freqtrade create-userdir --userdir user_data
|
|
||||||
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily
|
|
||||||
name: hyperopt
|
|
||||||
- script: flake8
|
|
||||||
name: flake8
|
|
||||||
- script:
|
|
||||||
# Test Documentation boxes -
|
|
||||||
# !!! <TYPE>: is not allowed!
|
|
||||||
# !!! <TYPE> "title" - Title needs to be quoted!
|
|
||||||
- grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]' docs/*; test $? -ne 0
|
|
||||||
name: doc syntax
|
|
||||||
- script: mypy freqtrade scripts
|
|
||||||
name: mypy
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
slack:
|
|
||||||
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=
|
|
||||||
cache:
|
|
||||||
pip: True
|
|
||||||
directories:
|
|
||||||
- $HOME/dependencies
|
|
|
@ -56,6 +56,13 @@ To help with that, we encourage you to install the git pre-commit
|
||||||
hook that will warn you when you try to commit code that fails these checks.
|
hook that will warn you when you try to commit code that fails these checks.
|
||||||
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
|
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
|
||||||
|
|
||||||
|
##### Additional styles applied
|
||||||
|
|
||||||
|
* Have docstrings on all public methods
|
||||||
|
* Use double-quotes for docstrings
|
||||||
|
* Multiline docstrings should be indented to the level of the first quote
|
||||||
|
* Doc-strings should follow the reST format (`:param xxx: ...`, `:return: ...`, `:raises KeyError: ... `)
|
||||||
|
|
||||||
### 3. Test if all type-hints are correct
|
### 3. Test if all type-hints are correct
|
||||||
|
|
||||||
#### Run mypy
|
#### Run mypy
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM python:3.9.7-slim-buster as base
|
FROM python:3.9.9-slim-bullseye as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
|
|
@ -2,5 +2,6 @@ include LICENSE
|
||||||
include README.md
|
include README.md
|
||||||
recursive-include freqtrade *.py
|
recursive-include freqtrade *.py
|
||||||
recursive-include freqtrade/templates/ *.j2 *.ipynb
|
recursive-include freqtrade/templates/ *.j2 *.ipynb
|
||||||
|
include freqtrade/exchange/binance_leverage_tiers.json
|
||||||
include freqtrade/rpc/api_server/ui/fallback_file.html
|
include freqtrade/rpc/api_server/ui/fallback_file.html
|
||||||
include freqtrade/rpc/api_server/ui/favicon.ico
|
include freqtrade/rpc/api_server/ui/favicon.ico
|
||||||
|
|
51
README.md
51
README.md
|
@ -5,10 +5,14 @@
|
||||||
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
|
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
|
||||||
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||||
|
|
||||||
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
|
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
|
||||||
|
|
||||||
![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
|
![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
|
||||||
|
|
||||||
|
## Sponsored promotion
|
||||||
|
|
||||||
|
[![tokenbot-promo](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/TokenBot-Freqtrade-banner.png)](https://tokenbot.com/?utm_source=github&utm_medium=freqtrade&utm_campaign=algodevs)
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
This software is for educational purposes only. Do not risk money which
|
This software is for educational purposes only. Do not risk money which
|
||||||
|
@ -26,11 +30,13 @@ hesitate to read the source code and understand the mechanism of this bot.
|
||||||
|
|
||||||
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [FTX](https://ftx.com/#a=2258149)
|
||||||
- [X] [FTX](https://ftx.com)
|
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
|
- [X] [Huobi](http://huobi.com/)
|
||||||
|
- [X] [Kraken](https://kraken.com/)
|
||||||
|
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
|
@ -44,34 +50,28 @@ Exchanges confirmed working by the community:
|
||||||
|
|
||||||
We invite you to read the bot documentation to ensure you understand how the bot is working.
|
We invite you to read the bot documentation to ensure you understand how the bot is working.
|
||||||
|
|
||||||
Please find the complete documentation on our [website](https://www.freqtrade.io).
|
Please find the complete documentation on the [freqtrade website](https://www.freqtrade.io).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [x] **Based on Python 3.7+**: For botting on any operating system - Windows, macOS and Linux.
|
- [x] **Based on Python 3.8+**: For botting on any operating system - Windows, macOS and Linux.
|
||||||
- [x] **Persistence**: Persistence is achieved through sqlite.
|
- [x] **Persistence**: Persistence is achieved through sqlite.
|
||||||
- [x] **Dry-run**: Run the bot without paying money.
|
- [x] **Dry-run**: Run the bot without paying money.
|
||||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||||
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
|
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
|
||||||
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/latest/edge/).
|
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/).
|
||||||
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists.
|
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists.
|
||||||
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
||||||
|
- [x] **Builtin WebUI**: Builtin web UI to manage your bot.
|
||||||
- [x] **Manageable via Telegram**: Manage the bot with Telegram.
|
- [x] **Manageable via Telegram**: Manage the bot with Telegram.
|
||||||
- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat.
|
- [x] **Display profit/loss in fiat**: Display your profit/loss in fiat currency.
|
||||||
- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss.
|
|
||||||
- [x] **Performance status report**: Provide a performance status of your current trades.
|
- [x] **Performance status report**: Provide a performance status of your current trades.
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
|
Please refer to the [Docker Quickstart documentation](https://www.freqtrade.io/en/stable/docker_quickstart/) on how to get started quickly.
|
||||||
|
|
||||||
```bash
|
For further (native) installation methods, please refer to the [Installation documentation page](https://www.freqtrade.io/en/stable/installation/).
|
||||||
git clone -b develop https://github.com/freqtrade/freqtrade.git
|
|
||||||
cd freqtrade
|
|
||||||
./setup.sh --install
|
|
||||||
```
|
|
||||||
|
|
||||||
For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/latest/installation/).
|
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
|
|
||||||
|
@ -121,14 +121,15 @@ optional arguments:
|
||||||
|
|
||||||
### Telegram RPC commands
|
### Telegram RPC commands
|
||||||
|
|
||||||
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
|
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on the [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
|
||||||
|
|
||||||
- `/start`: Starts the trader.
|
- `/start`: Starts the trader.
|
||||||
- `/stop`: Stops the trader.
|
- `/stop`: Stops the trader.
|
||||||
- `/stopbuy`: Stop entering new trades.
|
- `/stopbuy`: Stop entering new trades.
|
||||||
- `/status <trade_id>|[table]`: Lists all or specific open trades.
|
- `/status <trade_id>|[table]`: Lists all or specific open trades.
|
||||||
- `/profit [<n>]`: Lists cumulative profit from all finished trades, over the last n days.
|
- `/profit [<n>]`: Lists cumulative profit from all finished trades, over the last n days.
|
||||||
- `/forcesell <trade_id>|all`: Instantly sells the given trade (Ignoring `minimum_roi`).
|
- `/forceexit <trade_id>|all`: Instantly exits the given trade (Ignoring `minimum_roi`).
|
||||||
|
- `/fx <trade_id>|all`: Alias to `/forceexit`
|
||||||
- `/performance`: Show performance of each finished trade grouped by pair
|
- `/performance`: Show performance of each finished trade grouped by pair
|
||||||
- `/balance`: Show account balance per currency.
|
- `/balance`: Show account balance per currency.
|
||||||
- `/daily <n>`: Shows profit or loss per day, over the last n days.
|
- `/daily <n>`: Shows profit or loss per day, over the last n days.
|
||||||
|
@ -152,10 +153,10 @@ For any questions not covered by the documentation or for further information ab
|
||||||
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||||
|
|
||||||
If you discover a bug in the bot, please
|
If you discover a bug in the bot, please
|
||||||
[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
[search the issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||||
first. If it hasn't been reported, please
|
first. If it hasn't been reported, please
|
||||||
[create a new issue](https://github.com/freqtrade/freqtrade/issues/new/choose) and
|
[create a new issue](https://github.com/freqtrade/freqtrade/issues/new/choose) and
|
||||||
ensure you follow the template guide so that our team can assist you as
|
ensure you follow the template guide so that the team can assist you as
|
||||||
quickly as possible.
|
quickly as possible.
|
||||||
|
|
||||||
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
|
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
|
||||||
|
@ -169,13 +170,13 @@ in the bug reports.
|
||||||
|
|
||||||
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
|
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
|
||||||
|
|
||||||
Feel like our bot is missing a feature? We welcome your pull requests!
|
Feel like the bot is missing a feature? We welcome your pull requests!
|
||||||
|
|
||||||
Please read our
|
Please read the
|
||||||
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
to understand the requirements before sending your pull-requests.
|
to understand the requirements before sending your pull-requests.
|
||||||
|
|
||||||
Coding is not a necessity to contribute - maybe start with improving our documentation?
|
Coding is not a necessity to contribute - maybe start with improving the documentation?
|
||||||
Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase.
|
Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase.
|
||||||
|
|
||||||
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/p7nuUNVfP7) (please use the #dev channel for this). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/p7nuUNVfP7) (please use the #dev channel for this). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
||||||
|
@ -196,7 +197,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
||||||
|
|
||||||
### Software requirements
|
### Software requirements
|
||||||
|
|
||||||
- [Python 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
- [Python >= 3.8](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
- [pip](https://pip.pypa.io/en/stable/installing/)
|
- [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.24-cp310-cp310-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.24-cp310-cp310-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.24-cp38-cp38-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.24-cp38-cp38-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.24-cp39-cp39-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.24-cp39-cp39-win_amd64.whl
Normal file
Binary file not shown.
|
@ -11,8 +11,13 @@ if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
|
||||||
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \
|
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \
|
||||||
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
|
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
|
||||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||||
&& make -j$(nproc) \
|
&& make
|
||||||
&& which sudo && sudo make install || make install
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed building ta-lib."
|
||||||
|
cd .. && rm -rf ./ta-lib/
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
which sudo && sudo make install || make install
|
||||||
if [ -x "$(command -v apt-get)" ]; then
|
if [ -x "$(command -v apt-get)" ]; then
|
||||||
echo "Updating library path using ldconfig"
|
echo "Updating library path using ldconfig"
|
||||||
sudo ldconfig
|
sudo ldconfig
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
# Downloads don't work automatically, since the URL is regenerated via javascript.
|
# Downloads don't work automatically, since the URL is regenerated via javascript.
|
||||||
# Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib
|
# Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib
|
||||||
|
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip wheel
|
||||||
|
|
||||||
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
||||||
|
|
||||||
if ($pyv -eq '3.7') {
|
|
||||||
pip install build_helpers\TA_Lib-0.4.21-cp37-cp37m-win_amd64.whl
|
|
||||||
}
|
|
||||||
if ($pyv -eq '3.8') {
|
if ($pyv -eq '3.8') {
|
||||||
pip install build_helpers\TA_Lib-0.4.21-cp38-cp38-win_amd64.whl
|
pip install build_helpers\TA_Lib-0.4.24-cp38-cp38-win_amd64.whl
|
||||||
}
|
}
|
||||||
if ($pyv -eq '3.9') {
|
if ($pyv -eq '3.9') {
|
||||||
pip install build_helpers\TA_Lib-0.4.21-cp39-cp39-win_amd64.whl
|
pip install build_helpers\TA_Lib-0.4.24-cp39-cp39-win_amd64.whl
|
||||||
|
}
|
||||||
|
if ($pyv -eq '3.10') {
|
||||||
|
pip install build_helpers\TA_Lib-0.4.24-cp310-cp310-win_amd64.whl
|
||||||
}
|
}
|
||||||
|
|
||||||
pip install -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
|
@ -42,7 +42,7 @@ docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_I
|
||||||
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
||||||
|
|
||||||
# Run backtest
|
# Run backtest
|
||||||
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2
|
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "failed running backtest"
|
echo "failed running backtest"
|
||||||
|
|
|
@ -53,7 +53,7 @@ docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE
|
||||||
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
|
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
|
||||||
|
|
||||||
# Run backtest
|
# Run backtest
|
||||||
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2
|
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "failed running backtest"
|
echo "failed running backtest"
|
||||||
|
|
|
@ -8,19 +8,23 @@
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"entry": 10,
|
||||||
"sell": 30
|
"exit": 10,
|
||||||
|
"exit_timeout_count": 0,
|
||||||
|
"unit": "minutes"
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"entry_pricing": {
|
||||||
"ask_last_balance": 0.0,
|
"price_side": "same",
|
||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
"order_book_top": 1,
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0,
|
||||||
"check_depth_of_market": {
|
"check_depth_of_market": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"bids_to_ask_delta": 1
|
"bids_to_ask_delta": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ask_strategy": {
|
"exit_pricing": {
|
||||||
|
"price_side": "same",
|
||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
"order_book_top": 1
|
"order_book_top": 1
|
||||||
},
|
},
|
||||||
|
@ -28,10 +32,8 @@
|
||||||
"name": "binance",
|
"name": "binance",
|
||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
"ccxt_config": {"enableRateLimit": true},
|
"ccxt_config": {},
|
||||||
"ccxt_async_config": {
|
"ccxt_async_config": {
|
||||||
"enableRateLimit": true,
|
|
||||||
"rateLimit": 200
|
|
||||||
},
|
},
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"ALGO/BTC",
|
"ALGO/BTC",
|
||||||
|
@ -88,7 +90,7 @@
|
||||||
},
|
},
|
||||||
"bot_name": "freqtrade",
|
"bot_name": "freqtrade",
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"forcebuy_enable": false,
|
"force_enter_enable": false,
|
||||||
"internals": {
|
"internals": {
|
||||||
"process_throttle_secs": 5
|
"process_throttle_secs": 5
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,19 +8,23 @@
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"entry": 10,
|
||||||
"sell": 30
|
"exit": 10,
|
||||||
|
"exit_timeout_count": 0,
|
||||||
|
"unit": "minutes"
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"entry_pricing": {
|
||||||
|
"price_side": "same",
|
||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
"ask_last_balance": 0.0,
|
|
||||||
"order_book_top": 1,
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0,
|
||||||
"check_depth_of_market": {
|
"check_depth_of_market": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"bids_to_ask_delta": 1
|
"bids_to_ask_delta": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ask_strategy":{
|
"exit_pricing":{
|
||||||
|
"price_side": "same",
|
||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
"order_book_top": 1
|
"order_book_top": 1
|
||||||
},
|
},
|
||||||
|
@ -83,7 +87,7 @@
|
||||||
},
|
},
|
||||||
"bot_name": "freqtrade",
|
"bot_name": "freqtrade",
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"forcebuy_enable": false,
|
"force_entry_enable": false,
|
||||||
"internals": {
|
"internals": {
|
||||||
"process_throttle_secs": 5
|
"process_throttle_secs": 5
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,19 +8,23 @@
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"entry": 10,
|
||||||
"sell": 30
|
"exit": 10,
|
||||||
|
"exit_timeout_count": 0,
|
||||||
|
"unit": "minutes"
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"entry_pricing": {
|
||||||
"ask_last_balance": 0.0,
|
"price_side": "same",
|
||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
"order_book_top": 1,
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0,
|
||||||
"check_depth_of_market": {
|
"check_depth_of_market": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"bids_to_ask_delta": 1
|
"bids_to_ask_delta": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ask_strategy": {
|
"exit_pricing": {
|
||||||
|
"price_side": "same",
|
||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
"order_book_top": 1
|
"order_book_top": 1
|
||||||
},
|
},
|
||||||
|
@ -28,11 +32,8 @@
|
||||||
"name": "ftx",
|
"name": "ftx",
|
||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
"ccxt_config": {"enableRateLimit": true},
|
"ccxt_config": {},
|
||||||
"ccxt_async_config": {
|
"ccxt_async_config": {},
|
||||||
"enableRateLimit": true,
|
|
||||||
"rateLimit": 50
|
|
||||||
},
|
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"BTC/USD",
|
"BTC/USD",
|
||||||
"ETH/USD",
|
"ETH/USD",
|
||||||
|
@ -88,7 +89,7 @@
|
||||||
},
|
},
|
||||||
"bot_name": "freqtrade",
|
"bot_name": "freqtrade",
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"forcebuy_enable": false,
|
"force_entry_enable": false,
|
||||||
"internals": {
|
"internals": {
|
||||||
"process_throttle_secs": 5
|
"process_throttle_secs": 5
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,16 +8,20 @@
|
||||||
"amend_last_stake_amount": false,
|
"amend_last_stake_amount": false,
|
||||||
"last_stake_amount_min_ratio": 0.5,
|
"last_stake_amount_min_ratio": 0.5,
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
|
"dry_run_wallet": 1000,
|
||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"timeframe": "5m",
|
"timeframe": "5m",
|
||||||
"trailing_stop": false,
|
"trailing_stop": false,
|
||||||
"trailing_stop_positive": 0.005,
|
"trailing_stop_positive": 0.005,
|
||||||
"trailing_stop_positive_offset": 0.0051,
|
"trailing_stop_positive_offset": 0.0051,
|
||||||
"trailing_only_offset_is_reached": false,
|
"trailing_only_offset_is_reached": false,
|
||||||
"use_sell_signal": true,
|
"use_exit_signal": true,
|
||||||
"sell_profit_only": false,
|
"exit_profit_only": false,
|
||||||
"sell_profit_offset": 0.0,
|
"exit_profit_offset": 0.0,
|
||||||
"ignore_roi_if_buy_signal": false,
|
"ignore_roi_if_entry_signal": false,
|
||||||
|
"ignore_buying_expired_candle_after": 300,
|
||||||
|
"trading_mode": "spot",
|
||||||
|
"margin_mode": "",
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
"30": 0.01,
|
"30": 0.01,
|
||||||
|
@ -26,38 +30,41 @@
|
||||||
},
|
},
|
||||||
"stoploss": -0.10,
|
"stoploss": -0.10,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"entry": 10,
|
||||||
"sell": 30,
|
"exit": 10,
|
||||||
|
"exit_timeout_count": 0,
|
||||||
"unit": "minutes"
|
"unit": "minutes"
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"entry_pricing": {
|
||||||
"price_side": "bid",
|
"price_side": "same",
|
||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
"ask_last_balance": 0.0,
|
|
||||||
"order_book_top": 1,
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0,
|
||||||
"check_depth_of_market": {
|
"check_depth_of_market": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"bids_to_ask_delta": 1
|
"bids_to_ask_delta": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ask_strategy":{
|
"exit_pricing":{
|
||||||
"price_side": "ask",
|
"price_side": "same",
|
||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
"order_book_top": 1
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0
|
||||||
},
|
},
|
||||||
"order_types": {
|
"order_types": {
|
||||||
"buy": "limit",
|
"entry": "limit",
|
||||||
"sell": "limit",
|
"exit": "limit",
|
||||||
"emergencysell": "market",
|
"emergency_exit": "market",
|
||||||
"forcesell": "market",
|
"force_exit": "market",
|
||||||
"forcebuy": "market",
|
"force_entry": "market",
|
||||||
"stoploss": "market",
|
"stoploss": "market",
|
||||||
"stoploss_on_exchange": false,
|
"stoploss_on_exchange": false,
|
||||||
"stoploss_on_exchange_interval": 60
|
"stoploss_on_exchange_interval": 60,
|
||||||
|
"stoploss_on_exchange_limit_ratio": 0.99
|
||||||
},
|
},
|
||||||
"order_time_in_force": {
|
"order_time_in_force": {
|
||||||
"buy": "gtc",
|
"entry": "gtc",
|
||||||
"sell": "gtc"
|
"exit": "gtc"
|
||||||
},
|
},
|
||||||
"pairlists": [
|
"pairlists": [
|
||||||
{"method": "StaticPairList"},
|
{"method": "StaticPairList"},
|
||||||
|
@ -84,12 +91,9 @@
|
||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
"password": "",
|
"password": "",
|
||||||
"ccxt_config": {"enableRateLimit": true},
|
"log_responses": false,
|
||||||
"ccxt_async_config": {
|
"ccxt_config": {},
|
||||||
"enableRateLimit": true,
|
"ccxt_async_config": {},
|
||||||
"rateLimit": 500,
|
|
||||||
"aiohttp_trust_env": false
|
|
||||||
},
|
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"ALGO/BTC",
|
"ALGO/BTC",
|
||||||
"ATOM/BTC",
|
"ATOM/BTC",
|
||||||
|
@ -135,21 +139,21 @@
|
||||||
"status": "on",
|
"status": "on",
|
||||||
"warning": "on",
|
"warning": "on",
|
||||||
"startup": "on",
|
"startup": "on",
|
||||||
"buy": "on",
|
"entry": "on",
|
||||||
"buy_fill": "on",
|
"entry_fill": "on",
|
||||||
"sell": {
|
"exit": {
|
||||||
"roi": "off",
|
"roi": "off",
|
||||||
"emergency_sell": "off",
|
"emergency_exit": "off",
|
||||||
"force_sell": "off",
|
"force_exit": "off",
|
||||||
"sell_signal": "off",
|
"exit_signal": "off",
|
||||||
"trailing_stop_loss": "off",
|
"trailing_stop_loss": "off",
|
||||||
"stop_loss": "off",
|
"stop_loss": "off",
|
||||||
"stoploss_on_exchange": "off",
|
"stoploss_on_exchange": "off",
|
||||||
"custom_sell": "off"
|
"custom_exit": "off"
|
||||||
},
|
},
|
||||||
"sell_fill": "on",
|
"exit_fill": "on",
|
||||||
"buy_cancel": "on",
|
"entry_cancel": "on",
|
||||||
"sell_cancel": "on",
|
"exit_cancel": "on",
|
||||||
"protection_trigger": "off",
|
"protection_trigger": "off",
|
||||||
"protection_trigger_global": "on"
|
"protection_trigger_global": "on"
|
||||||
},
|
},
|
||||||
|
@ -170,7 +174,7 @@
|
||||||
"bot_name": "freqtrade",
|
"bot_name": "freqtrade",
|
||||||
"db_url": "sqlite:///tradesv3.sqlite",
|
"db_url": "sqlite:///tradesv3.sqlite",
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"forcebuy_enable": false,
|
"force_entry_enable": false,
|
||||||
"internals": {
|
"internals": {
|
||||||
"process_throttle_secs": 5,
|
"process_throttle_secs": 5,
|
||||||
"heartbeat_interval": 60
|
"heartbeat_interval": 60
|
||||||
|
@ -178,6 +182,7 @@
|
||||||
"disable_dataframe_checks": false,
|
"disable_dataframe_checks": false,
|
||||||
"strategy": "SampleStrategy",
|
"strategy": "SampleStrategy",
|
||||||
"strategy_path": "user_data/strategies/",
|
"strategy_path": "user_data/strategies/",
|
||||||
|
"add_config_files": [],
|
||||||
"dataformat_ohlcv": "json",
|
"dataformat_ohlcv": "json",
|
||||||
"dataformat_trades": "jsongz"
|
"dataformat_trades": "jsongz"
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,19 +8,23 @@
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"entry": 10,
|
||||||
"sell": 30
|
"exit": 10,
|
||||||
|
"exit_timeout_count": 0,
|
||||||
|
"unit": "minutes"
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"entry_pricing": {
|
||||||
|
"price_side": "same",
|
||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
"ask_last_balance": 0.0,
|
|
||||||
"order_book_top": 1,
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0,
|
||||||
"check_depth_of_market": {
|
"check_depth_of_market": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"bids_to_ask_delta": 1
|
"bids_to_ask_delta": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ask_strategy":{
|
"exit_pricing":{
|
||||||
|
"price_side": "same",
|
||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
"order_book_top": 1
|
"order_book_top": 1
|
||||||
},
|
},
|
||||||
|
@ -28,10 +32,8 @@
|
||||||
"name": "kraken",
|
"name": "kraken",
|
||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_key",
|
"secret": "your_exchange_key",
|
||||||
"ccxt_config": {"enableRateLimit": true},
|
"ccxt_config": {},
|
||||||
"ccxt_async_config": {
|
"ccxt_async_config": {
|
||||||
"enableRateLimit": true,
|
|
||||||
"rateLimit": 1000
|
|
||||||
},
|
},
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"ADA/EUR",
|
"ADA/EUR",
|
||||||
|
@ -93,7 +95,7 @@
|
||||||
},
|
},
|
||||||
"bot_name": "freqtrade",
|
"bot_name": "freqtrade",
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"forcebuy_enable": false,
|
"force_entry_enable": false,
|
||||||
"internals": {
|
"internals": {
|
||||||
"process_throttle_secs": 5
|
"process_throttle_secs": 5
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,10 +15,10 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- "./user_data:/freqtrade/user_data"
|
- "./user_data:/freqtrade/user_data"
|
||||||
# Expose api on port 8080 (localhost only)
|
# Expose api on port 8080 (localhost only)
|
||||||
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
|
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
|
||||||
# before enabling this.
|
# before enabling this.
|
||||||
# ports:
|
ports:
|
||||||
# - "127.0.0.1:8080:8080"
|
- "127.0.0.1:8080:8080"
|
||||||
# Default command used when running `docker compose up`
|
# Default command used when running `docker compose up`
|
||||||
command: >
|
command: >
|
||||||
trade
|
trade
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM python:3.7.10-slim-buster as base
|
FROM python:3.9.9-slim-bullseye as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
|
|
@ -13,7 +13,7 @@ A sample of this can be found below, which is identical to the Default Hyperopt
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ Currently, the arguments are:
|
||||||
|
|
||||||
* `results`: DataFrame containing the resulting trades.
|
* `results`: DataFrame containing the resulting trades.
|
||||||
The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`):
|
The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`):
|
||||||
`pair, profit_ratio, profit_abs, open_date, open_rate, fee_open, close_date, close_rate, fee_close, amount, trade_duration, is_open, sell_reason, stake_amount, min_rate, max_rate, stop_loss_ratio, stop_loss_abs`
|
`pair, profit_ratio, profit_abs, open_date, open_rate, fee_open, close_date, close_rate, fee_close, amount, trade_duration, is_open, exit_reason, stake_amount, min_rate, max_rate, stop_loss_ratio, stop_loss_abs`
|
||||||
* `trade_count`: Amount of trades (identical to `len(results)`)
|
* `trade_count`: Amount of trades (identical to `len(results)`)
|
||||||
* `min_date`: Start date of the timerange used
|
* `min_date`: Start date of the timerange used
|
||||||
* `min_date`: End date of the timerange used
|
* `min_date`: End date of the timerange used
|
||||||
|
@ -105,7 +105,7 @@ You can define your own estimator for Hyperopt by implementing `generate_estimat
|
||||||
```python
|
```python
|
||||||
class MyAwesomeStrategy(IStrategy):
|
class MyAwesomeStrategy(IStrategy):
|
||||||
class HyperOpt:
|
class HyperOpt:
|
||||||
def generate_estimator():
|
def generate_estimator(dimensions: List['Dimension'], **kwargs):
|
||||||
return "RF"
|
return "RF"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -119,13 +119,34 @@ Example for `ExtraTreesRegressor` ("ET") with additional parameters:
|
||||||
```python
|
```python
|
||||||
class MyAwesomeStrategy(IStrategy):
|
class MyAwesomeStrategy(IStrategy):
|
||||||
class HyperOpt:
|
class HyperOpt:
|
||||||
def generate_estimator():
|
def generate_estimator(dimensions: List['Dimension'], **kwargs):
|
||||||
from skopt.learning import ExtraTreesRegressor
|
from skopt.learning import ExtraTreesRegressor
|
||||||
# Corresponds to "ET" - but allows additional parameters.
|
# Corresponds to "ET" - but allows additional parameters.
|
||||||
return ExtraTreesRegressor(n_estimators=100)
|
return ExtraTreesRegressor(n_estimators=100)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `dimensions` parameter is the list of `skopt.space.Dimension` objects corresponding to the parameters to be optimized. It can be used to create isotropic kernels for the `skopt.learning.GaussianProcessRegressor` estimator. Here's an example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyAwesomeStrategy(IStrategy):
|
||||||
|
class HyperOpt:
|
||||||
|
def generate_estimator(dimensions: List['Dimension'], **kwargs):
|
||||||
|
from skopt.utils import cook_estimator
|
||||||
|
from skopt.learning.gaussian_process.kernels import (Matern, ConstantKernel)
|
||||||
|
kernel_bounds = (0.0001, 10000)
|
||||||
|
kernel = (
|
||||||
|
ConstantKernel(1.0, kernel_bounds) *
|
||||||
|
Matern(length_scale=np.ones(len(dimensions)), length_scale_bounds=[kernel_bounds for d in dimensions], nu=2.5)
|
||||||
|
)
|
||||||
|
kernel += (
|
||||||
|
ConstantKernel(1.0, kernel_bounds) *
|
||||||
|
Matern(length_scale=np.ones(len(dimensions)), length_scale_bounds=[kernel_bounds for d in dimensions], nu=1.5)
|
||||||
|
)
|
||||||
|
|
||||||
|
return cook_estimator("GP", space=dimensions, kernel=kernel, n_restarts_optimizer=2)
|
||||||
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
While custom estimators can be provided, it's up to you as User to do research on possible parameters and analyze / understand which ones should be used.
|
While custom estimators can be provided, it's up to you as User to do research on possible parameters and analyze / understand which ones should be used.
|
||||||
If you're unsure about this, best use one of the Defaults (`"ET"` has proven to be the most versatile) without further parameters.
|
If you're unsure about this, best use one of the Defaults (`"ET"` has proven to be the most versatile) without further parameters.
|
||||||
|
|
|
@ -52,6 +52,71 @@ freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite:///user
|
||||||
|
|
||||||
For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the [SQL Cheatsheet](sql_cheatsheet.md).
|
For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the [SQL Cheatsheet](sql_cheatsheet.md).
|
||||||
|
|
||||||
|
### Multiple instances using docker
|
||||||
|
|
||||||
|
To run multiple instances of freqtrade using docker you will need to edit the docker-compose.yml file and add all the instances you want as separate services. Remember, you can separate your configuration into multiple files, so it's a good idea to think about making them modular, then if you need to edit something common to all bots, you can do that in a single config file.
|
||||||
|
``` yml
|
||||||
|
---
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
freqtrade1:
|
||||||
|
image: freqtradeorg/freqtrade:stable
|
||||||
|
# image: freqtradeorg/freqtrade:develop
|
||||||
|
# Use plotting image
|
||||||
|
# image: freqtradeorg/freqtrade:develop_plot
|
||||||
|
# Build step - only needed when additional dependencies are needed
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: "./docker/Dockerfile.custom"
|
||||||
|
restart: always
|
||||||
|
container_name: freqtrade1
|
||||||
|
volumes:
|
||||||
|
- "./user_data:/freqtrade/user_data"
|
||||||
|
# Expose api on port 8080 (localhost only)
|
||||||
|
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
|
||||||
|
# before enabling this.
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
|
# Default command used when running `docker compose up`
|
||||||
|
command: >
|
||||||
|
trade
|
||||||
|
--logfile /freqtrade/user_data/logs/freqtrade1.log
|
||||||
|
--db-url sqlite:////freqtrade/user_data/tradesv3_freqtrade1.sqlite
|
||||||
|
--config /freqtrade/user_data/config.json
|
||||||
|
--config /freqtrade/user_data/config.freqtrade1.json
|
||||||
|
--strategy SampleStrategy
|
||||||
|
|
||||||
|
freqtrade2:
|
||||||
|
image: freqtradeorg/freqtrade:stable
|
||||||
|
# image: freqtradeorg/freqtrade:develop
|
||||||
|
# Use plotting image
|
||||||
|
# image: freqtradeorg/freqtrade:develop_plot
|
||||||
|
# Build step - only needed when additional dependencies are needed
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: "./docker/Dockerfile.custom"
|
||||||
|
restart: always
|
||||||
|
container_name: freqtrade2
|
||||||
|
volumes:
|
||||||
|
- "./user_data:/freqtrade/user_data"
|
||||||
|
# Expose api on port 8080 (localhost only)
|
||||||
|
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
|
||||||
|
# before enabling this.
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8081:8080"
|
||||||
|
# Default command used when running `docker compose up`
|
||||||
|
command: >
|
||||||
|
trade
|
||||||
|
--logfile /freqtrade/user_data/logs/freqtrade2.log
|
||||||
|
--db-url sqlite:////freqtrade/user_data/tradesv3_freqtrade2.sqlite
|
||||||
|
--config /freqtrade/user_data/config.json
|
||||||
|
--config /freqtrade/user_data/config.freqtrade2.json
|
||||||
|
--strategy SampleStrategy
|
||||||
|
|
||||||
|
```
|
||||||
|
You can use whatever naming convention you want, freqtrade1 and 2 are arbitrary. Note, that you will need to use different database files, port mappings and telegram configurations for each instance, as mentioned above.
|
||||||
|
|
||||||
|
|
||||||
## Configure the bot running as a systemd service
|
## Configure the bot running as a systemd service
|
||||||
|
|
||||||
Copy the `freqtrade.service` file to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
Copy the `freqtrade.service` file to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
||||||
|
@ -111,12 +176,15 @@ Log messages are send to `syslog` with the `user` facility. So you can see them
|
||||||
On many systems `syslog` (`rsyslog`) fetches data from `journald` (and vice versa), so both `--logfile syslog` or `--logfile journald` can be used and the messages be viewed with both `journalctl` and a syslog viewer utility. You can combine this in any way which suites you better.
|
On many systems `syslog` (`rsyslog`) fetches data from `journald` (and vice versa), so both `--logfile syslog` or `--logfile journald` can be used and the messages be viewed with both `journalctl` and a syslog viewer utility. You can combine this in any way which suites you better.
|
||||||
|
|
||||||
For `rsyslog` the messages from the bot can be redirected into a separate dedicated log file. To achieve this, add
|
For `rsyslog` the messages from the bot can be redirected into a separate dedicated log file. To achieve this, add
|
||||||
|
|
||||||
```
|
```
|
||||||
if $programname startswith "freqtrade" then -/var/log/freqtrade.log
|
if $programname startswith "freqtrade" then -/var/log/freqtrade.log
|
||||||
```
|
```
|
||||||
|
|
||||||
to one of the rsyslog configuration files, for example at the end of the `/etc/rsyslog.d/50-default.conf`.
|
to one of the rsyslog configuration files, for example at the end of the `/etc/rsyslog.d/50-default.conf`.
|
||||||
|
|
||||||
For `syslog` (`rsyslog`), the reduction mode can be switched on. This will reduce the number of repeating messages. For instance, multiple bot Heartbeat messages will be reduced to a single message when nothing else happens with the bot. To achieve this, set in `/etc/rsyslog.conf`:
|
For `syslog` (`rsyslog`), the reduction mode can be switched on. This will reduce the number of repeating messages. For instance, multiple bot Heartbeat messages will be reduced to a single message when nothing else happens with the bot. To achieve this, set in `/etc/rsyslog.conf`:
|
||||||
|
|
||||||
```
|
```
|
||||||
# Filter duplicated messages
|
# Filter duplicated messages
|
||||||
$RepeatedMsgReduction on
|
$RepeatedMsgReduction on
|
||||||
|
|
BIN
docs/assets/TokenBot-Freqtrade-banner.png
Normal file
BIN
docs/assets/TokenBot-Freqtrade-banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
BIN
docs/assets/frequi_url.png
Normal file
BIN
docs/assets/frequi_url.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 143 KiB |
BIN
docs/assets/windows_install.png
Normal file
BIN
docs/assets/windows_install.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 92 KiB |
|
@ -21,16 +21,18 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
[--timeframe-detail TIMEFRAME_DETAIL]
|
[--timeframe-detail TIMEFRAME_DETAIL]
|
||||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||||
[--export {none,trades}] [--export-filename PATH]
|
[--export {none,trades}] [--export-filename PATH]
|
||||||
|
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||||
|
[--cache {none,day,week,month}]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
--data-format-ohlcv {json,jsongz,hdf5}
|
--data-format-ohlcv {json,jsongz,hdf5}
|
||||||
Storage format for downloaded candle (OHLCV) data.
|
Storage format for downloaded candle (OHLCV) data.
|
||||||
(default: `None`).
|
(default: `json`).
|
||||||
--max-open-trades INT
|
--max-open-trades INT
|
||||||
Override the value of the `max_open_trades`
|
Override the value of the `max_open_trades`
|
||||||
configuration setting.
|
configuration setting.
|
||||||
|
@ -61,12 +63,11 @@ optional arguments:
|
||||||
`30m`, `1h`, `1d`).
|
`30m`, `1h`, `1d`).
|
||||||
--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 ticker-interval needs to be
|
backtest. Please note that timeframe needs to be
|
||||||
set either in config or via command line. When using
|
set either in config or via command line. When using
|
||||||
this together with `--export trades`, the strategy-
|
this together with `--export trades`, the strategy-
|
||||||
name is injected into the filename (so `backtest-
|
name is injected into the filename (so `backtest-
|
||||||
data.json` becomes `backtest-data-
|
data.json` becomes `backtest-data-SampleStrategy.json`
|
||||||
SampleStrategy.json`
|
|
||||||
--export {none,trades}
|
--export {none,trades}
|
||||||
Export backtest results (default: trades).
|
Export backtest results (default: trades).
|
||||||
--export-filename PATH
|
--export-filename PATH
|
||||||
|
@ -74,6 +75,11 @@ optional arguments:
|
||||||
Requires `--export` to be set as well. Example:
|
Requires `--export` to be set as well. Example:
|
||||||
`--export-filename=user_data/backtest_results/backtest
|
`--export-filename=user_data/backtest_results/backtest
|
||||||
_today.json`
|
_today.json`
|
||||||
|
--breakdown {day,week,month} [{day,week,month} ...]
|
||||||
|
Show backtesting breakdown per [day, week, month].
|
||||||
|
--cache {none,day,week,month}
|
||||||
|
Load a cached backtest result no older than specified
|
||||||
|
age (default: day).
|
||||||
|
|
||||||
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).
|
||||||
|
@ -113,7 +119,7 @@ The result of backtesting will confirm if your bot has better odds of making a p
|
||||||
All profit calculations include fees, and freqtrade will use the exchange's default fees for the calculation.
|
All profit calculations include fees, and freqtrade will use the exchange's default fees for the calculation.
|
||||||
|
|
||||||
!!! Warning "Using dynamic pairlists for backtesting"
|
!!! Warning "Using dynamic pairlists for backtesting"
|
||||||
Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist.
|
Using dynamic pairlists is possible (not all of the handlers are allowed to be used in backtest mode), however it relies on the current market conditions - which will not reflect the historic status of the pairlist.
|
||||||
Also, when using pairlists other than StaticPairlist, reproducibility of backtesting-results cannot be guaranteed.
|
Also, when using pairlists other than StaticPairlist, reproducibility of backtesting-results cannot be guaranteed.
|
||||||
Please read the [pairlists documentation](plugins.md#pairlists) for more information.
|
Please read the [pairlists documentation](plugins.md#pairlists) for more information.
|
||||||
|
|
||||||
|
@ -268,55 +274,56 @@ A backtesting result will look like that:
|
||||||
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
| XRP/BTC | 35 | 0.66 | 22.96 | 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 | -10.18 | -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 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
||||||
========================================================= SELL REASON STATS ==========================================================
|
========================================================= EXIT REASON STATS ==========================================================
|
||||||
| Sell Reason | Sells | Wins | Draws | Losses |
|
| Exit Reason | Sells | Wins | Draws | Losses |
|
||||||
|:-------------------|--------:|------:|-------:|--------:|
|
|:-------------------|--------:|------:|-------:|--------:|
|
||||||
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
||||||
| stop_loss | 166 | 0 | 0 | 166 |
|
| stop_loss | 166 | 0 | 0 | 166 |
|
||||||
| sell_signal | 56 | 36 | 0 | 20 |
|
| exit_signal | 56 | 36 | 0 | 20 |
|
||||||
| force_sell | 2 | 0 | 0 | 2 |
|
| force_exit | 2 | 0 | 0 | 2 |
|
||||||
====================================================== LEFT OPEN TRADES REPORT ======================================================
|
====================================================== LEFT OPEN TRADES REPORT ======================================================
|
||||||
| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|
| Pair | Buys | Avg Profit % | Cum 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.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.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 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
||||||
=============== SUMMARY METRICS ===============
|
================ SUMMARY METRICS ===============
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|-----------------------+---------------------|
|
|------------------------+---------------------|
|
||||||
| Backtesting from | 2019-01-01 00:00:00 |
|
| Backtesting from | 2019-01-01 00:00:00 |
|
||||||
| Backtesting to | 2019-05-01 00:00:00 |
|
| Backtesting to | 2019-05-01 00:00:00 |
|
||||||
| Max open trades | 3 |
|
| Max open trades | 3 |
|
||||||
| | |
|
| | |
|
||||||
| Total/Daily Avg Trades| 429 / 3.575 |
|
| Total/Daily Avg Trades | 429 / 3.575 |
|
||||||
| Starting balance | 0.01000000 BTC |
|
| Starting balance | 0.01000000 BTC |
|
||||||
| Final balance | 0.01762792 BTC |
|
| Final balance | 0.01762792 BTC |
|
||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
| Total profit % | 76.2% |
|
| Total profit % | 76.2% |
|
||||||
| Trades per day | 3.575 |
|
| Trades per day | 3.575 |
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 BTC |
|
| Total trade volume | 0.429 BTC |
|
||||||
| | |
|
| | |
|
||||||
| Best Pair | LSK/BTC 26.26% |
|
| Best Pair | LSK/BTC 26.26% |
|
||||||
| Worst Pair | ZEC/BTC -10.18% |
|
| Worst Pair | ZEC/BTC -10.18% |
|
||||||
| Best Trade | LSK/BTC 4.25% |
|
| Best Trade | LSK/BTC 4.25% |
|
||||||
| Worst Trade | ZEC/BTC -10.25% |
|
| Worst Trade | ZEC/BTC -10.25% |
|
||||||
| Best day | 0.00076 BTC |
|
| Best day | 0.00076 BTC |
|
||||||
| Worst day | -0.00036 BTC |
|
| Worst day | -0.00036 BTC |
|
||||||
| Days win/draw/lose | 12 / 82 / 25 |
|
| Days win/draw/lose | 12 / 82 / 25 |
|
||||||
| Avg. Duration Winners | 4:23:00 |
|
| Avg. Duration Winners | 4:23:00 |
|
||||||
| Avg. Duration Loser | 6:55:00 |
|
| Avg. Duration Loser | 6:55:00 |
|
||||||
| Rejected Buy signals | 3089 |
|
| Rejected Entry signals | 3089 |
|
||||||
| | |
|
| Entry/Exit Timeouts | 0 / 0 |
|
||||||
| Min balance | 0.00945123 BTC |
|
| | |
|
||||||
| Max balance | 0.01846651 BTC |
|
| Min balance | 0.00945123 BTC |
|
||||||
| Drawdown | 50.63% |
|
| Max balance | 0.01846651 BTC |
|
||||||
| Drawdown | 0.0015 BTC |
|
| Drawdown (Account) | 13.33% |
|
||||||
| Drawdown high | 0.0013 BTC |
|
| Drawdown | 0.0015 BTC |
|
||||||
| Drawdown low | -0.0002 BTC |
|
| Drawdown high | 0.0013 BTC |
|
||||||
| Drawdown Start | 2019-02-15 14:10:00 |
|
| Drawdown low | -0.0002 BTC |
|
||||||
| Drawdown End | 2019-04-11 18:15:00 |
|
| Drawdown Start | 2019-02-15 14:10:00 |
|
||||||
| Market change | -5.88% |
|
| Drawdown End | 2019-04-11 18:15:00 |
|
||||||
|
| Market change | -5.88% |
|
||||||
===============================================
|
===============================================
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -338,9 +345,9 @@ The column `Avg Profit %` shows the average profit for all trades made while the
|
||||||
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%`.
|
||||||
|
|
||||||
Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set.
|
Your strategy performance is influenced by your buy strategy, your exit strategy, and also by the `minimal_roi` and `stop_loss` you have set.
|
||||||
|
|
||||||
For example, if your `minimal_roi` is only `"0": 0.01` you cannot expect the bot to make more profit than 1% (because it will sell every time a trade reaches 1%).
|
For example, if your `minimal_roi` is only `"0": 0.01` you cannot expect the bot to make more profit than 1% (because it will exit every time a trade reaches 1%).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
|
@ -352,14 +359,14 @@ On the other hand, if you set a too high `minimal_roi` like `"0": 0.55`
|
||||||
(55%), there is almost no chance that the bot will ever reach this profit.
|
(55%), there is almost no chance that the bot will ever reach this profit.
|
||||||
Hence, keep in mind that your performance is an integral mix of all different elements of the strategy, your configuration, and the crypto-currency pairs you have set up.
|
Hence, keep in mind that your performance is an integral mix of all different elements of the strategy, your configuration, and the crypto-currency pairs you have set up.
|
||||||
|
|
||||||
### Sell reasons table
|
### Exit reasons table
|
||||||
|
|
||||||
The 2nd table contains a recap of sell reasons.
|
The 2nd table contains a recap of exit reasons.
|
||||||
This table can tell you which area needs some additional work (e.g. all or many of the `sell_signal` trades are losses, so you should work on improving the sell signal, or consider disabling it).
|
This table can tell you which area needs some additional work (e.g. all or many of the `exit_signal` trades are losses, so you should work on improving the exit signal, or consider disabling it).
|
||||||
|
|
||||||
### Left open trades table
|
### Left open trades table
|
||||||
|
|
||||||
The 3rd table contains all trades the bot had to `forcesell` at the end of the backtesting period to present you the full picture.
|
The 3rd table contains all trades the bot had to `force_exit` at the end of the backtesting period to present you the full picture.
|
||||||
This is necessary to simulate realistic behavior, since the backtest period has to end at some point, while realistically, you could leave the bot running forever.
|
This is necessary to simulate realistic behavior, since the backtest period has to end at some point, while realistically, you could leave the bot running forever.
|
||||||
These trades are also included in the first table, but are also shown separately in this table for clarity.
|
These trades are also included in the first table, but are also shown separately in this table for clarity.
|
||||||
|
|
||||||
|
@ -369,42 +376,49 @@ The last element of the backtest report is the summary metrics table.
|
||||||
It contains some useful key metrics about performance of your strategy on backtesting data.
|
It contains some useful key metrics about performance of your strategy on backtesting data.
|
||||||
|
|
||||||
```
|
```
|
||||||
=============== SUMMARY METRICS ===============
|
================ SUMMARY METRICS ===============
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|-----------------------+---------------------|
|
|------------------------+---------------------|
|
||||||
| Backtesting from | 2019-01-01 00:00:00 |
|
| Backtesting from | 2019-01-01 00:00:00 |
|
||||||
| Backtesting to | 2019-05-01 00:00:00 |
|
| Backtesting to | 2019-05-01 00:00:00 |
|
||||||
| Max open trades | 3 |
|
| Max open trades | 3 |
|
||||||
| | |
|
| | |
|
||||||
| Total/Daily Avg Trades| 429 / 3.575 |
|
| Total/Daily Avg Trades | 429 / 3.575 |
|
||||||
| Starting balance | 0.01000000 BTC |
|
| Starting balance | 0.01000000 BTC |
|
||||||
| Final balance | 0.01762792 BTC |
|
| Final balance | 0.01762792 BTC |
|
||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
| Total profit % | 76.2% |
|
| Total profit % | 76.2% |
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 BTC |
|
| Total trade volume | 0.429 BTC |
|
||||||
| | |
|
| | |
|
||||||
| Best Pair | LSK/BTC 26.26% |
|
| Long / Short | 352 / 77 |
|
||||||
| Worst Pair | ZEC/BTC -10.18% |
|
| Total profit Long % | 1250.58% |
|
||||||
| Best Trade | LSK/BTC 4.25% |
|
| Total profit Short % | -15.02% |
|
||||||
| Worst Trade | ZEC/BTC -10.25% |
|
| Absolute profit Long | 0.00838792 BTC |
|
||||||
| Best day | 0.00076 BTC |
|
| Absolute profit Short | -0.00076 BTC |
|
||||||
| Worst day | -0.00036 BTC |
|
| | |
|
||||||
| Days win/draw/lose | 12 / 82 / 25 |
|
| Best Pair | LSK/BTC 26.26% |
|
||||||
| Avg. Duration Winners | 4:23:00 |
|
| Worst Pair | ZEC/BTC -10.18% |
|
||||||
| Avg. Duration Loser | 6:55:00 |
|
| Best Trade | LSK/BTC 4.25% |
|
||||||
| Rejected Buy signals | 3089 |
|
| Worst Trade | ZEC/BTC -10.25% |
|
||||||
| | |
|
| Best day | 0.00076 BTC |
|
||||||
| Min balance | 0.00945123 BTC |
|
| Worst day | -0.00036 BTC |
|
||||||
| Max balance | 0.01846651 BTC |
|
| Days win/draw/lose | 12 / 82 / 25 |
|
||||||
| Drawdown | 50.63% |
|
| Avg. Duration Winners | 4:23:00 |
|
||||||
| Drawdown | 0.0015 BTC |
|
| Avg. Duration Loser | 6:55:00 |
|
||||||
| Drawdown high | 0.0013 BTC |
|
| Rejected Entry signals | 3089 |
|
||||||
| Drawdown low | -0.0002 BTC |
|
| Entry/Exit Timeouts | 0 / 0 |
|
||||||
| Drawdown Start | 2019-02-15 14:10:00 |
|
| | |
|
||||||
| Drawdown End | 2019-04-11 18:15:00 |
|
| Min balance | 0.00945123 BTC |
|
||||||
| Market change | -5.88% |
|
| Max balance | 0.01846651 BTC |
|
||||||
===============================================
|
| Drawdown (Account) | 13.33% |
|
||||||
|
| Drawdown | 0.0015 BTC |
|
||||||
|
| Drawdown high | 0.0013 BTC |
|
||||||
|
| Drawdown low | -0.0002 BTC |
|
||||||
|
| Drawdown Start | 2019-02-15 14:10:00 |
|
||||||
|
| Drawdown End | 2019-04-11 18:15:00 |
|
||||||
|
| Market change | -5.88% |
|
||||||
|
================================================
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -422,17 +436,55 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||||
- `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).
|
||||||
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
||||||
- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached.
|
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
|
||||||
|
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
||||||
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
||||||
- `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced).
|
- `Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as $(Absolute Drawdown) / (DrawdownHigh + startingBalance)$.
|
||||||
|
- `Drawdown`: Maximum, absolute drawdown experienced. Difference between Drawdown High and Subsequent Low point.
|
||||||
- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost.
|
- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost.
|
||||||
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
||||||
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
|
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
|
||||||
|
- `Long / Short`: Split long/short values (Only shown when short trades were made).
|
||||||
|
- `Total profit Long %` / `Absolute profit Long`: Profit long trades only (Only shown when short trades were made).
|
||||||
|
- `Total profit Short %` / `Absolute profit Short`: Profit short trades only (Only shown when short trades were made).
|
||||||
|
|
||||||
|
### Daily / Weekly / Monthly breakdown
|
||||||
|
|
||||||
|
You can get an overview over daily / weekly or monthly results by using the `--breakdown <>` switch.
|
||||||
|
|
||||||
|
To visualize daily and weekly breakdowns, you can use the following:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade backtesting --strategy MyAwesomeStrategy --breakdown day month
|
||||||
|
```
|
||||||
|
|
||||||
|
``` output
|
||||||
|
======================== DAY BREAKDOWN =========================
|
||||||
|
| Day | Tot Profit USDT | Wins | Draws | Losses |
|
||||||
|
|------------+-------------------+--------+---------+----------|
|
||||||
|
| 03/07/2021 | 200.0 | 2 | 0 | 0 |
|
||||||
|
| 04/07/2021 | -50.31 | 0 | 0 | 2 |
|
||||||
|
| 05/07/2021 | 220.611 | 3 | 2 | 0 |
|
||||||
|
| 06/07/2021 | 150.974 | 3 | 0 | 2 |
|
||||||
|
| 07/07/2021 | -70.193 | 1 | 0 | 2 |
|
||||||
|
| 08/07/2021 | 212.413 | 2 | 0 | 3 |
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The output will show a table containing the realized absolute Profit (in stake currency) for the given timeperiod, as well as wins, draws and losses that materialized (closed) on this day.
|
||||||
|
|
||||||
|
### Backtest result caching
|
||||||
|
|
||||||
|
To save time, by default backtest will reuse a cached result from within the last day when the backtested strategy and config match that of a previous backtest. To force a new backtest despite existing result for an identical run specify `--cache none` parameter.
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
Caching is automatically disabled for open-ended timeranges (`--timerange 20210101-`), as freqtrade cannot ensure reliably that the underlying data didn't change. It can also use cached results where it shouldn't if the original backtest had missing data at the end, which was fixed by downloading more data.
|
||||||
|
In this instance, please use `--cache none` once to force a fresh backtest.
|
||||||
|
|
||||||
### 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, you can [export the trades](#exporting-trades-to-file).
|
||||||
You can then load the trades to perform further analysis as shown in our [data analysis](data-analysis.md#backtesting) backtesting section.
|
You can then load the trades to perform further analysis as shown in the [data analysis](data-analysis.md#backtesting) backtesting section.
|
||||||
|
|
||||||
## Assumptions made by backtesting
|
## Assumptions made by backtesting
|
||||||
|
|
||||||
|
@ -440,24 +492,25 @@ Since backtesting lacks some detailed information about what happens within a ca
|
||||||
|
|
||||||
- Buys happen at open-price
|
- Buys 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, no unfilled orders)
|
||||||
- Sell-signal sells happen at open-price of the consecutive candle
|
- Exit-signal exits happen at open-price of the consecutive candle
|
||||||
- Sell-signal is favored over Stoploss, because sell-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
|
||||||
- sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell 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%)
|
||||||
- sells are never "below the candle", so a ROI of 2% may result in a sell 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
|
||||||
- Forcesells caused by `<N>=-1` ROI entries use low as sell value, unless N falls on the candle open (e.g. `120: -1` for 1h candles)
|
- Forceexits 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 sells 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
|
||||||
- Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes
|
- Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` exit reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes
|
||||||
- Low happens before high for stoploss, protecting capital first
|
- Low happens before high for stoploss, protecting capital first
|
||||||
- Trailing stoploss
|
- Trailing stoploss
|
||||||
- Trailing Stoploss is only adjusted if it's below the candle's low (otherwise it would be triggered)
|
- Trailing Stoploss is only adjusted if it's below the candle's low (otherwise it would be triggered)
|
||||||
|
- On trade entry candles that trigger trailing stoploss, the "minimum offset" (`stop_positive_offset`) is assumed (instead of high) - and the stop is calculated from this point
|
||||||
- High happens first - adjusting stoploss
|
- High happens first - adjusting stoploss
|
||||||
- Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly)
|
- Low uses the adjusted stoploss (so exits with large high-low difference are backtested correctly)
|
||||||
- ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies
|
- ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies
|
||||||
- Sell-reason does not explain if a trade was positive or negative, just what triggered the sell (this can look odd if negative ROI values are used)
|
- Exit-reason does not explain if a trade was positive or negative, just what triggered the exit (this can look odd if negative ROI values are used)
|
||||||
- Evaluation sequence (if multiple signals happen on the same candle)
|
- Evaluation sequence (if multiple signals happen on the same candle)
|
||||||
|
- Exit-signal
|
||||||
- ROI (if not stoploss)
|
- ROI (if not stoploss)
|
||||||
- Sell-signal
|
|
||||||
- Stoploss
|
- Stoploss
|
||||||
|
|
||||||
Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode.
|
Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode.
|
||||||
|
@ -480,7 +533,7 @@ freqtrade backtesting --strategy AwesomeStrategy --timeframe 1h --timeframe-deta
|
||||||
```
|
```
|
||||||
|
|
||||||
This will load 1h data as well as 5m data for the timeframe. The strategy will be analyzed with the 1h timeframe - and for every "open trade candle" (candles where a trade is open) the 5m data will be used to simulate intra-candle movements.
|
This will load 1h data as well as 5m data for the timeframe. The strategy will be analyzed with the 1h timeframe - and for every "open trade candle" (candles where a trade is open) the 5m data will be used to simulate intra-candle movements.
|
||||||
All callback functions (`custom_sell()`, `custom_stoploss()`, ... ) will be running for each 5m candle once the trade is opened (so 12 times in the above example of 1h timeframe, and 5m detailed timeframe).
|
All callback functions (`custom_exit()`, `custom_stoploss()`, ... ) will be running for each 5m candle once the trade is opened (so 12 times in the above example of 1h timeframe, and 5m detailed timeframe).
|
||||||
|
|
||||||
`--timeframe-detail` must be smaller than the original timeframe, otherwise backtesting will fail to start.
|
`--timeframe-detail` must be smaller than the original timeframe, otherwise backtesting will fail to start.
|
||||||
|
|
||||||
|
|
|
@ -24,25 +24,27 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and
|
||||||
|
|
||||||
* Fetch open trades from persistence.
|
* Fetch open trades from persistence.
|
||||||
* Calculate current list of tradable pairs.
|
* Calculate current list of tradable pairs.
|
||||||
* Download ohlcv data for the pairlist including all [informative pairs](strategy-customization.md#get-data-for-non-tradeable-pairs)
|
* Download OHLCV data for the pairlist including all [informative pairs](strategy-customization.md#get-data-for-non-tradeable-pairs)
|
||||||
This step is only executed once per Candle to avoid unnecessary network traffic.
|
This step is only executed once per Candle to avoid unnecessary network traffic.
|
||||||
* Call `bot_loop_start()` strategy callback.
|
* Call `bot_loop_start()` strategy callback.
|
||||||
* Analyze strategy per pair.
|
* Analyze strategy per pair.
|
||||||
* Call `populate_indicators()`
|
* Call `populate_indicators()`
|
||||||
* Call `populate_buy_trend()`
|
* Call `populate_entry_trend()`
|
||||||
* Call `populate_sell_trend()`
|
* Call `populate_exit_trend()`
|
||||||
* Check timeouts for open orders.
|
* Check timeouts for open orders.
|
||||||
* Calls `check_buy_timeout()` strategy callback for open buy orders.
|
* Calls `check_entry_timeout()` strategy callback for open entry orders.
|
||||||
* Calls `check_sell_timeout()` strategy callback for open sell orders.
|
* Calls `check_exit_timeout()` strategy callback for open exit orders.
|
||||||
* Verifies existing positions and eventually places sell orders.
|
* Verifies existing positions and eventually places exit orders.
|
||||||
* Considers stoploss, ROI and sell-signal, `custom_sell()` and `custom_stoploss()`.
|
* Considers stoploss, ROI and exit-signal, `custom_exit()` and `custom_stoploss()`.
|
||||||
* Determine sell-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback.
|
* Determine exit-price based on `exit_pricing` configuration setting or by using the `custom_exit_price()` callback.
|
||||||
* Before a sell order is placed, `confirm_trade_exit()` strategy callback is called.
|
* Before a exit order is placed, `confirm_trade_exit()` strategy callback is called.
|
||||||
|
* Check position adjustments for open trades if enabled by calling `adjust_trade_position()` and place additional order if required.
|
||||||
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
||||||
* Verifies buy signal trying to enter new positions.
|
* Verifies entry signal trying to enter new positions.
|
||||||
* Determine buy-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback.
|
* Determine entry-price based on `entry_pricing` configuration setting, or by using the `custom_entry_price()` callback.
|
||||||
|
* 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.
|
||||||
* Before a buy order is placed, `confirm_trade_entry()` strategy callback is called.
|
* Before an entry order is placed, `confirm_trade_entry()` strategy callback is called.
|
||||||
|
|
||||||
This loop will be repeated again and again until the bot is stopped.
|
This loop will be repeated again and again until the bot is stopped.
|
||||||
|
|
||||||
|
@ -53,10 +55,17 @@ This loop will be repeated again and again until the bot is stopped.
|
||||||
* Load historic data for configured pairlist.
|
* Load historic data for configured pairlist.
|
||||||
* Calls `bot_loop_start()` once.
|
* Calls `bot_loop_start()` once.
|
||||||
* Calculate indicators (calls `populate_indicators()` once per pair).
|
* Calculate indicators (calls `populate_indicators()` once per pair).
|
||||||
* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair).
|
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
|
||||||
* Loops per candle simulating entry and exit points.
|
* Loops per candle simulating entry and exit points.
|
||||||
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
|
||||||
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
|
* Check for trade entry signals (`enter_long` / `enter_short` columns).
|
||||||
|
* Confirm trade entry / exits (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
||||||
|
* Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle).
|
||||||
|
* 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.
|
||||||
|
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
|
||||||
|
* Call `custom_stoploss()` and `custom_exit()` to find custom exit points.
|
||||||
|
* For exits based on exit-signal and custom-exit: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
|
||||||
* Generate backtest report output
|
* Generate backtest report output
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
|
|
|
@ -37,6 +37,15 @@ Using this scheme, all configuration settings will also be available as environm
|
||||||
|
|
||||||
Please note that Environment variables will overwrite corresponding settings in your configuration, but command line Arguments will always win.
|
Please note that Environment variables will overwrite corresponding settings in your configuration, but command line Arguments will always win.
|
||||||
|
|
||||||
|
Common example:
|
||||||
|
|
||||||
|
```
|
||||||
|
FREQTRADE__TELEGRAM__CHAT_ID=<telegramchatid>
|
||||||
|
FREQTRADE__TELEGRAM__TOKEN=<telegramToken>
|
||||||
|
FREQTRADE__EXCHANGE__KEY=<yourExchangeKey>
|
||||||
|
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.
|
||||||
|
|
||||||
|
@ -44,14 +53,63 @@ Please note that Environment variables will overwrite corresponding settings in
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
!!! 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.
|
||||||
|
|
||||||
|
``` json title="user_data/config.json"
|
||||||
|
"add_config_files": [
|
||||||
|
"config-private.json"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade trade --config user_data/config.json <...>
|
||||||
|
```
|
||||||
|
|
||||||
|
The 2nd file should only specify what you intend to override.
|
||||||
|
If a key is in more than one of the configurations, then the "last specified configuration" wins (in the above example, `config-private.json`).
|
||||||
|
|
||||||
|
For one-off commands, you can also use the below syntax by specifying multiple "--config" parameters.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade trade --config user_data/config.json --config user_data/config-private.json <...>
|
freqtrade trade --config user_data/config.json --config user_data/config-private.json <...>
|
||||||
```
|
```
|
||||||
The 2nd file should only specify what you intend to override.
|
|
||||||
If a key is in more than one of the configurations, then the "last specified configuration" wins (in the above example, `config-private.json`).
|
This is equivalent to the example above - but `config-private.json` is specified as cli argument.
|
||||||
|
|
||||||
|
??? Note "config collision handling"
|
||||||
|
If the same configuration setting takes place in both `config.json` and `config-import.json`, then the parent configuration wins.
|
||||||
|
In the below case, `max_open_trades` would be 3 after the merging - as the reusable "import" configuration has this key overwritten.
|
||||||
|
|
||||||
|
``` json title="user_data/config.json"
|
||||||
|
{
|
||||||
|
"max_open_trades": 3,
|
||||||
|
"stake_currency": "USDT",
|
||||||
|
"add_config_files": [
|
||||||
|
"config-import.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
``` json title="user_data/config-import.json"
|
||||||
|
{
|
||||||
|
"max_open_trades": 10,
|
||||||
|
"stake_amount": "unlimited",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Resulting combined configuration:
|
||||||
|
|
||||||
|
``` json title="Result"
|
||||||
|
{
|
||||||
|
"max_open_trades": 10,
|
||||||
|
"stake_currency": "USDT",
|
||||||
|
"stake_amount": "unlimited"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Configuration parameters
|
## Configuration parameters
|
||||||
|
|
||||||
|
@ -77,53 +135,59 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||||
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
|
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
|
||||||
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
|
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
|
||||||
| `timeframe` | The timeframe (former ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
|
| `timeframe` | The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
|
||||||
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
|
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
|
||||||
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||||
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
|
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
|
||||||
| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
| `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to exit a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
||||||
| `stoploss` | **Required.** Value as ratio of the stoploss used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float (as ratio)
|
| `stoploss` | **Required.** Value as ratio of the stoploss used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float (as ratio)
|
||||||
| `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md#trailing-stop-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Boolean
|
| `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md#trailing-stop-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Boolean
|
||||||
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
|
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
|
||||||
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
|
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
|
||||||
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
|
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
|
||||||
| `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
| `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md). <br>*Defaults to `"spot"`.* <br> **Datatype:** String
|
||||||
| `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
| `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md). <br> **Datatype:** String
|
||||||
|
| `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md). <br>*Defaults to `0.05`.* <br> **Datatype:** Float
|
||||||
|
| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||||
|
| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||||
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
|
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
|
||||||
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
|
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
|
||||||
| `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled).
|
| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#buy-price-side).<br> *Defaults to `same`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
|
||||||
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean
|
| `entry_pricing.price_last_balance` | **Required.** Interpolate the bidding price. More information [below](#entry-price-without-orderbook-enabled).
|
||||||
| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean
|
||||||
| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `entry_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to enter a trade. I.e. a value of 2 will allow the bot to pick the 2nd entry in [Order Book Entry](#entry-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||||
| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio)
|
| `entry_pricing. check_depth_of_market.enabled` | Do not enter if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `ask_strategy.price_side` | Select the side of the spread the bot should look at to get the sell rate. [More information below](#sell-price-side).<br> *Defaults to `ask`.* <br> **Datatype:** String (either `ask` or `bid`).
|
| `entry_pricing. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio)
|
||||||
| `ask_strategy.bid_last_balance` | Interpolate the selling price. More information [below](#sell-price-without-orderbook-enabled).
|
| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).<br> *Defaults to `same`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
|
||||||
| `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled). <br> **Datatype:** Boolean
|
| `exit_pricing.price_last_balance` | Interpolate the exiting price. More information [below](#exit-price-without-orderbook-enabled).
|
||||||
| `ask_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to sell. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Asks](#sell-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean
|
||||||
| `use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
| `exit_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to exit. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Exit](#exit-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||||
| `sell_profit_only` | Wait until the bot reaches `sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `use_exit_signal` | Use exit signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||||
| `sell_profit_offset` | Sell-signal is only active above this value. Only active in combination with `sell_profit_only=True`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio)
|
| `exit_profit_only` | Wait until the bot reaches `exit_profit_offset` before taking an exit decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `exit_profit_offset` | Exit-signal is only active above this value. Only active in combination with `exit_profit_only=True`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio)
|
||||||
|
| `ignore_roi_if_entry_signal` | Do not exit if the entry signal is still active. This setting takes preference over `minimal_roi` and `use_exit_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer
|
| `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer
|
||||||
| `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
|
| `order_types` | Configure order-types depending on the action (`"entry"`, `"exit"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
|
||||||
| `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
| `order_time_in_force` | Configure time in force for entry and exit orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
||||||
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
|
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
|
||||||
| `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. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
|
||||||
| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.<br> **Datatype:** Boolean
|
| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.<br> **Datatype:** Boolean
|
||||||
| `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
|
||||||
|
| `exchange.uid` | API uid to use for the exchange. Only required when you are in production mode and for exchanges that use uid for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList. [More information](plugins.md#pairlists-and-pairlist-handlers). <br> **Datatype:** List
|
| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList. [More information](plugins.md#pairlists-and-pairlist-handlers). <br> **Datatype:** List
|
||||||
| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting. [More information](plugins.md#pairlists-and-pairlist-handlers). <br> **Datatype:** List
|
| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting. [More information](plugins.md#pairlists-and-pairlist-handlers). <br> **Datatype:** List
|
||||||
| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for additional ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation). Please avoid adding exchange secrets here (use the dedicated fields instead), as they may be contained in logs. <br> **Datatype:** Dict
|
||||||
| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||||
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||||
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
||||||
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||||
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||||
|
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
|
||||||
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||||
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation.
|
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation.
|
||||||
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||||
|
@ -135,10 +199,12 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||||
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
|
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
|
||||||
| `webhook.enabled` | Enable usage of Webhook notifications <br> **Datatype:** Boolean
|
| `webhook.enabled` | Enable usage of Webhook notifications <br> **Datatype:** Boolean
|
||||||
| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
| `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
| `webhook.webhookentry` | Payload to send on entry. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
| `webhook.webhookbuycancel` | Payload to send on buy order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
| `webhook.webhookentrycancel` | Payload to send on entry order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
| `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
| `webhook.webhookentryfill` | Payload to send on entry order filled. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
| `webhook.webhooksellcancel` | Payload to send on sell order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
| `webhook.webhookexit` | Payload to send on exit. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
|
| `webhook.webhookexitcancel` | Payload to send on exit order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
|
| `webhook.webhookexitfill` | Payload to send on exit order filled. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||||
| `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** Boolean
|
| `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** Boolean
|
||||||
| `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** IPv4
|
| `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** IPv4
|
||||||
|
@ -149,7 +215,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||||
| `bot_name` | Name of the bot. Passed via API to a client - can be shown to distinguish / name bots.<br> *Defaults to `freqtrade`*<br> **Datatype:** String
|
| `bot_name` | Name of the bot. Passed via API to a client - can be shown to distinguish / name bots.<br> *Defaults to `freqtrade`*<br> **Datatype:** String
|
||||||
| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite:///tradesv3.dryrun.sqlite` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances. <br> **Datatype:** String, SQLAlchemy connect string
|
| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite:///tradesv3.dryrun.sqlite` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances. <br> **Datatype:** String, SQLAlchemy connect string
|
||||||
| `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command. <br>*Defaults to `stopped`.* <br> **Datatype:** Enum, either `stopped` or `running`
|
| `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command. <br>*Defaults to `stopped`.* <br> **Datatype:** Enum, either `stopped` or `running`
|
||||||
| `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. <br> **Datatype:** Boolean
|
| `force_entry_enable` | Enables the RPC Commands to force a Trade entry. More information below. <br> **Datatype:** Boolean
|
||||||
| `disable_dataframe_checks` | Disable checking the OHLCV dataframe returned from the strategy methods for correctness. Only use when intentionally changing the dataframe and understand what you are doing. [Strategy Override](#parameters-in-the-strategy).<br> *Defaults to `False`*. <br> **Datatype:** Boolean
|
| `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
|
||||||
| `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
|
||||||
|
@ -158,8 +224,11 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||||
| `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](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean
|
||||||
| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String
|
| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String
|
||||||
| `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> **Datatype:** String
|
| `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> **Datatype:** String
|
||||||
|
| `add_config_files` | Additional config files. These files will be loaded and merged with the current config file. The files are resolved relative to the initial file.<br> *Defaults to `[]`*. <br> **Datatype:** List of strings
|
||||||
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String
|
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String
|
||||||
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String
|
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String
|
||||||
|
| `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
|
||||||
|
|
||||||
### Parameters in the strategy
|
### Parameters in the strategy
|
||||||
|
|
||||||
|
@ -179,11 +248,13 @@ Values set in the configuration file always overwrite values set in the strategy
|
||||||
* `order_time_in_force`
|
* `order_time_in_force`
|
||||||
* `unfilledtimeout`
|
* `unfilledtimeout`
|
||||||
* `disable_dataframe_checks`
|
* `disable_dataframe_checks`
|
||||||
* `use_sell_signal`
|
- `use_exit_signal`
|
||||||
* `sell_profit_only`
|
* `exit_profit_only`
|
||||||
* `sell_profit_offset`
|
- `exit_profit_offset`
|
||||||
* `ignore_roi_if_buy_signal`
|
- `ignore_roi_if_entry_signal`
|
||||||
* `ignore_buying_expired_candle_after`
|
* `ignore_buying_expired_candle_after`
|
||||||
|
* `position_adjustment_enable`
|
||||||
|
* `max_entry_position_adjustment`
|
||||||
|
|
||||||
### Configuring amount per trade
|
### Configuring amount per trade
|
||||||
|
|
||||||
|
@ -192,9 +263,8 @@ There are several methods to configure how much of the stake currency the bot wi
|
||||||
#### Minimum trade stake
|
#### Minimum trade stake
|
||||||
|
|
||||||
The minimum stake amount will depend on exchange and pair and is usually listed in the exchange support pages.
|
The minimum stake amount will depend on exchange and pair and is usually listed in the exchange support pages.
|
||||||
Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.6$.
|
|
||||||
|
|
||||||
The minimum stake amount to buy this pair is, therefore, `20 * 0.6 ~= 12`.
|
Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.6$, the minimum stake amount to buy this pair is `20 * 0.6 ~= 12`.
|
||||||
This exchange has also a limit on USD - where all orders must be > 10$ - which however does not apply in this case.
|
This exchange has also a limit on USD - where all orders must be > 10$ - which however does not apply in this case.
|
||||||
|
|
||||||
To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%).
|
To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%).
|
||||||
|
@ -204,7 +274,7 @@ With a reserve of 5%, the minimum stake amount would be ~12.6$ (`12 * (1 + 0.05)
|
||||||
To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit.
|
To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange.
|
Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange. Freqtrade adjusts the stake-amount to this value, unless it's > 30% more than the calculated/desired stake-amount - in which case the trade is rejected.
|
||||||
|
|
||||||
#### Tradable balance
|
#### Tradable balance
|
||||||
|
|
||||||
|
@ -291,6 +361,15 @@ To allow the bot to trade all the available `stake_currency` in your account (mi
|
||||||
When using `"stake_amount" : "unlimited",` in combination with Dry-Run, Backtesting or Hyperopt, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve.
|
When using `"stake_amount" : "unlimited",` in combination with Dry-Run, Backtesting or Hyperopt, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve.
|
||||||
It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise, it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency.
|
It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise, it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency.
|
||||||
|
|
||||||
|
#### Dynamic stake amount with position adjustment
|
||||||
|
|
||||||
|
When you want to use position adjustment with unlimited stakes, you must also implement `custom_stake_amount` to a return a value depending on your strategy.
|
||||||
|
Typical value would be in the range of 25% - 50% of the proposed stakes, but depends highly on your strategy and how much you wish to leave into the wallet as position adjustment buffer.
|
||||||
|
|
||||||
|
For example if your position adjustment assumes it can do 2 additional buys with the same stake amounts then your buffer should be 66.6667% of the initially proposed unlimited stake amount.
|
||||||
|
|
||||||
|
Or another example if your position adjustment assumes it can do 1 additional buy with 3x the original stake amount then `custom_stake_amount` should return 25% of proposed stake amount and leave 75% for possible later position adjustments.
|
||||||
|
|
||||||
--8<-- "includes/pricing.md"
|
--8<-- "includes/pricing.md"
|
||||||
|
|
||||||
### Understand minimal_roi
|
### Understand minimal_roi
|
||||||
|
@ -301,10 +380,10 @@ See the example below:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0, # Sell after 40 minutes if the profit is not negative
|
"40": 0.0, # Exit after 40 minutes if the profit is not negative
|
||||||
"30": 0.01, # Sell after 30 minutes if there is at least 1% profit
|
"30": 0.01, # Exit after 30 minutes if there is at least 1% profit
|
||||||
"20": 0.02, # Sell after 20 minutes if there is at least 2% profit
|
"20": 0.02, # Exit after 20 minutes if there is at least 2% profit
|
||||||
"0": 0.04 # Sell immediately if there is at least 4% profit
|
"0": 0.04 # Exit immediately if there is at least 4% profit
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -313,14 +392,14 @@ This parameter can be set in either Strategy or Configuration file. If you use i
|
||||||
`minimal_roi` value from the strategy file.
|
`minimal_roi` value from the strategy file.
|
||||||
If it is not set in either Strategy or Configuration, a default of 1000% `{"0": 10}` is used, and minimal ROI is disabled unless your trade generates 1000% profit.
|
If it is not set in either Strategy or Configuration, a default of 1000% `{"0": 10}` is used, and minimal ROI is disabled unless your trade generates 1000% profit.
|
||||||
|
|
||||||
!!! Note "Special case to forcesell after a specific time"
|
!!! Note "Special case to forceexit after a specific time"
|
||||||
A special case presents using `"<N>": -1` as ROI. This forces the bot to sell a trade after N Minutes, no matter if it's positive or negative, so represents a time-limited force-sell.
|
A special case presents using `"<N>": -1` as ROI. This forces the bot to exit a trade after N Minutes, no matter if it's positive or negative, so represents a time-limited force-exit.
|
||||||
|
|
||||||
### Understand forcebuy_enable
|
### Understand force_entry_enable
|
||||||
|
|
||||||
The `forcebuy_enable` configuration parameter enables the usage of forcebuy commands via Telegram and REST API.
|
The `force_entry_enable` configuration parameter enables the usage of force-enter (`/forcelong`, `/forceshort`) commands via Telegram and REST API.
|
||||||
For security reasons, it's disabled by default, and freqtrade will show a warning message on startup if enabled.
|
For security reasons, it's disabled by default, and freqtrade will show a warning message on startup if enabled.
|
||||||
For example, you can send `/forcebuy ETH/BTC` to the bot, which will result in freqtrade buying the pair and holds it until a regular sell-signal (ROI, stoploss, /forcesell) appears.
|
For example, you can send `/forceenter ETH/BTC` to the bot, which will result in freqtrade buying the pair and holds it until a regular exit-signal (ROI, stoploss, /forceexit) appears.
|
||||||
|
|
||||||
This can be dangerous with some strategies, so use with care.
|
This can be dangerous with some strategies, so use with care.
|
||||||
|
|
||||||
|
@ -347,29 +426,27 @@ For example, if your strategy is using a 1h timeframe, and you only want to buy
|
||||||
|
|
||||||
### Understand order_types
|
### Understand order_types
|
||||||
|
|
||||||
The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`, `forcesell`, `forcebuy`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
|
The `order_types` configuration parameter maps actions (`entry`, `exit`, `stoploss`, `emergency_exit`, `force_exit`, `force_entry`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
|
||||||
|
|
||||||
|
This allows to enter using limit orders, exit using limit-orders, and create stoplosses using market orders.
|
||||||
|
It also allows to set the
|
||||||
|
stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled.
|
||||||
|
|
||||||
This allows to buy using limit orders, sell using
|
|
||||||
limit-orders, and create stoplosses using market orders. It also allows to set the
|
|
||||||
stoploss "on exchange" which means stoploss order would be placed immediately once
|
|
||||||
the buy order is fulfilled.
|
|
||||||
|
|
||||||
`order_types` set in the configuration file overwrites values set in the strategy as a whole, so you need to configure the whole `order_types` dictionary in one place.
|
`order_types` set in the configuration file overwrites values set in the strategy as a whole, so you need to configure the whole `order_types` dictionary in one place.
|
||||||
|
|
||||||
If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
|
If this is configured, the following 4 values (`entry`, `exit`, `stoploss` and `stoploss_on_exchange`) need to be present, otherwise, the bot will fail to start.
|
||||||
`stoploss_on_exchange`) need to be present, otherwise, the bot will fail to start.
|
|
||||||
|
|
||||||
For information on (`emergencysell`,`forcesell`, `forcebuy`, `stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md)
|
For information on (`emergency_exit`,`force_exit`, `force_entry`, `stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md)
|
||||||
|
|
||||||
Syntax for Strategy:
|
Syntax for Strategy:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
order_types = {
|
order_types = {
|
||||||
"buy": "limit",
|
"entry": "limit",
|
||||||
"sell": "limit",
|
"exit": "limit",
|
||||||
"emergencysell": "market",
|
"emergency_exit": "market",
|
||||||
"forcebuy": "market",
|
"force_entry": "market",
|
||||||
"forcesell": "market",
|
"force_exit": "market",
|
||||||
"stoploss": "market",
|
"stoploss": "market",
|
||||||
"stoploss_on_exchange": False,
|
"stoploss_on_exchange": False,
|
||||||
"stoploss_on_exchange_interval": 60,
|
"stoploss_on_exchange_interval": 60,
|
||||||
|
@ -381,11 +458,11 @@ Configuration:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"order_types": {
|
"order_types": {
|
||||||
"buy": "limit",
|
"entry": "limit",
|
||||||
"sell": "limit",
|
"exit": "limit",
|
||||||
"emergencysell": "market",
|
"emergency_exit": "market",
|
||||||
"forcebuy": "market",
|
"force_entry": "market",
|
||||||
"forcesell": "market",
|
"force_exit": "market",
|
||||||
"stoploss": "market",
|
"stoploss": "market",
|
||||||
"stoploss_on_exchange": false,
|
"stoploss_on_exchange": false,
|
||||||
"stoploss_on_exchange_interval": 60
|
"stoploss_on_exchange_interval": 60
|
||||||
|
@ -408,7 +485,7 @@ Configuration:
|
||||||
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 `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: stoploss_on_exchange failures"
|
||||||
If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` 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
|
||||||
|
|
||||||
|
@ -438,8 +515,8 @@ The possible values are: `gtc` (default), `fok` or `ioc`.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
"order_time_in_force": {
|
"order_time_in_force": {
|
||||||
"buy": "gtc",
|
"entry": "gtc",
|
||||||
"sell": "gtc"
|
"exit": "gtc"
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -447,45 +524,6 @@ The possible values are: `gtc` (default), `fok` or `ioc`.
|
||||||
This is ongoing work. For now, it is supported only for binance and kucoin.
|
This is ongoing work. For now, it is supported only for binance and kucoin.
|
||||||
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
|
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
|
||||||
|
|
||||||
### Exchange configuration
|
|
||||||
|
|
||||||
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 cryptocurrency
|
|
||||||
exchange markets and trading APIs. The complete up-to-date list can be found in the
|
|
||||||
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python).
|
|
||||||
However, the bot was tested by the development team with only Bittrex, Binance and Kraken,
|
|
||||||
so these are the only officially supported exchanges:
|
|
||||||
|
|
||||||
- [Bittrex](https://bittrex.com/): "bittrex"
|
|
||||||
- [Binance](https://www.binance.com/): "binance"
|
|
||||||
- [Kraken](https://kraken.com/): "kraken"
|
|
||||||
|
|
||||||
Feel free to test other exchanges and submit your PR to improve the bot.
|
|
||||||
|
|
||||||
Some exchanges require special configuration, which can be found on the [Exchange-specific Notes](exchanges.md) documentation page.
|
|
||||||
|
|
||||||
#### Sample exchange configuration
|
|
||||||
|
|
||||||
A exchange configuration for "binance" would look as follows:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"exchange": {
|
|
||||||
"name": "binance",
|
|
||||||
"key": "your_exchange_key",
|
|
||||||
"secret": "your_exchange_secret",
|
|
||||||
"ccxt_config": {"enableRateLimit": true},
|
|
||||||
"ccxt_async_config": {
|
|
||||||
"enableRateLimit": true,
|
|
||||||
"rateLimit": 200
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
This configuration enables binance, as well as rate-limiting to avoid bans from the exchange.
|
|
||||||
`"rateLimit": 200` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
|
|
||||||
We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.
|
|
||||||
|
|
||||||
### What values can be used for fiat_display_currency?
|
### What values can be used for fiat_display_currency?
|
||||||
|
|
||||||
The `fiat_display_currency` configuration parameter sets the base currency to use for the
|
The `fiat_display_currency` configuration parameter sets the base currency to use for the
|
||||||
|
@ -524,10 +562,10 @@ creating trades on the exchange.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
"key": "key",
|
"key": "key",
|
||||||
"secret": "secret",
|
"secret": "secret",
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Analyzing bot data with Jupyter notebooks
|
# Analyzing bot data with Jupyter notebooks
|
||||||
|
|
||||||
You can analyze the results of backtests and trading history easily using Jupyter notebooks. Sample notebooks are located at `user_data/notebooks/` after initializing the user directory with `freqtrade create-userdir --userdir user_data`.
|
You can analyze the results of backtests and trading history easily using Jupyter notebooks. Sample notebooks are located at `user_data/notebooks/` after initializing the user directory with `freqtrade create-userdir --userdir user_data`.
|
||||||
|
|
||||||
## Quick start with docker
|
## Quick start with docker
|
||||||
|
|
||||||
|
@ -41,32 +41,35 @@ ipython kernel install --user --name=freqtrade
|
||||||
!!! Warning
|
!!! Warning
|
||||||
Some tasks don't work especially well in notebooks. For example, anything using asynchronous execution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required objects and parameters to helper functions. You may need to set those values or create expected objects manually.
|
Some tasks don't work especially well in notebooks. For example, anything using asynchronous execution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required objects and parameters to helper functions. You may need to set those values or create expected objects manually.
|
||||||
|
|
||||||
## Recommended workflow
|
## Recommended workflow
|
||||||
|
|
||||||
| Task | Tool |
|
| Task | Tool |
|
||||||
--- | ---
|
--- | ---
|
||||||
Bot operations | CLI
|
Bot operations | CLI
|
||||||
Repetitive tasks | Shell scripts
|
Repetitive tasks | Shell scripts
|
||||||
Data analysis & visualization | Notebook
|
Data analysis & visualization | Notebook
|
||||||
|
|
||||||
1. Use the CLI to
|
1. Use the CLI to
|
||||||
|
|
||||||
* download historical data
|
* download historical data
|
||||||
* run a backtest
|
* run a backtest
|
||||||
* run with real-time data
|
* run with real-time data
|
||||||
* export results
|
* export results
|
||||||
|
|
||||||
1. Collect these actions in shell scripts
|
1. Collect these actions in shell scripts
|
||||||
|
|
||||||
* save complicated commands with arguments
|
* save complicated commands with arguments
|
||||||
* execute multi-step operations
|
* execute multi-step operations
|
||||||
* automate testing strategies and preparing data for analysis
|
* automate testing strategies and preparing data for analysis
|
||||||
|
|
||||||
1. Use a notebook to
|
1. Use a notebook to
|
||||||
|
|
||||||
* visualize data
|
* visualize data
|
||||||
* munge and plot to generate insights
|
* mangle and plot to generate insights
|
||||||
|
|
||||||
## Example utility snippets
|
## Example utility snippets
|
||||||
|
|
||||||
### Change directory to root
|
### Change directory to root
|
||||||
|
|
||||||
Jupyter notebooks execute from the notebook directory. The following snippet searches for the project root, so relative paths remain consistent.
|
Jupyter notebooks execute from the notebook directory. The following snippet searches for the project root, so relative paths remain consistent.
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ Otherwise `--exchange` becomes mandatory.
|
||||||
You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used.
|
You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used.
|
||||||
|
|
||||||
!!! Tip "Tip: Updating existing data"
|
!!! Tip "Tip: Updating existing data"
|
||||||
If you already have backtesting data available in your data-directory and would like to refresh this data up to today, do not use `--days` or `--timerange` parameters. Freqtrade will keep the available data and only download the missing data.
|
If you already have backtesting data available in your data-directory and would like to refresh this data up to today, freqtrade will automatically calculate the data missing for the existing pairs and the download will occur from the latest available point until "now", neither --days or --timerange parameters are required. Freqtrade will keep the available data and only download the missing data.
|
||||||
If you are updating existing data after inserting new pairs that you have no data for, use `--new-pairs-days xx` parameter. Specified number of days will be downloaded for new pairs while old pairs will be updated with missing data only.
|
If you are updating existing data after inserting new pairs that you have no data for, use `--new-pairs-days xx` parameter. Specified number of days will be downloaded for new pairs while old pairs will be updated with missing data only.
|
||||||
If you use `--days xx` parameter alone - data for specified number of days will be downloaded for _all_ pairs. Be careful, if specified number of days is smaller than gap between now and last downloaded candle - freqtrade will delete all existing data to avoid gaps in candle data.
|
If you use `--days xx` parameter alone - data for specified number of days will be downloaded for _all_ pairs. Be careful, if specified number of days is smaller than gap between now and last downloaded candle - freqtrade will delete all existing data to avoid gaps in candle data.
|
||||||
|
|
||||||
|
@ -22,12 +22,14 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
[-d PATH] [--userdir PATH]
|
[-d PATH] [--userdir PATH]
|
||||||
[-p PAIRS [PAIRS ...]] [--pairs-file FILE]
|
[-p PAIRS [PAIRS ...]] [--pairs-file FILE]
|
||||||
[--days INT] [--new-pairs-days INT]
|
[--days INT] [--new-pairs-days INT]
|
||||||
|
[--include-inactive-pairs]
|
||||||
[--timerange TIMERANGE] [--dl-trades]
|
[--timerange TIMERANGE] [--dl-trades]
|
||||||
[--exchange EXCHANGE]
|
[--exchange EXCHANGE]
|
||||||
[-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]]
|
[-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]]
|
||||||
[--erase]
|
[--erase]
|
||||||
[--data-format-ohlcv {json,jsongz,hdf5}]
|
[--data-format-ohlcv {json,jsongz,hdf5}]
|
||||||
[--data-format-trades {json,jsongz,hdf5}]
|
[--data-format-trades {json,jsongz,hdf5}]
|
||||||
|
[--trading-mode {spot,margin,futures}]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
@ -38,6 +40,8 @@ optional arguments:
|
||||||
--days INT Download data for given number of days.
|
--days INT Download data for given number of days.
|
||||||
--new-pairs-days INT Download data of new pairs for given number of days.
|
--new-pairs-days INT Download data of new pairs for given number of days.
|
||||||
Default: `None`.
|
Default: `None`.
|
||||||
|
--include-inactive-pairs
|
||||||
|
Also download data from inactive pairs.
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
--dl-trades Download trades instead of OHLCV data. The bot will
|
--dl-trades Download trades instead of OHLCV data. The bot will
|
||||||
|
@ -52,10 +56,12 @@ optional arguments:
|
||||||
exchange/pairs/timeframes.
|
exchange/pairs/timeframes.
|
||||||
--data-format-ohlcv {json,jsongz,hdf5}
|
--data-format-ohlcv {json,jsongz,hdf5}
|
||||||
Storage format for downloaded candle (OHLCV) data.
|
Storage format for downloaded candle (OHLCV) data.
|
||||||
(default: `None`).
|
(default: `json`).
|
||||||
--data-format-trades {json,jsongz,hdf5}
|
--data-format-trades {json,jsongz,hdf5}
|
||||||
Storage format for downloaded trades data. (default:
|
Storage format for downloaded trades data. (default:
|
||||||
`None`).
|
`jsongz`).
|
||||||
|
--trading-mode {spot,margin,futures}
|
||||||
|
Select Trading mode
|
||||||
|
|
||||||
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).
|
||||||
|
@ -80,6 +86,82 @@ Common arguments:
|
||||||
|
|
||||||
For that reason, `download-data` does not care about the "startup-period" defined in a strategy. It's up to the user to download additional days if the backtest should start at a specific point in time (while respecting startup period).
|
For that reason, `download-data` does not care about the "startup-period" defined in a strategy. It's up to the user to download additional days if the backtest should start at a specific point in time (while respecting startup period).
|
||||||
|
|
||||||
|
### Pairs file
|
||||||
|
|
||||||
|
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
|
||||||
|
If you are using Binance for example:
|
||||||
|
|
||||||
|
- create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
|
||||||
|
- update the `pairs.json` file to contain the currency pairs you are interested in.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p user_data/data/binance
|
||||||
|
touch user_data/data/binance/pairs.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The format of the `pairs.json` file is a simple json list.
|
||||||
|
Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
|
||||||
|
|
||||||
|
``` json
|
||||||
|
[
|
||||||
|
"ETH/BTC",
|
||||||
|
"ETH/USDT",
|
||||||
|
"BTC/USDT",
|
||||||
|
"XRP/ETH"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Tip "Downloading all data for one quote currency"
|
||||||
|
Often, you'll want to download data for all pairs of a specific quote-currency. In such cases, you can use the following shorthand:
|
||||||
|
`freqtrade download-data --exchange binance --pairs .*/USDT <...>`. The provided "pairs" string will be expanded to contain all active pairs on the exchange.
|
||||||
|
To also download data for inactive (delisted) pairs, add `--include-inactive-pairs` to the command.
|
||||||
|
|
||||||
|
??? Note "Permission denied errors"
|
||||||
|
If your configuration directory `user_data` was made by docker, you may get the following error:
|
||||||
|
|
||||||
|
```
|
||||||
|
cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied
|
||||||
|
```
|
||||||
|
|
||||||
|
You can fix the permissions of your user-data directory as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo chown -R $UID:$GID user_data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start download
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade download-data --exchange binance
|
||||||
|
```
|
||||||
|
|
||||||
|
This will download historical candle (OHLCV) data for all the currency pairs you defined in `pairs.json`.
|
||||||
|
|
||||||
|
Alternatively, specify the pairs directly
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade download-data --exchange binance --pairs ETH/USDT XRP/USDT BTC/USDT
|
||||||
|
```
|
||||||
|
|
||||||
|
or as regex (to download all active USDT pairs)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade download-data --exchange binance --pairs .*/USDT
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Notes
|
||||||
|
|
||||||
|
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
|
||||||
|
- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.)
|
||||||
|
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
|
||||||
|
- To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
|
||||||
|
- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. Eventually set end dates are ignored.
|
||||||
|
- Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
|
||||||
|
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
||||||
|
|
||||||
|
|
||||||
### Data format
|
### Data format
|
||||||
|
|
||||||
Freqtrade currently supports 3 data-formats for both OHLCV and trades data:
|
Freqtrade currently supports 3 data-formats for both OHLCV and trades data:
|
||||||
|
@ -114,11 +196,14 @@ usage: freqtrade convert-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
{json,jsongz,hdf5} --format-to
|
{json,jsongz,hdf5} --format-to
|
||||||
{json,jsongz,hdf5} [--erase]
|
{json,jsongz,hdf5} [--erase]
|
||||||
[-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]]
|
[-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]]
|
||||||
|
[--exchange EXCHANGE]
|
||||||
|
[--trading-mode {spot,margin,futures}]
|
||||||
|
[--candle-types {spot,,futures,mark,index,premiumIndex,funding_rate} [{spot,,futures,mark,index,premiumIndex,funding_rate} ...]]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||||
Show profits for only these pairs. Pairs are space-
|
Limit command to these pairs. Pairs are space-
|
||||||
separated.
|
separated.
|
||||||
--format-from {json,jsongz,hdf5}
|
--format-from {json,jsongz,hdf5}
|
||||||
Source format for data conversion.
|
Source format for data conversion.
|
||||||
|
@ -129,6 +214,12 @@ optional arguments:
|
||||||
-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]
|
-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]
|
||||||
Specify which tickers to download. Space-separated
|
Specify which tickers to download. Space-separated
|
||||||
list. Default: `1m 5m`.
|
list. Default: `1m 5m`.
|
||||||
|
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
||||||
|
config is provided.
|
||||||
|
--trading-mode {spot,margin,futures}
|
||||||
|
Select Trading mode
|
||||||
|
--candle-types {spot,,futures,mark,index,premiumIndex,funding_rate} [{spot,,futures,mark,index,premiumIndex,funding_rate} ...]
|
||||||
|
Select candle type to use
|
||||||
|
|
||||||
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).
|
||||||
|
@ -145,6 +236,7 @@ Common arguments:
|
||||||
Path to directory with historical backtesting data.
|
Path to directory with historical backtesting data.
|
||||||
--userdir PATH, --user-data-dir PATH
|
--userdir PATH, --user-data-dir PATH
|
||||||
Path to userdata directory.
|
Path to userdata directory.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Example converting data
|
##### Example converting data
|
||||||
|
@ -204,6 +296,61 @@ It'll also remove original jsongz data files (`--erase` parameter).
|
||||||
freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase
|
freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Sub-command trades to ohlcv
|
||||||
|
|
||||||
|
When you need to use `--dl-trades` (kraken only) to download data, conversion of trades data to ohlcv data is the last step.
|
||||||
|
This command will allow you to repeat this last step for additional timeframes without re-downloading the data.
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: freqtrade trades-to-ohlcv [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
|
[-d PATH] [--userdir PATH]
|
||||||
|
[-p PAIRS [PAIRS ...]]
|
||||||
|
[-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]]
|
||||||
|
[--exchange EXCHANGE]
|
||||||
|
[--data-format-ohlcv {json,jsongz,hdf5}]
|
||||||
|
[--data-format-trades {json,jsongz,hdf5}]
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||||
|
Limit command to these pairs. Pairs are space-
|
||||||
|
separated.
|
||||||
|
-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]
|
||||||
|
Specify which tickers to download. Space-separated
|
||||||
|
list. Default: `1m 5m`.
|
||||||
|
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
||||||
|
config is provided.
|
||||||
|
--data-format-ohlcv {json,jsongz,hdf5}
|
||||||
|
Storage format for downloaded candle (OHLCV) data.
|
||||||
|
(default: `json`).
|
||||||
|
--data-format-trades {json,jsongz,hdf5}
|
||||||
|
Storage format for downloaded trades data. (default:
|
||||||
|
`jsongz`).
|
||||||
|
|
||||||
|
Common arguments:
|
||||||
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
--logfile FILE Log to the file specified. Special values are:
|
||||||
|
'syslog', 'journald'. See the documentation for more
|
||||||
|
details.
|
||||||
|
-V, --version show program's version number and exit
|
||||||
|
-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.
|
||||||
|
-d PATH, --datadir PATH
|
||||||
|
Path to directory with historical backtesting data.
|
||||||
|
--userdir PATH, --user-data-dir PATH
|
||||||
|
Path to userdata directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example trade-to-ohlcv conversion
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade trades-to-ohlcv --exchange kraken -t 5m 1h 1d --pairs BTC/EUR ETH/EUR
|
||||||
|
```
|
||||||
|
|
||||||
### Sub-command list-data
|
### Sub-command list-data
|
||||||
|
|
||||||
You can get a list of downloaded data using the `list-data` sub-command.
|
You can get a list of downloaded data using the `list-data` sub-command.
|
||||||
|
@ -213,6 +360,7 @@ usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||||
[--userdir PATH] [--exchange EXCHANGE]
|
[--userdir PATH] [--exchange EXCHANGE]
|
||||||
[--data-format-ohlcv {json,jsongz,hdf5}]
|
[--data-format-ohlcv {json,jsongz,hdf5}]
|
||||||
[-p PAIRS [PAIRS ...]]
|
[-p PAIRS [PAIRS ...]]
|
||||||
|
[--trading-mode {spot,margin,futures}]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
@ -222,8 +370,10 @@ optional arguments:
|
||||||
Storage format for downloaded candle (OHLCV) data.
|
Storage format for downloaded candle (OHLCV) data.
|
||||||
(default: `json`).
|
(default: `json`).
|
||||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||||
Show profits for only these pairs. Pairs are space-
|
Limit command to these pairs. Pairs are space-
|
||||||
separated.
|
separated.
|
||||||
|
--trading-mode {spot,margin,futures}
|
||||||
|
Select Trading mode
|
||||||
|
|
||||||
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).
|
||||||
|
@ -257,64 +407,6 @@ ETH/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
|
||||||
ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
|
ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pairs file
|
|
||||||
|
|
||||||
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
|
|
||||||
|
|
||||||
If you are using Binance for example:
|
|
||||||
|
|
||||||
- create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
|
|
||||||
- update the `pairs.json` file to contain the currency pairs you are interested in.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p user_data/data/binance
|
|
||||||
cp tests/testdata/pairs.json user_data/data/binance
|
|
||||||
```
|
|
||||||
|
|
||||||
If your configuration directory `user_data` was made by docker, you may get the following error:
|
|
||||||
|
|
||||||
```
|
|
||||||
cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied
|
|
||||||
```
|
|
||||||
|
|
||||||
You can fix the permissions of your user-data directory as follows:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo chown -R $UID:$GID user_data
|
|
||||||
```
|
|
||||||
|
|
||||||
The format of the `pairs.json` file is a simple json list.
|
|
||||||
Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
|
|
||||||
|
|
||||||
``` json
|
|
||||||
[
|
|
||||||
"ETH/BTC",
|
|
||||||
"ETH/USDT",
|
|
||||||
"BTC/USDT",
|
|
||||||
"XRP/ETH"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Start download
|
|
||||||
|
|
||||||
Then run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade download-data --exchange binance
|
|
||||||
```
|
|
||||||
|
|
||||||
This will download historical candle (OHLCV) data for all the currency pairs you defined in `pairs.json`.
|
|
||||||
|
|
||||||
### Other Notes
|
|
||||||
|
|
||||||
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
|
|
||||||
- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.)
|
|
||||||
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
|
|
||||||
- To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
|
|
||||||
- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. Eventually set end dates are ignored.
|
|
||||||
- Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
|
|
||||||
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
|
||||||
|
|
||||||
### 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. Some exchanges also provide historic trade-data via their API.
|
||||||
|
|
|
@ -15,8 +15,8 @@ This command line option was deprecated in 2019.7-dev (develop branch) and remov
|
||||||
|
|
||||||
### The **--dynamic-whitelist** command line option
|
### The **--dynamic-whitelist** command line option
|
||||||
|
|
||||||
This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch)
|
This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch) and in freqtrade 2019.7.
|
||||||
and in freqtrade 2019.7.
|
Please refer to [pairlists](plugins.md#pairlists-and-pairlist-handlers) instead.
|
||||||
|
|
||||||
### the `--live` command line option
|
### the `--live` command line option
|
||||||
|
|
||||||
|
@ -24,6 +24,10 @@ and in freqtrade 2019.7.
|
||||||
Did only download the latest 500 candles, so was ineffective in getting good backtest data.
|
Did only download the latest 500 candles, so was ineffective in getting good backtest data.
|
||||||
Removed in 2019-7-dev (develop branch) and in freqtrade 2019.8.
|
Removed in 2019-7-dev (develop branch) and in freqtrade 2019.8.
|
||||||
|
|
||||||
|
### `ticker_interval` (now `timeframe`)
|
||||||
|
|
||||||
|
Support for `ticker_interval` terminology was deprecated in 2020.6 in favor of `timeframe` - and compatibility code was removed in 2022.3.
|
||||||
|
|
||||||
### Allow running multiple pairlists in sequence
|
### Allow running multiple pairlists in sequence
|
||||||
|
|
||||||
The former `"pairlist"` section in the configuration has been removed, and is replaced by `"pairlists"` - being a list to specify a sequence of pairlists.
|
The former `"pairlist"` section in the configuration has been removed, and is replaced by `"pairlists"` - being a list to specify a sequence of pairlists.
|
||||||
|
@ -34,7 +38,7 @@ The old section of configuration parameters (`"pairlist"`) has been deprecated i
|
||||||
|
|
||||||
Since only quoteVolume can be compared between assets, the other options (bidVolume, askVolume) have been deprecated in 2020.4, and have been removed in 2020.9.
|
Since only quoteVolume can be compared between assets, the other options (bidVolume, askVolume) have been deprecated in 2020.4, and have been removed in 2020.9.
|
||||||
|
|
||||||
### Using order book steps for sell price
|
### Using order book steps for exit price
|
||||||
|
|
||||||
Using `order_book_min` and `order_book_max` used to allow stepping the orderbook and trying to find the next ROI slot - trying to place sell-orders early.
|
Using `order_book_min` and `order_book_max` used to allow stepping the orderbook and trying to find the next ROI slot - trying to place sell-orders early.
|
||||||
As this does however increase risk and provides no benefit, it's been removed for maintainability purposes in 2021.7.
|
As this does however increase risk and provides no benefit, it's been removed for maintainability purposes in 2021.7.
|
||||||
|
@ -43,3 +47,30 @@ As this does however increase risk and provides no benefit, it's been removed fo
|
||||||
|
|
||||||
Using separate hyperopt files was deprecated in 2021.4 and was removed in 2021.9.
|
Using separate hyperopt files was deprecated in 2021.4 and was removed in 2021.9.
|
||||||
Please switch to the new [Parametrized Strategies](hyperopt.md) to benefit from the new hyperopt interface.
|
Please switch to the new [Parametrized Strategies](hyperopt.md) to benefit from the new hyperopt interface.
|
||||||
|
|
||||||
|
## Strategy changes between V2 and V3
|
||||||
|
|
||||||
|
Isolated Futures / short trading was introduced in 2022.4. This required major changes to configuration settings, strategy interfaces, ...
|
||||||
|
|
||||||
|
We have put a great effort into keeping compatibility with existing strategies, so if you just want to continue using freqtrade in spot markets, there are no changes necessary.
|
||||||
|
While we may drop support for the current interface sometime in the future, we will announce this separately and have an appropriate transition period.
|
||||||
|
|
||||||
|
Please follow the [Strategy migration](strategy_migration.md) guide to migrate your strategy to the new format to start using the new functionalities.
|
||||||
|
|
||||||
|
### webhooks - changes with 2022.4
|
||||||
|
|
||||||
|
#### `buy_tag` has been renamed to `enter_tag`
|
||||||
|
|
||||||
|
This should apply only to your strategy and potentially to webhooks.
|
||||||
|
We will keep a compatibility layer for 1-2 versions (so both `buy_tag` and `enter_tag` will still work), but support for this in webhooks will disappear after that.
|
||||||
|
|
||||||
|
#### Naming changes
|
||||||
|
|
||||||
|
Webhook terminology changed from "sell" to "exit", and from "buy" to "entry".
|
||||||
|
|
||||||
|
* `webhookbuy` -> `webhookentry`
|
||||||
|
* `webhookbuyfill` -> `webhookentryfill`
|
||||||
|
* `webhookbuycancel` -> `webhookentrycancel`
|
||||||
|
* `webhooksell` -> `webhookexit`
|
||||||
|
* `webhooksellfill` -> `webhookexitfill`
|
||||||
|
* `webhooksellcancel` -> `webhookexitcancel`
|
||||||
|
|
|
@ -8,7 +8,7 @@ All contributions, bug reports, bug fixes, documentation improvements, enhanceme
|
||||||
|
|
||||||
Documentation is available at [https://freqtrade.io](https://www.freqtrade.io/) and needs to be provided with every new feature PR.
|
Documentation is available at [https://freqtrade.io](https://www.freqtrade.io/) and needs to be provided with every new feature PR.
|
||||||
|
|
||||||
Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/extensions/admonition/).
|
Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/reference/admonitions/).
|
||||||
|
|
||||||
To test the documentation locally use the following commands.
|
To test the documentation locally use the following commands.
|
||||||
|
|
||||||
|
@ -26,6 +26,11 @@ Alternatively (e.g. if your system is not supported by the setup.sh script), fol
|
||||||
|
|
||||||
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
|
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
|
||||||
|
|
||||||
|
Then install the git hook scripts by running `pre-commit install`, so your changes will be verified locally before committing.
|
||||||
|
This avoids a lot of waiting for CI already, as some basic formatting checks are done locally on your machine.
|
||||||
|
|
||||||
|
Before opening a pull request, please familiarize yourself with our [Contributing Guidelines](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md).
|
||||||
|
|
||||||
### Devcontainer setup
|
### Devcontainer setup
|
||||||
|
|
||||||
The fastest and easiest way to get started is to use [VSCode](https://code.visualstudio.com/) with the Remote container extension.
|
The fastest and easiest way to get started is to use [VSCode](https://code.visualstudio.com/) with the Remote container extension.
|
||||||
|
@ -218,13 +223,13 @@ Protections can have 2 different ways to stop trading for a limited :
|
||||||
##### Protections - per pair
|
##### Protections - per pair
|
||||||
|
|
||||||
Protections that implement the per pair approach must set `has_local_stop=True`.
|
Protections that implement the per pair approach must set `has_local_stop=True`.
|
||||||
The method `stop_per_pair()` will be called whenever a trade closed (sell order completed).
|
The method `stop_per_pair()` will be called whenever a trade closed (exit order completed).
|
||||||
|
|
||||||
##### Protections - global protection
|
##### Protections - global protection
|
||||||
|
|
||||||
These Protections should do their evaluation across all pairs, and consequently will also lock all pairs from trading (called a global PairLock).
|
These Protections should do their evaluation across all pairs, and consequently will also lock all pairs from trading (called a global PairLock).
|
||||||
Global protection must set `has_global_stop=True` to be evaluated for global stops.
|
Global protection must set `has_global_stop=True` to be evaluated for global stops.
|
||||||
The method `global_stop()` will be called whenever a trade closed (sell order completed).
|
The method `global_stop()` will be called whenever a trade closed (exit order completed).
|
||||||
|
|
||||||
##### Protections - calculating lock end time
|
##### Protections - calculating lock end time
|
||||||
|
|
||||||
|
@ -250,7 +255,23 @@ 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 `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/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 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).
|
||||||
|
|
||||||
|
These are prerequisites to have an exchange listed as either Supported or Community tested (listed on the homepage).
|
||||||
|
The below are "extras", which will make an exchange better (feature-complete) - but are not absolutely necessary for either of the 2 categories.
|
||||||
|
|
||||||
|
Additional tests / steps to complete:
|
||||||
|
|
||||||
|
* Verify data provided by `fetch_ohlcv()` - and eventually adjust `ohlcv_candle_limit` for this exchange
|
||||||
|
* Check L2 orderbook limit range (API documentation) - and eventually set as necessary
|
||||||
|
* Check if balance shows correctly (*)
|
||||||
|
* Create market order (*)
|
||||||
|
* Create limit order (*)
|
||||||
|
* Complete trade (enter + exit) (*)
|
||||||
|
* Compare result calculation between exchange and bot
|
||||||
|
* Ensure fees are applied correctly (check the database against the exchange)
|
||||||
|
|
||||||
|
(*) Requires API keys and Balance on the exchange.
|
||||||
|
|
||||||
### Stoploss On Exchange
|
### Stoploss On Exchange
|
||||||
|
|
||||||
|
@ -306,9 +327,8 @@ jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade
|
||||||
This documents some decisions taken for the CI Pipeline.
|
This documents some decisions taken for the CI Pipeline.
|
||||||
|
|
||||||
* CI runs on all OS variants, Linux (ubuntu), macOS and Windows.
|
* CI runs on all OS variants, Linux (ubuntu), macOS and Windows.
|
||||||
* Docker images are build for the branches `stable` and `develop`.
|
* Docker images are build for the branches `stable` and `develop`, and are built as multiarch builds, supporting multiple platforms via the same tag.
|
||||||
* Docker images containing Plot dependencies are also available as `stable_plot` and `develop_plot`.
|
* Docker images containing Plot dependencies are also available as `stable_plot` and `develop_plot`.
|
||||||
* Raspberry PI Docker images are postfixed with `_pi` - so tags will be `:stable_pi` and `develop_pi`.
|
|
||||||
* Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of.
|
* Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of.
|
||||||
* Full docker image rebuilds are run once a week via schedule.
|
* Full docker image rebuilds are run once a week via schedule.
|
||||||
* Deployments run on ubuntu.
|
* Deployments run on ubuntu.
|
||||||
|
|
|
@ -70,6 +70,18 @@ docker-compose up -d
|
||||||
!!! Warning "Default configuration"
|
!!! Warning "Default configuration"
|
||||||
While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot.
|
While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot.
|
||||||
|
|
||||||
|
#### Accessing the UI
|
||||||
|
|
||||||
|
If you've selected to enable FreqUI in the `new-config` step, you will have freqUI available at port `localhost:8080`.
|
||||||
|
|
||||||
|
You can now access the UI by typing localhost:8080 in your browser.
|
||||||
|
|
||||||
|
??? Note "UI Access on a remote servers"
|
||||||
|
If you're running on a VPS, you should consider using either a ssh tunnel, or setup a VPN (openVPN, wireguard) to connect to your bot.
|
||||||
|
This will ensure that freqUI is not directly exposed to the internet, which is not recommended for security reasons (freqUI does not support https out of the box).
|
||||||
|
Setup of these tools is not part of this tutorial, however many good tutorials can be found on the internet.
|
||||||
|
Please also read the [API configuration with docker](rest-api.md#configuration-with-docker) section to learn more about this configuration.
|
||||||
|
|
||||||
#### Monitoring the bot
|
#### Monitoring the bot
|
||||||
|
|
||||||
You can check for running instances with `docker-compose ps`.
|
You can check for running instances with `docker-compose ps`.
|
||||||
|
@ -109,10 +121,17 @@ All freqtrade arguments will be available by running `docker-compose run --rm fr
|
||||||
!!! Warning "`docker-compose` for trade commands"
|
!!! Warning "`docker-compose` for trade commands"
|
||||||
Trade commands (`freqtrade trade <...>`) should not be ran via `docker-compose run` - but should use `docker-compose up -d` instead.
|
Trade commands (`freqtrade trade <...>`) should not be ran via `docker-compose run` - but should use `docker-compose up -d` instead.
|
||||||
This makes sure that the container is properly started (including port forwardings) and will make sure that the container will restart after a system reboot.
|
This makes sure that the container is properly started (including port forwardings) and will make sure that the container will restart after a system reboot.
|
||||||
|
If you intend to use freqUI, please also ensure to adjust the [configuration accordingly](rest-api.md#configuration-with-docker), otherwise the UI will not be available.
|
||||||
|
|
||||||
!!! Note "`docker-compose run --rm`"
|
!!! Note "`docker-compose run --rm`"
|
||||||
Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command).
|
Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command).
|
||||||
|
|
||||||
|
??? Note "Using docker without docker-compose"
|
||||||
|
"`docker-compose run --rm`" will require a compose file to be provided.
|
||||||
|
Some freqtrade commands that don't require authentication such as `list-pairs` can be run with "`docker run --rm`" instead.
|
||||||
|
For example `docker run --rm freqtradeorg/freqtrade:stable list-pairs --exchange binance --quote BTC --print-json`.
|
||||||
|
This can be useful for fetching exchange information to add to your `config.json` without affecting your running containers.
|
||||||
|
|
||||||
#### Example: Download data with docker-compose
|
#### Example: Download data with docker-compose
|
||||||
|
|
||||||
Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host.
|
Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host.
|
||||||
|
@ -147,27 +166,9 @@ You'll then also need to modify the `docker-compose.yml` file and uncomment the
|
||||||
dockerfile: "./Dockerfile.<yourextension>"
|
dockerfile: "./Dockerfile.<yourextension>"
|
||||||
```
|
```
|
||||||
|
|
||||||
You can then run `docker-compose build` to build the docker image, and run it using the commands described above.
|
You can then run `docker-compose build --pull` to build the docker image, and run it using the commands described above.
|
||||||
|
|
||||||
### Troubleshooting
|
### Plotting with docker-compose
|
||||||
|
|
||||||
#### Docker on Windows
|
|
||||||
|
|
||||||
* Error: `"Timestamp for this request is outside of the recvWindow."`
|
|
||||||
* The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past.
|
|
||||||
To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so).
|
|
||||||
A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler.
|
|
||||||
```
|
|
||||||
taskkill /IM "Docker Desktop.exe" /F
|
|
||||||
wsl --shutdown
|
|
||||||
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Warning
|
|
||||||
Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting.
|
|
||||||
Best use a linux-VPS for running freqtrade reliably.
|
|
||||||
|
|
||||||
## Plotting with docker-compose
|
|
||||||
|
|
||||||
Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file.
|
Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file.
|
||||||
You can then use these commands as follows:
|
You can then use these commands as follows:
|
||||||
|
@ -178,7 +179,7 @@ docker-compose run --rm freqtrade plot-dataframe --strategy AwesomeStrategy -p B
|
||||||
|
|
||||||
The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser.
|
The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser.
|
||||||
|
|
||||||
## Data analysis using docker compose
|
### Data analysis using docker compose
|
||||||
|
|
||||||
Freqtrade provides a docker-compose file which starts up a jupyter lab server.
|
Freqtrade provides a docker-compose file which starts up a jupyter lab server.
|
||||||
You can run this server using the following command:
|
You can run this server using the following command:
|
||||||
|
@ -195,3 +196,22 @@ Since part of this image is built on your machine, it is recommended to rebuild
|
||||||
``` bash
|
``` bash
|
||||||
docker-compose -f docker/docker-compose-jupyter.yml build --no-cache
|
docker-compose -f docker/docker-compose-jupyter.yml build --no-cache
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Docker on Windows
|
||||||
|
|
||||||
|
* Error: `"Timestamp for this request is outside of the recvWindow."`
|
||||||
|
* The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past.
|
||||||
|
To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so).
|
||||||
|
A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
taskkill /IM "Docker Desktop.exe" /F
|
||||||
|
wsl --shutdown
|
||||||
|
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting.
|
||||||
|
Best use a linux-VPS for running freqtrade reliably.
|
||||||
|
|
|
@ -222,7 +222,7 @@ usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
|
|
|
@ -1,19 +1,91 @@
|
||||||
# Exchange-specific Notes
|
# Exchange-specific Notes
|
||||||
|
|
||||||
This page combines common gotchas and informations which are exchange-specific and most likely don't apply to other exchanges.
|
This page combines common gotchas and Information which are exchange-specific and most likely don't apply to other exchanges.
|
||||||
|
|
||||||
|
## Exchange configuration
|
||||||
|
|
||||||
|
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 cryptocurrency
|
||||||
|
exchange markets and trading APIs. The complete up-to-date list can be found in the
|
||||||
|
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python).
|
||||||
|
However, the bot was tested by the development team with only a few exchanges.
|
||||||
|
A current list of these can be found in the "Home" section of this documentation.
|
||||||
|
|
||||||
|
Feel free to test other exchanges and submit your feedback or PR to improve the bot or confirm exchanges that work flawlessly..
|
||||||
|
|
||||||
|
Some exchanges require special configuration, which can be found below.
|
||||||
|
|
||||||
|
### Sample exchange configuration
|
||||||
|
|
||||||
|
A exchange configuration for "binance" would look as follows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"exchange": {
|
||||||
|
"name": "binance",
|
||||||
|
"key": "your_exchange_key",
|
||||||
|
"secret": "your_exchange_secret",
|
||||||
|
"ccxt_config": {},
|
||||||
|
"ccxt_async_config": {},
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setting rate limits
|
||||||
|
|
||||||
|
Usually, rate limits set by CCXT are reliable and work well.
|
||||||
|
In case of problems related to rate-limits (usually DDOS Exceptions in your logs), it's easy to change rateLimit settings to other values.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"exchange": {
|
||||||
|
"name": "kraken",
|
||||||
|
"key": "your_exchange_key",
|
||||||
|
"secret": "your_exchange_secret",
|
||||||
|
"ccxt_config": {"enableRateLimit": true},
|
||||||
|
"ccxt_async_config": {
|
||||||
|
"enableRateLimit": true,
|
||||||
|
"rateLimit": 3100
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
This configuration enables kraken, as well as rate-limiting to avoid bans from the exchange.
|
||||||
|
`"rateLimit": 3100` defines a wait-event of 3.1s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
|
||||||
|
We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.
|
||||||
|
|
||||||
## Binance
|
## Binance
|
||||||
|
|
||||||
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
Binance supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange..
|
||||||
|
|
||||||
### Binance Blacklist
|
### Binance Blacklist
|
||||||
|
|
||||||
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
|
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
|
||||||
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore.
|
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore.
|
||||||
|
|
||||||
|
### Binance Futures
|
||||||
|
|
||||||
|
Binance has specific (unfortunately complex) [Futures Trading Quantitative Rules](https://www.binance.com/en/support/faq/4f462ebe6ff445d4a170be7d9e897272) which need to be followed, and which prohibit a too low stake-amount (among others) for too many orders.
|
||||||
|
Violating these rules will result in a trading restriction.
|
||||||
|
|
||||||
|
When trading on Binance Futures market, orderbook must be used because there is no price ticker data for futures.
|
||||||
|
|
||||||
|
``` jsonc
|
||||||
|
"entry_pricing": {
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": false,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exit_pricing": {
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
### 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.
|
||||||
|
@ -127,11 +199,45 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th
|
||||||
|
|
||||||
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Kucoin supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||||
|
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used.
|
||||||
|
|
||||||
### Kucoin Blacklists
|
### Kucoin Blacklists
|
||||||
|
|
||||||
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
||||||
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
|
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
|
||||||
|
|
||||||
|
## Huobi
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Huobi supports `stoploss_on_exchange` and uses `stop-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||||
|
|
||||||
|
## OKX (former OKEX)
|
||||||
|
|
||||||
|
OKX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"exchange": {
|
||||||
|
"name": "okx",
|
||||||
|
"key": "your_exchange_key",
|
||||||
|
"secret": "your_exchange_secret",
|
||||||
|
"password": "your_exchange_api_key_password",
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
OKX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode.
|
||||||
|
|
||||||
|
## Gate.io
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Gate.io supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange..
|
||||||
|
|
||||||
|
Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0).
|
||||||
|
The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value.
|
||||||
|
|
||||||
## All exchanges
|
## All exchanges
|
||||||
|
|
||||||
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
||||||
|
|
67
docs/faq.md
67
docs/faq.md
|
@ -6,13 +6,15 @@ Freqtrade supports spot trading only.
|
||||||
|
|
||||||
### Can I open short positions?
|
### Can I open short positions?
|
||||||
|
|
||||||
No, Freqtrade does not support trading with margin / leverage, and cannot open short positions.
|
Freqtrade can open short positions in futures markets.
|
||||||
|
This requires the strategy to be made for this - and `"trading_mode": "futures"` in the configuration.
|
||||||
|
Please make sure to read the [relevant documentation page](leverage.md) first.
|
||||||
|
|
||||||
In some cases, your exchange may provide leveraged spot tokens which can be traded with Freqtrade eg. BTCUP/USD, BTCDOWN/USD, ETHBULL/USD, ETHBEAR/USD, etc...
|
In spot markets, you can in some cases use leveraged spot tokens, which reflect an inverted pair (eg. BTCUP/USD, BTCDOWN/USD, ETHBULL/USD, ETHBEAR/USD,...) which can be traded with Freqtrade.
|
||||||
|
|
||||||
### Can I trade options or futures?
|
### Can I trade options or futures?
|
||||||
|
|
||||||
No, options and futures trading are not supported.
|
Futures trading is supported for selected exchanges.
|
||||||
|
|
||||||
## Beginner Tips & Tricks
|
## Beginner Tips & Tricks
|
||||||
|
|
||||||
|
@ -42,7 +44,7 @@ position for a trade. Be patient!
|
||||||
### I have made 12 trades already, why is my total profit negative?
|
### I have made 12 trades already, why is my total profit negative?
|
||||||
|
|
||||||
I understand your disappointment but unfortunately 12 trades is just
|
I understand your disappointment but unfortunately 12 trades is just
|
||||||
not enough to say anything. If you run backtesting, you can see that our
|
not enough to say anything. If you run backtesting, you can see that the
|
||||||
current algorithm does leave you on the plus side, but that is after
|
current algorithm does leave you on the plus side, but that is after
|
||||||
thousands of trades and even there, you will be left with losses on
|
thousands of trades and even there, you will be left with losses on
|
||||||
specific coins that you have traded tens if not hundreds of times. We
|
specific coins that you have traded tens if not hundreds of times. We
|
||||||
|
@ -54,13 +56,30 @@ you can't say much from few trades.
|
||||||
|
|
||||||
Yes. You can edit your config and use the `/reload_config` command to reload the configuration. The bot will stop, reload the configuration and strategy and will restart with the new configuration and strategy.
|
Yes. You can edit your config and use the `/reload_config` command to reload the configuration. The bot will stop, reload the configuration and strategy and will restart with the new configuration and strategy.
|
||||||
|
|
||||||
### I want to improve the bot with a new strategy
|
### Why does my bot not sell everything it bought?
|
||||||
|
|
||||||
That's great. We have a nice backtesting and hyperoptimization setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands).
|
This is called "coin dust" and can happen on all exchanges.
|
||||||
|
It happens because many exchanges subtract fees from the "receiving currency" - so you buy 100 COIN - but you only get 99.9 COIN.
|
||||||
|
As COIN is trading in full lot sizes (1COIN steps), you cannot sell 0.9 COIN (or 99.9 COIN) - but you need to round down to 99 COIN.
|
||||||
|
|
||||||
|
This is not a bot-problem, but will also happen while manual trading.
|
||||||
|
|
||||||
|
While freqtrade can handle this (it'll sell 99 COIN), fees are often below the minimum tradable lot-size (you can only trade full COIN, not 0.9 COIN).
|
||||||
|
Leaving the dust (0.9 COIN) on the exchange makes usually sense, as the next time freqtrade buys COIN, it'll eat into the remaining small balance, this time selling everything it bought, and therefore slowly declining the dust balance (although it most likely will never reach exactly 0).
|
||||||
|
|
||||||
|
Where possible (e.g. on binance), the use of the exchange's dedicated fee currency will fix this.
|
||||||
|
On binance, it's sufficient to have BNB in your account, and have "Pay fees in BNB" enabled in your profile. Your BNB balance will slowly decline (as it's used to pay fees) - but you'll no longer encounter dust (Freqtrade will include the fees in the profit calculations).
|
||||||
|
Other exchanges don't offer such possibilities, where it's simply something you'll have to accept or move to a different exchange.
|
||||||
|
|
||||||
|
### I want to use incomplete candles
|
||||||
|
|
||||||
|
Freqtrade will not provide incomplete candles to strategies. Using incomplete candles will lead to repainting and consequently to strategies with "ghost" buys, which are impossible to both backtest, and verify after they happened.
|
||||||
|
|
||||||
|
You can use "current" market data by using the [dataprovider](strategy-customization.md#orderbookpair-maximum)'s orderbook or ticker methods - which however cannot be used during backtesting.
|
||||||
|
|
||||||
### Is there a setting to only SELL the coins being held and not perform anymore BUYS?
|
### Is there a setting to only SELL the coins being held and not perform anymore BUYS?
|
||||||
|
|
||||||
You can use the `/stopbuy` command in Telegram to prevent future buys, followed by `/forcesell all` (sell all open trades).
|
You can use the `/stopbuy` command in Telegram to prevent future buys, followed by `/forceexit all` (sell all open trades).
|
||||||
|
|
||||||
### I want to run multiple bots on the same machine
|
### I want to run multiple bots on the same machine
|
||||||
|
|
||||||
|
@ -76,22 +95,34 @@ If this happens for all pairs in the pairlist, this might indicate a recent exch
|
||||||
|
|
||||||
Irrespectively of the reason, Freqtrade will fill up these candles with "empty" candles, where open, high, low and close are set to the previous candle close - and volume is empty. In a chart, this will look like a `_` - and is aligned with how exchanges usually represent 0 volume candles.
|
Irrespectively of the reason, Freqtrade will fill up these candles with "empty" candles, where open, high, low and close are set to the previous candle close - and volume is empty. In a chart, this will look like a `_` - and is aligned with how exchanges usually represent 0 volume candles.
|
||||||
|
|
||||||
|
### I'm getting "Outdated history for pair xxx" in the log
|
||||||
|
|
||||||
|
The bot is trying to tell you that it got an outdated last candle (not the last complete candle).
|
||||||
|
As a consequence, Freqtrade will not enter a trade for this pair - as trading on old information is usually not what is desired.
|
||||||
|
|
||||||
|
This warning can point to one of the below problems:
|
||||||
|
|
||||||
|
* Exchange downtime -> Check your exchange status page / blog / twitter feed for details.
|
||||||
|
* Wrong system time -> Ensure your system-time is correct.
|
||||||
|
* Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling.
|
||||||
|
* API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges).
|
||||||
|
|
||||||
### I'm getting the "RESTRICTED_MARKET" message in the log
|
### I'm getting the "RESTRICTED_MARKET" message in the log
|
||||||
|
|
||||||
Currently known to happen for US Bittrex users.
|
Currently known to happen for US Bittrex users.
|
||||||
|
|
||||||
Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information.
|
Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information.
|
||||||
|
|
||||||
### I'm getting the "Exchange Bittrex does not support market orders." message and cannot run my strategy
|
### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy
|
||||||
|
|
||||||
As the message says, Bittrex does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex).
|
As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex and Gate.io).
|
||||||
|
|
||||||
To fix it for Bittrex, redefine order types in the strategy to use "limit" instead of "market":
|
To fix this, redefine order types in the strategy to use "limit" instead of "market":
|
||||||
|
|
||||||
```
|
``` python
|
||||||
order_types = {
|
order_types = {
|
||||||
...
|
...
|
||||||
'stoploss': 'limit',
|
"stoploss": "limit",
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -136,6 +167,8 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us
|
||||||
> type \path\to\mylogfile.log | findstr "something"
|
> type \path\to\mylogfile.log | findstr "something"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Hyperopt module
|
||||||
|
|
||||||
### Why does freqtrade not have GPU support?
|
### Why does freqtrade not have GPU support?
|
||||||
|
|
||||||
First of all, most indicator libraries don't have GPU support - as such, there would be little benefit for indicator calculations.
|
First of all, most indicator libraries don't have GPU support - as such, there would be little benefit for indicator calculations.
|
||||||
|
@ -152,19 +185,17 @@ The benefit of using GPU would therefore be pretty slim - and will not justify t
|
||||||
|
|
||||||
There is however nothing preventing you from using GPU-enabled indicators within your strategy if you think you must have this - you will however probably be disappointed by the slim gain that will give you (compared to the complexity).
|
There is however nothing preventing you from using GPU-enabled indicators within your strategy if you think you must have this - you will however probably be disappointed by the slim gain that will give you (compared to the complexity).
|
||||||
|
|
||||||
## Hyperopt module
|
|
||||||
|
|
||||||
### How many epochs do I need to get a good Hyperopt result?
|
### How many epochs do I need to get a good Hyperopt result?
|
||||||
|
|
||||||
Per default Hyperopt called without the `-e`/`--epochs` command line option will only
|
Per default Hyperopt called without the `-e`/`--epochs` command line option will only
|
||||||
run 100 epochs, means 100 evaluations of your triggers, guards, ... Too few
|
run 100 epochs, means 100 evaluations of your triggers, guards, ... Too few
|
||||||
to find a great result (unless if you are very lucky), so you probably
|
to find a great result (unless if you are very lucky), so you probably
|
||||||
have to run it for 10.000 or more. But it will take an eternity to
|
have to run it for 10000 or more. But it will take an eternity to
|
||||||
compute.
|
compute.
|
||||||
|
|
||||||
Since hyperopt uses Bayesian search, running for too many epochs may not produce greater results.
|
Since hyperopt uses Bayesian search, running for too many epochs may not produce greater results.
|
||||||
|
|
||||||
It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going.
|
It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000
|
freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000
|
||||||
|
@ -188,9 +219,9 @@ already 8\*10^9\*10 evaluations. A roughly total of 80 billion evaluations.
|
||||||
Did you run 100 000 evaluations? Congrats, you've done roughly 1 / 100 000 th
|
Did you run 100 000 evaluations? Congrats, you've done roughly 1 / 100 000 th
|
||||||
of the search space, assuming that the bot never tests the same parameters more than once.
|
of the search space, assuming that the bot never tests the same parameters more than once.
|
||||||
|
|
||||||
* The time it takes to run 1000 hyperopt epochs depends on things like: The available cpu, hard-disk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 10.0000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades.
|
* The time it takes to run 1000 hyperopt epochs depends on things like: The available cpu, hard-disk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 100000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades.
|
||||||
|
|
||||||
Example: 4% profit 650 times vs 0,3% profit a trade 10.000 times in a year. If we assume you set the --timerange to 365 days.
|
Example: 4% profit 650 times vs 0,3% profit a trade 10000 times in a year. If we assume you set the --timerange to 365 days.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
`freqtrade --config config.json --strategy SampleStrategy --hyperopt SampleHyperopt -e 1000 --timerange 20190601-20200601`
|
`freqtrade --config config.json --strategy SampleStrategy --hyperopt SampleHyperopt -e 1000 --timerange 20190601-20200601`
|
||||||
|
|
102
docs/hyperopt.md
102
docs/hyperopt.md
|
@ -51,16 +51,17 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||||
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
||||||
[--random-state INT] [--min-trades INT]
|
[--random-state INT] [--min-trades INT]
|
||||||
[--hyperopt-loss NAME] [--disable-param-export]
|
[--hyperopt-loss NAME] [--disable-param-export]
|
||||||
|
[--ignore-missing-spaces]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
--data-format-ohlcv {json,jsongz,hdf5}
|
--data-format-ohlcv {json,jsongz,hdf5}
|
||||||
Storage format for downloaded candle (OHLCV) data.
|
Storage format for downloaded candle (OHLCV) data.
|
||||||
(default: `None`).
|
(default: `json`).
|
||||||
--max-open-trades INT
|
--max-open-trades INT
|
||||||
Override the value of the `max_open_trades`
|
Override the value of the `max_open_trades`
|
||||||
configuration setting.
|
configuration setting.
|
||||||
|
@ -114,9 +115,13 @@ optional arguments:
|
||||||
Hyperopt-loss-functions are:
|
Hyperopt-loss-functions are:
|
||||||
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
|
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
|
||||||
SharpeHyperOptLoss, SharpeHyperOptLossDaily,
|
SharpeHyperOptLoss, SharpeHyperOptLossDaily,
|
||||||
SortinoHyperOptLoss, SortinoHyperOptLossDaily
|
SortinoHyperOptLoss, SortinoHyperOptLossDaily,
|
||||||
|
CalmarHyperOptLoss, MaxDrawDownHyperOptLoss, ProfitDrawDownHyperOptLoss
|
||||||
--disable-param-export
|
--disable-param-export
|
||||||
Disable automatic hyperopt parameter export.
|
Disable automatic hyperopt parameter export.
|
||||||
|
--ignore-missing-spaces, --ignore-unparameterized-spaces
|
||||||
|
Suppress errors for any requested Hyperopt spaces that
|
||||||
|
do not contain any parameters.
|
||||||
|
|
||||||
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).
|
||||||
|
@ -148,8 +153,8 @@ Checklist on all tasks / possibilities in hyperopt
|
||||||
|
|
||||||
Depending on the space you want to optimize, only some of the below are required:
|
Depending on the space you want to optimize, only some of the below are required:
|
||||||
|
|
||||||
* define parameters with `space='buy'` - for buy signal optimization
|
* define parameters with `space='buy'` - for entry signal optimization
|
||||||
* define parameters with `space='sell'` - for sell signal optimization
|
* define parameters with `space='sell'` - for exit signal optimization
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
`populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work.
|
`populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work.
|
||||||
|
@ -175,7 +180,7 @@ Hyperopt will first load your data into memory and will then run `populate_indic
|
||||||
|
|
||||||
Hyperopt will then spawn into different processes (number of processors, or `-j <n>`), and run backtesting over and over again, changing the parameters that are part of the `--spaces` defined.
|
Hyperopt will then spawn into different processes (number of processors, or `-j <n>`), and run backtesting over and over again, changing the parameters that are part of the `--spaces` defined.
|
||||||
|
|
||||||
For every new set of parameters, freqtrade will run first `populate_buy_trend()` followed by `populate_sell_trend()`, and then run the regular backtesting process to simulate trades.
|
For every new set of parameters, freqtrade will run first `populate_entry_trend()` followed by `populate_exit_trend()`, and then run the regular backtesting process to simulate trades.
|
||||||
|
|
||||||
After backtesting, the results are passed into the [loss function](#loss-functions), which will evaluate if this result was better or worse than previous results.
|
After backtesting, the results are passed into the [loss function](#loss-functions), which will evaluate if this result was better or worse than previous results.
|
||||||
Based on the loss function result, hyperopt will determine the next set of parameters to try in the next round of backtesting.
|
Based on the loss function result, hyperopt will determine the next set of parameters to try in the next round of backtesting.
|
||||||
|
@ -185,7 +190,7 @@ Based on the loss function result, hyperopt will determine the next set of param
|
||||||
There are two places you need to change in your strategy file to add a new buy hyperopt for testing:
|
There are two places you need to change in your strategy file to add a new buy hyperopt for testing:
|
||||||
|
|
||||||
* Define the parameters at the class level hyperopt shall be optimizing.
|
* Define the parameters at the class level hyperopt shall be optimizing.
|
||||||
* Within `populate_buy_trend()` - use defined parameter values instead of raw constants.
|
* Within `populate_entry_trend()` - use defined parameter values instead of raw constants.
|
||||||
|
|
||||||
There you have two different types of indicators: 1. `guards` and 2. `triggers`.
|
There you have two different types of indicators: 1. `guards` and 2. `triggers`.
|
||||||
|
|
||||||
|
@ -195,25 +200,25 @@ There you have two different types of indicators: 1. `guards` and 2. `triggers`.
|
||||||
!!! Hint "Guards and Triggers"
|
!!! Hint "Guards and Triggers"
|
||||||
Technically, there is no difference between Guards and Triggers.
|
Technically, there is no difference between Guards and Triggers.
|
||||||
However, this guide will make this distinction to make it clear that signals should not be "sticking".
|
However, this guide will make this distinction to make it clear that signals should not be "sticking".
|
||||||
Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning).
|
Sticking signals are signals that are active for multiple candles. This can lead into entering a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning).
|
||||||
|
|
||||||
Hyper-optimization will, for each epoch round, pick one trigger and possibly multiple guards.
|
Hyper-optimization will, for each epoch round, pick one trigger and possibly multiple guards.
|
||||||
|
|
||||||
#### Sell optimization
|
#### Exit signal optimization
|
||||||
|
|
||||||
Similar to the buy-signal above, sell-signals can also be optimized.
|
Similar to the entry-signal above, exit-signals can also be optimized.
|
||||||
Place the corresponding settings into the following methods
|
Place the corresponding settings into the following methods
|
||||||
|
|
||||||
* Define the parameters at the class level hyperopt shall be optimizing, either naming them `sell_*`, or by explicitly defining `space='sell'`.
|
* Define the parameters at the class level hyperopt shall be optimizing, either naming them `sell_*`, or by explicitly defining `space='sell'`.
|
||||||
* Within `populate_sell_trend()` - use defined parameter values instead of raw constants.
|
* Within `populate_exit_trend()` - use defined parameter values instead of raw constants.
|
||||||
|
|
||||||
The configuration and rules are the same than for buy signals.
|
The configuration and rules are the same than for buy signals.
|
||||||
|
|
||||||
## Solving a Mystery
|
## Solving a Mystery
|
||||||
|
|
||||||
Let's say you are curious: should you use MACD crossings or lower Bollinger Bands to trigger your buys.
|
Let's say you are curious: should you use MACD crossings or lower Bollinger Bands to trigger your long entries.
|
||||||
And you also wonder should you use RSI or ADX to help with those buy decisions.
|
And you also wonder should you use RSI or ADX to help with those decisions.
|
||||||
If you decide to use RSI or ADX, which values should I use for them?
|
If you decide to use RSI or ADX, which values should I use for them?
|
||||||
|
|
||||||
So let's use hyperparameter optimization to solve this mystery.
|
So let's use hyperparameter optimization to solve this mystery.
|
||||||
|
|
||||||
|
@ -269,7 +274,7 @@ The last one we call `trigger` and use it to decide which buy trigger we want to
|
||||||
So let's write the buy strategy using these values:
|
So let's write the buy strategy using these values:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
conditions = []
|
conditions = []
|
||||||
# GUARDS AND TRENDS
|
# GUARDS AND TRENDS
|
||||||
if self.buy_adx_enabled.value:
|
if self.buy_adx_enabled.value:
|
||||||
|
@ -291,12 +296,12 @@ So let's write the buy strategy using these values:
|
||||||
if conditions:
|
if conditions:
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
reduce(lambda x, y: x & y, conditions),
|
reduce(lambda x, y: x & y, conditions),
|
||||||
'buy'] = 1
|
'enter_long'] = 1
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations.
|
Hyperopt will now call `populate_entry_trend()` many times (`epochs`) with different value combinations.
|
||||||
It will use the given historical data and simulate buys based on the buy signals generated with the above function.
|
It will use the given historical data and simulate buys based on the buy signals generated with the above function.
|
||||||
Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)).
|
Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)).
|
||||||
|
|
||||||
|
@ -359,7 +364,7 @@ class MyAwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
conditions = []
|
conditions = []
|
||||||
conditions.append(qtpylib.crossed_above(
|
conditions.append(qtpylib.crossed_above(
|
||||||
dataframe[f'ema_short_{self.buy_ema_short.value}'], dataframe[f'ema_long_{self.buy_ema_long.value}']
|
dataframe[f'ema_short_{self.buy_ema_short.value}'], dataframe[f'ema_long_{self.buy_ema_long.value}']
|
||||||
|
@ -371,10 +376,10 @@ class MyAwesomeStrategy(IStrategy):
|
||||||
if conditions:
|
if conditions:
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
reduce(lambda x, y: x & y, conditions),
|
reduce(lambda x, y: x & y, conditions),
|
||||||
'buy'] = 1
|
'enter_long'] = 1
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
conditions = []
|
conditions = []
|
||||||
conditions.append(qtpylib.crossed_above(
|
conditions.append(qtpylib.crossed_above(
|
||||||
dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}']
|
dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}']
|
||||||
|
@ -386,7 +391,7 @@ class MyAwesomeStrategy(IStrategy):
|
||||||
if conditions:
|
if conditions:
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
reduce(lambda x, y: x & y, conditions),
|
reduce(lambda x, y: x & y, conditions),
|
||||||
'sell'] = 1
|
'exit_long'] = 1
|
||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -503,6 +508,46 @@ class MyAwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
You will then obviously also change potential interesting entries to parameters to allow hyper-optimization.
|
You will then obviously also change potential interesting entries to parameters to allow hyper-optimization.
|
||||||
|
|
||||||
|
### Optimizing `max_entry_position_adjustment`
|
||||||
|
|
||||||
|
While `max_entry_position_adjustment` is not a separate space, it can still be used in hyperopt by using the property approach shown above.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from pandas import DataFrame
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
|
||||||
|
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||||
|
IStrategy, IntParameter)
|
||||||
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
|
||||||
|
class MyAwesomeStrategy(IStrategy):
|
||||||
|
stoploss = -0.05
|
||||||
|
timeframe = '15m'
|
||||||
|
|
||||||
|
# Define the parameter spaces
|
||||||
|
max_epa = CategoricalParameter([-1, 0, 1, 3, 5, 10], default=1, space="buy", optimize=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_entry_position_adjustment(self):
|
||||||
|
return self.max_epa.value
|
||||||
|
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
??? Tip "Using `IntParameter`"
|
||||||
|
You can also use the `IntParameter` for this optimization, but you must explicitly return an integer:
|
||||||
|
``` python
|
||||||
|
max_epa = IntParameter(-1, 10, default=1, space="buy", optimize=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_entry_position_adjustment(self):
|
||||||
|
return int(self.max_epa.value)
|
||||||
|
```
|
||||||
|
|
||||||
## Loss-functions
|
## Loss-functions
|
||||||
|
|
||||||
Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.
|
Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.
|
||||||
|
@ -512,12 +557,15 @@ This class should be in its own file within the `user_data/hyperopts/` directory
|
||||||
|
|
||||||
Currently, the following loss functions are builtin:
|
Currently, the following loss functions are builtin:
|
||||||
|
|
||||||
* `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses.
|
* `ShortTradeDurHyperOptLoss` - (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses.
|
||||||
* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration)
|
* `OnlyProfitHyperOptLoss` - takes only amount of profit into consideration.
|
||||||
* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation)
|
* `SharpeHyperOptLoss` - optimizes Sharpe Ratio calculated on trade returns relative to standard deviation.
|
||||||
* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation)
|
* `SharpeHyperOptLossDaily` - optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation.
|
||||||
* `SortinoHyperOptLoss` (optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation)
|
* `SortinoHyperOptLoss` - optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation.
|
||||||
* `SortinoHyperOptLossDaily` (optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation)
|
* `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation.
|
||||||
|
* `MaxDrawDownHyperOptLoss` - Optimizes Maximum drawdown.
|
||||||
|
* `CalmarHyperOptLoss` - Optimizes Calmar Ratio calculated on trade returns relative to max drawdown.
|
||||||
|
* `ProfitDrawDownHyperOptLoss` - Optimizes by max Profit & min Drawdown objective. `DRAWDOWN_MULT` variable within the hyperoptloss file can be adjusted to be stricter or more flexible on drawdown purposes.
|
||||||
|
|
||||||
Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation.
|
Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation.
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,8 @@ To skip pair validation against active markets, set `"allow_inactive": true` wit
|
||||||
This can be useful for backtesting expired pairs (like quarterly spot-markets).
|
This can be useful for backtesting expired pairs (like quarterly spot-markets).
|
||||||
This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration.
|
This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration.
|
||||||
|
|
||||||
|
When used in a "follow-up" position (e.g. after VolumePairlist), all pairs in `'pair_whitelist'` will be added to the end of the pairlist.
|
||||||
|
|
||||||
#### Volume Pair List
|
#### Volume Pair List
|
||||||
|
|
||||||
`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
|
`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
|
||||||
|
@ -194,23 +196,33 @@ Trade count is used as a tie breaker.
|
||||||
You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window).
|
You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window).
|
||||||
Not defining this parameter (or setting it to 0) will use all-time performance.
|
Not defining this parameter (or setting it to 0) will use all-time performance.
|
||||||
|
|
||||||
|
The optional `min_profit` (as ratio -> a setting of `0.01` corresponds to 1%) parameter defines the minimum profit a pair must have to be considered.
|
||||||
|
Pairs below this level will be filtered out.
|
||||||
|
Using this parameter without `minutes` is highly discouraged, as it can lead to an empty pairlist without a way to recover.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"pairlists": [
|
"pairlists": [
|
||||||
// ...
|
// ...
|
||||||
{
|
{
|
||||||
"method": "PerformanceFilter",
|
"method": "PerformanceFilter",
|
||||||
"minutes": 1440 // rolling 24h
|
"minutes": 1440, // rolling 24h
|
||||||
|
"min_profit": 0.01 // minimal profit 1%
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
As this Filter uses past performance of the bot, it'll have some startup-period - and should only be used after the bot has a few 100 trades in the database.
|
||||||
|
|
||||||
|
!!! Warning "Backtesting"
|
||||||
`PerformanceFilter` does not support backtesting mode.
|
`PerformanceFilter` does not support backtesting mode.
|
||||||
|
|
||||||
#### PrecisionFilter
|
#### PrecisionFilter
|
||||||
|
|
||||||
Filters low-value coins which would not allow setting stoplosses.
|
Filters low-value coins which would not allow setting stoplosses.
|
||||||
|
|
||||||
|
!!! Warning "Backtesting"
|
||||||
|
`PrecisionFilter` does not support backtesting mode using multiple strategies.
|
||||||
|
|
||||||
#### PriceFilter
|
#### PriceFilter
|
||||||
|
|
||||||
The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported:
|
The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported:
|
||||||
|
@ -234,7 +246,7 @@ On exchanges that deduct fees from the receiving currency (e.g. FTX) - this can
|
||||||
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
|
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
|
||||||
This option is disabled by default, and will only apply if set to > 0.
|
This option is disabled by default, and will only apply if set to > 0.
|
||||||
|
|
||||||
For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied.
|
For `PriceFilter` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied.
|
||||||
|
|
||||||
Calculation example:
|
Calculation example:
|
||||||
|
|
||||||
|
@ -248,7 +260,7 @@ Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 -
|
||||||
Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority.
|
Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority.
|
||||||
|
|
||||||
!!! Tip
|
!!! Tip
|
||||||
You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order.
|
You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order. ShuffleFilter will automatically detect runmodes and apply the `seed` only for backtesting modes - if a `seed` value is set.
|
||||||
|
|
||||||
#### SpreadFilter
|
#### SpreadFilter
|
||||||
|
|
||||||
|
@ -283,7 +295,7 @@ If the trading range over the last 10 days is <1% or >99%, remove the pair from
|
||||||
|
|
||||||
#### VolatilityFilter
|
#### VolatilityFilter
|
||||||
|
|
||||||
Volatility is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatility of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)).
|
Volatility is the degree of historical variation of a pairs over time, it is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatility of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)).
|
||||||
|
|
||||||
This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`.
|
This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`.
|
||||||
|
|
||||||
|
@ -337,5 +349,5 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets,
|
||||||
"refresh_period": 86400
|
"refresh_period": 86400
|
||||||
},
|
},
|
||||||
{"method": "ShuffleFilter", "seed": 42}
|
{"method": "ShuffleFilter", "seed": 42}
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
## Prices used for orders
|
## Prices used for orders
|
||||||
|
|
||||||
Prices for regular orders can be controlled via the parameter structures `bid_strategy` for buying and `ask_strategy` for selling.
|
Prices for regular orders can be controlled via the parameter structures `entry_pricing` for trade entries and `exit_pricing` for trade exits.
|
||||||
Prices are always retrieved right before an order is placed, either by querying the exchange tickers or by using the orderbook data.
|
Prices are always retrieved right before an order is placed, either by querying the exchange tickers or by using the orderbook data.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
|
@ -9,20 +9,11 @@ Prices are always retrieved right before an order is placed, either by querying
|
||||||
!!! Warning "Using market orders"
|
!!! Warning "Using market orders"
|
||||||
Please read the section [Market order pricing](#market-order-pricing) section when using market orders.
|
Please read the section [Market order pricing](#market-order-pricing) section when using market orders.
|
||||||
|
|
||||||
### Buy price
|
### Entry price
|
||||||
|
|
||||||
#### Check depth of market
|
#### Enter price side
|
||||||
|
|
||||||
When check depth of market is enabled (`bid_strategy.check_depth_of_market.enabled=True`), the buy signals are filtered based on the orderbook depth (sum of all amounts) for each orderbook side.
|
The configuration setting `entry_pricing.price_side` defines the side of the orderbook the bot looks for when buying.
|
||||||
|
|
||||||
Orderbook `bid` (buy) side depth is then divided by the orderbook `ask` (sell) side depth and the resulting delta is compared to the value of the `bid_strategy.check_depth_of_market.bids_to_ask_delta` parameter. The buy order is only executed if the orderbook delta is greater than or equal to the configured delta value.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
A delta value below 1 means that `ask` (sell) orderbook side depth is greater than the depth of the `bid` (buy) orderbook side, while a value greater than 1 means opposite (depth of the buy side is higher than the depth of the sell side).
|
|
||||||
|
|
||||||
#### Buy price side
|
|
||||||
|
|
||||||
The configuration setting `bid_strategy.price_side` defines the side of the spread the bot looks for when buying.
|
|
||||||
|
|
||||||
The following displays an orderbook.
|
The following displays an orderbook.
|
||||||
|
|
||||||
|
@ -38,30 +29,53 @@ The following displays an orderbook.
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
If `bid_strategy.price_side` is set to `"bid"`, then the bot will use 99 as buying price.
|
If `entry_pricing.price_side` is set to `"bid"`, then the bot will use 99 as entry price.
|
||||||
In line with that, if `bid_strategy.price_side` is set to `"ask"`, then the bot will use 101 as buying price.
|
In line with that, if `entry_pricing.price_side` is set to `"ask"`, then the bot will use 101 as entry price.
|
||||||
|
|
||||||
Using `ask` price often guarantees quicker filled orders, but the bot can also end up paying more than what would have been necessary.
|
Depending on the order direction (_long_/_short_), this will lead to different results. Therefore we recommend to use `"same"` or `"other"` for this configuration instead.
|
||||||
|
This would result in the following pricing matrix:
|
||||||
|
|
||||||
|
| direction | Order | setting | price | crosses spread |
|
||||||
|
|------ |--------|-----|-----|-----|
|
||||||
|
| long | buy | ask | 101 | yes |
|
||||||
|
| long | buy | bid | 99 | no |
|
||||||
|
| long | buy | same | 99 | no |
|
||||||
|
| long | buy | other | 101 | yes |
|
||||||
|
| short | sell | ask | 101 | no |
|
||||||
|
| short | sell | bid | 99 | yes |
|
||||||
|
| short | sell | same | 101 | no |
|
||||||
|
| short | sell | other | 99 | yes |
|
||||||
|
|
||||||
|
Using the other side of the orderbook often guarantees quicker filled orders, but the bot can also end up paying more than what would have been necessary.
|
||||||
Taker fees instead of maker fees will most likely apply even when using limit buy orders.
|
Taker fees instead of maker fees will most likely apply even when using limit buy orders.
|
||||||
Also, prices at the "ask" side of the spread are higher than prices at the "bid" side in the orderbook, so the order behaves similar to a market order (however with a maximum price).
|
Also, prices at the "other" side of the spread are higher than prices at the "bid" side in the orderbook, so the order behaves similar to a market order (however with a maximum price).
|
||||||
|
|
||||||
#### Buy price with Orderbook enabled
|
#### Entry price with Orderbook enabled
|
||||||
|
|
||||||
When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and uses the entry specified as `bid_strategy.order_book_top` on the configured side (`bid_strategy.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
|
When entering a trade with the orderbook enabled (`entry_pricing.use_order_book=True`), Freqtrade fetches the `entry_pricing.order_book_top` entries from the orderbook and uses the entry specified as `entry_pricing.order_book_top` on the configured side (`entry_pricing.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
|
||||||
|
|
||||||
#### Buy price without Orderbook enabled
|
#### Entry price without Orderbook enabled
|
||||||
|
|
||||||
The following section uses `side` as the configured `bid_strategy.price_side`.
|
The following section uses `side` as the configured `entry_pricing.price_side` (defaults to `"same"`).
|
||||||
|
|
||||||
When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price.
|
When not using orderbook (`entry_pricing.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price based on `entry_pricing.price_last_balance`.
|
||||||
|
|
||||||
The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price.
|
The `entry_pricing.price_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price.
|
||||||
|
|
||||||
### Sell price
|
#### Check depth of market
|
||||||
|
|
||||||
#### Sell price side
|
When check depth of market is enabled (`entry_pricing.check_depth_of_market.enabled=True`), the entry signals are filtered based on the orderbook depth (sum of all amounts) for each orderbook side.
|
||||||
|
|
||||||
The configuration setting `ask_strategy.price_side` defines the side of the spread the bot looks for when selling.
|
Orderbook `bid` (buy) side depth is then divided by the orderbook `ask` (sell) side depth and the resulting delta is compared to the value of the `entry_pricing.check_depth_of_market.bids_to_ask_delta` parameter. The entry order is only executed if the orderbook delta is greater than or equal to the configured delta value.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
A delta value below 1 means that `ask` (sell) orderbook side depth is greater than the depth of the `bid` (buy) orderbook side, while a value greater than 1 means opposite (depth of the buy side is higher than the depth of the sell side).
|
||||||
|
|
||||||
|
### Exit price
|
||||||
|
|
||||||
|
#### Exit price side
|
||||||
|
|
||||||
|
The configuration setting `exit_pricing.price_side` defines the side of the spread the bot looks for when exiting a trade.
|
||||||
|
|
||||||
The following displays an orderbook:
|
The following displays an orderbook:
|
||||||
|
|
||||||
|
@ -77,40 +91,54 @@ The following displays an orderbook:
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
If `ask_strategy.price_side` is set to `"ask"`, then the bot will use 101 as selling price.
|
If `exit_pricing.price_side` is set to `"ask"`, then the bot will use 101 as exiting price.
|
||||||
In line with that, if `ask_strategy.price_side` is set to `"bid"`, then the bot will use 99 as selling price.
|
In line with that, if `exit_pricing.price_side` is set to `"bid"`, then the bot will use 99 as exiting price.
|
||||||
|
|
||||||
#### Sell price with Orderbook enabled
|
Depending on the order direction (_long_/_short_), this will lead to different results. Therefore we recommend to use `"same"` or `"other"` for this configuration instead.
|
||||||
|
This would result in the following pricing matrix:
|
||||||
|
|
||||||
When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_top` entries in the orderbook and uses the entry specified as `ask_strategy.order_book_top` from the configured side (`ask_strategy.price_side`) as selling price.
|
| Direction | Order | setting | price | crosses spread |
|
||||||
|
|------ |--------|-----|-----|-----|
|
||||||
|
| long | sell | ask | 101 | no |
|
||||||
|
| long | sell | bid | 99 | yes |
|
||||||
|
| long | sell | same | 101 | no |
|
||||||
|
| long | sell | other | 99 | yes |
|
||||||
|
| short | buy | ask | 101 | yes |
|
||||||
|
| short | buy | bid | 99 | no |
|
||||||
|
| short | buy | same | 99 | no |
|
||||||
|
| short | buy | other | 101 | yes |
|
||||||
|
|
||||||
|
#### Exit price with Orderbook enabled
|
||||||
|
|
||||||
|
When exiting with the orderbook enabled (`exit_pricing.use_order_book=True`), Freqtrade fetches the `exit_pricing.order_book_top` entries in the orderbook and uses the entry specified as `exit_pricing.order_book_top` from the configured side (`exit_pricing.price_side`) as trade exit price.
|
||||||
|
|
||||||
1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
|
1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
|
||||||
|
|
||||||
#### Sell price without Orderbook enabled
|
#### Exit price without Orderbook enabled
|
||||||
|
|
||||||
When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price.
|
The following section uses `side` as the configured `exit_pricing.price_side` (defaults to `"ask"`).
|
||||||
|
|
||||||
When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price.
|
When not using orderbook (`exit_pricing.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's above the `last` traded price from the ticker. Otherwise (when the `side` price is below the `last` price), it calculates a rate between `side` and `last` price based on `exit_pricing.price_last_balance`.
|
||||||
|
|
||||||
The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price.
|
The `exit_pricing.price_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price.
|
||||||
|
|
||||||
### Market order pricing
|
### Market order pricing
|
||||||
|
|
||||||
When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection.
|
When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection.
|
||||||
Assuming both buy and sell are using market orders, a configuration similar to the following might be used
|
Assuming both entry and exits are using market orders, a configuration similar to the following must be used
|
||||||
|
|
||||||
``` jsonc
|
``` jsonc
|
||||||
"order_types": {
|
"order_types": {
|
||||||
"buy": "market",
|
"entry": "market",
|
||||||
"sell": "market"
|
"exit": "market"
|
||||||
// ...
|
// ...
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"entry_pricing": {
|
||||||
"price_side": "ask",
|
"price_side": "other",
|
||||||
// ...
|
// ...
|
||||||
},
|
},
|
||||||
"ask_strategy":{
|
"exit_pricing":{
|
||||||
"price_side": "bid",
|
"price_side": "other",
|
||||||
// ...
|
// ...
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Freqtrade is a crypto-currency algorithmic trading software developed in python (3.7+) and supported on Windows, macOS and Linux.
|
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
|
||||||
|
|
||||||
!!! Danger "DISCLAIMER"
|
!!! Danger "DISCLAIMER"
|
||||||
This software is for educational purposes only. Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
|
This software is for educational purposes only. Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
|
||||||
|
@ -20,6 +20,12 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python
|
||||||
|
|
||||||
We strongly recommend you to have basic coding skills and Python knowledge. Do not hesitate to read the source code and understand the mechanisms of this bot, algorithms and techniques implemented in it.
|
We strongly recommend you to have basic coding skills and Python knowledge. Do not hesitate to read the source code and understand the mechanisms of this bot, algorithms and techniques implemented in it.
|
||||||
|
|
||||||
|
![freqtrade screenshot](assets/freqtrade-screenshot.png)
|
||||||
|
|
||||||
|
## Sponsored promotion
|
||||||
|
|
||||||
|
[![tokenbot-promo](assets/TokenBot-Freqtrade-banner.png)](https://tokenbot.com/?utm_source=github&utm_medium=freqtrade&utm_campaign=algodevs)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Develop your Strategy: Write your strategy in python, using [pandas](https://pandas.pydata.org/). Example strategies to inspire you are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies).
|
- Develop your Strategy: Write your strategy in python, using [pandas](https://pandas.pydata.org/). Example strategies to inspire you are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies).
|
||||||
|
@ -29,18 +35,20 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python
|
||||||
- Select markets: Create your static list or use an automatic one based on top traded volumes and/or prices (not available during backtesting). You can also explicitly blacklist markets you don't want to trade.
|
- Select markets: Create your static list or use an automatic one based on top traded volumes and/or prices (not available during backtesting). You can also explicitly blacklist markets you don't want to trade.
|
||||||
- Run: Test your strategy with simulated money (Dry-Run mode) or deploy it with real money (Live-Trade mode).
|
- Run: Test your strategy with simulated money (Dry-Run mode) or deploy it with real money (Live-Trade mode).
|
||||||
- Run using Edge (optional module): The concept is to find the best historical [trade expectancy](edge.md#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital.
|
- Run using Edge (optional module): The concept is to find the best historical [trade expectancy](edge.md#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital.
|
||||||
- Control/Monitor: Use Telegram or a REST API (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.).
|
- Control/Monitor: Use Telegram or a WebUI (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.).
|
||||||
- Analyse: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md).
|
- Analyse: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md).
|
||||||
|
|
||||||
## Supported exchange marketplaces
|
## Supported exchange marketplaces
|
||||||
|
|
||||||
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com/#a=2258149)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
|
- [X] [Huobi](http://huobi.com/)
|
||||||
|
- [X] [Kraken](https://kraken.com/)
|
||||||
|
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||||
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
|
@ -66,7 +74,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
|
||||||
|
|
||||||
Alternatively
|
Alternatively
|
||||||
|
|
||||||
- Python 3.7+
|
- Python 3.8+
|
||||||
- pip (pip3)
|
- pip (pip3)
|
||||||
- git
|
- git
|
||||||
- TA-Lib
|
- TA-Lib
|
||||||
|
@ -80,4 +88,4 @@ For any questions not covered by the documentation or for further information ab
|
||||||
|
|
||||||
## Ready to try?
|
## Ready to try?
|
||||||
|
|
||||||
Begin by reading our installation guide [for docker](docker_quickstart.md) (recommended), or for [installation without docker](installation.md).
|
Begin by reading the installation guide [for docker](docker_quickstart.md) (recommended), or for [installation without docker](installation.md).
|
||||||
|
|
|
@ -24,7 +24,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
|
||||||
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
|
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Python3.7 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
|
Python3.8 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
|
||||||
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
|
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
|
||||||
|
|
||||||
!!! Warning "Up-to-date clock"
|
!!! Warning "Up-to-date clock"
|
||||||
|
@ -36,9 +36,13 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
|
||||||
|
|
||||||
These requirements apply to both [Script Installation](#script-installation) and [Manual Installation](#manual-installation).
|
These requirements apply to both [Script Installation](#script-installation) and [Manual Installation](#manual-installation).
|
||||||
|
|
||||||
|
!!! Note "ARM64 systems"
|
||||||
|
If you are running an ARM64 system (like a MacOS M1 or an Oracle VM), please use [docker](docker_quickstart.md) to run freqtrade.
|
||||||
|
While native installation is possible with some manual effort, this is not supported at the moment.
|
||||||
|
|
||||||
### Install guide
|
### Install guide
|
||||||
|
|
||||||
* [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
* [Python >= 3.8.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
||||||
|
@ -50,7 +54,7 @@ We've included/collected install instructions for Ubuntu, MacOS, and Windows. Th
|
||||||
OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems.
|
OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Python3.7 or higher and the corresponding pip are assumed to be available.
|
Python3.8 or higher and the corresponding pip are assumed to be available.
|
||||||
|
|
||||||
=== "Debian/Ubuntu"
|
=== "Debian/Ubuntu"
|
||||||
#### Install necessary dependencies
|
#### Install necessary dependencies
|
||||||
|
@ -60,18 +64,18 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
|
|
||||||
# install packages
|
# install packages
|
||||||
sudo apt install -y python3-pip python3-venv python3-dev python3-pandas git
|
sudo apt install -y python3-pip python3-venv python3-dev python3-pandas git curl
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "RaspberryPi/Raspbian"
|
=== "RaspberryPi/Raspbian"
|
||||||
The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/).
|
The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/).
|
||||||
This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running.
|
This image comes with python3.9 preinstalled, making it easy to get freqtrade up and running.
|
||||||
|
|
||||||
Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied.
|
Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied.
|
||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get install python3-venv libatlas-base-dev cmake
|
sudo apt-get install python3-venv libatlas-base-dev cmake curl
|
||||||
# Use pywheels.org to speed up installation
|
# Use pywheels.org to speed up installation
|
||||||
sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf
|
sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf
|
||||||
|
|
||||||
|
@ -113,6 +117,13 @@ git checkout develop
|
||||||
|
|
||||||
You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands.
|
You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands.
|
||||||
|
|
||||||
|
??? Note "Install from pypi"
|
||||||
|
An alternative way to install Freqtrade is from [pypi](https://pypi.org/project/freqtrade/). The downside is that this method requires ta-lib to be correctly installed beforehand, and is therefore currently not the recommended way to install Freqtrade.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
pip install freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
## Script Installation
|
## Script Installation
|
||||||
|
@ -158,7 +169,7 @@ You can as well update, configure and reset the codebase of your bot with `./scr
|
||||||
** --install **
|
** --install **
|
||||||
|
|
||||||
With this option, the script will install the bot and most dependencies:
|
With this option, the script will install the bot and most dependencies:
|
||||||
You will need to have git and python3.7+ installed beforehand for this to work.
|
You will need to have git and python3.8+ installed beforehand for this to work.
|
||||||
|
|
||||||
* Mandatory software as: `ta-lib`
|
* Mandatory software as: `ta-lib`
|
||||||
* Setup your virtualenv under `.env/`
|
* Setup your virtualenv under `.env/`
|
||||||
|
@ -409,16 +420,3 @@ open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10
|
||||||
```
|
```
|
||||||
|
|
||||||
If this file is inexistent, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details.
|
If this file is inexistent, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details.
|
||||||
|
|
||||||
### MacOS installation error with python 3.9
|
|
||||||
|
|
||||||
When using python 3.9 on macOS, it's currently necessary to install some os-level modules to allow dependencies to compile.
|
|
||||||
The errors you'll see happen during installation and are related to the installation of `tables` or `blosc`.
|
|
||||||
|
|
||||||
You can install the necessary libraries with the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew install hdf5 c-blosc
|
|
||||||
```
|
|
||||||
|
|
||||||
After this, please run the installation (script) again.
|
|
||||||
|
|
116
docs/leverage.md
Normal file
116
docs/leverage.md
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
# Trading with Leverage
|
||||||
|
|
||||||
|
!!! Warning "Beta feature"
|
||||||
|
This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord or via Github Issue.
|
||||||
|
|
||||||
|
!!! Note "Multiple bots on one account"
|
||||||
|
You can't run 2 bots on the same account with leverage. For leveraged / margin trading, freqtrade assumes it's the only user of the account, and all liquidation levels are calculated based on this assumption.
|
||||||
|
|
||||||
|
!!! Danger "Trading with leverage is very risky"
|
||||||
|
Do not trade with a leverage > 1 using a strategy that hasn't shown positive results in a live run using the spot market. Check the stoploss of your strategy. With a leverage of 2, a stoploss of 0.5 (50%) would be too low, and these trades would be liquidated before reaching that stoploss.
|
||||||
|
We do not assume any responsibility for eventual losses that occur from using this software or this mode.
|
||||||
|
|
||||||
|
Please only use advanced trading modes when you know how freqtrade (and your strategy) works.
|
||||||
|
Also, never risk more than what you can afford to lose.
|
||||||
|
|
||||||
|
Please read the [strategy migration guide](strategy_migration.md#strategy-migration-between-v2-and-v3) to migrate your strategy from a freqtrade v2 strategy, to v3 strategy that can short and trade futures.
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
For a strategy to short, the strategy class must set the class variable `can_short = True`
|
||||||
|
|
||||||
|
Please read [strategy customization](strategy-customization.md#entry-signal-rules) for instructions on how to set signals to enter and exit short trades.
|
||||||
|
|
||||||
|
## Understand `trading_mode`
|
||||||
|
|
||||||
|
The possible values are: `spot` (default), `margin`(*Currently unavailable*) or `futures`.
|
||||||
|
|
||||||
|
### Spot
|
||||||
|
|
||||||
|
Regular trading mode (low risk)
|
||||||
|
|
||||||
|
- Long trades only (No short trades).
|
||||||
|
- No leverage.
|
||||||
|
- No Liquidation.
|
||||||
|
- Profits gained/lost are equal to the change in value of the assets (minus trading fees).
|
||||||
|
|
||||||
|
### Leverage trading modes
|
||||||
|
|
||||||
|
With leverage, a trader borrows capital from the exchange. The capital must be re-payed fully to the exchange (potentially with interest), and the trader keeps any profits, or pays any losses, from any trades made using the borrowed capital.
|
||||||
|
|
||||||
|
Because the capital must always be re-payed, exchanges will **liquidate** (forcefully sell the traders assets) a trade made using borrowed capital when the total value of assets in the leverage account drops to a certain point (a point where the total value of losses is less than the value of the collateral that the trader actually owns in the leverage account), in order to ensure that the trader has enough capital to pay the borrowed assets back to the exchange. The exchange will also charge a **liquidation fee**, adding to the traders losses.
|
||||||
|
|
||||||
|
For this reason, **DO NOT TRADE WITH LEVERAGE IF YOU DON'T KNOW EXACTLY WHAT YOUR DOING. LEVERAGE TRADING IS HIGH RISK, AND CAN RESULT IN THE VALUE OF YOUR ASSETS DROPPING TO 0 VERY QUICKLY, WITH NO CHANCE OF INCREASING IN VALUE AGAIN.**
|
||||||
|
|
||||||
|
#### Margin (currently unavailable)
|
||||||
|
|
||||||
|
Trading occurs on the spot market, but the exchange lends currency to you in an amount equal to the chosen leverage. You pay the amount lent to you back to the exchange with interest, and your profits/losses are multiplied by the leverage specified.
|
||||||
|
|
||||||
|
#### Futures
|
||||||
|
|
||||||
|
Perpetual swaps (also known as Perpetual Futures) are contracts traded at a price that is closely tied to the underlying asset they are based off of (ex.). You are not trading the actual asset but instead are trading a derivative contract. Perpetual swap contracts can last indefinitely, in contrast to futures or option contracts.
|
||||||
|
|
||||||
|
In addition to the gains/losses from the change in price of the futures contract, traders also exchange _funding fees_, which are gains/losses worth an amount that is derived from the difference in price between the futures contract and the underlying asset. The difference in price between a futures contract and the underlying asset varies between exchanges.
|
||||||
|
|
||||||
|
To trade in futures markets, you'll have to set `trading_mode` to "futures".
|
||||||
|
You will also have to pick a "margin mode" (explanation below) - with freqtrade currently only supporting isolated margin.
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"trading_mode": "futures",
|
||||||
|
"margin_mode": "isolated"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Margin mode
|
||||||
|
|
||||||
|
The possible values are: `isolated`, or `cross`(*currently unavailable*)
|
||||||
|
|
||||||
|
#### Isolated margin mode
|
||||||
|
|
||||||
|
Each market(trading pair), keeps collateral in a separate account
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"margin_mode": "isolated"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Cross margin mode (currently unavailable)
|
||||||
|
|
||||||
|
One account is used to share collateral between markets (trading pairs). Margin is taken from total account balance to avoid liquidation when needed.
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"margin_mode": "cross"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Understand `liquidation_buffer`
|
||||||
|
|
||||||
|
*Defaults to `0.05`*
|
||||||
|
|
||||||
|
A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price.
|
||||||
|
This artificial liquidation price is calculated as:
|
||||||
|
|
||||||
|
`freqtrade_liquidation_price = liquidation_price ± (abs(open_rate - liquidation_price) * liquidation_buffer)`
|
||||||
|
|
||||||
|
- `±` = `+` for long trades
|
||||||
|
- `±` = `-` for short trades
|
||||||
|
|
||||||
|
Possible values are any floats between 0.0 and 0.99
|
||||||
|
|
||||||
|
**ex:** If a trade is entered at a price of 10 coin/USDT, and the liquidation price of this trade is 8 coin/USDT, then with `liquidation_buffer` set to `0.05` the minimum stoploss for this trade would be $8 + ((10 - 8) * 0.05) = 8 + 0.1 = 8.1$
|
||||||
|
|
||||||
|
!!! Danger "A `liquidation_buffer` of 0.0, or a low `liquidation_buffer` is likely to result in liquidations, and liquidation fees"
|
||||||
|
Currently Freqtrade is able to calculate liquidation prices, but does not calculate liquidation fees. Setting your `liquidation_buffer` to 0.0, or using a low `liquidation_buffer` could result in your positions being liquidated. Freqtrade does not track liquidation fees, so liquidations will result in inaccurate profit/loss results for your bot. If you use a low `liquidation_buffer`, it is recommended to use `stoploss_on_exchange` if your exchange supports this.
|
||||||
|
|
||||||
|
### Developer
|
||||||
|
|
||||||
|
#### Margin mode
|
||||||
|
|
||||||
|
For shorts, the currency which pays the interest fee for the `borrowed` currency is purchased at the same time of the closing trade (This means that the amount purchased in short closing trades is greater than the amount sold in short opening trades).
|
||||||
|
|
||||||
|
For longs, the currency which pays the interest fee for the `borrowed` will already be owned by the user and does not need to be purchased. The interest is subtracted from the `close_value` of the trade.
|
||||||
|
|
||||||
|
All Fees are included in `current_profit` calculations during the trade.
|
||||||
|
|
||||||
|
#### Futures mode
|
||||||
|
|
||||||
|
Funding fees are either added or subtracted from the total amount of a trade
|
132
docs/plotting.md
132
docs/plotting.md
|
@ -14,7 +14,7 @@ pip install -U -r requirements-plot.txt
|
||||||
|
|
||||||
The `freqtrade plot-dataframe` subcommand shows an interactive graph with three subplots:
|
The `freqtrade plot-dataframe` subcommand shows an interactive graph with three subplots:
|
||||||
|
|
||||||
* Main plot with candlestics and indicators following price (sma/ema)
|
* Main plot with candlesticks and indicators following price (sma/ema)
|
||||||
* Volume bars
|
* Volume bars
|
||||||
* Additional indicators as specified by `--indicators2`
|
* Additional indicators as specified by `--indicators2`
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ optional arguments:
|
||||||
_today.json`
|
_today.json`
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--no-trades Skip using trades from backtesting file and DB.
|
--no-trades Skip using trades from backtesting file and DB.
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ Strategy arguments:
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade plot-dataframe -p BTC/ETH
|
freqtrade plot-dataframe -p BTC/ETH --strategy AwesomeStrategy
|
||||||
```
|
```
|
||||||
|
|
||||||
The `-p/--pairs` argument can be used to specify pairs you would like to plot.
|
The `-p/--pairs` argument can be used to specify pairs you would like to plot.
|
||||||
|
@ -107,9 +107,6 @@ The `-p/--pairs` argument can be used to specify pairs you would like to plot.
|
||||||
Specify custom indicators.
|
Specify custom indicators.
|
||||||
Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices).
|
Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices).
|
||||||
|
|
||||||
!!! Tip
|
|
||||||
You will almost certainly want to specify a custom strategy! This can be done by adding `-s Classname` / `--strategy ClassName` to the command.
|
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --indicators1 sma ema --indicators2 macd
|
freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --indicators1 sma ema --indicators2 macd
|
||||||
```
|
```
|
||||||
|
@ -164,16 +161,17 @@ The resulting plot will have the following elements:
|
||||||
|
|
||||||
An advanced plot configuration can be specified in the strategy in the `plot_config` parameter.
|
An advanced plot configuration can be specified in the strategy in the `plot_config` parameter.
|
||||||
|
|
||||||
Additional features when using plot_config include:
|
Additional features when using `plot_config` include:
|
||||||
|
|
||||||
* Specify colors per indicator
|
* Specify colors per indicator
|
||||||
* Specify additional subplots
|
* Specify additional subplots
|
||||||
* Specify indicator pairs to fill area in between
|
* Specify indicator pairs to fill area in between
|
||||||
|
|
||||||
The sample plot configuration below specifies fixed colors for the indicators. Otherwise, consecutive plots may produce different color schemes each time, making comparisons difficult.
|
The sample plot configuration below specifies fixed colors for the indicators. Otherwise, consecutive plots may produce different color schemes each time, making comparisons difficult.
|
||||||
It also allows multiple subplots to display both MACD and RSI at the same time.
|
It also allows multiple subplots to display both MACD and RSI at the same time.
|
||||||
|
|
||||||
Plot type can be configured using `type` key. Possible types are:
|
Plot type can be configured using `type` key. Possible types are:
|
||||||
|
|
||||||
* `scatter` corresponding to `plotly.graph_objects.Scatter` class (default).
|
* `scatter` corresponding to `plotly.graph_objects.Scatter` class (default).
|
||||||
* `bar` corresponding to `plotly.graph_objects.Bar` class.
|
* `bar` corresponding to `plotly.graph_objects.Bar` class.
|
||||||
|
|
||||||
|
@ -182,40 +180,89 @@ Extra parameters to `plotly.graph_objects.*` constructor can be specified in `pl
|
||||||
Sample configuration with inline comments explaining the process:
|
Sample configuration with inline comments explaining the process:
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
plot_config = {
|
@property
|
||||||
'main_plot': {
|
def plot_config(self):
|
||||||
# Configuration for main plot indicators.
|
"""
|
||||||
# Specifies `ema10` to be red, and `ema50` to be a shade of gray
|
There are a lot of solutions how to build the return dictionary.
|
||||||
'ema10': {'color': 'red'},
|
The only important point is the return value.
|
||||||
'ema50': {'color': '#CCCCCC'},
|
Example:
|
||||||
# By omitting color, a random color is selected.
|
plot_config = {'main_plot': {}, 'subplots': {}}
|
||||||
'sar': {},
|
|
||||||
# fill area between senkou_a and senkou_b
|
"""
|
||||||
'senkou_a': {
|
plot_config = {}
|
||||||
'color': 'green', #optional
|
plot_config['main_plot'] = {
|
||||||
'fill_to': 'senkou_b',
|
# Configuration for main plot indicators.
|
||||||
'fill_label': 'Ichimoku Cloud', #optional
|
# Assumes 2 parameters, emashort and emalong to be specified.
|
||||||
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
f'ema_{self.emashort.value}': {'color': 'red'},
|
||||||
},
|
f'ema_{self.emalong.value}': {'color': '#CCCCCC'},
|
||||||
# plot senkou_b, too. Not only the area to it.
|
# By omitting color, a random color is selected.
|
||||||
'senkou_b': {}
|
'sar': {},
|
||||||
|
# fill area between senkou_a and senkou_b
|
||||||
|
'senkou_a': {
|
||||||
|
'color': 'green', #optional
|
||||||
|
'fill_to': 'senkou_b',
|
||||||
|
'fill_label': 'Ichimoku Cloud', #optional
|
||||||
|
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
||||||
},
|
},
|
||||||
'subplots': {
|
# plot senkou_b, too. Not only the area to it.
|
||||||
# Create subplot MACD
|
'senkou_b': {}
|
||||||
"MACD": {
|
}
|
||||||
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
plot_config['subplots'] = {
|
||||||
'macdsignal': {'color': 'orange'},
|
# Create subplot MACD
|
||||||
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
"MACD": {
|
||||||
},
|
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
||||||
# Additional subplot RSI
|
'macdsignal': {'color': 'orange'},
|
||||||
"RSI": {
|
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
||||||
'rsi': {'color': 'red'}
|
},
|
||||||
}
|
# Additional subplot RSI
|
||||||
|
"RSI": {
|
||||||
|
'rsi': {'color': 'red'}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return plot_config
|
||||||
```
|
```
|
||||||
|
|
||||||
|
??? Note "As attribute (former method)"
|
||||||
|
Assigning plot_config is also possible as Attribute (this used to be the default way).
|
||||||
|
This has the disadvantage that strategy parameters are not available, preventing certain configurations from working.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
plot_config = {
|
||||||
|
'main_plot': {
|
||||||
|
# Configuration for main plot indicators.
|
||||||
|
# Specifies `ema10` to be red, and `ema50` to be a shade of gray
|
||||||
|
'ema10': {'color': 'red'},
|
||||||
|
'ema50': {'color': '#CCCCCC'},
|
||||||
|
# By omitting color, a random color is selected.
|
||||||
|
'sar': {},
|
||||||
|
# fill area between senkou_a and senkou_b
|
||||||
|
'senkou_a': {
|
||||||
|
'color': 'green', #optional
|
||||||
|
'fill_to': 'senkou_b',
|
||||||
|
'fill_label': 'Ichimoku Cloud', #optional
|
||||||
|
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
||||||
|
},
|
||||||
|
# plot senkou_b, too. Not only the area to it.
|
||||||
|
'senkou_b': {}
|
||||||
|
},
|
||||||
|
'subplots': {
|
||||||
|
# Create subplot MACD
|
||||||
|
"MACD": {
|
||||||
|
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
||||||
|
'macdsignal': {'color': 'orange'},
|
||||||
|
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
||||||
|
},
|
||||||
|
# Additional subplot RSI
|
||||||
|
"RSI": {
|
||||||
|
'rsi': {'color': 'red'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`,
|
The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`,
|
||||||
`macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy.
|
`macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy.
|
||||||
|
@ -223,6 +270,9 @@ Sample configuration with inline comments explaining the process:
|
||||||
!!! Warning
|
!!! Warning
|
||||||
`plotly` arguments are only supported with plotly library and will not work with freq-ui.
|
`plotly` arguments are only supported with plotly library and will not work with freq-ui.
|
||||||
|
|
||||||
|
!!! Note "Trade position adjustments"
|
||||||
|
If `position_adjustment_enable` / `adjust_trade_position()` is used, the trade initial buy price is averaged over multiple orders and the trade start price will most likely appear outside the candle range.
|
||||||
|
|
||||||
## Plot profit
|
## Plot profit
|
||||||
|
|
||||||
![plot-profit](assets/plot-profit.png)
|
![plot-profit](assets/plot-profit.png)
|
||||||
|
@ -233,6 +283,8 @@ The `plot-profit` subcommand shows an interactive graph with three plots:
|
||||||
* The summarized profit made by backtesting.
|
* The summarized profit made by backtesting.
|
||||||
Note that this is not the real-world profit, but more of an estimate.
|
Note that this is not the real-world profit, but more of an estimate.
|
||||||
* Profit for each individual pair.
|
* Profit for each individual pair.
|
||||||
|
* Parallelism of trades.
|
||||||
|
* Underwater (Periods of drawdown).
|
||||||
|
|
||||||
The first graph is good to get a grip of how the overall market progresses.
|
The first graph is good to get a grip of how the overall market progresses.
|
||||||
|
|
||||||
|
@ -242,6 +294,8 @@ This graph will also highlight the start (and end) of the Max drawdown period.
|
||||||
|
|
||||||
The third graph can be useful to spot outliers, events in pairs that cause profit spikes.
|
The third graph can be useful to spot outliers, events in pairs that cause profit spikes.
|
||||||
|
|
||||||
|
The forth graph can help you analyze trade parallelism, showing how often max_open_trades have been maxed out.
|
||||||
|
|
||||||
Possible options for the `freqtrade plot-profit` subcommand:
|
Possible options for the `freqtrade plot-profit` subcommand:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -261,8 +315,8 @@ optional arguments:
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
--export EXPORT Export backtest results, argument are: trades.
|
--export EXPORT Export backtest results, argument are: trades.
|
||||||
Example: `--export=trades`
|
Example: `--export=trades`
|
||||||
--export-filename PATH
|
--export-filename PATH, --backtest-filename PATH
|
||||||
Save backtest results to the file with this filename.
|
Use backtest results from this filename.
|
||||||
Requires `--export` to be set as well. Example:
|
Requires `--export` to be set as well. Example:
|
||||||
`--export-filename=user_data/backtest_results/backtest
|
`--export-filename=user_data/backtest_results/backtest
|
||||||
_today.json`
|
_today.json`
|
||||||
|
@ -273,7 +327,7 @@ optional arguments:
|
||||||
--trade-source {DB,file}
|
--trade-source {DB,file}
|
||||||
Specify the source for trades (Can be DB or file
|
Specify the source for trades (Can be DB or file
|
||||||
(backtest file)) Default: file
|
(backtest file)) Default: file
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--auto-open Automatically open generated plot.
|
--auto-open Automatically open generated plot.
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
mkdocs==1.2.2
|
mkdocs==1.3.0
|
||||||
mkdocs-material==7.3.0
|
mkdocs-material==8.2.9
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==8.2
|
pymdown-extensions==9.3
|
||||||
|
jinja2==3.1.1
|
||||||
|
|
|
@ -38,6 +38,11 @@ Sample configuration:
|
||||||
!!! Danger "Security warning"
|
!!! Danger "Security warning"
|
||||||
By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot.
|
By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot.
|
||||||
|
|
||||||
|
??? Note "API/UI Access on a remote servers"
|
||||||
|
If you're running on a VPS, you should consider using either a ssh tunnel, or setup a VPN (openVPN, wireguard) to connect to your bot.
|
||||||
|
This will ensure that freqUI is not directly exposed to the internet, which is not recommended for security reasons (freqUI does not support https out of the box).
|
||||||
|
Setup of these tools is not part of this tutorial, however many good tutorials can be found on the internet.
|
||||||
|
|
||||||
You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly.
|
You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly.
|
||||||
This should return the response:
|
This should return the response:
|
||||||
|
|
||||||
|
@ -78,7 +83,7 @@ If you run your bot using docker, you'll need to have the bot listen to incoming
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
Uncomment the following from your docker-compose file:
|
Make sure that the following 2 lines are available in your docker-compose file:
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
ports:
|
ports:
|
||||||
|
@ -140,9 +145,10 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
|
||||||
| `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.
|
||||||
| `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.
|
||||||
| `forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
|
| `forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`).
|
||||||
| `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
|
| `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).
|
||||||
| `forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
|
| `forceenter <pair> [rate]` | Instantly enters the given pair. Rate is optional. (`force_entry_enable` must be set to True)
|
||||||
|
| `forceenter <pair> <side> [rate]` | Instantly longs or shorts the given pair. Rate is optional. (`force_entry_enable` must be set to True)
|
||||||
| `performance` | Show performance of each finished trade grouped by pair.
|
| `performance` | Show performance of each finished trade grouped by pair.
|
||||||
| `balance` | Show account balance per currency.
|
| `balance` | Show account balance per currency.
|
||||||
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7).
|
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7).
|
||||||
|
@ -210,8 +216,15 @@ forcebuy
|
||||||
:param pair: Pair to buy (ETH/BTC)
|
:param pair: Pair to buy (ETH/BTC)
|
||||||
:param price: Optional - price to buy
|
:param price: Optional - price to buy
|
||||||
|
|
||||||
forcesell
|
forceenter
|
||||||
Force-sell a trade.
|
Force entering a trade
|
||||||
|
|
||||||
|
:param pair: Pair to buy (ETH/BTC)
|
||||||
|
:param side: 'long' or 'short'
|
||||||
|
:param price: Optional - price to buy
|
||||||
|
|
||||||
|
forceexit
|
||||||
|
Force-exit a trade.
|
||||||
|
|
||||||
:param tradeid: Id of the trade (can be received via status command)
|
:param tradeid: Id of the trade (can be received via status command)
|
||||||
|
|
||||||
|
@ -280,6 +293,9 @@ strategy
|
||||||
|
|
||||||
:param strategy: Strategy class name
|
:param strategy: Strategy class name
|
||||||
|
|
||||||
|
sysinfo
|
||||||
|
Provides system information (CPU, RAM usage)
|
||||||
|
|
||||||
trade
|
trade
|
||||||
Return specific trade
|
Return specific trade
|
||||||
|
|
||||||
|
@ -330,12 +346,15 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques
|
||||||
|
|
||||||
### CORS
|
### CORS
|
||||||
|
|
||||||
All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing.
|
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.
|
||||||
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 configure this themselves via the `CORS_origins` configuration setting.
|
??? info "Technical explanation"
|
||||||
It consists of a list of allowed sites that are allowed to consume resources from the bot's API.
|
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:
|
Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary:
|
||||||
|
|
||||||
|
@ -348,5 +367,19 @@ Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - t
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
!!! 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.
|
We strongly recommend to also set `jwt_secret_key` to something random and known only to yourself to avoid unauthorized access to your bot.
|
||||||
|
|
|
@ -104,16 +104,16 @@ To mitigate this, you can try to match the first order on the opposite orderbook
|
||||||
|
|
||||||
``` jsonc
|
``` jsonc
|
||||||
"order_types": {
|
"order_types": {
|
||||||
"buy": "limit",
|
"entry": "limit",
|
||||||
"sell": "limit"
|
"exit": "limit"
|
||||||
// ...
|
// ...
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"entry_pricing": {
|
||||||
"price_side": "ask",
|
"price_side": "other",
|
||||||
// ...
|
// ...
|
||||||
},
|
},
|
||||||
"ask_strategy":{
|
"exit_pricing":{
|
||||||
"price_side": "bid",
|
"price_side": "other",
|
||||||
// ...
|
// ...
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
|
@ -49,14 +49,14 @@ sqlite3
|
||||||
SELECT * FROM trades;
|
SELECT * FROM trades;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Fix trade still open after a manual sell on the exchange
|
## Fix trade still open after a manual exit on the exchange
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. Whenever possible, forcesell <tradeid> should be used to accomplish the same thing.
|
Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. Whenever possible, /forceexit <tradeid> should be used to accomplish the same thing.
|
||||||
It is strongly advised to backup your database file before making any manual changes.
|
It is strongly advised to backup your database file before making any manual changes.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This should not be necessary after /forcesell, as forcesell orders are closed automatically by the bot on the next iteration.
|
This should not be necessary after /forceexit, as force_exit orders are closed automatically by the bot on the next iteration.
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
UPDATE trades
|
UPDATE trades
|
||||||
|
@ -65,7 +65,7 @@ SET is_open=0,
|
||||||
close_rate=<close_rate>,
|
close_rate=<close_rate>,
|
||||||
close_profit = close_rate / open_rate - 1,
|
close_profit = close_rate / open_rate - 1,
|
||||||
close_profit_abs = (amount * <close_rate> * (1 - fee_close) - (amount * (open_rate * (1 - fee_open)))),
|
close_profit_abs = (amount * <close_rate> * (1 - fee_close) - (amount * (open_rate * (1 - fee_open)))),
|
||||||
sell_reason=<sell_reason>
|
exit_reason=<exit_reason>
|
||||||
WHERE id=<trade_ID_to_update>;
|
WHERE id=<trade_ID_to_update>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ SET is_open=0,
|
||||||
close_rate=0.19638016,
|
close_rate=0.19638016,
|
||||||
close_profit=0.0496,
|
close_profit=0.0496,
|
||||||
close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * (open_rate * (1 - fee_open)))),
|
close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * (open_rate * (1 - fee_open)))),
|
||||||
sell_reason='force_sell'
|
exit_reason='force_exit'
|
||||||
WHERE id=31;
|
WHERE id=31;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
The `stoploss` configuration parameter is loss as ratio that should trigger a sale.
|
The `stoploss` configuration parameter is loss as ratio that should trigger a sale.
|
||||||
For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional.
|
For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional.
|
||||||
|
Stoploss calculations do include fees, so a stoploss of -10% is placed exactly 10% below the entry point.
|
||||||
|
|
||||||
Most of the strategy files already include the optimal `stoploss` value.
|
Most of the strategy files already include the optimal `stoploss` value.
|
||||||
|
|
||||||
|
@ -16,21 +17,21 @@ Those stoploss modes can be *on exchange* or *off exchange*.
|
||||||
These modes can be configured with these values:
|
These modes can be configured with these values:
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
'emergencysell': 'market',
|
'emergency_exit': 'market',
|
||||||
'stoploss_on_exchange': False
|
'stoploss_on_exchange': False
|
||||||
'stoploss_on_exchange_interval': 60,
|
'stoploss_on_exchange_interval': 60,
|
||||||
'stoploss_on_exchange_limit_ratio': 0.99
|
'stoploss_on_exchange_limit_ratio': 0.99
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market, stop-loss-limit) and FTX (stop limit and stop-market) as of now.
|
Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), FTX (stop limit and stop-market) Gateio (stop-limit), and Kucoin (stop-limit and stop-market) as of now.
|
||||||
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
||||||
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
|
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
|
||||||
|
|
||||||
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
|
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
|
||||||
|
|
||||||
Enable or Disable stop loss on exchange.
|
Enable or Disable stop loss on exchange.
|
||||||
If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled.
|
If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order fills. This will protect you against sudden crashes in market, as the order execution happens purely within the exchange, and has no potential network overhead.
|
||||||
|
|
||||||
If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
|
If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
|
||||||
`stoploss` defines the stop-price where the limit order is placed - and limit should be slightly below this.
|
`stoploss` defines the stop-price where the limit order is placed - and limit should be slightly below this.
|
||||||
|
@ -51,30 +52,30 @@ The bot cannot do these every 5 seconds (at each iteration), otherwise it would
|
||||||
So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute).
|
So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute).
|
||||||
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
|
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
|
||||||
|
|
||||||
### forcesell
|
### force_exit
|
||||||
|
|
||||||
`forcesell` is an optional value, which defaults to the same value as `sell` and is used when sending a `/forcesell` command from Telegram or from the Rest API.
|
`force_exit` is an optional value, which defaults to the same value as `exit` and is used when sending a `/forceexit` command from Telegram or from the Rest API.
|
||||||
|
|
||||||
### forcebuy
|
### force_entry
|
||||||
|
|
||||||
`forcebuy` is an optional value, which defaults to the same value as `buy` and is used when sending a `/forcebuy` command from Telegram or from the Rest API.
|
`force_entry` is an optional value, which defaults to the same value as `entry` and is used when sending a `/forceentry` command from Telegram or from the Rest API.
|
||||||
|
|
||||||
### emergencysell
|
### emergency_exit
|
||||||
|
|
||||||
`emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails.
|
`emergency_exit` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails.
|
||||||
The below is the default which is used if not changed in strategy or configuration file.
|
The below is the default which is used if not changed in strategy or configuration file.
|
||||||
|
|
||||||
Example from strategy file:
|
Example from strategy file:
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
order_types = {
|
order_types = {
|
||||||
'buy': 'limit',
|
"entry": "limit",
|
||||||
'sell': 'limit',
|
"exit": "limit",
|
||||||
'emergencysell': 'market',
|
"emergency_exit": "market",
|
||||||
'stoploss': 'market',
|
"stoploss": "market",
|
||||||
'stoploss_on_exchange': True,
|
"stoploss_on_exchange": True,
|
||||||
'stoploss_on_exchange_interval': 60,
|
"stoploss_on_exchange_interval": 60,
|
||||||
'stoploss_on_exchange_limit_ratio': 0.99
|
"stoploss_on_exchange_limit_ratio": 0.99
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -182,7 +183,7 @@ For example, simplified math:
|
||||||
* the bot buys an asset at a price of 100$
|
* the bot buys an asset at a price of 100$
|
||||||
* the stop loss is defined at -10%
|
* the stop loss is defined at -10%
|
||||||
* the stop loss would get triggered once the asset drops below 90$
|
* the stop loss would get triggered once the asset drops below 90$
|
||||||
* stoploss will remain at 90$ unless asset increases to or above our configured offset
|
* stoploss will remain at 90$ unless asset increases to or above the configured offset
|
||||||
* assuming the asset now increases to 103$ (where we have the offset configured)
|
* assuming the asset now increases to 103$ (where we have the offset configured)
|
||||||
* the stop loss will now be -2% of 103$ = 100.94$
|
* the stop loss will now be -2% of 103$ = 100.94$
|
||||||
* now the asset drops in value to 101\$, the stop loss will still be 100.94$ and would trigger at 100.94$
|
* now the asset drops in value to 101\$, the stop loss will still be 100.94$ and would trigger at 100.94$
|
||||||
|
|
|
@ -49,7 +49,7 @@ from freqtrade.exchange import timeframe_to_prev_date
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
|
def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
|
||||||
rate: float, time_in_force: str, sell_reason: str,
|
rate: float, time_in_force: str, exit_reason: str,
|
||||||
current_time: 'datetime', **kwargs) -> bool:
|
current_time: 'datetime', **kwargs) -> bool:
|
||||||
# Obtain pair dataframe.
|
# Obtain pair dataframe.
|
||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
|
@ -77,578 +77,76 @@ class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
## Custom sell signal
|
## Enter Tag
|
||||||
|
|
||||||
It is possible to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision.
|
|
||||||
|
|
||||||
For example you could implement a 1:2 risk-reward ROI with `custom_sell()`.
|
|
||||||
|
|
||||||
Using custom_sell() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
|
|
||||||
|
|
||||||
An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day:
|
|
||||||
|
|
||||||
``` python
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
|
||||||
current_profit: float, **kwargs):
|
|
||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
|
||||||
last_candle = dataframe.iloc[-1].squeeze()
|
|
||||||
|
|
||||||
# Above 20% profit, sell when rsi < 80
|
|
||||||
if current_profit > 0.2:
|
|
||||||
if last_candle['rsi'] < 80:
|
|
||||||
return 'rsi_below_80'
|
|
||||||
|
|
||||||
# Between 2% and 10%, sell if EMA-long above EMA-short
|
|
||||||
if 0.02 < current_profit < 0.1:
|
|
||||||
if last_candle['emalong'] > last_candle['emashort']:
|
|
||||||
return 'ema_long_below_80'
|
|
||||||
|
|
||||||
# Sell any positions at a loss if they are held for more than one day.
|
|
||||||
if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1:
|
|
||||||
return 'unclog'
|
|
||||||
```
|
|
||||||
|
|
||||||
See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks.
|
|
||||||
|
|
||||||
## Buy Tag
|
|
||||||
|
|
||||||
When your strategy has multiple buy signals, you can name the signal that triggered.
|
When your strategy has multiple buy signals, you can name the signal that triggered.
|
||||||
Then you can access you buy signal on `custom_sell`
|
Then you can access you buy signal on `custom_exit`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe['rsi'] < 35) &
|
(dataframe['rsi'] < 35) &
|
||||||
(dataframe['volume'] > 0)
|
(dataframe['volume'] > 0)
|
||||||
),
|
),
|
||||||
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
|
['enter_long', 'enter_tag']] = (1, 'buy_signal_rsi')
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||||
current_profit: float, **kwargs):
|
current_profit: float, **kwargs):
|
||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
last_candle = dataframe.iloc[-1].squeeze()
|
last_candle = dataframe.iloc[-1].squeeze()
|
||||||
if trade.buy_tag == 'buy_signal_rsi' and last_candle['rsi'] > 80:
|
if trade.enter_tag == 'buy_signal_rsi' and last_candle['rsi'] > 80:
|
||||||
return 'sell_signal_rsi'
|
return 'sell_signal_rsi'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
`buy_tag` is limited to 100 characters, remaining data will be truncated.
|
`enter_tag` is limited to 100 characters, remaining data will be truncated.
|
||||||
|
|
||||||
|
## Exit tag
|
||||||
|
|
||||||
## Custom stoploss
|
Similar to [Buy Tagging](#buy-tag), you can also specify a sell tag.
|
||||||
|
|
||||||
The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss.
|
|
||||||
|
|
||||||
The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object.
|
|
||||||
The method must return a stoploss value (float / number) as a percentage of the current price.
|
|
||||||
E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
|
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
# additional imports required
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
from datetime import datetime
|
dataframe.loc[
|
||||||
from freqtrade.persistence import Trade
|
(
|
||||||
|
(dataframe['rsi'] > 70) &
|
||||||
|
(dataframe['volume'] > 0)
|
||||||
|
),
|
||||||
|
['exit_long', 'exit_tag']] = (1, 'exit_rsi')
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
return dataframe
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
use_custom_stoploss = True
|
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
|
||||||
"""
|
|
||||||
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
|
||||||
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
|
||||||
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
|
||||||
|
|
||||||
When not implemented by a strategy, returns the initial stoploss value
|
|
||||||
Only called when use_custom_stoploss is set to True.
|
|
||||||
|
|
||||||
:param pair: Pair that's currently analyzed
|
|
||||||
:param trade: trade object.
|
|
||||||
:param current_time: datetime object, containing the current datetime
|
|
||||||
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
|
||||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
|
||||||
:return float: New stoploss value, relative to the current rate
|
|
||||||
"""
|
|
||||||
return -0.04
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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)).
|
The provided exit-tag is then used as sell-reason - and shown as such in backtest results.
|
||||||
|
|
||||||
!!! 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.
|
|
||||||
|
|
||||||
!!! Tip "Trailing stoploss"
|
|
||||||
It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior.
|
|
||||||
|
|
||||||
### Custom stoploss examples
|
|
||||||
|
|
||||||
The next section will show some examples on what's possible with the custom stoploss function.
|
|
||||||
Of course, many more things are possible, and all examples can be combined at will.
|
|
||||||
|
|
||||||
#### Time based trailing stop
|
|
||||||
|
|
||||||
Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
use_custom_stoploss = True
|
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
|
||||||
|
|
||||||
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
|
|
||||||
if current_time - timedelta(minutes=120) > trade.open_date_utc:
|
|
||||||
return -0.05
|
|
||||||
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
|
|
||||||
return -0.10
|
|
||||||
return 1
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Different stoploss per pair
|
|
||||||
|
|
||||||
Use a different stoploss depending on the pair.
|
|
||||||
In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from datetime import datetime
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
use_custom_stoploss = True
|
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
|
||||||
|
|
||||||
if pair in ('ETH/BTC', 'XRP/BTC'):
|
|
||||||
return -0.10
|
|
||||||
elif pair in ('LTC/BTC'):
|
|
||||||
return -0.05
|
|
||||||
return -0.15
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Trailing stoploss with positive offset
|
|
||||||
|
|
||||||
Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%.
|
|
||||||
|
|
||||||
Please note that the stoploss can only increase, values lower than the current stoploss are ignored.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
use_custom_stoploss = True
|
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
|
||||||
|
|
||||||
if current_profit < 0.04:
|
|
||||||
return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss
|
|
||||||
|
|
||||||
# After reaching the desired offset, allow the stoploss to trail by half the profit
|
|
||||||
desired_stoploss = current_profit / 2
|
|
||||||
|
|
||||||
# Use a minimum of 2.5% and a maximum of 5%
|
|
||||||
return max(min(desired_stoploss, 0.05), 0.025)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Calculating stoploss relative to open price
|
|
||||||
|
|
||||||
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
|
||||||
|
|
||||||
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
|
|
||||||
|
|
||||||
### Calculating stoploss percentage from absolute price
|
|
||||||
|
|
||||||
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
|
||||||
|
|
||||||
The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`.
|
|
||||||
|
|
||||||
#### Stepped stoploss
|
|
||||||
|
|
||||||
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
|
|
||||||
|
|
||||||
* Use the regular stoploss until 20% profit is reached
|
|
||||||
* Once profit is > 20% - set stoploss to 7% above open price.
|
|
||||||
* Once profit is > 25% - set stoploss to 15% above open price.
|
|
||||||
* Once profit is > 40% - set stoploss to 25% above open price.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from datetime import datetime
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
from freqtrade.strategy import stoploss_from_open
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
use_custom_stoploss = True
|
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
|
||||||
|
|
||||||
# evaluate highest to lowest, so that highest possible stop is used
|
|
||||||
if current_profit > 0.40:
|
|
||||||
return stoploss_from_open(0.25, current_profit)
|
|
||||||
elif current_profit > 0.25:
|
|
||||||
return stoploss_from_open(0.15, current_profit)
|
|
||||||
elif current_profit > 0.20:
|
|
||||||
return stoploss_from_open(0.07, current_profit)
|
|
||||||
|
|
||||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
|
||||||
return 1
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Custom stoploss using an indicator from dataframe example
|
|
||||||
|
|
||||||
Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
# <...>
|
|
||||||
dataframe['sar'] = ta.SAR(dataframe)
|
|
||||||
|
|
||||||
use_custom_stoploss = True
|
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
|
||||||
|
|
||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
|
||||||
last_candle = dataframe.iloc[-1].squeeze()
|
|
||||||
|
|
||||||
# Use parabolic sar as absolute stoploss price
|
|
||||||
stoploss_price = last_candle['sar']
|
|
||||||
|
|
||||||
# Convert absolute price to percentage relative to current_rate
|
|
||||||
if stoploss_price < current_rate:
|
|
||||||
return (stoploss_price / current_rate) - 1
|
|
||||||
|
|
||||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
|
||||||
return 1
|
|
||||||
```
|
|
||||||
|
|
||||||
See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Custom order price rules
|
|
||||||
|
|
||||||
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
|
||||||
|
|
||||||
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.
|
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
|
`exit_reason` is limited to 100 characters, remaining data will be truncated.
|
||||||
|
|
||||||
### Custom order entry and exit price example
|
## Strategy version
|
||||||
|
|
||||||
|
You can implement custom strategy versioning by using the "version" method, and returning the version you would like this strategy to have.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
from datetime import datetime, timedelta, timezone
|
def version(self) -> str:
|
||||||
from freqtrade.persistence import Trade
|
"""
|
||||||
|
Returns version of the strategy.
|
||||||
class AwesomeStrategy(IStrategy):
|
"""
|
||||||
|
return "1.1"
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
def custom_entry_price(self, pair: str, current_time: datetime,
|
|
||||||
proposed_rate, **kwargs) -> float:
|
|
||||||
|
|
||||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
|
||||||
timeframe=self.timeframe)
|
|
||||||
new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1]
|
|
||||||
|
|
||||||
return new_entryprice
|
|
||||||
|
|
||||||
def custom_exit_price(self, pair: str, trade: Trade,
|
|
||||||
current_time: datetime, proposed_rate: float,
|
|
||||||
current_profit: float, **kwargs) -> float:
|
|
||||||
|
|
||||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
|
||||||
timeframe=self.timeframe)
|
|
||||||
new_exitprice = dataframe['bollinger_10_upperband'].iat[-1]
|
|
||||||
|
|
||||||
return new_exitprice
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Warning
|
|
||||||
Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter.
|
|
||||||
|
|
||||||
!!! Example
|
|
||||||
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98.
|
|
||||||
|
|
||||||
!!! Warning "No backtesting support"
|
|
||||||
Custom entry-prices are currently not supported during backtesting.
|
|
||||||
|
|
||||||
## Custom order timeout rules
|
|
||||||
|
|
||||||
Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
|
|
||||||
|
|
||||||
However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances.
|
|
||||||
|
|
||||||
### Custom order timeout example
|
|
||||||
|
|
||||||
A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below.
|
|
||||||
It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins.
|
|
||||||
|
|
||||||
The function must return either `True` (cancel order) or `False` (keep order alive).
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
# Set unfilledtimeout to 25 hours, since our maximum timeout from below is 24 hours.
|
|
||||||
unfilledtimeout = {
|
|
||||||
'buy': 60 * 25,
|
|
||||||
'sell': 60 * 25
|
|
||||||
}
|
|
||||||
|
|
||||||
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
|
|
||||||
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5):
|
|
||||||
return True
|
|
||||||
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3):
|
|
||||||
return True
|
|
||||||
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
|
|
||||||
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5):
|
|
||||||
return True
|
|
||||||
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3):
|
|
||||||
return True
|
|
||||||
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
For the above example, `unfilledtimeout` must be set to something bigger than 24h, otherwise that type of timeout will apply first.
|
You should make sure to implement proper version control (like a git repository) alongside this, as freqtrade will not keep historic versions of your strategy, so it's up to the user to be able to eventually roll back to a prior version of the strategy.
|
||||||
|
|
||||||
### Custom order timeout example (using additional data)
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from datetime import datetime
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
# Set unfilledtimeout to 25 hours, since our maximum timeout from below is 24 hours.
|
|
||||||
unfilledtimeout = {
|
|
||||||
'buy': 60 * 25,
|
|
||||||
'sell': 60 * 25
|
|
||||||
}
|
|
||||||
|
|
||||||
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
|
||||||
ob = self.dp.orderbook(pair, 1)
|
|
||||||
current_price = ob['bids'][0][0]
|
|
||||||
# Cancel buy order if price is more than 2% above the order.
|
|
||||||
if current_price > order['price'] * 1.02:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
|
||||||
ob = self.dp.orderbook(pair, 1)
|
|
||||||
current_price = ob['asks'][0][0]
|
|
||||||
# Cancel sell order if price is more than 2% below the order.
|
|
||||||
if current_price < order['price'] * 0.98:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bot loop start callback
|
|
||||||
|
|
||||||
A simple callback which is called once at the start of every bot throttling iteration.
|
|
||||||
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
import requests
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
def bot_loop_start(self, **kwargs) -> None:
|
|
||||||
"""
|
|
||||||
Called at the start of the bot iteration (one loop).
|
|
||||||
Might be used to perform pair-independent tasks
|
|
||||||
(e.g. gather some remote resource for comparison)
|
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
|
||||||
"""
|
|
||||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
|
||||||
# Assign this to the class by using self.*
|
|
||||||
# can then be used by populate_* methods
|
|
||||||
self.remote_data = requests.get('https://some_remote_source.example.com')
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Bot order confirmation
|
|
||||||
|
|
||||||
### Trade entry (buy order) confirmation
|
|
||||||
|
|
||||||
`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect).
|
|
||||||
|
|
||||||
``` python
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
|
||||||
time_in_force: str, current_time: datetime, **kwargs) -> bool:
|
|
||||||
"""
|
|
||||||
Called right before placing a buy order.
|
|
||||||
Timing for this function is critical, so avoid doing heavy computations or
|
|
||||||
network requests in this method.
|
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
|
||||||
|
|
||||||
When not implemented by a strategy, returns True (always confirming).
|
|
||||||
|
|
||||||
:param pair: Pair that's about to be bought.
|
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
|
||||||
:param amount: Amount in target (quote) currency that's going to be traded.
|
|
||||||
:param rate: Rate that's going to be used when using limit orders
|
|
||||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
|
||||||
: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.
|
|
||||||
:return bool: When True is returned, then the buy-order is placed on the exchange.
|
|
||||||
False aborts the process
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Trade exit (sell order) confirmation
|
|
||||||
|
|
||||||
`confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect).
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
|
|
||||||
rate: float, time_in_force: str, sell_reason: str,
|
|
||||||
current_time: datetime, **kwargs) -> bool:
|
|
||||||
"""
|
|
||||||
Called right before placing a regular sell order.
|
|
||||||
Timing for this function is critical, so avoid doing heavy computations or
|
|
||||||
network requests in this method.
|
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
|
||||||
|
|
||||||
When not implemented by a strategy, returns True (always confirming).
|
|
||||||
|
|
||||||
:param pair: Pair that's about to be sold.
|
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
|
||||||
:param amount: Amount in quote currency.
|
|
||||||
:param rate: Rate that's going to be used when using limit orders
|
|
||||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
|
||||||
:param sell_reason: Sell reason.
|
|
||||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
|
||||||
'sell_signal', 'force_sell', 'emergency_sell']
|
|
||||||
: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.
|
|
||||||
:return bool: When True is returned, then the sell-order is placed on the exchange.
|
|
||||||
False aborts the process
|
|
||||||
"""
|
|
||||||
if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0:
|
|
||||||
# Reject force-sells with negative profit
|
|
||||||
# This is just a sample, please adjust to your needs
|
|
||||||
# (this does not necessarily make sense, assuming you know when you're force-selling)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stake size management
|
|
||||||
|
|
||||||
It is possible to manage your risk by reducing or increasing stake amount when placing a new trade.
|
|
||||||
|
|
||||||
```python
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
|
||||||
proposed_stake: float, min_stake: float, max_stake: float,
|
|
||||||
**kwargs) -> float:
|
|
||||||
|
|
||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
|
|
||||||
current_candle = dataframe.iloc[-1].squeeze()
|
|
||||||
|
|
||||||
if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']:
|
|
||||||
if self.config['stake_amount'] == 'unlimited':
|
|
||||||
# Use entire available wallet during favorable conditions when in compounding mode.
|
|
||||||
return max_stake
|
|
||||||
else:
|
|
||||||
# Compound profits during favorable conditions instead of using a static stake.
|
|
||||||
return self.wallets.get_total_stake_amount() / self.config['max_open_trades']
|
|
||||||
|
|
||||||
# Use default stake amount.
|
|
||||||
return proposed_stake
|
|
||||||
```
|
|
||||||
|
|
||||||
Freqtrade will fall back to the `proposed_stake` value should your code raise an exception. The exception itself will be logged.
|
|
||||||
|
|
||||||
!!! Tip
|
|
||||||
You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this acton will be logged.
|
|
||||||
|
|
||||||
!!! Tip
|
|
||||||
Returning `0` or `None` will prevent trades from being placed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Derived strategies
|
## Derived strategies
|
||||||
|
|
||||||
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
||||||
|
|
||||||
``` python
|
``` python title="user_data/strategies/myawesomestrategy.py"
|
||||||
class MyAwesomeStrategy(IStrategy):
|
class MyAwesomeStrategy(IStrategy):
|
||||||
...
|
...
|
||||||
stoploss = 0.13
|
stoploss = 0.13
|
||||||
|
@ -657,6 +155,10 @@ class MyAwesomeStrategy(IStrategy):
|
||||||
# should be in any custom strategy...
|
# should be in any custom strategy...
|
||||||
...
|
...
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
``` python title="user_data/strategies/MyAwesomeStrategy2.py"
|
||||||
|
from myawesomestrategy import MyAwesomeStrategy
|
||||||
class MyAwesomeStrategy2(MyAwesomeStrategy):
|
class MyAwesomeStrategy2(MyAwesomeStrategy):
|
||||||
# Override something
|
# Override something
|
||||||
stoploss = 0.08
|
stoploss = 0.08
|
||||||
|
@ -665,16 +167,7 @@ class MyAwesomeStrategy2(MyAwesomeStrategy):
|
||||||
|
|
||||||
Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need.
|
Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need.
|
||||||
|
|
||||||
!!! Note "Parent-strategy in different files"
|
While keeping the subclass in the same file is technically possible, it can lead to some problems with hyperopt parameter files, we therefore recommend to use separate strategy files, and import the parent strategy as shown above.
|
||||||
If you have the parent-strategy in a different file, you'll need to add the following to the top of your "child"-file to ensure proper loading, otherwise freqtrade may not be able to load the parent strategy correctly.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
sys.path.append(str(Path(__file__).parent))
|
|
||||||
|
|
||||||
from myawesomestrategy import MyAwesomeStrategy
|
|
||||||
```
|
|
||||||
|
|
||||||
## Embedding Strategies
|
## Embedding Strategies
|
||||||
|
|
||||||
|
@ -724,9 +217,9 @@ should be rewritten to
|
||||||
```python
|
```python
|
||||||
frames = [dataframe]
|
frames = [dataframe]
|
||||||
for val in self.buy_ema_short.range:
|
for val in self.buy_ema_short.range:
|
||||||
frames.append({
|
frames.append(DataFrame({
|
||||||
f'ema_short_{val}': ta.EMA(dataframe, timeperiod=val)
|
f'ema_short_{val}': ta.EMA(dataframe, timeperiod=val)
|
||||||
})
|
}))
|
||||||
|
|
||||||
# Append columns to existing dataframe
|
# Append columns to existing dataframe
|
||||||
merged_frame = pd.concat(frames, axis=1)
|
merged_frame = pd.concat(frames, axis=1)
|
||||||
|
|
718
docs/strategy-callbacks.md
Normal file
718
docs/strategy-callbacks.md
Normal file
|
@ -0,0 +1,718 @@
|
||||||
|
# Strategy Callbacks
|
||||||
|
|
||||||
|
While the main strategy functions (`populate_indicators()`, `populate_entry_trend()`, `populate_exit_trend()`) should be used in a vectorized way, and are only called [once during backtesting](bot-basics.md#backtesting-hyperopt-execution-logic), callbacks are called "whenever needed".
|
||||||
|
|
||||||
|
As such, you should avoid doing heavy calculations in callbacks to avoid delays during operations.
|
||||||
|
Depending on the callback used, they may be called when entering / exiting a trade, or throughout the duration of a trade.
|
||||||
|
|
||||||
|
Currently available callbacks:
|
||||||
|
|
||||||
|
* [`bot_loop_start()`](#bot-loop-start)
|
||||||
|
* [`custom_stake_amount()`](#stake-size-management)
|
||||||
|
* [`custom_exit()`](#custom-exit-signal)
|
||||||
|
* [`custom_stoploss()`](#custom-stoploss)
|
||||||
|
* [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules)
|
||||||
|
* [`check_entry_timeout()` and `check_exit_timeout()`](#custom-order-timeout-rules)
|
||||||
|
* [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation)
|
||||||
|
* [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation)
|
||||||
|
* [`adjust_trade_position()`](#adjust-trade-position)
|
||||||
|
* [`leverage()`](#leverage-callback)
|
||||||
|
|
||||||
|
!!! Tip "Callback calling sequence"
|
||||||
|
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
|
||||||
|
|
||||||
|
## Bot loop start
|
||||||
|
|
||||||
|
A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently).
|
||||||
|
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
def bot_loop_start(self, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
Called at the start of the bot iteration (one loop).
|
||||||
|
Might be used to perform pair-independent tasks
|
||||||
|
(e.g. gather some remote resource for comparison)
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
"""
|
||||||
|
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||||
|
# Assign this to the class by using self.*
|
||||||
|
# can then be used by populate_* methods
|
||||||
|
self.remote_data = requests.get('https://some_remote_source.example.com')
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stake size management
|
||||||
|
|
||||||
|
Called before entering a trade, makes it possible to manage your position size when placing a new trade.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
|
proposed_stake: float, min_stake: float, max_stake: float,
|
||||||
|
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||||
|
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
|
||||||
|
current_candle = dataframe.iloc[-1].squeeze()
|
||||||
|
|
||||||
|
if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']:
|
||||||
|
if self.config['stake_amount'] == 'unlimited':
|
||||||
|
# Use entire available wallet during favorable conditions when in compounding mode.
|
||||||
|
return max_stake
|
||||||
|
else:
|
||||||
|
# Compound profits during favorable conditions instead of using a static stake.
|
||||||
|
return self.wallets.get_total_stake_amount() / self.config['max_open_trades']
|
||||||
|
|
||||||
|
# Use default stake amount.
|
||||||
|
return proposed_stake
|
||||||
|
```
|
||||||
|
|
||||||
|
Freqtrade will fall back to the `proposed_stake` value should your code raise an exception. The exception itself will be logged.
|
||||||
|
|
||||||
|
!!! Tip
|
||||||
|
You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this action will be logged.
|
||||||
|
|
||||||
|
!!! Tip
|
||||||
|
Returning `0` or `None` will prevent trades from being placed.
|
||||||
|
|
||||||
|
## Custom exit signal
|
||||||
|
|
||||||
|
Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed.
|
||||||
|
|
||||||
|
Allows to define custom exit signals, indicating that specified position should be sold. This is very useful when we need to customize exit conditions for each individual trade, or if you need trade data to make an exit decision.
|
||||||
|
|
||||||
|
For example you could implement a 1:2 risk-reward ROI with `custom_exit()`.
|
||||||
|
|
||||||
|
Using `custom_exit()` signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Returning a (none-empty) `string` or `True` from this method is equal to setting exit signal on a candle at specified time. This method is not called when exit signal is set already, or if exit signals are disabled (`use_exit_signal=False`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
|
||||||
|
`custom_exit()` will ignore `exit_profit_only`, and will always be called unless `use_exit_signal=False`, even if there is a new enter signal.
|
||||||
|
|
||||||
|
An example of how we can use different indicators depending on the current profit and also exit trades that were open longer than one day:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
||||||
|
current_profit: float, **kwargs):
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
|
last_candle = dataframe.iloc[-1].squeeze()
|
||||||
|
|
||||||
|
# Above 20% profit, sell when rsi < 80
|
||||||
|
if current_profit > 0.2:
|
||||||
|
if last_candle['rsi'] < 80:
|
||||||
|
return 'rsi_below_80'
|
||||||
|
|
||||||
|
# Between 2% and 10%, sell if EMA-long above EMA-short
|
||||||
|
if 0.02 < current_profit < 0.1:
|
||||||
|
if last_candle['emalong'] > last_candle['emashort']:
|
||||||
|
return 'ema_long_below_80'
|
||||||
|
|
||||||
|
# Sell any positions at a loss if they are held for more than one day.
|
||||||
|
if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1:
|
||||||
|
return 'unclog'
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks.
|
||||||
|
|
||||||
|
## Custom stoploss
|
||||||
|
|
||||||
|
Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed.
|
||||||
|
|
||||||
|
The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object.
|
||||||
|
|
||||||
|
The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss (before this method is called for the first time for a trade).
|
||||||
|
|
||||||
|
The method must return a stoploss value (float / number) as a percentage of the current price.
|
||||||
|
E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
# additional imports required
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
"""
|
||||||
|
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
||||||
|
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||||
|
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
||||||
|
|
||||||
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
|
|
||||||
|
When not implemented by a strategy, returns the initial stoploss value
|
||||||
|
Only called when use_custom_stoploss is set to True.
|
||||||
|
|
||||||
|
:param pair: Pair that's currently analyzed
|
||||||
|
:param trade: trade object.
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
|
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||||
|
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
:return float: New stoploss value, relative to the current rate
|
||||||
|
"""
|
||||||
|
return -0.04
|
||||||
|
```
|
||||||
|
|
||||||
|
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)).
|
||||||
|
|
||||||
|
!!! 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.
|
||||||
|
|
||||||
|
!!! Tip "Trailing stoploss"
|
||||||
|
It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior.
|
||||||
|
|
||||||
|
### Custom stoploss examples
|
||||||
|
|
||||||
|
The next section will show some examples on what's possible with the custom stoploss function.
|
||||||
|
Of course, many more things are possible, and all examples can be combined at will.
|
||||||
|
|
||||||
|
#### Time based trailing stop
|
||||||
|
|
||||||
|
Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
|
||||||
|
if current_time - timedelta(minutes=120) > trade.open_date_utc:
|
||||||
|
return -0.05
|
||||||
|
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
|
||||||
|
return -0.10
|
||||||
|
return 1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Different stoploss per pair
|
||||||
|
|
||||||
|
Use a different stoploss depending on the pair.
|
||||||
|
In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
if pair in ('ETH/BTC', 'XRP/BTC'):
|
||||||
|
return -0.10
|
||||||
|
elif pair in ('LTC/BTC'):
|
||||||
|
return -0.05
|
||||||
|
return -0.15
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Trailing stoploss with positive offset
|
||||||
|
|
||||||
|
Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%.
|
||||||
|
|
||||||
|
Please note that the stoploss can only increase, values lower than the current stoploss are ignored.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
if current_profit < 0.04:
|
||||||
|
return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss
|
||||||
|
|
||||||
|
# After reaching the desired offset, allow the stoploss to trail by half the profit
|
||||||
|
desired_stoploss = current_profit / 2
|
||||||
|
|
||||||
|
# Use a minimum of 2.5% and a maximum of 5%
|
||||||
|
return max(min(desired_stoploss, 0.05), 0.025)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Stepped stoploss
|
||||||
|
|
||||||
|
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
|
||||||
|
|
||||||
|
* Use the regular stoploss until 20% profit is reached
|
||||||
|
* Once profit is > 20% - set stoploss to 7% above open price.
|
||||||
|
* Once profit is > 25% - set stoploss to 15% above open price.
|
||||||
|
* Once profit is > 40% - set stoploss to 25% above open price.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import stoploss_from_open
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
# evaluate highest to lowest, so that highest possible stop is used
|
||||||
|
if current_profit > 0.40:
|
||||||
|
return stoploss_from_open(0.25, current_profit, is_short=trade.is_short)
|
||||||
|
elif current_profit > 0.25:
|
||||||
|
return stoploss_from_open(0.15, current_profit, is_short=trade.is_short)
|
||||||
|
elif current_profit > 0.20:
|
||||||
|
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
|
||||||
|
|
||||||
|
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||||
|
return 1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custom stoploss using an indicator from dataframe example
|
||||||
|
|
||||||
|
Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# <...>
|
||||||
|
dataframe['sar'] = ta.SAR(dataframe)
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
|
last_candle = dataframe.iloc[-1].squeeze()
|
||||||
|
|
||||||
|
# Use parabolic sar as absolute stoploss price
|
||||||
|
stoploss_price = last_candle['sar']
|
||||||
|
|
||||||
|
# Convert absolute price to percentage relative to current_rate
|
||||||
|
if stoploss_price < current_rate:
|
||||||
|
return (stoploss_price / current_rate) - 1
|
||||||
|
|
||||||
|
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||||
|
return 1
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks.
|
||||||
|
|
||||||
|
### Common helpers for stoploss calculations
|
||||||
|
|
||||||
|
#### Stoploss relative to open price
|
||||||
|
|
||||||
|
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
||||||
|
|
||||||
|
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||||
|
|
||||||
|
#### Stoploss percentage from absolute price
|
||||||
|
|
||||||
|
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
||||||
|
|
||||||
|
The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Custom order price rules
|
||||||
|
|
||||||
|
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
||||||
|
|
||||||
|
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.
|
||||||
|
|
||||||
|
Each of these methods are called right before placing an order on the exchange.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
|
||||||
|
|
||||||
|
### Custom order entry and exit price example
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
|
||||||
|
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||||
|
|
||||||
|
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||||
|
timeframe=self.timeframe)
|
||||||
|
new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1]
|
||||||
|
|
||||||
|
return new_entryprice
|
||||||
|
|
||||||
|
def custom_exit_price(self, pair: str, trade: Trade,
|
||||||
|
current_time: datetime, proposed_rate: float,
|
||||||
|
current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||||
|
timeframe=self.timeframe)
|
||||||
|
new_exitprice = dataframe['bollinger_10_upperband'].iat[-1]
|
||||||
|
|
||||||
|
return new_exitprice
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter.
|
||||||
|
**Example**:
|
||||||
|
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate.
|
||||||
|
|
||||||
|
!!! Warning "Backtesting"
|
||||||
|
Custom prices are supported in backtesting (starting with 2021.12), and orders will fill if the price falls within the candle's low/high range.
|
||||||
|
Orders that don't fill immediately are subject to regular timeout handling, which happens once per (detail) candle.
|
||||||
|
`custom_exit_price()` is only called for sells of type exit_signal and Custom exit. All other exit-types will use regular backtesting prices.
|
||||||
|
|
||||||
|
## Custom order timeout rules
|
||||||
|
|
||||||
|
Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
|
||||||
|
|
||||||
|
However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Backtesting fills orders if their price falls within the candle's low/high range.
|
||||||
|
The below callbacks will be called once per (detail) candle for orders that don't fill immediately (which use custom pricing).
|
||||||
|
|
||||||
|
### Custom order timeout example
|
||||||
|
|
||||||
|
Called for every open order until that order is either filled or cancelled.
|
||||||
|
`check_entry_timeout()` is called for trade entries, while `check_exit_timeout()` is called for trade exit orders.
|
||||||
|
|
||||||
|
A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below.
|
||||||
|
It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins.
|
||||||
|
|
||||||
|
The function must return either `True` (cancel order) or `False` (keep order alive).
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from freqtrade.persistence import Trade, Order
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
|
||||||
|
unfilledtimeout = {
|
||||||
|
'entry': 60 * 25,
|
||||||
|
'exit': 60 * 25
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
|
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
|
||||||
|
return True
|
||||||
|
elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3):
|
||||||
|
return True
|
||||||
|
elif trade.open_rate < 1 and trade.open_date_utc < current_time - timedelta(hours=24):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_exit_timeout(self, pair: str, trade: Trade, order: 'Order',
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
|
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
|
||||||
|
return True
|
||||||
|
elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3):
|
||||||
|
return True
|
||||||
|
elif trade.open_rate < 1 and trade.open_date_utc < current_time - timedelta(hours=24):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
For the above example, `unfilledtimeout` must be set to something bigger than 24h, otherwise that type of timeout will apply first.
|
||||||
|
|
||||||
|
### Custom order timeout example (using additional data)
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade, Order
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
|
||||||
|
unfilledtimeout = {
|
||||||
|
'entry': 60 * 25,
|
||||||
|
'exit': 60 * 25
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
|
ob = self.dp.orderbook(pair, 1)
|
||||||
|
current_price = ob['bids'][0][0]
|
||||||
|
# Cancel buy order if price is more than 2% above the order.
|
||||||
|
if current_price > order.price * 1.02:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
|
ob = self.dp.orderbook(pair, 1)
|
||||||
|
current_price = ob['asks'][0][0]
|
||||||
|
# Cancel sell order if price is more than 2% below the order.
|
||||||
|
if current_price < order.price * 0.98:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bot order confirmation
|
||||||
|
|
||||||
|
Confirm trade entry / exits.
|
||||||
|
This are the last methods that will be called before an order is placed.
|
||||||
|
|
||||||
|
### Trade entry (buy order) confirmation
|
||||||
|
|
||||||
|
`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect).
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||||
|
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
|
||||||
|
side: str, **kwargs) -> bool:
|
||||||
|
"""
|
||||||
|
Called right before placing a entry order.
|
||||||
|
Timing for this function is critical, so avoid doing heavy computations or
|
||||||
|
network requests in this method.
|
||||||
|
|
||||||
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
|
|
||||||
|
When not implemented by a strategy, returns True (always confirming).
|
||||||
|
|
||||||
|
:param pair: Pair that's about to be bought/shorted.
|
||||||
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
|
:param amount: Amount in target (quote) currency that's going to be traded.
|
||||||
|
:param rate: Rate that's going to be used when using limit orders
|
||||||
|
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
|
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
:return bool: When True is returned, then the buy-order is placed on the exchange.
|
||||||
|
False aborts the process
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trade exit (sell order) confirmation
|
||||||
|
|
||||||
|
`confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect).
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
|
||||||
|
rate: float, time_in_force: str, exit_reason: str,
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
|
"""
|
||||||
|
Called right before placing a regular sell order.
|
||||||
|
Timing for this function is critical, so avoid doing heavy computations or
|
||||||
|
network requests in this method.
|
||||||
|
|
||||||
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
|
|
||||||
|
When not implemented by a strategy, returns True (always confirming).
|
||||||
|
|
||||||
|
:param pair: Pair that's about to be sold.
|
||||||
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
|
:param amount: Amount in quote currency.
|
||||||
|
:param rate: Rate that's going to be used when using limit orders
|
||||||
|
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
|
:param exit_reason: Exit reason.
|
||||||
|
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||||
|
'exit_signal', 'force_exit', 'emergency_exit']
|
||||||
|
: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.
|
||||||
|
:return bool: When True is returned, then the exit-order is placed on the exchange.
|
||||||
|
False aborts the process
|
||||||
|
"""
|
||||||
|
if exit_reason == 'force_exit' and trade.calc_profit_ratio(rate) < 0:
|
||||||
|
# Reject force-sells with negative profit
|
||||||
|
# This is just a sample, please adjust to your needs
|
||||||
|
# (this does not necessarily make sense, assuming you know when you're force-selling)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adjust trade position
|
||||||
|
|
||||||
|
The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in the strategy.
|
||||||
|
For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled.
|
||||||
|
`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging).
|
||||||
|
|
||||||
|
`max_entry_position_adjustment` property is used to limit the number of additional buys per trade (on top of the first buy) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment buys.
|
||||||
|
|
||||||
|
The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional buy order should be made (position is increased).
|
||||||
|
If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored.
|
||||||
|
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
|
||||||
|
|
||||||
|
This callback is **not** called when there is an open order (either buy or sell) waiting for execution, or when you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`.
|
||||||
|
`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, no matter if it's a long or short trade. Modifications to leverage are not possible.
|
||||||
|
|
||||||
|
!!! Note "About stake size"
|
||||||
|
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
|
||||||
|
If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that.
|
||||||
|
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
|
||||||
|
Stoploss is still calculated from the initial opening price, not averaged price.
|
||||||
|
|
||||||
|
!!! Warning "/stopbuy"
|
||||||
|
While `/stopbuy` command stops the bot from entering new trades, the position adjustment feature will continue buying new orders on existing trades.
|
||||||
|
|
||||||
|
!!! Warning "Backtesting"
|
||||||
|
During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so performance will be affected.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
|
class DigDeeperStrategy(IStrategy):
|
||||||
|
|
||||||
|
position_adjustment_enable = True
|
||||||
|
|
||||||
|
# Attempts to handle large drops with DCA. High stoploss is required.
|
||||||
|
stoploss = -0.30
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
# Example specific variables
|
||||||
|
max_entry_position_adjustment = 3
|
||||||
|
# This number is explained a bit further down
|
||||||
|
max_dca_multiplier = 5.5
|
||||||
|
|
||||||
|
# This is called when placing the initial order (opening trade)
|
||||||
|
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
|
proposed_stake: float, min_stake: float, max_stake: float,
|
||||||
|
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||||
|
|
||||||
|
# We need to leave most of the funds for possible further DCA orders
|
||||||
|
# This also applies to fixed stakes
|
||||||
|
return proposed_stake / self.max_dca_multiplier
|
||||||
|
|
||||||
|
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, min_stake: float,
|
||||||
|
max_stake: float, **kwargs):
|
||||||
|
"""
|
||||||
|
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||||
|
This means extra buy orders with additional fees.
|
||||||
|
|
||||||
|
:param trade: trade object.
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
|
:param current_rate: Current buy rate.
|
||||||
|
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||||
|
:param min_stake: Minimal stake size allowed by exchange.
|
||||||
|
:param max_stake: Balance available for trading.
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
:return float: Stake amount to adjust your trade
|
||||||
|
"""
|
||||||
|
|
||||||
|
if current_profit > -0.05:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Obtain pair dataframe (just to show how to access it)
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
|
||||||
|
# Only buy when not actively falling price.
|
||||||
|
last_candle = dataframe.iloc[-1].squeeze()
|
||||||
|
previous_candle = dataframe.iloc[-2].squeeze()
|
||||||
|
if last_candle['close'] < previous_candle['close']:
|
||||||
|
return None
|
||||||
|
|
||||||
|
filled_entries = trade.select_filled_orders(trade.entry_side)
|
||||||
|
count_of_entries = trade.nr_of_successful_entries
|
||||||
|
# Allow up to 3 additional increasingly larger buys (4 in total)
|
||||||
|
# Initial buy is 1x
|
||||||
|
# If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2%
|
||||||
|
# If that falls down to -5% again, we buy 1.5x more
|
||||||
|
# If that falls once again down to -5%, we buy 1.75x more
|
||||||
|
# Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake.
|
||||||
|
# That is why max_dca_multiplier is 5.5
|
||||||
|
# Hope you have a deep wallet!
|
||||||
|
try:
|
||||||
|
# This returns first order stake size
|
||||||
|
stake_amount = filled_entries[0].cost
|
||||||
|
# This then calculates current safety order size
|
||||||
|
stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
|
||||||
|
return stake_amount
|
||||||
|
except Exception as exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Leverage Callback
|
||||||
|
|
||||||
|
When trading in markets that allow leverage, this method must return the desired Leverage (Defaults to 1 -> No leverage).
|
||||||
|
|
||||||
|
Assuming a capital of 500USDT, a trade with leverage=3 would result in a position with 500 x 3 = 1500 USDT.
|
||||||
|
|
||||||
|
Values that are above `max_leverage` will be adjusted to `max_leverage`.
|
||||||
|
For markets / exchanges that don't support leverage, this method is ignored.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def leverage(self, pair: str, current_time: 'datetime', current_rate: float,
|
||||||
|
proposed_leverage: float, max_leverage: float, side: str,
|
||||||
|
**kwargs) -> float:
|
||||||
|
"""
|
||||||
|
Customize leverage for each new trade.
|
||||||
|
|
||||||
|
:param pair: Pair that's currently analyzed
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
|
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||||
|
:param proposed_leverage: A leverage proposed by the bot.
|
||||||
|
:param max_leverage: Max leverage allowed on this pair
|
||||||
|
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||||
|
:return: A leverage amount, which is between 1.0 and max_leverage.
|
||||||
|
"""
|
||||||
|
return 1.0
|
||||||
|
```
|
|
@ -4,40 +4,30 @@ This page explains how to customize your strategies, add new indicators and set
|
||||||
|
|
||||||
Please familiarize yourself with [Freqtrade basics](bot-basics.md) first, which provides overall info on how the bot operates.
|
Please familiarize yourself with [Freqtrade basics](bot-basics.md) first, which provides overall info on how the bot operates.
|
||||||
|
|
||||||
## Install a custom strategy file
|
|
||||||
|
|
||||||
This is very simple. Copy paste your strategy file into the directory `user_data/strategies`.
|
|
||||||
|
|
||||||
Let assume you have a class called `AwesomeStrategy` in the file `AwesomeStrategy.py`:
|
|
||||||
|
|
||||||
1. Move your file into `user_data/strategies` (you should have `user_data/strategies/AwesomeStrategy.py`
|
|
||||||
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade trade --strategy AwesomeStrategy
|
|
||||||
```
|
|
||||||
|
|
||||||
## Develop your own strategy
|
## Develop your own strategy
|
||||||
|
|
||||||
The bot includes a default strategy file.
|
The bot includes a default strategy file.
|
||||||
Also, several other strategies are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies).
|
Also, several other strategies are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies).
|
||||||
|
|
||||||
You will however most likely have your own idea for a strategy.
|
You will however most likely have your own idea for a strategy.
|
||||||
This document intends to help you develop one for yourself.
|
This document intends to help you convert your strategy idea into your own strategy.
|
||||||
|
|
||||||
To get started, use `freqtrade new-strategy --strategy AwesomeStrategy`.
|
To get started, use `freqtrade new-strategy --strategy AwesomeStrategy` (you can obviously use your own naming for your strategy).
|
||||||
This will create a new strategy file from a template, which will be located under `user_data/strategies/AwesomeStrategy.py`.
|
This will create a new strategy file from a template, which will be located under `user_data/strategies/AwesomeStrategy.py`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This is just a template file, which will most likely not be profitable out of the box.
|
This is just a template file, which will most likely not be profitable out of the box.
|
||||||
|
|
||||||
|
??? Hint "Different template levels"
|
||||||
|
`freqtrade new-strategy` has an additional parameter, `--template`, which controls the amount of pre-build information you get in the created strategy. Use `--template minimal` to get an empty strategy without any indicator examples, or `--template advanced` to get a template with most callbacks defined.
|
||||||
|
|
||||||
### Anatomy of a strategy
|
### Anatomy of a strategy
|
||||||
|
|
||||||
A strategy file contains all the information needed to build a good strategy:
|
A strategy file contains all the information needed to build a good strategy:
|
||||||
|
|
||||||
- Indicators
|
- Indicators
|
||||||
- Buy strategy rules
|
- Entry strategy rules
|
||||||
- Sell strategy rules
|
- Exit strategy rules
|
||||||
- Minimal ROI recommended
|
- Minimal ROI recommended
|
||||||
- Stoploss strongly recommended
|
- Stoploss strongly recommended
|
||||||
|
|
||||||
|
@ -45,7 +35,7 @@ The bot also include a sample strategy called `SampleStrategy` you can update: `
|
||||||
You can test it with the parameter: `--strategy SampleStrategy`
|
You can test it with the parameter: `--strategy SampleStrategy`
|
||||||
|
|
||||||
Additionally, there is an attribute called `INTERFACE_VERSION`, which defines the version of the strategy interface the bot should use.
|
Additionally, there is an attribute called `INTERFACE_VERSION`, which defines the version of the strategy interface the bot should use.
|
||||||
The current version is 2 - which is also the default when it's not set explicitly in the strategy.
|
The current version is 3 - which is also the default when it's not set explicitly in the strategy.
|
||||||
|
|
||||||
Future versions will require this to be set.
|
Future versions will require this to be set.
|
||||||
|
|
||||||
|
@ -67,11 +57,51 @@ file as reference.**
|
||||||
needs to take care to avoid having the strategy utilize data from the future.
|
needs to take care to avoid having the strategy utilize data from the future.
|
||||||
Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document.
|
Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document.
|
||||||
|
|
||||||
|
### Dataframe
|
||||||
|
|
||||||
|
Freqtrade uses [pandas](https://pandas.pydata.org/) to store/provide the candlestick (OHLCV) data.
|
||||||
|
Pandas is a great library developed for processing large amounts of data.
|
||||||
|
|
||||||
|
Each row in a dataframe corresponds to one candle on a chart, with the latest candle always being the last in the dataframe (sorted by date).
|
||||||
|
|
||||||
|
``` output
|
||||||
|
> dataframe.head()
|
||||||
|
date open high low close volume
|
||||||
|
0 2021-11-09 23:25:00+00:00 67279.67 67321.84 67255.01 67300.97 44.62253
|
||||||
|
1 2021-11-09 23:30:00+00:00 67300.97 67301.34 67183.03 67187.01 61.38076
|
||||||
|
2 2021-11-09 23:35:00+00:00 67187.02 67187.02 67031.93 67123.81 113.42728
|
||||||
|
3 2021-11-09 23:40:00+00:00 67123.80 67222.40 67080.33 67160.48 78.96008
|
||||||
|
4 2021-11-09 23:45:00+00:00 67160.48 67160.48 66901.26 66943.37 111.39292
|
||||||
|
```
|
||||||
|
|
||||||
|
Pandas provides fast ways to calculate metrics. To benefit from this speed, it's advised to not use loops, but use vectorized methods instead.
|
||||||
|
|
||||||
|
Vectorized operations perform calculations across the whole range of data and are therefore, compared to looping through each row, a lot faster when calculating indicators.
|
||||||
|
|
||||||
|
As a dataframe is a table, simple python comparisons like the following will not work
|
||||||
|
|
||||||
|
``` python
|
||||||
|
if dataframe['rsi'] > 30:
|
||||||
|
dataframe['enter_long'] = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
The above section will fail with `The truth value of a Series is ambiguous. [...]`.
|
||||||
|
|
||||||
|
This must instead be written in a pandas-compatible way, so the operation is performed across the whole dataframe.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe['rsi'] > 30)
|
||||||
|
, 'enter_long'] = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
With this section, you have a new column in your dataframe, which has `1` assigned whenever RSI is above 30.
|
||||||
|
|
||||||
### Customize Indicators
|
### Customize Indicators
|
||||||
|
|
||||||
Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
|
Buy and sell signals need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
|
||||||
|
|
||||||
You should only add the indicators used in either `populate_buy_trend()`, `populate_sell_trend()`, or to populate another indicator, otherwise performance may suffer.
|
You should only add the indicators used in either `populate_entry_trend()`, `populate_exit_trend()`, or to populate another indicator, otherwise performance may suffer.
|
||||||
|
|
||||||
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
||||||
|
|
||||||
|
@ -122,9 +152,19 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
|
||||||
Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py).
|
Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py).
|
||||||
Then uncomment indicators you need.
|
Then uncomment indicators you need.
|
||||||
|
|
||||||
|
#### Indicator libraries
|
||||||
|
|
||||||
|
Out of the box, freqtrade installs the following technical libraries:
|
||||||
|
|
||||||
|
* [ta-lib](http://mrjbq7.github.io/ta-lib/)
|
||||||
|
* [pandas-ta](https://twopirllc.github.io/pandas-ta/)
|
||||||
|
* [technical](https://github.com/freqtrade/technical/)
|
||||||
|
|
||||||
|
Additional technical libraries can be installed as necessary, or custom indicators may be written / invented by the strategy author.
|
||||||
|
|
||||||
### Strategy startup period
|
### Strategy startup period
|
||||||
|
|
||||||
Most indicators have an instable startup period, in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
|
Most indicators have an instable startup period, in which they are either not available (NaN), or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
|
||||||
To account for this, the strategy can be assigned the `startup_candle_count` attribute.
|
To account for this, the strategy can be assigned the `startup_candle_count` attribute.
|
||||||
This should be set to the maximum number of candles that the strategy requires to calculate stable indicators.
|
This should be set to the maximum number of candles that the strategy requires to calculate stable indicators.
|
||||||
|
|
||||||
|
@ -136,8 +176,14 @@ In this example strategy, this should be set to 100 (`startup_candle_count = 100
|
||||||
|
|
||||||
By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt.
|
By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt.
|
||||||
|
|
||||||
|
!!! Warning "Using x calls to get OHLCV"
|
||||||
|
If you receive a warning like `WARNING - Using 3 calls to get OHLCV. This can result in slower operations for the bot. Please check if you really need 1500 candles for your strategy` - you should consider if you really need this much historic data for your signals.
|
||||||
|
Having this will cause Freqtrade to make multiple calls for the same pair, which will obviously be slower than one network request.
|
||||||
|
As a consequence, Freqtrade will take longer to refresh candles - and should therefore be avoided if possible.
|
||||||
|
This is capped to 5 total calls to avoid overloading the exchange, or make freqtrade too slow.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
`startup_candle_count` should be below `ohlcv_candle_limit` (which is 500 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations.
|
`startup_candle_count` should be below `ohlcv_candle_limit * 5` (which is 500 * 5 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations.
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
|
@ -153,18 +199,18 @@ If this data is available, indicators will be calculated with this extended time
|
||||||
!!! Note
|
!!! Note
|
||||||
If data for the startup period is not available, then the timerange will be adjusted to account for this startup period - so Backtesting would start at 2019-01-01 08:30:00.
|
If data for the startup period is not available, then the timerange will be adjusted to account for this startup period - so Backtesting would start at 2019-01-01 08:30:00.
|
||||||
|
|
||||||
### Buy signal rules
|
### Entry signal rules
|
||||||
|
|
||||||
Edit the method `populate_buy_trend()` in your strategy file to update your buy strategy.
|
Edit the method `populate_entry_trend()` in your strategy file to update your entry strategy.
|
||||||
|
|
||||||
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
||||||
|
|
||||||
This method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action".
|
This method will also define a new column, `"enter_long"` (`"enter_short"` for shorts), which needs to contain 1 for entries, and 0 for "no action". `enter_long` is a mandatory column that must be set even if the strategy is shorting only.
|
||||||
|
|
||||||
Sample from `user_data/strategies/sample_strategy.py`:
|
Sample from `user_data/strategies/sample_strategy.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the buy signal for the given dataframe
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
:param dataframe: DataFrame populated with indicators
|
:param dataframe: DataFrame populated with indicators
|
||||||
|
@ -178,29 +224,58 @@ def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard
|
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard
|
||||||
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
),
|
),
|
||||||
'buy'] = 1
|
['enter_long', 'enter_tag']] = (1, 'rsi_cross')
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
|
??? Note "Enter short trades"
|
||||||
|
Short-entries can be created by setting `enter_short` (corresponds to `enter_long` for long trades).
|
||||||
|
The `enter_tag` column remains identical.
|
||||||
|
Short-trades need to be supported by your exchange and market configuration!
|
||||||
|
Please make sure to set [`can_short`]() appropriately on your strategy if you intend to short.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30
|
||||||
|
(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard
|
||||||
|
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard
|
||||||
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
|
),
|
||||||
|
['enter_long', 'enter_tag']] = (1, 'rsi_cross')
|
||||||
|
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(qtpylib.crossed_below(dataframe['rsi'], 70)) & # Signal: RSI crosses below 70
|
||||||
|
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard
|
||||||
|
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard
|
||||||
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
|
),
|
||||||
|
['enter_short', 'enter_tag']] = (1, 'rsi_cross')
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Buying requires sellers to buy from - therefore volume needs to be > 0 (`dataframe['volume'] > 0`) to make sure that the bot does not buy/sell in no-activity periods.
|
Buying requires sellers to buy from - therefore volume needs to be > 0 (`dataframe['volume'] > 0`) to make sure that the bot does not buy/sell in no-activity periods.
|
||||||
|
|
||||||
### Sell signal rules
|
### Exit signal rules
|
||||||
|
|
||||||
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
|
Edit the method `populate_exit_trend()` into your strategy file to update your exit strategy.
|
||||||
Please note that the sell-signal is only used if `use_sell_signal` is set to true in the configuration.
|
Please note that the exit-signal is only used if `use_exit_signal` is set to true in the configuration.
|
||||||
|
|
||||||
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
||||||
|
|
||||||
This method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action".
|
This method will also define a new column, `"exit_long"` (`"exit_short"` for shorts), which needs to contain 1 for exits, and 0 for "no action".
|
||||||
|
|
||||||
Sample from `user_data/strategies/sample_strategy.py`:
|
Sample from `user_data/strategies/sample_strategy.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the sell signal for the given dataframe
|
Based on TA indicators, populates the exit signal for the given dataframe
|
||||||
:param dataframe: DataFrame populated with indicators
|
:param dataframe: DataFrame populated with indicators
|
||||||
:param metadata: Additional information, like the currently traded pair
|
:param metadata: Additional information, like the currently traded pair
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
|
@ -212,13 +287,39 @@ def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame
|
||||||
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard
|
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard
|
||||||
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
),
|
),
|
||||||
'sell'] = 1
|
['exit_long', 'exit_tag']] = (1, 'rsi_too_high')
|
||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
|
??? Note "Exit short trades"
|
||||||
|
Short-exits can be created by setting `exit_short` (corresponds to `exit_long`).
|
||||||
|
The `exit_tag` column remains identical.
|
||||||
|
Short-trades need to be supported by your exchange and market configuration!
|
||||||
|
|
||||||
|
```python
|
||||||
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70
|
||||||
|
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard
|
||||||
|
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard
|
||||||
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
|
),
|
||||||
|
['exit_long', 'exit_tag']] = (1, 'rsi_too_high')
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(qtpylib.crossed_below(dataframe['rsi'], 30)) & # Signal: RSI crosses below 30
|
||||||
|
(dataframe['tema'] < dataframe['bb_middleband']) & # Guard
|
||||||
|
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard
|
||||||
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
|
),
|
||||||
|
['exit_short', 'exit_tag']] = (1, 'rsi_too_low')
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
### Minimal ROI
|
### Minimal ROI
|
||||||
|
|
||||||
This dict defines the minimal Return On Investment (ROI) a trade should reach before selling, independent from the sell signal.
|
This dict defines the minimal Return On Investment (ROI) a trade should reach before exiting, independent from the exit signal.
|
||||||
|
|
||||||
It is of the following format, with the dict key (left side of the colon) being the minutes passed since the trade opened, and the value (right side of the colon) being the percentage.
|
It is of the following format, with the dict key (left side of the colon) being the minutes passed since the trade opened, and the value (right side of the colon) being the percentage.
|
||||||
|
|
||||||
|
@ -233,10 +334,10 @@ minimal_roi = {
|
||||||
|
|
||||||
The above configuration would therefore mean:
|
The above configuration would therefore mean:
|
||||||
|
|
||||||
- Sell whenever 4% profit was reached
|
- Exit whenever 4% profit was reached
|
||||||
- Sell when 2% profit was reached (in effect after 20 minutes)
|
- Exit when 2% profit was reached (in effect after 20 minutes)
|
||||||
- Sell when 1% profit was reached (in effect after 30 minutes)
|
- Exit when 1% profit was reached (in effect after 30 minutes)
|
||||||
- Sell when trade is non-loosing (in effect after 40 minutes)
|
- Exit when trade is non-loosing (in effect after 40 minutes)
|
||||||
|
|
||||||
The calculation does include fees.
|
The calculation does include fees.
|
||||||
|
|
||||||
|
@ -248,7 +349,7 @@ minimal_roi = {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
While technically not completely disabled, this would sell once the trade reaches 10000% Profit.
|
While technically not completely disabled, this would exit once the trade reaches 10000% Profit.
|
||||||
|
|
||||||
To use times based on candle duration (timeframe), the following snippet can be handy.
|
To use times based on candle duration (timeframe), the following snippet can be handy.
|
||||||
This will allow you to change the timeframe for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...)
|
This will allow you to change the timeframe for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...)
|
||||||
|
@ -271,38 +372,51 @@ class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
Setting a stoploss is highly recommended to protect your capital from strong moves against you.
|
Setting a stoploss is highly recommended to protect your capital from strong moves against you.
|
||||||
|
|
||||||
Sample:
|
Sample of setting a 10% stoploss:
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
stoploss = -0.10
|
stoploss = -0.10
|
||||||
```
|
```
|
||||||
|
|
||||||
This would signify a stoploss of -10%.
|
|
||||||
|
|
||||||
For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md).
|
For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md).
|
||||||
|
|
||||||
If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order_types dictionary, so your stoploss is on the exchange and cannot be missed due to network problems, high load or other reasons.
|
### Timeframe
|
||||||
|
|
||||||
For more information on order_types please look [here](configuration.md#understand-order_types).
|
|
||||||
|
|
||||||
### Timeframe (formerly ticker interval)
|
|
||||||
|
|
||||||
This is the set of candles the bot should download and use for the analysis.
|
This is the set of candles the bot should download and use for the analysis.
|
||||||
Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work.
|
Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work.
|
||||||
|
|
||||||
Please note that the same buy/sell signals may work well with one timeframe, but not with the others.
|
Please note that the same entry/exit signals may work well with one timeframe, but not with the others.
|
||||||
|
|
||||||
This setting is accessible within the strategy methods as the `self.timeframe` attribute.
|
This setting is accessible within the strategy methods as the `self.timeframe` attribute.
|
||||||
|
|
||||||
|
### Can short
|
||||||
|
|
||||||
|
To use short signals in futures markets, you will have to let us know to do so by setting `can_short=True`.
|
||||||
|
Strategies which enable this will fail to load on spot markets.
|
||||||
|
Disabling of this will have short signals ignored (also in futures markets).
|
||||||
|
|
||||||
### Metadata dict
|
### Metadata dict
|
||||||
|
|
||||||
The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information.
|
The metadata-dict (available for `populate_entry_trend`, `populate_exit_trend`, `populate_indicators`) contains additional information.
|
||||||
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 section [Storing information](strategy-advanced.md#Storing-information)
|
Instead, have a look at the [Storing information](strategy-advanced.md#Storing-information) section.
|
||||||
|
|
||||||
## Additional data (informative_pairs)
|
## Strategy file loading
|
||||||
|
|
||||||
|
By default, freqtrade will attempt to load strategies from all `.py` files within `user_data/strategies`.
|
||||||
|
|
||||||
|
Assuming your strategy is called `AwesomeStrategy`, stored in the file `user_data/strategies/AwesomeStrategy.py`, then you can start freqtrade with `freqtrade trade --strategy AwesomeStrategy`.
|
||||||
|
Note that we're using the class-name, not the file name.
|
||||||
|
|
||||||
|
You can use `freqtrade list-strategies` to see a list of all strategies Freqtrade is able to load (all strategies in the correct folder).
|
||||||
|
It will also include a "status" field, highlighting potential problems.
|
||||||
|
|
||||||
|
??? Hint "Customize strategy directory"
|
||||||
|
You can use a different directory by using `--strategy-path user_data/otherPath`. This parameter is available to all commands that require a strategy.
|
||||||
|
|
||||||
|
## Informative Pairs
|
||||||
|
|
||||||
### Get data for non-tradeable pairs
|
### Get data for non-tradeable pairs
|
||||||
|
|
||||||
|
@ -329,8 +443,150 @@ A full sample can be found [in the DataProvider section](#complete-data-provider
|
||||||
It is however better to use resampling to longer timeframes whenever possible
|
It is however better to use resampling to longer timeframes whenever possible
|
||||||
to avoid hammering the exchange with too many requests and risk being blocked.
|
to avoid hammering the exchange with too many requests and risk being blocked.
|
||||||
|
|
||||||
|
??? Note "Alternative candle types"
|
||||||
|
Informative_pairs can also provide a 3rd tuple element defining the candle type explicitly.
|
||||||
|
Availability of alternative candle-types will depend on the trading-mode and the exchange. Details about this can be found in the exchange documentation.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
def informative_pairs(self):
|
||||||
|
return [
|
||||||
|
("ETH/USDT", "5m", ""), # Uses default candletype, depends on trading_mode
|
||||||
|
("ETH/USDT", "5m", "spot"), # Forces usage of spot candles
|
||||||
|
("BTC/TUSD", "15m", "futures"), # Uses futures candles
|
||||||
|
("BTC/TUSD", "15m", "mark"), # Uses mark candles
|
||||||
|
]
|
||||||
|
```
|
||||||
***
|
***
|
||||||
|
|
||||||
|
### Informative pairs decorator (`@informative()`)
|
||||||
|
|
||||||
|
In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation,
|
||||||
|
not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method.
|
||||||
|
When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter)
|
||||||
|
for more information.
|
||||||
|
|
||||||
|
??? info "Full documentation"
|
||||||
|
``` python
|
||||||
|
def informative(timeframe: str, asset: str = '',
|
||||||
|
fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
|
||||||
|
*,
|
||||||
|
candle_type: Optional[CandleType] = None,
|
||||||
|
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
||||||
|
"""
|
||||||
|
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
||||||
|
define informative indicators.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
@informative('1h')
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
||||||
|
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
||||||
|
current pair.
|
||||||
|
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
||||||
|
specified, defaults to:
|
||||||
|
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
||||||
|
* {column}_{timeframe} if asset is not specified.
|
||||||
|
Format string supports these format variables:
|
||||||
|
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
||||||
|
* {base} - base currency in lower case, for example 'eth'.
|
||||||
|
* {BASE} - same as {base}, except in upper case.
|
||||||
|
* {quote} - quote currency in lower case, for example 'usdt'.
|
||||||
|
* {QUOTE} - same as {quote}, except in upper case.
|
||||||
|
* {column} - name of dataframe column.
|
||||||
|
* {timeframe} - timeframe of informative dataframe.
|
||||||
|
:param ffill: ffill dataframe after merging informative pair.
|
||||||
|
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
??? Example "Fast and easy way to define informative pairs"
|
||||||
|
|
||||||
|
Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import IStrategy, informative
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# This method is not required.
|
||||||
|
# def informative_pairs(self): ...
|
||||||
|
|
||||||
|
# Define informative upper timeframe for each pair. Decorators can be stacked on same
|
||||||
|
# method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'.
|
||||||
|
@informative('30m')
|
||||||
|
@informative('1h')
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Define BTC/STAKE informative pair. Available in populate_indicators and other methods as
|
||||||
|
# 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable
|
||||||
|
# instead of hard-coding actual stake currency. Available in populate_indicators and other
|
||||||
|
# methods as 'btc_usdt_rsi_1h' (when stake currency is USDT).
|
||||||
|
@informative('1h', 'BTC/{stake}')
|
||||||
|
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Define BTC/ETH informative pair. You must specify quote currency if it is different from
|
||||||
|
# stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'.
|
||||||
|
@informative('1h', 'ETH/BTC')
|
||||||
|
def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# formatting. Available in populate_indicators and other methods as 'rsi_upper'.
|
||||||
|
@informative('1h', 'BTC/{stake}', '{column}')
|
||||||
|
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# Strategy timeframe indicators for current pair.
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
# Informative pairs are available in this method.
|
||||||
|
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs
|
||||||
|
manually as described [in the DataProvider section](#complete-data-provider-sample).
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
stake = self.config['stake_currency']
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe[f'btc_{stake}_rsi_1h'] < 35)
|
||||||
|
&
|
||||||
|
(dataframe['volume'] > 0)
|
||||||
|
),
|
||||||
|
['enter_long', 'enter_tag']] = (1, 'buy_signal_rsi')
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`.
|
||||||
|
|
||||||
|
!!! Warning "Duplicate method names"
|
||||||
|
Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method)
|
||||||
|
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
|
||||||
|
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
|
||||||
|
|
||||||
## Additional data (DataProvider)
|
## Additional data (DataProvider)
|
||||||
|
|
||||||
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
|
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
|
||||||
|
@ -374,9 +630,9 @@ The strategy might look something like this:
|
||||||
|
|
||||||
*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day RSI to buy and sell.*
|
*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day RSI to buy and sell.*
|
||||||
|
|
||||||
Due to the limited available data, it's very difficult to resample our `5m` candles into daily candles for use in a 14 day RSI. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least!
|
Due to the limited available data, it's very difficult to resample `5m` candles into daily candles for use in a 14 day RSI. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least!
|
||||||
|
|
||||||
Since we can't resample our data we will have to use an informative pair; and since our whitelist will be dynamic we don't know which pair(s) to use.
|
Since we can't resample the data we will have to use an informative pair; and since the whitelist will be dynamic we don't know which pair(s) to use.
|
||||||
|
|
||||||
This is where calling `self.dp.current_whitelist()` comes in handy.
|
This is where calling `self.dp.current_whitelist()` comes in handy.
|
||||||
|
|
||||||
|
@ -526,7 +782,7 @@ class SampleStrategy(IStrategy):
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
|
@ -534,7 +790,7 @@ class SampleStrategy(IStrategy):
|
||||||
(dataframe['rsi_1d'] < 30) & # Ensure daily RSI is < 30
|
(dataframe['rsi_1d'] < 30) & # Ensure daily RSI is < 30
|
||||||
(dataframe['volume'] > 0) # Ensure this candle had volume (important for backtesting)
|
(dataframe['volume'] > 0) # Ensure this candle had volume (important for backtesting)
|
||||||
),
|
),
|
||||||
'buy'] = 1
|
['enter_long', 'enter_tag']] = (1, 'rsi_cross')
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -611,7 +867,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
|
||||||
|
|
||||||
Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`).
|
Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`).
|
||||||
|
|
||||||
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
|
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit, False)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
|
||||||
|
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
|
@ -631,7 +887,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
|
||||||
|
|
||||||
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
||||||
if current_profit > 0.10:
|
if current_profit > 0.10:
|
||||||
return stoploss_from_open(0.07, current_profit)
|
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
@ -642,7 +898,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
|
||||||
!!! 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.
|
||||||
This may happen if `current_profit` parameter is below specified `open_relative_stop`. Such situations may arise when closing trade
|
This may happen if `current_profit` parameter is below specified `open_relative_stop`. Such situations may arise when closing trade
|
||||||
is blocked by `confirm_trade_exit()` method. Warnings can be solved by never blocking stop loss sells by checking `sell_reason` in
|
is blocked by `confirm_trade_exit()` method. Warnings can be solved by never blocking stop loss sells by checking `exit_reason` in
|
||||||
`confirm_trade_exit()`, or by using `return stoploss_from_open(...) or 1` idiom, which will request to not change stop loss when
|
`confirm_trade_exit()`, or by using `return stoploss_from_open(...) or 1` idiom, which will request to not change stop loss when
|
||||||
`current_profit < open_relative_stop`.
|
`current_profit < open_relative_stop`.
|
||||||
|
|
||||||
|
@ -652,13 +908,13 @@ In some situations it may be confusing to deal with stops relative to current ra
|
||||||
|
|
||||||
??? Example "Returning a stoploss using absolute price from the custom stoploss function"
|
??? Example "Returning a stoploss using absolute price from the custom stoploss function"
|
||||||
|
|
||||||
If we want to trail a stop price at 2xATR below current proce we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)`.
|
If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)`.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.strategy import IStrategy, stoploss_from_open
|
from freqtrade.strategy import IStrategy, stoploss_from_absolute
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
@ -672,135 +928,10 @@ In some situations it may be confusing to deal with stops relative to current ra
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
candle = dataframe.iloc[-1].squeeze()
|
candle = dataframe.iloc[-1].squeeze()
|
||||||
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)
|
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### *@informative()*
|
|
||||||
|
|
||||||
``` python
|
|
||||||
def informative(timeframe: str, asset: str = '',
|
|
||||||
fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
|
|
||||||
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
|
||||||
"""
|
|
||||||
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
|
||||||
define informative indicators.
|
|
||||||
|
|
||||||
Example usage:
|
|
||||||
|
|
||||||
@informative('1h')
|
|
||||||
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
|
||||||
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
|
||||||
current pair.
|
|
||||||
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
|
||||||
specified, defaults to:
|
|
||||||
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
|
||||||
* {column}_{timeframe} if asset is not specified.
|
|
||||||
Format string supports these format variables:
|
|
||||||
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
|
||||||
* {base} - base currency in lower case, for example 'eth'.
|
|
||||||
* {BASE} - same as {base}, except in upper case.
|
|
||||||
* {quote} - quote currency in lower case, for example 'usdt'.
|
|
||||||
* {QUOTE} - same as {quote}, except in upper case.
|
|
||||||
* {column} - name of dataframe column.
|
|
||||||
* {timeframe} - timeframe of informative dataframe.
|
|
||||||
:param ffill: ffill dataframe after merging informative pair.
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation,
|
|
||||||
not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method.
|
|
||||||
When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter)
|
|
||||||
for more information.
|
|
||||||
|
|
||||||
??? Example "Fast and easy way to define informative pairs"
|
|
||||||
|
|
||||||
Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
from freqtrade.strategy import IStrategy, informative
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# This method is not required.
|
|
||||||
# def informative_pairs(self): ...
|
|
||||||
|
|
||||||
# Define informative upper timeframe for each pair. Decorators can be stacked on same
|
|
||||||
# method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'.
|
|
||||||
@informative('30m')
|
|
||||||
@informative('1h')
|
|
||||||
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
# Define BTC/STAKE informative pair. Available in populate_indicators and other methods as
|
|
||||||
# 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable
|
|
||||||
# instead of hardcoding actual stake currency. Available in populate_indicators and other
|
|
||||||
# methods as 'btc_usdt_rsi_1h' (when stake currency is USDT).
|
|
||||||
@informative('1h', 'BTC/{stake}')
|
|
||||||
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
# Define BTC/ETH informative pair. You must specify quote currency if it is different from
|
|
||||||
# stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'.
|
|
||||||
@informative('1h', 'ETH/BTC')
|
|
||||||
def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
# 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
|
|
||||||
# formatting. Available in populate_indicators and other methods as 'rsi_upper'.
|
|
||||||
@informative('1h', 'BTC/{stake}', '{column}')
|
|
||||||
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
# Strategy timeframe indicators for current pair.
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
|
||||||
# Informative pairs are available in this method.
|
|
||||||
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs
|
|
||||||
manually as described [in the DataProvider section](#complete-data-provider-sample).
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
stake = self.config['stake_currency']
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
(dataframe[f'btc_{stake}_rsi_1h'] < 35)
|
|
||||||
&
|
|
||||||
(dataframe['volume'] > 0)
|
|
||||||
),
|
|
||||||
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`.
|
|
||||||
|
|
||||||
!!! Warning "Duplicate method names"
|
|
||||||
Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method)
|
|
||||||
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
|
|
||||||
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
|
|
||||||
|
|
||||||
## Additional data (Wallets)
|
## Additional data (Wallets)
|
||||||
|
|
||||||
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
|
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
|
||||||
|
@ -865,7 +996,7 @@ if self.config['runmode'].value in ('live', 'dry_run'):
|
||||||
Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5% (ratio of 0.015).
|
Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5% (ratio of 0.015).
|
||||||
|
|
||||||
``` json
|
``` json
|
||||||
{'pair': "ETH/BTC", 'profit': 0.015, 'count': 5}
|
{"pair": "ETH/BTC", "profit": 0.015, "count": 5}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
|
@ -884,7 +1015,8 @@ Sometimes it may be desired to lock a pair after certain events happen (e.g. mul
|
||||||
Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until, [reason])`.
|
Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until, [reason])`.
|
||||||
`until` must be a datetime object in the future, after which trading will be re-enabled for that pair, while `reason` is an optional string detailing why the pair was locked.
|
`until` must be a datetime object in the future, after which trading will be re-enabled for that pair, while `reason` is an optional string detailing why the pair was locked.
|
||||||
|
|
||||||
Locks can also be lifted manually, by calling `self.unlock_pair(pair)`.
|
Locks can also be lifted manually, by calling `self.unlock_pair(pair)` or `self.unlock_reason(<reason>)` - providing reason the pair was locked with.
|
||||||
|
`self.unlock_reason(<reason>)` will unlock all pairs currently locked with the provided reason.
|
||||||
|
|
||||||
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
||||||
|
|
||||||
|
@ -918,16 +1050,16 @@ if self.config['runmode'].value in ('live', 'dry_run'):
|
||||||
|
|
||||||
## Print created dataframe
|
## Print created dataframe
|
||||||
|
|
||||||
To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`.
|
To inspect the created dataframe, you can issue a print-statement in either `populate_entry_trend()` or `populate_exit_trend()`.
|
||||||
You may also want to print the pair so it's clear what data is currently shown.
|
You may also want to print the pair so it's clear what data is currently shown.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
#>> whatever condition<<<
|
#>> whatever condition<<<
|
||||||
),
|
),
|
||||||
'buy'] = 1
|
['enter_long', 'enter_tag']] = (1, 'somestring')
|
||||||
|
|
||||||
# Print the Analyzed pair
|
# Print the Analyzed pair
|
||||||
print(f"result for {metadata['pair']}")
|
print(f"result for {metadata['pair']}")
|
||||||
|
@ -954,9 +1086,18 @@ The following lists some common patterns which should be avoided to prevent frus
|
||||||
- don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling(<window>).mean()` instead
|
- don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling(<window>).mean()` instead
|
||||||
- don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead.
|
- don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead.
|
||||||
|
|
||||||
|
### Colliding signals
|
||||||
|
|
||||||
|
When conflicting signals collide (e.g. both `'enter_long'` and `'exit_long'` are 1), freqtrade will do nothing and ignore the entry signal. This will avoid trades that enter, and exit immediately. Obviously, this can potentially lead to missed entries.
|
||||||
|
|
||||||
|
The following rules apply, and entry signals will be ignored if more than one of the 3 signals is set:
|
||||||
|
|
||||||
|
- `enter_long` -> `exit_long`, `enter_short`
|
||||||
|
- `enter_short` -> `exit_short`, `enter_long`
|
||||||
|
|
||||||
## Further strategy ideas
|
## Further strategy ideas
|
||||||
|
|
||||||
To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk.
|
To get additional Ideas for strategies, head over to the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk.
|
||||||
Feel free to use any of them as inspiration for your own strategies.
|
Feel free to use any of them as inspiration for your own strategies.
|
||||||
We're happy to accept Pull Requests containing new Strategies to that repo.
|
We're happy to accept Pull Requests containing new Strategies to that repo.
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,9 @@ candles.head()
|
||||||
```python
|
```python
|
||||||
# Load strategy using values set above
|
# Load strategy using values set above
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
strategy = StrategyResolver.load_strategy(config)
|
strategy = StrategyResolver.load_strategy(config)
|
||||||
|
strategy.dp = DataProvider(config, None, None)
|
||||||
|
|
||||||
# Generate buy/sell signals using strategy
|
# Generate buy/sell signals using strategy
|
||||||
df = strategy.analyze_ticker(candles, {'pair': pair})
|
df = strategy.analyze_ticker(candles, {'pair': pair})
|
||||||
|
@ -71,7 +73,7 @@ df.tail()
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Report results
|
# Report results
|
||||||
print(f"Generated {df['buy'].sum()} buy signals")
|
print(f"Generated {df['enter_long'].sum()} entry signals")
|
||||||
data = df.set_index('date', drop=False)
|
data = df.set_index('date', drop=False)
|
||||||
data.tail()
|
data.tail()
|
||||||
```
|
```
|
||||||
|
@ -127,7 +129,7 @@ print(stats['strategy_comparison'])
|
||||||
trades = load_backtest_data(backtest_dir)
|
trades = load_backtest_data(backtest_dir)
|
||||||
|
|
||||||
# Show value-counts per pair
|
# Show value-counts per pair
|
||||||
trades.groupby("pair")["sell_reason"].value_counts()
|
trades.groupby("pair")["exit_reason"].value_counts()
|
||||||
```
|
```
|
||||||
|
|
||||||
## Plotting daily profit / equity line
|
## Plotting daily profit / equity line
|
||||||
|
@ -180,7 +182,7 @@ from freqtrade.data.btanalysis import load_trades_from_db
|
||||||
trades = load_trades_from_db("sqlite:///tradesv3.sqlite")
|
trades = load_trades_from_db("sqlite:///tradesv3.sqlite")
|
||||||
|
|
||||||
# Display results
|
# Display results
|
||||||
trades.groupby("pair")["sell_reason"].value_counts()
|
trades.groupby("pair")["exit_reason"].value_counts()
|
||||||
```
|
```
|
||||||
|
|
||||||
## Analyze the loaded trades for trade parallelism
|
## Analyze the loaded trades for trade parallelism
|
||||||
|
@ -228,7 +230,7 @@ graph = generate_candlestick_graph(pair=pair,
|
||||||
# Show graph inline
|
# Show graph inline
|
||||||
# graph.show()
|
# graph.show()
|
||||||
|
|
||||||
# Render graph in a separate window
|
# Render graph in a seperate window
|
||||||
graph.show(renderer="browser")
|
graph.show(renderer="browser")
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -242,7 +244,7 @@ import plotly.figure_factory as ff
|
||||||
hist_data = [trades.profit_ratio]
|
hist_data = [trades.profit_ratio]
|
||||||
group_labels = ['profit_ratio'] # name of the dataset
|
group_labels = ['profit_ratio'] # name of the dataset
|
||||||
|
|
||||||
fig = ff.create_distplot(hist_data, group_labels,bin_size=0.01)
|
fig = ff.create_distplot(hist_data, group_labels, bin_size=0.01)
|
||||||
fig.show()
|
fig.show()
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
471
docs/strategy_migration.md
Normal file
471
docs/strategy_migration.md
Normal file
|
@ -0,0 +1,471 @@
|
||||||
|
# Strategy Migration between V2 and V3
|
||||||
|
|
||||||
|
To support new markets and trade-types (namely short trades / trades with leverage), some things had to change in the interface.
|
||||||
|
If you intend on using markets other than spot markets, please migrate your strategy to the new format.
|
||||||
|
|
||||||
|
We have put a great effort into keeping compatibility with existing strategies, so if you just want to continue using freqtrade in __spot markets__, there should be no changes necessary for now.
|
||||||
|
|
||||||
|
You can use the quick summary as checklist. Please refer to the detailed sections below for full migration details.
|
||||||
|
|
||||||
|
## Quick summary / migration checklist
|
||||||
|
|
||||||
|
Note : `forcesell`, `forcebuy`, `emergencysell` are changed to `force_exit`, `force_enter`, `emergency_exit` respectively.
|
||||||
|
|
||||||
|
* Strategy methods:
|
||||||
|
* [`populate_buy_trend()` -> `populate_entry_trend()`](#populate_buy_trend)
|
||||||
|
* [`populate_sell_trend()` -> `populate_exit_trend()`](#populate_sell_trend)
|
||||||
|
* [`custom_sell()` -> `custom_exit()`](#custom_sell)
|
||||||
|
* [`check_buy_timeout()` -> `check_entry_timeout()`](#custom_entry_timeout)
|
||||||
|
* [`check_sell_timeout()` -> `check_exit_timeout()`](#custom_entry_timeout)
|
||||||
|
* New `side` argument to callbacks without trade object
|
||||||
|
* [`custom_stake_amount`](#custom-stake-amount)
|
||||||
|
* [`confirm_trade_entry`](#confirm_trade_entry)
|
||||||
|
* [`custom_entry_price`](#custom_entry_price)
|
||||||
|
* [Changed argument name in `confirm_trade_exit`](#confirm_trade_exit)
|
||||||
|
* Dataframe columns:
|
||||||
|
* [`buy` -> `enter_long`](#populate_buy_trend)
|
||||||
|
* [`sell` -> `exit_long`](#populate_sell_trend)
|
||||||
|
* [`buy_tag` -> `enter_tag` (used for both long and short trades)](#populate_buy_trend)
|
||||||
|
* [New column `enter_short` and corresponding new column `exit_short`](#populate_sell_trend)
|
||||||
|
* trade-object now has the following new properties:
|
||||||
|
* `is_short`
|
||||||
|
* `entry_side`
|
||||||
|
* `exit_side`
|
||||||
|
* `trade_direction`
|
||||||
|
* renamed: `sell_reason` -> `exit_reason`
|
||||||
|
* [Renamed `trade.nr_of_successful_buys` to `trade.nr_of_successful_entries` (mostly relevant for `adjust_trade_position()`)](#adjust-trade-position-changes)
|
||||||
|
* Introduced new [`leverage` callback](strategy-callbacks.md#leverage-callback).
|
||||||
|
* Informative pairs can now pass a 3rd element in the Tuple, defining the candle type.
|
||||||
|
* `@informative` decorator now takes an optional `candle_type` argument.
|
||||||
|
* [helper methods](#helper-methods) `stoploss_from_open` and `stoploss_from_absolute` now take `is_short` as additional argument.
|
||||||
|
* `INTERFACE_VERSION` should be set to 3.
|
||||||
|
* [Strategy/Configuration settings](#strategyconfiguration-settings).
|
||||||
|
* `order_time_in_force` buy -> entry, sell -> exit.
|
||||||
|
* `order_types` buy -> entry, sell -> exit.
|
||||||
|
* `unfilledtimeout` buy -> entry, sell -> exit.
|
||||||
|
* Terminology changes
|
||||||
|
* Sell reasons changed to reflect the new naming of "exit" instead of sells. Be careful in your strategy if you're using `exit_reason` checks and eventually update your strategy.
|
||||||
|
* `sell_signal` -> `exit_signal`
|
||||||
|
* `custom_sell` -> `custom_exit`
|
||||||
|
* `force_sell` -> `force_exit`
|
||||||
|
* `emergency_sell` -> `emergency_exit`
|
||||||
|
* Webhook terminology changed from "sell" to "exit", and from "buy" to entry
|
||||||
|
* `webhookbuy` -> `webhookentry`
|
||||||
|
* `webhookbuyfill` -> `webhookentryfill`
|
||||||
|
* `webhookbuycancel` -> `webhookentrycancel`
|
||||||
|
* `webhooksell` -> `webhookexit`
|
||||||
|
* `webhooksellfill` -> `webhookexitfill`
|
||||||
|
* `webhooksellcancel` -> `webhookexitcancel`
|
||||||
|
* Telegram notification settings
|
||||||
|
* `buy` -> `entry`
|
||||||
|
* `buy_fill` -> `entry_fill`
|
||||||
|
* `buy_cancel` -> `entry_cancel`
|
||||||
|
* `sell` -> `exit`
|
||||||
|
* `sell_fill` -> `exit_fill`
|
||||||
|
* `sell_cancel` -> `exit_cancel`
|
||||||
|
* Strategy/config settings:
|
||||||
|
* `use_sell_signal` -> `use_exit_signal`
|
||||||
|
* `sell_profit_only` -> `exit_profit_only`
|
||||||
|
* `sell_profit_offset` -> `exit_profit_offset`
|
||||||
|
* `ignore_roi_if_buy_signal` -> `ignore_roi_if_entry_signal`
|
||||||
|
* `forcebuy_enable` -> `force_entry_enable`
|
||||||
|
|
||||||
|
## Extensive explanation
|
||||||
|
|
||||||
|
### `populate_buy_trend`
|
||||||
|
|
||||||
|
In `populate_buy_trend()` - you will want to change the columns you assign from `'buy`' to `'enter_long'`, as well as the method name from `populate_buy_trend` to `populate_entry_trend`.
|
||||||
|
|
||||||
|
```python hl_lines="1 9"
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30
|
||||||
|
(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard
|
||||||
|
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard
|
||||||
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
|
),
|
||||||
|
['buy', 'buy_tag']] = (1, 'rsi_cross')
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```python hl_lines="1 9"
|
||||||
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30
|
||||||
|
(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard
|
||||||
|
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard
|
||||||
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
|
),
|
||||||
|
['enter_long', 'enter_tag']] = (1, 'rsi_cross')
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
Please refer to the [Strategy documentation](strategy-customization.md#entry-signal-rules) on how to enter and exit short trades.
|
||||||
|
|
||||||
|
### `populate_sell_trend`
|
||||||
|
|
||||||
|
Similar to `populate_buy_trend`, `populate_sell_trend()` will be renamed to `populate_exit_trend()`.
|
||||||
|
We'll also change the column from `'sell'` to `'exit_long'`.
|
||||||
|
|
||||||
|
``` python hl_lines="1 9"
|
||||||
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70
|
||||||
|
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard
|
||||||
|
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard
|
||||||
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
|
),
|
||||||
|
['sell', 'exit_tag']] = (1, 'some_exit_tag')
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
After
|
||||||
|
|
||||||
|
``` python hl_lines="1 9"
|
||||||
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70
|
||||||
|
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard
|
||||||
|
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard
|
||||||
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
|
),
|
||||||
|
['exit_long', 'exit_tag']] = (1, 'some_exit_tag')
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
Please refer to the [Strategy documentation](strategy-customization.md#exit-signal-rules) on how to enter and exit short trades.
|
||||||
|
|
||||||
|
### `custom_sell`
|
||||||
|
|
||||||
|
`custom_sell` has been renamed to `custom_exit`.
|
||||||
|
It's now also being called for every iteration, independent of current profit and `exit_profit_only` settings.
|
||||||
|
|
||||||
|
``` python hl_lines="2"
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
||||||
|
current_profit: float, **kwargs):
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
|
last_candle = dataframe.iloc[-1].squeeze()
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
``` python hl_lines="2"
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
||||||
|
current_profit: float, **kwargs):
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
|
last_candle = dataframe.iloc[-1].squeeze()
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### `custom_entry_timeout`
|
||||||
|
|
||||||
|
`check_buy_timeout()` has been renamed to `check_entry_timeout()`, and `check_sell_timeout()` has been renamed to `check_exit_timeout()`.
|
||||||
|
|
||||||
|
``` python hl_lines="2 6"
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict,
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict,
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
``` python hl_lines="2 6"
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom-stake-amount
|
||||||
|
|
||||||
|
New string argument `side` - which can be either `"long"` or `"short"`.
|
||||||
|
|
||||||
|
``` python hl_lines="4"
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
|
proposed_stake: float, min_stake: float, max_stake: float,
|
||||||
|
entry_tag: Optional[str], **kwargs) -> float:
|
||||||
|
# ...
|
||||||
|
return proposed_stake
|
||||||
|
```
|
||||||
|
|
||||||
|
``` python hl_lines="4"
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
|
proposed_stake: float, min_stake: float, max_stake: float,
|
||||||
|
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||||
|
# ...
|
||||||
|
return proposed_stake
|
||||||
|
```
|
||||||
|
|
||||||
|
### `confirm_trade_entry`
|
||||||
|
|
||||||
|
New string argument `side` - which can be either `"long"` or `"short"`.
|
||||||
|
|
||||||
|
``` python hl_lines="4"
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||||
|
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
|
||||||
|
**kwargs) -> bool:
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
``` python hl_lines="4"
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||||
|
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
|
||||||
|
side: str, **kwargs) -> bool:
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
### `confirm_trade_exit`
|
||||||
|
|
||||||
|
Changed argument `sell_reason` to `exit_reason`.
|
||||||
|
For compatibility, `sell_reason` will still be provided for a limited time.
|
||||||
|
|
||||||
|
``` python hl_lines="3"
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
|
||||||
|
rate: float, time_in_force: str, sell_reason: str,
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
``` python hl_lines="3"
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
|
||||||
|
rate: float, time_in_force: str, exit_reason: str,
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
### `custom_entry_price`
|
||||||
|
|
||||||
|
New string argument `side` - which can be either `"long"` or `"short"`.
|
||||||
|
|
||||||
|
``` python hl_lines="3"
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
|
||||||
|
entry_tag: Optional[str], **kwargs) -> float:
|
||||||
|
return proposed_rate
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
``` python hl_lines="3"
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
|
||||||
|
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||||
|
return proposed_rate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adjust trade position changes
|
||||||
|
|
||||||
|
While adjust-trade-position itself did not change, you should no longer use `trade.nr_of_successful_buys` - and instead use `trade.nr_of_successful_entries`, which will also include short entries.
|
||||||
|
|
||||||
|
### Helper methods
|
||||||
|
|
||||||
|
Added argument "is_short" to `stoploss_from_open` and `stoploss_from_absolute`.
|
||||||
|
This should be given the value of `trade.is_short`.
|
||||||
|
|
||||||
|
``` python hl_lines="5 7"
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
||||||
|
if current_profit > 0.10:
|
||||||
|
return stoploss_from_open(0.07, current_profit)
|
||||||
|
|
||||||
|
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
``` python hl_lines="5 7"
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
||||||
|
if current_profit > 0.10:
|
||||||
|
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
|
||||||
|
|
||||||
|
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Strategy/Configuration settings
|
||||||
|
|
||||||
|
#### `order_time_in_force`
|
||||||
|
|
||||||
|
`order_time_in_force` attributes changed from `"buy"` to `"entry"` and `"sell"` to `"exit"`.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
order_time_in_force: Dict = {
|
||||||
|
"buy": "gtc",
|
||||||
|
"sell": "gtc",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
``` python hl_lines="2 3"
|
||||||
|
order_time_in_force: Dict = {
|
||||||
|
"entry": "gtc",
|
||||||
|
"exit": "gtc",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `order_types`
|
||||||
|
|
||||||
|
`order_types` have changed all wordings from `buy` to `entry` - and `sell` to `exit`.
|
||||||
|
And two words are joined with `_`.
|
||||||
|
|
||||||
|
``` python hl_lines="2-6"
|
||||||
|
order_types = {
|
||||||
|
"buy": "limit",
|
||||||
|
"sell": "limit",
|
||||||
|
"emergencysell": "market",
|
||||||
|
"forcesell": "market",
|
||||||
|
"forcebuy": "market",
|
||||||
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": false,
|
||||||
|
"stoploss_on_exchange_interval": 60
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
``` python hl_lines="2-6"
|
||||||
|
order_types = {
|
||||||
|
"entry": "limit",
|
||||||
|
"exit": "limit",
|
||||||
|
"emergency_exit": "market",
|
||||||
|
"force_exit": "market",
|
||||||
|
"force_entry": "market",
|
||||||
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": false,
|
||||||
|
"stoploss_on_exchange_interval": 60
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Strategy level settings
|
||||||
|
|
||||||
|
* `use_sell_signal` -> `use_exit_signal`
|
||||||
|
* `sell_profit_only` -> `exit_profit_only`
|
||||||
|
* `sell_profit_offset` -> `exit_profit_offset`
|
||||||
|
* `ignore_roi_if_buy_signal` -> `ignore_roi_if_entry_signal`
|
||||||
|
|
||||||
|
``` python hl_lines="2-5"
|
||||||
|
# These values can be overridden in the config.
|
||||||
|
use_sell_signal = True
|
||||||
|
sell_profit_only = True
|
||||||
|
sell_profit_offset: 0.01
|
||||||
|
ignore_roi_if_buy_signal = False
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
``` python hl_lines="2-5"
|
||||||
|
# These values can be overridden in the config.
|
||||||
|
use_exit_signal = True
|
||||||
|
exit_profit_only = True
|
||||||
|
exit_profit_offset: 0.01
|
||||||
|
ignore_roi_if_entry_signal = False
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `unfilledtimeout`
|
||||||
|
|
||||||
|
`unfilledtimeout` have changed all wordings from `buy` to `entry` - and `sell` to `exit`.
|
||||||
|
|
||||||
|
``` python hl_lines="2-3"
|
||||||
|
unfilledtimeout = {
|
||||||
|
"buy": 10,
|
||||||
|
"sell": 10,
|
||||||
|
"exit_timeout_count": 0,
|
||||||
|
"unit": "minutes"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
``` python hl_lines="2-3"
|
||||||
|
unfilledtimeout = {
|
||||||
|
"entry": 10,
|
||||||
|
"exit": 10,
|
||||||
|
"exit_timeout_count": 0,
|
||||||
|
"unit": "minutes"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `order pricing`
|
||||||
|
|
||||||
|
Order pricing changed in 2 ways. `bid_strategy` was renamed to `entry_pricing` and `ask_strategy` was renamed to `exit_pricing`.
|
||||||
|
The attributes `ask_last_balance` -> `price_last_balance` and `bid_last_balance` -> `price_last_balance` were renamed as well.
|
||||||
|
Also, price-side can now be defined as `ask`, `bid`, `same` or `other`.
|
||||||
|
Please refer to the [pricing documentation](configuration.md#prices-used-for-orders) for more information.
|
||||||
|
|
||||||
|
``` json hl_lines="2-3 6 12-13 16"
|
||||||
|
{
|
||||||
|
"bid_strategy": {
|
||||||
|
"price_side": "bid",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"ask_last_balance": 0.0,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": false,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ask_strategy":{
|
||||||
|
"price_side": "ask",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"bid_last_balance": 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
after:
|
||||||
|
|
||||||
|
``` json hl_lines="2-3 6 12-13 16"
|
||||||
|
{
|
||||||
|
"entry_pricing": {
|
||||||
|
"price_side": "same",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": false,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exit_pricing":{
|
||||||
|
"price_side": "same",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
|
@ -58,6 +58,8 @@ For the Freqtrade configuration, you can then use the the full value (including
|
||||||
```json
|
```json
|
||||||
"chat_id": "-1001332619709"
|
"chat_id": "-1001332619709"
|
||||||
```
|
```
|
||||||
|
!!! 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.
|
||||||
|
|
||||||
## Control telegram noise
|
## Control telegram noise
|
||||||
|
|
||||||
|
@ -79,21 +81,21 @@ Example configuration showing the different settings:
|
||||||
"status": "silent",
|
"status": "silent",
|
||||||
"warning": "on",
|
"warning": "on",
|
||||||
"startup": "off",
|
"startup": "off",
|
||||||
"buy": "silent",
|
"entry": "silent",
|
||||||
"sell": {
|
"exit": {
|
||||||
"roi": "silent",
|
"roi": "silent",
|
||||||
"emergency_sell": "on",
|
"emergency_exit": "on",
|
||||||
"force_sell": "on",
|
"force_exit": "on",
|
||||||
"sell_signal": "silent",
|
"exit_signal": "silent",
|
||||||
"trailing_stop_loss": "on",
|
"trailing_stop_loss": "on",
|
||||||
"stop_loss": "on",
|
"stop_loss": "on",
|
||||||
"stoploss_on_exchange": "on",
|
"stoploss_on_exchange": "on",
|
||||||
"custom_sell": "silent"
|
"custom_exit": "silent"
|
||||||
},
|
},
|
||||||
"buy_cancel": "silent",
|
"entry_cancel": "silent",
|
||||||
"sell_cancel": "on",
|
"exit_cancel": "on",
|
||||||
"buy_fill": "off",
|
"entry_fill": "off",
|
||||||
"sell_fill": "off",
|
"exit_fill": "off",
|
||||||
"protection_trigger": "off",
|
"protection_trigger": "off",
|
||||||
"protection_trigger_global": "on"
|
"protection_trigger_global": "on"
|
||||||
},
|
},
|
||||||
|
@ -102,8 +104,8 @@ Example configuration showing the different settings:
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
`buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange.
|
`entry` notifications are sent when the order is placed, while `entry_fill` notifications are sent when the order is filled on the exchange.
|
||||||
`sell` notifications are sent when the order is placed, while `sell_fill` notifications are sent when the order is filled on the exchange.
|
`exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange.
|
||||||
`*_fill` notifications are off by default and must be explicitly enabled.
|
`*_fill` notifications are off by default and must be explicitly enabled.
|
||||||
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
|
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
|
||||||
|
|
||||||
|
@ -169,13 +171,19 @@ 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).
|
||||||
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
||||||
| `/forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
|
| `/forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`).
|
||||||
| `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
|
| `/forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).
|
||||||
| `/forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
|
| `/fx` | alias for `/forceexit`
|
||||||
|
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True)
|
||||||
|
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True)
|
||||||
| `/performance` | Show performance of each finished trade grouped by pair
|
| `/performance` | Show performance of each finished trade grouped by pair
|
||||||
| `/balance` | Show account balance per currency
|
| `/balance` | Show account balance per currency
|
||||||
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
|
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
|
||||||
| `/stats` | Shows Wins / losses by Sell reason as well as Avg. holding durations for buys and sells
|
| `/weekly <n>` | Shows profit or loss per week, over the last n weeks (n defaults to 8)
|
||||||
|
| `/monthly <n>` | Shows profit or loss per month, over the last n months (n defaults to 6)
|
||||||
|
| `/stats` | Shows Wins / losses by Exit reason as well as Avg. holding durations for buys and sells
|
||||||
|
| `/exits` | Shows Wins / losses by Exit reason as well as Avg. holding durations for buys and sells
|
||||||
|
| `/entries` | Shows Wins / losses by Exit reason as well as Avg. holding durations for buys and sells
|
||||||
| `/whitelist` | Show the current whitelist
|
| `/whitelist` | Show the current whitelist
|
||||||
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||||
| `/edge` | Show validated pairs by Edge if it is enabled.
|
| `/edge` | Show validated pairs by Edge if it is enabled.
|
||||||
|
@ -212,11 +220,14 @@ Once all positions are sold, run `/stop` to completely stop the bot.
|
||||||
### /status
|
### /status
|
||||||
|
|
||||||
For each open trade, the bot will send you the following message.
|
For each open trade, the bot will send you the following message.
|
||||||
|
Enter Tag is configurable via Strategy.
|
||||||
|
|
||||||
> **Trade ID:** `123` `(since 1 days ago)`
|
> **Trade ID:** `123` `(since 1 days ago)`
|
||||||
> **Current Pair:** CVC/BTC
|
> **Current Pair:** CVC/BTC
|
||||||
> **Open Since:** `1 days ago`
|
> **Direction:** Long
|
||||||
|
> **Leverage:** 1.0
|
||||||
> **Amount:** `26.64180098`
|
> **Amount:** `26.64180098`
|
||||||
|
> **Enter Tag:** Awesome Long Signal
|
||||||
> **Open Rate:** `0.00007489`
|
> **Open Rate:** `0.00007489`
|
||||||
> **Current Rate:** `0.00007489`
|
> **Current Rate:** `0.00007489`
|
||||||
> **Current Profit:** `12.95%`
|
> **Current Profit:** `12.95%`
|
||||||
|
@ -227,10 +238,10 @@ For each open trade, the bot will send you the following message.
|
||||||
Return the status of all open trades in a table format.
|
Return the status of all open trades in a table format.
|
||||||
|
|
||||||
```
|
```
|
||||||
ID Pair Since Profit
|
ID L/S Pair Since Profit
|
||||||
---- -------- ------- --------
|
---- -------- ------- --------
|
||||||
67 SC/BTC 1 d 13.33%
|
67 L SC/BTC 1 d 13.33%
|
||||||
123 CVC/BTC 1 h 12.95%
|
123 S CVC/BTC 1 h 12.95%
|
||||||
```
|
```
|
||||||
|
|
||||||
### /count
|
### /count
|
||||||
|
@ -264,21 +275,27 @@ The relative profit of `1.2%` is the average profit per trade.
|
||||||
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
|
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
|
||||||
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
||||||
|
|
||||||
### /forcesell <trade_id>
|
### /forceexit <trade_id>
|
||||||
|
|
||||||
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
|
> **BINANCE:** Exiting BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
|
||||||
|
|
||||||
### /forcebuy <pair> [rate]
|
!!! Tip
|
||||||
|
You can get a list of all open trades by calling `/forceexit` without parameter, which will show a list of buttons to simply exit a trade.
|
||||||
|
|
||||||
> **BITTREX:** Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
|
### /forcelong <pair> [rate] | /forceshort <pair> [rate]
|
||||||
|
|
||||||
Omitting the pair will open a query asking for the pair to buy (based on the current whitelist).
|
`/forcebuy <pair> [rate]` is also supported for longs but should be considered deprecated.
|
||||||
|
|
||||||
|
> **BINANCE:** Long ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
|
||||||
|
|
||||||
|
Omitting the pair will open a query asking for the pair to trade (based on the current whitelist).
|
||||||
|
Trades created through `/forcelong` will have the buy-tag of `force_entry`.
|
||||||
|
|
||||||
![Telegram force-buy screenshot](assets/telegram_forcebuy.png)
|
![Telegram force-buy screenshot](assets/telegram_forcebuy.png)
|
||||||
|
|
||||||
Note that for this to work, `forcebuy_enable` needs to be set to true.
|
Note that for this to work, `force_entry_enable` needs to be set to true.
|
||||||
|
|
||||||
[More details](configuration.md#understand-forcebuy_enable)
|
[More details](configuration.md#understand-force_entry_enable)
|
||||||
|
|
||||||
### /performance
|
### /performance
|
||||||
|
|
||||||
|
@ -307,8 +324,7 @@ Return the balance of all crypto-currency your have on the exchange.
|
||||||
|
|
||||||
### /daily <n>
|
### /daily <n>
|
||||||
|
|
||||||
Per default `/daily` will return the 7 last days.
|
Per default `/daily` will return the 7 last days. The example below if for `/daily 3`:
|
||||||
The example below if for `/daily 3`:
|
|
||||||
|
|
||||||
> **Daily Profit over the last 3 days:**
|
> **Daily Profit over the last 3 days:**
|
||||||
```
|
```
|
||||||
|
@ -319,6 +335,34 @@ Day Profit BTC Profit USD
|
||||||
2018-01-01 0.00269130 BTC 34.986 USD
|
2018-01-01 0.00269130 BTC 34.986 USD
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### /weekly <n>
|
||||||
|
|
||||||
|
Per default `/weekly` will return the 8 last weeks, including the current week. Each week starts
|
||||||
|
from Monday. The example below if for `/weekly 3`:
|
||||||
|
|
||||||
|
> **Weekly Profit over the last 3 weeks (starting from Monday):**
|
||||||
|
```
|
||||||
|
Monday Profit BTC Profit USD
|
||||||
|
---------- -------------- ------------
|
||||||
|
2018-01-03 0.00224175 BTC 29,142 USD
|
||||||
|
2017-12-27 0.00033131 BTC 4,307 USD
|
||||||
|
2017-12-20 0.00269130 BTC 34.986 USD
|
||||||
|
```
|
||||||
|
|
||||||
|
### /monthly <n>
|
||||||
|
|
||||||
|
Per default `/monthly` will return the 6 last months, including the current month. The example below
|
||||||
|
if for `/monthly 3`:
|
||||||
|
|
||||||
|
> **Monthly Profit over the last 3 months:**
|
||||||
|
```
|
||||||
|
Month Profit BTC Profit USD
|
||||||
|
---------- -------------- ------------
|
||||||
|
2018-01 0.00224175 BTC 29,142 USD
|
||||||
|
2017-12 0.00033131 BTC 4,307 USD
|
||||||
|
2017-11 0.00269130 BTC 34.986 USD
|
||||||
|
```
|
||||||
|
|
||||||
### /whitelist
|
### /whitelist
|
||||||
|
|
||||||
Shows the current whitelist
|
Shows the current whitelist
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
To update your freqtrade installation, please use one of the below methods, corresponding to your installation method.
|
To update your freqtrade installation, please use one of the below methods, corresponding to your installation method.
|
||||||
|
|
||||||
|
!!! Note "Tracking changes"
|
||||||
|
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.
|
||||||
|
|
||||||
## docker-compose
|
## docker-compose
|
||||||
|
|
||||||
!!! Note "Legacy installations using the `master` image"
|
!!! Note "Legacy installations using the `master` image"
|
||||||
|
|
|
@ -59,7 +59,7 @@ $ freqtrade new-config --config 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
|
||||||
? Please insert your stake amount: 0.05
|
? Please insert your stake amount: 0.05
|
||||||
? Please insert max_open_trades (Integer or 'unlimited'): 3
|
? Please insert max_open_trades (Integer or -1 for unlimited open trades): 3
|
||||||
? Please insert your desired timeframe (e.g. 5m): 5m
|
? Please insert your desired timeframe (e.g. 5m): 5m
|
||||||
? Please insert your display Currency (for reporting): USD
|
? Please insert your display Currency (for reporting): USD
|
||||||
? Select exchange binance
|
? Select exchange binance
|
||||||
|
@ -281,7 +281,7 @@ bitmax True missing opt: fetchMyTrades
|
||||||
bitmex False Various reasons.
|
bitmex False Various reasons.
|
||||||
bitpanda True
|
bitpanda True
|
||||||
bitso False missing: fetchOHLCV
|
bitso False missing: fetchOHLCV
|
||||||
bitstamp False Does not provide history. Details in https://github.com/freqtrade/freqtrade/issues/1983
|
bitstamp True missing opt: fetchTickers
|
||||||
bitstamp1 False missing: fetchOrder, fetchOHLCV
|
bitstamp1 False missing: fetchOrder, fetchOHLCV
|
||||||
bittrex True
|
bittrex True
|
||||||
bitvavo True
|
bitvavo True
|
||||||
|
@ -439,14 +439,15 @@ usage: freqtrade list-markets [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
[-d PATH] [--userdir PATH] [--exchange EXCHANGE]
|
[-d PATH] [--userdir PATH] [--exchange EXCHANGE]
|
||||||
[--print-list] [--print-json] [-1] [--print-csv]
|
[--print-list] [--print-json] [-1] [--print-csv]
|
||||||
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
|
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
|
||||||
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
|
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a]
|
||||||
[-a]
|
[--trading-mode {spot,margin,futures}]
|
||||||
|
|
||||||
usage: freqtrade list-pairs [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
usage: freqtrade list-pairs [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
[-d PATH] [--userdir PATH] [--exchange EXCHANGE]
|
[-d PATH] [--userdir PATH] [--exchange EXCHANGE]
|
||||||
[--print-list] [--print-json] [-1] [--print-csv]
|
[--print-list] [--print-json] [-1] [--print-csv]
|
||||||
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
|
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
|
||||||
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a]
|
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a]
|
||||||
|
[--trading-mode {spot,margin,futures}]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
@ -463,6 +464,8 @@ optional arguments:
|
||||||
Specify quote currency(-ies). Space-separated list.
|
Specify quote currency(-ies). Space-separated list.
|
||||||
-a, --all Print all pairs or market symbols. By default only
|
-a, --all Print all pairs or market symbols. By default only
|
||||||
active ones are shown.
|
active ones are shown.
|
||||||
|
--trading-mode {spot,margin,futures}
|
||||||
|
Select Trading mode
|
||||||
|
|
||||||
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).
|
||||||
|
@ -517,20 +520,25 @@ Requires a configuration with specified `pairlists` attribute.
|
||||||
Can be used to generate static pairlists to be used during backtesting / hyperopt.
|
Can be used to generate static pairlists to be used during backtesting / hyperopt.
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade test-pairlist [-h] [-c PATH]
|
usage: freqtrade test-pairlist [-h] [-v] [-c PATH]
|
||||||
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
|
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
|
||||||
[-1] [--print-json]
|
[-1] [--print-json] [--exchange EXCHANGE]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
-c PATH, --config PATH
|
-c PATH, --config PATH
|
||||||
Specify configuration file (default: `config.json`).
|
Specify configuration file (default:
|
||||||
Multiple --config options may be used. Can be set to
|
`userdir/config.json` or `config.json` whichever
|
||||||
`-` to read config from stdin.
|
exists). Multiple --config options may be used. Can be
|
||||||
|
set to `-` to read config from stdin.
|
||||||
--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]
|
--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]
|
||||||
Specify quote currency(-ies). Space-separated list.
|
Specify quote currency(-ies). Space-separated list.
|
||||||
-1, --one-column Print output in one column.
|
-1, --one-column Print output in one column.
|
||||||
--print-json Print list of pairs or market symbols in JSON format.
|
--print-json Print list of pairs or market symbols in JSON format.
|
||||||
|
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
||||||
|
config is provided.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
@ -577,6 +585,46 @@ Common arguments:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Show previous Backtest results
|
||||||
|
|
||||||
|
Allows you to show previous backtest results.
|
||||||
|
Adding `--show-pair-list` outputs a sorted pair list you can easily copy/paste into your configuration (omitting bad pairs).
|
||||||
|
|
||||||
|
??? Warning "Strategy overfitting"
|
||||||
|
Only using winning pairs can lead to an overfitted strategy, which will not work well on future data. Make sure to extensively test your strategy in dry-run before risking real money.
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: freqtrade backtesting-show [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
|
[-d PATH] [--userdir PATH]
|
||||||
|
[--export-filename PATH] [--show-pair-list]
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--export-filename PATH
|
||||||
|
Save backtest results to the file with this filename.
|
||||||
|
Requires `--export` to be set as well. Example:
|
||||||
|
`--export-filename=user_data/backtest_results/backtest
|
||||||
|
_today.json`
|
||||||
|
--show-pair-list Show backtesting pairlist sorted by profit.
|
||||||
|
|
||||||
|
Common arguments:
|
||||||
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
--logfile FILE Log to the file specified. Special values are:
|
||||||
|
'syslog', 'journald'. See the documentation for more
|
||||||
|
details.
|
||||||
|
-V, --version show program's version number and exit
|
||||||
|
-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.
|
||||||
|
-d PATH, --datadir PATH
|
||||||
|
Path to directory with historical backtesting data.
|
||||||
|
--userdir PATH, --user-data-dir PATH
|
||||||
|
Path to userdata directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## List Hyperopt results
|
## List Hyperopt results
|
||||||
|
|
||||||
You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command.
|
You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command.
|
||||||
|
@ -667,6 +715,7 @@ usage: freqtrade hyperopt-show [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
[--profitable] [-n INT] [--print-json]
|
[--profitable] [-n INT] [--print-json]
|
||||||
[--hyperopt-filename FILENAME] [--no-header]
|
[--hyperopt-filename FILENAME] [--no-header]
|
||||||
[--disable-param-export]
|
[--disable-param-export]
|
||||||
|
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
@ -680,6 +729,8 @@ optional arguments:
|
||||||
--no-header Do not print epoch details header.
|
--no-header Do not print epoch details header.
|
||||||
--disable-param-export
|
--disable-param-export
|
||||||
Disable automatic hyperopt parameter export.
|
Disable automatic hyperopt parameter export.
|
||||||
|
--breakdown {day,week,month} [{day,week,month} ...]
|
||||||
|
Show backtesting breakdown per [day, week, month].
|
||||||
|
|
||||||
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).
|
||||||
|
|
|
@ -10,33 +10,33 @@ Sample configuration (tested using IFTTT).
|
||||||
"webhook": {
|
"webhook": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "https://maker.ifttt.com/trigger/<YOUREVENT>/with/key/<YOURKEY>/",
|
"url": "https://maker.ifttt.com/trigger/<YOUREVENT>/with/key/<YOURKEY>/",
|
||||||
"webhookbuy": {
|
"webhookentry": {
|
||||||
"value1": "Buying {pair}",
|
"value1": "Buying {pair}",
|
||||||
"value2": "limit {limit:8f}",
|
"value2": "limit {limit:8f}",
|
||||||
"value3": "{stake_amount:8f} {stake_currency}"
|
"value3": "{stake_amount:8f} {stake_currency}"
|
||||||
},
|
},
|
||||||
"webhookbuycancel": {
|
"webhookentrycancel": {
|
||||||
"value1": "Cancelling Open Buy Order for {pair}",
|
"value1": "Cancelling Open Buy Order for {pair}",
|
||||||
"value2": "limit {limit:8f}",
|
"value2": "limit {limit:8f}",
|
||||||
"value3": "{stake_amount:8f} {stake_currency}"
|
"value3": "{stake_amount:8f} {stake_currency}"
|
||||||
},
|
},
|
||||||
"webhookbuyfill": {
|
"webhookentryfill": {
|
||||||
"value1": "Buy Order for {pair} filled",
|
"value1": "Buy Order for {pair} filled",
|
||||||
"value2": "at {open_rate:8f}",
|
"value2": "at {open_rate:8f}",
|
||||||
"value3": ""
|
"value3": ""
|
||||||
},
|
},
|
||||||
"webhooksell": {
|
"webhookexit": {
|
||||||
"value1": "Selling {pair}",
|
"value1": "Exiting {pair}",
|
||||||
"value2": "limit {limit:8f}",
|
"value2": "limit {limit:8f}",
|
||||||
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
|
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
|
||||||
},
|
},
|
||||||
"webhooksellcancel": {
|
"webhookexitcancel": {
|
||||||
"value1": "Cancelling Open Sell Order for {pair}",
|
"value1": "Cancelling Open Exit Order for {pair}",
|
||||||
"value2": "limit {limit:8f}",
|
"value2": "limit {limit:8f}",
|
||||||
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
|
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
|
||||||
},
|
},
|
||||||
"webhooksellfill": {
|
"webhookexitfill": {
|
||||||
"value1": "Sell Order for {pair} filled",
|
"value1": "Exit Order for {pair} filled",
|
||||||
"value2": "at {close_rate:8f}.",
|
"value2": "at {close_rate:8f}.",
|
||||||
"value3": ""
|
"value3": ""
|
||||||
},
|
},
|
||||||
|
@ -48,9 +48,9 @@ Sample configuration (tested using IFTTT).
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert our event and key to the url.
|
The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert your event and key to the url.
|
||||||
|
|
||||||
You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use `"format": "form"` or `"format": "json"` respectively. Example configuration for Mattermost Cloud integration:
|
You can set the POST body format to Form-Encoded (default), JSON-Encoded, or raw data. Use `"format": "form"`, `"format": "json"`, or `"format": "raw"` respectively. Example configuration for Mattermost Cloud integration:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"webhook": {
|
"webhook": {
|
||||||
|
@ -63,70 +63,113 @@ You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
The result would be 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:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"webhook": {
|
||||||
|
"enabled": true,
|
||||||
|
"url": "https://<YOURHOOKURL>",
|
||||||
|
"format": "raw",
|
||||||
|
"webhookstatus": {
|
||||||
|
"data": "Status: {status}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
The result would be a POST request with e.g. `Status: running` body and `Content-Type: text/plain` header.
|
||||||
|
|
||||||
|
Optional parameters are available to enable automatic retries for webhook messages. The `webhook.retries` parameter can be set for the maximum number of retries the webhook request should attempt if it is unsuccessful (i.e. HTTP response status is not 200). By default this is set to `0` which is disabled. An additional `webhook.retry_delay` parameter can be set to specify the time in seconds between retry attempts. By default this is set to `0.1` (i.e. 100ms). Note that increasing the number of retries or retry delay may slow down the trader if there are connectivity issues with the webhook. Example configuration for retries:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"webhook": {
|
||||||
|
"enabled": true,
|
||||||
|
"url": "https://<YOURHOOKURL>",
|
||||||
|
"retries": 3,
|
||||||
|
"retry_delay": 0.2,
|
||||||
|
"webhookstatus": {
|
||||||
|
"status": "Status: {status}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
|
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
|
||||||
|
|
||||||
### Webhookbuy
|
### Webhookentry
|
||||||
|
|
||||||
The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format.
|
The fields in `webhook.webhookentry` are filled when the bot executes a long/short. Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
|
||||||
|
|
||||||
* `trade_id`
|
|
||||||
* `exchange`
|
|
||||||
* `pair`
|
|
||||||
* `limit`
|
|
||||||
* `amount`
|
|
||||||
* `open_date`
|
|
||||||
* `stake_amount`
|
|
||||||
* `stake_currency`
|
|
||||||
* `fiat_currency`
|
|
||||||
* `order_type`
|
|
||||||
* `current_rate`
|
|
||||||
* `buy_tag`
|
|
||||||
|
|
||||||
### Webhookbuycancel
|
|
||||||
|
|
||||||
The fields in `webhook.webhookbuycancel` are filled when the bot cancels a buy order. Parameters are filled using string.format.
|
|
||||||
Possible parameters are:
|
|
||||||
|
|
||||||
* `trade_id`
|
|
||||||
* `exchange`
|
|
||||||
* `pair`
|
|
||||||
* `limit`
|
|
||||||
* `amount`
|
|
||||||
* `open_date`
|
|
||||||
* `stake_amount`
|
|
||||||
* `stake_currency`
|
|
||||||
* `fiat_currency`
|
|
||||||
* `order_type`
|
|
||||||
* `current_rate`
|
|
||||||
* `buy_tag`
|
|
||||||
|
|
||||||
### Webhookbuyfill
|
|
||||||
|
|
||||||
The fields in `webhook.webhookbuyfill` are filled when the bot filled a buy order. Parameters are filled using string.format.
|
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
* `trade_id`
|
* `trade_id`
|
||||||
* `exchange`
|
* `exchange`
|
||||||
* `pair`
|
* `pair`
|
||||||
|
* `direction`
|
||||||
|
* `leverage`
|
||||||
|
* ~~`limit` # Deprecated - should no longer be used.~~
|
||||||
* `open_rate`
|
* `open_rate`
|
||||||
* `amount`
|
* `amount`
|
||||||
* `open_date`
|
* `open_date`
|
||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
|
* `base_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `buy_tag`
|
* `order_type`
|
||||||
|
* `current_rate`
|
||||||
|
* `enter_tag`
|
||||||
|
|
||||||
### Webhooksell
|
### Webhookentrycancel
|
||||||
|
|
||||||
The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format.
|
The fields in `webhook.webhookentrycancel` are filled when the bot cancels a long/short order. Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
* `trade_id`
|
* `trade_id`
|
||||||
* `exchange`
|
* `exchange`
|
||||||
* `pair`
|
* `pair`
|
||||||
|
* `direction`
|
||||||
|
* `leverage`
|
||||||
|
* `limit`
|
||||||
|
* `amount`
|
||||||
|
* `open_date`
|
||||||
|
* `stake_amount`
|
||||||
|
* `stake_currency`
|
||||||
|
* `base_currency`
|
||||||
|
* `fiat_currency`
|
||||||
|
* `order_type`
|
||||||
|
* `current_rate`
|
||||||
|
* `enter_tag`
|
||||||
|
|
||||||
|
### Webhookentryfill
|
||||||
|
|
||||||
|
The fields in `webhook.webhookentryfill` are filled when the bot filled a long/short order. Parameters are filled using string.format.
|
||||||
|
Possible parameters are:
|
||||||
|
|
||||||
|
* `trade_id`
|
||||||
|
* `exchange`
|
||||||
|
* `pair`
|
||||||
|
* `direction`
|
||||||
|
* `leverage`
|
||||||
|
* `open_rate`
|
||||||
|
* `amount`
|
||||||
|
* `open_date`
|
||||||
|
* `stake_amount`
|
||||||
|
* `stake_currency`
|
||||||
|
* `base_currency`
|
||||||
|
* `fiat_currency`
|
||||||
|
* `order_type`
|
||||||
|
* `current_rate`
|
||||||
|
* `enter_tag`
|
||||||
|
|
||||||
|
### Webhookexit
|
||||||
|
|
||||||
|
The fields in `webhook.webhookexit` are filled when the bot exits a trade. Parameters are filled using string.format.
|
||||||
|
Possible parameters are:
|
||||||
|
|
||||||
|
* `trade_id`
|
||||||
|
* `exchange`
|
||||||
|
* `pair`
|
||||||
|
* `direction`
|
||||||
|
* `leverage`
|
||||||
* `gain`
|
* `gain`
|
||||||
* `limit`
|
* `limit`
|
||||||
* `amount`
|
* `amount`
|
||||||
|
@ -134,20 +177,23 @@ Possible parameters are:
|
||||||
* `profit_amount`
|
* `profit_amount`
|
||||||
* `profit_ratio`
|
* `profit_ratio`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
|
* `base_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `sell_reason`
|
* `exit_reason`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `open_date`
|
* `open_date`
|
||||||
* `close_date`
|
* `close_date`
|
||||||
|
|
||||||
### Webhooksellfill
|
### Webhookexitfill
|
||||||
|
|
||||||
The fields in `webhook.webhooksellfill` are filled when the bot fills a sell order (closes a Trae). Parameters are filled using string.format.
|
The fields in `webhook.webhookexitfill` are filled when the bot fills a exit order (closes a Trade). Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
* `trade_id`
|
* `trade_id`
|
||||||
* `exchange`
|
* `exchange`
|
||||||
* `pair`
|
* `pair`
|
||||||
|
* `direction`
|
||||||
|
* `leverage`
|
||||||
* `gain`
|
* `gain`
|
||||||
* `close_rate`
|
* `close_rate`
|
||||||
* `amount`
|
* `amount`
|
||||||
|
@ -156,20 +202,23 @@ Possible parameters are:
|
||||||
* `profit_amount`
|
* `profit_amount`
|
||||||
* `profit_ratio`
|
* `profit_ratio`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
|
* `base_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `sell_reason`
|
* `exit_reason`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `open_date`
|
* `open_date`
|
||||||
* `close_date`
|
* `close_date`
|
||||||
|
|
||||||
### Webhooksellcancel
|
### Webhookexitcancel
|
||||||
|
|
||||||
The fields in `webhook.webhooksellcancel` are filled when the bot cancels a sell order. Parameters are filled using string.format.
|
The fields in `webhook.webhookexitcancel` are filled when the bot cancels a exit order. Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
* `trade_id`
|
* `trade_id`
|
||||||
* `exchange`
|
* `exchange`
|
||||||
* `pair`
|
* `pair`
|
||||||
|
* `direction`
|
||||||
|
* `leverage`
|
||||||
* `gain`
|
* `gain`
|
||||||
* `limit`
|
* `limit`
|
||||||
* `amount`
|
* `amount`
|
||||||
|
@ -178,8 +227,9 @@ Possible parameters are:
|
||||||
* `profit_amount`
|
* `profit_amount`
|
||||||
* `profit_ratio`
|
* `profit_ratio`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
|
* `base_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `sell_reason`
|
* `exit_reason`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `open_date`
|
* `open_date`
|
||||||
* `close_date`
|
* `close_date`
|
||||||
|
|
|
@ -23,9 +23,9 @@ git clone https://github.com/freqtrade/freqtrade.git
|
||||||
|
|
||||||
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
||||||
|
|
||||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.21-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version).
|
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.24-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version).
|
||||||
|
|
||||||
Freqtrade provides these dependencies for the latest 2 Python versions (3.7 and 3.8) and for 64bit Windows.
|
Freqtrade provides these dependencies for the latest 3 Python versions (3.8, 3.9 and 3.10) and for 64bit Windows.
|
||||||
Other versions must be downloaded from the above link.
|
Other versions must be downloaded from the above link.
|
||||||
|
|
||||||
``` powershell
|
``` powershell
|
||||||
|
@ -54,6 +54,8 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++
|
||||||
|
|
||||||
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||||
|
|
||||||
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker compose](docker_quickstart.md) first.
|
You can download the Visual C++ build tools from [here](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and install "Desktop development with C++" in it's default configuration. Unfortunately, this is a heavy download / dependency so you might want to consider WSL2 or [docker compose](docker_quickstart.md) first.
|
||||||
|
|
||||||
|
![Windows installation](assets/windows_install.png)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
@ -4,7 +4,7 @@ channels:
|
||||||
# - defaults
|
# - defaults
|
||||||
dependencies:
|
dependencies:
|
||||||
# 1/4 req main
|
# 1/4 req main
|
||||||
- python>=3.7,<3.9
|
- python>=3.8,<=3.10
|
||||||
- numpy
|
- numpy
|
||||||
- pandas
|
- pandas
|
||||||
- pip
|
- pip
|
||||||
|
@ -16,7 +16,6 @@ dependencies:
|
||||||
- cachetools
|
- cachetools
|
||||||
- requests
|
- requests
|
||||||
- urllib3
|
- urllib3
|
||||||
- wrapt
|
|
||||||
- jsonschema
|
- jsonschema
|
||||||
- TA-Lib
|
- TA-Lib
|
||||||
- tabulate
|
- tabulate
|
||||||
|
@ -26,9 +25,13 @@ dependencies:
|
||||||
- fastapi
|
- fastapi
|
||||||
- uvicorn
|
- uvicorn
|
||||||
- pyjwt
|
- pyjwt
|
||||||
|
- aiofiles
|
||||||
|
- psutil
|
||||||
- colorama
|
- colorama
|
||||||
- questionary
|
- questionary
|
||||||
- prompt-toolkit
|
- prompt-toolkit
|
||||||
|
- schedule
|
||||||
|
- python-dateutil
|
||||||
|
|
||||||
|
|
||||||
# ============================
|
# ============================
|
||||||
|
@ -64,7 +67,6 @@ dependencies:
|
||||||
- py_find_1st
|
- py_find_1st
|
||||||
- tables
|
- tables
|
||||||
- pytest-random-order
|
- pytest-random-order
|
||||||
- flake8-type-annotations
|
|
||||||
- ccxt
|
- ccxt
|
||||||
- flake8-tidy-imports
|
- flake8-tidy-imports
|
||||||
- -e .
|
- -e .
|
||||||
|
|
|
@ -1,27 +1,14 @@
|
||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = 'develop'
|
__version__ = 'develop'
|
||||||
|
|
||||||
if __version__ == 'develop':
|
if 'dev' in __version__:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
__version__ = 'develop-' + subprocess.check_output(
|
__version__ = __version__ + '-' + subprocess.check_output(
|
||||||
['git', 'log', '--format="%h"', '-n 1'],
|
['git', 'log', '--format="%h"', '-n 1'],
|
||||||
stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
||||||
|
|
||||||
# from datetime import datetime
|
|
||||||
# last_release = subprocess.check_output(
|
|
||||||
# ['git', 'tag']
|
|
||||||
# ).decode('utf-8').split()[-1].split(".")
|
|
||||||
# # Releases are in the format "2020.1" - we increment the latest version for dev.
|
|
||||||
# prefix = f"{last_release[0]}.{int(last_release[1]) + 1}"
|
|
||||||
# dev_version = int(datetime.now().timestamp() // 1000)
|
|
||||||
# __version__ = f"{prefix}.dev{dev_version}"
|
|
||||||
|
|
||||||
# subprocess.check_output(
|
|
||||||
# ['git', 'log', '--format="%h"', '-n 1'],
|
|
||||||
# stderr=subprocess.DEVNULL).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:
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
__main__.py for Freqtrade
|
__main__.py for Freqtrade
|
||||||
To launch Freqtrade as a module
|
To launch Freqtrade as a module
|
||||||
|
|
||||||
> python -m freqtrade (with Python >= 3.7)
|
> python -m freqtrade (with Python >= 3.8)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from freqtrade import main
|
from freqtrade import main
|
||||||
|
|
|
@ -8,15 +8,16 @@ Note: Be careful with file-scoped imports in these subfiles.
|
||||||
"""
|
"""
|
||||||
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
|
||||||
from freqtrade.commands.data_commands import (start_convert_data, start_download_data,
|
from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades,
|
||||||
start_list_data)
|
start_download_data, start_list_data)
|
||||||
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui,
|
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui,
|
||||||
start_new_strategy)
|
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_markets,
|
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_markets,
|
||||||
start_list_strategies, start_list_timeframes,
|
start_list_strategies, start_list_timeframes,
|
||||||
start_show_trades)
|
start_show_trades)
|
||||||
from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt
|
from freqtrade.commands.optimize_commands import (start_backtesting, start_backtesting_show,
|
||||||
|
start_edge, start_hyperopt)
|
||||||
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.trade_commands import start_trading
|
from freqtrade.commands.trade_commands import start_trading
|
||||||
|
|
|
@ -23,7 +23,8 @@ ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
|
||||||
|
|
||||||
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
||||||
"enable_protections", "dry_run_wallet", "timeframe_detail",
|
"enable_protections", "dry_run_wallet", "timeframe_detail",
|
||||||
"strategy_list", "export", "exportfilename"]
|
"strategy_list", "export", "exportfilename",
|
||||||
|
"backtest_breakdown", "backtest_cache"]
|
||||||
|
|
||||||
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||||
"position_stacking", "use_max_market_positions",
|
"position_stacking", "use_max_market_positions",
|
||||||
|
@ -31,7 +32,8 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||||
"epochs", "spaces", "print_all",
|
"epochs", "spaces", "print_all",
|
||||||
"print_colorized", "print_json", "hyperopt_jobs",
|
"print_colorized", "print_json", "hyperopt_jobs",
|
||||||
"hyperopt_random_state", "hyperopt_min_trades",
|
"hyperopt_random_state", "hyperopt_min_trades",
|
||||||
"hyperopt_loss", "disableparamexport"]
|
"hyperopt_loss", "disableparamexport",
|
||||||
|
"hyperopt_ignore_missing_space"]
|
||||||
|
|
||||||
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
||||||
|
|
||||||
|
@ -39,15 +41,18 @@ ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized"]
|
||||||
|
|
||||||
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"]
|
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"]
|
||||||
|
|
||||||
|
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list"]
|
||||||
|
|
||||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
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 = ["exchange", "print_list", "list_pairs_print_json", "print_one_column",
|
||||||
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all"]
|
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all",
|
||||||
|
"trading_mode"]
|
||||||
|
|
||||||
ARGS_TEST_PAIRLIST = ["verbosity", "config", "quote_currencies", "print_one_column",
|
ARGS_TEST_PAIRLIST = ["verbosity", "config", "quote_currencies", "print_one_column",
|
||||||
"list_pairs_print_json"]
|
"list_pairs_print_json", "exchange"]
|
||||||
|
|
||||||
ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]
|
ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]
|
||||||
|
|
||||||
|
@ -56,22 +61,26 @@ ARGS_BUILD_CONFIG = ["config"]
|
||||||
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
||||||
|
|
||||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
|
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
|
||||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
|
|
||||||
|
|
||||||
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"]
|
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "exchange", "trading_mode",
|
||||||
|
"candle_types"]
|
||||||
|
|
||||||
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "timerange",
|
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
|
||||||
"download_trades", "exchange", "timeframes", "erase", "dataformat_ohlcv",
|
|
||||||
"dataformat_trades"]
|
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs", "trading_mode"]
|
||||||
|
|
||||||
|
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "include_inactive",
|
||||||
|
"timerange", "download_trades", "exchange", "timeframes",
|
||||||
|
"erase", "dataformat_ohlcv", "dataformat_trades", "trading_mode"]
|
||||||
|
|
||||||
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
|
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
|
||||||
"db_url", "trade_source", "export", "exportfilename",
|
"db_url", "trade_source", "export", "exportfilename",
|
||||||
"timerange", "timeframe", "no_trades"]
|
"timerange", "timeframe", "no_trades"]
|
||||||
|
|
||||||
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
|
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
|
||||||
"trade_source", "timeframe", "plot_auto_open"]
|
"trade_source", "timeframe", "plot_auto_open", ]
|
||||||
|
|
||||||
ARGS_INSTALL_UI = ["erase_ui_only"]
|
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"]
|
||||||
|
|
||||||
|
@ -86,12 +95,12 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
|
||||||
|
|
||||||
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
|
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
|
||||||
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
|
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
|
||||||
"disableparamexport"]
|
"disableparamexport", "backtest_breakdown"]
|
||||||
|
|
||||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||||
"list-markets", "list-pairs", "list-strategies", "list-data",
|
"list-markets", "list-pairs", "list-strategies", "list-data",
|
||||||
"hyperopt-list", "hyperopt-show",
|
"hyperopt-list", "hyperopt-show", "backtest-filter",
|
||||||
"plot-dataframe", "plot-profit", "show-trades"]
|
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"]
|
||||||
|
|
||||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||||
|
|
||||||
|
@ -169,14 +178,15 @@ class Arguments:
|
||||||
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
||||||
self._build_args(optionlist=['version'], parser=self.parser)
|
self._build_args(optionlist=['version'], parser=self.parser)
|
||||||
|
|
||||||
from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir,
|
from freqtrade.commands import (start_backtesting, start_backtesting_show,
|
||||||
start_download_data, start_edge, start_hyperopt,
|
start_convert_data, start_convert_trades,
|
||||||
start_hyperopt_list, start_hyperopt_show, start_install_ui,
|
start_create_userdir, start_download_data, start_edge,
|
||||||
start_list_data, start_list_exchanges, start_list_markets,
|
start_hyperopt, start_hyperopt_list, start_hyperopt_show,
|
||||||
start_list_strategies, start_list_timeframes,
|
start_install_ui, start_list_data, start_list_exchanges,
|
||||||
start_new_config, start_new_strategy, start_plot_dataframe,
|
start_list_markets, start_list_strategies,
|
||||||
start_plot_profit, start_show_trades, start_test_pairlist,
|
start_list_timeframes, start_new_config, start_new_strategy,
|
||||||
start_trading, start_webserver)
|
start_plot_dataframe, start_plot_profit, start_show_trades,
|
||||||
|
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
|
||||||
|
@ -236,6 +246,15 @@ class Arguments:
|
||||||
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))
|
||||||
self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd)
|
self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd)
|
||||||
|
|
||||||
|
# Add trades-to-ohlcv subcommand
|
||||||
|
convert_trade_data_cmd = subparsers.add_parser(
|
||||||
|
'trades-to-ohlcv',
|
||||||
|
help='Convert trade data to OHLCV data.',
|
||||||
|
parents=[_common_parser],
|
||||||
|
)
|
||||||
|
convert_trade_data_cmd.set_defaults(func=start_convert_trades)
|
||||||
|
self._build_args(optionlist=ARGS_CONVERT_TRADES, parser=convert_trade_data_cmd)
|
||||||
|
|
||||||
# Add list-data subcommand
|
# Add list-data subcommand
|
||||||
list_data_cmd = subparsers.add_parser(
|
list_data_cmd = subparsers.add_parser(
|
||||||
'list-data',
|
'list-data',
|
||||||
|
@ -251,6 +270,15 @@ class Arguments:
|
||||||
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
|
||||||
|
backtesting_show_cmd = subparsers.add_parser(
|
||||||
|
'backtesting-show',
|
||||||
|
help='Show past Backtest results',
|
||||||
|
parents=[_common_parser],
|
||||||
|
)
|
||||||
|
backtesting_show_cmd.set_defaults(func=start_backtesting_show)
|
||||||
|
self._build_args(optionlist=ARGS_BACKTEST_SHOW, parser=backtesting_show_cmd)
|
||||||
|
|
||||||
# Add edge subcommand
|
# Add edge subcommand
|
||||||
edge_cmd = subparsers.add_parser('edge', help='Edge module.',
|
edge_cmd = subparsers.add_parser('edge', help='Edge module.',
|
||||||
parents=[_common_parser, _strategy_parser])
|
parents=[_common_parser, _strategy_parser])
|
||||||
|
|
|
@ -76,18 +76,23 @@ def ask_user_config() -> Dict[str, Any]:
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"name": "max_open_trades",
|
"name": "max_open_trades",
|
||||||
"message": f"Please insert max_open_trades (Integer or '{UNLIMITED_STAKE_AMOUNT}'):",
|
"message": "Please insert max_open_trades (Integer or -1 for unlimited open trades):",
|
||||||
"default": "3",
|
"default": "3",
|
||||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_int(val),
|
"validate": lambda val: validate_is_int(val)
|
||||||
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
|
},
|
||||||
if val == UNLIMITED_STAKE_AMOUNT
|
{
|
||||||
else val
|
"type": "select",
|
||||||
|
"name": "timeframe_in_config",
|
||||||
|
"message": "Time",
|
||||||
|
"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.'
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -99,18 +104,28 @@ def ask_user_config() -> Dict[str, Any]:
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"name": "exchange_name",
|
"name": "exchange_name",
|
||||||
"message": "Select exchange",
|
"message": "Select exchange",
|
||||||
"choices": [
|
"choices": lambda x: [
|
||||||
"binance",
|
"binance",
|
||||||
"binanceus",
|
"binanceus",
|
||||||
"bittrex",
|
"bittrex",
|
||||||
"kraken",
|
|
||||||
"ftx",
|
"ftx",
|
||||||
"kucoin",
|
|
||||||
"gateio",
|
"gateio",
|
||||||
Separator(),
|
"huobi",
|
||||||
|
"kraken",
|
||||||
|
"kucoin",
|
||||||
|
"okx",
|
||||||
|
Separator("------------------"),
|
||||||
"other",
|
"other",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "confirm",
|
||||||
|
"name": "trading_mode",
|
||||||
|
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
|
||||||
|
"default": False,
|
||||||
|
"filter": lambda val: 'futures' if val else 'spot',
|
||||||
|
"when": lambda x: x["exchange_name"] in ['binance', 'gateio', 'okx'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "autocomplete",
|
"type": "autocomplete",
|
||||||
"name": "exchange_name",
|
"name": "exchange_name",
|
||||||
|
@ -134,7 +149,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||||
"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'] == 'kucoin'
|
"when": lambda x: not x['dry_run'] and x['exchange_name'] in ('kucoin', 'okx')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "confirm",
|
"type": "confirm",
|
||||||
|
@ -163,7 +178,8 @@ 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 (best left untouched default!)",
|
"message": ("Insert Api server Listen Address (0.0.0.0 for docker, "
|
||||||
|
"otherwise best left untouched)"),
|
||||||
"default": "127.0.0.1",
|
"default": "127.0.0.1",
|
||||||
"when": lambda x: x['api_server']
|
"when": lambda x: x['api_server']
|
||||||
},
|
},
|
||||||
|
@ -186,7 +202,13 @@ def ask_user_config() -> Dict[str, Any]:
|
||||||
if not answers:
|
if not answers:
|
||||||
# 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
|
||||||
|
answers['trading_mode'] = answers.get('trading_mode', "spot")
|
||||||
|
answers['margin_mode'] = (
|
||||||
|
'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()
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ from argparse import SUPPRESS, ArgumentTypeError
|
||||||
|
|
||||||
from freqtrade import __version__, constants
|
from freqtrade import __version__, constants
|
||||||
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN
|
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN
|
||||||
|
from freqtrade.enums import CandleType
|
||||||
|
|
||||||
|
|
||||||
def check_int_positive(value: str) -> int:
|
def check_int_positive(value: str) -> int:
|
||||||
|
@ -117,7 +118,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||||
),
|
),
|
||||||
# Optimize common
|
# Optimize common
|
||||||
"timeframe": Arg(
|
"timeframe": Arg(
|
||||||
'-i', '--timeframe', '--ticker-interval',
|
'-i', '--timeframe',
|
||||||
help='Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
help='Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
||||||
),
|
),
|
||||||
"timerange": Arg(
|
"timerange": Arg(
|
||||||
|
@ -152,6 +153,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||||
action='store_false',
|
action='store_false',
|
||||||
default=True,
|
default=True,
|
||||||
),
|
),
|
||||||
|
"backtest_show_pair_list": Arg(
|
||||||
|
'--show-pair-list',
|
||||||
|
help='Show backtesting pairlist sorted by profit.',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
"enable_protections": Arg(
|
"enable_protections": Arg(
|
||||||
'--enable-protections', '--enableprotections',
|
'--enable-protections', '--enableprotections',
|
||||||
help='Enable protections for backtesting.'
|
help='Enable protections for backtesting.'
|
||||||
|
@ -163,7 +170,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||||
"strategy_list": Arg(
|
"strategy_list": Arg(
|
||||||
'--strategy-list',
|
'--strategy-list',
|
||||||
help='Provide a space-separated list of strategies to backtest. '
|
help='Provide a space-separated list of strategies to backtest. '
|
||||||
'Please note that ticker-interval needs to be set either in config '
|
'Please note that timeframe needs to be set either in config '
|
||||||
'or via command line. When using this together with `--export trades`, '
|
'or via command line. When using this together with `--export trades`, '
|
||||||
'the strategy-name is injected into the filename '
|
'the strategy-name is injected into the filename '
|
||||||
'(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`',
|
'(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`',
|
||||||
|
@ -173,14 +180,14 @@ AVAILABLE_CLI_OPTIONS = {
|
||||||
'--export',
|
'--export',
|
||||||
help='Export backtest results (default: trades).',
|
help='Export backtest results (default: trades).',
|
||||||
choices=constants.EXPORT_OPTIONS,
|
choices=constants.EXPORT_OPTIONS,
|
||||||
|
|
||||||
),
|
),
|
||||||
"exportfilename": Arg(
|
"exportfilename": Arg(
|
||||||
'--export-filename',
|
"--export-filename",
|
||||||
help='Save backtest results to the file with this filename. '
|
"--backtest-filename",
|
||||||
'Requires `--export` to be set as well. '
|
help="Use this filename for backtest results."
|
||||||
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
|
"Requires `--export` to be set as well. "
|
||||||
metavar='PATH',
|
"Example: `--export-filename=user_data/backtest_results/backtest_today.json`",
|
||||||
|
metavar="PATH",
|
||||||
),
|
),
|
||||||
"disableparamexport": Arg(
|
"disableparamexport": Arg(
|
||||||
'--disable-param-export',
|
'--disable-param-export',
|
||||||
|
@ -193,6 +200,18 @@ AVAILABLE_CLI_OPTIONS = {
|
||||||
type=float,
|
type=float,
|
||||||
metavar='FLOAT',
|
metavar='FLOAT',
|
||||||
),
|
),
|
||||||
|
"backtest_breakdown": Arg(
|
||||||
|
'--breakdown',
|
||||||
|
help='Show backtesting breakdown per [day, week, month].',
|
||||||
|
nargs='+',
|
||||||
|
choices=constants.BACKTEST_BREAKDOWNS
|
||||||
|
),
|
||||||
|
"backtest_cache": Arg(
|
||||||
|
'--cache',
|
||||||
|
help='Load a cached backtest result no older than specified age (default: %(default)s).',
|
||||||
|
default=constants.BACKTEST_CACHE_DEFAULT,
|
||||||
|
choices=constants.BACKTEST_CACHE_AGE,
|
||||||
|
),
|
||||||
# Edge
|
# Edge
|
||||||
"stoploss_range": Arg(
|
"stoploss_range": Arg(
|
||||||
'--stoplosses',
|
'--stoplosses',
|
||||||
|
@ -337,6 +356,17 @@ AVAILABLE_CLI_OPTIONS = {
|
||||||
nargs='+',
|
nargs='+',
|
||||||
metavar='BASE_CURRENCY',
|
metavar='BASE_CURRENCY',
|
||||||
),
|
),
|
||||||
|
"trading_mode": Arg(
|
||||||
|
'--trading-mode',
|
||||||
|
help='Select Trading mode',
|
||||||
|
choices=constants.TRADING_MODES,
|
||||||
|
),
|
||||||
|
"candle_types": Arg(
|
||||||
|
'--candle-types',
|
||||||
|
help='Select candle type to use',
|
||||||
|
choices=[c.value for c in CandleType],
|
||||||
|
nargs='+',
|
||||||
|
),
|
||||||
# Script options
|
# Script options
|
||||||
"pairs": Arg(
|
"pairs": Arg(
|
||||||
'-p', '--pairs',
|
'-p', '--pairs',
|
||||||
|
@ -355,6 +385,11 @@ AVAILABLE_CLI_OPTIONS = {
|
||||||
type=check_int_positive,
|
type=check_int_positive,
|
||||||
metavar='INT',
|
metavar='INT',
|
||||||
),
|
),
|
||||||
|
"include_inactive": Arg(
|
||||||
|
'--include-inactive-pairs',
|
||||||
|
help='Also download data from inactive pairs.',
|
||||||
|
action='store_true',
|
||||||
|
),
|
||||||
"new_pairs_days": Arg(
|
"new_pairs_days": Arg(
|
||||||
'--new-pairs-days',
|
'--new-pairs-days',
|
||||||
help='Download data of new pairs for given number of days. Default: `%(default)s`.',
|
help='Download data of new pairs for given number of days. Default: `%(default)s`.',
|
||||||
|
@ -381,12 +416,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||||
),
|
),
|
||||||
"dataformat_ohlcv": Arg(
|
"dataformat_ohlcv": Arg(
|
||||||
'--data-format-ohlcv',
|
'--data-format-ohlcv',
|
||||||
help='Storage format for downloaded candle (OHLCV) data. (default: `%(default)s`).',
|
help='Storage format for downloaded candle (OHLCV) data. (default: `json`).',
|
||||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||||
),
|
),
|
||||||
"dataformat_trades": Arg(
|
"dataformat_trades": Arg(
|
||||||
'--data-format-trades',
|
'--data-format-trades',
|
||||||
help='Storage format for downloaded trades data. (default: `%(default)s`).',
|
help='Storage format for downloaded trades data. (default: `jsongz`).',
|
||||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||||
),
|
),
|
||||||
"exchange": Arg(
|
"exchange": Arg(
|
||||||
|
@ -414,6 +449,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||||
action='store_true',
|
action='store_true',
|
||||||
default=False,
|
default=False,
|
||||||
),
|
),
|
||||||
|
"ui_version": Arg(
|
||||||
|
'--ui-version',
|
||||||
|
help=('Specify a specific version of FreqUI to install. '
|
||||||
|
'Not specifying this installs the latest version.'),
|
||||||
|
type=str,
|
||||||
|
),
|
||||||
# Templating options
|
# Templating options
|
||||||
"template": Arg(
|
"template": Arg(
|
||||||
'--template',
|
'--template',
|
||||||
|
@ -552,4 +593,10 @@ AVAILABLE_CLI_OPTIONS = {
|
||||||
help='Do not print epoch details header.',
|
help='Do not print epoch details header.',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
),
|
),
|
||||||
|
"hyperopt_ignore_missing_space": Arg(
|
||||||
|
"--ignore-missing-spaces", "--ignore-unparameterized-spaces",
|
||||||
|
help=("Suppress errors for any requested Hyperopt spaces "
|
||||||
|
"that do not contain any parameters."),
|
||||||
|
action="store_true",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,10 @@ from freqtrade.configuration import TimeRange, setup_utils_configuration
|
||||||
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
||||||
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
|
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
|
||||||
refresh_backtest_trades_data)
|
refresh_backtest_trades_data)
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import CandleType, RunMode, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
|
from freqtrade.exchange.exchange import market_is_active
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
from freqtrade.resolvers import ExchangeResolver
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
|
|
||||||
|
@ -47,11 +48,13 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
# Init exchange
|
# Init exchange
|
||||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
||||||
|
markets = [p for p, m in exchange.markets.items() if market_is_active(m)
|
||||||
|
or config.get('include_inactive')]
|
||||||
|
expanded_pairs = expand_pairlist(config['pairs'], markets)
|
||||||
|
|
||||||
# Manual validations of relevant settings
|
# Manual validations of relevant settings
|
||||||
if not config['exchange'].get('skip_pair_validation', False):
|
if not config['exchange'].get('skip_pair_validation', False):
|
||||||
exchange.validate_pairs(config['pairs'])
|
exchange.validate_pairs(expanded_pairs)
|
||||||
expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
|
|
||||||
|
|
||||||
logger.info(f"About to download pairs: {expanded_pairs}, "
|
logger.info(f"About to download pairs: {expanded_pairs}, "
|
||||||
f"intervals: {config['timeframes']} to {config['datadir']}")
|
f"intervals: {config['timeframes']} to {config['datadir']}")
|
||||||
|
|
||||||
|
@ -61,6 +64,8 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
if config.get('download_trades'):
|
if config.get('download_trades'):
|
||||||
|
if config.get('trading_mode') == 'futures':
|
||||||
|
raise OperationalException("Trade download not supported for futures.")
|
||||||
pairs_not_available = refresh_backtest_trades_data(
|
pairs_not_available = refresh_backtest_trades_data(
|
||||||
exchange, pairs=expanded_pairs, datadir=config['datadir'],
|
exchange, pairs=expanded_pairs, datadir=config['datadir'],
|
||||||
timerange=timerange, new_pairs_days=config['new_pairs_days'],
|
timerange=timerange, new_pairs_days=config['new_pairs_days'],
|
||||||
|
@ -78,7 +83,9 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||||
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||||
datadir=config['datadir'], timerange=timerange,
|
datadir=config['datadir'], timerange=timerange,
|
||||||
new_pairs_days=config['new_pairs_days'],
|
new_pairs_days=config['new_pairs_days'],
|
||||||
erase=bool(config.get('erase')), data_format=config['dataformat_ohlcv'])
|
erase=bool(config.get('erase')), data_format=config['dataformat_ohlcv'],
|
||||||
|
trading_mode=config.get('trading_mode', 'spot'),
|
||||||
|
)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
sys.exit("SIGINT received, aborting ...")
|
sys.exit("SIGINT received, aborting ...")
|
||||||
|
@ -89,15 +96,52 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||||
f"on exchange {exchange.name}.")
|
f"on exchange {exchange.name}.")
|
||||||
|
|
||||||
|
|
||||||
|
def start_convert_trades(args: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||||
|
|
||||||
|
timerange = TimeRange()
|
||||||
|
|
||||||
|
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||||
|
config['stake_currency'] = ''
|
||||||
|
|
||||||
|
if 'pairs' not in config:
|
||||||
|
raise OperationalException(
|
||||||
|
"Downloading data requires a list of pairs. "
|
||||||
|
"Please check the documentation on how to configure this.")
|
||||||
|
|
||||||
|
# Init exchange
|
||||||
|
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
||||||
|
# Manual validations of relevant settings
|
||||||
|
if not config['exchange'].get('skip_pair_validation', False):
|
||||||
|
exchange.validate_pairs(config['pairs'])
|
||||||
|
expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
|
||||||
|
|
||||||
|
logger.info(f"About to Convert pairs: {expanded_pairs}, "
|
||||||
|
f"intervals: {config['timeframes']} to {config['datadir']}")
|
||||||
|
|
||||||
|
for timeframe in config['timeframes']:
|
||||||
|
exchange.validate_timeframes(timeframe)
|
||||||
|
# Convert downloaded trade data to different timeframes
|
||||||
|
convert_trades_to_ohlcv(
|
||||||
|
pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||||
|
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
|
||||||
|
data_format_ohlcv=config['dataformat_ohlcv'],
|
||||||
|
data_format_trades=config['dataformat_trades'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
||||||
"""
|
"""
|
||||||
Convert data from one format to another
|
Convert data from one format to another
|
||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
if ohlcv:
|
if ohlcv:
|
||||||
convert_ohlcv_format(config,
|
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', ['spot'])]
|
||||||
convert_from=args['format_from'], convert_to=args['format_to'],
|
for candle_type in candle_types:
|
||||||
erase=args['erase'])
|
convert_ohlcv_format(config,
|
||||||
|
convert_from=args['format_from'], convert_to=args['format_to'],
|
||||||
|
erase=args['erase'], candle_type=candle_type)
|
||||||
else:
|
else:
|
||||||
convert_trades_format(config,
|
convert_trades_format(config,
|
||||||
convert_from=args['format_from'], convert_to=args['format_to'],
|
convert_from=args['format_from'], convert_to=args['format_to'],
|
||||||
|
@ -116,17 +160,26 @@ def start_list_data(args: Dict[str, Any]) -> None:
|
||||||
from freqtrade.data.history.idatahandler import get_datahandler
|
from freqtrade.data.history.idatahandler 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(config['datadir'])
|
paircombs = dhc.ohlcv_get_available_data(
|
||||||
|
config['datadir'],
|
||||||
|
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.")
|
||||||
groupedpair = defaultdict(list)
|
groupedpair = defaultdict(list)
|
||||||
for pair, timeframe in sorted(paircombs, key=lambda x: (x[0], timeframe_to_minutes(x[1]))):
|
for pair, timeframe, candle_type in sorted(
|
||||||
groupedpair[pair].append(timeframe)
|
paircombs,
|
||||||
|
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])
|
||||||
|
):
|
||||||
|
groupedpair[(pair, candle_type)].append(timeframe)
|
||||||
|
|
||||||
if groupedpair:
|
if groupedpair:
|
||||||
print(tabulate([(pair, ', '.join(timeframes)) for pair, timeframes in groupedpair.items()],
|
print(tabulate([
|
||||||
headers=("Pair", "Timeframe"),
|
(pair, ', '.join(timeframes), candle_type)
|
||||||
tablefmt='psql', stralign='right'))
|
for (pair, candle_type), timeframes in groupedpair.items()
|
||||||
|
],
|
||||||
|
headers=("Pair", "Timeframe", "Type"),
|
||||||
|
tablefmt='psql', stralign='right'))
|
||||||
|
|
|
@ -128,7 +128,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
|
||||||
f.write(version)
|
f.write(version)
|
||||||
|
|
||||||
|
|
||||||
def get_ui_download_url() -> 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
|
||||||
|
|
||||||
|
@ -136,8 +136,16 @@ def get_ui_download_url() -> Tuple[str, str]:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
r = resp.json()
|
r = resp.json()
|
||||||
|
|
||||||
latest_version = r[0]['name']
|
if version:
|
||||||
assets = r[0].get('assets', [])
|
tmp = [x for x in r if x['name'] == version]
|
||||||
|
if tmp:
|
||||||
|
latest_version = tmp[0]['name']
|
||||||
|
assets = tmp[0].get('assets', [])
|
||||||
|
else:
|
||||||
|
raise ValueError("UI-Version not found.")
|
||||||
|
else:
|
||||||
|
latest_version = r[0]['name']
|
||||||
|
assets = r[0].get('assets', [])
|
||||||
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']
|
||||||
|
@ -156,7 +164,7 @@ 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()
|
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'):
|
||||||
|
|
|
@ -96,7 +96,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
||||||
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(strategy_name, metrics,
|
||||||
metrics['stake_currency'])
|
metrics['stake_currency'], config.get('backtest_breakdown', []))
|
||||||
|
|
||||||
HyperoptTools.try_export_params(config, strategy_name, val)
|
HyperoptTools.try_export_params(config, strategy_name, val)
|
||||||
|
|
||||||
|
|
|
@ -131,7 +131,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
|
||||||
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,
|
||||||
pairs_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()))
|
||||||
|
@ -151,15 +151,19 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
|
||||||
if quote_currencies else ""))
|
if quote_currencies else ""))
|
||||||
|
|
||||||
headers = ["Id", "Symbol", "Base", "Quote", "Active",
|
headers = ["Id", "Symbol", "Base", "Quote", "Active",
|
||||||
*(['Is pair'] if not pairs_only else [])]
|
"Spot", "Margin", "Future", "Leverage"]
|
||||||
|
|
||||||
tabular_data = []
|
tabular_data = [{
|
||||||
for _, v in pairs.items():
|
'Id': v['id'],
|
||||||
tabular_data.append({'Id': v['id'], 'Symbol': v['symbol'],
|
'Symbol': v['symbol'],
|
||||||
'Base': v['base'], 'Quote': v['quote'],
|
'Base': v['base'],
|
||||||
'Active': market_is_active(v),
|
'Quote': v['quote'],
|
||||||
**({'Is pair': exchange.market_is_tradable(v)}
|
'Active': market_is_active(v),
|
||||||
if not pairs_only else {})})
|
'Spot': 'Spot' if exchange.market_is_spot(v) else '',
|
||||||
|
'Margin': 'Margin' if exchange.market_is_margin(v) else '',
|
||||||
|
'Future': 'Future' if exchange.market_is_future(v) else '',
|
||||||
|
'Leverage': exchange.get_max_leverage(v['symbol'], 20)
|
||||||
|
} for _, v in pairs.items()]
|
||||||
|
|
||||||
if (args.get('print_one_column', False) or
|
if (args.get('print_one_column', False) or
|
||||||
args.get('list_pairs_print_json', False) or
|
args.get('list_pairs_print_json', False) or
|
||||||
|
|
|
@ -25,12 +25,16 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
|
||||||
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']
|
||||||
|
# tradable_balance_ratio
|
||||||
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
|
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
|
||||||
and config['stake_amount'] > config['dry_run_wallet']):
|
and config['stake_amount'] > wallet_size):
|
||||||
wallet = round_coin_value(config['dry_run_wallet'], config['stake_currency'])
|
wallet = round_coin_value(wallet_size, config['stake_currency'])
|
||||||
stake = round_coin_value(config['stake_amount'], config['stake_currency'])
|
stake = round_coin_value(config['stake_amount'], config['stake_currency'])
|
||||||
raise OperationalException(f"Starting balance ({wallet}) "
|
raise OperationalException(
|
||||||
f"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`."
|
||||||
|
)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
@ -54,6 +58,22 @@ def start_backtesting(args: Dict[str, Any]) -> None:
|
||||||
backtesting.start()
|
backtesting.start()
|
||||||
|
|
||||||
|
|
||||||
|
def start_backtesting_show(args: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Show previous backtest result
|
||||||
|
"""
|
||||||
|
|
||||||
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
|
from freqtrade.data.btanalysis import load_backtest_stats
|
||||||
|
from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist
|
||||||
|
|
||||||
|
results = load_backtest_stats(config['exportfilename'])
|
||||||
|
|
||||||
|
show_backtest_results(config, results)
|
||||||
|
show_sorted_pairlist(config, results)
|
||||||
|
|
||||||
|
|
||||||
def start_hyperopt(args: Dict[str, Any]) -> None:
|
def start_hyperopt(args: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Start hyperopt script
|
Start hyperopt script
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from cachetools.ttl import TTLCache
|
from cachetools import TTLCache
|
||||||
|
|
||||||
|
|
||||||
class PeriodicCache(TTLCache):
|
class PeriodicCache(TTLCache):
|
||||||
|
@ -16,4 +16,4 @@ class PeriodicCache(TTLCache):
|
||||||
return ts - offset
|
return ts - offset
|
||||||
|
|
||||||
# Init with smlight offset
|
# Init with smlight offset
|
||||||
super().__init__(maxsize=maxsize, ttl=ttl-1e-5, timer=local_timer, getsizeof=getsizeof)
|
super().__init__(maxsize=maxsize, ttl=ttl - 1e-5, timer=local_timer, getsizeof=getsizeof)
|
||||||
|
|
|
@ -6,7 +6,8 @@ from jsonschema import Draft4Validator, validators
|
||||||
from jsonschema.exceptions import ValidationError, best_match
|
from jsonschema.exceptions import ValidationError, best_match
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.configuration.deprecated_settings import process_deprecated_setting
|
||||||
|
from freqtrade.enums import RunMode, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
|
|
||||||
|
@ -80,6 +81,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None:
|
||||||
_validate_protections(conf)
|
_validate_protections(conf)
|
||||||
_validate_unlimited_amount(conf)
|
_validate_unlimited_amount(conf)
|
||||||
_validate_ask_orderbook(conf)
|
_validate_ask_orderbook(conf)
|
||||||
|
validate_migrated_strategy_settings(conf)
|
||||||
|
|
||||||
# validate configuration before returning
|
# validate configuration before returning
|
||||||
logger.info('Validating configuration ...')
|
logger.info('Validating configuration ...')
|
||||||
|
@ -92,8 +94,8 @@ def _validate_unlimited_amount(conf: Dict[str, Any]) -> None:
|
||||||
:raise: OperationalException if config validation failed
|
:raise: OperationalException if config validation failed
|
||||||
"""
|
"""
|
||||||
if (not conf.get('edge', {}).get('enabled')
|
if (not conf.get('edge', {}).get('enabled')
|
||||||
and conf.get('max_open_trades') == float('inf')
|
and conf.get('max_open_trades') == float('inf')
|
||||||
and conf.get('stake_amount') == constants.UNLIMITED_STAKE_AMOUNT):
|
and conf.get('stake_amount') == constants.UNLIMITED_STAKE_AMOUNT):
|
||||||
raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.")
|
raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.")
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,13 +103,15 @@ 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
|
||||||
"""
|
"""
|
||||||
if (conf.get('order_types', {}).get('buy') == 'market'
|
# TODO: The below could be an enforced setting when using market orders
|
||||||
and conf.get('bid_strategy', {}).get('price_side') != 'ask'):
|
if (conf.get('order_types', {}).get('entry') == 'market'
|
||||||
raise OperationalException('Market buy orders require bid_strategy.price_side = "ask".')
|
and conf.get('entry_pricing', {}).get('price_side') not in ('ask', 'other')):
|
||||||
|
raise OperationalException(
|
||||||
|
'Market entry orders require entry_pricing.price_side = "other".')
|
||||||
|
|
||||||
if (conf.get('order_types', {}).get('sell') == 'market'
|
if (conf.get('order_types', {}).get('exit') == 'market'
|
||||||
and conf.get('ask_strategy', {}).get('price_side') != 'bid'):
|
and conf.get('exit_pricing', {}).get('price_side') not in ('bid', 'other')):
|
||||||
raise OperationalException('Market sell orders require ask_strategy.price_side = "bid".')
|
raise OperationalException('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:
|
||||||
|
@ -150,9 +154,9 @@ def _validate_edge(conf: Dict[str, Any]) -> None:
|
||||||
if not conf.get('edge', {}).get('enabled'):
|
if not conf.get('edge', {}).get('enabled'):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not conf.get('use_sell_signal', True):
|
if not conf.get('use_exit_signal', True):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"Edge requires `use_sell_signal` to be True, otherwise no sells will happen."
|
"Edge requires `use_exit_signal` to be True, otherwise no sells will happen."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -190,13 +194,13 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
|
|
||||||
def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
|
def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
|
||||||
ask_strategy = conf.get('ask_strategy', {})
|
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 OperationalException(
|
||||||
"Using order_book_max != order_book_min in ask_strategy 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:
|
||||||
|
@ -205,5 +209,121 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
|
||||||
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` "
|
||||||
"for your `ask_strategy` configuration."
|
"for your `exit_pricing` configuration."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
|
_validate_time_in_force(conf)
|
||||||
|
_validate_order_types(conf)
|
||||||
|
_validate_unfilledtimeout(conf)
|
||||||
|
_validate_pricing_rules(conf)
|
||||||
|
_strategy_settings(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_time_in_force(conf: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
|
time_in_force = conf.get('order_time_in_force', {})
|
||||||
|
if 'buy' in time_in_force or 'sell' in time_in_force:
|
||||||
|
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
||||||
|
raise OperationalException(
|
||||||
|
"Please migrate your time_in_force settings to use 'entry' and 'exit'.")
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"DEPRECATED: Using 'buy' and 'sell' for time_in_force is deprecated."
|
||||||
|
"Please migrate your time_in_force settings to use 'entry' and 'exit'."
|
||||||
|
)
|
||||||
|
process_deprecated_setting(
|
||||||
|
conf, 'order_time_in_force', 'buy', 'order_time_in_force', 'entry')
|
||||||
|
|
||||||
|
process_deprecated_setting(
|
||||||
|
conf, 'order_time_in_force', 'sell', 'order_time_in_force', 'exit')
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_order_types(conf: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
|
order_types = conf.get('order_types', {})
|
||||||
|
old_order_types = ['buy', 'sell', 'emergencysell', 'forcebuy',
|
||||||
|
'forcesell', 'emergencyexit', 'forceexit', 'forceentry']
|
||||||
|
if any(x in order_types for x in old_order_types):
|
||||||
|
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
||||||
|
raise OperationalException(
|
||||||
|
"Please migrate your order_types settings to use the new wording.")
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"DEPRECATED: Using 'buy' and 'sell' for order_types is deprecated."
|
||||||
|
"Please migrate your order_types settings to use 'entry' and 'exit' wording."
|
||||||
|
)
|
||||||
|
for o, n in [
|
||||||
|
('buy', 'entry'),
|
||||||
|
('sell', 'exit'),
|
||||||
|
('emergencysell', 'emergency_exit'),
|
||||||
|
('forcesell', 'force_exit'),
|
||||||
|
('forcebuy', 'force_entry'),
|
||||||
|
('emergencyexit', 'emergency_exit'),
|
||||||
|
('forceexit', 'force_exit'),
|
||||||
|
('forceentry', 'force_entry'),
|
||||||
|
]:
|
||||||
|
|
||||||
|
process_deprecated_setting(conf, 'order_types', o, 'order_types', n)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None:
|
||||||
|
unfilledtimeout = conf.get('unfilledtimeout', {})
|
||||||
|
if any(x in unfilledtimeout for x in ['buy', 'sell']):
|
||||||
|
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
||||||
|
raise OperationalException(
|
||||||
|
"Please migrate your unfilledtimeout settings to use the new wording.")
|
||||||
|
else:
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
"DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is deprecated."
|
||||||
|
"Please migrate your unfilledtimeout settings to use 'entry' and 'exit' wording."
|
||||||
|
)
|
||||||
|
for o, n in [
|
||||||
|
('buy', 'entry'),
|
||||||
|
('sell', 'exit'),
|
||||||
|
]:
|
||||||
|
|
||||||
|
process_deprecated_setting(conf, 'unfilledtimeout', o, 'unfilledtimeout', n)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_pricing_rules(conf: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
|
if conf.get('ask_strategy') or conf.get('bid_strategy'):
|
||||||
|
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
||||||
|
raise OperationalException(
|
||||||
|
"Please migrate your pricing settings to use the new wording.")
|
||||||
|
else:
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
"DEPRECATED: Using 'ask_strategy' and 'bid_strategy' is deprecated."
|
||||||
|
"Please migrate your settings to use 'entry_pricing' and 'exit_pricing'."
|
||||||
|
)
|
||||||
|
conf['entry_pricing'] = {}
|
||||||
|
for obj in list(conf.get('bid_strategy', {}).keys()):
|
||||||
|
if obj == 'ask_last_balance':
|
||||||
|
process_deprecated_setting(conf, 'bid_strategy', obj,
|
||||||
|
'entry_pricing', 'price_last_balance')
|
||||||
|
else:
|
||||||
|
process_deprecated_setting(conf, 'bid_strategy', obj, 'entry_pricing', obj)
|
||||||
|
del conf['bid_strategy']
|
||||||
|
|
||||||
|
conf['exit_pricing'] = {}
|
||||||
|
for obj in list(conf.get('ask_strategy', {}).keys()):
|
||||||
|
if obj == 'bid_last_balance':
|
||||||
|
process_deprecated_setting(conf, 'ask_strategy', obj,
|
||||||
|
'exit_pricing', 'price_last_balance')
|
||||||
|
else:
|
||||||
|
process_deprecated_setting(conf, 'ask_strategy', obj, 'exit_pricing', obj)
|
||||||
|
del conf['ask_strategy']
|
||||||
|
|
||||||
|
|
||||||
|
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, 'sell_profit_only', None, 'exit_profit_only')
|
||||||
|
process_deprecated_setting(conf, None, 'sell_profit_offset', None, 'exit_profit_offset')
|
||||||
|
process_deprecated_setting(conf, None, 'ignore_roi_if_buy_signal',
|
||||||
|
None, 'ignore_roi_if_entry_signal')
|
||||||
|
|
|
@ -12,8 +12,8 @@ from freqtrade.configuration.check_exchange import check_exchange
|
||||||
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
|
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
|
||||||
from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir
|
from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir
|
||||||
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_config_file, load_file
|
from freqtrade.configuration.load_config import load_file, load_from_files
|
||||||
from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, RunMode
|
from freqtrade.enums import NON_UTIL_MODES, TRADING_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
|
||||||
|
@ -55,47 +55,28 @@ class Configuration:
|
||||||
:param files: List of file paths
|
:param files: List of file paths
|
||||||
:return: configuration dictionary
|
:return: configuration dictionary
|
||||||
"""
|
"""
|
||||||
|
# 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_from_files(self, files: List[str]) -> Dict[str, Any]:
|
|
||||||
|
|
||||||
# Keep this method as staticmethod, so it can be used from interactive environments
|
|
||||||
config: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
if not files:
|
|
||||||
return deepcopy(constants.MINIMAL_CONFIG)
|
|
||||||
|
|
||||||
# We expect here a list of config filenames
|
|
||||||
for path in files:
|
|
||||||
logger.info(f'Using config: {path} ...')
|
|
||||||
|
|
||||||
# Merge config options, overwriting old values
|
|
||||||
config = deep_merge_dicts(load_config_file(path), config)
|
|
||||||
|
|
||||||
# Load environment variables
|
|
||||||
env_data = enironment_vars_to_dict()
|
|
||||||
config = deep_merge_dicts(env_data, config)
|
|
||||||
|
|
||||||
config['config_files'] = files
|
|
||||||
# Normalize config
|
|
||||||
if 'internals' not in config:
|
|
||||||
config['internals'] = {}
|
|
||||||
if 'ask_strategy' not in config:
|
|
||||||
config['ask_strategy'] = {}
|
|
||||||
|
|
||||||
if 'pairlists' not in config:
|
|
||||||
config['pairlists'] = []
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
def load_config(self) -> Dict[str, Any]:
|
def load_config(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Extract information for sys.argv and load the bot configuration
|
Extract information for sys.argv and load the bot configuration
|
||||||
:return: Configuration dictionary
|
:return: Configuration dictionary
|
||||||
"""
|
"""
|
||||||
# Load all configs
|
# Load all configs
|
||||||
config: Dict[str, Any] = self.load_from_files(self.args.get("config", []))
|
config: Dict[str, Any] = load_from_files(self.args.get("config", []))
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
env_data = enironment_vars_to_dict()
|
||||||
|
config = deep_merge_dicts(env_data, config)
|
||||||
|
|
||||||
|
# Normalize config
|
||||||
|
if 'internals' not in config:
|
||||||
|
config['internals'] = {}
|
||||||
|
|
||||||
|
if 'pairlists' not in config:
|
||||||
|
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)
|
||||||
|
@ -166,8 +147,8 @@ class Configuration:
|
||||||
config.update({'db_url': self.args['db_url']})
|
config.update({'db_url': self.args['db_url']})
|
||||||
logger.info('Parameter --db-url detected ...')
|
logger.info('Parameter --db-url detected ...')
|
||||||
|
|
||||||
if config.get('forcebuy_enable', False):
|
if config.get('force_entry_enable', False):
|
||||||
logger.warning('`forcebuy` 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']:
|
||||||
|
@ -245,6 +226,10 @@ class Configuration:
|
||||||
self._args_to_config(config, argname='timeframe_detail',
|
self._args_to_config(config, argname='timeframe_detail',
|
||||||
logstring='Parameter --timeframe-detail detected, '
|
logstring='Parameter --timeframe-detail detected, '
|
||||||
'using {} for intra-candle backtesting ...')
|
'using {} for intra-candle backtesting ...')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='backtest_show_pair_list',
|
||||||
|
logstring='Parameter --show-pair-list detected.')
|
||||||
|
|
||||||
self._args_to_config(config, argname='stake_amount',
|
self._args_to_config(config, argname='stake_amount',
|
||||||
logstring='Parameter --stake-amount detected, '
|
logstring='Parameter --stake-amount detected, '
|
||||||
'overriding stake_amount to: {} ...')
|
'overriding stake_amount to: {} ...')
|
||||||
|
@ -269,8 +254,15 @@ class Configuration:
|
||||||
self._args_to_config(config, argname='export',
|
self._args_to_config(config, argname='export',
|
||||||
logstring='Parameter --export detected: {} ...')
|
logstring='Parameter --export detected: {} ...')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='backtest_breakdown',
|
||||||
|
logstring='Parameter --breakdown detected ...')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='backtest_cache',
|
||||||
|
logstring='Parameter --cache={} detected ...')
|
||||||
|
|
||||||
self._args_to_config(config, argname='disableparamexport',
|
self._args_to_config(config, argname='disableparamexport',
|
||||||
logstring='Parameter --disableparamexport detected: {} ...')
|
logstring='Parameter --disableparamexport detected: {} ...')
|
||||||
|
|
||||||
# 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"])
|
||||||
|
@ -369,6 +361,9 @@ class Configuration:
|
||||||
self._args_to_config(config, argname='hyperopt_show_no_header',
|
self._args_to_config(config, argname='hyperopt_show_no_header',
|
||||||
logstring='Parameter --no-header detected: {}')
|
logstring='Parameter --no-header detected: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname="hyperopt_ignore_missing_space",
|
||||||
|
logstring="Paramter --ignore-missing-space detected: {}")
|
||||||
|
|
||||||
def _process_plot_options(self, config: Dict[str, Any]) -> None:
|
def _process_plot_options(self, config: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
self._args_to_config(config, argname='pairs',
|
self._args_to_config(config, argname='pairs',
|
||||||
|
@ -404,6 +399,9 @@ class Configuration:
|
||||||
self._args_to_config(config, argname='days',
|
self._args_to_config(config, argname='days',
|
||||||
logstring='Detected --days: {}')
|
logstring='Detected --days: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='include_inactive',
|
||||||
|
logstring='Detected --include-inactive-pairs: {}')
|
||||||
|
|
||||||
self._args_to_config(config, argname='download_trades',
|
self._args_to_config(config, argname='download_trades',
|
||||||
logstring='Detected --dl-trades: {}')
|
logstring='Detected --dl-trades: {}')
|
||||||
|
|
||||||
|
@ -414,9 +412,15 @@ class Configuration:
|
||||||
logstring='Using "{}" to store trades data.')
|
logstring='Using "{}" to store trades data.')
|
||||||
|
|
||||||
def _process_data_options(self, config: Dict[str, Any]) -> None:
|
def _process_data_options(self, config: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
self._args_to_config(config, argname='new_pairs_days',
|
self._args_to_config(config, argname='new_pairs_days',
|
||||||
logstring='Detected --new-pairs-days: {}')
|
logstring='Detected --new-pairs-days: {}')
|
||||||
|
self._args_to_config(config, argname='trading_mode',
|
||||||
|
logstring='Detected --trading-mode: {}')
|
||||||
|
config['candle_type_def'] = CandleType.get_default(
|
||||||
|
config.get('trading_mode', 'spot') or 'spot')
|
||||||
|
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_runmode(self, config: Dict[str, Any]) -> None:
|
def _process_runmode(self, config: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
|
|
|
@ -12,14 +12,15 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def check_conflicting_settings(config: Dict[str, Any],
|
def check_conflicting_settings(config: Dict[str, Any],
|
||||||
section_old: str, name_old: str,
|
section_old: Optional[str], name_old: str,
|
||||||
section_new: Optional[str], name_new: str) -> None:
|
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, {})
|
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:
|
||||||
new_name = f"{section_new}.{name_new}" if section_new else f"{name_new}"
|
new_name = f"{section_new}.{name_new}" if section_new else f"{name_new}"
|
||||||
|
old_name = f"{section_old}.{name_old}" if section_old else f"{name_old}"
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"Conflicting settings `{new_name}` and `{section_old}.{name_old}` "
|
f"Conflicting settings `{new_name}` and `{old_name}` "
|
||||||
"(DEPRECATED) detected in the configuration file. "
|
"(DEPRECATED) detected in the configuration file. "
|
||||||
"This deprecated setting will be removed in the next versions of Freqtrade. "
|
"This deprecated setting will be removed in the next versions of Freqtrade. "
|
||||||
f"Please delete it from your configuration and use the `{new_name}` "
|
f"Please delete it from your configuration and use the `{new_name}` "
|
||||||
|
@ -47,23 +48,25 @@ def process_removed_setting(config: Dict[str, Any],
|
||||||
|
|
||||||
|
|
||||||
def process_deprecated_setting(config: Dict[str, Any],
|
def process_deprecated_setting(config: Dict[str, Any],
|
||||||
section_old: str, name_old: str,
|
section_old: Optional[str], name_old: str,
|
||||||
section_new: Optional[str], name_new: 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, {})
|
section_old_config = config.get(section_old, {}) if section_old else config
|
||||||
|
|
||||||
if name_old in section_old_config:
|
if name_old in section_old_config:
|
||||||
|
section_1 = f"{section_old}.{name_old}" if section_old else f"{name_old}"
|
||||||
section_2 = f"{section_new}.{name_new}" if section_new else f"{name_new}"
|
section_2 = f"{section_new}.{name_new}" if section_new else f"{name_new}"
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"DEPRECATED: "
|
"DEPRECATED: "
|
||||||
f"The `{section_old}.{name_old}` setting is deprecated and "
|
f"The `{section_1}` setting is deprecated and "
|
||||||
"will be removed in the next versions of Freqtrade. "
|
"will be removed in the next versions of Freqtrade. "
|
||||||
f"Please use the `{section_2}` setting in your configuration instead."
|
f"Please use the `{section_2}` setting in your configuration instead."
|
||||||
)
|
)
|
||||||
|
|
||||||
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_new_config[name_new] = section_old_config[name_old]
|
section_new_config[name_new] = section_old_config[name_old]
|
||||||
|
del section_old_config[name_old]
|
||||||
|
|
||||||
|
|
||||||
def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
||||||
|
@ -71,25 +74,51 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> 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', 'use_sell_signal',
|
|
||||||
None, 'use_sell_signal')
|
|
||||||
process_deprecated_setting(config, 'ask_strategy', 'sell_profit_only',
|
|
||||||
None, 'sell_profit_only')
|
|
||||||
process_deprecated_setting(config, 'ask_strategy', 'sell_profit_offset',
|
|
||||||
None, 'sell_profit_offset')
|
|
||||||
process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal',
|
|
||||||
None, 'ignore_roi_if_buy_signal')
|
|
||||||
process_deprecated_setting(config, 'ask_strategy', 'ignore_buying_expired_candle_after',
|
process_deprecated_setting(config, 'ask_strategy', 'ignore_buying_expired_candle_after',
|
||||||
None, 'ignore_buying_expired_candle_after')
|
None, 'ignore_buying_expired_candle_after')
|
||||||
|
|
||||||
# Legacy way - having them in experimental ...
|
process_deprecated_setting(config, None, 'forcebuy_enable', None, 'force_entry_enable')
|
||||||
process_removed_setting(config, 'experimental', 'use_sell_signal',
|
|
||||||
None, 'use_sell_signal')
|
|
||||||
process_removed_setting(config, 'experimental', 'sell_profit_only',
|
|
||||||
None, 'sell_profit_only')
|
|
||||||
process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal',
|
|
||||||
None, 'ignore_roi_if_buy_signal')
|
|
||||||
|
|
||||||
|
# New settings
|
||||||
|
if config.get('telegram'):
|
||||||
|
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell',
|
||||||
|
'notification_settings', 'exit')
|
||||||
|
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell_fill',
|
||||||
|
'notification_settings', 'exit_fill')
|
||||||
|
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell_cancel',
|
||||||
|
'notification_settings', 'exit_cancel')
|
||||||
|
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy',
|
||||||
|
'notification_settings', 'entry')
|
||||||
|
process_deprecated_setting(config['telegram'], '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 ...
|
||||||
|
|
||||||
|
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', 'ignore_roi_if_buy_signal',
|
||||||
|
None, 'ignore_roi_if_entry_signal')
|
||||||
|
|
||||||
|
process_removed_setting(config, 'ask_strategy', 'use_sell_signal', None, 'exit_sell_signal')
|
||||||
|
process_removed_setting(config, 'ask_strategy', 'sell_profit_only', None, 'exit_profit_only')
|
||||||
|
process_removed_setting(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')
|
||||||
if (config.get('edge', {}).get('enabled', False)
|
if (config.get('edge', {}).get('enabled', False)
|
||||||
and 'capital_available_percentage' in config.get('edge', {})):
|
and 'capital_available_percentage' in config.get('edge', {})):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
@ -100,16 +129,11 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
||||||
"from the edge configuration."
|
"from the edge configuration."
|
||||||
)
|
)
|
||||||
if 'ticker_interval' in config:
|
if 'ticker_interval' in config:
|
||||||
logger.warning(
|
|
||||||
"DEPRECATED: "
|
raise OperationalException(
|
||||||
|
"DEPRECATED: 'ticker_interval' detected. "
|
||||||
"Please use 'timeframe' instead of 'ticker_interval."
|
"Please use 'timeframe' instead of 'ticker_interval."
|
||||||
)
|
)
|
||||||
if 'timeframe' in config:
|
|
||||||
raise OperationalException(
|
|
||||||
"Both 'timeframe' and 'ticker_interval' detected."
|
|
||||||
"Please remove 'ticker_interval' from your configuration to continue operating."
|
|
||||||
)
|
|
||||||
config['timeframe'] = config['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.")
|
||||||
|
|
|
@ -32,6 +32,7 @@ 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']
|
||||||
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()):
|
||||||
|
@ -39,9 +40,9 @@ def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str,
|
||||||
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) if type(val) != dict else val}
|
val = {k.lower(): get_var_typed(val)
|
||||||
|
if type(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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,15 @@ This module contain functions to load the configuration file
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import rapidjson
|
import rapidjson
|
||||||
|
|
||||||
|
from freqtrade.constants import MINIMAL_CONFIG
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.misc import deep_merge_dicts
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -28,7 +31,7 @@ def log_config_error_range(path: str, errmsg: str) -> str:
|
||||||
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
|
||||||
|
@ -70,3 +73,43 @@ def load_config_file(path: str) -> Dict[str, Any]:
|
||||||
)
|
)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def load_from_files(files: List[str], base_path: Path = None, level: int = 0) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Recursively load configuration files if specified.
|
||||||
|
Sub-files are assumed to be relative to the initial config.
|
||||||
|
"""
|
||||||
|
config: Dict[str, Any] = {}
|
||||||
|
if level > 5:
|
||||||
|
raise OperationalException("Config loop detected.")
|
||||||
|
|
||||||
|
if not files:
|
||||||
|
return deepcopy(MINIMAL_CONFIG)
|
||||||
|
files_loaded = []
|
||||||
|
# We expect here a list of config filenames
|
||||||
|
for filename in files:
|
||||||
|
logger.info(f'Using config: {filename} ...')
|
||||||
|
if filename == '-':
|
||||||
|
# Immediately load stdin and return
|
||||||
|
return load_config_file(filename)
|
||||||
|
file = Path(filename)
|
||||||
|
if base_path:
|
||||||
|
# Prepend basepath to allow for relative assignments
|
||||||
|
file = base_path / file
|
||||||
|
|
||||||
|
config_tmp = load_config_file(str(file))
|
||||||
|
if 'add_config_files' in config_tmp:
|
||||||
|
config_sub = load_from_files(
|
||||||
|
config_tmp['add_config_files'], file.resolve().parent, level + 1)
|
||||||
|
files_loaded.extend(config_sub.get('config_files', []))
|
||||||
|
config_tmp = deep_merge_dicts(config_tmp, config_sub)
|
||||||
|
|
||||||
|
files_loaded.insert(0, str(file))
|
||||||
|
|
||||||
|
# Merge config options, overwriting prior values
|
||||||
|
config = deep_merge_dicts(config_tmp, config)
|
||||||
|
|
||||||
|
config['config_files'] = files_loaded
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
"""
|
"""
|
||||||
bot constants
|
bot constants
|
||||||
"""
|
"""
|
||||||
from typing import List, Tuple
|
from typing import List, Literal, Tuple
|
||||||
|
|
||||||
|
from freqtrade.enums import CandleType
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = 'config.json'
|
DEFAULT_CONFIG = 'config.json'
|
||||||
|
@ -17,20 +19,25 @@ DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||||
DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite'
|
DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite'
|
||||||
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
||||||
DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05
|
DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05
|
||||||
REQUIRED_ORDERTIF = ['buy', 'sell']
|
REQUIRED_ORDERTIF = ['entry', 'exit']
|
||||||
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
REQUIRED_ORDERTYPES = ['entry', 'exit', 'stoploss', 'stoploss_on_exchange']
|
||||||
ORDERBOOK_SIDES = ['ask', 'bid']
|
PRICING_SIDES = ['ask', 'bid', 'same', 'other']
|
||||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||||
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||||
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
||||||
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
||||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily']
|
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
|
||||||
|
'CalmarHyperOptLoss',
|
||||||
|
'MaxDrawDownHyperOptLoss', 'ProfitDrawDownHyperOptLoss']
|
||||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||||
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
||||||
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
||||||
AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
||||||
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5']
|
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5']
|
||||||
|
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
||||||
|
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
||||||
|
BACKTEST_CACHE_DEFAULT = 'day'
|
||||||
DRY_RUN_WALLET = 1000
|
DRY_RUN_WALLET = 1000
|
||||||
DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S'
|
DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||||
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
|
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
|
||||||
|
@ -38,6 +45,8 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||||
# Don't modify sequence of DEFAULT_TRADES_COLUMNS
|
# Don't modify sequence of DEFAULT_TRADES_COLUMNS
|
||||||
# it has wide consequences for stored trades files
|
# it has wide consequences for stored trades files
|
||||||
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
|
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
|
||||||
|
TRADING_MODES = ['spot', 'margin', 'futures']
|
||||||
|
MARGIN_MODES = ['cross', 'isolated', '']
|
||||||
|
|
||||||
LAST_BT_RESULT_FN = '.last_result.json'
|
LAST_BT_RESULT_FN = '.last_result.json'
|
||||||
FTHYPT_FILEVERSION = 'fthypt_fileversion'
|
FTHYPT_FILEVERSION = 'fthypt_fileversion'
|
||||||
|
@ -47,11 +56,12 @@ USERPATH_STRATEGIES = 'strategies'
|
||||||
USERPATH_NOTEBOOKS = 'notebooks'
|
USERPATH_NOTEBOOKS = 'notebooks'
|
||||||
|
|
||||||
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
||||||
|
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
|
||||||
|
|
||||||
ENV_VAR_PREFIX = 'FREQTRADE__'
|
ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||||
|
|
||||||
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired')
|
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired')
|
||||||
|
|
||||||
|
|
||||||
# Define decimals per coin for outputs
|
# Define decimals per coin for outputs
|
||||||
# Only used for outputs.
|
# Only used for outputs.
|
||||||
DECIMAL_PER_COIN_FALLBACK = 3 # Should be low to avoid listing all possible FIAT's
|
DECIMAL_PER_COIN_FALLBACK = 3 # Should be low to avoid listing all possible FIAT's
|
||||||
|
@ -65,7 +75,6 @@ DUST_PER_COIN = {
|
||||||
'ETH': 0.01
|
'ETH': 0.01
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Source files with destination directories within user-directory
|
# Source files with destination directories within user-directory
|
||||||
USER_DATA_FILES = {
|
USER_DATA_FILES = {
|
||||||
'sample_strategy.py': USERPATH_STRATEGIES,
|
'sample_strategy.py': USERPATH_STRATEGIES,
|
||||||
|
@ -77,20 +86,19 @@ SUPPORTED_FIAT = [
|
||||||
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
|
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
|
||||||
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
||||||
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
||||||
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD",
|
"RUB", "UAH", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR",
|
||||||
"BTC", "ETH", "XRP", "LTC", "BCH"
|
"USD", "BTC", "ETH", "XRP", "LTC", "BCH"
|
||||||
]
|
]
|
||||||
|
|
||||||
MINIMAL_CONFIG = {
|
MINIMAL_CONFIG = {
|
||||||
'stake_currency': '',
|
"stake_currency": "",
|
||||||
'dry_run': True,
|
"dry_run": True,
|
||||||
'exchange': {
|
"exchange": {
|
||||||
'name': '',
|
"name": "",
|
||||||
'key': '',
|
"key": "",
|
||||||
'secret': '',
|
"secret": "",
|
||||||
'pair_whitelist': [],
|
"pair_whitelist": [],
|
||||||
'ccxt_async_config': {
|
"ccxt_async_config": {
|
||||||
'enableRateLimit': True,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,35 +143,43 @@ CONF_SCHEMA = {
|
||||||
'minProperties': 1
|
'minProperties': 1
|
||||||
},
|
},
|
||||||
'amount_reserve_percent': {'type': 'number', 'minimum': 0.0, 'maximum': 0.5},
|
'amount_reserve_percent': {'type': 'number', 'minimum': 0.0, 'maximum': 0.5},
|
||||||
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True, 'minimum': -1},
|
||||||
'trailing_stop': {'type': 'boolean'},
|
'trailing_stop': {'type': 'boolean'},
|
||||||
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||||
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||||
'trailing_only_offset_is_reached': {'type': 'boolean'},
|
'trailing_only_offset_is_reached': {'type': 'boolean'},
|
||||||
'use_sell_signal': {'type': 'boolean'},
|
'use_exit_signal': {'type': 'boolean'},
|
||||||
'sell_profit_only': {'type': 'boolean'},
|
'exit_profit_only': {'type': 'boolean'},
|
||||||
'sell_profit_offset': {'type': 'number'},
|
'exit_profit_offset': {'type': 'number'},
|
||||||
'ignore_roi_if_buy_signal': {'type': 'boolean'},
|
'ignore_roi_if_entry_signal': {'type': 'boolean'},
|
||||||
'ignore_buying_expired_candle_after': {'type': 'number'},
|
'ignore_buying_expired_candle_after': {'type': 'number'},
|
||||||
|
'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
|
||||||
|
'margin_mode': {'type': 'string', 'enum': MARGIN_MODES},
|
||||||
|
'liquidation_buffer': {'type': 'number', 'minimum': 0.0, 'maximum': 0.99},
|
||||||
|
'backtest_breakdown': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS}
|
||||||
|
},
|
||||||
'bot_name': {'type': 'string'},
|
'bot_name': {'type': 'string'},
|
||||||
'unfilledtimeout': {
|
'unfilledtimeout': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'buy': {'type': 'number', 'minimum': 1},
|
'entry': {'type': 'number', 'minimum': 1},
|
||||||
'sell': {'type': 'number', 'minimum': 1},
|
'exit': {'type': 'number', 'minimum': 1},
|
||||||
|
'exit_timeout_count': {'type': 'number', 'minimum': 0, 'default': 0},
|
||||||
'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'}
|
'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'bid_strategy': {
|
'entry_pricing': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'ask_last_balance': {
|
'price_last_balance': {
|
||||||
'type': 'number',
|
'type': 'number',
|
||||||
'minimum': 0,
|
'minimum': 0,
|
||||||
'maximum': 1,
|
'maximum': 1,
|
||||||
'exclusiveMaximum': False,
|
'exclusiveMaximum': False,
|
||||||
},
|
},
|
||||||
'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'bid'},
|
'price_side': {'type': 'string', 'enum': PRICING_SIDES, 'default': 'same'},
|
||||||
'use_order_book': {'type': 'boolean'},
|
'use_order_book': {'type': 'boolean'},
|
||||||
'order_book_top': {'type': 'integer', 'minimum': 1, 'maximum': 50, },
|
'order_book_top': {'type': 'integer', 'minimum': 1, 'maximum': 50, },
|
||||||
'check_depth_of_market': {
|
'check_depth_of_market': {
|
||||||
|
@ -176,11 +192,11 @@ CONF_SCHEMA = {
|
||||||
},
|
},
|
||||||
'required': ['price_side']
|
'required': ['price_side']
|
||||||
},
|
},
|
||||||
'ask_strategy': {
|
'exit_pricing': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'ask'},
|
'price_side': {'type': 'string', 'enum': PRICING_SIDES, 'default': 'same'},
|
||||||
'bid_last_balance': {
|
'price_last_balance': {
|
||||||
'type': 'number',
|
'type': 'number',
|
||||||
'minimum': 0,
|
'minimum': 0,
|
||||||
'maximum': 1,
|
'maximum': 1,
|
||||||
|
@ -192,31 +208,34 @@ CONF_SCHEMA = {
|
||||||
'required': ['price_side']
|
'required': ['price_side']
|
||||||
},
|
},
|
||||||
'custom_price_max_distance_ratio': {
|
'custom_price_max_distance_ratio': {
|
||||||
'type': 'number', 'minimum': 0.0
|
'type': 'number', 'minimum': 0.0
|
||||||
},
|
},
|
||||||
'order_types': {
|
'order_types': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'entry': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'exit': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'forcesell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'force_exit': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'forcebuy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'force_entry': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'emergency_exit': {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': ORDERTYPE_POSSIBILITIES,
|
||||||
|
'default': 'market'},
|
||||||
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'stoploss_on_exchange': {'type': 'boolean'},
|
'stoploss_on_exchange': {'type': 'boolean'},
|
||||||
'stoploss_on_exchange_interval': {'type': 'number'},
|
'stoploss_on_exchange_interval': {'type': 'number'},
|
||||||
'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0,
|
'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0,
|
||||||
'maximum': 1.0}
|
'maximum': 1.0}
|
||||||
},
|
},
|
||||||
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
'required': ['entry', 'exit', 'stoploss', 'stoploss_on_exchange']
|
||||||
},
|
},
|
||||||
'order_time_in_force': {
|
'order_time_in_force': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'buy': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES},
|
'entry': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES},
|
||||||
'sell': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES}
|
'exit': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES}
|
||||||
},
|
},
|
||||||
'required': ['buy', 'sell']
|
'required': REQUIRED_ORDERTIF
|
||||||
},
|
},
|
||||||
'exchange': {'$ref': '#/definitions/exchange'},
|
'exchange': {'$ref': '#/definitions/exchange'},
|
||||||
'edge': {'$ref': '#/definitions/edge'},
|
'edge': {'$ref': '#/definitions/edge'},
|
||||||
|
@ -265,21 +284,21 @@ CONF_SCHEMA = {
|
||||||
'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||||
'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||||
'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||||
'buy': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
'entry': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||||
'buy_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
'entry_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||||
'buy_fill': {'type': 'string',
|
'entry_fill': {'type': 'string',
|
||||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||||
'default': 'off'
|
'default': 'off'
|
||||||
},
|
},
|
||||||
'sell': {
|
'exit': {
|
||||||
'type': ['string', 'object'],
|
'type': ['string', 'object'],
|
||||||
'additionalProperties': {
|
'additionalProperties': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'enum': TELEGRAM_SETTING_OPTIONS
|
'enum': TELEGRAM_SETTING_OPTIONS
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
'exit_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||||
'sell_fill': {
|
'exit_fill': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||||
'default': 'off'
|
'default': 'off'
|
||||||
|
@ -303,10 +322,16 @@ CONF_SCHEMA = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'enabled': {'type': 'boolean'},
|
'enabled': {'type': 'boolean'},
|
||||||
'webhookbuy': {'type': 'object'},
|
'url': {'type': 'string'},
|
||||||
'webhookbuycancel': {'type': 'object'},
|
'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'},
|
||||||
'webhooksell': {'type': 'object'},
|
'retries': {'type': 'integer', 'minimum': 0},
|
||||||
'webhooksellcancel': {'type': 'object'},
|
'retry_delay': {'type': 'number', 'minimum': 0},
|
||||||
|
'webhookentry': {'type': 'object'},
|
||||||
|
'webhookentrycancel': {'type': 'object'},
|
||||||
|
'webhookentryfill': {'type': 'object'},
|
||||||
|
'webhookexit': {'type': 'object'},
|
||||||
|
'webhookexitcancel': {'type': 'object'},
|
||||||
|
'webhookexitfill': {'type': 'object'},
|
||||||
'webhookstatus': {'type': 'object'},
|
'webhookstatus': {'type': 'object'},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -332,7 +357,7 @@ CONF_SCHEMA = {
|
||||||
'export': {'type': 'string', 'enum': EXPORT_OPTIONS, 'default': 'trades'},
|
'export': {'type': 'string', 'enum': EXPORT_OPTIONS, 'default': 'trades'},
|
||||||
'disableparamexport': {'type': 'boolean'},
|
'disableparamexport': {'type': 'boolean'},
|
||||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||||
'forcebuy_enable': {'type': 'boolean'},
|
'force_entry_enable': {'type': 'boolean'},
|
||||||
'disable_dataframe_checks': {'type': 'boolean'},
|
'disable_dataframe_checks': {'type': 'boolean'},
|
||||||
'internals': {
|
'internals': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
|
@ -345,14 +370,16 @@ CONF_SCHEMA = {
|
||||||
},
|
},
|
||||||
'dataformat_ohlcv': {
|
'dataformat_ohlcv': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'enum': AVAILABLE_DATAHANDLERS,
|
'enum': AVAILABLE_DATAHANDLERS,
|
||||||
'default': 'json'
|
'default': 'json'
|
||||||
},
|
},
|
||||||
'dataformat_trades': {
|
'dataformat_trades': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'enum': AVAILABLE_DATAHANDLERS,
|
'enum': AVAILABLE_DATAHANDLERS,
|
||||||
'default': 'jsongz'
|
'default': 'jsongz'
|
||||||
}
|
},
|
||||||
|
'position_adjustment_enable': {'type': 'boolean'},
|
||||||
|
'max_entry_position_adjustment': {'type': ['integer', 'number'], 'minimum': -1},
|
||||||
},
|
},
|
||||||
'definitions': {
|
'definitions': {
|
||||||
'exchange': {
|
'exchange': {
|
||||||
|
@ -378,6 +405,7 @@ CONF_SCHEMA = {
|
||||||
},
|
},
|
||||||
'uniqueItems': True
|
'uniqueItems': True
|
||||||
},
|
},
|
||||||
|
'unknown_fee_rate': {'type': 'number'},
|
||||||
'outdated_offset': {'type': 'integer', 'minimum': 1},
|
'outdated_offset': {'type': 'integer', 'minimum': 1},
|
||||||
'markets_refresh_interval': {'type': 'integer'},
|
'markets_refresh_interval': {'type': 'integer'},
|
||||||
'ccxt_config': {'type': 'object'},
|
'ccxt_config': {'type': 'object'},
|
||||||
|
@ -416,9 +444,8 @@ SCHEMA_TRADE_REQUIRED = [
|
||||||
'last_stake_amount_min_ratio',
|
'last_stake_amount_min_ratio',
|
||||||
'dry_run',
|
'dry_run',
|
||||||
'dry_run_wallet',
|
'dry_run_wallet',
|
||||||
'ask_strategy',
|
'exit_pricing',
|
||||||
'bid_strategy',
|
'entry_pricing',
|
||||||
'unfilledtimeout',
|
|
||||||
'stoploss',
|
'stoploss',
|
||||||
'minimal_roi',
|
'minimal_roi',
|
||||||
'internals',
|
'internals',
|
||||||
|
@ -450,12 +477,15 @@ CANCEL_REASON = {
|
||||||
"FULLY_CANCELLED": "fully cancelled",
|
"FULLY_CANCELLED": "fully cancelled",
|
||||||
"ALL_CANCELLED": "cancelled (all unfilled and partially filled open orders cancelled)",
|
"ALL_CANCELLED": "cancelled (all unfilled and partially filled open orders cancelled)",
|
||||||
"CANCELLED_ON_EXCHANGE": "cancelled on exchange",
|
"CANCELLED_ON_EXCHANGE": "cancelled on exchange",
|
||||||
"FORCE_SELL": "forcesold",
|
"FORCE_EXIT": "forcesold",
|
||||||
}
|
}
|
||||||
|
|
||||||
# List of pairs with their timeframes
|
# List of pairs with their timeframes
|
||||||
PairWithTimeframe = Tuple[str, str]
|
PairWithTimeframe = Tuple[str, str, CandleType]
|
||||||
ListPairsWithTimeframes = List[PairWithTimeframe]
|
ListPairsWithTimeframes = List[PairWithTimeframe]
|
||||||
|
|
||||||
# Type for trades list
|
# Type for trades list
|
||||||
TradeList = List[List]
|
TradeList = List[List]
|
||||||
|
|
||||||
|
LongShort = Literal['long', 'short']
|
||||||
|
EntryExit = Literal['entry', 'exit']
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
Helpers when analyzing backtest data
|
Helpers when analyzing backtest data
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from copy import copy
|
||||||
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
@ -9,28 +11,22 @@ import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||||
from freqtrade.misc import json_load
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.misc import get_backtest_metadata_filename, json_load
|
||||||
from freqtrade.persistence import LocalTrade, Trade, init_db
|
from freqtrade.persistence import LocalTrade, Trade, init_db
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Old format - maybe remove?
|
|
||||||
BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index",
|
|
||||||
"trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"]
|
|
||||||
|
|
||||||
# Mid-term format, created by BacktestResult Named Tuple
|
|
||||||
BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'trade_duration',
|
|
||||||
'open_rate', 'close_rate', 'open_at_end', 'sell_reason', 'fee_open',
|
|
||||||
'fee_close', 'amount', 'profit_abs', 'profit_ratio']
|
|
||||||
|
|
||||||
# Newest format
|
# Newest format
|
||||||
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
|
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
|
||||||
'open_rate', 'close_rate',
|
'open_rate', 'close_rate',
|
||||||
'fee_open', 'fee_close', 'trade_duration',
|
'fee_open', 'fee_close', 'trade_duration',
|
||||||
'profit_ratio', 'profit_abs', 'sell_reason',
|
'profit_ratio', 'profit_abs', 'exit_reason',
|
||||||
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
||||||
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag']
|
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag',
|
||||||
|
'is_short'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:
|
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:
|
||||||
|
@ -106,10 +102,30 @@ def get_latest_hyperopt_file(directory: Union[Path, str], predef_filename: str =
|
||||||
if isinstance(directory, str):
|
if isinstance(directory, str):
|
||||||
directory = Path(directory)
|
directory = Path(directory)
|
||||||
if predef_filename:
|
if predef_filename:
|
||||||
|
if Path(predef_filename).is_absolute():
|
||||||
|
raise OperationalException(
|
||||||
|
"--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)
|
||||||
|
|
||||||
|
|
||||||
|
def load_backtest_metadata(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Read metadata dictionary from backtest results file without reading and deserializing entire
|
||||||
|
file.
|
||||||
|
:param filename: path to backtest results file.
|
||||||
|
:return: metadata dict or None if metadata is not present.
|
||||||
|
"""
|
||||||
|
filename = get_backtest_metadata_filename(filename)
|
||||||
|
try:
|
||||||
|
with filename.open() as fp:
|
||||||
|
return json_load(fp)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {}
|
||||||
|
except Exception as e:
|
||||||
|
raise OperationalException('Unexpected error while loading backtest metadata.') from e
|
||||||
|
|
||||||
|
|
||||||
def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
|
def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Load backtest statistics file.
|
Load backtest statistics file.
|
||||||
|
@ -126,9 +142,104 @@ def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||||
with filename.open() as file:
|
with filename.open() as file:
|
||||||
data = json_load(file)
|
data = json_load(file)
|
||||||
|
|
||||||
|
# Legacy list format does not contain metadata.
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data['metadata'] = load_backtest_metadata(filename)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def load_and_merge_backtest_result(strategy_name: str, filename: Path, results: Dict[str, Any]):
|
||||||
|
"""
|
||||||
|
Load one strategy from multi-strategy result
|
||||||
|
and merge it with results
|
||||||
|
:param strategy_name: Name of the strategy contained in the result
|
||||||
|
:param filename: Backtest-result-filename to load
|
||||||
|
:param results: dict to merge the result to.
|
||||||
|
"""
|
||||||
|
bt_data = load_backtest_stats(filename)
|
||||||
|
for k in ('metadata', 'strategy'):
|
||||||
|
results[k][strategy_name] = bt_data[k][strategy_name]
|
||||||
|
comparison = bt_data['strategy_comparison']
|
||||||
|
for i in range(len(comparison)):
|
||||||
|
if comparison[i]['key'] == strategy_name:
|
||||||
|
results['strategy_comparison'].append(comparison[i])
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def _get_backtest_files(dirname: Path) -> List[Path]:
|
||||||
|
return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))))
|
||||||
|
|
||||||
|
|
||||||
|
def get_backtest_resultlist(dirname: Path):
|
||||||
|
"""
|
||||||
|
Get list of backtest results read from metadata files
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for filename in _get_backtest_files(dirname):
|
||||||
|
metadata = load_backtest_metadata(filename)
|
||||||
|
if not metadata:
|
||||||
|
continue
|
||||||
|
for s, v in metadata.items():
|
||||||
|
results.append({
|
||||||
|
'filename': filename.name,
|
||||||
|
'strategy': s,
|
||||||
|
'run_id': v['run_id'],
|
||||||
|
'backtest_start_time': v['backtest_start_time'],
|
||||||
|
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
|
||||||
|
min_backtest_date: datetime = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Find existing backtest stats that match specified run IDs and load them.
|
||||||
|
:param dirname: pathlib.Path object, or string pointing to the file.
|
||||||
|
:param run_ids: {strategy_name: id_string} dictionary.
|
||||||
|
:param min_backtest_date: do not load a backtest older than specified date.
|
||||||
|
:return: results dict.
|
||||||
|
"""
|
||||||
|
# Copy so we can modify this dict without affecting parent scope.
|
||||||
|
run_ids = copy(run_ids)
|
||||||
|
dirname = Path(dirname)
|
||||||
|
results: Dict[str, Any] = {
|
||||||
|
'metadata': {},
|
||||||
|
'strategy': {},
|
||||||
|
'strategy_comparison': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Weird glob expression here avoids including .meta.json files.
|
||||||
|
for filename in _get_backtest_files(dirname):
|
||||||
|
metadata = load_backtest_metadata(filename)
|
||||||
|
if not metadata:
|
||||||
|
# Files are sorted from newest to oldest. When file without metadata is encountered it
|
||||||
|
# is safe to assume older files will also not have any metadata.
|
||||||
|
break
|
||||||
|
|
||||||
|
for strategy_name, run_id in list(run_ids.items()):
|
||||||
|
strategy_metadata = metadata.get(strategy_name, None)
|
||||||
|
if not strategy_metadata:
|
||||||
|
# This strategy is not present in analyzed backtest.
|
||||||
|
continue
|
||||||
|
|
||||||
|
if min_backtest_date is not None:
|
||||||
|
backtest_date = strategy_metadata['backtest_start_time']
|
||||||
|
backtest_date = datetime.fromtimestamp(backtest_date, tz=timezone.utc)
|
||||||
|
if backtest_date < min_backtest_date:
|
||||||
|
# Do not use a cached result for this strategy as first result is too old.
|
||||||
|
del run_ids[strategy_name]
|
||||||
|
continue
|
||||||
|
|
||||||
|
if strategy_metadata['run_id'] == run_id:
|
||||||
|
del run_ids[strategy_name]
|
||||||
|
load_and_merge_backtest_result(strategy_name, filename, results)
|
||||||
|
|
||||||
|
if len(run_ids) == 0:
|
||||||
|
break
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame:
|
def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Load backtest data file.
|
Load backtest data file.
|
||||||
|
@ -165,25 +276,18 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
|
||||||
utc=True,
|
utc=True,
|
||||||
infer_datetime_format=True
|
infer_datetime_format=True
|
||||||
)
|
)
|
||||||
|
# Compatibility support for pre short Columns
|
||||||
|
if 'is_short' not in df.columns:
|
||||||
|
df['is_short'] = 0
|
||||||
|
if 'enter_tag' not in df.columns:
|
||||||
|
df['enter_tag'] = df['buy_tag']
|
||||||
|
df = df.drop(['buy_tag'], axis=1)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# old format - only with lists.
|
# old format - only with lists.
|
||||||
df = pd.DataFrame(data, columns=BT_DATA_COLUMNS_OLD)
|
raise OperationalException(
|
||||||
if not df.empty:
|
"Backtest-results with only trades data are no longer supported.")
|
||||||
df['open_date'] = pd.to_datetime(df['open_date'],
|
|
||||||
unit='s',
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True
|
|
||||||
)
|
|
||||||
df['close_date'] = pd.to_datetime(df['close_date'],
|
|
||||||
unit='s',
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True
|
|
||||||
)
|
|
||||||
# Create compatibility with new format
|
|
||||||
df['profit_abs'] = df['close_rate'] - df['open_rate']
|
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
if 'profit_ratio' not in df.columns:
|
|
||||||
df['profit_ratio'] = df['profit_percent']
|
|
||||||
df = df.sort_values("open_date").reset_index(drop=True)
|
df = df.sort_values("open_date").reset_index(drop=True)
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
@ -325,6 +429,7 @@ def combine_dataframes_with_mean(data: Dict[str, pd.DataFrame],
|
||||||
:param column: Column in the original dataframes to use
|
:param column: Column in the original dataframes to use
|
||||||
:return: DataFrame with the column renamed to the dict key, and a column
|
:return: DataFrame with the column renamed to the dict key, and a column
|
||||||
named mean, containing the mean of all pairs.
|
named mean, containing the mean of all pairs.
|
||||||
|
:raise: ValueError if no data is provided.
|
||||||
"""
|
"""
|
||||||
df_comb = pd.concat([data[pair].set_index('date').rename(
|
df_comb = pd.concat([data[pair].set_index('date').rename(
|
||||||
{column: pair}, axis=1)[pair] for pair in data], axis=1)
|
{column: pair}, axis=1)[pair] for pair in data], axis=1)
|
||||||
|
@ -360,9 +465,19 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_col: str
|
||||||
value_col: str = 'profit_ratio'
|
) -> pd.DataFrame:
|
||||||
) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float]:
|
max_drawdown_df = pd.DataFrame()
|
||||||
|
max_drawdown_df['cumulative'] = profit_results[value_col].cumsum()
|
||||||
|
max_drawdown_df['high_value'] = max_drawdown_df['cumulative'].cummax()
|
||||||
|
max_drawdown_df['drawdown'] = max_drawdown_df['cumulative'] - max_drawdown_df['high_value']
|
||||||
|
max_drawdown_df['date'] = profit_results.loc[:, date_col]
|
||||||
|
return max_drawdown_df
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
||||||
|
value_col: str = 'profit_ratio'
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Calculate max drawdown and the corresponding close dates
|
Calculate max drawdown and the corresponding close dates
|
||||||
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
|
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
|
||||||
|
@ -375,10 +490,29 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date'
|
||||||
if len(trades) == 0:
|
if len(trades) == 0:
|
||||||
raise ValueError("Trade dataframe empty.")
|
raise ValueError("Trade dataframe empty.")
|
||||||
profit_results = trades.sort_values(date_col).reset_index(drop=True)
|
profit_results = trades.sort_values(date_col).reset_index(drop=True)
|
||||||
max_drawdown_df = pd.DataFrame()
|
max_drawdown_df = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col)
|
||||||
max_drawdown_df['cumulative'] = profit_results[value_col].cumsum()
|
|
||||||
max_drawdown_df['high_value'] = max_drawdown_df['cumulative'].cummax()
|
return max_drawdown_df
|
||||||
max_drawdown_df['drawdown'] = max_drawdown_df['cumulative'] - max_drawdown_df['high_value']
|
|
||||||
|
|
||||||
|
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
||||||
|
value_col: str = 'profit_abs', starting_balance: float = 0
|
||||||
|
) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float, float]:
|
||||||
|
"""
|
||||||
|
Calculate max drawdown and the corresponding close dates
|
||||||
|
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
|
||||||
|
:param date_col: Column in DataFrame to use for dates (defaults to 'close_date')
|
||||||
|
:param value_col: Column in DataFrame to use for values (defaults to 'profit_abs')
|
||||||
|
:param starting_balance: Portfolio starting balance - properly calculate relative drawdown.
|
||||||
|
:return: Tuple (float, highdate, lowdate, highvalue, lowvalue, relative_drawdown)
|
||||||
|
with absolute max drawdown, high and low time and high and low value,
|
||||||
|
and the relative account drawdown
|
||||||
|
:raise: ValueError if trade-dataframe was found empty.
|
||||||
|
"""
|
||||||
|
if len(trades) == 0:
|
||||||
|
raise ValueError("Trade dataframe empty.")
|
||||||
|
profit_results = trades.sort_values(date_col).reset_index(drop=True)
|
||||||
|
max_drawdown_df = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col)
|
||||||
|
|
||||||
idxmin = max_drawdown_df['drawdown'].idxmin()
|
idxmin = max_drawdown_df['drawdown'].idxmin()
|
||||||
if idxmin == 0:
|
if idxmin == 0:
|
||||||
|
@ -388,7 +522,18 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date'
|
||||||
high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin]
|
high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin]
|
||||||
['high_value'].idxmax(), 'cumulative']
|
['high_value'].idxmax(), 'cumulative']
|
||||||
low_val = max_drawdown_df.loc[idxmin, 'cumulative']
|
low_val = max_drawdown_df.loc[idxmin, 'cumulative']
|
||||||
return abs(min(max_drawdown_df['drawdown'])), high_date, low_date, high_val, low_val
|
max_drawdown_rel = 0.0
|
||||||
|
if high_val + starting_balance != 0:
|
||||||
|
max_drawdown_rel = (high_val - low_val) / (high_val + starting_balance)
|
||||||
|
|
||||||
|
return (
|
||||||
|
abs(min(max_drawdown_df['drawdown'])),
|
||||||
|
high_date,
|
||||||
|
low_date,
|
||||||
|
high_val,
|
||||||
|
low_val,
|
||||||
|
max_drawdown_rel
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[float, float]:
|
def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[float, float]:
|
||||||
|
|
|
@ -11,6 +11,7 @@ import pandas as pd
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
|
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
|
||||||
|
from freqtrade.enums import CandleType
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -113,7 +114,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
||||||
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
|
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
|
||||||
if len_before != len_after:
|
if len_before != len_after:
|
||||||
message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}"
|
message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}"
|
||||||
f" - {round(pct_missing * 100, 2)}%")
|
f" - {pct_missing:.2%}")
|
||||||
if pct_missing > 0.01:
|
if pct_missing > 0.01:
|
||||||
logger.info(message)
|
logger.info(message)
|
||||||
else:
|
else:
|
||||||
|
@ -261,13 +262,20 @@ def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to:
|
||||||
src.trades_purge(pair=pair)
|
src.trades_purge(pair=pair)
|
||||||
|
|
||||||
|
|
||||||
def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to: str, erase: bool):
|
def convert_ohlcv_format(
|
||||||
|
config: Dict[str, Any],
|
||||||
|
convert_from: str,
|
||||||
|
convert_to: str,
|
||||||
|
erase: bool,
|
||||||
|
candle_type: CandleType
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Convert OHLCV from one format to another
|
Convert OHLCV from one format to another
|
||||||
:param config: Config dictionary
|
:param config: Config dictionary
|
||||||
:param convert_from: Source format
|
:param convert_from: Source 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)
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
"""
|
"""
|
||||||
from freqtrade.data.history.idatahandler import get_datahandler
|
from freqtrade.data.history.idatahandler import get_datahandler
|
||||||
src = get_datahandler(config['datadir'], convert_from)
|
src = get_datahandler(config['datadir'], convert_from)
|
||||||
|
@ -279,8 +287,11 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to:
|
||||||
config['pairs'] = []
|
config['pairs'] = []
|
||||||
# Check timeframes or fall back to timeframe.
|
# Check timeframes or fall back to timeframe.
|
||||||
for timeframe in timeframes:
|
for timeframe in timeframes:
|
||||||
config['pairs'].extend(src.ohlcv_get_pairs(config['datadir'],
|
config['pairs'].extend(src.ohlcv_get_pairs(
|
||||||
timeframe))
|
config['datadir'],
|
||||||
|
timeframe,
|
||||||
|
candle_type=candle_type
|
||||||
|
))
|
||||||
logger.info(f"Converting candle (OHLCV) data for {config['pairs']}")
|
logger.info(f"Converting candle (OHLCV) data for {config['pairs']}")
|
||||||
|
|
||||||
for timeframe in timeframes:
|
for timeframe in timeframes:
|
||||||
|
@ -289,10 +300,16 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to:
|
||||||
timerange=None,
|
timerange=None,
|
||||||
fill_missing=False,
|
fill_missing=False,
|
||||||
drop_incomplete=False,
|
drop_incomplete=False,
|
||||||
startup_candles=0)
|
startup_candles=0,
|
||||||
logger.info(f"Converting {len(data)} candles for {pair}")
|
candle_type=candle_type)
|
||||||
|
logger.info(f"Converting {len(data)} {candle_type} candles for {pair}")
|
||||||
if len(data) > 0:
|
if len(data) > 0:
|
||||||
trg.ohlcv_store(pair=pair, timeframe=timeframe, data=data)
|
trg.ohlcv_store(
|
||||||
|
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)
|
src.ohlcv_purge(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||||
|
|
|
@ -13,7 +13,7 @@ from pandas import DataFrame
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
||||||
from freqtrade.data.history import load_pair_history
|
from freqtrade.data.history import load_pair_history
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import CandleType, RunMode
|
||||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||||
from freqtrade.exchange import Exchange, timeframe_to_seconds
|
from freqtrade.exchange import Exchange, timeframe_to_seconds
|
||||||
|
|
||||||
|
@ -41,7 +41,13 @@ class DataProvider:
|
||||||
"""
|
"""
|
||||||
self.__slice_index = limit_index
|
self.__slice_index = limit_index
|
||||||
|
|
||||||
def _set_cached_df(self, pair: str, timeframe: str, dataframe: DataFrame) -> None:
|
def _set_cached_df(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
timeframe: str,
|
||||||
|
dataframe: DataFrame,
|
||||||
|
candle_type: CandleType
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Store cached Dataframe.
|
Store cached Dataframe.
|
||||||
Using private method as this should never be used by a user
|
Using private method as this should never be used by a user
|
||||||
|
@ -49,8 +55,10 @@ class DataProvider:
|
||||||
:param pair: pair to get the data for
|
:param pair: pair to get the data for
|
||||||
:param timeframe: Timeframe to get data for
|
:param timeframe: Timeframe to get data for
|
||||||
:param dataframe: analyzed dataframe
|
:param dataframe: analyzed dataframe
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
"""
|
"""
|
||||||
self.__cached_pairs[(pair, timeframe)] = (dataframe, datetime.now(timezone.utc))
|
self.__cached_pairs[(pair, timeframe, candle_type)] = (
|
||||||
|
dataframe, datetime.now(timezone.utc))
|
||||||
|
|
||||||
def add_pairlisthandler(self, pairlists) -> None:
|
def add_pairlisthandler(self, pairlists) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -58,13 +66,21 @@ class DataProvider:
|
||||||
"""
|
"""
|
||||||
self._pairlists = pairlists
|
self._pairlists = pairlists
|
||||||
|
|
||||||
def historic_ohlcv(self, pair: str, timeframe: str = None) -> DataFrame:
|
def historic_ohlcv(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
timeframe: str = None,
|
||||||
|
candle_type: str = ''
|
||||||
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Get stored historical candle (OHLCV) data
|
Get stored historical candle (OHLCV) data
|
||||||
:param pair: pair to get the data for
|
:param pair: pair to get the data for
|
||||||
:param timeframe: timeframe to get data for
|
:param timeframe: timeframe to get data for
|
||||||
|
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
||||||
"""
|
"""
|
||||||
saved_pair = (pair, str(timeframe))
|
_candle_type = CandleType.from_string(
|
||||||
|
candle_type) if candle_type != '' else self._config['candle_type_def']
|
||||||
|
saved_pair = (pair, str(timeframe), _candle_type)
|
||||||
if saved_pair not in self.__cached_pairs_backtesting:
|
if saved_pair not in self.__cached_pairs_backtesting:
|
||||||
timerange = TimeRange.parse_timerange(None if self._config.get(
|
timerange = TimeRange.parse_timerange(None if self._config.get(
|
||||||
'timerange') is None else str(self._config.get('timerange')))
|
'timerange') is None else str(self._config.get('timerange')))
|
||||||
|
@ -77,26 +93,36 @@ class DataProvider:
|
||||||
timeframe=timeframe or self._config['timeframe'],
|
timeframe=timeframe or self._config['timeframe'],
|
||||||
datadir=self._config['datadir'],
|
datadir=self._config['datadir'],
|
||||||
timerange=timerange,
|
timerange=timerange,
|
||||||
data_format=self._config.get('dataformat_ohlcv', 'json')
|
data_format=self._config.get('dataformat_ohlcv', 'json'),
|
||||||
|
candle_type=_candle_type,
|
||||||
|
|
||||||
)
|
)
|
||||||
return self.__cached_pairs_backtesting[saved_pair].copy()
|
return self.__cached_pairs_backtesting[saved_pair].copy()
|
||||||
|
|
||||||
def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame:
|
def get_pair_dataframe(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
timeframe: str = None,
|
||||||
|
candle_type: str = ''
|
||||||
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Return pair candle (OHLCV) data, either live or cached historical -- depending
|
Return pair candle (OHLCV) data, either live or cached historical -- depending
|
||||||
on the runmode.
|
on the runmode.
|
||||||
|
Only combinations in the pairlist or which have been specified as informative pairs
|
||||||
|
will be available.
|
||||||
:param pair: pair to get the data for
|
:param pair: pair to get the data for
|
||||||
:param timeframe: timeframe to get data for
|
:param timeframe: timeframe to get data for
|
||||||
:return: Dataframe for this pair
|
:return: Dataframe for this pair
|
||||||
|
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
||||||
"""
|
"""
|
||||||
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||||
# Get live OHLCV data.
|
# Get live OHLCV data.
|
||||||
data = self.ohlcv(pair=pair, timeframe=timeframe)
|
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||||
else:
|
else:
|
||||||
# Get historical OHLCV data (cached on disk).
|
# Get historical OHLCV data (cached on disk).
|
||||||
data = self.historic_ohlcv(pair=pair, timeframe=timeframe)
|
data = self.historic_ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
logger.warning(f"No data found for ({pair}, {timeframe}).")
|
logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_analyzed_dataframe(self, pair: str, timeframe: str) -> Tuple[DataFrame, datetime]:
|
def get_analyzed_dataframe(self, pair: str, timeframe: str) -> Tuple[DataFrame, datetime]:
|
||||||
|
@ -109,7 +135,7 @@ class DataProvider:
|
||||||
combination.
|
combination.
|
||||||
Returns empty dataframe and Epoch 0 (1970-01-01) if no dataframe was cached.
|
Returns empty dataframe and Epoch 0 (1970-01-01) if no dataframe was cached.
|
||||||
"""
|
"""
|
||||||
pair_key = (pair, timeframe)
|
pair_key = (pair, timeframe, self._config.get('candle_type_def', CandleType.SPOT))
|
||||||
if pair_key in self.__cached_pairs:
|
if pair_key in self.__cached_pairs:
|
||||||
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||||
df, date = self.__cached_pairs[pair_key]
|
df, date = self.__cached_pairs[pair_key]
|
||||||
|
@ -149,6 +175,8 @@ class DataProvider:
|
||||||
Clear pair dataframe cache.
|
Clear pair dataframe cache.
|
||||||
"""
|
"""
|
||||||
self.__cached_pairs = {}
|
self.__cached_pairs = {}
|
||||||
|
self.__cached_pairs_backtesting = {}
|
||||||
|
self.__slice_index = 0
|
||||||
|
|
||||||
# Exchange functions
|
# Exchange functions
|
||||||
|
|
||||||
|
@ -175,20 +203,31 @@ class DataProvider:
|
||||||
raise OperationalException(NO_EXCHANGE_EXCEPTION)
|
raise OperationalException(NO_EXCHANGE_EXCEPTION)
|
||||||
return list(self._exchange._klines.keys())
|
return list(self._exchange._klines.keys())
|
||||||
|
|
||||||
def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame:
|
def ohlcv(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
timeframe: str = None,
|
||||||
|
copy: bool = True,
|
||||||
|
candle_type: str = ''
|
||||||
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Get candle (OHLCV) data for the given pair as DataFrame
|
Get candle (OHLCV) data for the given pair as DataFrame
|
||||||
Please use the `available_pairs` method to verify which pairs are currently cached.
|
Please use the `available_pairs` method to verify which pairs are currently cached.
|
||||||
:param pair: pair to get the data for
|
:param pair: pair to get the data for
|
||||||
:param timeframe: Timeframe to get data for
|
:param timeframe: Timeframe to get data for
|
||||||
|
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
||||||
:param copy: copy dataframe before returning if True.
|
:param copy: copy dataframe before returning if True.
|
||||||
Use False only for read-only operations (where the dataframe is not modified)
|
Use False only for read-only operations (where the dataframe is not modified)
|
||||||
"""
|
"""
|
||||||
if self._exchange is None:
|
if self._exchange is None:
|
||||||
raise OperationalException(NO_EXCHANGE_EXCEPTION)
|
raise OperationalException(NO_EXCHANGE_EXCEPTION)
|
||||||
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||||
return self._exchange.klines((pair, timeframe or self._config['timeframe']),
|
_candle_type = CandleType.from_string(
|
||||||
copy=copy)
|
candle_type) if candle_type != '' else self._config['candle_type_def']
|
||||||
|
return self._exchange.klines(
|
||||||
|
(pair, timeframe or self._config['timeframe'], _candle_type),
|
||||||
|
copy=copy
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return DataFrame()
|
return DataFrame()
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,10 @@ from typing import List, Optional
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade import misc
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS,
|
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS,
|
||||||
ListPairsWithTimeframes, TradeList)
|
ListPairsWithTimeframes, TradeList)
|
||||||
|
from freqtrade.enums import CandleType, TradingMode
|
||||||
|
|
||||||
from .idatahandler import IDataHandler
|
from .idatahandler import IDataHandler
|
||||||
|
|
||||||
|
@ -22,52 +22,72 @@ class HDF5DataHandler(IDataHandler):
|
||||||
_columns = DEFAULT_DATAFRAME_COLUMNS
|
_columns = DEFAULT_DATAFRAME_COLUMNS
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
|
def ohlcv_get_available_data(
|
||||||
|
cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes:
|
||||||
"""
|
"""
|
||||||
Returns a list of all pairs with ohlcv data available in this datadir
|
Returns a list of all pairs with ohlcv data available in this datadir
|
||||||
:param datadir: Directory to search for ohlcv files
|
:param datadir: Directory to search for ohlcv files
|
||||||
|
:param trading_mode: trading-mode to be used
|
||||||
:return: List of Tuples of (pair, timeframe)
|
:return: List of Tuples of (pair, timeframe)
|
||||||
"""
|
"""
|
||||||
_tmp = [re.search(r'^([a-zA-Z_]+)\-(\d+\S+)(?=.h5)', p.name)
|
if trading_mode == TradingMode.FUTURES:
|
||||||
for p in datadir.glob("*.h5")]
|
datadir = datadir.joinpath('futures')
|
||||||
return [(match[1].replace('_', '/'), match[2]) for match in _tmp
|
_tmp = [
|
||||||
if match and len(match.groups()) > 1]
|
re.search(
|
||||||
|
cls._OHLCV_REGEX, p.name
|
||||||
|
) for p in datadir.glob("*.h5")
|
||||||
|
]
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
cls.rebuild_pair_from_filename(match[1]),
|
||||||
|
match[2],
|
||||||
|
CandleType.from_string(match[3])
|
||||||
|
) for match in _tmp if match and len(match.groups()) > 1]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]:
|
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Returns a list of all pairs with ohlcv data available in this datadir
|
Returns a list of all pairs with ohlcv data available in this datadir
|
||||||
for the specified timeframe
|
for the specified timeframe
|
||||||
:param datadir: Directory to search for ohlcv files
|
:param datadir: Directory to search for ohlcv files
|
||||||
:param timeframe: Timeframe to search pairs for
|
:param timeframe: Timeframe to search pairs for
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
:return: List of Pairs
|
:return: List of Pairs
|
||||||
"""
|
"""
|
||||||
|
candle = ""
|
||||||
|
if candle_type != CandleType.SPOT:
|
||||||
|
datadir = datadir.joinpath('futures')
|
||||||
|
candle = f"-{candle_type}"
|
||||||
|
|
||||||
_tmp = [re.search(r'^(\S+)(?=\-' + timeframe + '.h5)', p.name)
|
_tmp = [re.search(r'^(\S+)(?=\-' + timeframe + candle + '.h5)', p.name)
|
||||||
for p in datadir.glob(f"*{timeframe}.h5")]
|
for p in datadir.glob(f"*{timeframe}{candle}.h5")]
|
||||||
# Check if regex found something and only return these results
|
# Check if regex found something and only return these results
|
||||||
return [match[0].replace('_', '/') for match in _tmp if match]
|
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
||||||
|
|
||||||
def ohlcv_store(self, pair: str, timeframe: str, data: pd.DataFrame) -> None:
|
def ohlcv_store(
|
||||||
|
self, pair: str, timeframe: str, data: pd.DataFrame, candle_type: CandleType) -> None:
|
||||||
"""
|
"""
|
||||||
Store data in hdf5 file.
|
Store data in hdf5 file.
|
||||||
:param pair: Pair - used to generate filename
|
:param pair: Pair - used to generate filename
|
||||||
:param timeframe: Timeframe - used to generate filename
|
:param timeframe: Timeframe - used to generate filename
|
||||||
:param data: Dataframe containing OHLCV data
|
:param data: Dataframe containing OHLCV data
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
key = self._pair_ohlcv_key(pair, timeframe)
|
key = self._pair_ohlcv_key(pair, timeframe)
|
||||||
_data = data.copy()
|
_data = data.copy()
|
||||||
|
|
||||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
|
||||||
|
self.create_dir_if_needed(filename)
|
||||||
|
|
||||||
ds = pd.HDFStore(filename, mode='a', complevel=9, complib='blosc')
|
_data.loc[:, self._columns].to_hdf(
|
||||||
ds.put(key, _data.loc[:, self._columns], format='table', data_columns=['date'])
|
filename, key, mode='a', complevel=9, complib='blosc',
|
||||||
|
format='table', data_columns=['date']
|
||||||
ds.close()
|
)
|
||||||
|
|
||||||
def _ohlcv_load(self, pair: str, timeframe: str,
|
def _ohlcv_load(self, pair: str, timeframe: str,
|
||||||
timerange: Optional[TimeRange] = None) -> pd.DataFrame:
|
timerange: Optional[TimeRange], candle_type: CandleType
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Internal method used to load data for one pair from disk.
|
Internal method used to load data for one pair from disk.
|
||||||
Implements the loading and conversion to a Pandas dataframe.
|
Implements the loading and conversion to a Pandas dataframe.
|
||||||
|
@ -77,10 +97,16 @@ class HDF5DataHandler(IDataHandler):
|
||||||
:param timerange: Limit data to be loaded to this timerange.
|
:param timerange: Limit data to be loaded to this timerange.
|
||||||
Optionally implemented by subclasses to avoid loading
|
Optionally implemented by subclasses to avoid loading
|
||||||
all data where possible.
|
all data where possible.
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
:return: DataFrame with ohlcv data, or empty DataFrame
|
:return: DataFrame with ohlcv data, or empty DataFrame
|
||||||
"""
|
"""
|
||||||
key = self._pair_ohlcv_key(pair, timeframe)
|
key = self._pair_ohlcv_key(pair, timeframe)
|
||||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
filename = self._pair_data_filename(
|
||||||
|
self._datadir,
|
||||||
|
pair,
|
||||||
|
timeframe,
|
||||||
|
candle_type=candle_type
|
||||||
|
)
|
||||||
|
|
||||||
if not filename.exists():
|
if not filename.exists():
|
||||||
return pd.DataFrame(columns=self._columns)
|
return pd.DataFrame(columns=self._columns)
|
||||||
|
@ -99,25 +125,19 @@ class HDF5DataHandler(IDataHandler):
|
||||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
|
def ohlcv_append(
|
||||||
"""
|
self,
|
||||||
Remove data for this pair
|
pair: str,
|
||||||
:param pair: Delete data for this pair.
|
timeframe: str,
|
||||||
:param timeframe: Timeframe (e.g. "5m")
|
data: pd.DataFrame,
|
||||||
:return: True when deleted, false if file did not exist.
|
candle_type: CandleType
|
||||||
"""
|
) -> None:
|
||||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
|
||||||
if filename.exists():
|
|
||||||
filename.unlink()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def ohlcv_append(self, pair: str, timeframe: str, data: pd.DataFrame) -> None:
|
|
||||||
"""
|
"""
|
||||||
Append data to existing data structures
|
Append data to existing data structures
|
||||||
:param pair: Pair
|
:param pair: Pair
|
||||||
:param timeframe: Timeframe this ohlcv data is for
|
:param timeframe: Timeframe this ohlcv data is for
|
||||||
:param data: Data to append.
|
:param data: Data to append.
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@ -131,7 +151,7 @@ class HDF5DataHandler(IDataHandler):
|
||||||
_tmp = [re.search(r'^(\S+)(?=\-trades.h5)', p.name)
|
_tmp = [re.search(r'^(\S+)(?=\-trades.h5)', p.name)
|
||||||
for p in datadir.glob("*trades.h5")]
|
for p in datadir.glob("*trades.h5")]
|
||||||
# Check if regex found something and only return these results to avoid exceptions.
|
# Check if regex found something and only return these results to avoid exceptions.
|
||||||
return [match[0].replace('_', '/') for match in _tmp if match]
|
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
||||||
|
|
||||||
def trades_store(self, pair: str, data: TradeList) -> None:
|
def trades_store(self, pair: str, data: TradeList) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -142,11 +162,11 @@ class HDF5DataHandler(IDataHandler):
|
||||||
"""
|
"""
|
||||||
key = self._pair_trades_key(pair)
|
key = self._pair_trades_key(pair)
|
||||||
|
|
||||||
ds = pd.HDFStore(self._pair_trades_filename(self._datadir, pair),
|
pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS).to_hdf(
|
||||||
mode='a', complevel=9, complib='blosc')
|
self._pair_trades_filename(self._datadir, pair), key,
|
||||||
ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS),
|
mode='a', complevel=9, complib='blosc',
|
||||||
format='table', data_columns=['timestamp'])
|
format='table', data_columns=['timestamp']
|
||||||
ds.close()
|
)
|
||||||
|
|
||||||
def trades_append(self, pair: str, data: TradeList):
|
def trades_append(self, pair: str, data: TradeList):
|
||||||
"""
|
"""
|
||||||
|
@ -180,34 +200,16 @@ class HDF5DataHandler(IDataHandler):
|
||||||
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
||||||
return trades.values.tolist()
|
return trades.values.tolist()
|
||||||
|
|
||||||
def trades_purge(self, pair: str) -> bool:
|
@classmethod
|
||||||
"""
|
def _get_file_extension(cls):
|
||||||
Remove data for this pair
|
return "h5"
|
||||||
:param pair: Delete data for this pair.
|
|
||||||
:return: True when deleted, false if file did not exist.
|
|
||||||
"""
|
|
||||||
filename = self._pair_trades_filename(self._datadir, pair)
|
|
||||||
if filename.exists():
|
|
||||||
filename.unlink()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str:
|
def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str:
|
||||||
return f"{pair}/ohlcv/tf_{timeframe}"
|
# Escape futures pairs to avoid warnings
|
||||||
|
pair_esc = pair.replace(':', '_')
|
||||||
|
return f"{pair_esc}/ohlcv/tf_{timeframe}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _pair_trades_key(cls, pair: str) -> str:
|
def _pair_trades_key(cls, pair: str) -> str:
|
||||||
return f"{pair}/trades"
|
return f"{pair}/trades"
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path:
|
|
||||||
pair_s = misc.pair_to_filename(pair)
|
|
||||||
filename = datadir.joinpath(f'{pair_s}-{timeframe}.h5')
|
|
||||||
return filename
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path:
|
|
||||||
pair_s = misc.pair_to_filename(pair)
|
|
||||||
filename = datadir.joinpath(f'{pair_s}-trades.h5')
|
|
||||||
return filename
|
|
||||||
|
|
|
@ -5,13 +5,14 @@ from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame, concat
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
||||||
from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe,
|
from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe,
|
||||||
trades_remove_duplicates, trades_to_ohlcv)
|
trades_remove_duplicates, trades_to_ohlcv)
|
||||||
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
|
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
|
||||||
|
from freqtrade.enums import CandleType
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.misc import format_ms_time
|
from freqtrade.misc import format_ms_time
|
||||||
|
@ -29,6 +30,7 @@ def load_pair_history(pair: str,
|
||||||
startup_candles: int = 0,
|
startup_candles: int = 0,
|
||||||
data_format: str = None,
|
data_format: str = None,
|
||||||
data_handler: IDataHandler = None,
|
data_handler: IDataHandler = None,
|
||||||
|
candle_type: CandleType = CandleType.SPOT
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Load cached ohlcv history for the given pair.
|
Load cached ohlcv history for the given pair.
|
||||||
|
@ -43,6 +45,7 @@ def load_pair_history(pair: str,
|
||||||
:param startup_candles: Additional candles to load at the start of the period
|
:param startup_candles: Additional candles to load at the start of the period
|
||||||
:param data_handler: Initialized data-handler to use.
|
:param data_handler: Initialized data-handler to use.
|
||||||
Will be initialized from data_format if not set
|
Will be initialized from data_format if not set
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
:return: DataFrame with ohlcv data, or empty DataFrame
|
:return: DataFrame with ohlcv data, or empty DataFrame
|
||||||
"""
|
"""
|
||||||
data_handler = get_datahandler(datadir, data_format, data_handler)
|
data_handler = get_datahandler(datadir, data_format, data_handler)
|
||||||
|
@ -53,6 +56,7 @@ def load_pair_history(pair: str,
|
||||||
fill_missing=fill_up_missing,
|
fill_missing=fill_up_missing,
|
||||||
drop_incomplete=drop_incomplete,
|
drop_incomplete=drop_incomplete,
|
||||||
startup_candles=startup_candles,
|
startup_candles=startup_candles,
|
||||||
|
candle_type=candle_type
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -64,6 +68,7 @@ def load_data(datadir: Path,
|
||||||
startup_candles: int = 0,
|
startup_candles: int = 0,
|
||||||
fail_without_data: bool = False,
|
fail_without_data: bool = False,
|
||||||
data_format: str = 'json',
|
data_format: str = 'json',
|
||||||
|
candle_type: CandleType = CandleType.SPOT
|
||||||
) -> Dict[str, DataFrame]:
|
) -> Dict[str, DataFrame]:
|
||||||
"""
|
"""
|
||||||
Load ohlcv history data for a list of pairs.
|
Load ohlcv history data for a list of pairs.
|
||||||
|
@ -76,6 +81,7 @@ def load_data(datadir: Path,
|
||||||
:param startup_candles: Additional candles to load at the start of the period
|
:param startup_candles: Additional candles to load at the start of the period
|
||||||
:param fail_without_data: Raise OperationalException if no data is found.
|
:param fail_without_data: Raise OperationalException if no data is found.
|
||||||
:param data_format: Data format which should be used. Defaults to json
|
:param data_format: Data format which should be used. Defaults to json
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
:return: dict(<pair>:<Dataframe>)
|
:return: dict(<pair>:<Dataframe>)
|
||||||
"""
|
"""
|
||||||
result: Dict[str, DataFrame] = {}
|
result: Dict[str, DataFrame] = {}
|
||||||
|
@ -89,7 +95,8 @@ def load_data(datadir: Path,
|
||||||
datadir=datadir, timerange=timerange,
|
datadir=datadir, timerange=timerange,
|
||||||
fill_up_missing=fill_up_missing,
|
fill_up_missing=fill_up_missing,
|
||||||
startup_candles=startup_candles,
|
startup_candles=startup_candles,
|
||||||
data_handler=data_handler
|
data_handler=data_handler,
|
||||||
|
candle_type=candle_type
|
||||||
)
|
)
|
||||||
if not hist.empty:
|
if not hist.empty:
|
||||||
result[pair] = hist
|
result[pair] = hist
|
||||||
|
@ -99,12 +106,13 @@ def load_data(datadir: Path,
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def refresh_data(datadir: Path,
|
def refresh_data(*, datadir: Path,
|
||||||
timeframe: str,
|
timeframe: str,
|
||||||
pairs: List[str],
|
pairs: List[str],
|
||||||
exchange: Exchange,
|
exchange: Exchange,
|
||||||
data_format: str = None,
|
data_format: str = None,
|
||||||
timerange: Optional[TimeRange] = None,
|
timerange: Optional[TimeRange] = None,
|
||||||
|
candle_type: CandleType,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Refresh ohlcv history data for a list of pairs.
|
Refresh ohlcv history data for a list of pairs.
|
||||||
|
@ -115,17 +123,24 @@ def refresh_data(datadir: Path,
|
||||||
:param exchange: Exchange object
|
:param exchange: Exchange object
|
||||||
:param data_format: dataformat to use
|
:param data_format: dataformat to use
|
||||||
:param timerange: Limit data to be loaded to this timerange
|
:param timerange: Limit data to be loaded to this timerange
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
"""
|
"""
|
||||||
data_handler = get_datahandler(datadir, data_format)
|
data_handler = get_datahandler(datadir, data_format)
|
||||||
for idx, pair in enumerate(pairs):
|
for idx, pair in enumerate(pairs):
|
||||||
process = f'{idx}/{len(pairs)}'
|
process = f'{idx}/{len(pairs)}'
|
||||||
_download_pair_history(pair=pair, process=process,
|
_download_pair_history(pair=pair, process=process,
|
||||||
timeframe=timeframe, datadir=datadir,
|
timeframe=timeframe, datadir=datadir,
|
||||||
timerange=timerange, exchange=exchange, data_handler=data_handler)
|
timerange=timerange, exchange=exchange, data_handler=data_handler,
|
||||||
|
candle_type=candle_type)
|
||||||
|
|
||||||
|
|
||||||
def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optional[TimeRange],
|
def _load_cached_data_for_updating(
|
||||||
data_handler: IDataHandler) -> Tuple[DataFrame, Optional[int]]:
|
pair: str,
|
||||||
|
timeframe: str,
|
||||||
|
timerange: Optional[TimeRange],
|
||||||
|
data_handler: IDataHandler,
|
||||||
|
candle_type: CandleType
|
||||||
|
) -> Tuple[DataFrame, Optional[int]]:
|
||||||
"""
|
"""
|
||||||
Load cached data to download more data.
|
Load cached data to download more data.
|
||||||
If timerange is passed in, checks whether data from an before the stored data will be
|
If timerange is passed in, checks whether data from an before the stored data will be
|
||||||
|
@ -142,7 +157,8 @@ def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optiona
|
||||||
# Intentionally don't pass timerange in - since we need to load the full dataset.
|
# Intentionally don't pass timerange in - since we need to load the full dataset.
|
||||||
data = data_handler.ohlcv_load(pair, timeframe=timeframe,
|
data = data_handler.ohlcv_load(pair, timeframe=timeframe,
|
||||||
timerange=None, fill_missing=False,
|
timerange=None, fill_missing=False,
|
||||||
drop_incomplete=True, warn_no_data=False)
|
drop_incomplete=True, warn_no_data=False,
|
||||||
|
candle_type=candle_type)
|
||||||
if not data.empty:
|
if not data.empty:
|
||||||
if start and start < data.iloc[0]['date']:
|
if start and start < data.iloc[0]['date']:
|
||||||
# Earlier data than existing data requested, redownload all
|
# Earlier data than existing data requested, redownload all
|
||||||
|
@ -161,7 +177,10 @@ def _download_pair_history(pair: str, *,
|
||||||
process: str = '',
|
process: str = '',
|
||||||
new_pairs_days: int = 30,
|
new_pairs_days: int = 30,
|
||||||
data_handler: IDataHandler = None,
|
data_handler: IDataHandler = None,
|
||||||
timerange: Optional[TimeRange] = None) -> bool:
|
timerange: Optional[TimeRange] = None,
|
||||||
|
candle_type: CandleType,
|
||||||
|
erase: bool = False,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Download latest candles from the exchange for the pair and timeframe passed in parameters
|
Download latest candles from the exchange for the pair and timeframe passed in parameters
|
||||||
The data is downloaded starting from the last correct data that
|
The data is downloaded starting from the last correct data that
|
||||||
|
@ -173,19 +192,25 @@ def _download_pair_history(pair: str, *,
|
||||||
:param pair: pair to download
|
:param pair: pair to download
|
||||||
:param timeframe: Timeframe (e.g "5m")
|
:param timeframe: Timeframe (e.g "5m")
|
||||||
:param timerange: range of time to download
|
:param timerange: range of time to download
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
|
:param erase: Erase existing data
|
||||||
:return: bool with success state
|
:return: bool with success state
|
||||||
"""
|
"""
|
||||||
data_handler = get_datahandler(datadir, data_handler=data_handler)
|
data_handler = get_datahandler(datadir, data_handler=data_handler)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if erase:
|
||||||
|
if data_handler.ohlcv_purge(pair, timeframe, candle_type=candle_type):
|
||||||
|
logger.info(f'Deleting existing data for pair {pair}, {timeframe}, {candle_type}.')
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f'Download history data for pair: "{pair}" ({process}), timeframe: {timeframe} '
|
f'Download history data for pair: "{pair}" ({process}), timeframe: {timeframe}, '
|
||||||
f'and store in {datadir}.'
|
f'candle type: {candle_type} and store in {datadir}.'
|
||||||
)
|
)
|
||||||
|
|
||||||
# data, since_ms = _load_cached_data_for_updating_old(datadir, pair, timeframe, timerange)
|
|
||||||
data, since_ms = _load_cached_data_for_updating(pair, timeframe, timerange,
|
data, since_ms = _load_cached_data_for_updating(pair, timeframe, timerange,
|
||||||
data_handler=data_handler)
|
data_handler=data_handler,
|
||||||
|
candle_type=candle_type)
|
||||||
|
|
||||||
logger.debug("Current Start: %s",
|
logger.debug("Current Start: %s",
|
||||||
f"{data.iloc[0]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None')
|
f"{data.iloc[0]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None')
|
||||||
|
@ -198,7 +223,8 @@ def _download_pair_history(pair: str, *,
|
||||||
since_ms=since_ms if since_ms else
|
since_ms=since_ms if since_ms else
|
||||||
arrow.utcnow().shift(
|
arrow.utcnow().shift(
|
||||||
days=-new_pairs_days).int_timestamp * 1000,
|
days=-new_pairs_days).int_timestamp * 1000,
|
||||||
is_new_pair=data.empty
|
is_new_pair=data.empty,
|
||||||
|
candle_type=candle_type,
|
||||||
)
|
)
|
||||||
# TODO: Maybe move parsing to exchange class (?)
|
# TODO: Maybe move parsing to exchange class (?)
|
||||||
new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair,
|
new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair,
|
||||||
|
@ -208,7 +234,7 @@ def _download_pair_history(pair: str, *,
|
||||||
else:
|
else:
|
||||||
# Run cleaning again to ensure there were no duplicate candles
|
# Run cleaning again to ensure there were no duplicate candles
|
||||||
# Especially between existing and new data.
|
# Especially between existing and new data.
|
||||||
data = clean_ohlcv_dataframe(data.append(new_dataframe), timeframe, pair,
|
data = clean_ohlcv_dataframe(concat([data, new_dataframe], axis=0), timeframe, pair,
|
||||||
fill_missing=False, drop_incomplete=False)
|
fill_missing=False, drop_incomplete=False)
|
||||||
|
|
||||||
logger.debug("New Start: %s",
|
logger.debug("New Start: %s",
|
||||||
|
@ -216,7 +242,7 @@ def _download_pair_history(pair: str, *,
|
||||||
logger.debug("New End: %s",
|
logger.debug("New End: %s",
|
||||||
f"{data.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None')
|
f"{data.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None')
|
||||||
|
|
||||||
data_handler.ohlcv_store(pair, timeframe, data=data)
|
data_handler.ohlcv_store(pair, timeframe, data=data, candle_type=candle_type)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -227,9 +253,11 @@ def _download_pair_history(pair: str, *,
|
||||||
|
|
||||||
|
|
||||||
def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str],
|
def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str],
|
||||||
datadir: Path, timerange: Optional[TimeRange] = None,
|
datadir: Path, trading_mode: str,
|
||||||
|
timerange: Optional[TimeRange] = None,
|
||||||
new_pairs_days: int = 30, erase: bool = False,
|
new_pairs_days: int = 30, erase: bool = False,
|
||||||
data_format: str = None) -> List[str]:
|
data_format: str = None,
|
||||||
|
) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Refresh stored ohlcv data for backtesting and hyperopt operations.
|
Refresh stored ohlcv data for backtesting and hyperopt operations.
|
||||||
Used by freqtrade download-data subcommand.
|
Used by freqtrade download-data subcommand.
|
||||||
|
@ -237,6 +265,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
||||||
"""
|
"""
|
||||||
pairs_not_available = []
|
pairs_not_available = []
|
||||||
data_handler = get_datahandler(datadir, data_format)
|
data_handler = get_datahandler(datadir, data_format)
|
||||||
|
candle_type = CandleType.get_default(trading_mode)
|
||||||
for idx, pair in enumerate(pairs, start=1):
|
for idx, pair in enumerate(pairs, start=1):
|
||||||
if pair not in exchange.markets:
|
if pair not in exchange.markets:
|
||||||
pairs_not_available.append(pair)
|
pairs_not_available.append(pair)
|
||||||
|
@ -244,17 +273,29 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
||||||
continue
|
continue
|
||||||
for timeframe in timeframes:
|
for timeframe in timeframes:
|
||||||
|
|
||||||
if erase:
|
|
||||||
if data_handler.ohlcv_purge(pair, timeframe):
|
|
||||||
logger.info(
|
|
||||||
f'Deleting existing data for pair {pair}, interval {timeframe}.')
|
|
||||||
|
|
||||||
logger.info(f'Downloading pair {pair}, interval {timeframe}.')
|
logger.info(f'Downloading pair {pair}, interval {timeframe}.')
|
||||||
process = f'{idx}/{len(pairs)}'
|
process = f'{idx}/{len(pairs)}'
|
||||||
_download_pair_history(pair=pair, process=process,
|
_download_pair_history(pair=pair, process=process,
|
||||||
datadir=datadir, exchange=exchange,
|
datadir=datadir, exchange=exchange,
|
||||||
timerange=timerange, data_handler=data_handler,
|
timerange=timerange, data_handler=data_handler,
|
||||||
timeframe=str(timeframe), new_pairs_days=new_pairs_days)
|
timeframe=str(timeframe), new_pairs_days=new_pairs_days,
|
||||||
|
candle_type=candle_type,
|
||||||
|
erase=erase)
|
||||||
|
if trading_mode == 'futures':
|
||||||
|
# Predefined candletype (and timeframe) depending on exchange
|
||||||
|
# Downloads what is necessary to backtest based on futures data.
|
||||||
|
tf_mark = exchange._ft_has['mark_ohlcv_timeframe']
|
||||||
|
fr_candle_type = CandleType.from_string(exchange._ft_has['mark_ohlcv_price'])
|
||||||
|
# All exchanges need FundingRate for futures trading.
|
||||||
|
# The timeframe is aligned to the mark-price timeframe.
|
||||||
|
for funding_candle_type in (CandleType.FUNDING_RATE, fr_candle_type):
|
||||||
|
_download_pair_history(pair=pair, process=process,
|
||||||
|
datadir=datadir, exchange=exchange,
|
||||||
|
timerange=timerange, data_handler=data_handler,
|
||||||
|
timeframe=str(tf_mark), new_pairs_days=new_pairs_days,
|
||||||
|
candle_type=funding_candle_type,
|
||||||
|
erase=erase)
|
||||||
|
|
||||||
return pairs_not_available
|
return pairs_not_available
|
||||||
|
|
||||||
|
|
||||||
|
@ -353,10 +394,16 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir:
|
||||||
return pairs_not_available
|
return pairs_not_available
|
||||||
|
|
||||||
|
|
||||||
def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str],
|
def convert_trades_to_ohlcv(
|
||||||
datadir: Path, timerange: TimeRange, erase: bool = False,
|
pairs: List[str],
|
||||||
data_format_ohlcv: str = 'json',
|
timeframes: List[str],
|
||||||
data_format_trades: str = 'jsongz') -> None:
|
datadir: Path,
|
||||||
|
timerange: TimeRange,
|
||||||
|
erase: bool = False,
|
||||||
|
data_format_ohlcv: str = 'json',
|
||||||
|
data_format_trades: str = 'jsongz',
|
||||||
|
candle_type: CandleType = CandleType.SPOT
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Convert stored trades data to ohlcv data
|
Convert stored trades data to ohlcv data
|
||||||
"""
|
"""
|
||||||
|
@ -367,12 +414,12 @@ def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str],
|
||||||
trades = data_handler_trades.trades_load(pair)
|
trades = data_handler_trades.trades_load(pair)
|
||||||
for timeframe in timeframes:
|
for timeframe in timeframes:
|
||||||
if erase:
|
if erase:
|
||||||
if data_handler_ohlcv.ohlcv_purge(pair, timeframe):
|
if data_handler_ohlcv.ohlcv_purge(pair, timeframe, candle_type=candle_type):
|
||||||
logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.')
|
logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.')
|
||||||
try:
|
try:
|
||||||
ohlcv = trades_to_ohlcv(trades, timeframe)
|
ohlcv = trades_to_ohlcv(trades, timeframe)
|
||||||
# Store ohlcv
|
# Store ohlcv
|
||||||
data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv)
|
data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv, candle_type=candle_type)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.exception(f'Could not convert {pair} to OHLCV.')
|
logger.exception(f'Could not convert {pair} to OHLCV.')
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ It's subclasses handle and storing data from disk.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from abc import ABC, abstractclassmethod, abstractmethod
|
from abc import ABC, abstractclassmethod, abstractmethod
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
@ -12,9 +13,11 @@ from typing import List, Optional, Type
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade import misc
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import ListPairsWithTimeframes, TradeList
|
from freqtrade.constants import ListPairsWithTimeframes, TradeList
|
||||||
from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe
|
from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe
|
||||||
|
from freqtrade.enums import CandleType, TradingMode
|
||||||
from freqtrade.exchange import timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_seconds
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,40 +26,54 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class IDataHandler(ABC):
|
class IDataHandler(ABC):
|
||||||
|
|
||||||
|
_OHLCV_REGEX = r'^([a-zA-Z_-]+)\-(\d+\S)\-?([a-zA-Z_]*)?(?=\.)'
|
||||||
|
|
||||||
def __init__(self, datadir: Path) -> None:
|
def __init__(self, datadir: Path) -> None:
|
||||||
self._datadir = datadir
|
self._datadir = datadir
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_file_extension(cls) -> str:
|
||||||
|
"""
|
||||||
|
Get file extension for this particular datahandler
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
@abstractclassmethod
|
@abstractclassmethod
|
||||||
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
|
def ohlcv_get_available_data(
|
||||||
|
cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes:
|
||||||
"""
|
"""
|
||||||
Returns a list of all pairs with ohlcv data available in this datadir
|
Returns a list of all pairs with ohlcv data available in this datadir
|
||||||
:param datadir: Directory to search for ohlcv files
|
:param datadir: Directory to search for ohlcv files
|
||||||
|
:param trading_mode: trading-mode to be used
|
||||||
:return: List of Tuples of (pair, timeframe)
|
:return: List of Tuples of (pair, timeframe)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractclassmethod
|
@abstractclassmethod
|
||||||
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]:
|
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Returns a list of all pairs with ohlcv data available in this datadir
|
Returns a list of all pairs with ohlcv data available in this datadir
|
||||||
for the specified timeframe
|
for the specified timeframe
|
||||||
:param datadir: Directory to search for ohlcv files
|
:param datadir: Directory to search for ohlcv files
|
||||||
:param timeframe: Timeframe to search pairs for
|
:param timeframe: Timeframe to search pairs for
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
:return: List of Pairs
|
:return: List of Pairs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def ohlcv_store(self, pair: str, timeframe: str, data: DataFrame) -> None:
|
def ohlcv_store(
|
||||||
|
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType) -> None:
|
||||||
"""
|
"""
|
||||||
Store ohlcv data.
|
Store ohlcv data.
|
||||||
:param pair: Pair - used to generate filename
|
:param pair: Pair - used to generate filename
|
||||||
:param timeframe: Timeframe - used to generate filename
|
:param timeframe: Timeframe - used to generate filename
|
||||||
:param data: Dataframe containing OHLCV data
|
:param data: Dataframe containing OHLCV data
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _ohlcv_load(self, pair: str, timeframe: str,
|
def _ohlcv_load(self, pair: str, timeframe: str, timerange: Optional[TimeRange],
|
||||||
timerange: Optional[TimeRange] = None,
|
candle_type: CandleType
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Internal method used to load data for one pair from disk.
|
Internal method used to load data for one pair from disk.
|
||||||
|
@ -67,25 +84,38 @@ class IDataHandler(ABC):
|
||||||
:param timerange: Limit data to be loaded to this timerange.
|
:param timerange: Limit data to be loaded to this timerange.
|
||||||
Optionally implemented by subclasses to avoid loading
|
Optionally implemented by subclasses to avoid loading
|
||||||
all data where possible.
|
all data where possible.
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
:return: DataFrame with ohlcv data, or empty DataFrame
|
:return: DataFrame with ohlcv data, or empty DataFrame
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
def ohlcv_purge(self, pair: str, timeframe: str, candle_type: CandleType) -> bool:
|
||||||
def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Remove data for this pair
|
Remove data for this pair
|
||||||
:param pair: Delete data for this pair.
|
:param pair: Delete data for this pair.
|
||||||
:param timeframe: Timeframe (e.g. "5m")
|
:param timeframe: Timeframe (e.g. "5m")
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
:return: True when deleted, false if file did not exist.
|
:return: True when deleted, false if file did not exist.
|
||||||
"""
|
"""
|
||||||
|
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
|
||||||
|
if filename.exists():
|
||||||
|
filename.unlink()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def ohlcv_append(self, pair: str, timeframe: str, data: DataFrame) -> None:
|
def ohlcv_append(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
timeframe: str,
|
||||||
|
data: DataFrame,
|
||||||
|
candle_type: CandleType
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Append data to existing data structures
|
Append data to existing data structures
|
||||||
:param pair: Pair
|
:param pair: Pair
|
||||||
:param timeframe: Timeframe this ohlcv data is for
|
:param timeframe: Timeframe this ohlcv data is for
|
||||||
:param data: Data to append.
|
:param data: Data to append.
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractclassmethod
|
@abstractclassmethod
|
||||||
|
@ -123,13 +153,17 @@ class IDataHandler(ABC):
|
||||||
:return: List of trades
|
:return: List of trades
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def trades_purge(self, pair: str) -> bool:
|
def trades_purge(self, pair: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Remove data for this pair
|
Remove data for this pair
|
||||||
:param pair: Delete data for this pair.
|
:param pair: Delete data for this pair.
|
||||||
:return: True when deleted, false if file did not exist.
|
:return: True when deleted, false if file did not exist.
|
||||||
"""
|
"""
|
||||||
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
|
if filename.exists():
|
||||||
|
filename.unlink()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
||||||
"""
|
"""
|
||||||
|
@ -141,12 +175,55 @@ class IDataHandler(ABC):
|
||||||
"""
|
"""
|
||||||
return trades_remove_duplicates(self._trades_load(pair, timerange=timerange))
|
return trades_remove_duplicates(self._trades_load(pair, timerange=timerange))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_dir_if_needed(cls, datadir: Path):
|
||||||
|
"""
|
||||||
|
Creates datadir if necessary
|
||||||
|
should only create directories for "futures" mode at the moment.
|
||||||
|
"""
|
||||||
|
if not datadir.parent.is_dir():
|
||||||
|
datadir.parent.mkdir()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _pair_data_filename(
|
||||||
|
cls,
|
||||||
|
datadir: Path,
|
||||||
|
pair: str,
|
||||||
|
timeframe: str,
|
||||||
|
candle_type: CandleType
|
||||||
|
) -> Path:
|
||||||
|
pair_s = misc.pair_to_filename(pair)
|
||||||
|
candle = ""
|
||||||
|
if candle_type != CandleType.SPOT:
|
||||||
|
datadir = datadir.joinpath('futures')
|
||||||
|
candle = f"-{candle_type}"
|
||||||
|
filename = datadir.joinpath(
|
||||||
|
f'{pair_s}-{timeframe}{candle}.{cls._get_file_extension()}')
|
||||||
|
return filename
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path:
|
||||||
|
pair_s = misc.pair_to_filename(pair)
|
||||||
|
filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}')
|
||||||
|
return filename
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def rebuild_pair_from_filename(pair: str) -> str:
|
||||||
|
"""
|
||||||
|
Rebuild pair name from filename
|
||||||
|
Assumes a asset name of max. 7 length to also support BTC-PERP and BTC-PERP:USD names.
|
||||||
|
"""
|
||||||
|
res = re.sub(r'^(([A-Za-z]{1,10})|^([A-Za-z\-]{1,6}))(_)', r'\g<1>/', pair, 1)
|
||||||
|
res = re.sub('_', ':', res, 1)
|
||||||
|
return res
|
||||||
|
|
||||||
def ohlcv_load(self, pair, timeframe: str,
|
def ohlcv_load(self, pair, timeframe: str,
|
||||||
|
candle_type: CandleType,
|
||||||
timerange: Optional[TimeRange] = None,
|
timerange: Optional[TimeRange] = None,
|
||||||
fill_missing: bool = True,
|
fill_missing: bool = True,
|
||||||
drop_incomplete: bool = True,
|
drop_incomplete: bool = True,
|
||||||
startup_candles: int = 0,
|
startup_candles: int = 0,
|
||||||
warn_no_data: bool = True
|
warn_no_data: bool = True,
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Load cached candle (OHLCV) data for the given pair.
|
Load cached candle (OHLCV) data for the given pair.
|
||||||
|
@ -158,6 +235,7 @@ class IDataHandler(ABC):
|
||||||
:param drop_incomplete: Drop last candle assuming it may be incomplete.
|
:param drop_incomplete: Drop last candle assuming it may be incomplete.
|
||||||
:param startup_candles: Additional candles to load at the start of the period
|
:param startup_candles: Additional candles to load at the start of the period
|
||||||
:param warn_no_data: Log a warning message when no data is found
|
:param warn_no_data: Log a warning message when no data is found
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
:return: DataFrame with ohlcv data, or empty DataFrame
|
:return: DataFrame with ohlcv data, or empty DataFrame
|
||||||
"""
|
"""
|
||||||
# Fix startup period
|
# Fix startup period
|
||||||
|
@ -165,17 +243,21 @@ class IDataHandler(ABC):
|
||||||
if startup_candles > 0 and timerange_startup:
|
if startup_candles > 0 and timerange_startup:
|
||||||
timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles)
|
timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles)
|
||||||
|
|
||||||
pairdf = self._ohlcv_load(pair, timeframe,
|
pairdf = self._ohlcv_load(
|
||||||
timerange=timerange_startup)
|
pair,
|
||||||
if self._check_empty_df(pairdf, pair, timeframe, warn_no_data):
|
timeframe,
|
||||||
|
timerange=timerange_startup,
|
||||||
|
candle_type=candle_type
|
||||||
|
)
|
||||||
|
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
|
||||||
return pairdf
|
return pairdf
|
||||||
else:
|
else:
|
||||||
enddate = pairdf.iloc[-1]['date']
|
enddate = pairdf.iloc[-1]['date']
|
||||||
|
|
||||||
if timerange_startup:
|
if timerange_startup:
|
||||||
self._validate_pairdata(pair, pairdf, timerange_startup)
|
self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup)
|
||||||
pairdf = trim_dataframe(pairdf, timerange_startup)
|
pairdf = trim_dataframe(pairdf, timerange_startup)
|
||||||
if self._check_empty_df(pairdf, pair, timeframe, warn_no_data):
|
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
|
||||||
return pairdf
|
return pairdf
|
||||||
|
|
||||||
# incomplete candles should only be dropped if we didn't trim the end beforehand.
|
# incomplete candles should only be dropped if we didn't trim the end beforehand.
|
||||||
|
@ -184,23 +266,25 @@ class IDataHandler(ABC):
|
||||||
fill_missing=fill_missing,
|
fill_missing=fill_missing,
|
||||||
drop_incomplete=(drop_incomplete and
|
drop_incomplete=(drop_incomplete and
|
||||||
enddate == pairdf.iloc[-1]['date']))
|
enddate == pairdf.iloc[-1]['date']))
|
||||||
self._check_empty_df(pairdf, pair, timeframe, warn_no_data)
|
self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data)
|
||||||
return pairdf
|
return pairdf
|
||||||
|
|
||||||
def _check_empty_df(self, pairdf: DataFrame, pair: str, timeframe: str, warn_no_data: bool):
|
def _check_empty_df(self, pairdf: DataFrame, pair: str, timeframe: str,
|
||||||
|
candle_type: CandleType, warn_no_data: bool):
|
||||||
"""
|
"""
|
||||||
Warn on empty dataframe
|
Warn on empty dataframe
|
||||||
"""
|
"""
|
||||||
if pairdf.empty:
|
if pairdf.empty:
|
||||||
if warn_no_data:
|
if warn_no_data:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'No history data for pair: "{pair}", timeframe: {timeframe}. '
|
f"No history for {pair}, {candle_type}, {timeframe} found. "
|
||||||
'Use `freqtrade download-data` to download the data'
|
"Use `freqtrade download-data` to download the data"
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _validate_pairdata(self, pair, pairdata: DataFrame, timerange: TimeRange):
|
def _validate_pairdata(self, pair, pairdata: DataFrame, timeframe: str,
|
||||||
|
candle_type: CandleType, timerange: TimeRange):
|
||||||
"""
|
"""
|
||||||
Validates pairdata for missing data at start end end and logs warnings.
|
Validates pairdata for missing data at start end end and logs warnings.
|
||||||
:param pairdata: Dataframe to validate
|
:param pairdata: Dataframe to validate
|
||||||
|
@ -210,12 +294,12 @@ class IDataHandler(ABC):
|
||||||
if timerange.starttype == 'date':
|
if timerange.starttype == 'date':
|
||||||
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
|
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
|
||||||
if pairdata.iloc[0]['date'] > start:
|
if pairdata.iloc[0]['date'] > start:
|
||||||
logger.warning(f"Missing data at start for pair {pair}, "
|
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
|
||||||
f"data starts at {pairdata.iloc[0]['date']:%Y-%m-%d %H:%M:%S}")
|
f"data starts at {pairdata.iloc[0]['date']:%Y-%m-%d %H:%M:%S}")
|
||||||
if timerange.stoptype == 'date':
|
if timerange.stoptype == 'date':
|
||||||
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
|
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
|
||||||
if pairdata.iloc[-1]['date'] < stop:
|
if pairdata.iloc[-1]['date'] < stop:
|
||||||
logger.warning(f"Missing data at end for pair {pair}, "
|
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
|
||||||
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")
|
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ from freqtrade import misc
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes, TradeList
|
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes, TradeList
|
||||||
from freqtrade.data.converter import trades_dict_to_list
|
from freqtrade.data.converter import trades_dict_to_list
|
||||||
|
from freqtrade.enums import CandleType, TradingMode
|
||||||
|
|
||||||
from .idatahandler import IDataHandler
|
from .idatahandler import IDataHandler
|
||||||
|
|
||||||
|
@ -23,33 +24,49 @@ class JsonDataHandler(IDataHandler):
|
||||||
_columns = DEFAULT_DATAFRAME_COLUMNS
|
_columns = DEFAULT_DATAFRAME_COLUMNS
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
|
def ohlcv_get_available_data(
|
||||||
|
cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes:
|
||||||
"""
|
"""
|
||||||
Returns a list of all pairs with ohlcv data available in this datadir
|
Returns a list of all pairs with ohlcv data available in this datadir
|
||||||
:param datadir: Directory to search for ohlcv files
|
:param datadir: Directory to search for ohlcv files
|
||||||
|
:param trading_mode: trading-mode to be used
|
||||||
:return: List of Tuples of (pair, timeframe)
|
:return: List of Tuples of (pair, timeframe)
|
||||||
"""
|
"""
|
||||||
_tmp = [re.search(r'^([a-zA-Z_]+)\-(\d+\S+)(?=.json)', p.name)
|
if trading_mode == 'futures':
|
||||||
for p in datadir.glob(f"*.{cls._get_file_extension()}")]
|
datadir = datadir.joinpath('futures')
|
||||||
return [(match[1].replace('_', '/'), match[2]) for match in _tmp
|
_tmp = [
|
||||||
if match and len(match.groups()) > 1]
|
re.search(
|
||||||
|
cls._OHLCV_REGEX, p.name
|
||||||
|
) for p in datadir.glob(f"*.{cls._get_file_extension()}")]
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
cls.rebuild_pair_from_filename(match[1]),
|
||||||
|
match[2],
|
||||||
|
CandleType.from_string(match[3])
|
||||||
|
) for match in _tmp if match and len(match.groups()) > 1]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]:
|
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Returns a list of all pairs with ohlcv data available in this datadir
|
Returns a list of all pairs with ohlcv data available in this datadir
|
||||||
for the specified timeframe
|
for the specified timeframe
|
||||||
:param datadir: Directory to search for ohlcv files
|
:param datadir: Directory to search for ohlcv files
|
||||||
:param timeframe: Timeframe to search pairs for
|
:param timeframe: Timeframe to search pairs for
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
:return: List of Pairs
|
:return: List of Pairs
|
||||||
"""
|
"""
|
||||||
|
candle = ""
|
||||||
|
if candle_type != CandleType.SPOT:
|
||||||
|
datadir = datadir.joinpath('futures')
|
||||||
|
candle = f"-{candle_type}"
|
||||||
|
|
||||||
_tmp = [re.search(r'^(\S+)(?=\-' + timeframe + '.json)', p.name)
|
_tmp = [re.search(r'^(\S+)(?=\-' + timeframe + candle + '.json)', p.name)
|
||||||
for p in datadir.glob(f"*{timeframe}.{cls._get_file_extension()}")]
|
for p in datadir.glob(f"*{timeframe}{candle}.{cls._get_file_extension()}")]
|
||||||
# Check if regex found something and only return these results
|
# Check if regex found something and only return these results
|
||||||
return [match[0].replace('_', '/') for match in _tmp if match]
|
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
||||||
|
|
||||||
def ohlcv_store(self, pair: str, timeframe: str, data: DataFrame) -> None:
|
def ohlcv_store(
|
||||||
|
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType) -> None:
|
||||||
"""
|
"""
|
||||||
Store data in json format "values".
|
Store data in json format "values".
|
||||||
format looks as follows:
|
format looks as follows:
|
||||||
|
@ -57,9 +74,11 @@ class JsonDataHandler(IDataHandler):
|
||||||
:param pair: Pair - used to generate filename
|
:param pair: Pair - used to generate filename
|
||||||
:param timeframe: Timeframe - used to generate filename
|
:param timeframe: Timeframe - used to generate filename
|
||||||
:param data: Dataframe containing OHLCV data
|
:param data: Dataframe containing OHLCV data
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
|
||||||
|
self.create_dir_if_needed(filename)
|
||||||
_data = data.copy()
|
_data = data.copy()
|
||||||
# Convert date to int
|
# Convert date to int
|
||||||
_data['date'] = _data['date'].view(np.int64) // 1000 // 1000
|
_data['date'] = _data['date'].view(np.int64) // 1000 // 1000
|
||||||
|
@ -70,7 +89,7 @@ class JsonDataHandler(IDataHandler):
|
||||||
compression='gzip' if self._use_zip else None)
|
compression='gzip' if self._use_zip else None)
|
||||||
|
|
||||||
def _ohlcv_load(self, pair: str, timeframe: str,
|
def _ohlcv_load(self, pair: str, timeframe: str,
|
||||||
timerange: Optional[TimeRange] = None,
|
timerange: Optional[TimeRange], candle_type: CandleType
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Internal method used to load data for one pair from disk.
|
Internal method used to load data for one pair from disk.
|
||||||
|
@ -81,9 +100,10 @@ class JsonDataHandler(IDataHandler):
|
||||||
:param timerange: Limit data to be loaded to this timerange.
|
:param timerange: Limit data to be loaded to this timerange.
|
||||||
Optionally implemented by subclasses to avoid loading
|
Optionally implemented by subclasses to avoid loading
|
||||||
all data where possible.
|
all data where possible.
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
:return: DataFrame with ohlcv data, or empty DataFrame
|
:return: DataFrame with ohlcv data, or empty DataFrame
|
||||||
"""
|
"""
|
||||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type=candle_type)
|
||||||
if not filename.exists():
|
if not filename.exists():
|
||||||
return DataFrame(columns=self._columns)
|
return DataFrame(columns=self._columns)
|
||||||
try:
|
try:
|
||||||
|
@ -100,25 +120,19 @@ class JsonDataHandler(IDataHandler):
|
||||||
infer_datetime_format=True)
|
infer_datetime_format=True)
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
|
def ohlcv_append(
|
||||||
"""
|
self,
|
||||||
Remove data for this pair
|
pair: str,
|
||||||
:param pair: Delete data for this pair.
|
timeframe: str,
|
||||||
:param timeframe: Timeframe (e.g. "5m")
|
data: DataFrame,
|
||||||
:return: True when deleted, false if file did not exist.
|
candle_type: CandleType
|
||||||
"""
|
) -> None:
|
||||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
|
||||||
if filename.exists():
|
|
||||||
filename.unlink()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def ohlcv_append(self, pair: str, timeframe: str, data: DataFrame) -> None:
|
|
||||||
"""
|
"""
|
||||||
Append data to existing data structures
|
Append data to existing data structures
|
||||||
:param pair: Pair
|
:param pair: Pair
|
||||||
:param timeframe: Timeframe this ohlcv data is for
|
:param timeframe: Timeframe this ohlcv data is for
|
||||||
:param data: Data to append.
|
:param data: Data to append.
|
||||||
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@ -132,7 +146,7 @@ class JsonDataHandler(IDataHandler):
|
||||||
_tmp = [re.search(r'^(\S+)(?=\-trades.json)', p.name)
|
_tmp = [re.search(r'^(\S+)(?=\-trades.json)', p.name)
|
||||||
for p in datadir.glob(f"*trades.{cls._get_file_extension()}")]
|
for p in datadir.glob(f"*trades.{cls._get_file_extension()}")]
|
||||||
# Check if regex found something and only return these results to avoid exceptions.
|
# Check if regex found something and only return these results to avoid exceptions.
|
||||||
return [match[0].replace('_', '/') for match in _tmp if match]
|
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
||||||
|
|
||||||
def trades_store(self, pair: str, data: TradeList) -> None:
|
def trades_store(self, pair: str, data: TradeList) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -174,34 +188,10 @@ class JsonDataHandler(IDataHandler):
|
||||||
pass
|
pass
|
||||||
return tradesdata
|
return tradesdata
|
||||||
|
|
||||||
def trades_purge(self, pair: str) -> bool:
|
|
||||||
"""
|
|
||||||
Remove data for this pair
|
|
||||||
:param pair: Delete data for this pair.
|
|
||||||
:return: True when deleted, false if file did not exist.
|
|
||||||
"""
|
|
||||||
filename = self._pair_trades_filename(self._datadir, pair)
|
|
||||||
if filename.exists():
|
|
||||||
filename.unlink()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path:
|
|
||||||
pair_s = misc.pair_to_filename(pair)
|
|
||||||
filename = datadir.joinpath(f'{pair_s}-{timeframe}.{cls._get_file_extension()}')
|
|
||||||
return filename
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_extension(cls):
|
def _get_file_extension(cls):
|
||||||
return "json.gz" if cls._use_zip else "json"
|
return "json.gz" if cls._use_zip else "json"
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path:
|
|
||||||
pair_s = misc.pair_to_filename(pair)
|
|
||||||
filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}')
|
|
||||||
return filename
|
|
||||||
|
|
||||||
|
|
||||||
class JsonGzDataHandler(JsonDataHandler):
|
class JsonGzDataHandler(JsonDataHandler):
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ from pandas import DataFrame
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT
|
||||||
from freqtrade.data.history import get_timerange, load_data, refresh_data
|
from freqtrade.data.history import get_timerange, load_data, refresh_data
|
||||||
from freqtrade.enums import RunMode, SellType
|
from freqtrade.enums import CandleType, ExitType, RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.exchange import timeframe_to_seconds
|
from freqtrade.exchange.exchange import timeframe_to_seconds
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
|
@ -116,11 +116,12 @@ class Edge:
|
||||||
timeframe=self.strategy.timeframe,
|
timeframe=self.strategy.timeframe,
|
||||||
timerange=timerange_startup,
|
timerange=timerange_startup,
|
||||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||||
|
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
|
||||||
)
|
)
|
||||||
# Download informative pairs too
|
# Download informative pairs too
|
||||||
res = defaultdict(list)
|
res = defaultdict(list)
|
||||||
for p, t in self.strategy.gather_informative_pairs():
|
for pair, timeframe, _ in self.strategy.gather_informative_pairs():
|
||||||
res[t].append(p)
|
res[timeframe].append(pair)
|
||||||
for timeframe, inf_pairs in res.items():
|
for timeframe, inf_pairs in res.items():
|
||||||
timerange_startup = deepcopy(self._timerange)
|
timerange_startup = deepcopy(self._timerange)
|
||||||
timerange_startup.subtract_start(timeframe_to_seconds(
|
timerange_startup.subtract_start(timeframe_to_seconds(
|
||||||
|
@ -132,6 +133,7 @@ class Edge:
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
timerange=timerange_startup,
|
timerange=timerange_startup,
|
||||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||||
|
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
|
||||||
)
|
)
|
||||||
|
|
||||||
data = load_data(
|
data = load_data(
|
||||||
|
@ -141,6 +143,7 @@ class Edge:
|
||||||
timerange=self._timerange,
|
timerange=self._timerange,
|
||||||
startup_candles=self.strategy.startup_candle_count,
|
startup_candles=self.strategy.startup_candle_count,
|
||||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||||
|
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
|
@ -159,7 +162,9 @@ class Edge:
|
||||||
logger.info(f'Measuring data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
|
logger.info(f'Measuring data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||||
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
|
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||||
f'({(max_date - min_date).days} days)..')
|
f'({(max_date - min_date).days} days)..')
|
||||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low']
|
# TODO: Should edge support shorts? needs to be investigated further
|
||||||
|
# * (add enter_short exit_short)
|
||||||
|
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long']
|
||||||
|
|
||||||
trades: list = []
|
trades: list = []
|
||||||
for pair, pair_data in preprocessed.items():
|
for pair, pair_data in preprocessed.items():
|
||||||
|
@ -167,8 +172,13 @@ class Edge:
|
||||||
pair_data = pair_data.sort_values(by=['date'])
|
pair_data = pair_data.sort_values(by=['date'])
|
||||||
pair_data = pair_data.reset_index(drop=True)
|
pair_data = pair_data.reset_index(drop=True)
|
||||||
|
|
||||||
df_analyzed = self.strategy.advise_sell(
|
df_analyzed = self.strategy.advise_exit(
|
||||||
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
|
dataframe=self.strategy.advise_entry(
|
||||||
|
dataframe=pair_data,
|
||||||
|
metadata={'pair': pair}
|
||||||
|
),
|
||||||
|
metadata={'pair': pair}
|
||||||
|
)[headers].copy()
|
||||||
|
|
||||||
trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)
|
trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)
|
||||||
|
|
||||||
|
@ -219,9 +229,11 @@ class Edge:
|
||||||
"""
|
"""
|
||||||
final = []
|
final = []
|
||||||
for pair, info in self._cached_pairs.items():
|
for pair, info in self._cached_pairs.items():
|
||||||
if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
|
if (
|
||||||
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)) and \
|
info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2))
|
||||||
pair in pairs:
|
and info.winrate > float(self.edge_config.get('minimum_winrate', 0.60))
|
||||||
|
and pair in pairs
|
||||||
|
):
|
||||||
final.append(pair)
|
final.append(pair)
|
||||||
|
|
||||||
if self._final_pairs != final:
|
if self._final_pairs != final:
|
||||||
|
@ -246,8 +258,8 @@ class Edge:
|
||||||
"""
|
"""
|
||||||
final = []
|
final = []
|
||||||
for pair, info in self._cached_pairs.items():
|
for pair, info in self._cached_pairs.items():
|
||||||
if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
|
if (info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and
|
||||||
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)):
|
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60))):
|
||||||
final.append({
|
final.append({
|
||||||
'Pair': pair,
|
'Pair': pair,
|
||||||
'Winrate': info.winrate,
|
'Winrate': info.winrate,
|
||||||
|
@ -382,8 +394,8 @@ class Edge:
|
||||||
return final
|
return final
|
||||||
|
|
||||||
def _find_trades_for_stoploss_range(self, df, pair, stoploss_range):
|
def _find_trades_for_stoploss_range(self, df, pair, stoploss_range):
|
||||||
buy_column = df['buy'].values
|
buy_column = df['enter_long'].values
|
||||||
sell_column = df['sell'].values
|
sell_column = df['exit_long'].values
|
||||||
date_column = df['date'].values
|
date_column = df['date'].values
|
||||||
ohlc_columns = df[['open', 'high', 'low', 'close']].values
|
ohlc_columns = df[['open', 'high', 'low', 'close']].values
|
||||||
|
|
||||||
|
@ -448,7 +460,7 @@ class Edge:
|
||||||
|
|
||||||
if stop_index <= sell_index:
|
if stop_index <= sell_index:
|
||||||
exit_index = open_trade_index + stop_index
|
exit_index = open_trade_index + stop_index
|
||||||
exit_type = SellType.STOP_LOSS
|
exit_type = ExitType.STOP_LOSS
|
||||||
exit_price = stop_price
|
exit_price = stop_price
|
||||||
elif stop_index > sell_index:
|
elif stop_index > sell_index:
|
||||||
# If exit is SELL then we exit at the next candle
|
# If exit is SELL then we exit at the next candle
|
||||||
|
@ -458,7 +470,7 @@ class Edge:
|
||||||
if len(ohlc_columns) - 1 < exit_index:
|
if len(ohlc_columns) - 1 < exit_index:
|
||||||
break
|
break
|
||||||
|
|
||||||
exit_type = SellType.SELL_SIGNAL
|
exit_type = ExitType.EXIT_SIGNAL
|
||||||
exit_price = ohlc_columns[exit_index, 0]
|
exit_price = ohlc_columns[exit_index, 0]
|
||||||
|
|
||||||
trade = {'pair': pair,
|
trade = {'pair': pair,
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
from freqtrade.enums.backteststate import BacktestState
|
from freqtrade.enums.backteststate import BacktestState
|
||||||
|
from freqtrade.enums.candletype import CandleType
|
||||||
|
from freqtrade.enums.exitchecktuple import ExitCheckTuple
|
||||||
|
from freqtrade.enums.exittype import ExitType
|
||||||
|
from freqtrade.enums.marginmode import MarginMode
|
||||||
|
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
||||||
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
||||||
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
||||||
from freqtrade.enums.selltype import SellType
|
from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType
|
||||||
from freqtrade.enums.signaltype import SignalTagType, SignalType
|
|
||||||
from freqtrade.enums.state import State
|
from freqtrade.enums.state import State
|
||||||
|
from freqtrade.enums.tradingmode import TradingMode
|
||||||
|
|
27
freqtrade/enums/candletype.py
Normal file
27
freqtrade/enums/candletype.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class CandleType(str, Enum):
|
||||||
|
"""Enum to distinguish candle types"""
|
||||||
|
SPOT = "spot"
|
||||||
|
FUTURES = "futures"
|
||||||
|
MARK = "mark"
|
||||||
|
INDEX = "index"
|
||||||
|
PREMIUMINDEX = "premiumIndex"
|
||||||
|
|
||||||
|
# TODO: Could take up less memory if these weren't a CandleType
|
||||||
|
FUNDING_RATE = "funding_rate"
|
||||||
|
# BORROW_RATE = "borrow_rate" # * unimplemented
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_string(value: str) -> 'CandleType':
|
||||||
|
if not value:
|
||||||
|
# Default to spot
|
||||||
|
return CandleType.SPOT
|
||||||
|
return CandleType(value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_default(trading_mode: str) -> 'CandleType':
|
||||||
|
if trading_mode == 'futures':
|
||||||
|
return CandleType.FUTURES
|
||||||
|
return CandleType.SPOT
|
17
freqtrade/enums/exitchecktuple.py
Normal file
17
freqtrade/enums/exitchecktuple.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from freqtrade.enums.exittype import ExitType
|
||||||
|
|
||||||
|
|
||||||
|
class ExitCheckTuple:
|
||||||
|
"""
|
||||||
|
NamedTuple for Exit type + reason
|
||||||
|
"""
|
||||||
|
exit_type: ExitType
|
||||||
|
exit_reason: str = ''
|
||||||
|
|
||||||
|
def __init__(self, exit_type: ExitType, exit_reason: str = ''):
|
||||||
|
self.exit_type = exit_type
|
||||||
|
self.exit_reason = exit_reason or exit_type.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exit_flag(self):
|
||||||
|
return self.exit_type != ExitType.NONE
|
|
@ -1,18 +1,18 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class SellType(Enum):
|
class ExitType(Enum):
|
||||||
"""
|
"""
|
||||||
Enum to distinguish between sell reasons
|
Enum to distinguish between exit reasons
|
||||||
"""
|
"""
|
||||||
ROI = "roi"
|
ROI = "roi"
|
||||||
STOP_LOSS = "stop_loss"
|
STOP_LOSS = "stop_loss"
|
||||||
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
|
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
|
||||||
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
||||||
SELL_SIGNAL = "sell_signal"
|
EXIT_SIGNAL = "exit_signal"
|
||||||
FORCE_SELL = "force_sell"
|
FORCE_EXIT = "force_exit"
|
||||||
EMERGENCY_SELL = "emergency_sell"
|
EMERGENCY_EXIT = "emergency_exit"
|
||||||
CUSTOM_SELL = "custom_sell"
|
CUSTOM_EXIT = "custom_exit"
|
||||||
NONE = ""
|
NONE = ""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user