Merge remote-tracking branch 'upstream/develop' into feature/fetch-public-trades
@@ -1,22 +0,0 @@
|
|||||||
FROM freqtradeorg/freqtrade:develop_freqairl
|
|
||||||
|
|
||||||
USER root
|
|
||||||
# Install dependencies
|
|
||||||
COPY requirements-dev.txt /freqtrade/
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get -y install --no-install-recommends apt-utils dialog \
|
|
||||||
&& apt-get -y install --no-install-recommends git sudo vim build-essential \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \
|
|
||||||
&& echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \
|
|
||||||
&& echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \
|
|
||||||
&& chown ftuser:ftuser -R /home/ftuser/.local/ \
|
|
||||||
&& chown ftuser: -R /home/ftuser/
|
|
||||||
|
|
||||||
USER ftuser
|
|
||||||
|
|
||||||
RUN pip install --user autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir
|
|
||||||
|
|
||||||
# Empty the ENTRYPOINT to allow all commands
|
|
||||||
ENTRYPOINT []
|
|
||||||
@@ -1,42 +1,44 @@
|
|||||||
{
|
{
|
||||||
"name": "freqtrade Develop",
|
"name": "freqtrade Develop",
|
||||||
"build": {
|
"image": "ghcr.io/freqtrade/freqtrade-devcontainer:latest",
|
||||||
"dockerfile": "Dockerfile",
|
|
||||||
"context": ".."
|
|
||||||
},
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
"forwardPorts": [
|
"forwardPorts": [
|
||||||
8080
|
8080
|
||||||
],
|
],
|
||||||
"mounts": [
|
|
||||||
"source=freqtrade-bashhistory,target=/home/ftuser/commandhistory,type=volume"
|
|
||||||
],
|
|
||||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/freqtrade,type=bind,consistency=cached",
|
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/freqtrade,type=bind,consistency=cached",
|
||||||
// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
|
// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
|
||||||
"remoteUser": "ftuser",
|
"remoteUser": "ftuser",
|
||||||
|
|
||||||
"onCreateCommand": "pip install --user -e .",
|
"onCreateCommand": "pip install --user -e .",
|
||||||
"postCreateCommand": "freqtrade create-userdir --userdir user_data/",
|
"postCreateCommand": "freqtrade create-userdir --userdir user_data/",
|
||||||
|
|
||||||
"workspaceFolder": "/workspaces/freqtrade",
|
"workspaceFolder": "/workspaces/freqtrade",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"settings": {
|
"vscode": {
|
||||||
"terminal.integrated.shell.linux": "/bin/bash",
|
"settings": {
|
||||||
"editor.insertSpaces": true,
|
"terminal.integrated.shell.linux": "/bin/bash",
|
||||||
"files.trimTrailingWhitespace": true,
|
"editor.insertSpaces": true,
|
||||||
"[markdown]": {
|
"files.trimTrailingWhitespace": true,
|
||||||
"files.trimTrailingWhitespace": false,
|
"[markdown]": {
|
||||||
|
"files.trimTrailingWhitespace": false
|
||||||
|
},
|
||||||
|
"python.pythonPath": "/usr/local/bin/python",
|
||||||
|
"[python]": {
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"python.pythonPath": "/usr/local/bin/python",
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
},
|
"extensions": [
|
||||||
|
"ms-python.python",
|
||||||
// Add the IDs of extensions you want installed when the container is created.
|
"ms-python.vscode-pylance",
|
||||||
"extensions": [
|
"charliermarsh.ruff",
|
||||||
"ms-python.python",
|
"davidanson.vscode-markdownlint",
|
||||||
"ms-python.vscode-pylance",
|
"ms-azuretools.vscode-docker",
|
||||||
"davidanson.vscode-markdownlint",
|
"vscode-icons-team.vscode-icons",
|
||||||
"ms-azuretools.vscode-docker",
|
"github.vscode-github-actions",
|
||||||
"vscode-icons-team.vscode-icons",
|
],
|
||||||
],
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
FROM freqtradeorg/freqtrade:develop_freqairl
|
||||||
|
|
||||||
|
USER root
|
||||||
|
# Install dependencies
|
||||||
|
COPY requirements-dev.txt /freqtrade/
|
||||||
|
|
||||||
|
ARG USERNAME=ftuser
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get -y install --no-install-recommends apt-utils dialog git ssh vim build-essential zsh \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& mkdir -p /home/${USERNAME}/.vscode-server /home/${USERNAME}/.vscode-server-insiders /home/${USERNAME}/commandhistory \
|
||||||
|
&& chown ${USERNAME}:${USERNAME} -R /home/${USERNAME}/.local/ \
|
||||||
|
&& chown ${USERNAME}: -R /home/${USERNAME}/
|
||||||
|
|
||||||
|
USER ftuser
|
||||||
|
|
||||||
|
RUN pip install --user autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir
|
||||||
|
|
||||||
|
# Empty the ENTRYPOINT to allow all commands
|
||||||
|
ENTRYPOINT []
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "freqtrade Dev container image builder",
|
||||||
|
"build": {
|
||||||
|
"dockerfile": "Dockerfile",
|
||||||
|
"context": "../../"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/common-utils:2": {
|
||||||
|
},
|
||||||
|
"ghcr.io/stuartleeks/dev-container-features/shell-history:0.0.3": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,9 @@ updates:
|
|||||||
pytest:
|
pytest:
|
||||||
patterns:
|
patterns:
|
||||||
- "pytest*"
|
- "pytest*"
|
||||||
|
mkdocs:
|
||||||
|
patterns:
|
||||||
|
- "mkdocs*"
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
|
|||||||
@@ -111,7 +111,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Ruff
|
- name: Run Ruff
|
||||||
run: |
|
run: |
|
||||||
ruff check --output-format=github .
|
ruff check --output-format=github
|
||||||
|
|
||||||
|
- name: Run Ruff format check
|
||||||
|
run: |
|
||||||
|
ruff format --check
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
@@ -129,7 +133,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ "macos-latest", "macos-13", "macos-14" ]
|
os: [ "macos-12", "macos-13", "macos-14" ]
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||||
exclude:
|
exclude:
|
||||||
- os: "macos-14"
|
- os: "macos-14"
|
||||||
@@ -230,7 +234,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Ruff
|
- name: Run Ruff
|
||||||
run: |
|
run: |
|
||||||
ruff check --output-format=github .
|
ruff check --output-format=github
|
||||||
|
|
||||||
|
- name: Run Ruff format check
|
||||||
|
run: |
|
||||||
|
ruff format --check
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
@@ -300,7 +308,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Ruff
|
- name: Run Ruff
|
||||||
run: |
|
run: |
|
||||||
ruff check --output-format=github .
|
ruff check --output-format=github
|
||||||
|
|
||||||
|
- name: Run Ruff format check
|
||||||
|
run: |
|
||||||
|
ruff format --check
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
@@ -414,7 +426,7 @@ jobs:
|
|||||||
pytest --random-order --longrun --durations 20 -n auto
|
pytest --random-order --longrun --durations 20 -n auto
|
||||||
|
|
||||||
|
|
||||||
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
# Notify only once - when CI completes (and after deploy) in case it's successful
|
||||||
notify-complete:
|
notify-complete:
|
||||||
needs: [
|
needs: [
|
||||||
build-linux,
|
build-linux,
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
name: Devcontainer Pre-Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 3 * * 0"
|
||||||
|
# push:
|
||||||
|
# branches:
|
||||||
|
# - "master"
|
||||||
|
# tags:
|
||||||
|
# - "v*.*.*"
|
||||||
|
# pull_requests:
|
||||||
|
# branches:
|
||||||
|
# - "master"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "${{ github.workflow }}"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
id: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
-
|
||||||
|
name: Pre-build dev container image
|
||||||
|
uses: devcontainers/ci@v0.3
|
||||||
|
with:
|
||||||
|
subFolder: .github
|
||||||
|
imageName: ghcr.io/${{ github.repository }}-devcontainer
|
||||||
|
cacheFrom: ghcr.io/${{ github.repository }}-devcontainer
|
||||||
|
push: always
|
||||||
@@ -9,17 +9,17 @@ repos:
|
|||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: "v1.9.0"
|
rev: "v1.10.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.3.0.7
|
- types-cachetools==5.3.0.7
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.31.0.20240311
|
- types-requests==2.31.0.20240406
|
||||||
- types-tabulate==0.9.0.20240106
|
- types-tabulate==0.9.0.20240106
|
||||||
- types-python-dateutil==2.9.0.20240316
|
- types-python-dateutil==2.9.0.20240316
|
||||||
- SQLAlchemy==2.0.29
|
- SQLAlchemy==2.0.30
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
@@ -31,12 +31,12 @@ repos:
|
|||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.3.5'
|
rev: 'v0.4.4'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.5.0
|
rev: v4.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
exclude: |
|
exclude: |
|
||||||
@@ -54,3 +54,10 @@ repos:
|
|||||||
(?x)^(
|
(?x)^(
|
||||||
.*\.md
|
.*\.md
|
||||||
)$
|
)$
|
||||||
|
|
||||||
|
- repo: https://github.com/codespell-project/codespell
|
||||||
|
rev: v2.2.6
|
||||||
|
hooks:
|
||||||
|
- id: codespell
|
||||||
|
additional_dependencies:
|
||||||
|
- tomli
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"ms-python.python",
|
||||||
|
"ms-python.vscode-pylance",
|
||||||
|
"charliermarsh.ruff",
|
||||||
|
"davidanson.vscode-markdownlint",
|
||||||
|
"ms-azuretools.vscode-docker",
|
||||||
|
"vscode-icons-team.vscode-icons",
|
||||||
|
"github.vscode-github-actions",
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -72,12 +72,12 @@ you can manually run pre-commit with `pre-commit run -a`.
|
|||||||
mypy freqtrade
|
mypy freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Ensure all imports are correct
|
### 4. Ensure formatting is correct
|
||||||
|
|
||||||
#### Run isort
|
#### Run ruff
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
isort .
|
ruff format .
|
||||||
```
|
```
|
||||||
|
|
||||||
## (Core)-Committer Guide
|
## (Core)-Committer Guide
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.12.2-slim-bookworm as base
|
FROM python:3.12.3-slim-bookworm as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
|||||||
@@ -6,21 +6,18 @@ from pathlib import Path
|
|||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
|
|
||||||
key = os.environ.get('FREQTRADE__EXCHANGE__KEY')
|
key = os.environ.get("FREQTRADE__EXCHANGE__KEY")
|
||||||
secret = os.environ.get('FREQTRADE__EXCHANGE__SECRET')
|
secret = os.environ.get("FREQTRADE__EXCHANGE__SECRET")
|
||||||
|
|
||||||
proxy = os.environ.get('CI_WEB_PROXY')
|
proxy = os.environ.get("CI_WEB_PROXY")
|
||||||
|
|
||||||
exchange = ccxt.binance({
|
exchange = ccxt.binance(
|
||||||
'apiKey': key,
|
{"apiKey": key, "secret": secret, "httpsProxy": proxy, "options": {"defaultType": "swap"}}
|
||||||
'secret': secret,
|
)
|
||||||
'httpsProxy': proxy,
|
|
||||||
'options': {'defaultType': 'swap'}
|
|
||||||
})
|
|
||||||
_ = exchange.load_markets()
|
_ = exchange.load_markets()
|
||||||
|
|
||||||
lev_tiers = exchange.fetch_leverage_tiers()
|
lev_tiers = exchange.fetch_leverage_tiers()
|
||||||
|
|
||||||
# Assumes this is running in the root of the repository.
|
# Assumes this is running in the root of the repository.
|
||||||
file = Path('freqtrade/exchange/binance_leverage_tiers.json')
|
file = Path("freqtrade/exchange/binance_leverage_tiers.json")
|
||||||
json.dump(dict(sorted(lev_tiers.items())), file.open('w'), indent=2)
|
json.dump(dict(sorted(lev_tiers.items())), file.open("w"), indent=2)
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from freqtrade_client import __version__ as client_version
|
|
||||||
|
|
||||||
from freqtrade import __version__ as ft_version
|
from freqtrade import __version__ as ft_version
|
||||||
|
from freqtrade_client import __version__ as client_version
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if ft_version != client_version:
|
if ft_version != client_version:
|
||||||
print(f"Versions do not match: \n"
|
print(f"Versions do not match: \nft: {ft_version} \nclient: {client_version}")
|
||||||
f"ft: {ft_version} \n"
|
|
||||||
f"client: {client_version}")
|
|
||||||
exit(1)
|
exit(1)
|
||||||
print(f"Versions match: ft: {ft_version}, client: {client_version}")
|
print(f"Versions match: ft: {ft_version}, client: {client_version}")
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# File used in CI to ensure pre-commit dependencies are kept uptodate.
|
# File used in CI to ensure pre-commit dependencies are kept up-to-date.
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -6,28 +6,30 @@ from pathlib import Path
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
pre_commit_file = Path('.pre-commit-config.yaml')
|
pre_commit_file = Path(".pre-commit-config.yaml")
|
||||||
require_dev = Path('requirements-dev.txt')
|
require_dev = Path("requirements-dev.txt")
|
||||||
require = Path('requirements.txt')
|
require = Path("requirements.txt")
|
||||||
|
|
||||||
with require_dev.open('r') as rfile:
|
with require_dev.open("r") as rfile:
|
||||||
requirements = rfile.readlines()
|
requirements = rfile.readlines()
|
||||||
|
|
||||||
with require.open('r') as rfile:
|
with require.open("r") as rfile:
|
||||||
requirements.extend(rfile.readlines())
|
requirements.extend(rfile.readlines())
|
||||||
|
|
||||||
# Extract types only
|
# Extract types only
|
||||||
type_reqs = [r.strip('\n') for r in requirements if r.startswith(
|
type_reqs = [
|
||||||
'types-') or r.startswith('SQLAlchemy')]
|
r.strip("\n") for r in requirements if r.startswith("types-") or r.startswith("SQLAlchemy")
|
||||||
|
]
|
||||||
|
|
||||||
with pre_commit_file.open('r') as file:
|
with pre_commit_file.open("r") as file:
|
||||||
f = yaml.load(file, Loader=yaml.FullLoader)
|
f = yaml.load(file, Loader=yaml.SafeLoader)
|
||||||
|
|
||||||
|
|
||||||
mypy_repo = [repo for repo in f['repos'] if repo['repo']
|
mypy_repo = [
|
||||||
== 'https://github.com/pre-commit/mirrors-mypy']
|
repo for repo in f["repos"] if repo["repo"] == "https://github.com/pre-commit/mirrors-mypy"
|
||||||
|
]
|
||||||
|
|
||||||
hooks = mypy_repo[0]['hooks'][0]['additional_dependencies']
|
hooks = mypy_repo[0]["hooks"][0]["additional_dependencies"]
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
for hook in hooks:
|
for hook in hooks:
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4 5
|
|||||||
```
|
```
|
||||||
|
|
||||||
This command will read from the last backtesting results. The `--analysis-groups` option is
|
This command will read from the last backtesting results. The `--analysis-groups` option is
|
||||||
used to specify the various tabular outputs showing the profit fo each group or trade,
|
used to specify the various tabular outputs showing the profit of each group or trade,
|
||||||
ranging from the simplest (0) to the most detailed per pair, per buy and per sell tag (4):
|
ranging from the simplest (0) to the most detailed per pair, per buy and per sell tag (4):
|
||||||
|
|
||||||
* 0: overall winrate and profit summary by enter_tag
|
* 0: overall winrate and profit summary by enter_tag
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ This guide walks you through utilizing public trade data for advanced orderflow
|
|||||||
```json
|
```json
|
||||||
"orderflow": {
|
"orderflow": {
|
||||||
"scale": 0.5,
|
"scale": 0.5,
|
||||||
"stacked_imbalance_range": 3, // needs at least this amount of imblance next to each other
|
"stacked_imbalance_range": 3, // needs at least this amount of imbalance next to each other
|
||||||
"imbalance_volume": 1, // filters out below
|
"imbalance_volume": 1, // filters out below
|
||||||
"imbalance_ratio": 300 // filters out ratio lower than
|
"imbalance_ratio": 300 // filters out ratio lower than
|
||||||
},
|
},
|
||||||
|
|||||||
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 79 KiB |
@@ -522,8 +522,8 @@ To save time, by default backtest will reuse a cached result from within the las
|
|||||||
|
|
||||||
### Further backtest-result analysis
|
### Further backtest-result analysis
|
||||||
|
|
||||||
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
|
To further analyze your backtest results, freqtrade will export the trades to file by default.
|
||||||
You can then load the trades to perform further analysis as shown in the [data analysis](data-analysis.md#backtesting) backtesting section.
|
You can then load the trades to perform further analysis as shown in the [data analysis](strategy_analysis_example.md#load-backtest-results-to-pandas-dataframe) backtesting section.
|
||||||
|
|
||||||
## Assumptions made by backtesting
|
## Assumptions made by backtesting
|
||||||
|
|
||||||
@@ -531,12 +531,13 @@ Since backtesting lacks some detailed information about what happens within a ca
|
|||||||
|
|
||||||
- Exchange [trading limits](#trading-limits-in-backtesting) are respected
|
- Exchange [trading limits](#trading-limits-in-backtesting) are respected
|
||||||
- Entries happen at open-price
|
- Entries happen at open-price
|
||||||
- All orders are filled at the requested price (no slippage, no unfilled orders)
|
- All orders are filled at the requested price (no slippage) as long as the price is within the candle's high/low range
|
||||||
- Exit-signal exits happen at open-price of the consecutive candle
|
- Exit-signal exits happen at open-price of the consecutive candle
|
||||||
|
- Exits don't free their trade slot for a new trade until the next candle
|
||||||
- Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open
|
- Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open
|
||||||
- ROI
|
- ROI
|
||||||
- exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%)
|
- Exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%)
|
||||||
- exits are never "below the candle", so a ROI of 2% may result in a exit at 2.4% if low was at 2.4% profit
|
- Exits are never "below the candle", so a ROI of 2% may result in a exit at 2.4% if low was at 2.4% profit
|
||||||
- ROI entries which came into effect on the triggering candle (e.g. `120: 0.02` for 1h candles, from `60: 0.05`) will use the candle's open as exit rate
|
- ROI entries which came into effect on the triggering candle (e.g. `120: 0.02` for 1h candles, from `60: 0.05`) will use the candle's open as exit rate
|
||||||
- Force-exits caused by `<N>=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles)
|
- Force-exits caused by `<N>=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles)
|
||||||
- Stoploss exits happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price
|
- Stoploss exits happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price
|
||||||
@@ -587,7 +588,7 @@ These precision values are based on current exchange limits (as described in the
|
|||||||
|
|
||||||
## Improved backtest accuracy
|
## Improved backtest accuracy
|
||||||
|
|
||||||
One big limitation of backtesting is it's inability to know how prices moved intra-candle (was high before close, or viceversa?).
|
One big limitation of backtesting is it's inability to know how prices moved intra-candle (was high before close, or vice-versa?).
|
||||||
So assuming you run backtesting with a 1h timeframe, there will be 4 prices for that candle (Open, High, Low, Close).
|
So assuming you run backtesting with a 1h timeframe, there will be 4 prices for that candle (Open, High, Low, Close).
|
||||||
|
|
||||||
While backtesting does take some assumptions (read above) about this - this can never be perfect, and will always be biased in one way or the other.
|
While backtesting does take some assumptions (read above) about this - this can never be perfect, and will always be biased in one way or the other.
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean
|
| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean
|
||||||
| `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `-1`.*<br> **Datatype:** Positive Integer or -1
|
| `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `-1`.*<br> **Datatype:** Positive Integer or -1
|
||||||
| | **Exchange**
|
| | **Exchange**
|
||||||
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
|
| `exchange.name` | **Required.** Name of the exchange class to use. <br> **Datatype:** String
|
||||||
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
@@ -252,7 +252,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `disable_dataframe_checks` | Disable checking the OHLCV dataframe returned from the strategy methods for correctness. Only use when intentionally changing the dataframe and understand what you are doing. [Strategy Override](#parameters-in-the-strategy).<br> *Defaults to `False`*. <br> **Datatype:** Boolean
|
| `disable_dataframe_checks` | Disable checking the OHLCV dataframe returned from the strategy methods for correctness. Only use when intentionally changing the dataframe and understand what you are doing. [Strategy Override](#parameters-in-the-strategy).<br> *Defaults to `False`*. <br> **Datatype:** Boolean
|
||||||
| `internals.process_throttle_secs` | Set the process throttle, or minimum loop duration for one bot iteration loop. Value in second. <br>*Defaults to `5` seconds.* <br> **Datatype:** Positive Integer
|
| `internals.process_throttle_secs` | Set the process throttle, or minimum loop duration for one bot iteration loop. Value in second. <br>*Defaults to `5` seconds.* <br> **Datatype:** Positive Integer
|
||||||
| `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages. <br>*Defaults to `60` seconds.* <br> **Datatype:** Positive Integer or 0
|
| `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages. <br>*Defaults to `60` seconds.* <br> **Datatype:** Positive Integer or 0
|
||||||
| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean
|
| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](advanced-setup.md#configure-the-bot-running-as-a-systemd-service) for more details. <br> **Datatype:** Boolean
|
||||||
| `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> **Datatype:** ClassName
|
| `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> **Datatype:** ClassName
|
||||||
| `strategy_path` | Adds an additional strategy lookup path (must be a directory). <br> **Datatype:** String
|
| `strategy_path` | Adds an additional strategy lookup path (must be a directory). <br> **Datatype:** String
|
||||||
| `recursive_strategy_search` | Set to `true` to recursively search sub-directories inside `user_data/strategies` for a strategy. <br> **Datatype:** Boolean
|
| `recursive_strategy_search` | Set to `true` to recursively search sub-directories inside `user_data/strategies` for a strategy. <br> **Datatype:** Boolean
|
||||||
@@ -370,7 +370,7 @@ This setting works in combination with `max_open_trades`. The maximum capital en
|
|||||||
For example, the bot will at most use (0.05 BTC x 3) = 0.15 BTC, assuming a configuration of `max_open_trades=3` and `stake_amount=0.05`.
|
For example, the bot will at most use (0.05 BTC x 3) = 0.15 BTC, assuming a configuration of `max_open_trades=3` and `stake_amount=0.05`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This setting respects the [available balance configuration](#available-balance).
|
This setting respects the [available balance configuration](#tradable-balance).
|
||||||
|
|
||||||
#### Dynamic stake amount
|
#### Dynamic stake amount
|
||||||
|
|
||||||
@@ -547,7 +547,7 @@ is automatically cancelled by the exchange.
|
|||||||
**PO (Post only):**
|
**PO (Post only):**
|
||||||
|
|
||||||
Post only order. The order is either placed as a maker order, or it is canceled.
|
Post only order. The order is either placed as a maker order, or it is canceled.
|
||||||
This means the order must be placed on orderbook for at at least time in an unfilled state.
|
This means the order must be placed on orderbook for at least time in an unfilled state.
|
||||||
|
|
||||||
#### time_in_force config
|
#### time_in_force config
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ Details will obviously vary between setups - but this should work to get you sta
|
|||||||
``` json
|
``` json
|
||||||
{
|
{
|
||||||
"name": "freqtrade trade",
|
"name": "freqtrade trade",
|
||||||
"type": "python",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "freqtrade",
|
"module": "freqtrade",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
@@ -261,7 +261,7 @@ For that reason, they must implement the following methods:
|
|||||||
|
|
||||||
The `until` portion should be calculated using the provided `calculate_lock_end()` method.
|
The `until` portion should be calculated using the provided `calculate_lock_end()` method.
|
||||||
|
|
||||||
All Protections should use `"stop_duration"` / `"stop_duration_candles"` to define how long a a pair (or all pairs) should be locked.
|
All Protections should use `"stop_duration"` / `"stop_duration_candles"` to define how long a pair (or all pairs) should be locked.
|
||||||
The content of this is made available as `self._stop_duration` to the each Protection.
|
The content of this is made available as `self._stop_duration` to the each Protection.
|
||||||
|
|
||||||
If your protection requires a look-back period, please use `"lookback_period"` / `"lockback_period_candles"` to keep all protections aligned.
|
If your protection requires a look-back period, please use `"lookback_period"` / `"lockback_period_candles"` to keep all protections aligned.
|
||||||
@@ -305,7 +305,7 @@ The `IProtection` parent class provides a helper method for this in `calculate_l
|
|||||||
|
|
||||||
Most exchanges supported by CCXT should work out of the box.
|
Most exchanges supported by CCXT should work out of the box.
|
||||||
|
|
||||||
To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`.
|
To quickly test the public endpoints of an exchange, add a configuration for your exchange to `tests/exchange_online/conftest.py` and run these tests with `pytest --longrun tests/exchange_online/test_ccxt_compat.py`.
|
||||||
Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar).
|
Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar).
|
||||||
|
|
||||||
Also try to use `freqtrade download-data` for an extended timerange (multiple months) and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded).
|
Also try to use `freqtrade download-data` for an extended timerange (multiple months) and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded).
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ $$ R = \frac{\text{average_profit}}{\text{average_loss}} = \frac{\mu_{win}}{\mu_
|
|||||||
|
|
||||||
### Expectancy
|
### Expectancy
|
||||||
|
|
||||||
By combining the Win Rate $W$ and and the Risk Reward ratio $R$ to create an expectancy ratio $E$. A expectance ratio is the expected return of the investment made in a trade. We can compute the value of $E$ as follows:
|
By combining the Win Rate $W$ and the Risk Reward ratio $R$ to create an expectancy ratio $E$. A expectance ratio is the expected return of the investment made in a trade. We can compute the value of $E$ as follows:
|
||||||
|
|
||||||
$$E = R * W - L$$
|
$$E = R * W - L$$
|
||||||
|
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ $ pip3 install web3
|
|||||||
Most exchanges return current incomplete candle via their OHLCV/klines API interface.
|
Most exchanges return current incomplete candle via their OHLCV/klines API interface.
|
||||||
By default, Freqtrade assumes that incomplete candle is fetched from the exchange and removes the last candle assuming it's the incomplete candle.
|
By default, Freqtrade assumes that incomplete candle is fetched from the exchange and removes the last candle assuming it's the incomplete candle.
|
||||||
|
|
||||||
Whether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#Incomplete-candles) from the Contributor documentation.
|
Whether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#incomplete-candles) from the Contributor documentation.
|
||||||
|
|
||||||
Due to the danger of repainting, Freqtrade does not allow you to use this incomplete candle.
|
Due to the danger of repainting, Freqtrade does not allow you to use this incomplete candle.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Supported Markets
|
## Supported Markets
|
||||||
|
|
||||||
Freqtrade supports spot trading, as well as (isolated) futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an uptodate list of supported exchanges.
|
Freqtrade supports spot trading, as well as (isolated) futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an up-to-date list of supported exchanges.
|
||||||
|
|
||||||
### Can my bot open short positions?
|
### Can my bot open short positions?
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ In spot markets, you can in some cases use leveraged spot tokens, which reflect
|
|||||||
|
|
||||||
### Can my bot trade options or futures?
|
### Can my bot trade options or futures?
|
||||||
|
|
||||||
Futures trading is supported for selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an uptodate list of supported exchanges.
|
Futures trading is supported for selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an up-to-date list of supported exchanges.
|
||||||
|
|
||||||
## Beginner Tips & Tricks
|
## Beginner Tips & Tricks
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# FreqUI
|
||||||
|
|
||||||
|
Freqtrade provides a builtin webserver, which can serve [FreqUI](https://github.com/freqtrade/frequi), the freqtrade frontend.
|
||||||
|
|
||||||
|
By default, the UI is automatically installed as part of the installation (script, docker).
|
||||||
|
freqUI can also be manually installed by using the `freqtrade install-ui` command.
|
||||||
|
This same command can also be used to update freqUI to new new releases.
|
||||||
|
|
||||||
|
Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured API port (by default `http://127.0.0.1:8080`).
|
||||||
|
|
||||||
|
??? Note "Looking to contribute to freqUI?"
|
||||||
|
Developers should not use this method, but instead clone the corresponding use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI. A working installation of node will be required to build the frontend.
|
||||||
|
|
||||||
|
!!! tip "freqUI is not required to run freqtrade"
|
||||||
|
freqUI is an optional component of freqtrade, and is not required to run the bot.
|
||||||
|
It is a frontend that can be used to monitor the bot and to interact with it - but freqtrade itself will work perfectly fine without it.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
FreqUI does not have it's own configuration file - but assumes a working setup for the [rest-api](rest-api.md) is available.
|
||||||
|
Please refer to the corresponding documentation page to get setup with freqUI
|
||||||
|
|
||||||
|
## UI
|
||||||
|
|
||||||
|
FreqUI is a modern, responsive web application that can be used to monitor and interact with your bot.
|
||||||
|
|
||||||
|
FreqUI provides a light, as well as a dark theme.
|
||||||
|
Themes can be easily switched via a prominent button at the top of the page.
|
||||||
|
The theme of the screenshots on this page will adapt to the selected documentation Theme, so to see the dark (or light) version, please switch the theme of the Documentation.
|
||||||
|
|
||||||
|
### Login
|
||||||
|
|
||||||
|
The below screenshot shows the login screen of freqUI.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
!!! Hint "CORS"
|
||||||
|
The Cors error shown in this screenshot is due to the fact that the UI is running on a different port than the API, and [CORS](#cors) has not been setup correctly yet.
|
||||||
|
|
||||||
|
### Trade view
|
||||||
|
|
||||||
|
The trade view allows you to visualize the trades that the bot is making and to interact with the bot.
|
||||||
|
On this page, you can also interact with the bot by starting and stopping it and - if configured - force trade entries and exits.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
### Plot Configurator
|
||||||
|
|
||||||
|
FreqUI Plots can be configured either via a `plot_config` configuration object in the strategy (which can be loaded via "from strategy" button) or via the UI.
|
||||||
|
Multiple plot configurations can be created and switched at will - allowing for flexible, different views into your charts.
|
||||||
|
|
||||||
|
The plot configuration can be accessed via the "Plot Configurator" (Cog icon) button in the top right corner of the trade view.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
### Settings
|
||||||
|
|
||||||
|
|
||||||
|
Several UI related settings can be changed by accessing the settings page.
|
||||||
|
|
||||||
|
Things you can change (among others):
|
||||||
|
|
||||||
|
* Timezone of the UI
|
||||||
|
* Visualization of open trades as part of the favicon (browser tab)
|
||||||
|
* Candle colors (up/down -> red/green)
|
||||||
|
* Enable / disable in-app notification types
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## Backtesting
|
||||||
|
|
||||||
|
When freqtrade is started in [webserver mode](utils.md#webserver-mode) (freqtrade started with `freqtrade webserver`), the backtesting view becomes available.
|
||||||
|
This view allows you to backtest strategies and visualize the results.
|
||||||
|
|
||||||
|
You can also load and visualize previous backtest results, as well as compare the results with each other.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
--8<-- "includes/cors.md"
|
||||||
@@ -235,7 +235,7 @@ By default, FreqAI builds a dynamic pipeline based on user congfiguration settin
|
|||||||
Users are encouraged to customize the data pipeline to their needs by building their own data pipeline. This can be done by simply setting `dk.feature_pipeline` to their desired `Pipeline` object inside their `IFreqaiModel` `train()` function, or if they prefer not to touch the `train()` function, they can override `define_data_pipeline`/`define_label_pipeline` functions in their `IFreqaiModel`:
|
Users are encouraged to customize the data pipeline to their needs by building their own data pipeline. This can be done by simply setting `dk.feature_pipeline` to their desired `Pipeline` object inside their `IFreqaiModel` `train()` function, or if they prefer not to touch the `train()` function, they can override `define_data_pipeline`/`define_label_pipeline` functions in their `IFreqaiModel`:
|
||||||
|
|
||||||
!!! note "More information available"
|
!!! note "More information available"
|
||||||
FreqAI uses the the [`DataSieve`](https://github.com/emergentmethods/datasieve) pipeline, which follows the SKlearn pipeline API, but adds, among other features, coherence between the X, y, and sample_weight vector point removals, feature removal, feature name following.
|
FreqAI uses the [`DataSieve`](https://github.com/emergentmethods/datasieve) pipeline, which follows the SKlearn pipeline API, but adds, among other features, coherence between the X, y, and sample_weight vector point removals, feature removal, feature name following.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from datasieve.transforms import SKLearnWrapper, DissimilarityIndex
|
from datasieve.transforms import SKLearnWrapper, DissimilarityIndex
|
||||||
@@ -391,3 +391,18 @@ Given a number of data points $N$, and a distance $\varepsilon$, DBSCAN clusters
|
|||||||

|

|
||||||
|
|
||||||
FreqAI uses `sklearn.cluster.DBSCAN` (details are available on scikit-learn's webpage [here](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html) (external website)) with `min_samples` ($N$) taken as 1/4 of the no. of time points (candles) in the feature set. `eps` ($\varepsilon$) is computed automatically as the elbow point in the *k-distance graph* computed from the nearest neighbors in the pairwise distances of all data points in the feature set.
|
FreqAI uses `sklearn.cluster.DBSCAN` (details are available on scikit-learn's webpage [here](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html) (external website)) with `min_samples` ($N$) taken as 1/4 of the no. of time points (candles) in the feature set. `eps` ($\varepsilon$) is computed automatically as the elbow point in the *k-distance graph* computed from the nearest neighbors in the pairwise distances of all data points in the feature set.
|
||||||
|
|
||||||
|
|
||||||
|
### Data dimensionality reduction with Principal Component Analysis
|
||||||
|
|
||||||
|
You can reduce the dimensionality of your features by activating the principal_component_analysis in the config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"freqai": {
|
||||||
|
"feature_parameters" : {
|
||||||
|
"principal_component_analysis": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will perform PCA on the features and reduce their dimensionality so that the explained variance of the data set is >= 0.999. Reducing data dimensionality makes training the model faster and hence allows for more up-to-date models.
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](freqai-feature-engineering.md). <br> **Datatype:** Dictionary.
|
| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](freqai-feature-engineering.md). <br> **Datatype:** Dictionary.
|
||||||
| `include_timeframes` | A list of timeframes that all indicators in `feature_engineering_expand_*()` will be created for. The list is added as features to the base indicators dataset. <br> **Datatype:** List of timeframes (strings).
|
| `include_timeframes` | A list of timeframes that all indicators in `feature_engineering_expand_*()` will be created for. The list is added as features to the base indicators dataset. <br> **Datatype:** List of timeframes (strings).
|
||||||
| `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `feature_engineering_expand_*()` during feature engineering (see details [here](freqai-feature-engineering.md)) will be created for each correlated coin. The correlated coins features are added to the base indicators dataset. <br> **Datatype:** List of assets (strings).
|
| `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `feature_engineering_expand_*()` during feature engineering (see details [here](freqai-feature-engineering.md)) will be created for each correlated coin. The correlated coins features are added to the base indicators dataset. <br> **Datatype:** List of assets (strings).
|
||||||
| `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `feature_engineering_expand_all()` (see `templates/FreqaiExampleStrategy.py` for detailed usage). You can create custom labels and choose whether to make use of this parameter or not. <br> **Datatype:** Positive integer.
|
| `label_period_candles` | Number of candles into the future that the labels are created for. This can be used in `set_freqai_targets()` (see `templates/FreqaiExampleStrategy.py` for detailed usage). This parameter is not necessarily required, you can create custom labels and choose whether to make use of this parameter or not. Please see `templates/FreqaiExampleStrategy.py` to see the example usage. <br> **Datatype:** Positive integer.
|
||||||
| `include_shifted_candles` | Add features from previous candles to subsequent candles with the intent of adding historical information. If used, FreqAI will duplicate and shift all features from the `include_shifted_candles` previous candles so that the information is available for the subsequent candle. <br> **Datatype:** Positive integer.
|
| `include_shifted_candles` | Add features from previous candles to subsequent candles with the intent of adding historical information. If used, FreqAI will duplicate and shift all features from the `include_shifted_candles` previous candles so that the information is available for the subsequent candle. <br> **Datatype:** Positive integer.
|
||||||
| `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)). <br> **Datatype:** Positive float (typically < 1).
|
| `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)). <br> **Datatype:** Positive float (typically < 1).
|
||||||
| `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `feature_engineering_*()` for indicator creation. FreqAI uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN. <br> **Datatype:** Positive integer.
|
| `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `feature_engineering_*()` for indicator creation. FreqAI uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN. <br> **Datatype:** Positive integer.
|
||||||
| `indicator_periods_candles` | Time periods to calculate indicators for. The indicators are added to the base indicator dataset. <br> **Datatype:** List of positive integers.
|
| `indicator_periods_candles` | Time periods to calculate indicators for. The indicators are added to the base indicator dataset. <br> **Datatype:** List of positive integers.
|
||||||
| `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](#reducing-data-dimensionality-with-principal-component-analysis) <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis) <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
| `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features. Plot is stored in `user_data/models/<identifier>/sub-train-<COIN>_<timestamp>.html`. <br> **Datatype:** Integer. <br> Default: `0`.
|
| `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features. Plot is stored in `user_data/models/<identifier>/sub-train-<COIN>_<timestamp>.html`. <br> **Datatype:** Integer. <br> Default: `0`.
|
||||||
| `DI_threshold` | Activates the use of the Dissimilarity Index for outlier detection when set to > 0. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Positive float (typically < 1).
|
| `DI_threshold` | Activates the use of the Dissimilarity Index for outlier detection when set to > 0. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Positive float (typically < 1).
|
||||||
| `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training dataset, as well as from incoming data points. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Boolean.
|
| `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training dataset, as well as from incoming data points. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Boolean.
|
||||||
@@ -55,7 +55,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| | **Data split parameters within the `freqai.data_split_parameters` sub dictionary**
|
| | **Data split parameters within the `freqai.data_split_parameters` sub dictionary**
|
||||||
| `data_split_parameters` | Include any additional parameters available from scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). <br> **Datatype:** Dictionary.
|
| `data_split_parameters` | Include any additional parameters available from scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). <br> **Datatype:** Dictionary.
|
||||||
| `test_size` | The fraction of data that should be used for testing instead of training. <br> **Datatype:** Positive float < 1.
|
| `test_size` | The fraction of data that should be used for testing instead of training. <br> **Datatype:** Positive float < 1.
|
||||||
| `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`. <br> **Datatype:** Boolean. <br> Defaut: `False`.
|
| `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
|
|
||||||
### Model training parameters
|
### Model training parameters
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ To learn how to get data for the pairs and exchange you're interested in, head o
|
|||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Since 2021.4 release you no longer have to write a separate hyperopt class, but can configure the parameters directly in the strategy.
|
Since 2021.4 release you no longer have to write a separate hyperopt class, but can configure the parameters directly in the strategy.
|
||||||
The legacy method is still supported, but it is no longer the recommended way of setting up hyperopt.
|
The legacy method was supported up to 2021.8 and has been removed in 2021.9.
|
||||||
The legacy documentation is available at [Legacy Hyperopt](advanced-hyperopt.md#legacy-hyperopt).
|
|
||||||
|
|
||||||
## Install hyperopt dependencies
|
## Install hyperopt dependencies
|
||||||
|
|
||||||
@@ -765,7 +764,7 @@ Override the `roi_space()` method if you need components of the ROI tables to va
|
|||||||
A sample for these methods can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces).
|
A sample for these methods can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces).
|
||||||
|
|
||||||
!!! Note "Reduced search space"
|
!!! Note "Reduced search space"
|
||||||
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
|
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#overriding-pre-defined-spaces) to change this to your needs.
|
||||||
|
|
||||||
### Understand Hyperopt Stoploss results
|
### Understand Hyperopt Stoploss results
|
||||||
|
|
||||||
@@ -807,7 +806,7 @@ If you have the `stoploss_space()` method in your custom hyperopt file, remove i
|
|||||||
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces).
|
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces).
|
||||||
|
|
||||||
!!! Note "Reduced search space"
|
!!! Note "Reduced search space"
|
||||||
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
|
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#overriding-pre-defined-spaces) to change this to your needs.
|
||||||
|
|
||||||
### Understand Hyperopt Trailing Stop results
|
### Understand Hyperopt Trailing Stop results
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
## CORS
|
||||||
|
|
||||||
|
This whole section is only necessary in cross-origin cases (where you multiple bot API's running on `localhost:8081`, `localhost:8082`, ...), and want to combine them into one FreqUI instance.
|
||||||
|
|
||||||
|
??? info "Technical explanation"
|
||||||
|
All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing.
|
||||||
|
Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems.
|
||||||
|
Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately.
|
||||||
|
|
||||||
|
Users can allow access from different origin URL's to the bot API via the `CORS_origins` configuration setting.
|
||||||
|
It consists of a list of allowed URL's that are allowed to consume resources from the bot's API.
|
||||||
|
|
||||||
|
Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
//...
|
||||||
|
"jwt_secret_key": "somethingrandom",
|
||||||
|
"CORS_origins": ["https://frequi.freqtrade.io"],
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the following (pretty common) case, FreqUI is accessible on `http://localhost:8080/trade` (this is what you see in your navbar when navigating to freqUI).
|
||||||
|

|
||||||
|
|
||||||
|
The correct configuration for this case is `http://localhost:8080` - the main part of the URL including the port.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
//...
|
||||||
|
"jwt_secret_key": "somethingrandom",
|
||||||
|
"CORS_origins": ["http://localhost:8080"],
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Tip "trailing Slash"
|
||||||
|
The trailing slash is not allowed in the `CORS_origins` configuration (e.g. `"http://localhots:8080/"`).
|
||||||
|
Such a configuration will not take effect, and the cors errors will remain.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
We strongly recommend to also set `jwt_secret_key` to something random and known only to yourself to avoid unauthorized access to your bot.
|
||||||
@@ -51,7 +51,7 @@ These requirements apply to both [Script Installation](#script-installation) and
|
|||||||
### Install code
|
### Install code
|
||||||
|
|
||||||
We've included/collected install instructions for Ubuntu, MacOS, and Windows. These are guidelines and your success may vary with other distros.
|
We've included/collected install instructions for Ubuntu, MacOS, and Windows. These are guidelines and your success may vary with other distros.
|
||||||
OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems.
|
OS Specific steps are listed first, the common section below is necessary for all systems.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Python3.9 or higher and the corresponding pip are assumed to be available.
|
Python3.9 or higher and the corresponding pip are assumed to be available.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ If you already have an existing strategy, please read the [strategy migration gu
|
|||||||
|
|
||||||
## Shorting
|
## Shorting
|
||||||
|
|
||||||
Shorting is not possible when trading with [`trading_mode`](#understand-tradingmode) set to `spot`. To short trade, `trading_mode` must be set to `margin`(currently unavailable) or [`futures`](#futures), with [`margin_mode`](#margin-mode) set to `cross`(currently unavailable) or [`isolated`](#isolated-margin-mode)
|
Shorting is not possible when trading with [`trading_mode`](#leverage-trading-modes) set to `spot`. To short trade, `trading_mode` must be set to `margin`(currently unavailable) or [`futures`](#futures), with [`margin_mode`](#margin-mode) set to `cross`(currently unavailable) or [`isolated`](#isolated-margin-mode)
|
||||||
|
|
||||||
For a strategy to short, the strategy class must set the class variable `can_short = True`
|
For a strategy to short, the strategy class must set the class variable `can_short = True`
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
markdown==3.6
|
markdown==3.6
|
||||||
mkdocs==1.5.3
|
mkdocs==1.6.0
|
||||||
mkdocs-material==9.5.16
|
mkdocs-material==9.5.22
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.7.1
|
pymdown-extensions==10.8.1
|
||||||
jinja2==3.1.3
|
jinja2==3.1.4
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
# REST API & FreqUI
|
# REST API
|
||||||
|
|
||||||
## FreqUI
|
## FreqUI
|
||||||
|
|
||||||
Freqtrade provides a builtin webserver, which can serve [FreqUI](https://github.com/freqtrade/frequi), the freqtrade UI.
|
FreqUI now has it's own dedicated [documentation section](frequi.md) - please refer to that section for all information regarding the FreqUI.
|
||||||
|
|
||||||
By default, the UI is not included in the installation (except for docker images), and must be installed explicitly with `freqtrade install-ui`.
|
|
||||||
This same command can also be used to update freqUI, should there be a new release.
|
|
||||||
|
|
||||||
Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`).
|
|
||||||
|
|
||||||
!!! Note "developers"
|
|
||||||
Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI.
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -89,7 +81,8 @@ Make sure that the following 2 lines are available in your docker-compose file:
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! Danger "Security warning"
|
!!! Danger "Security warning"
|
||||||
By using `8080:8080` in the docker port mapping, the API will be available to everyone connecting to the server under the correct port, so others may be able to control your bot.
|
By using `"8080:8080"` (or `"0.0.0.0:8080:8080"`) in the docker port mapping, the API will be available to everyone connecting to the server under the correct port, so others may be able to control your bot.
|
||||||
|
This **may** be safe if you're running the bot in a secure environment (like your home network), but it's not recommended to expose the API to the internet.
|
||||||
|
|
||||||
## Rest API
|
## Rest API
|
||||||
|
|
||||||
@@ -454,7 +447,7 @@ To properly configure your reverse proxy (securely), please consult it's documen
|
|||||||
- **Caddy**: Caddy v2 supports websockets out of the box, see the [documentation](https://caddyserver.com/docs/v2-upgrade#proxy)
|
- **Caddy**: Caddy v2 supports websockets out of the box, see the [documentation](https://caddyserver.com/docs/v2-upgrade#proxy)
|
||||||
|
|
||||||
!!! Tip "SSL certificates"
|
!!! Tip "SSL certificates"
|
||||||
You can use tools like certbot to setup ssl certificates to access your bot's UI through encrypted connection by using any fo the above reverse proxies.
|
You can use tools like certbot to setup ssl certificates to access your bot's UI through encrypted connection by using any of the above reverse proxies.
|
||||||
While this will protect your data in transit, we do not recommend to run the freqtrade API outside of your private network (VPN, SSH tunnel).
|
While this will protect your data in transit, we do not recommend to run the freqtrade API outside of your private network (VPN, SSH tunnel).
|
||||||
|
|
||||||
### OpenAPI interface
|
### OpenAPI interface
|
||||||
@@ -487,42 +480,4 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques
|
|||||||
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"}
|
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"}
|
||||||
```
|
```
|
||||||
|
|
||||||
### CORS
|
--8<-- "includes/cors.md"
|
||||||
|
|
||||||
This whole section is only necessary in cross-origin cases (where you multiple bot API's running on `localhost:8081`, `localhost:8082`, ...), and want to combine them into one FreqUI instance.
|
|
||||||
|
|
||||||
??? info "Technical explanation"
|
|
||||||
All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing.
|
|
||||||
Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems.
|
|
||||||
Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately.
|
|
||||||
|
|
||||||
Users can allow access from different origin URL's to the bot API via the `CORS_origins` configuration setting.
|
|
||||||
It consists of a list of allowed URL's that are allowed to consume resources from the bot's API.
|
|
||||||
|
|
||||||
Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary:
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
//...
|
|
||||||
"jwt_secret_key": "somethingrandom",
|
|
||||||
"CORS_origins": ["https://frequi.freqtrade.io"],
|
|
||||||
//...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In the following (pretty common) case, FreqUI is accessible on `http://localhost:8080/trade` (this is what you see in your navbar when navigating to freqUI).
|
|
||||||

|
|
||||||
|
|
||||||
The correct configuration for this case is `http://localhost:8080` - the main part of the URL including the port.
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
//...
|
|
||||||
"jwt_secret_key": "somethingrandom",
|
|
||||||
"CORS_origins": ["http://localhost:8080"],
|
|
||||||
//...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
We strongly recommend to also set `jwt_secret_key` to something random and known only to yourself to avoid unauthorized access to your bot.
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ You could also have a default stop loss when you are in the red with your buy (b
|
|||||||
For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used.
|
For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refer to next section with [offset enabled](#Trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset).
|
If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refer to next section with [offset enabled](#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset).
|
||||||
|
|
||||||
Both values require `trailing_stop` to be set to true and `trailing_stop_positive` with a value.
|
Both values require `trailing_stop` to be set to true and `trailing_stop_positive` with a value.
|
||||||
|
|
||||||
@@ -240,7 +240,7 @@ When using leverage, the same principle is applied - with stoploss defining the
|
|||||||
|
|
||||||
Therefore, a stoploss of 10% on a 10x trade would trigger on a 1% price move.
|
Therefore, a stoploss of 10% on a 10x trade would trigger on a 1% price move.
|
||||||
If your stake amount (own capital) was 100$ - this trade would be 1000$ at 10x (after leverage).
|
If your stake amount (own capital) was 100$ - this trade would be 1000$ at 10x (after leverage).
|
||||||
If price moves 1% - you've lost 10$ of your own capital - therfore stoploss will trigger in this case.
|
If price moves 1% - you've lost 10$ of your own capital - therefore stoploss will trigger in this case.
|
||||||
|
|
||||||
Make sure to be aware of this, and avoid using too tight stoploss (at 10x leverage, 10% risk may be too little to allow the trade to "breath" a little).
|
Make sure to be aware of this, and avoid using too tight stoploss (at 10x leverage, 10% risk may be too little to allow the trade to "breath" a little).
|
||||||
|
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_r
|
|||||||
|
|
||||||
## Exit tag
|
## Exit tag
|
||||||
|
|
||||||
Similar to [Buy Tagging](#buy-tag), you can also specify a sell tag.
|
Similar to [Entry Tagging](#enter-tag), you can also specify an exit tag.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
@@ -326,4 +326,4 @@ for val in self.buy_ema_short.range:
|
|||||||
dataframe = pd.concat(frames, axis=1)
|
dataframe = pd.concat(frames, axis=1)
|
||||||
```
|
```
|
||||||
|
|
||||||
Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existant.
|
Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existent.
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ During backtesting, `current_rate` (and `current_profit`) are provided against t
|
|||||||
The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price.
|
The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price.
|
||||||
Returning None will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss.
|
Returning None will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss.
|
||||||
|
|
||||||
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)).
|
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchangefreqtrade)).
|
||||||
|
|
||||||
!!! Note "Use of dates"
|
!!! Note "Use of dates"
|
||||||
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
|
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
|
||||||
@@ -332,7 +332,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
**kwargs) -> Optional[float]:
|
**kwargs) -> Optional[float]:
|
||||||
|
|
||||||
if current_profit < 0.04:
|
if current_profit < 0.04:
|
||||||
return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss
|
return None # return None to keep using the initial stoploss
|
||||||
|
|
||||||
# After reaching the desired offset, allow the stoploss to trail by half the profit
|
# After reaching the desired offset, allow the stoploss to trail by half the profit
|
||||||
desired_stoploss = current_profit / 2
|
desired_stoploss = current_profit / 2
|
||||||
@@ -450,7 +450,7 @@ Stoploss values returned from `custom_stoploss()` must specify a percentage rela
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation.
|
Full examples can be found in the [Custom stoploss](strategy-callbacks.md#custom-stoploss) section of the Documentation.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings.
|
Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings.
|
||||||
@@ -809,6 +809,7 @@ Returning a value more than the above (so remaining stake_amount would become ne
|
|||||||
|
|
||||||
``` python
|
``` python
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
from typing import Optional, Tuple, Union
|
||||||
|
|
||||||
|
|
||||||
class DigDeeperStrategy(IStrategy):
|
class DigDeeperStrategy(IStrategy):
|
||||||
@@ -948,7 +949,7 @@ If the cancellation of the original order fails, then the order will not be repl
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from datetime import timedelta
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ The metadata-dict (available for `populate_entry_trend`, `populate_exit_trend`,
|
|||||||
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
|
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
|
||||||
|
|
||||||
The Metadata-dict should not be modified and does not persist information across multiple calls.
|
The Metadata-dict should not be modified and does not persist information across multiple calls.
|
||||||
Instead, have a look at the [Storing information](strategy-advanced.md#Storing-information) section.
|
Instead, have a look at the [Storing information](strategy-advanced.md#storing-information-persistent) section.
|
||||||
|
|
||||||
## Strategy file loading
|
## Strategy file loading
|
||||||
|
|
||||||
@@ -551,8 +551,8 @@ for more information.
|
|||||||
|
|
||||||
# Define BTC/STAKE informative pair. A custom formatter may be specified for formatting
|
# Define BTC/STAKE informative pair. A custom formatter may be specified for formatting
|
||||||
# column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom
|
# column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom
|
||||||
# formatting. Available in populate_indicators and other methods as 'rsi_upper'.
|
# formatting. Available in populate_indicators and other methods as 'rsi_upper_1h'.
|
||||||
@informative('1h', 'BTC/{stake}', '{column}')
|
@informative('1h', 'BTC/{stake}', '{column}_{timeframe}')
|
||||||
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
|
dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
return dataframe
|
return dataframe
|
||||||
@@ -776,7 +776,7 @@ The orderbook structure is aligned with the order structure from [ccxt](https://
|
|||||||
Therefore, using `ob['bids'][0][0]` as demonstrated above will result in using the best bid price. `ob['bids'][0][1]` would look at the amount at this orderbook position.
|
Therefore, using `ob['bids'][0][0]` as demonstrated above will result in using the best bid price. `ob['bids'][0][1]` would look at the amount at this orderbook position.
|
||||||
|
|
||||||
!!! Warning "Warning about backtesting"
|
!!! Warning "Warning about backtesting"
|
||||||
The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used, as the method will return uptodate values.
|
The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used, as the method will return up-to-date values.
|
||||||
|
|
||||||
### *ticker(pair)*
|
### *ticker(pair)*
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ You can use bots in telegram groups by just adding them to the group. You can fi
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
For the Freqtrade configuration, you can then use the the full value (including `-` if it's there) as string:
|
For the Freqtrade configuration, you can then use the full value (including `-` if it's there) as string:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"chat_id": "-1001332619709"
|
"chat_id": "-1001332619709"
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ An `Order` object will always be tied to it's corresponding [`Trade`](#trade-obj
|
|||||||
### Order - Available attributes
|
### Order - Available attributes
|
||||||
|
|
||||||
an Order object is typically attached to a trade.
|
an Order object is typically attached to a trade.
|
||||||
Most properties here can be None as they are dependant on the exchange response.
|
Most properties here can be None as they are dependent on the exchange response.
|
||||||
|
|
||||||
| Attribute | DataType | Description |
|
| Attribute | DataType | Description |
|
||||||
|------------|-------------|-------------|
|
|------------|-------------|-------------|
|
||||||
@@ -141,7 +141,7 @@ Most properties here can be None as they are dependant on the exchange response.
|
|||||||
`amount` | float | Amount in base currency
|
`amount` | float | Amount in base currency
|
||||||
`filled` | float | Filled amount (in base currency)
|
`filled` | float | Filled amount (in base currency)
|
||||||
`remaining` | float | Remaining amount
|
`remaining` | float | Remaining amount
|
||||||
`cost` | float | Cost of the order - usually average * filled (*Exchange dependant on futures, may contain the cost with or without leverage and may be in contracts.*)
|
`cost` | float | Cost of the order - usually average * filled (*Exchange dependent on futures, may contain the cost with or without leverage and may be in contracts.*)
|
||||||
`stake_amount` | float | Stake amount used for this order. *Added in 2023.7.*
|
`stake_amount` | float | Stake amount used for this order. *Added in 2023.7.*
|
||||||
`order_date` | datetime | Order creation date **use `order_date_utc` instead**
|
`order_date` | datetime | Order creation date **use `order_date_utc` instead**
|
||||||
`order_date_utc` | datetime | Order creation date (in UTC)
|
`order_date_utc` | datetime | Order creation date (in UTC)
|
||||||
|
|||||||
@@ -1,21 +1,33 @@
|
|||||||
""" Freqtrade bot """
|
"""Freqtrade bot"""
|
||||||
__version__ = '2024.4-dev'
|
|
||||||
|
|
||||||
if 'dev' in __version__:
|
__version__ = "2024.5-dev"
|
||||||
|
|
||||||
|
if "dev" in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
freqtrade_basedir = Path(__file__).parent
|
freqtrade_basedir = Path(__file__).parent
|
||||||
|
|
||||||
__version__ = __version__ + '-' + subprocess.check_output(
|
__version__ = (
|
||||||
['git', 'log', '--format="%h"', '-n 1'],
|
__version__
|
||||||
stderr=subprocess.DEVNULL, cwd=freqtrade_basedir).decode("utf-8").rstrip().strip('"')
|
+ "-"
|
||||||
|
+ subprocess.check_output(
|
||||||
|
["git", "log", '--format="%h"', "-n 1"],
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
cwd=freqtrade_basedir,
|
||||||
|
)
|
||||||
|
.decode("utf-8")
|
||||||
|
.rstrip()
|
||||||
|
.strip('"')
|
||||||
|
)
|
||||||
|
|
||||||
except Exception: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
# git not available, ignore
|
# git not available, ignore
|
||||||
try:
|
try:
|
||||||
# Try Fallback to freqtrade_commit file (created by CI while building docker image)
|
# Try Fallback to freqtrade_commit file (created by CI while building docker image)
|
||||||
versionfile = Path('./freqtrade_commit')
|
versionfile = Path("./freqtrade_commit")
|
||||||
if versionfile.is_file():
|
if versionfile.is_file():
|
||||||
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
|
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ To launch Freqtrade as a module
|
|||||||
from freqtrade import main
|
from freqtrade import main
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main.main()
|
main.main()
|
||||||
|
|||||||
@@ -6,22 +6,39 @@ Contains all start-commands, subcommands and CLI Interface creation.
|
|||||||
Note: Be careful with file-scoped imports in these subfiles.
|
Note: Be careful with file-scoped imports in these subfiles.
|
||||||
as they are parsed on startup, nothing containing optional modules should be loaded.
|
as they are parsed on startup, nothing containing optional modules should be loaded.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from freqtrade.commands.analyze_commands import start_analysis_entries_exits
|
from freqtrade.commands.analyze_commands import start_analysis_entries_exits
|
||||||
from freqtrade.commands.arguments import Arguments
|
from freqtrade.commands.arguments import Arguments
|
||||||
from freqtrade.commands.build_config_commands import start_new_config, start_show_config
|
from freqtrade.commands.build_config_commands import start_new_config, start_show_config
|
||||||
from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades,
|
from freqtrade.commands.data_commands import (
|
||||||
start_download_data, start_list_data)
|
start_convert_data,
|
||||||
|
start_convert_trades,
|
||||||
|
start_download_data,
|
||||||
|
start_list_data,
|
||||||
|
)
|
||||||
from freqtrade.commands.db_commands import start_convert_db
|
from freqtrade.commands.db_commands import start_convert_db
|
||||||
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui,
|
from freqtrade.commands.deploy_commands import (
|
||||||
start_new_strategy)
|
start_create_userdir,
|
||||||
|
start_install_ui,
|
||||||
|
start_new_strategy,
|
||||||
|
)
|
||||||
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
|
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
|
||||||
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_freqAI_models,
|
from freqtrade.commands.list_commands import (
|
||||||
start_list_markets, start_list_strategies,
|
start_list_exchanges,
|
||||||
start_list_timeframes, start_show_trades)
|
start_list_freqAI_models,
|
||||||
from freqtrade.commands.optimize_commands import (start_backtesting, start_backtesting_show,
|
start_list_markets,
|
||||||
start_edge, start_hyperopt,
|
start_list_strategies,
|
||||||
start_lookahead_analysis,
|
start_list_timeframes,
|
||||||
start_recursive_analysis)
|
start_show_trades,
|
||||||
|
)
|
||||||
|
from freqtrade.commands.optimize_commands import (
|
||||||
|
start_backtesting,
|
||||||
|
start_backtesting_show,
|
||||||
|
start_edge,
|
||||||
|
start_hyperopt,
|
||||||
|
start_lookahead_analysis,
|
||||||
|
start_recursive_analysis,
|
||||||
|
)
|
||||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||||
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
||||||
from freqtrade.commands.strategy_utils_commands import start_strategy_update
|
from freqtrade.commands.strategy_utils_commands import start_strategy_update
|
||||||
|
|||||||
@@ -20,25 +20,25 @@ def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[s
|
|||||||
config = setup_utils_configuration(args, method)
|
config = setup_utils_configuration(args, method)
|
||||||
|
|
||||||
no_unlimited_runmodes = {
|
no_unlimited_runmodes = {
|
||||||
RunMode.BACKTEST: 'backtesting',
|
RunMode.BACKTEST: "backtesting",
|
||||||
}
|
}
|
||||||
if method in no_unlimited_runmodes.keys():
|
if method in no_unlimited_runmodes.keys():
|
||||||
from freqtrade.data.btanalysis import get_latest_backtest_filename
|
from freqtrade.data.btanalysis import get_latest_backtest_filename
|
||||||
|
|
||||||
if 'exportfilename' in config:
|
if "exportfilename" in config:
|
||||||
if config['exportfilename'].is_dir():
|
if config["exportfilename"].is_dir():
|
||||||
btfile = Path(get_latest_backtest_filename(config['exportfilename']))
|
btfile = Path(get_latest_backtest_filename(config["exportfilename"]))
|
||||||
signals_file = f"{config['exportfilename']}/{btfile.stem}_signals.pkl"
|
signals_file = f"{config['exportfilename']}/{btfile.stem}_signals.pkl"
|
||||||
else:
|
else:
|
||||||
if config['exportfilename'].exists():
|
if config["exportfilename"].exists():
|
||||||
btfile = Path(config['exportfilename'])
|
btfile = Path(config["exportfilename"])
|
||||||
signals_file = f"{btfile.parent}/{btfile.stem}_signals.pkl"
|
signals_file = f"{btfile.parent}/{btfile.stem}_signals.pkl"
|
||||||
else:
|
else:
|
||||||
raise ConfigurationError(f"{config['exportfilename']} does not exist.")
|
raise ConfigurationError(f"{config['exportfilename']} does not exist.")
|
||||||
else:
|
else:
|
||||||
raise ConfigurationError('exportfilename not in config.')
|
raise ConfigurationError("exportfilename not in config.")
|
||||||
|
|
||||||
if (not Path(signals_file).exists()):
|
if not Path(signals_file).exists():
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"Cannot find latest backtest signals file: {signals_file}."
|
f"Cannot find latest backtest signals file: {signals_file}."
|
||||||
"Run backtesting with `--export signals`."
|
"Run backtesting with `--export signals`."
|
||||||
@@ -58,6 +58,6 @@ def start_analysis_entries_exits(args: Dict[str, Any]) -> None:
|
|||||||
# Initialize configuration
|
# Initialize configuration
|
||||||
config = setup_analyze_configuration(args, RunMode.BACKTEST)
|
config = setup_analyze_configuration(args, RunMode.BACKTEST)
|
||||||
|
|
||||||
logger.info('Starting freqtrade in analysis mode')
|
logger.info("Starting freqtrade in analysis mode")
|
||||||
|
|
||||||
process_entry_exit_reasons(config)
|
process_entry_exit_reasons(config)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
This module contains the argument manager class
|
This module contains the argument manager class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -12,35 +13,72 @@ from freqtrade.constants import DEFAULT_CONFIG
|
|||||||
|
|
||||||
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"]
|
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"]
|
||||||
|
|
||||||
ARGS_STRATEGY = ["strategy", "strategy_path", "recursive_strategy_search", "freqaimodel",
|
ARGS_STRATEGY = [
|
||||||
"freqaimodel_path"]
|
"strategy",
|
||||||
|
"strategy_path",
|
||||||
|
"recursive_strategy_search",
|
||||||
|
"freqaimodel",
|
||||||
|
"freqaimodel_path",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"]
|
ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"]
|
||||||
|
|
||||||
ARGS_WEBSERVER: List[str] = []
|
ARGS_WEBSERVER: List[str] = []
|
||||||
|
|
||||||
ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
|
ARGS_COMMON_OPTIMIZE = [
|
||||||
"max_open_trades", "stake_amount", "fee", "pairs"]
|
"timeframe",
|
||||||
|
"timerange",
|
||||||
|
"dataformat_ohlcv",
|
||||||
|
"max_open_trades",
|
||||||
|
"stake_amount",
|
||||||
|
"fee",
|
||||||
|
"pairs",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + [
|
||||||
"enable_protections", "dry_run_wallet", "timeframe_detail",
|
"position_stacking",
|
||||||
"strategy_list", "export", "exportfilename",
|
"use_max_market_positions",
|
||||||
"backtest_breakdown", "backtest_cache",
|
"enable_protections",
|
||||||
"freqai_backtest_live_models"]
|
"dry_run_wallet",
|
||||||
|
"timeframe_detail",
|
||||||
|
"strategy_list",
|
||||||
|
"export",
|
||||||
|
"exportfilename",
|
||||||
|
"backtest_breakdown",
|
||||||
|
"backtest_cache",
|
||||||
|
"freqai_backtest_live_models",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + [
|
||||||
"position_stacking", "use_max_market_positions",
|
"hyperopt",
|
||||||
"enable_protections", "dry_run_wallet", "timeframe_detail",
|
"hyperopt_path",
|
||||||
"epochs", "spaces", "print_all",
|
"position_stacking",
|
||||||
"print_colorized", "print_json", "hyperopt_jobs",
|
"use_max_market_positions",
|
||||||
"hyperopt_random_state", "hyperopt_min_trades",
|
"enable_protections",
|
||||||
"hyperopt_loss", "disableparamexport",
|
"dry_run_wallet",
|
||||||
"hyperopt_ignore_missing_space", "analyze_per_epoch"]
|
"timeframe_detail",
|
||||||
|
"epochs",
|
||||||
|
"spaces",
|
||||||
|
"print_all",
|
||||||
|
"print_colorized",
|
||||||
|
"print_json",
|
||||||
|
"hyperopt_jobs",
|
||||||
|
"hyperopt_random_state",
|
||||||
|
"hyperopt_min_trades",
|
||||||
|
"hyperopt_loss",
|
||||||
|
"disableparamexport",
|
||||||
|
"hyperopt_ignore_missing_space",
|
||||||
|
"analyze_per_epoch",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
||||||
|
|
||||||
ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized",
|
ARGS_LIST_STRATEGIES = [
|
||||||
"recursive_strategy_search"]
|
"strategy_path",
|
||||||
|
"print_one_column",
|
||||||
|
"print_colorized",
|
||||||
|
"recursive_strategy_search",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column", "print_colorized"]
|
ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column", "print_colorized"]
|
||||||
|
|
||||||
@@ -52,12 +90,27 @@ ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
|||||||
|
|
||||||
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
||||||
|
|
||||||
ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column",
|
ARGS_LIST_PAIRS = [
|
||||||
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all",
|
"exchange",
|
||||||
"trading_mode"]
|
"print_list",
|
||||||
|
"list_pairs_print_json",
|
||||||
|
"print_one_column",
|
||||||
|
"print_csv",
|
||||||
|
"base_currencies",
|
||||||
|
"quote_currencies",
|
||||||
|
"list_pairs_all",
|
||||||
|
"trading_mode",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_TEST_PAIRLIST = ["user_data_dir", "verbosity", "config", "quote_currencies",
|
ARGS_TEST_PAIRLIST = [
|
||||||
"print_one_column", "list_pairs_print_json", "exchange"]
|
"user_data_dir",
|
||||||
|
"verbosity",
|
||||||
|
"config",
|
||||||
|
"quote_currencies",
|
||||||
|
"print_one_column",
|
||||||
|
"list_pairs_print_json",
|
||||||
|
"exchange",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]
|
ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]
|
||||||
|
|
||||||
@@ -70,22 +123,58 @@ ARGS_CONVERT_DATA_TRADES = ["pairs", "format_from_trades", "format_to", "erase",
|
|||||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"]
|
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"]
|
||||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode", "candle_types"]
|
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode", "candle_types"]
|
||||||
|
|
||||||
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades",
|
ARGS_CONVERT_TRADES = [
|
||||||
"trading_mode"]
|
"pairs",
|
||||||
|
"timeframes",
|
||||||
|
"exchange",
|
||||||
|
"dataformat_ohlcv",
|
||||||
|
"dataformat_trades",
|
||||||
|
"trading_mode",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs", "trading_mode", "show_timerange"]
|
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs", "trading_mode", "show_timerange"]
|
||||||
|
|
||||||
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "include_inactive",
|
ARGS_DOWNLOAD_DATA = [
|
||||||
"timerange", "download_trades", "exchange", "timeframes",
|
"pairs",
|
||||||
"erase", "dataformat_ohlcv", "dataformat_trades", "trading_mode",
|
"pairs_file",
|
||||||
"prepend_data"]
|
"days",
|
||||||
|
"new_pairs_days",
|
||||||
|
"include_inactive",
|
||||||
|
"timerange",
|
||||||
|
"download_trades",
|
||||||
|
"exchange",
|
||||||
|
"timeframes",
|
||||||
|
"erase",
|
||||||
|
"dataformat_ohlcv",
|
||||||
|
"dataformat_trades",
|
||||||
|
"trading_mode",
|
||||||
|
"prepend_data",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
|
ARGS_PLOT_DATAFRAME = [
|
||||||
"db_url", "trade_source", "export", "exportfilename",
|
"pairs",
|
||||||
"timerange", "timeframe", "no_trades"]
|
"indicators1",
|
||||||
|
"indicators2",
|
||||||
|
"plot_limit",
|
||||||
|
"db_url",
|
||||||
|
"trade_source",
|
||||||
|
"export",
|
||||||
|
"exportfilename",
|
||||||
|
"timerange",
|
||||||
|
"timeframe",
|
||||||
|
"no_trades",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
|
ARGS_PLOT_PROFIT = [
|
||||||
"trade_source", "timeframe", "plot_auto_open", ]
|
"pairs",
|
||||||
|
"timerange",
|
||||||
|
"export",
|
||||||
|
"exportfilename",
|
||||||
|
"db_url",
|
||||||
|
"trade_source",
|
||||||
|
"timeframe",
|
||||||
|
"plot_auto_open",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_CONVERT_DB = ["db_url", "db_url_from"]
|
ARGS_CONVERT_DB = ["db_url", "db_url_from"]
|
||||||
|
|
||||||
@@ -93,36 +182,76 @@ ARGS_INSTALL_UI = ["erase_ui_only", "ui_version"]
|
|||||||
|
|
||||||
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]
|
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]
|
||||||
|
|
||||||
ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
|
ARGS_HYPEROPT_LIST = [
|
||||||
"hyperopt_list_min_trades", "hyperopt_list_max_trades",
|
"hyperopt_list_best",
|
||||||
"hyperopt_list_min_avg_time", "hyperopt_list_max_avg_time",
|
"hyperopt_list_profitable",
|
||||||
"hyperopt_list_min_avg_profit", "hyperopt_list_max_avg_profit",
|
"hyperopt_list_min_trades",
|
||||||
"hyperopt_list_min_total_profit", "hyperopt_list_max_total_profit",
|
"hyperopt_list_max_trades",
|
||||||
"hyperopt_list_min_objective", "hyperopt_list_max_objective",
|
"hyperopt_list_min_avg_time",
|
||||||
"print_colorized", "print_json", "hyperopt_list_no_details",
|
"hyperopt_list_max_avg_time",
|
||||||
"hyperoptexportfilename", "export_csv"]
|
"hyperopt_list_min_avg_profit",
|
||||||
|
"hyperopt_list_max_avg_profit",
|
||||||
|
"hyperopt_list_min_total_profit",
|
||||||
|
"hyperopt_list_max_total_profit",
|
||||||
|
"hyperopt_list_min_objective",
|
||||||
|
"hyperopt_list_max_objective",
|
||||||
|
"print_colorized",
|
||||||
|
"print_json",
|
||||||
|
"hyperopt_list_no_details",
|
||||||
|
"hyperoptexportfilename",
|
||||||
|
"export_csv",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
|
ARGS_HYPEROPT_SHOW = [
|
||||||
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
|
"hyperopt_list_best",
|
||||||
"disableparamexport", "backtest_breakdown"]
|
"hyperopt_list_profitable",
|
||||||
|
"hyperopt_show_index",
|
||||||
|
"print_json",
|
||||||
|
"hyperoptexportfilename",
|
||||||
|
"hyperopt_show_no_header",
|
||||||
|
"disableparamexport",
|
||||||
|
"backtest_breakdown",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list",
|
ARGS_ANALYZE_ENTRIES_EXITS = [
|
||||||
"exit_reason_list", "indicator_list", "timerange",
|
"exportfilename",
|
||||||
"analysis_rejected", "analysis_to_csv", "analysis_csv_path"]
|
"analysis_groups",
|
||||||
|
"enter_reason_list",
|
||||||
|
"exit_reason_list",
|
||||||
|
"indicator_list",
|
||||||
|
"timerange",
|
||||||
|
"analysis_rejected",
|
||||||
|
"analysis_to_csv",
|
||||||
|
"analysis_csv_path",
|
||||||
|
]
|
||||||
|
|
||||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
NO_CONF_REQURIED = [
|
||||||
"list-markets", "list-pairs", "list-strategies", "list-freqaimodels",
|
"convert-data",
|
||||||
"list-data", "hyperopt-list", "hyperopt-show", "backtest-filter",
|
"convert-trade-data",
|
||||||
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv",
|
"download-data",
|
||||||
"strategy-updater"]
|
"list-timeframes",
|
||||||
|
"list-markets",
|
||||||
|
"list-pairs",
|
||||||
|
"list-strategies",
|
||||||
|
"list-freqaimodels",
|
||||||
|
"list-data",
|
||||||
|
"hyperopt-list",
|
||||||
|
"hyperopt-show",
|
||||||
|
"backtest-filter",
|
||||||
|
"plot-dataframe",
|
||||||
|
"plot-profit",
|
||||||
|
"show-trades",
|
||||||
|
"trades-to-ohlcv",
|
||||||
|
"strategy-updater",
|
||||||
|
]
|
||||||
|
|
||||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||||
|
|
||||||
ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_search"]
|
ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_search"]
|
||||||
|
|
||||||
ARGS_LOOKAHEAD_ANALYSIS = [
|
ARGS_LOOKAHEAD_ANALYSIS = [
|
||||||
a for a in ARGS_BACKTEST if a not in ("position_stacking", "use_max_market_positions", 'cache')
|
a for a in ARGS_BACKTEST if a not in ("position_stacking", "use_max_market_positions", "cache")
|
||||||
] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"]
|
] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"]
|
||||||
|
|
||||||
ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"]
|
ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"]
|
||||||
|
|
||||||
@@ -156,14 +285,14 @@ class Arguments:
|
|||||||
# Workaround issue in argparse with action='append' and default value
|
# Workaround issue in argparse with action='append' and default value
|
||||||
# (see https://bugs.python.org/issue16399)
|
# (see https://bugs.python.org/issue16399)
|
||||||
# Allow no-config for certain commands (like downloading / plotting)
|
# Allow no-config for certain commands (like downloading / plotting)
|
||||||
if ('config' in parsed_arg and parsed_arg.config is None):
|
if "config" in parsed_arg and parsed_arg.config is None:
|
||||||
conf_required = ('command' in parsed_arg and parsed_arg.command in NO_CONF_REQURIED)
|
conf_required = "command" in parsed_arg and parsed_arg.command in NO_CONF_REQURIED
|
||||||
|
|
||||||
if 'user_data_dir' in parsed_arg and parsed_arg.user_data_dir is not None:
|
if "user_data_dir" in parsed_arg and parsed_arg.user_data_dir is not None:
|
||||||
user_dir = parsed_arg.user_data_dir
|
user_dir = parsed_arg.user_data_dir
|
||||||
else:
|
else:
|
||||||
# Default case
|
# Default case
|
||||||
user_dir = 'user_data'
|
user_dir = "user_data"
|
||||||
# Try loading from "user_data/config.json"
|
# Try loading from "user_data/config.json"
|
||||||
cfgfile = Path(user_dir) / DEFAULT_CONFIG
|
cfgfile = Path(user_dir) / DEFAULT_CONFIG
|
||||||
if cfgfile.is_file():
|
if cfgfile.is_file():
|
||||||
@@ -177,7 +306,6 @@ class Arguments:
|
|||||||
return parsed_arg
|
return parsed_arg
|
||||||
|
|
||||||
def _build_args(self, optionlist, parser):
|
def _build_args(self, optionlist, parser):
|
||||||
|
|
||||||
for val in optionlist:
|
for val in optionlist:
|
||||||
opt = AVAILABLE_CLI_OPTIONS[val]
|
opt = AVAILABLE_CLI_OPTIONS[val]
|
||||||
parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
|
parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
|
||||||
@@ -197,41 +325,62 @@ class Arguments:
|
|||||||
self._build_args(optionlist=ARGS_STRATEGY, parser=strategy_group)
|
self._build_args(optionlist=ARGS_STRATEGY, parser=strategy_group)
|
||||||
|
|
||||||
# Build main command
|
# Build main command
|
||||||
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
self.parser = argparse.ArgumentParser(
|
||||||
self._build_args(optionlist=['version'], parser=self.parser)
|
prog="freqtrade", description="Free, open source crypto trading bot"
|
||||||
|
)
|
||||||
|
self._build_args(optionlist=["version"], parser=self.parser)
|
||||||
|
|
||||||
from freqtrade.commands import (start_analysis_entries_exits, start_backtesting,
|
from freqtrade.commands import (
|
||||||
start_backtesting_show, start_convert_data,
|
start_analysis_entries_exits,
|
||||||
start_convert_db, start_convert_trades,
|
start_backtesting,
|
||||||
start_create_userdir, start_download_data, start_edge,
|
start_backtesting_show,
|
||||||
start_hyperopt, start_hyperopt_list, start_hyperopt_show,
|
start_convert_data,
|
||||||
start_install_ui, start_list_data, start_list_exchanges,
|
start_convert_db,
|
||||||
start_list_freqAI_models, start_list_markets,
|
start_convert_trades,
|
||||||
start_list_strategies, start_list_timeframes,
|
start_create_userdir,
|
||||||
start_lookahead_analysis, start_new_config,
|
start_download_data,
|
||||||
start_new_strategy, start_plot_dataframe, start_plot_profit,
|
start_edge,
|
||||||
start_recursive_analysis, start_show_config,
|
start_hyperopt,
|
||||||
start_show_trades, start_strategy_update,
|
start_hyperopt_list,
|
||||||
start_test_pairlist, start_trading, start_webserver)
|
start_hyperopt_show,
|
||||||
|
start_install_ui,
|
||||||
|
start_list_data,
|
||||||
|
start_list_exchanges,
|
||||||
|
start_list_freqAI_models,
|
||||||
|
start_list_markets,
|
||||||
|
start_list_strategies,
|
||||||
|
start_list_timeframes,
|
||||||
|
start_lookahead_analysis,
|
||||||
|
start_new_config,
|
||||||
|
start_new_strategy,
|
||||||
|
start_plot_dataframe,
|
||||||
|
start_plot_profit,
|
||||||
|
start_recursive_analysis,
|
||||||
|
start_show_config,
|
||||||
|
start_show_trades,
|
||||||
|
start_strategy_update,
|
||||||
|
start_test_pairlist,
|
||||||
|
start_trading,
|
||||||
|
start_webserver,
|
||||||
|
)
|
||||||
|
|
||||||
subparsers = self.parser.add_subparsers(dest='command',
|
subparsers = self.parser.add_subparsers(
|
||||||
# Use custom message when no subhandler is added
|
dest="command",
|
||||||
# shown from `main.py`
|
# Use custom message when no subhandler is added
|
||||||
# required=True
|
# shown from `main.py`
|
||||||
)
|
# required=True
|
||||||
|
)
|
||||||
|
|
||||||
# Add trade subcommand
|
# Add trade subcommand
|
||||||
trade_cmd = subparsers.add_parser(
|
trade_cmd = subparsers.add_parser(
|
||||||
'trade',
|
"trade", help="Trade module.", parents=[_common_parser, _strategy_parser]
|
||||||
help='Trade module.',
|
|
||||||
parents=[_common_parser, _strategy_parser]
|
|
||||||
)
|
)
|
||||||
trade_cmd.set_defaults(func=start_trading)
|
trade_cmd.set_defaults(func=start_trading)
|
||||||
self._build_args(optionlist=ARGS_TRADE, parser=trade_cmd)
|
self._build_args(optionlist=ARGS_TRADE, parser=trade_cmd)
|
||||||
|
|
||||||
# add create-userdir subcommand
|
# add create-userdir subcommand
|
||||||
create_userdir_cmd = subparsers.add_parser(
|
create_userdir_cmd = subparsers.add_parser(
|
||||||
'create-userdir',
|
"create-userdir",
|
||||||
help="Create user-data directory.",
|
help="Create user-data directory.",
|
||||||
)
|
)
|
||||||
create_userdir_cmd.set_defaults(func=start_create_userdir)
|
create_userdir_cmd.set_defaults(func=start_create_userdir)
|
||||||
@@ -239,7 +388,7 @@ class Arguments:
|
|||||||
|
|
||||||
# add new-config subcommand
|
# add new-config subcommand
|
||||||
build_config_cmd = subparsers.add_parser(
|
build_config_cmd = subparsers.add_parser(
|
||||||
'new-config',
|
"new-config",
|
||||||
help="Create new config",
|
help="Create new config",
|
||||||
)
|
)
|
||||||
build_config_cmd.set_defaults(func=start_new_config)
|
build_config_cmd.set_defaults(func=start_new_config)
|
||||||
@@ -247,7 +396,7 @@ class Arguments:
|
|||||||
|
|
||||||
# add show-config subcommand
|
# add show-config subcommand
|
||||||
show_config_cmd = subparsers.add_parser(
|
show_config_cmd = subparsers.add_parser(
|
||||||
'show-config',
|
"show-config",
|
||||||
help="Show resolved config",
|
help="Show resolved config",
|
||||||
)
|
)
|
||||||
show_config_cmd.set_defaults(func=start_show_config)
|
show_config_cmd.set_defaults(func=start_show_config)
|
||||||
@@ -255,7 +404,7 @@ class Arguments:
|
|||||||
|
|
||||||
# add new-strategy subcommand
|
# add new-strategy subcommand
|
||||||
build_strategy_cmd = subparsers.add_parser(
|
build_strategy_cmd = subparsers.add_parser(
|
||||||
'new-strategy',
|
"new-strategy",
|
||||||
help="Create new strategy",
|
help="Create new strategy",
|
||||||
)
|
)
|
||||||
build_strategy_cmd.set_defaults(func=start_new_strategy)
|
build_strategy_cmd.set_defaults(func=start_new_strategy)
|
||||||
@@ -263,8 +412,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add download-data subcommand
|
# Add download-data subcommand
|
||||||
download_data_cmd = subparsers.add_parser(
|
download_data_cmd = subparsers.add_parser(
|
||||||
'download-data',
|
"download-data",
|
||||||
help='Download backtesting data.',
|
help="Download backtesting data.",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
download_data_cmd.set_defaults(func=start_download_data)
|
download_data_cmd.set_defaults(func=start_download_data)
|
||||||
@@ -272,8 +421,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add convert-data subcommand
|
# Add convert-data subcommand
|
||||||
convert_data_cmd = subparsers.add_parser(
|
convert_data_cmd = subparsers.add_parser(
|
||||||
'convert-data',
|
"convert-data",
|
||||||
help='Convert candle (OHLCV) data from one format to another.',
|
help="Convert candle (OHLCV) data from one format to another.",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
convert_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=True))
|
convert_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=True))
|
||||||
@@ -281,8 +430,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add convert-trade-data subcommand
|
# Add convert-trade-data subcommand
|
||||||
convert_trade_data_cmd = subparsers.add_parser(
|
convert_trade_data_cmd = subparsers.add_parser(
|
||||||
'convert-trade-data',
|
"convert-trade-data",
|
||||||
help='Convert trade data from one format to another.',
|
help="Convert trade data from one format to another.",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
|
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
|
||||||
@@ -290,8 +439,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add trades-to-ohlcv subcommand
|
# Add trades-to-ohlcv subcommand
|
||||||
convert_trade_data_cmd = subparsers.add_parser(
|
convert_trade_data_cmd = subparsers.add_parser(
|
||||||
'trades-to-ohlcv',
|
"trades-to-ohlcv",
|
||||||
help='Convert trade data to OHLCV data.',
|
help="Convert trade data to OHLCV data.",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
convert_trade_data_cmd.set_defaults(func=start_convert_trades)
|
convert_trade_data_cmd.set_defaults(func=start_convert_trades)
|
||||||
@@ -299,8 +448,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add list-data subcommand
|
# Add list-data subcommand
|
||||||
list_data_cmd = subparsers.add_parser(
|
list_data_cmd = subparsers.add_parser(
|
||||||
'list-data',
|
"list-data",
|
||||||
help='List downloaded data.',
|
help="List downloaded data.",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
list_data_cmd.set_defaults(func=start_list_data)
|
list_data_cmd.set_defaults(func=start_list_data)
|
||||||
@@ -308,17 +457,15 @@ class Arguments:
|
|||||||
|
|
||||||
# Add backtesting subcommand
|
# Add backtesting subcommand
|
||||||
backtesting_cmd = subparsers.add_parser(
|
backtesting_cmd = subparsers.add_parser(
|
||||||
'backtesting',
|
"backtesting", help="Backtesting module.", parents=[_common_parser, _strategy_parser]
|
||||||
help='Backtesting module.',
|
|
||||||
parents=[_common_parser, _strategy_parser]
|
|
||||||
)
|
)
|
||||||
backtesting_cmd.set_defaults(func=start_backtesting)
|
backtesting_cmd.set_defaults(func=start_backtesting)
|
||||||
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
|
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
|
||||||
|
|
||||||
# Add backtesting-show subcommand
|
# Add backtesting-show subcommand
|
||||||
backtesting_show_cmd = subparsers.add_parser(
|
backtesting_show_cmd = subparsers.add_parser(
|
||||||
'backtesting-show',
|
"backtesting-show",
|
||||||
help='Show past Backtest results',
|
help="Show past Backtest results",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
backtesting_show_cmd.set_defaults(func=start_backtesting_show)
|
backtesting_show_cmd.set_defaults(func=start_backtesting_show)
|
||||||
@@ -326,26 +473,22 @@ class Arguments:
|
|||||||
|
|
||||||
# Add backtesting analysis subcommand
|
# Add backtesting analysis subcommand
|
||||||
analysis_cmd = subparsers.add_parser(
|
analysis_cmd = subparsers.add_parser(
|
||||||
'backtesting-analysis',
|
"backtesting-analysis", help="Backtest Analysis module.", parents=[_common_parser]
|
||||||
help='Backtest Analysis module.',
|
|
||||||
parents=[_common_parser]
|
|
||||||
)
|
)
|
||||||
analysis_cmd.set_defaults(func=start_analysis_entries_exits)
|
analysis_cmd.set_defaults(func=start_analysis_entries_exits)
|
||||||
self._build_args(optionlist=ARGS_ANALYZE_ENTRIES_EXITS, parser=analysis_cmd)
|
self._build_args(optionlist=ARGS_ANALYZE_ENTRIES_EXITS, parser=analysis_cmd)
|
||||||
|
|
||||||
# Add edge subcommand
|
# Add edge subcommand
|
||||||
edge_cmd = subparsers.add_parser(
|
edge_cmd = subparsers.add_parser(
|
||||||
'edge',
|
"edge", help="Edge module.", parents=[_common_parser, _strategy_parser]
|
||||||
help='Edge module.',
|
|
||||||
parents=[_common_parser, _strategy_parser]
|
|
||||||
)
|
)
|
||||||
edge_cmd.set_defaults(func=start_edge)
|
edge_cmd.set_defaults(func=start_edge)
|
||||||
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
|
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
|
||||||
|
|
||||||
# Add hyperopt subcommand
|
# Add hyperopt subcommand
|
||||||
hyperopt_cmd = subparsers.add_parser(
|
hyperopt_cmd = subparsers.add_parser(
|
||||||
'hyperopt',
|
"hyperopt",
|
||||||
help='Hyperopt module.',
|
help="Hyperopt module.",
|
||||||
parents=[_common_parser, _strategy_parser],
|
parents=[_common_parser, _strategy_parser],
|
||||||
)
|
)
|
||||||
hyperopt_cmd.set_defaults(func=start_hyperopt)
|
hyperopt_cmd.set_defaults(func=start_hyperopt)
|
||||||
@@ -353,8 +496,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add hyperopt-list subcommand
|
# Add hyperopt-list subcommand
|
||||||
hyperopt_list_cmd = subparsers.add_parser(
|
hyperopt_list_cmd = subparsers.add_parser(
|
||||||
'hyperopt-list',
|
"hyperopt-list",
|
||||||
help='List Hyperopt results',
|
help="List Hyperopt results",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
hyperopt_list_cmd.set_defaults(func=start_hyperopt_list)
|
hyperopt_list_cmd.set_defaults(func=start_hyperopt_list)
|
||||||
@@ -362,8 +505,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add hyperopt-show subcommand
|
# Add hyperopt-show subcommand
|
||||||
hyperopt_show_cmd = subparsers.add_parser(
|
hyperopt_show_cmd = subparsers.add_parser(
|
||||||
'hyperopt-show',
|
"hyperopt-show",
|
||||||
help='Show details of Hyperopt results',
|
help="Show details of Hyperopt results",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
hyperopt_show_cmd.set_defaults(func=start_hyperopt_show)
|
hyperopt_show_cmd.set_defaults(func=start_hyperopt_show)
|
||||||
@@ -371,8 +514,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add list-exchanges subcommand
|
# Add list-exchanges subcommand
|
||||||
list_exchanges_cmd = subparsers.add_parser(
|
list_exchanges_cmd = subparsers.add_parser(
|
||||||
'list-exchanges',
|
"list-exchanges",
|
||||||
help='Print available exchanges.',
|
help="Print available exchanges.",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
||||||
@@ -380,8 +523,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add list-markets subcommand
|
# Add list-markets subcommand
|
||||||
list_markets_cmd = subparsers.add_parser(
|
list_markets_cmd = subparsers.add_parser(
|
||||||
'list-markets',
|
"list-markets",
|
||||||
help='Print markets on exchange.',
|
help="Print markets on exchange.",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False))
|
list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False))
|
||||||
@@ -389,8 +532,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add list-pairs subcommand
|
# Add list-pairs subcommand
|
||||||
list_pairs_cmd = subparsers.add_parser(
|
list_pairs_cmd = subparsers.add_parser(
|
||||||
'list-pairs',
|
"list-pairs",
|
||||||
help='Print pairs on exchange.',
|
help="Print pairs on exchange.",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True))
|
list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True))
|
||||||
@@ -398,8 +541,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add list-strategies subcommand
|
# Add list-strategies subcommand
|
||||||
list_strategies_cmd = subparsers.add_parser(
|
list_strategies_cmd = subparsers.add_parser(
|
||||||
'list-strategies',
|
"list-strategies",
|
||||||
help='Print available strategies.',
|
help="Print available strategies.",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
list_strategies_cmd.set_defaults(func=start_list_strategies)
|
list_strategies_cmd.set_defaults(func=start_list_strategies)
|
||||||
@@ -407,8 +550,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add list-freqAI Models subcommand
|
# Add list-freqAI Models subcommand
|
||||||
list_freqaimodels_cmd = subparsers.add_parser(
|
list_freqaimodels_cmd = subparsers.add_parser(
|
||||||
'list-freqaimodels',
|
"list-freqaimodels",
|
||||||
help='Print available freqAI models.',
|
help="Print available freqAI models.",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
list_freqaimodels_cmd.set_defaults(func=start_list_freqAI_models)
|
list_freqaimodels_cmd.set_defaults(func=start_list_freqAI_models)
|
||||||
@@ -416,8 +559,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add list-timeframes subcommand
|
# Add list-timeframes subcommand
|
||||||
list_timeframes_cmd = subparsers.add_parser(
|
list_timeframes_cmd = subparsers.add_parser(
|
||||||
'list-timeframes',
|
"list-timeframes",
|
||||||
help='Print available timeframes for the exchange.',
|
help="Print available timeframes for the exchange.",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
list_timeframes_cmd.set_defaults(func=start_list_timeframes)
|
list_timeframes_cmd.set_defaults(func=start_list_timeframes)
|
||||||
@@ -425,8 +568,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add show-trades subcommand
|
# Add show-trades subcommand
|
||||||
show_trades = subparsers.add_parser(
|
show_trades = subparsers.add_parser(
|
||||||
'show-trades',
|
"show-trades",
|
||||||
help='Show trades.',
|
help="Show trades.",
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
show_trades.set_defaults(func=start_show_trades)
|
show_trades.set_defaults(func=start_show_trades)
|
||||||
@@ -434,8 +577,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Add test-pairlist subcommand
|
# Add test-pairlist subcommand
|
||||||
test_pairlist_cmd = subparsers.add_parser(
|
test_pairlist_cmd = subparsers.add_parser(
|
||||||
'test-pairlist',
|
"test-pairlist",
|
||||||
help='Test your pairlist configuration.',
|
help="Test your pairlist configuration.",
|
||||||
)
|
)
|
||||||
test_pairlist_cmd.set_defaults(func=start_test_pairlist)
|
test_pairlist_cmd.set_defaults(func=start_test_pairlist)
|
||||||
self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd)
|
self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd)
|
||||||
@@ -450,16 +593,16 @@ class Arguments:
|
|||||||
|
|
||||||
# Add install-ui subcommand
|
# Add install-ui subcommand
|
||||||
install_ui_cmd = subparsers.add_parser(
|
install_ui_cmd = subparsers.add_parser(
|
||||||
'install-ui',
|
"install-ui",
|
||||||
help='Install FreqUI',
|
help="Install FreqUI",
|
||||||
)
|
)
|
||||||
install_ui_cmd.set_defaults(func=start_install_ui)
|
install_ui_cmd.set_defaults(func=start_install_ui)
|
||||||
self._build_args(optionlist=ARGS_INSTALL_UI, parser=install_ui_cmd)
|
self._build_args(optionlist=ARGS_INSTALL_UI, parser=install_ui_cmd)
|
||||||
|
|
||||||
# Add Plotting subcommand
|
# Add Plotting subcommand
|
||||||
plot_dataframe_cmd = subparsers.add_parser(
|
plot_dataframe_cmd = subparsers.add_parser(
|
||||||
'plot-dataframe',
|
"plot-dataframe",
|
||||||
help='Plot candles with indicators.',
|
help="Plot candles with indicators.",
|
||||||
parents=[_common_parser, _strategy_parser],
|
parents=[_common_parser, _strategy_parser],
|
||||||
)
|
)
|
||||||
plot_dataframe_cmd.set_defaults(func=start_plot_dataframe)
|
plot_dataframe_cmd.set_defaults(func=start_plot_dataframe)
|
||||||
@@ -467,8 +610,8 @@ class Arguments:
|
|||||||
|
|
||||||
# Plot profit
|
# Plot profit
|
||||||
plot_profit_cmd = subparsers.add_parser(
|
plot_profit_cmd = subparsers.add_parser(
|
||||||
'plot-profit',
|
"plot-profit",
|
||||||
help='Generate plot showing profits.',
|
help="Generate plot showing profits.",
|
||||||
parents=[_common_parser, _strategy_parser],
|
parents=[_common_parser, _strategy_parser],
|
||||||
)
|
)
|
||||||
plot_profit_cmd.set_defaults(func=start_plot_profit)
|
plot_profit_cmd.set_defaults(func=start_plot_profit)
|
||||||
@@ -476,40 +619,36 @@ class Arguments:
|
|||||||
|
|
||||||
# Add webserver subcommand
|
# Add webserver subcommand
|
||||||
webserver_cmd = subparsers.add_parser(
|
webserver_cmd = subparsers.add_parser(
|
||||||
'webserver',
|
"webserver", help="Webserver module.", parents=[_common_parser]
|
||||||
help='Webserver module.',
|
|
||||||
parents=[_common_parser]
|
|
||||||
)
|
)
|
||||||
webserver_cmd.set_defaults(func=start_webserver)
|
webserver_cmd.set_defaults(func=start_webserver)
|
||||||
self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd)
|
self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd)
|
||||||
|
|
||||||
# Add strategy_updater subcommand
|
# Add strategy_updater subcommand
|
||||||
strategy_updater_cmd = subparsers.add_parser(
|
strategy_updater_cmd = subparsers.add_parser(
|
||||||
'strategy-updater',
|
"strategy-updater",
|
||||||
help='updates outdated strategy files to the current version',
|
help="updates outdated strategy files to the current version",
|
||||||
parents=[_common_parser]
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
strategy_updater_cmd.set_defaults(func=start_strategy_update)
|
strategy_updater_cmd.set_defaults(func=start_strategy_update)
|
||||||
self._build_args(optionlist=ARGS_STRATEGY_UPDATER, parser=strategy_updater_cmd)
|
self._build_args(optionlist=ARGS_STRATEGY_UPDATER, parser=strategy_updater_cmd)
|
||||||
|
|
||||||
# Add lookahead_analysis subcommand
|
# Add lookahead_analysis subcommand
|
||||||
lookahead_analayis_cmd = subparsers.add_parser(
|
lookahead_analayis_cmd = subparsers.add_parser(
|
||||||
'lookahead-analysis',
|
"lookahead-analysis",
|
||||||
help="Check for potential look ahead bias.",
|
help="Check for potential look ahead bias.",
|
||||||
parents=[_common_parser, _strategy_parser]
|
parents=[_common_parser, _strategy_parser],
|
||||||
)
|
)
|
||||||
lookahead_analayis_cmd.set_defaults(func=start_lookahead_analysis)
|
lookahead_analayis_cmd.set_defaults(func=start_lookahead_analysis)
|
||||||
|
|
||||||
self._build_args(optionlist=ARGS_LOOKAHEAD_ANALYSIS,
|
self._build_args(optionlist=ARGS_LOOKAHEAD_ANALYSIS, parser=lookahead_analayis_cmd)
|
||||||
parser=lookahead_analayis_cmd)
|
|
||||||
|
|
||||||
# Add recursive_analysis subcommand
|
# Add recursive_analysis subcommand
|
||||||
recursive_analayis_cmd = subparsers.add_parser(
|
recursive_analayis_cmd = subparsers.add_parser(
|
||||||
'recursive-analysis',
|
"recursive-analysis",
|
||||||
help="Check for potential recursive formula issue.",
|
help="Check for potential recursive formula issue.",
|
||||||
parents=[_common_parser, _strategy_parser]
|
parents=[_common_parser, _strategy_parser],
|
||||||
)
|
)
|
||||||
recursive_analayis_cmd.set_defaults(func=start_recursive_analysis)
|
recursive_analayis_cmd.set_defaults(func=start_recursive_analysis)
|
||||||
|
|
||||||
self._build_args(optionlist=ARGS_RECURSIVE_ANALYSIS,
|
self._build_args(optionlist=ARGS_RECURSIVE_ANALYSIS, parser=recursive_analayis_cmd)
|
||||||
parser=recursive_analayis_cmd)
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def ask_user_overwrite(config_path: Path) -> bool:
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
answers = prompt(questions)
|
answers = prompt(questions)
|
||||||
return answers['overwrite']
|
return answers["overwrite"]
|
||||||
|
|
||||||
|
|
||||||
def ask_user_config() -> Dict[str, Any]:
|
def ask_user_config() -> Dict[str, Any]:
|
||||||
@@ -65,7 +65,7 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"name": "stake_currency",
|
"name": "stake_currency",
|
||||||
"message": "Please insert your stake currency:",
|
"message": "Please insert your stake currency:",
|
||||||
"default": 'USDT',
|
"default": "USDT",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -73,36 +73,35 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
|
"message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
|
||||||
"default": "unlimited",
|
"default": "unlimited",
|
||||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
|
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
|
||||||
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
|
"filter": lambda val: (
|
||||||
if val == UNLIMITED_STAKE_AMOUNT
|
'"' + UNLIMITED_STAKE_AMOUNT + '"' if val == UNLIMITED_STAKE_AMOUNT else val
|
||||||
else val
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"name": "max_open_trades",
|
"name": "max_open_trades",
|
||||||
"message": "Please insert max_open_trades (Integer or -1 for unlimited open trades):",
|
"message": "Please insert max_open_trades (Integer or -1 for unlimited open trades):",
|
||||||
"default": "3",
|
"default": "3",
|
||||||
"validate": lambda val: validate_is_int(val)
|
"validate": lambda val: validate_is_int(val),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"name": "timeframe_in_config",
|
"name": "timeframe_in_config",
|
||||||
"message": "Time",
|
"message": "Time",
|
||||||
"choices": ["Have the strategy define timeframe.", "Override in configuration."]
|
"choices": ["Have the strategy define timeframe.", "Override in configuration."],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"name": "timeframe",
|
"name": "timeframe",
|
||||||
"message": "Please insert your desired timeframe (e.g. 5m):",
|
"message": "Please insert your desired timeframe (e.g. 5m):",
|
||||||
"default": "5m",
|
"default": "5m",
|
||||||
"when": lambda x: x["timeframe_in_config"] == 'Override in configuration.'
|
"when": lambda x: x["timeframe_in_config"] == "Override in configuration.",
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"name": "fiat_display_currency",
|
"name": "fiat_display_currency",
|
||||||
"message": "Please insert your display Currency (for reporting):",
|
"message": "Please insert your display Currency (for reporting):",
|
||||||
"default": 'USD',
|
"default": "USD",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "select",
|
"type": "select",
|
||||||
@@ -125,33 +124,33 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"name": "trading_mode",
|
"name": "trading_mode",
|
||||||
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
|
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
|
||||||
"default": False,
|
"default": False,
|
||||||
"filter": lambda val: 'futures' if val else 'spot',
|
"filter": lambda val: "futures" if val else "spot",
|
||||||
"when": lambda x: x["exchange_name"] in ['binance', 'gate', 'okx'],
|
"when": lambda x: x["exchange_name"] in ["binance", "gate", "okx"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "autocomplete",
|
"type": "autocomplete",
|
||||||
"name": "exchange_name",
|
"name": "exchange_name",
|
||||||
"message": "Type your exchange name (Must be supported by ccxt)",
|
"message": "Type your exchange name (Must be supported by ccxt)",
|
||||||
"choices": available_exchanges(),
|
"choices": available_exchanges(),
|
||||||
"when": lambda x: x["exchange_name"] == 'other'
|
"when": lambda x: x["exchange_name"] == "other",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"name": "exchange_key",
|
"name": "exchange_key",
|
||||||
"message": "Insert Exchange Key",
|
"message": "Insert Exchange Key",
|
||||||
"when": lambda x: not x['dry_run']
|
"when": lambda x: not x["dry_run"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"name": "exchange_secret",
|
"name": "exchange_secret",
|
||||||
"message": "Insert Exchange Secret",
|
"message": "Insert Exchange Secret",
|
||||||
"when": lambda x: not x['dry_run']
|
"when": lambda x: not x["dry_run"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"name": "exchange_key_password",
|
"name": "exchange_key_password",
|
||||||
"message": "Insert Exchange API Key password",
|
"message": "Insert Exchange API Key password",
|
||||||
"when": lambda x: not x['dry_run'] and x['exchange_name'] in ('kucoin', 'okx')
|
"when": lambda x: not x["dry_run"] and x["exchange_name"] in ("kucoin", "okx"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "confirm",
|
"type": "confirm",
|
||||||
@@ -163,13 +162,13 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"type": "password",
|
"type": "password",
|
||||||
"name": "telegram_token",
|
"name": "telegram_token",
|
||||||
"message": "Insert Telegram token",
|
"message": "Insert Telegram token",
|
||||||
"when": lambda x: x['telegram']
|
"when": lambda x: x["telegram"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"name": "telegram_chat_id",
|
"name": "telegram_chat_id",
|
||||||
"message": "Insert Telegram chat id",
|
"message": "Insert Telegram chat id",
|
||||||
"when": lambda x: x['telegram']
|
"when": lambda x: x["telegram"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "confirm",
|
"type": "confirm",
|
||||||
@@ -180,23 +179,25 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"name": "api_server_listen_addr",
|
"name": "api_server_listen_addr",
|
||||||
"message": ("Insert Api server Listen Address (0.0.0.0 for docker, "
|
"message": (
|
||||||
"otherwise best left untouched)"),
|
"Insert Api server Listen Address (0.0.0.0 for docker, "
|
||||||
|
"otherwise best left untouched)"
|
||||||
|
),
|
||||||
"default": "127.0.0.1" if not running_in_docker() else "0.0.0.0",
|
"default": "127.0.0.1" if not running_in_docker() else "0.0.0.0",
|
||||||
"when": lambda x: x['api_server']
|
"when": lambda x: x["api_server"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"name": "api_server_username",
|
"name": "api_server_username",
|
||||||
"message": "Insert api-server username",
|
"message": "Insert api-server username",
|
||||||
"default": "freqtrader",
|
"default": "freqtrader",
|
||||||
"when": lambda x: x['api_server']
|
"when": lambda x: x["api_server"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"name": "api_server_password",
|
"name": "api_server_password",
|
||||||
"message": "Insert api-server password",
|
"message": "Insert api-server password",
|
||||||
"when": lambda x: x['api_server']
|
"when": lambda x: x["api_server"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
answers = prompt(questions)
|
answers = prompt(questions)
|
||||||
@@ -205,15 +206,11 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
# Interrupted questionary sessions return an empty dict.
|
# Interrupted questionary sessions return an empty dict.
|
||||||
raise OperationalException("User interrupted interactive questions.")
|
raise OperationalException("User interrupted interactive questions.")
|
||||||
# Ensure default is set for non-futures exchanges
|
# Ensure default is set for non-futures exchanges
|
||||||
answers['trading_mode'] = answers.get('trading_mode', "spot")
|
answers["trading_mode"] = answers.get("trading_mode", "spot")
|
||||||
answers['margin_mode'] = (
|
answers["margin_mode"] = "isolated" if answers.get("trading_mode") == "futures" else ""
|
||||||
'isolated'
|
|
||||||
if answers.get('trading_mode') == 'futures'
|
|
||||||
else ''
|
|
||||||
)
|
|
||||||
# Force JWT token to be a random string
|
# Force JWT token to be a random string
|
||||||
answers['api_server_jwt_key'] = secrets.token_hex()
|
answers["api_server_jwt_key"] = secrets.token_hex()
|
||||||
answers['api_server_ws_token'] = secrets.token_urlsafe(25)
|
answers["api_server_ws_token"] = secrets.token_urlsafe(25)
|
||||||
|
|
||||||
return answers
|
return answers
|
||||||
|
|
||||||
@@ -225,26 +222,26 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
|
|||||||
:param selections: Dict containing selections taken by the user.
|
:param selections: Dict containing selections taken by the user.
|
||||||
"""
|
"""
|
||||||
from jinja2.exceptions import TemplateNotFound
|
from jinja2.exceptions import TemplateNotFound
|
||||||
|
|
||||||
try:
|
try:
|
||||||
exchange_template = MAP_EXCHANGE_CHILDCLASS.get(
|
exchange_template = MAP_EXCHANGE_CHILDCLASS.get(
|
||||||
selections['exchange_name'], selections['exchange_name'])
|
selections["exchange_name"], selections["exchange_name"]
|
||||||
|
)
|
||||||
|
|
||||||
selections['exchange'] = render_template(
|
selections["exchange"] = render_template(
|
||||||
templatefile=f"subtemplates/exchange_{exchange_template}.j2",
|
templatefile=f"subtemplates/exchange_{exchange_template}.j2", arguments=selections
|
||||||
arguments=selections
|
|
||||||
)
|
)
|
||||||
except TemplateNotFound:
|
except TemplateNotFound:
|
||||||
selections['exchange'] = render_template(
|
selections["exchange"] = render_template(
|
||||||
templatefile="subtemplates/exchange_generic.j2",
|
templatefile="subtemplates/exchange_generic.j2", arguments=selections
|
||||||
arguments=selections
|
|
||||||
)
|
)
|
||||||
|
|
||||||
config_text = render_template(templatefile='base_config.json.j2',
|
config_text = render_template(templatefile="base_config.json.j2", arguments=selections)
|
||||||
arguments=selections)
|
|
||||||
|
|
||||||
logger.info(f"Writing config to `{config_path}`.")
|
logger.info(f"Writing config to `{config_path}`.")
|
||||||
logger.info(
|
logger.info(
|
||||||
"Please make sure to check the configuration contents and adjust settings to your needs.")
|
"Please make sure to check the configuration contents and adjust settings to your needs."
|
||||||
|
)
|
||||||
|
|
||||||
config_path.write_text(config_text)
|
config_path.write_text(config_text)
|
||||||
|
|
||||||
@@ -255,7 +252,7 @@ def start_new_config(args: Dict[str, Any]) -> None:
|
|||||||
Asking the user questions to fill out the template accordingly.
|
Asking the user questions to fill out the template accordingly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config_path = Path(args['config'][0])
|
config_path = Path(args["config"][0])
|
||||||
chown_user_directory(config_path.parent)
|
chown_user_directory(config_path.parent)
|
||||||
if config_path.exists():
|
if config_path.exists():
|
||||||
overwrite = ask_user_overwrite(config_path)
|
overwrite = ask_user_overwrite(config_path)
|
||||||
@@ -264,22 +261,22 @@ def start_new_config(args: Dict[str, Any]) -> None:
|
|||||||
else:
|
else:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"Configuration file `{config_path}` already exists. "
|
f"Configuration file `{config_path}` already exists. "
|
||||||
"Please delete it or use a different configuration file name.")
|
"Please delete it or use a different configuration file name."
|
||||||
|
)
|
||||||
selections = ask_user_config()
|
selections = ask_user_config()
|
||||||
deploy_new_config(config_path, selections)
|
deploy_new_config(config_path, selections)
|
||||||
|
|
||||||
|
|
||||||
def start_show_config(args: Dict[str, Any]) -> None:
|
def start_show_config(args: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE, set_dry=False)
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE, set_dry=False)
|
||||||
|
|
||||||
# TODO: Sanitize from sensitive info before printing
|
# TODO: Sanitize from sensitive info before printing
|
||||||
|
|
||||||
print("Your combined configuration is:")
|
print("Your combined configuration is:")
|
||||||
config_sanitized = sanitize_config(
|
config_sanitized = sanitize_config(
|
||||||
config['original_config'],
|
config["original_config"], show_sensitive=args.get("show_sensitive", False)
|
||||||
show_sensitive=args.get('show_sensitive', False)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from rich import print_json
|
from rich import print_json
|
||||||
|
|
||||||
print_json(data=config_sanitized)
|
print_json(data=config_sanitized)
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ from typing import Any, Dict
|
|||||||
|
|
||||||
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Config
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Config
|
||||||
from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_format,
|
from freqtrade.data.converter import (
|
||||||
convert_trades_to_ohlcv)
|
convert_ohlcv_format,
|
||||||
|
convert_trades_format,
|
||||||
|
convert_trades_to_ohlcv,
|
||||||
|
)
|
||||||
from freqtrade.data.history import download_data_main
|
from freqtrade.data.history import download_data_main
|
||||||
from freqtrade.enums import CandleType, RunMode, TradingMode
|
from freqtrade.enums import CandleType, RunMode, TradingMode
|
||||||
from freqtrade.exceptions import ConfigurationError
|
from freqtrade.exceptions import ConfigurationError
|
||||||
@@ -20,14 +23,17 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def _check_data_config_download_sanity(config: Config) -> None:
|
def _check_data_config_download_sanity(config: Config) -> None:
|
||||||
if 'days' in config and 'timerange' in config:
|
if "days" in config and "timerange" in config:
|
||||||
raise ConfigurationError("--days and --timerange are mutually exclusive. "
|
raise ConfigurationError(
|
||||||
"You can only specify one or the other.")
|
"--days and --timerange are mutually exclusive. "
|
||||||
|
"You can only specify one or the other."
|
||||||
|
)
|
||||||
|
|
||||||
if 'pairs' not in config:
|
if "pairs" not in config:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"Downloading data requires a list of pairs. "
|
"Downloading data requires a list of pairs. "
|
||||||
"Please check the documentation on how to configure this.")
|
"Please check the documentation on how to configure this."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def start_download_data(args: Dict[str, Any]) -> None:
|
def start_download_data(args: Dict[str, Any]) -> None:
|
||||||
@@ -46,38 +52,41 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def start_convert_trades(args: Dict[str, Any]) -> None:
|
def start_convert_trades(args: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||||
|
|
||||||
timerange = TimeRange()
|
timerange = TimeRange()
|
||||||
|
|
||||||
# Remove stake-currency to skip checks which are not relevant for datadownload
|
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||||
config['stake_currency'] = ''
|
config["stake_currency"] = ""
|
||||||
|
|
||||||
if 'timeframes' not in config:
|
if "timeframes" not in config:
|
||||||
config['timeframes'] = DL_DATA_TIMEFRAMES
|
config["timeframes"] = DL_DATA_TIMEFRAMES
|
||||||
|
|
||||||
# Init exchange
|
# Init exchange
|
||||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
# Manual validations of relevant settings
|
# Manual validations of relevant settings
|
||||||
|
|
||||||
for timeframe in config['timeframes']:
|
for timeframe in config["timeframes"]:
|
||||||
exchange.validate_timeframes(timeframe)
|
exchange.validate_timeframes(timeframe)
|
||||||
available_pairs = [
|
available_pairs = [
|
||||||
p for p in exchange.get_markets(
|
p
|
||||||
tradable_only=True, active_only=not config.get('include_inactive')
|
for p in exchange.get_markets(
|
||||||
).keys()
|
tradable_only=True, active_only=not config.get("include_inactive")
|
||||||
|
).keys()
|
||||||
]
|
]
|
||||||
|
|
||||||
expanded_pairs = dynamic_expand_pairlist(config, available_pairs)
|
expanded_pairs = dynamic_expand_pairlist(config, available_pairs)
|
||||||
|
|
||||||
# Convert downloaded trade data to different timeframes
|
# Convert downloaded trade data to different timeframes
|
||||||
convert_trades_to_ohlcv(
|
convert_trades_to_ohlcv(
|
||||||
pairs=expanded_pairs, timeframes=config['timeframes'],
|
pairs=expanded_pairs,
|
||||||
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
|
timeframes=config["timeframes"],
|
||||||
data_format_ohlcv=config['dataformat_ohlcv'],
|
datadir=config["datadir"],
|
||||||
data_format_trades=config['dataformat_trades'],
|
timerange=timerange,
|
||||||
candle_type=config.get('candle_type_def', CandleType.SPOT)
|
erase=bool(config.get("erase")),
|
||||||
|
data_format_ohlcv=config["dataformat_ohlcv"],
|
||||||
|
data_format_trades=config["dataformat_trades"],
|
||||||
|
candle_type=config.get("candle_type_def", CandleType.SPOT),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -88,14 +97,19 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
|||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
if ohlcv:
|
if ohlcv:
|
||||||
migrate_data(config)
|
migrate_data(config)
|
||||||
convert_ohlcv_format(config,
|
convert_ohlcv_format(
|
||||||
convert_from=args['format_from'],
|
config,
|
||||||
convert_to=args['format_to'],
|
convert_from=args["format_from"],
|
||||||
erase=args['erase'])
|
convert_to=args["format_to"],
|
||||||
|
erase=args["erase"],
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
convert_trades_format(config,
|
convert_trades_format(
|
||||||
convert_from=args['format_from_trades'], convert_to=args['format_to'],
|
config,
|
||||||
erase=args['erase'])
|
convert_from=args["format_from_trades"],
|
||||||
|
convert_to=args["format_to"],
|
||||||
|
erase=args["erase"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def start_list_data(args: Dict[str, Any]) -> None:
|
def start_list_data(args: Dict[str, Any]) -> None:
|
||||||
@@ -108,45 +122,59 @@ def start_list_data(args: Dict[str, Any]) -> None:
|
|||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
from freqtrade.data.history import get_datahandler
|
from freqtrade.data.history import get_datahandler
|
||||||
dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv'])
|
|
||||||
|
dhc = get_datahandler(config["datadir"], config["dataformat_ohlcv"])
|
||||||
|
|
||||||
paircombs = dhc.ohlcv_get_available_data(
|
paircombs = dhc.ohlcv_get_available_data(
|
||||||
config['datadir'],
|
config["datadir"], config.get("trading_mode", TradingMode.SPOT)
|
||||||
config.get('trading_mode', TradingMode.SPOT)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if args['pairs']:
|
if args["pairs"]:
|
||||||
paircombs = [comb for comb in paircombs if comb[0] in args['pairs']]
|
paircombs = [comb for comb in paircombs if comb[0] in args["pairs"]]
|
||||||
|
|
||||||
print(f"Found {len(paircombs)} pair / timeframe combinations.")
|
print(f"Found {len(paircombs)} pair / timeframe combinations.")
|
||||||
if not config.get('show_timerange'):
|
if not config.get("show_timerange"):
|
||||||
groupedpair = defaultdict(list)
|
groupedpair = defaultdict(list)
|
||||||
for pair, timeframe, candle_type in sorted(
|
for pair, timeframe, candle_type in sorted(
|
||||||
paircombs,
|
paircombs, key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])
|
||||||
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])
|
|
||||||
):
|
):
|
||||||
groupedpair[(pair, candle_type)].append(timeframe)
|
groupedpair[(pair, candle_type)].append(timeframe)
|
||||||
|
|
||||||
if groupedpair:
|
if groupedpair:
|
||||||
print(tabulate([
|
print(
|
||||||
(pair, ', '.join(timeframes), candle_type)
|
tabulate(
|
||||||
for (pair, candle_type), timeframes in groupedpair.items()
|
[
|
||||||
],
|
(pair, ", ".join(timeframes), candle_type)
|
||||||
headers=("Pair", "Timeframe", "Type"),
|
for (pair, candle_type), timeframes in groupedpair.items()
|
||||||
tablefmt='psql', stralign='right'))
|
],
|
||||||
|
headers=("Pair", "Timeframe", "Type"),
|
||||||
|
tablefmt="psql",
|
||||||
|
stralign="right",
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
paircombs1 = [(
|
paircombs1 = [
|
||||||
pair, timeframe, candle_type,
|
(pair, timeframe, candle_type, *dhc.ohlcv_data_min_max(pair, timeframe, candle_type))
|
||||||
*dhc.ohlcv_data_min_max(pair, timeframe, candle_type)
|
for pair, timeframe, candle_type in paircombs
|
||||||
) for pair, timeframe, candle_type in paircombs]
|
]
|
||||||
|
|
||||||
print(tabulate([
|
print(
|
||||||
(pair, timeframe, candle_type,
|
tabulate(
|
||||||
start.strftime(DATETIME_PRINT_FORMAT),
|
[
|
||||||
end.strftime(DATETIME_PRINT_FORMAT), length)
|
(
|
||||||
for pair, timeframe, candle_type, start, end, length in sorted(
|
pair,
|
||||||
paircombs1,
|
timeframe,
|
||||||
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2]))
|
candle_type,
|
||||||
],
|
start.strftime(DATETIME_PRINT_FORMAT),
|
||||||
headers=("Pair", "Timeframe", "Type", 'From', 'To', 'Candles'),
|
end.strftime(DATETIME_PRINT_FORMAT),
|
||||||
tablefmt='psql', stralign='right'))
|
length,
|
||||||
|
)
|
||||||
|
for pair, timeframe, candle_type, start, end, length in sorted(
|
||||||
|
paircombs1, key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])
|
||||||
|
)
|
||||||
|
],
|
||||||
|
headers=("Pair", "Timeframe", "Type", "From", "To", "Candles"),
|
||||||
|
tablefmt="psql",
|
||||||
|
stralign="right",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ def start_convert_db(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
init_db(config['db_url'])
|
init_db(config["db_url"])
|
||||||
session_target = Trade.session
|
session_target = Trade.session
|
||||||
init_db(config['db_url_from'])
|
init_db(config["db_url_from"])
|
||||||
logger.info("Starting db migration.")
|
logger.info("Starting db migration.")
|
||||||
|
|
||||||
trade_count = 0
|
trade_count = 0
|
||||||
@@ -47,9 +47,11 @@ def start_convert_db(args: Dict[str, Any]) -> None:
|
|||||||
max_order_id = session_target.scalar(select(func.max(Order.id)))
|
max_order_id = session_target.scalar(select(func.max(Order.id)))
|
||||||
max_pairlock_id = session_target.scalar(select(func.max(PairLock.id)))
|
max_pairlock_id = session_target.scalar(select(func.max(PairLock.id)))
|
||||||
|
|
||||||
set_sequence_ids(session_target.get_bind(),
|
set_sequence_ids(
|
||||||
trade_id=max_trade_id,
|
session_target.get_bind(),
|
||||||
order_id=max_order_id,
|
trade_id=max_trade_id,
|
||||||
pairlock_id=max_pairlock_id)
|
order_id=max_order_id,
|
||||||
|
pairlock_id=max_pairlock_id,
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(f"Migrated {trade_count} Trades, and {pairlock_count} Pairlocks.")
|
logger.info(f"Migrated {trade_count} Trades, and {pairlock_count} Pairlocks.")
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ from freqtrade.util import render_template, render_template_with_fallback
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Timeout for requests
|
||||||
|
req_timeout = 30
|
||||||
|
|
||||||
|
|
||||||
def start_create_userdir(args: Dict[str, Any]) -> None:
|
def start_create_userdir(args: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Create "user_data" directory to contain user data strategies, hyperopt, ...)
|
Create "user_data" directory to contain user data strategies, hyperopt, ...)
|
||||||
@@ -34,7 +38,7 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
|
|||||||
"""
|
"""
|
||||||
Deploy new strategy from template to strategy_path
|
Deploy new strategy from template to strategy_path
|
||||||
"""
|
"""
|
||||||
fallback = 'full'
|
fallback = "full"
|
||||||
attributes = render_template_with_fallback(
|
attributes = render_template_with_fallback(
|
||||||
templatefile=f"strategy_subtemplates/strategy_attributes_{subtemplate}.j2",
|
templatefile=f"strategy_subtemplates/strategy_attributes_{subtemplate}.j2",
|
||||||
templatefallbackfile=f"strategy_subtemplates/strategy_attributes_{fallback}.j2",
|
templatefallbackfile=f"strategy_subtemplates/strategy_attributes_{fallback}.j2",
|
||||||
@@ -60,33 +64,35 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
|
|||||||
templatefallbackfile="strategy_subtemplates/strategy_methods_empty.j2",
|
templatefallbackfile="strategy_subtemplates/strategy_methods_empty.j2",
|
||||||
)
|
)
|
||||||
|
|
||||||
strategy_text = render_template(templatefile='base_strategy.py.j2',
|
strategy_text = render_template(
|
||||||
arguments={"strategy": strategy_name,
|
templatefile="base_strategy.py.j2",
|
||||||
"attributes": attributes,
|
arguments={
|
||||||
"indicators": indicators,
|
"strategy": strategy_name,
|
||||||
"buy_trend": buy_trend,
|
"attributes": attributes,
|
||||||
"sell_trend": sell_trend,
|
"indicators": indicators,
|
||||||
"plot_config": plot_config,
|
"buy_trend": buy_trend,
|
||||||
"additional_methods": additional_methods,
|
"sell_trend": sell_trend,
|
||||||
})
|
"plot_config": plot_config,
|
||||||
|
"additional_methods": additional_methods,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(f"Writing strategy to `{strategy_path}`.")
|
logger.info(f"Writing strategy to `{strategy_path}`.")
|
||||||
strategy_path.write_text(strategy_text)
|
strategy_path.write_text(strategy_text)
|
||||||
|
|
||||||
|
|
||||||
def start_new_strategy(args: Dict[str, Any]) -> None:
|
def start_new_strategy(args: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
if "strategy" in args and args["strategy"]:
|
if "strategy" in args and args["strategy"]:
|
||||||
|
new_path = config["user_data_dir"] / USERPATH_STRATEGIES / (args["strategy"] + ".py")
|
||||||
new_path = config['user_data_dir'] / USERPATH_STRATEGIES / (args['strategy'] + '.py')
|
|
||||||
|
|
||||||
if new_path.exists():
|
if new_path.exists():
|
||||||
raise OperationalException(f"`{new_path}` already exists. "
|
raise OperationalException(
|
||||||
"Please choose another Strategy Name.")
|
f"`{new_path}` already exists. Please choose another Strategy Name."
|
||||||
|
)
|
||||||
|
|
||||||
deploy_new_strategy(args['strategy'], new_path, args['template'])
|
deploy_new_strategy(args["strategy"], new_path, args["template"])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ConfigurationError("`new-strategy` requires --strategy to be set.")
|
raise ConfigurationError("`new-strategy` requires --strategy to be set.")
|
||||||
@@ -96,8 +102,8 @@ def clean_ui_subdir(directory: Path):
|
|||||||
if directory.is_dir():
|
if directory.is_dir():
|
||||||
logger.info("Removing UI directory content.")
|
logger.info("Removing UI directory content.")
|
||||||
|
|
||||||
for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root
|
for p in reversed(list(directory.glob("**/*"))): # iterate contents from leaves to root
|
||||||
if p.name in ('.gitkeep', 'fallback_file.html'):
|
if p.name in (".gitkeep", "fallback_file.html"):
|
||||||
continue
|
continue
|
||||||
if p.is_file():
|
if p.is_file():
|
||||||
p.unlink()
|
p.unlink()
|
||||||
@@ -106,11 +112,11 @@ def clean_ui_subdir(directory: Path):
|
|||||||
|
|
||||||
|
|
||||||
def read_ui_version(dest_folder: Path) -> Optional[str]:
|
def read_ui_version(dest_folder: Path) -> Optional[str]:
|
||||||
file = dest_folder / '.uiversion'
|
file = dest_folder / ".uiversion"
|
||||||
if not file.is_file():
|
if not file.is_file():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
with file.open('r') as f:
|
with file.open("r") as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
@@ -119,7 +125,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
|
|||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
logger.info(f"Downloading {dl_url}")
|
logger.info(f"Downloading {dl_url}")
|
||||||
resp = requests.get(dl_url).content
|
resp = requests.get(dl_url, timeout=req_timeout).content
|
||||||
dest_folder.mkdir(parents=True, exist_ok=True)
|
dest_folder.mkdir(parents=True, exist_ok=True)
|
||||||
with ZipFile(BytesIO(resp)) as zf:
|
with ZipFile(BytesIO(resp)) as zf:
|
||||||
for fn in zf.filelist:
|
for fn in zf.filelist:
|
||||||
@@ -129,55 +135,54 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
|
|||||||
destfile.mkdir(exist_ok=True)
|
destfile.mkdir(exist_ok=True)
|
||||||
else:
|
else:
|
||||||
destfile.write_bytes(x.read())
|
destfile.write_bytes(x.read())
|
||||||
with (dest_folder / '.uiversion').open('w') as f:
|
with (dest_folder / ".uiversion").open("w") as f:
|
||||||
f.write(version)
|
f.write(version)
|
||||||
|
|
||||||
|
|
||||||
def get_ui_download_url(version: Optional[str] = None) -> Tuple[str, str]:
|
def get_ui_download_url(version: Optional[str] = None) -> Tuple[str, str]:
|
||||||
base_url = 'https://api.github.com/repos/freqtrade/frequi/'
|
base_url = "https://api.github.com/repos/freqtrade/frequi/"
|
||||||
# Get base UI Repo path
|
# Get base UI Repo path
|
||||||
|
|
||||||
resp = requests.get(f"{base_url}releases")
|
resp = requests.get(f"{base_url}releases", timeout=req_timeout)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
r = resp.json()
|
r = resp.json()
|
||||||
|
|
||||||
if version:
|
if version:
|
||||||
tmp = [x for x in r if x['name'] == version]
|
tmp = [x for x in r if x["name"] == version]
|
||||||
if tmp:
|
if tmp:
|
||||||
latest_version = tmp[0]['name']
|
latest_version = tmp[0]["name"]
|
||||||
assets = tmp[0].get('assets', [])
|
assets = tmp[0].get("assets", [])
|
||||||
else:
|
else:
|
||||||
raise ValueError("UI-Version not found.")
|
raise ValueError("UI-Version not found.")
|
||||||
else:
|
else:
|
||||||
latest_version = r[0]['name']
|
latest_version = r[0]["name"]
|
||||||
assets = r[0].get('assets', [])
|
assets = r[0].get("assets", [])
|
||||||
dl_url = ''
|
dl_url = ""
|
||||||
if assets and len(assets) > 0:
|
if assets and len(assets) > 0:
|
||||||
dl_url = assets[0]['browser_download_url']
|
dl_url = assets[0]["browser_download_url"]
|
||||||
|
|
||||||
# URL not found - try assets url
|
# URL not found - try assets url
|
||||||
if not dl_url:
|
if not dl_url:
|
||||||
assets = r[0]['assets_url']
|
assets = r[0]["assets_url"]
|
||||||
resp = requests.get(assets)
|
resp = requests.get(assets, timeout=req_timeout)
|
||||||
r = resp.json()
|
r = resp.json()
|
||||||
dl_url = r[0]['browser_download_url']
|
dl_url = r[0]["browser_download_url"]
|
||||||
|
|
||||||
return dl_url, latest_version
|
return dl_url, latest_version
|
||||||
|
|
||||||
|
|
||||||
def start_install_ui(args: Dict[str, Any]) -> None:
|
def start_install_ui(args: Dict[str, Any]) -> None:
|
||||||
|
dest_folder = Path(__file__).parents[1] / "rpc/api_server/ui/installed/"
|
||||||
dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui/installed/'
|
|
||||||
# First make sure the assets are removed.
|
# First make sure the assets are removed.
|
||||||
dl_url, latest_version = get_ui_download_url(args.get('ui_version'))
|
dl_url, latest_version = get_ui_download_url(args.get("ui_version"))
|
||||||
|
|
||||||
curr_version = read_ui_version(dest_folder)
|
curr_version = read_ui_version(dest_folder)
|
||||||
if curr_version == latest_version and not args.get('erase_ui_only'):
|
if curr_version == latest_version and not args.get("erase_ui_only"):
|
||||||
logger.info(f"UI already up-to-date, FreqUI Version {curr_version}.")
|
logger.info(f"UI already up-to-date, FreqUI Version {curr_version}.")
|
||||||
return
|
return
|
||||||
|
|
||||||
clean_ui_subdir(dest_folder)
|
clean_ui_subdir(dest_folder)
|
||||||
if args.get('erase_ui_only'):
|
if args.get("erase_ui_only"):
|
||||||
logger.info("Erased UI directory content. Not downloading new version.")
|
logger.info("Erased UI directory content. Not downloading new version.")
|
||||||
else:
|
else:
|
||||||
# Download a new version
|
# Download a new version
|
||||||
|
|||||||
@@ -22,15 +22,15 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
print_colorized = config.get('print_colorized', False)
|
print_colorized = config.get("print_colorized", False)
|
||||||
print_json = config.get('print_json', False)
|
print_json = config.get("print_json", False)
|
||||||
export_csv = config.get('export_csv')
|
export_csv = config.get("export_csv")
|
||||||
no_details = config.get('hyperopt_list_no_details', False)
|
no_details = config.get("hyperopt_list_no_details", False)
|
||||||
no_header = False
|
no_header = False
|
||||||
|
|
||||||
results_file = get_latest_hyperopt_file(
|
results_file = get_latest_hyperopt_file(
|
||||||
config['user_data_dir'] / 'hyperopt_results',
|
config["user_data_dir"] / "hyperopt_results", config.get("hyperoptexportfilename")
|
||||||
config.get('hyperoptexportfilename'))
|
)
|
||||||
|
|
||||||
# Previous evaluations
|
# Previous evaluations
|
||||||
epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
|
epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
|
||||||
@@ -40,21 +40,26 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
if not export_csv:
|
if not export_csv:
|
||||||
try:
|
try:
|
||||||
print(HyperoptTools.get_result_table(config, epochs, total_epochs,
|
print(
|
||||||
not config.get('hyperopt_list_best', False),
|
HyperoptTools.get_result_table(
|
||||||
print_colorized, 0))
|
config,
|
||||||
|
epochs,
|
||||||
|
total_epochs,
|
||||||
|
not config.get("hyperopt_list_best", False),
|
||||||
|
print_colorized,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print('User interrupted..')
|
print("User interrupted..")
|
||||||
|
|
||||||
if epochs and not no_details:
|
if epochs and not no_details:
|
||||||
sorted_epochs = sorted(epochs, key=itemgetter('loss'))
|
sorted_epochs = sorted(epochs, key=itemgetter("loss"))
|
||||||
results = sorted_epochs[0]
|
results = sorted_epochs[0]
|
||||||
HyperoptTools.show_epoch_details(results, total_epochs, print_json, no_header)
|
HyperoptTools.show_epoch_details(results, total_epochs, print_json, no_header)
|
||||||
|
|
||||||
if epochs and export_csv:
|
if epochs and export_csv:
|
||||||
HyperoptTools.export_csv_file(
|
HyperoptTools.export_csv_file(config, epochs, export_csv)
|
||||||
config, epochs, export_csv
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
||||||
@@ -65,13 +70,13 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
print_json = config.get('print_json', False)
|
print_json = config.get("print_json", False)
|
||||||
no_header = config.get('hyperopt_show_no_header', False)
|
no_header = config.get("hyperopt_show_no_header", False)
|
||||||
results_file = get_latest_hyperopt_file(
|
results_file = get_latest_hyperopt_file(
|
||||||
config['user_data_dir'] / 'hyperopt_results',
|
config["user_data_dir"] / "hyperopt_results", config.get("hyperoptexportfilename")
|
||||||
config.get('hyperoptexportfilename'))
|
)
|
||||||
|
|
||||||
n = config.get('hyperopt_show_index', -1)
|
n = config.get("hyperopt_show_index", -1)
|
||||||
|
|
||||||
# Previous evaluations
|
# Previous evaluations
|
||||||
epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
|
epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
|
||||||
@@ -80,10 +85,12 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
if n > filtered_epochs:
|
if n > filtered_epochs:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"The index of the epoch to show should be less than {filtered_epochs + 1}.")
|
f"The index of the epoch to show should be less than {filtered_epochs + 1}."
|
||||||
|
)
|
||||||
if n < -filtered_epochs:
|
if n < -filtered_epochs:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"The index of the epoch to show should be greater than {-filtered_epochs - 1}.")
|
f"The index of the epoch to show should be greater than {-filtered_epochs - 1}."
|
||||||
|
)
|
||||||
|
|
||||||
# Translate epoch index from human-readable format to pythonic
|
# Translate epoch index from human-readable format to pythonic
|
||||||
if n > 0:
|
if n > 0:
|
||||||
@@ -92,13 +99,18 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
|||||||
if epochs:
|
if epochs:
|
||||||
val = epochs[n]
|
val = epochs[n]
|
||||||
|
|
||||||
metrics = val['results_metrics']
|
metrics = val["results_metrics"]
|
||||||
if 'strategy_name' in metrics:
|
if "strategy_name" in metrics:
|
||||||
strategy_name = metrics['strategy_name']
|
strategy_name = metrics["strategy_name"]
|
||||||
show_backtest_result(strategy_name, metrics,
|
show_backtest_result(
|
||||||
metrics['stake_currency'], config.get('backtest_breakdown', []))
|
strategy_name,
|
||||||
|
metrics,
|
||||||
|
metrics["stake_currency"],
|
||||||
|
config.get("backtest_breakdown", []),
|
||||||
|
)
|
||||||
|
|
||||||
HyperoptTools.try_export_params(config, strategy_name, val)
|
HyperoptTools.try_export_params(config, strategy_name, val)
|
||||||
|
|
||||||
HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header,
|
HyperoptTools.show_epoch_details(
|
||||||
header_str="Epoch details")
|
val, total_epochs, print_json, no_header, header_str="Epoch details"
|
||||||
|
)
|
||||||
|
|||||||
@@ -26,42 +26,47 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
|
|||||||
:param args: Cli args from Arguments()
|
:param args: Cli args from Arguments()
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
exchanges = list_available_exchanges(args['list_exchanges_all'])
|
exchanges = list_available_exchanges(args["list_exchanges_all"])
|
||||||
|
|
||||||
if args['print_one_column']:
|
if args["print_one_column"]:
|
||||||
print('\n'.join([e['name'] for e in exchanges]))
|
print("\n".join([e["name"] for e in exchanges]))
|
||||||
else:
|
else:
|
||||||
headers = {
|
headers = {
|
||||||
'name': 'Exchange name',
|
"name": "Exchange name",
|
||||||
'supported': 'Supported',
|
"supported": "Supported",
|
||||||
'trade_modes': 'Markets',
|
"trade_modes": "Markets",
|
||||||
'comment': 'Reason',
|
"comment": "Reason",
|
||||||
}
|
}
|
||||||
headers.update({'valid': 'Valid'} if args['list_exchanges_all'] else {})
|
headers.update({"valid": "Valid"} if args["list_exchanges_all"] else {})
|
||||||
|
|
||||||
def build_entry(exchange: ValidExchangesType, valid: bool):
|
def build_entry(exchange: ValidExchangesType, valid: bool):
|
||||||
valid_entry = {'valid': exchange['valid']} if valid else {}
|
valid_entry = {"valid": exchange["valid"]} if valid else {}
|
||||||
result: Dict[str, Union[str, bool]] = {
|
result: Dict[str, Union[str, bool]] = {
|
||||||
'name': exchange['name'],
|
"name": exchange["name"],
|
||||||
**valid_entry,
|
**valid_entry,
|
||||||
'supported': 'Official' if exchange['supported'] else '',
|
"supported": "Official" if exchange["supported"] else "",
|
||||||
'trade_modes': ', '.join(
|
"trade_modes": ", ".join(
|
||||||
(f"{a['margin_mode']} " if a['margin_mode'] else '') + a['trading_mode']
|
(f"{a['margin_mode']} " if a["margin_mode"] else "") + a["trading_mode"]
|
||||||
for a in exchange['trade_modes']
|
for a in exchange["trade_modes"]
|
||||||
),
|
),
|
||||||
'comment': exchange['comment'],
|
"comment": exchange["comment"],
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
if args['list_exchanges_all']:
|
if args["list_exchanges_all"]:
|
||||||
print("All exchanges supported by the ccxt library:")
|
print("All exchanges supported by the ccxt library:")
|
||||||
exchanges = [build_entry(e, True) for e in exchanges]
|
exchanges = [build_entry(e, True) for e in exchanges]
|
||||||
else:
|
else:
|
||||||
print("Exchanges available for Freqtrade:")
|
print("Exchanges available for Freqtrade:")
|
||||||
exchanges = [build_entry(e, False) for e in exchanges if e['valid'] is not False]
|
exchanges = [build_entry(e, False) for e in exchanges if e["valid"] is not False]
|
||||||
|
|
||||||
print(tabulate(exchanges, headers=headers, ))
|
print(
|
||||||
|
tabulate(
|
||||||
|
exchanges,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
|
def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
|
||||||
@@ -71,26 +76,35 @@ def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
|
|||||||
yellow = Fore.YELLOW
|
yellow = Fore.YELLOW
|
||||||
reset = Style.RESET_ALL
|
reset = Style.RESET_ALL
|
||||||
else:
|
else:
|
||||||
red = ''
|
red = ""
|
||||||
yellow = ''
|
yellow = ""
|
||||||
reset = ''
|
reset = ""
|
||||||
|
|
||||||
names = [s['name'] for s in objs]
|
names = [s["name"] for s in objs]
|
||||||
objs_to_print = [{
|
objs_to_print = [
|
||||||
'name': s['name'] if s['name'] else "--",
|
{
|
||||||
'location': s['location_rel'],
|
"name": s["name"] if s["name"] else "--",
|
||||||
'status': (red + "LOAD FAILED" + reset if s['class'] is None
|
"location": s["location_rel"],
|
||||||
else "OK" if names.count(s['name']) == 1
|
"status": (
|
||||||
else yellow + "DUPLICATE NAME" + reset)
|
red + "LOAD FAILED" + reset
|
||||||
} for s in objs]
|
if s["class"] is None
|
||||||
|
else "OK"
|
||||||
|
if names.count(s["name"]) == 1
|
||||||
|
else yellow + "DUPLICATE NAME" + reset
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for s in objs
|
||||||
|
]
|
||||||
for idx, s in enumerate(objs):
|
for idx, s in enumerate(objs):
|
||||||
if 'hyperoptable' in s:
|
if "hyperoptable" in s:
|
||||||
objs_to_print[idx].update({
|
objs_to_print[idx].update(
|
||||||
'hyperoptable': "Yes" if s['hyperoptable']['count'] > 0 else "No",
|
{
|
||||||
'buy-Params': len(s['hyperoptable'].get('buy', [])),
|
"hyperoptable": "Yes" if s["hyperoptable"]["count"] > 0 else "No",
|
||||||
'sell-Params': len(s['hyperoptable'].get('sell', [])),
|
"buy-Params": len(s["hyperoptable"].get("buy", [])),
|
||||||
})
|
"sell-Params": len(s["hyperoptable"].get("sell", [])),
|
||||||
print(tabulate(objs_to_print, headers='keys', tablefmt='psql', stralign='right'))
|
}
|
||||||
|
)
|
||||||
|
print(tabulate(objs_to_print, headers="keys", tablefmt="psql", stralign="right"))
|
||||||
|
|
||||||
|
|
||||||
def start_list_strategies(args: Dict[str, Any]) -> None:
|
def start_list_strategies(args: Dict[str, Any]) -> None:
|
||||||
@@ -100,19 +114,20 @@ def start_list_strategies(args: Dict[str, Any]) -> None:
|
|||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
strategy_objs = StrategyResolver.search_all_objects(
|
strategy_objs = StrategyResolver.search_all_objects(
|
||||||
config, not args['print_one_column'], config.get('recursive_strategy_search', False))
|
config, not args["print_one_column"], config.get("recursive_strategy_search", False)
|
||||||
|
)
|
||||||
# Sort alphabetically
|
# Sort alphabetically
|
||||||
strategy_objs = sorted(strategy_objs, key=lambda x: x['name'])
|
strategy_objs = sorted(strategy_objs, key=lambda x: x["name"])
|
||||||
for obj in strategy_objs:
|
for obj in strategy_objs:
|
||||||
if obj['class']:
|
if obj["class"]:
|
||||||
obj['hyperoptable'] = obj['class'].detect_all_parameters()
|
obj["hyperoptable"] = obj["class"].detect_all_parameters()
|
||||||
else:
|
else:
|
||||||
obj['hyperoptable'] = {'count': 0}
|
obj["hyperoptable"] = {"count": 0}
|
||||||
|
|
||||||
if args['print_one_column']:
|
if args["print_one_column"]:
|
||||||
print('\n'.join([s['name'] for s in strategy_objs]))
|
print("\n".join([s["name"] for s in strategy_objs]))
|
||||||
else:
|
else:
|
||||||
_print_objs_tabular(strategy_objs, config.get('print_colorized', False))
|
_print_objs_tabular(strategy_objs, config.get("print_colorized", False))
|
||||||
|
|
||||||
|
|
||||||
def start_list_freqAI_models(args: Dict[str, Any]) -> None:
|
def start_list_freqAI_models(args: Dict[str, Any]) -> None:
|
||||||
@@ -121,13 +136,14 @@ def start_list_freqAI_models(args: Dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
|
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
|
||||||
model_objs = FreqaiModelResolver.search_all_objects(config, not args['print_one_column'])
|
|
||||||
|
model_objs = FreqaiModelResolver.search_all_objects(config, not args["print_one_column"])
|
||||||
# Sort alphabetically
|
# Sort alphabetically
|
||||||
model_objs = sorted(model_objs, key=lambda x: x['name'])
|
model_objs = sorted(model_objs, key=lambda x: x["name"])
|
||||||
if args['print_one_column']:
|
if args["print_one_column"]:
|
||||||
print('\n'.join([s['name'] for s in model_objs]))
|
print("\n".join([s["name"] for s in model_objs]))
|
||||||
else:
|
else:
|
||||||
_print_objs_tabular(model_objs, config.get('print_colorized', False))
|
_print_objs_tabular(model_objs, config.get("print_colorized", False))
|
||||||
|
|
||||||
|
|
||||||
def start_list_timeframes(args: Dict[str, Any]) -> None:
|
def start_list_timeframes(args: Dict[str, Any]) -> None:
|
||||||
@@ -136,16 +152,18 @@ def start_list_timeframes(args: Dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||||
# Do not use timeframe set in the config
|
# Do not use timeframe set in the config
|
||||||
config['timeframe'] = None
|
config["timeframe"] = None
|
||||||
|
|
||||||
# Init exchange
|
# Init exchange
|
||||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
|
|
||||||
if args['print_one_column']:
|
if args["print_one_column"]:
|
||||||
print('\n'.join(exchange.timeframes))
|
print("\n".join(exchange.timeframes))
|
||||||
else:
|
else:
|
||||||
print(f"Timeframes available for the exchange `{exchange.name}`: "
|
print(
|
||||||
f"{', '.join(exchange.timeframes)}")
|
f"Timeframes available for the exchange `{exchange.name}`: "
|
||||||
|
f"{', '.join(exchange.timeframes)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
|
def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
|
||||||
@@ -161,51 +179,75 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
|
|||||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
|
|
||||||
# By default only active pairs/markets are to be shown
|
# By default only active pairs/markets are to be shown
|
||||||
active_only = not args.get('list_pairs_all', False)
|
active_only = not args.get("list_pairs_all", False)
|
||||||
|
|
||||||
base_currencies = args.get('base_currencies', [])
|
base_currencies = args.get("base_currencies", [])
|
||||||
quote_currencies = args.get('quote_currencies', [])
|
quote_currencies = args.get("quote_currencies", [])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pairs = exchange.get_markets(base_currencies=base_currencies,
|
pairs = exchange.get_markets(
|
||||||
quote_currencies=quote_currencies,
|
base_currencies=base_currencies,
|
||||||
tradable_only=pairs_only,
|
quote_currencies=quote_currencies,
|
||||||
active_only=active_only)
|
tradable_only=pairs_only,
|
||||||
|
active_only=active_only,
|
||||||
|
)
|
||||||
# Sort the pairs/markets by symbol
|
# Sort the pairs/markets by symbol
|
||||||
pairs = dict(sorted(pairs.items()))
|
pairs = dict(sorted(pairs.items()))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise OperationalException(f"Cannot get markets. Reason: {e}") from e
|
raise OperationalException(f"Cannot get markets. Reason: {e}") from e
|
||||||
|
|
||||||
else:
|
else:
|
||||||
summary_str = ((f"Exchange {exchange.name} has {len(pairs)} ") +
|
summary_str = (
|
||||||
("active " if active_only else "") +
|
(f"Exchange {exchange.name} has {len(pairs)} ")
|
||||||
(plural(len(pairs), "pair" if pairs_only else "market")) +
|
+ ("active " if active_only else "")
|
||||||
(f" with {', '.join(base_currencies)} as base "
|
+ (plural(len(pairs), "pair" if pairs_only else "market"))
|
||||||
f"{plural(len(base_currencies), 'currency', 'currencies')}"
|
+ (
|
||||||
if base_currencies else "") +
|
f" with {', '.join(base_currencies)} as base "
|
||||||
(" and" if base_currencies and quote_currencies else "") +
|
f"{plural(len(base_currencies), 'currency', 'currencies')}"
|
||||||
(f" with {', '.join(quote_currencies)} as quote "
|
if base_currencies
|
||||||
f"{plural(len(quote_currencies), 'currency', 'currencies')}"
|
else ""
|
||||||
if quote_currencies else ""))
|
)
|
||||||
|
+ (" and" if base_currencies and quote_currencies else "")
|
||||||
|
+ (
|
||||||
|
f" with {', '.join(quote_currencies)} as quote "
|
||||||
|
f"{plural(len(quote_currencies), 'currency', 'currencies')}"
|
||||||
|
if quote_currencies
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
headers = ["Id", "Symbol", "Base", "Quote", "Active",
|
headers = [
|
||||||
"Spot", "Margin", "Future", "Leverage"]
|
"Id",
|
||||||
|
"Symbol",
|
||||||
|
"Base",
|
||||||
|
"Quote",
|
||||||
|
"Active",
|
||||||
|
"Spot",
|
||||||
|
"Margin",
|
||||||
|
"Future",
|
||||||
|
"Leverage",
|
||||||
|
]
|
||||||
|
|
||||||
tabular_data = [{
|
tabular_data = [
|
||||||
'Id': v['id'],
|
{
|
||||||
'Symbol': v['symbol'],
|
"Id": v["id"],
|
||||||
'Base': v['base'],
|
"Symbol": v["symbol"],
|
||||||
'Quote': v['quote'],
|
"Base": v["base"],
|
||||||
'Active': market_is_active(v),
|
"Quote": v["quote"],
|
||||||
'Spot': 'Spot' if exchange.market_is_spot(v) else '',
|
"Active": market_is_active(v),
|
||||||
'Margin': 'Margin' if exchange.market_is_margin(v) else '',
|
"Spot": "Spot" if exchange.market_is_spot(v) else "",
|
||||||
'Future': 'Future' if exchange.market_is_future(v) else '',
|
"Margin": "Margin" if exchange.market_is_margin(v) else "",
|
||||||
'Leverage': exchange.get_max_leverage(v['symbol'], 20)
|
"Future": "Future" if exchange.market_is_future(v) else "",
|
||||||
} for _, v in pairs.items()]
|
"Leverage": exchange.get_max_leverage(v["symbol"], 20),
|
||||||
|
}
|
||||||
|
for _, v in pairs.items()
|
||||||
|
]
|
||||||
|
|
||||||
if (args.get('print_one_column', False) or
|
if (
|
||||||
args.get('list_pairs_print_json', False) or
|
args.get("print_one_column", False)
|
||||||
args.get('print_csv', False)):
|
or args.get("list_pairs_print_json", False)
|
||||||
|
or args.get("print_csv", False)
|
||||||
|
):
|
||||||
# Print summary string in the log in case of machine-readable
|
# Print summary string in the log in case of machine-readable
|
||||||
# regular formats.
|
# regular formats.
|
||||||
logger.info(f"{summary_str}.")
|
logger.info(f"{summary_str}.")
|
||||||
@@ -215,24 +257,26 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
if pairs:
|
if pairs:
|
||||||
if args.get('print_list', False):
|
if args.get("print_list", False):
|
||||||
# print data as a list, with human-readable summary
|
# print data as a list, with human-readable summary
|
||||||
print(f"{summary_str}: {', '.join(pairs.keys())}.")
|
print(f"{summary_str}: {', '.join(pairs.keys())}.")
|
||||||
elif args.get('print_one_column', False):
|
elif args.get("print_one_column", False):
|
||||||
print('\n'.join(pairs.keys()))
|
print("\n".join(pairs.keys()))
|
||||||
elif args.get('list_pairs_print_json', False):
|
elif args.get("list_pairs_print_json", False):
|
||||||
print(rapidjson.dumps(list(pairs.keys()), default=str))
|
print(rapidjson.dumps(list(pairs.keys()), default=str))
|
||||||
elif args.get('print_csv', False):
|
elif args.get("print_csv", False):
|
||||||
writer = csv.DictWriter(sys.stdout, fieldnames=headers)
|
writer = csv.DictWriter(sys.stdout, fieldnames=headers)
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
writer.writerows(tabular_data)
|
writer.writerows(tabular_data)
|
||||||
else:
|
else:
|
||||||
# print data as a table, with the human-readable summary
|
# print data as a table, with the human-readable summary
|
||||||
print(f"{summary_str}:")
|
print(f"{summary_str}:")
|
||||||
print(tabulate(tabular_data, headers='keys', tablefmt='psql', stralign='right'))
|
print(tabulate(tabular_data, headers="keys", tablefmt="psql", stralign="right"))
|
||||||
elif not (args.get('print_one_column', False) or
|
elif not (
|
||||||
args.get('list_pairs_print_json', False) or
|
args.get("print_one_column", False)
|
||||||
args.get('print_csv', False)):
|
or args.get("list_pairs_print_json", False)
|
||||||
|
or args.get("print_csv", False)
|
||||||
|
):
|
||||||
print(f"{summary_str}.")
|
print(f"{summary_str}.")
|
||||||
|
|
||||||
|
|
||||||
@@ -243,21 +287,22 @@ def start_show_trades(args: Dict[str, Any]) -> None:
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from freqtrade.persistence import Trade, init_db
|
from freqtrade.persistence import Trade, init_db
|
||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
if 'db_url' not in config:
|
if "db_url" not in config:
|
||||||
raise ConfigurationError("--db-url is required for this command.")
|
raise ConfigurationError("--db-url is required for this command.")
|
||||||
|
|
||||||
logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
|
logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
|
||||||
init_db(config['db_url'])
|
init_db(config["db_url"])
|
||||||
tfilter = []
|
tfilter = []
|
||||||
|
|
||||||
if config.get('trade_ids'):
|
if config.get("trade_ids"):
|
||||||
tfilter.append(Trade.id.in_(config['trade_ids']))
|
tfilter.append(Trade.id.in_(config["trade_ids"]))
|
||||||
|
|
||||||
trades = Trade.get_trades(tfilter).all()
|
trades = Trade.get_trades(tfilter).all()
|
||||||
logger.info(f"Printing {len(trades)} Trades: ")
|
logger.info(f"Printing {len(trades)} Trades: ")
|
||||||
if config.get('print_json', False):
|
if config.get("print_json", False):
|
||||||
print(json.dumps([trade.to_json() for trade in trades], indent=4))
|
print(json.dumps([trade.to_json() for trade in trades], indent=4))
|
||||||
else:
|
else:
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
|
|||||||
@@ -21,20 +21,22 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
|
|||||||
config = setup_utils_configuration(args, method)
|
config = setup_utils_configuration(args, method)
|
||||||
|
|
||||||
no_unlimited_runmodes = {
|
no_unlimited_runmodes = {
|
||||||
RunMode.BACKTEST: 'backtesting',
|
RunMode.BACKTEST: "backtesting",
|
||||||
RunMode.HYPEROPT: 'hyperoptimization',
|
RunMode.HYPEROPT: "hyperoptimization",
|
||||||
}
|
}
|
||||||
if method in no_unlimited_runmodes.keys():
|
if method in no_unlimited_runmodes.keys():
|
||||||
wallet_size = config['dry_run_wallet'] * config['tradable_balance_ratio']
|
wallet_size = config["dry_run_wallet"] * config["tradable_balance_ratio"]
|
||||||
# tradable_balance_ratio
|
# tradable_balance_ratio
|
||||||
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
|
if (
|
||||||
and config['stake_amount'] > wallet_size):
|
config["stake_amount"] != constants.UNLIMITED_STAKE_AMOUNT
|
||||||
wallet = fmt_coin(wallet_size, config['stake_currency'])
|
and config["stake_amount"] > wallet_size
|
||||||
stake = fmt_coin(config['stake_amount'], config['stake_currency'])
|
):
|
||||||
|
wallet = fmt_coin(wallet_size, config["stake_currency"])
|
||||||
|
stake = fmt_coin(config["stake_amount"], config["stake_currency"])
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
|
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
|
||||||
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
|
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
|
||||||
)
|
)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ def start_backtesting(args: Dict[str, Any]) -> None:
|
|||||||
# Initialize configuration
|
# Initialize configuration
|
||||||
config = setup_optimize_configuration(args, RunMode.BACKTEST)
|
config = setup_optimize_configuration(args, RunMode.BACKTEST)
|
||||||
|
|
||||||
logger.info('Starting freqtrade in Backtesting mode')
|
logger.info("Starting freqtrade in Backtesting mode")
|
||||||
|
|
||||||
# Initialize backtesting object
|
# Initialize backtesting object
|
||||||
backtesting = Backtesting(config)
|
backtesting = Backtesting(config)
|
||||||
@@ -68,7 +70,7 @@ def start_backtesting_show(args: Dict[str, Any]) -> None:
|
|||||||
from freqtrade.data.btanalysis import load_backtest_stats
|
from freqtrade.data.btanalysis import load_backtest_stats
|
||||||
from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist
|
from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist
|
||||||
|
|
||||||
results = load_backtest_stats(config['exportfilename'])
|
results = load_backtest_stats(config["exportfilename"])
|
||||||
|
|
||||||
show_backtest_results(config, results)
|
show_backtest_results(config, results)
|
||||||
show_sorted_pairlist(config, results)
|
show_sorted_pairlist(config, results)
|
||||||
@@ -87,20 +89,20 @@ def start_hyperopt(args: Dict[str, Any]) -> None:
|
|||||||
from freqtrade.optimize.hyperopt import Hyperopt
|
from freqtrade.optimize.hyperopt import Hyperopt
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"{e}. Please ensure that the hyperopt dependencies are installed.") from e
|
f"{e}. Please ensure that the hyperopt dependencies are installed."
|
||||||
|
) from e
|
||||||
# Initialize configuration
|
# Initialize configuration
|
||||||
config = setup_optimize_configuration(args, RunMode.HYPEROPT)
|
config = setup_optimize_configuration(args, RunMode.HYPEROPT)
|
||||||
|
|
||||||
logger.info('Starting freqtrade in Hyperopt mode')
|
logger.info("Starting freqtrade in Hyperopt mode")
|
||||||
|
|
||||||
lock = FileLock(Hyperopt.get_lock_filename(config))
|
lock = FileLock(Hyperopt.get_lock_filename(config))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with lock.acquire(timeout=1):
|
with lock.acquire(timeout=1):
|
||||||
|
|
||||||
# Remove noisy log messages
|
# Remove noisy log messages
|
||||||
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
|
logging.getLogger("hyperopt.tpe").setLevel(logging.WARNING)
|
||||||
logging.getLogger('filelock').setLevel(logging.WARNING)
|
logging.getLogger("filelock").setLevel(logging.WARNING)
|
||||||
|
|
||||||
# Initialize backtesting object
|
# Initialize backtesting object
|
||||||
hyperopt = Hyperopt(config)
|
hyperopt = Hyperopt(config)
|
||||||
@@ -108,9 +110,11 @@ def start_hyperopt(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
except Timeout:
|
except Timeout:
|
||||||
logger.info("Another running instance of freqtrade Hyperopt detected.")
|
logger.info("Another running instance of freqtrade Hyperopt detected.")
|
||||||
logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. "
|
logger.info(
|
||||||
"Hyperopt module is resource hungry. Please run your Hyperopt sequentially "
|
"Simultaneous execution of multiple Hyperopt commands is not supported. "
|
||||||
"or on separate machines.")
|
"Hyperopt module is resource hungry. Please run your Hyperopt sequentially "
|
||||||
|
"or on separate machines."
|
||||||
|
)
|
||||||
logger.info("Quitting now.")
|
logger.info("Quitting now.")
|
||||||
# TODO: return False here in order to help freqtrade to exit
|
# TODO: return False here in order to help freqtrade to exit
|
||||||
# with non-zero exit code...
|
# with non-zero exit code...
|
||||||
@@ -127,7 +131,7 @@ def start_edge(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
# Initialize configuration
|
# Initialize configuration
|
||||||
config = setup_optimize_configuration(args, RunMode.EDGE)
|
config = setup_optimize_configuration(args, RunMode.EDGE)
|
||||||
logger.info('Starting freqtrade in Edge mode')
|
logger.info("Starting freqtrade in Edge mode")
|
||||||
|
|
||||||
# Initialize Edge object
|
# Initialize Edge object
|
||||||
edge_cli = EdgeCli(config)
|
edge_cli = EdgeCli(config)
|
||||||
|
|||||||
@@ -17,28 +17,29 @@ def start_test_pairlist(args: Dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
from freqtrade.persistence import FtNoDBContext
|
from freqtrade.persistence import FtNoDBContext
|
||||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||||
|
|
||||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
|
|
||||||
quote_currencies = args.get('quote_currencies')
|
quote_currencies = args.get("quote_currencies")
|
||||||
if not quote_currencies:
|
if not quote_currencies:
|
||||||
quote_currencies = [config.get('stake_currency')]
|
quote_currencies = [config.get("stake_currency")]
|
||||||
results = {}
|
results = {}
|
||||||
with FtNoDBContext():
|
with FtNoDBContext():
|
||||||
for curr in quote_currencies:
|
for curr in quote_currencies:
|
||||||
config['stake_currency'] = curr
|
config["stake_currency"] = curr
|
||||||
pairlists = PairListManager(exchange, config)
|
pairlists = PairListManager(exchange, config)
|
||||||
pairlists.refresh_pairlist()
|
pairlists.refresh_pairlist()
|
||||||
results[curr] = pairlists.whitelist
|
results[curr] = pairlists.whitelist
|
||||||
|
|
||||||
for curr, pairlist in results.items():
|
for curr, pairlist in results.items():
|
||||||
if not args.get('print_one_column', False) and not args.get('list_pairs_print_json', False):
|
if not args.get("print_one_column", False) and not args.get("list_pairs_print_json", False):
|
||||||
print(f"Pairs for {curr}: ")
|
print(f"Pairs for {curr}: ")
|
||||||
|
|
||||||
if args.get('print_one_column', False):
|
if args.get("print_one_column", False):
|
||||||
print('\n'.join(pairlist))
|
print("\n".join(pairlist))
|
||||||
elif args.get('list_pairs_print_json', False):
|
elif args.get("list_pairs_print_json", False):
|
||||||
print(rapidjson.dumps(list(pairlist), default=str))
|
print(rapidjson.dumps(list(pairlist), default=str))
|
||||||
else:
|
else:
|
||||||
print(pairlist)
|
print(pairlist)
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ from freqtrade.exceptions import ConfigurationError
|
|||||||
|
|
||||||
|
|
||||||
def validate_plot_args(args: Dict[str, Any]) -> None:
|
def validate_plot_args(args: Dict[str, Any]) -> None:
|
||||||
if not args.get('datadir') and not args.get('config'):
|
if not args.get("datadir") and not args.get("config"):
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"You need to specify either `--datadir` or `--config` "
|
"You need to specify either `--datadir` or `--config` "
|
||||||
"for plot-profit and plot-dataframe.")
|
"for plot-profit and plot-dataframe."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def start_plot_dataframe(args: Dict[str, Any]) -> None:
|
def start_plot_dataframe(args: Dict[str, Any]) -> None:
|
||||||
@@ -18,6 +19,7 @@ def start_plot_dataframe(args: Dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
# Import here to avoid errors if plot-dependencies are not installed.
|
# Import here to avoid errors if plot-dependencies are not installed.
|
||||||
from freqtrade.plot.plotting import load_and_plot_trades
|
from freqtrade.plot.plotting import load_and_plot_trades
|
||||||
|
|
||||||
validate_plot_args(args)
|
validate_plot_args(args)
|
||||||
config = setup_utils_configuration(args, RunMode.PLOT)
|
config = setup_utils_configuration(args, RunMode.PLOT)
|
||||||
|
|
||||||
@@ -30,6 +32,7 @@ def start_plot_profit(args: Dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
# Import here to avoid errors if plot-dependencies are not installed.
|
# Import here to avoid errors if plot-dependencies are not installed.
|
||||||
from freqtrade.plot.plotting import plot_profit
|
from freqtrade.plot.plotting import plot_profit
|
||||||
|
|
||||||
validate_plot_args(args)
|
validate_plot_args(args)
|
||||||
config = setup_utils_configuration(args, RunMode.PLOT)
|
config = setup_utils_configuration(args, RunMode.PLOT)
|
||||||
|
|
||||||
|
|||||||
@@ -26,13 +26,15 @@ def start_strategy_update(args: Dict[str, Any]) -> None:
|
|||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
strategy_objs = StrategyResolver.search_all_objects(
|
strategy_objs = StrategyResolver.search_all_objects(
|
||||||
config, enum_failed=False, recursive=config.get('recursive_strategy_search', False))
|
config, enum_failed=False, recursive=config.get("recursive_strategy_search", False)
|
||||||
|
)
|
||||||
|
|
||||||
filtered_strategy_objs = []
|
filtered_strategy_objs = []
|
||||||
if args['strategy_list']:
|
if args["strategy_list"]:
|
||||||
filtered_strategy_objs = [
|
filtered_strategy_objs = [
|
||||||
strategy_obj for strategy_obj in strategy_objs
|
strategy_obj
|
||||||
if strategy_obj['name'] in args['strategy_list']
|
for strategy_obj in strategy_objs
|
||||||
|
if strategy_obj["name"] in args["strategy_list"]
|
||||||
]
|
]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -41,8 +43,8 @@ def start_strategy_update(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
processed_locations = set()
|
processed_locations = set()
|
||||||
for strategy_obj in filtered_strategy_objs:
|
for strategy_obj in filtered_strategy_objs:
|
||||||
if strategy_obj['location'] not in processed_locations:
|
if strategy_obj["location"] not in processed_locations:
|
||||||
processed_locations.add(strategy_obj['location'])
|
processed_locations.add(strategy_obj["location"])
|
||||||
start_conversion(strategy_obj, config)
|
start_conversion(strategy_obj, config)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
|
|||||||
]
|
]
|
||||||
config = deepcopy(config)
|
config = deepcopy(config)
|
||||||
for key in keys_to_remove:
|
for key in keys_to_remove:
|
||||||
if '.' in key:
|
if "." in key:
|
||||||
nested_keys = key.split('.')
|
nested_keys = key.split(".")
|
||||||
nested_config = config
|
nested_config = config
|
||||||
for nested_key in nested_keys[:-1]:
|
for nested_key in nested_keys[:-1]:
|
||||||
nested_config = nested_config.get(nested_key, {})
|
nested_config = nested_config.get(nested_key, {})
|
||||||
nested_config[nested_keys[-1]] = 'REDACTED'
|
nested_config[nested_keys[-1]] = "REDACTED"
|
||||||
else:
|
else:
|
||||||
config[key] = 'REDACTED'
|
config[key] = "REDACTED"
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def setup_utils_configuration(
|
def setup_utils_configuration(
|
||||||
args: Dict[str, Any], method: RunMode, *, set_dry: bool = True) -> Dict[str, Any]:
|
args: Dict[str, Any], method: RunMode, *, set_dry: bool = True
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Prepare the configuration for utils subcommands
|
Prepare the configuration for utils subcommands
|
||||||
:param args: Cli args from Arguments()
|
:param args: Cli args from Arguments()
|
||||||
@@ -23,7 +24,7 @@ def setup_utils_configuration(
|
|||||||
|
|
||||||
# Ensure these modes are using Dry-run
|
# Ensure these modes are using Dry-run
|
||||||
if set_dry:
|
if set_dry:
|
||||||
config['dry_run'] = True
|
config["dry_run"] = True
|
||||||
validate_config_consistency(config, preliminary=True)
|
validate_config_consistency(config, preliminary=True)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -20,18 +20,16 @@ def _extend_validator(validator_class):
|
|||||||
Extended validator for the Freqtrade configuration JSON Schema.
|
Extended validator for the Freqtrade configuration JSON Schema.
|
||||||
Currently it only handles defaults for subschemas.
|
Currently it only handles defaults for subschemas.
|
||||||
"""
|
"""
|
||||||
validate_properties = validator_class.VALIDATORS['properties']
|
validate_properties = validator_class.VALIDATORS["properties"]
|
||||||
|
|
||||||
def set_defaults(validator, properties, instance, schema):
|
def set_defaults(validator, properties, instance, schema):
|
||||||
for prop, subschema in properties.items():
|
for prop, subschema in properties.items():
|
||||||
if 'default' in subschema:
|
if "default" in subschema:
|
||||||
instance.setdefault(prop, subschema['default'])
|
instance.setdefault(prop, subschema["default"])
|
||||||
|
|
||||||
yield from validate_properties(validator, properties, instance, schema)
|
yield from validate_properties(validator, properties, instance, schema)
|
||||||
|
|
||||||
return validators.extend(
|
return validators.extend(validator_class, {"properties": set_defaults})
|
||||||
validator_class, {'properties': set_defaults}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
FreqtradeValidator = _extend_validator(Draft4Validator)
|
FreqtradeValidator = _extend_validator(Draft4Validator)
|
||||||
@@ -44,27 +42,23 @@ def validate_config_schema(conf: Dict[str, Any], preliminary: bool = False) -> D
|
|||||||
:return: Returns the config if valid, otherwise throw an exception
|
:return: Returns the config if valid, otherwise throw an exception
|
||||||
"""
|
"""
|
||||||
conf_schema = deepcopy(constants.CONF_SCHEMA)
|
conf_schema = deepcopy(constants.CONF_SCHEMA)
|
||||||
if conf.get('runmode', RunMode.OTHER) in (RunMode.DRY_RUN, RunMode.LIVE):
|
if conf.get("runmode", RunMode.OTHER) in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||||
conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED
|
conf_schema["required"] = constants.SCHEMA_TRADE_REQUIRED
|
||||||
elif conf.get('runmode', RunMode.OTHER) in (RunMode.BACKTEST, RunMode.HYPEROPT):
|
elif conf.get("runmode", RunMode.OTHER) in (RunMode.BACKTEST, RunMode.HYPEROPT):
|
||||||
if preliminary:
|
if preliminary:
|
||||||
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED
|
conf_schema["required"] = constants.SCHEMA_BACKTEST_REQUIRED
|
||||||
else:
|
else:
|
||||||
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED_FINAL
|
conf_schema["required"] = constants.SCHEMA_BACKTEST_REQUIRED_FINAL
|
||||||
elif conf.get('runmode', RunMode.OTHER) == RunMode.WEBSERVER:
|
elif conf.get("runmode", RunMode.OTHER) == RunMode.WEBSERVER:
|
||||||
conf_schema['required'] = constants.SCHEMA_MINIMAL_WEBSERVER
|
conf_schema["required"] = constants.SCHEMA_MINIMAL_WEBSERVER
|
||||||
else:
|
else:
|
||||||
conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED
|
conf_schema["required"] = constants.SCHEMA_MINIMAL_REQUIRED
|
||||||
try:
|
try:
|
||||||
FreqtradeValidator(conf_schema).validate(conf)
|
FreqtradeValidator(conf_schema).validate(conf)
|
||||||
return conf
|
return conf
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.critical(
|
logger.critical(f"Invalid configuration. Reason: {e}")
|
||||||
f"Invalid configuration. Reason: {e}"
|
raise ValidationError(best_match(Draft4Validator(conf_schema).iter_errors(conf)).message)
|
||||||
)
|
|
||||||
raise ValidationError(
|
|
||||||
best_match(Draft4Validator(conf_schema).iter_errors(conf)).message
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = False) -> None:
|
def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = False) -> None:
|
||||||
@@ -92,7 +86,7 @@ def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = Fal
|
|||||||
_validate_orderflow(conf)
|
_validate_orderflow(conf)
|
||||||
|
|
||||||
# validate configuration before returning
|
# validate configuration before returning
|
||||||
logger.info('Validating configuration ...')
|
logger.info("Validating configuration ...")
|
||||||
validate_config_schema(conf, preliminary=preliminary)
|
validate_config_schema(conf, preliminary=preliminary)
|
||||||
|
|
||||||
|
|
||||||
@@ -101,9 +95,11 @@ def _validate_unlimited_amount(conf: Dict[str, Any]) -> None:
|
|||||||
If edge is disabled, either max_open_trades or stake_amount need to be set.
|
If edge is disabled, either max_open_trades or stake_amount need to be set.
|
||||||
:raise: ConfigurationError if config validation failed
|
:raise: ConfigurationError if config validation failed
|
||||||
"""
|
"""
|
||||||
if (not conf.get('edge', {}).get('enabled')
|
if (
|
||||||
and conf.get('max_open_trades') == float('inf')
|
not conf.get("edge", {}).get("enabled")
|
||||||
and conf.get('stake_amount') == constants.UNLIMITED_STAKE_AMOUNT):
|
and conf.get("max_open_trades") == float("inf")
|
||||||
|
and conf.get("stake_amount") == constants.UNLIMITED_STAKE_AMOUNT
|
||||||
|
):
|
||||||
raise ConfigurationError("`max_open_trades` and `stake_amount` cannot both be unlimited.")
|
raise ConfigurationError("`max_open_trades` and `stake_amount` cannot both be unlimited.")
|
||||||
|
|
||||||
|
|
||||||
@@ -112,45 +108,47 @@ def _validate_price_config(conf: Dict[str, Any]) -> None:
|
|||||||
When using market orders, price sides must be using the "other" side of the price
|
When using market orders, price sides must be using the "other" side of the price
|
||||||
"""
|
"""
|
||||||
# TODO: The below could be an enforced setting when using market orders
|
# TODO: The below could be an enforced setting when using market orders
|
||||||
if (conf.get('order_types', {}).get('entry') == 'market'
|
if conf.get("order_types", {}).get("entry") == "market" and conf.get("entry_pricing", {}).get(
|
||||||
and conf.get('entry_pricing', {}).get('price_side') not in ('ask', 'other')):
|
"price_side"
|
||||||
raise ConfigurationError(
|
) not in ("ask", "other"):
|
||||||
'Market entry orders require entry_pricing.price_side = "other".')
|
raise ConfigurationError('Market entry orders require entry_pricing.price_side = "other".')
|
||||||
|
|
||||||
if (conf.get('order_types', {}).get('exit') == 'market'
|
if conf.get("order_types", {}).get("exit") == "market" and conf.get("exit_pricing", {}).get(
|
||||||
and conf.get('exit_pricing', {}).get('price_side') not in ('bid', 'other')):
|
"price_side"
|
||||||
|
) not in ("bid", "other"):
|
||||||
raise ConfigurationError('Market exit orders require exit_pricing.price_side = "other".')
|
raise ConfigurationError('Market exit orders require exit_pricing.price_side = "other".')
|
||||||
|
|
||||||
|
|
||||||
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
|
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
|
||||||
|
if conf.get("stoploss") == 0.0:
|
||||||
if conf.get('stoploss') == 0.0:
|
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
'The config stoploss needs to be different from 0 to avoid problems with sell orders.'
|
"The config stoploss needs to be different from 0 to avoid problems with sell orders."
|
||||||
)
|
)
|
||||||
# Skip if trailing stoploss is not activated
|
# Skip if trailing stoploss is not activated
|
||||||
if not conf.get('trailing_stop', False):
|
if not conf.get("trailing_stop", False):
|
||||||
return
|
return
|
||||||
|
|
||||||
tsl_positive = float(conf.get('trailing_stop_positive', 0))
|
tsl_positive = float(conf.get("trailing_stop_positive", 0))
|
||||||
tsl_offset = float(conf.get('trailing_stop_positive_offset', 0))
|
tsl_offset = float(conf.get("trailing_stop_positive_offset", 0))
|
||||||
tsl_only_offset = conf.get('trailing_only_offset_is_reached', False)
|
tsl_only_offset = conf.get("trailing_only_offset_is_reached", False)
|
||||||
|
|
||||||
if tsl_only_offset:
|
if tsl_only_offset:
|
||||||
if tsl_positive == 0.0:
|
if tsl_positive == 0.0:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
'The config trailing_only_offset_is_reached needs '
|
"The config trailing_only_offset_is_reached needs "
|
||||||
'trailing_stop_positive_offset to be more than 0 in your config.')
|
"trailing_stop_positive_offset to be more than 0 in your config."
|
||||||
|
)
|
||||||
if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive:
|
if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
'The config trailing_stop_positive_offset needs '
|
"The config trailing_stop_positive_offset needs "
|
||||||
'to be greater than trailing_stop_positive in your config.')
|
"to be greater than trailing_stop_positive in your config."
|
||||||
|
)
|
||||||
|
|
||||||
# Fetch again without default
|
# Fetch again without default
|
||||||
if 'trailing_stop_positive' in conf and float(conf['trailing_stop_positive']) == 0.0:
|
if "trailing_stop_positive" in conf and float(conf["trailing_stop_positive"]) == 0.0:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
'The config trailing_stop_positive needs to be different from 0 '
|
"The config trailing_stop_positive needs to be different from 0 "
|
||||||
'to avoid problems with sell orders.'
|
"to avoid problems with sell orders."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -159,10 +157,10 @@ def _validate_edge(conf: Dict[str, Any]) -> None:
|
|||||||
Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists.
|
Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not conf.get('edge', {}).get('enabled'):
|
if not conf.get("edge", {}).get("enabled"):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not conf.get('use_exit_signal', True):
|
if not conf.get("use_exit_signal", True):
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"Edge requires `use_exit_signal` to be True, otherwise no sells will happen."
|
"Edge requires `use_exit_signal` to be True, otherwise no sells will happen."
|
||||||
)
|
)
|
||||||
@@ -172,13 +170,20 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does.
|
Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does.
|
||||||
"""
|
"""
|
||||||
if conf.get('runmode', RunMode.OTHER) in [RunMode.OTHER, RunMode.PLOT,
|
if conf.get("runmode", RunMode.OTHER) in [
|
||||||
RunMode.UTIL_NO_EXCHANGE, RunMode.UTIL_EXCHANGE]:
|
RunMode.OTHER,
|
||||||
|
RunMode.PLOT,
|
||||||
|
RunMode.UTIL_NO_EXCHANGE,
|
||||||
|
RunMode.UTIL_EXCHANGE,
|
||||||
|
]:
|
||||||
return
|
return
|
||||||
|
|
||||||
for pl in conf.get('pairlists', [{'method': 'StaticPairList'}]):
|
for pl in conf.get("pairlists", [{"method": "StaticPairList"}]):
|
||||||
if (isinstance(pl, dict) and pl.get('method') == 'StaticPairList'
|
if (
|
||||||
and not conf.get('exchange', {}).get('pair_whitelist')):
|
isinstance(pl, dict)
|
||||||
|
and pl.get("method") == "StaticPairList"
|
||||||
|
and not conf.get("exchange", {}).get("pair_whitelist")
|
||||||
|
):
|
||||||
raise ConfigurationError("StaticPairList requires pair_whitelist to be set.")
|
raise ConfigurationError("StaticPairList requires pair_whitelist to be set.")
|
||||||
|
|
||||||
|
|
||||||
@@ -187,14 +192,14 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
|
|||||||
Validate protection configuration validity
|
Validate protection configuration validity
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for prot in conf.get('protections', []):
|
for prot in conf.get("protections", []):
|
||||||
if ('stop_duration' in prot and 'stop_duration_candles' in prot):
|
if "stop_duration" in prot and "stop_duration_candles" in prot:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
|
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
|
||||||
f"Please fix the protection {prot.get('method')}"
|
f"Please fix the protection {prot.get('method')}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if ('lookback_period' in prot and 'lookback_period_candles' in prot):
|
if "lookback_period" in prot and "lookback_period_candles" in prot:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"Protections must specify either `lookback_period` or `lookback_period_candles`.\n"
|
"Protections must specify either `lookback_period` or `lookback_period_candles`.\n"
|
||||||
f"Please fix the protection {prot.get('method')}"
|
f"Please fix the protection {prot.get('method')}"
|
||||||
@@ -202,10 +207,10 @@ 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('exit_pricing', {})
|
ask_strategy = conf.get("exit_pricing", {})
|
||||||
ob_min = ask_strategy.get('order_book_min')
|
ob_min = ask_strategy.get("order_book_min")
|
||||||
ob_max = ask_strategy.get('order_book_max')
|
ob_max = ask_strategy.get("order_book_max")
|
||||||
if ob_min is not None and ob_max is not None and ask_strategy.get('use_order_book'):
|
if ob_min is not None and ob_max is not None and ask_strategy.get("use_order_book"):
|
||||||
if ob_min != ob_max:
|
if ob_min != ob_max:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"Using order_book_max != order_book_min in exit_pricing is no longer supported."
|
"Using order_book_max != order_book_min in exit_pricing is no longer supported."
|
||||||
@@ -213,7 +218,7 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Move value to order_book_top
|
# Move value to order_book_top
|
||||||
ask_strategy['order_book_top'] = ob_min
|
ask_strategy["order_book_top"] = ob_min
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"DEPRECATED: "
|
"DEPRECATED: "
|
||||||
"Please use `order_book_top` instead of `order_book_min` and `order_book_max` "
|
"Please use `order_book_top` instead of `order_book_min` and `order_book_max` "
|
||||||
@@ -222,7 +227,6 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
|
def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
_validate_time_in_force(conf)
|
_validate_time_in_force(conf)
|
||||||
_validate_order_types(conf)
|
_validate_order_types(conf)
|
||||||
_validate_unfilledtimeout(conf)
|
_validate_unfilledtimeout(conf)
|
||||||
@@ -231,119 +235,129 @@ def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _validate_time_in_force(conf: Dict[str, Any]) -> None:
|
def _validate_time_in_force(conf: Dict[str, Any]) -> None:
|
||||||
|
time_in_force = conf.get("order_time_in_force", {})
|
||||||
time_in_force = conf.get('order_time_in_force', {})
|
if "buy" in time_in_force or "sell" in time_in_force:
|
||||||
if 'buy' in time_in_force or 'sell' in time_in_force:
|
if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
|
||||||
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"Please migrate your time_in_force settings to use 'entry' and 'exit'.")
|
"Please migrate your time_in_force settings to use 'entry' and 'exit'."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"DEPRECATED: Using 'buy' and 'sell' for time_in_force is deprecated."
|
"DEPRECATED: Using 'buy' and 'sell' for time_in_force is deprecated."
|
||||||
"Please migrate your time_in_force settings to use 'entry' and 'exit'."
|
"Please migrate your time_in_force settings to use 'entry' and 'exit'."
|
||||||
)
|
)
|
||||||
process_deprecated_setting(
|
process_deprecated_setting(
|
||||||
conf, 'order_time_in_force', 'buy', 'order_time_in_force', 'entry')
|
conf, "order_time_in_force", "buy", "order_time_in_force", "entry"
|
||||||
|
)
|
||||||
|
|
||||||
process_deprecated_setting(
|
process_deprecated_setting(
|
||||||
conf, 'order_time_in_force', 'sell', 'order_time_in_force', 'exit')
|
conf, "order_time_in_force", "sell", "order_time_in_force", "exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_order_types(conf: Dict[str, Any]) -> None:
|
def _validate_order_types(conf: Dict[str, Any]) -> None:
|
||||||
|
order_types = conf.get("order_types", {})
|
||||||
order_types = conf.get('order_types', {})
|
old_order_types = [
|
||||||
old_order_types = ['buy', 'sell', 'emergencysell', 'forcebuy',
|
"buy",
|
||||||
'forcesell', 'emergencyexit', 'forceexit', 'forceentry']
|
"sell",
|
||||||
|
"emergencysell",
|
||||||
|
"forcebuy",
|
||||||
|
"forcesell",
|
||||||
|
"emergencyexit",
|
||||||
|
"forceexit",
|
||||||
|
"forceentry",
|
||||||
|
]
|
||||||
if any(x in order_types for x in old_order_types):
|
if any(x in order_types for x in old_order_types):
|
||||||
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"Please migrate your order_types settings to use the new wording.")
|
"Please migrate your order_types settings to use the new wording."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"DEPRECATED: Using 'buy' and 'sell' for order_types is deprecated."
|
"DEPRECATED: Using 'buy' and 'sell' for order_types is deprecated."
|
||||||
"Please migrate your order_types settings to use 'entry' and 'exit' wording."
|
"Please migrate your order_types settings to use 'entry' and 'exit' wording."
|
||||||
)
|
)
|
||||||
for o, n in [
|
for o, n in [
|
||||||
('buy', 'entry'),
|
("buy", "entry"),
|
||||||
('sell', 'exit'),
|
("sell", "exit"),
|
||||||
('emergencysell', 'emergency_exit'),
|
("emergencysell", "emergency_exit"),
|
||||||
('forcesell', 'force_exit'),
|
("forcesell", "force_exit"),
|
||||||
('forcebuy', 'force_entry'),
|
("forcebuy", "force_entry"),
|
||||||
('emergencyexit', 'emergency_exit'),
|
("emergencyexit", "emergency_exit"),
|
||||||
('forceexit', 'force_exit'),
|
("forceexit", "force_exit"),
|
||||||
('forceentry', 'force_entry'),
|
("forceentry", "force_entry"),
|
||||||
]:
|
]:
|
||||||
|
process_deprecated_setting(conf, "order_types", o, "order_types", n)
|
||||||
process_deprecated_setting(conf, 'order_types', o, 'order_types', n)
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None:
|
def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None:
|
||||||
unfilledtimeout = conf.get('unfilledtimeout', {})
|
unfilledtimeout = conf.get("unfilledtimeout", {})
|
||||||
if any(x in unfilledtimeout for x in ['buy', 'sell']):
|
if any(x in unfilledtimeout for x in ["buy", "sell"]):
|
||||||
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"Please migrate your unfilledtimeout settings to use the new wording.")
|
"Please migrate your unfilledtimeout settings to use the new wording."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is deprecated."
|
"DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is deprecated."
|
||||||
"Please migrate your unfilledtimeout settings to use 'entry' and 'exit' wording."
|
"Please migrate your unfilledtimeout settings to use 'entry' and 'exit' wording."
|
||||||
)
|
)
|
||||||
for o, n in [
|
for o, n in [
|
||||||
('buy', 'entry'),
|
("buy", "entry"),
|
||||||
('sell', 'exit'),
|
("sell", "exit"),
|
||||||
]:
|
]:
|
||||||
|
process_deprecated_setting(conf, "unfilledtimeout", o, "unfilledtimeout", n)
|
||||||
process_deprecated_setting(conf, 'unfilledtimeout', o, 'unfilledtimeout', n)
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_pricing_rules(conf: Dict[str, Any]) -> None:
|
def _validate_pricing_rules(conf: Dict[str, Any]) -> None:
|
||||||
|
if conf.get("ask_strategy") or conf.get("bid_strategy"):
|
||||||
if conf.get('ask_strategy') or conf.get('bid_strategy'):
|
if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
|
||||||
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
raise ConfigurationError("Please migrate your pricing settings to use the new wording.")
|
||||||
raise ConfigurationError(
|
|
||||||
"Please migrate your pricing settings to use the new wording.")
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"DEPRECATED: Using 'ask_strategy' and 'bid_strategy' is deprecated."
|
"DEPRECATED: Using 'ask_strategy' and 'bid_strategy' is deprecated."
|
||||||
"Please migrate your settings to use 'entry_pricing' and 'exit_pricing'."
|
"Please migrate your settings to use 'entry_pricing' and 'exit_pricing'."
|
||||||
)
|
)
|
||||||
conf['entry_pricing'] = {}
|
conf["entry_pricing"] = {}
|
||||||
for obj in list(conf.get('bid_strategy', {}).keys()):
|
for obj in list(conf.get("bid_strategy", {}).keys()):
|
||||||
if obj == 'ask_last_balance':
|
if obj == "ask_last_balance":
|
||||||
process_deprecated_setting(conf, 'bid_strategy', obj,
|
process_deprecated_setting(
|
||||||
'entry_pricing', 'price_last_balance')
|
conf, "bid_strategy", obj, "entry_pricing", "price_last_balance"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
process_deprecated_setting(conf, 'bid_strategy', obj, 'entry_pricing', obj)
|
process_deprecated_setting(conf, "bid_strategy", obj, "entry_pricing", obj)
|
||||||
del conf['bid_strategy']
|
del conf["bid_strategy"]
|
||||||
|
|
||||||
conf['exit_pricing'] = {}
|
conf["exit_pricing"] = {}
|
||||||
for obj in list(conf.get('ask_strategy', {}).keys()):
|
for obj in list(conf.get("ask_strategy", {}).keys()):
|
||||||
if obj == 'bid_last_balance':
|
if obj == "bid_last_balance":
|
||||||
process_deprecated_setting(conf, 'ask_strategy', obj,
|
process_deprecated_setting(
|
||||||
'exit_pricing', 'price_last_balance')
|
conf, "ask_strategy", obj, "exit_pricing", "price_last_balance"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
process_deprecated_setting(conf, 'ask_strategy', obj, 'exit_pricing', obj)
|
process_deprecated_setting(conf, "ask_strategy", obj, "exit_pricing", obj)
|
||||||
del conf['ask_strategy']
|
del conf["ask_strategy"]
|
||||||
|
|
||||||
|
|
||||||
def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None:
|
def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None:
|
||||||
freqai_enabled = conf.get('freqai', {}).get('enabled', False)
|
freqai_enabled = conf.get("freqai", {}).get("enabled", False)
|
||||||
analyze_per_epoch = conf.get('analyze_per_epoch', False)
|
analyze_per_epoch = conf.get("analyze_per_epoch", False)
|
||||||
if analyze_per_epoch and freqai_enabled:
|
if analyze_per_epoch and freqai_enabled:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.')
|
"Using analyze-per-epoch parameter is not supported with a FreqAI strategy."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool) -> None:
|
def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool) -> None:
|
||||||
freqai_enabled = conf.get('freqai', {}).get('enabled', False)
|
freqai_enabled = conf.get("freqai", {}).get("enabled", False)
|
||||||
if freqai_enabled:
|
if freqai_enabled:
|
||||||
main_tf = conf.get('timeframe', '5m')
|
main_tf = conf.get("timeframe", "5m")
|
||||||
freqai_include_timeframes = conf.get('freqai', {}).get('feature_parameters', {}
|
freqai_include_timeframes = (
|
||||||
).get('include_timeframes', [])
|
conf.get("freqai", {}).get("feature_parameters", {}).get("include_timeframes", [])
|
||||||
|
)
|
||||||
|
|
||||||
from freqtrade.exchange import timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_seconds
|
||||||
|
|
||||||
main_tf_s = timeframe_to_seconds(main_tf)
|
main_tf_s = timeframe_to_seconds(main_tf)
|
||||||
offending_lines = []
|
offending_lines = []
|
||||||
for tf in freqai_include_timeframes:
|
for tf in freqai_include_timeframes:
|
||||||
@@ -353,51 +367,59 @@ def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool)
|
|||||||
if offending_lines:
|
if offending_lines:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
f"Main timeframe of {main_tf} must be smaller or equal to FreqAI "
|
f"Main timeframe of {main_tf} must be smaller or equal to FreqAI "
|
||||||
f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}")
|
f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}"
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure that the base timeframe is included in the include_timeframes list
|
# Ensure that the base timeframe is included in the include_timeframes list
|
||||||
if not preliminary and main_tf not in freqai_include_timeframes:
|
if not preliminary and main_tf not in freqai_include_timeframes:
|
||||||
feature_parameters = conf.get('freqai', {}).get('feature_parameters', {})
|
feature_parameters = conf.get("freqai", {}).get("feature_parameters", {})
|
||||||
include_timeframes = [main_tf] + freqai_include_timeframes
|
include_timeframes = [main_tf] + freqai_include_timeframes
|
||||||
conf.get('freqai', {}).get('feature_parameters', {}) \
|
conf.get("freqai", {}).get("feature_parameters", {}).update(
|
||||||
.update({**feature_parameters, 'include_timeframes': include_timeframes})
|
{**feature_parameters, "include_timeframes": include_timeframes}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_freqai_backtest(conf: Dict[str, Any]) -> None:
|
def _validate_freqai_backtest(conf: Dict[str, Any]) -> None:
|
||||||
if conf.get('runmode', RunMode.OTHER) == RunMode.BACKTEST:
|
if conf.get("runmode", RunMode.OTHER) == RunMode.BACKTEST:
|
||||||
freqai_enabled = conf.get('freqai', {}).get('enabled', False)
|
freqai_enabled = conf.get("freqai", {}).get("enabled", False)
|
||||||
timerange = conf.get('timerange')
|
timerange = conf.get("timerange")
|
||||||
freqai_backtest_live_models = conf.get('freqai_backtest_live_models', False)
|
freqai_backtest_live_models = conf.get("freqai_backtest_live_models", False)
|
||||||
if freqai_backtest_live_models and freqai_enabled and timerange:
|
if freqai_backtest_live_models and freqai_enabled and timerange:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
'Using timerange parameter is not supported with '
|
"Using timerange parameter is not supported with "
|
||||||
'--freqai-backtest-live-models parameter.')
|
"--freqai-backtest-live-models parameter."
|
||||||
|
)
|
||||||
|
|
||||||
if freqai_backtest_live_models and not freqai_enabled:
|
if freqai_backtest_live_models and not freqai_enabled:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
'Using --freqai-backtest-live-models parameter is only '
|
"Using --freqai-backtest-live-models parameter is only "
|
||||||
'supported with a FreqAI strategy.')
|
"supported with a FreqAI strategy."
|
||||||
|
)
|
||||||
|
|
||||||
if freqai_enabled and not freqai_backtest_live_models and not timerange:
|
if freqai_enabled and not freqai_backtest_live_models and not timerange:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
'Please pass --timerange if you intend to use FreqAI for backtesting.')
|
"Please pass --timerange if you intend to use FreqAI for backtesting."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_consumers(conf: Dict[str, Any]) -> None:
|
def _validate_consumers(conf: Dict[str, Any]) -> None:
|
||||||
emc_conf = conf.get('external_message_consumer', {})
|
emc_conf = conf.get("external_message_consumer", {})
|
||||||
if emc_conf.get('enabled', False):
|
if emc_conf.get("enabled", False):
|
||||||
if len(emc_conf.get('producers', [])) < 1:
|
if len(emc_conf.get("producers", [])) < 1:
|
||||||
raise ConfigurationError("You must specify at least 1 Producer to connect to.")
|
raise ConfigurationError("You must specify at least 1 Producer to connect to.")
|
||||||
|
|
||||||
producer_names = [p['name'] for p in emc_conf.get('producers', [])]
|
producer_names = [p["name"] for p in emc_conf.get("producers", [])]
|
||||||
duplicates = [item for item, count in Counter(producer_names).items() if count > 1]
|
duplicates = [item for item, count in Counter(producer_names).items() if count > 1]
|
||||||
if duplicates:
|
if duplicates:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
f"Producer names must be unique. Duplicate: {', '.join(duplicates)}")
|
f"Producer names must be unique. Duplicate: {', '.join(duplicates)}"
|
||||||
if conf.get('process_only_new_candles', True):
|
)
|
||||||
|
if conf.get("process_only_new_candles", True):
|
||||||
# Warning here or require it?
|
# Warning here or require it?
|
||||||
logger.warning("To receive best performance with external data, "
|
logger.warning(
|
||||||
"please set `process_only_new_candles` to False")
|
"To receive best performance with external data, "
|
||||||
|
"please set `process_only_new_candles` to False"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_orderflow(conf: Dict[str, Any]) -> None:
|
def _validate_orderflow(conf: Dict[str, Any]) -> None:
|
||||||
@@ -409,9 +431,9 @@ def _validate_orderflow(conf: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _strategy_settings(conf: Dict[str, Any]) -> None:
|
def _strategy_settings(conf: Dict[str, Any]) -> None:
|
||||||
|
process_deprecated_setting(conf, None, "use_sell_signal", None, "use_exit_signal")
|
||||||
process_deprecated_setting(conf, None, 'use_sell_signal', None, 'use_exit_signal')
|
process_deprecated_setting(conf, None, "sell_profit_only", None, "exit_profit_only")
|
||||||
process_deprecated_setting(conf, None, 'sell_profit_only', None, 'exit_profit_only')
|
process_deprecated_setting(conf, None, "sell_profit_offset", None, "exit_profit_offset")
|
||||||
process_deprecated_setting(conf, None, 'sell_profit_offset', None, 'exit_profit_offset')
|
process_deprecated_setting(
|
||||||
process_deprecated_setting(conf, None, 'ignore_roi_if_buy_signal',
|
conf, None, "ignore_roi_if_buy_signal", None, "ignore_roi_if_entry_signal"
|
||||||
None, 'ignore_roi_if_entry_signal')
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
This module contains the configuration class
|
This module contains the configuration class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import warnings
|
import warnings
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
@@ -13,7 +14,7 @@ from freqtrade.configuration.directory_operations import create_datadir, create_
|
|||||||
from freqtrade.configuration.environment_vars import enironment_vars_to_dict
|
from freqtrade.configuration.environment_vars import enironment_vars_to_dict
|
||||||
from freqtrade.configuration.load_config import load_file, load_from_files
|
from freqtrade.configuration.load_config import load_file, load_from_files
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, CandleType, RunMode, TradingMode
|
from freqtrade.enums import NON_UTIL_MODES, TRADE_MODES, CandleType, RunMode, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.loggers import setup_logging
|
from freqtrade.loggers import setup_logging
|
||||||
from freqtrade.misc import deep_merge_dicts, parse_db_uri_for_logging
|
from freqtrade.misc import deep_merge_dicts, parse_db_uri_for_logging
|
||||||
@@ -56,7 +57,7 @@ class Configuration:
|
|||||||
:return: configuration dictionary
|
:return: configuration dictionary
|
||||||
"""
|
"""
|
||||||
# Keep this method as staticmethod, so it can be used from interactive environments
|
# Keep this method as staticmethod, so it can be used from interactive environments
|
||||||
c = Configuration({'config': files}, RunMode.OTHER)
|
c = Configuration({"config": files}, RunMode.OTHER)
|
||||||
return c.get_config()
|
return c.get_config()
|
||||||
|
|
||||||
def load_config(self) -> Dict[str, Any]:
|
def load_config(self) -> Dict[str, Any]:
|
||||||
@@ -69,19 +70,20 @@ class Configuration:
|
|||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
from freqtrade.commands.arguments import NO_CONF_ALLOWED
|
from freqtrade.commands.arguments import NO_CONF_ALLOWED
|
||||||
if self.args.get('command') not in NO_CONF_ALLOWED:
|
|
||||||
|
if self.args.get("command") not in NO_CONF_ALLOWED:
|
||||||
env_data = enironment_vars_to_dict()
|
env_data = enironment_vars_to_dict()
|
||||||
config = deep_merge_dicts(env_data, config)
|
config = deep_merge_dicts(env_data, config)
|
||||||
|
|
||||||
# Normalize config
|
# Normalize config
|
||||||
if 'internals' not in config:
|
if "internals" not in config:
|
||||||
config['internals'] = {}
|
config["internals"] = {}
|
||||||
|
|
||||||
if 'pairlists' not in config:
|
if "pairlists" not in config:
|
||||||
config['pairlists'] = []
|
config["pairlists"] = []
|
||||||
|
|
||||||
# Keep a copy of the original configuration file
|
# Keep a copy of the original configuration file
|
||||||
config['original_config'] = deepcopy(config)
|
config["original_config"] = deepcopy(config)
|
||||||
|
|
||||||
self._process_logging_options(config)
|
self._process_logging_options(config)
|
||||||
|
|
||||||
@@ -105,7 +107,7 @@ class Configuration:
|
|||||||
from freqtrade.exchange.check_exchange import check_exchange
|
from freqtrade.exchange.check_exchange import check_exchange
|
||||||
|
|
||||||
# Check if the exchange set by the user is supported
|
# Check if the exchange set by the user is supported
|
||||||
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
|
check_exchange(config, config.get("experimental", {}).get("block_bad_exchanges", True))
|
||||||
|
|
||||||
self._resolve_pairs_list(config)
|
self._resolve_pairs_list(config)
|
||||||
|
|
||||||
@@ -119,52 +121,56 @@ class Configuration:
|
|||||||
the -v/--verbose, --logfile options
|
the -v/--verbose, --logfile options
|
||||||
"""
|
"""
|
||||||
# Log level
|
# Log level
|
||||||
config.update({'verbosity': self.args.get('verbosity', 0)})
|
config.update({"verbosity": self.args.get("verbosity", 0)})
|
||||||
|
|
||||||
if 'logfile' in self.args and self.args['logfile']:
|
if "logfile" in self.args and self.args["logfile"]:
|
||||||
config.update({'logfile': self.args['logfile']})
|
config.update({"logfile": self.args["logfile"]})
|
||||||
|
|
||||||
setup_logging(config)
|
setup_logging(config)
|
||||||
|
|
||||||
def _process_trading_options(self, config: Config) -> None:
|
def _process_trading_options(self, config: Config) -> None:
|
||||||
if config['runmode'] not in TRADING_MODES:
|
if config["runmode"] not in TRADE_MODES:
|
||||||
return
|
return
|
||||||
|
|
||||||
if config.get('dry_run', False):
|
if config.get("dry_run", False):
|
||||||
logger.info('Dry run is enabled')
|
logger.info("Dry run is enabled")
|
||||||
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:
|
if config.get("db_url") in [None, constants.DEFAULT_DB_PROD_URL]:
|
||||||
# Default to in-memory db for dry_run if not specified
|
# Default to in-memory db for dry_run if not specified
|
||||||
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
|
config["db_url"] = constants.DEFAULT_DB_DRYRUN_URL
|
||||||
else:
|
else:
|
||||||
if not config.get('db_url'):
|
if not config.get("db_url"):
|
||||||
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
config["db_url"] = constants.DEFAULT_DB_PROD_URL
|
||||||
logger.info('Dry run is disabled')
|
logger.info("Dry run is disabled")
|
||||||
|
|
||||||
logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
|
logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
|
||||||
|
|
||||||
def _process_common_options(self, config: Config) -> None:
|
def _process_common_options(self, config: Config) -> None:
|
||||||
|
|
||||||
# Set strategy if not specified in config and or if it's non default
|
# Set strategy if not specified in config and or if it's non default
|
||||||
if self.args.get('strategy') or not config.get('strategy'):
|
if self.args.get("strategy") or not config.get("strategy"):
|
||||||
config.update({'strategy': self.args.get('strategy')})
|
config.update({"strategy": self.args.get("strategy")})
|
||||||
|
|
||||||
self._args_to_config(config, argname='strategy_path',
|
self._args_to_config(
|
||||||
logstring='Using additional Strategy lookup path: {}')
|
config, argname="strategy_path", logstring="Using additional Strategy lookup path: {}"
|
||||||
|
)
|
||||||
|
|
||||||
if ('db_url' in self.args and self.args['db_url'] and
|
if (
|
||||||
self.args['db_url'] != constants.DEFAULT_DB_PROD_URL):
|
"db_url" in self.args
|
||||||
config.update({'db_url': self.args['db_url']})
|
and self.args["db_url"]
|
||||||
logger.info('Parameter --db-url detected ...')
|
and self.args["db_url"] != constants.DEFAULT_DB_PROD_URL
|
||||||
|
):
|
||||||
|
config.update({"db_url": self.args["db_url"]})
|
||||||
|
logger.info("Parameter --db-url detected ...")
|
||||||
|
|
||||||
self._args_to_config(config, argname='db_url_from',
|
self._args_to_config(
|
||||||
logstring='Parameter --db-url-from detected ...')
|
config, argname="db_url_from", logstring="Parameter --db-url-from detected ..."
|
||||||
|
)
|
||||||
|
|
||||||
if config.get('force_entry_enable', False):
|
if config.get("force_entry_enable", False):
|
||||||
logger.warning('`force_entry_enable` RPC message enabled.')
|
logger.warning("`force_entry_enable` RPC message enabled.")
|
||||||
|
|
||||||
# Support for sd_notify
|
# Support for sd_notify
|
||||||
if 'sd_notify' in self.args and self.args['sd_notify']:
|
if "sd_notify" in self.args and self.args["sd_notify"]:
|
||||||
config['internals'].update({'sd_notify': True})
|
config["internals"].update({"sd_notify": True})
|
||||||
|
|
||||||
def _process_datadir_options(self, config: Config) -> None:
|
def _process_datadir_options(self, config: Config) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -172,245 +178,274 @@ class Configuration:
|
|||||||
--user-data, --datadir
|
--user-data, --datadir
|
||||||
"""
|
"""
|
||||||
# Check exchange parameter here - otherwise `datadir` might be wrong.
|
# Check exchange parameter here - otherwise `datadir` might be wrong.
|
||||||
if 'exchange' in self.args and self.args['exchange']:
|
if "exchange" in self.args and self.args["exchange"]:
|
||||||
config['exchange']['name'] = self.args['exchange']
|
config["exchange"]["name"] = self.args["exchange"]
|
||||||
logger.info(f"Using exchange {config['exchange']['name']}")
|
logger.info(f"Using exchange {config['exchange']['name']}")
|
||||||
|
|
||||||
if 'pair_whitelist' not in config['exchange']:
|
if "pair_whitelist" not in config["exchange"]:
|
||||||
config['exchange']['pair_whitelist'] = []
|
config["exchange"]["pair_whitelist"] = []
|
||||||
|
|
||||||
if 'user_data_dir' in self.args and self.args['user_data_dir']:
|
if "user_data_dir" in self.args and self.args["user_data_dir"]:
|
||||||
config.update({'user_data_dir': self.args['user_data_dir']})
|
config.update({"user_data_dir": self.args["user_data_dir"]})
|
||||||
elif 'user_data_dir' not in config:
|
elif "user_data_dir" not in config:
|
||||||
# Default to cwd/user_data (legacy option ...)
|
# Default to cwd/user_data (legacy option ...)
|
||||||
config.update({'user_data_dir': str(Path.cwd() / 'user_data')})
|
config.update({"user_data_dir": str(Path.cwd() / "user_data")})
|
||||||
|
|
||||||
# reset to user_data_dir so this contains the absolute path.
|
# reset to user_data_dir so this contains the absolute path.
|
||||||
config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False)
|
config["user_data_dir"] = create_userdata_dir(config["user_data_dir"], create_dir=False)
|
||||||
logger.info('Using user-data directory: %s ...', config['user_data_dir'])
|
logger.info("Using user-data directory: %s ...", config["user_data_dir"])
|
||||||
|
|
||||||
config.update({'datadir': create_datadir(config, self.args.get('datadir'))})
|
config.update({"datadir": create_datadir(config, self.args.get("datadir"))})
|
||||||
logger.info('Using data directory: %s ...', config.get('datadir'))
|
logger.info("Using data directory: %s ...", config.get("datadir"))
|
||||||
|
|
||||||
if self.args.get('exportfilename'):
|
if self.args.get("exportfilename"):
|
||||||
self._args_to_config(config, argname='exportfilename',
|
self._args_to_config(
|
||||||
logstring='Storing backtest results to {} ...')
|
config, argname="exportfilename", logstring="Storing backtest results to {} ..."
|
||||||
config['exportfilename'] = Path(config['exportfilename'])
|
)
|
||||||
|
config["exportfilename"] = Path(config["exportfilename"])
|
||||||
else:
|
else:
|
||||||
config['exportfilename'] = (config['user_data_dir']
|
config["exportfilename"] = config["user_data_dir"] / "backtest_results"
|
||||||
/ 'backtest_results')
|
|
||||||
|
|
||||||
if self.args.get('show_sensitive'):
|
if self.args.get("show_sensitive"):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Sensitive information will be shown in the upcomming output. "
|
"Sensitive information will be shown in the upcoming output. "
|
||||||
"Please make sure to never share this output without redacting "
|
"Please make sure to never share this output without redacting "
|
||||||
"the information yourself.")
|
"the information yourself."
|
||||||
|
)
|
||||||
|
|
||||||
def _process_optimize_options(self, config: Config) -> None:
|
def _process_optimize_options(self, config: Config) -> None:
|
||||||
|
|
||||||
# This will override the strategy configuration
|
# This will override the strategy configuration
|
||||||
self._args_to_config(config, argname='timeframe',
|
self._args_to_config(
|
||||||
logstring='Parameter -i/--timeframe detected ... '
|
config,
|
||||||
'Using timeframe: {} ...')
|
argname="timeframe",
|
||||||
|
logstring="Parameter -i/--timeframe detected ... Using timeframe: {} ...",
|
||||||
self._args_to_config(config, argname='position_stacking',
|
)
|
||||||
logstring='Parameter --enable-position-stacking detected ...')
|
|
||||||
|
|
||||||
self._args_to_config(
|
self._args_to_config(
|
||||||
config, argname='enable_protections',
|
config,
|
||||||
logstring='Parameter --enable-protections detected, enabling Protections. ...')
|
argname="position_stacking",
|
||||||
|
logstring="Parameter --enable-position-stacking detected ...",
|
||||||
|
)
|
||||||
|
|
||||||
if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]:
|
self._args_to_config(
|
||||||
config.update({'use_max_market_positions': False})
|
config,
|
||||||
logger.info('Parameter --disable-max-market-positions detected ...')
|
argname="enable_protections",
|
||||||
logger.info('max_open_trades set to unlimited ...')
|
logstring="Parameter --enable-protections detected, enabling Protections. ...",
|
||||||
elif 'max_open_trades' in self.args and self.args['max_open_trades']:
|
)
|
||||||
config.update({'max_open_trades': self.args['max_open_trades']})
|
|
||||||
logger.info('Parameter --max-open-trades detected, '
|
if "use_max_market_positions" in self.args and not self.args["use_max_market_positions"]:
|
||||||
'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
|
config.update({"use_max_market_positions": False})
|
||||||
elif config['runmode'] in NON_UTIL_MODES:
|
logger.info("Parameter --disable-max-market-positions detected ...")
|
||||||
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
logger.info("max_open_trades set to unlimited ...")
|
||||||
|
elif "max_open_trades" in self.args and self.args["max_open_trades"]:
|
||||||
|
config.update({"max_open_trades": self.args["max_open_trades"]})
|
||||||
|
logger.info(
|
||||||
|
"Parameter --max-open-trades detected, overriding max_open_trades to: %s ...",
|
||||||
|
config.get("max_open_trades"),
|
||||||
|
)
|
||||||
|
elif config["runmode"] in NON_UTIL_MODES:
|
||||||
|
logger.info("Using max_open_trades: %s ...", config.get("max_open_trades"))
|
||||||
# Setting max_open_trades to infinite if -1
|
# Setting max_open_trades to infinite if -1
|
||||||
if config.get('max_open_trades') == -1:
|
if config.get("max_open_trades") == -1:
|
||||||
config['max_open_trades'] = float('inf')
|
config["max_open_trades"] = float("inf")
|
||||||
|
|
||||||
if self.args.get('stake_amount'):
|
if self.args.get("stake_amount"):
|
||||||
# Convert explicitly to float to support CLI argument for both unlimited and value
|
# Convert explicitly to float to support CLI argument for both unlimited and value
|
||||||
try:
|
try:
|
||||||
self.args['stake_amount'] = float(self.args['stake_amount'])
|
self.args["stake_amount"] = float(self.args["stake_amount"])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
configurations = [
|
configurations = [
|
||||||
('timeframe_detail',
|
(
|
||||||
'Parameter --timeframe-detail detected, using {} for intra-candle backtesting ...'),
|
"timeframe_detail",
|
||||||
('backtest_show_pair_list', 'Parameter --show-pair-list detected.'),
|
"Parameter --timeframe-detail detected, using {} for intra-candle backtesting ...",
|
||||||
('stake_amount',
|
),
|
||||||
'Parameter --stake-amount detected, overriding stake_amount to: {} ...'),
|
("backtest_show_pair_list", "Parameter --show-pair-list detected."),
|
||||||
('dry_run_wallet',
|
(
|
||||||
'Parameter --dry-run-wallet detected, overriding dry_run_wallet to: {} ...'),
|
"stake_amount",
|
||||||
('fee', 'Parameter --fee detected, setting fee to: {} ...'),
|
"Parameter --stake-amount detected, overriding stake_amount to: {} ...",
|
||||||
('timerange', 'Parameter --timerange detected: {} ...'),
|
),
|
||||||
]
|
(
|
||||||
|
"dry_run_wallet",
|
||||||
|
"Parameter --dry-run-wallet detected, overriding dry_run_wallet to: {} ...",
|
||||||
|
),
|
||||||
|
("fee", "Parameter --fee detected, setting fee to: {} ..."),
|
||||||
|
("timerange", "Parameter --timerange detected: {} ..."),
|
||||||
|
]
|
||||||
|
|
||||||
self._args_to_config_loop(config, configurations)
|
self._args_to_config_loop(config, configurations)
|
||||||
|
|
||||||
self._process_datadir_options(config)
|
self._process_datadir_options(config)
|
||||||
|
|
||||||
self._args_to_config(config, argname='strategy_list',
|
self._args_to_config(
|
||||||
logstring='Using strategy list of {} strategies', logfun=len)
|
config,
|
||||||
|
argname="strategy_list",
|
||||||
|
logstring="Using strategy list of {} strategies",
|
||||||
|
logfun=len,
|
||||||
|
)
|
||||||
|
|
||||||
configurations = [
|
configurations = [
|
||||||
('recursive_strategy_search',
|
(
|
||||||
'Recursively searching for a strategy in the strategies folder.'),
|
"recursive_strategy_search",
|
||||||
('timeframe', 'Overriding timeframe with Command line argument'),
|
"Recursively searching for a strategy in the strategies folder.",
|
||||||
('export', 'Parameter --export detected: {} ...'),
|
),
|
||||||
('backtest_breakdown', 'Parameter --breakdown detected ...'),
|
("timeframe", "Overriding timeframe with Command line argument"),
|
||||||
('backtest_cache', 'Parameter --cache={} detected ...'),
|
("export", "Parameter --export detected: {} ..."),
|
||||||
('disableparamexport', 'Parameter --disableparamexport detected: {} ...'),
|
("backtest_breakdown", "Parameter --breakdown detected ..."),
|
||||||
('freqai_backtest_live_models',
|
("backtest_cache", "Parameter --cache={} detected ..."),
|
||||||
'Parameter --freqai-backtest-live-models detected ...'),
|
("disableparamexport", "Parameter --disableparamexport detected: {} ..."),
|
||||||
|
("freqai_backtest_live_models", "Parameter --freqai-backtest-live-models detected ..."),
|
||||||
]
|
]
|
||||||
self._args_to_config_loop(config, configurations)
|
self._args_to_config_loop(config, configurations)
|
||||||
|
|
||||||
# Edge section:
|
# Edge section:
|
||||||
if 'stoploss_range' in self.args and self.args["stoploss_range"]:
|
if "stoploss_range" in self.args and self.args["stoploss_range"]:
|
||||||
txt_range = eval(self.args["stoploss_range"])
|
txt_range = eval(self.args["stoploss_range"])
|
||||||
config['edge'].update({'stoploss_range_min': txt_range[0]})
|
config["edge"].update({"stoploss_range_min": txt_range[0]})
|
||||||
config['edge'].update({'stoploss_range_max': txt_range[1]})
|
config["edge"].update({"stoploss_range_max": txt_range[1]})
|
||||||
config['edge'].update({'stoploss_range_step': txt_range[2]})
|
config["edge"].update({"stoploss_range_step": txt_range[2]})
|
||||||
logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"])
|
logger.info("Parameter --stoplosses detected: %s ...", self.args["stoploss_range"])
|
||||||
|
|
||||||
# Hyperopt section
|
# Hyperopt section
|
||||||
|
|
||||||
configurations = [
|
configurations = [
|
||||||
('hyperopt', 'Using Hyperopt class name: {}'),
|
("hyperopt", "Using Hyperopt class name: {}"),
|
||||||
('hyperopt_path', 'Using additional Hyperopt lookup path: {}'),
|
("hyperopt_path", "Using additional Hyperopt lookup path: {}"),
|
||||||
('hyperoptexportfilename', 'Using hyperopt file: {}'),
|
("hyperoptexportfilename", "Using hyperopt file: {}"),
|
||||||
('lookahead_analysis_exportfilename', 'Saving lookahead analysis results into {} ...'),
|
("lookahead_analysis_exportfilename", "Saving lookahead analysis results into {} ..."),
|
||||||
('epochs', 'Parameter --epochs detected ... Will run Hyperopt with for {} epochs ...'),
|
("epochs", "Parameter --epochs detected ... Will run Hyperopt with for {} epochs ..."),
|
||||||
('spaces', 'Parameter -s/--spaces detected: {}'),
|
("spaces", "Parameter -s/--spaces detected: {}"),
|
||||||
('analyze_per_epoch', 'Parameter --analyze-per-epoch detected.'),
|
("analyze_per_epoch", "Parameter --analyze-per-epoch detected."),
|
||||||
('print_all', 'Parameter --print-all detected ...'),
|
("print_all", "Parameter --print-all detected ..."),
|
||||||
]
|
]
|
||||||
self._args_to_config_loop(config, configurations)
|
self._args_to_config_loop(config, configurations)
|
||||||
|
|
||||||
if 'print_colorized' in self.args and not self.args["print_colorized"]:
|
if "print_colorized" in self.args and not self.args["print_colorized"]:
|
||||||
logger.info('Parameter --no-color detected ...')
|
logger.info("Parameter --no-color detected ...")
|
||||||
config.update({'print_colorized': False})
|
config.update({"print_colorized": False})
|
||||||
else:
|
else:
|
||||||
config.update({'print_colorized': True})
|
config.update({"print_colorized": True})
|
||||||
|
|
||||||
configurations = [
|
configurations = [
|
||||||
('print_json', 'Parameter --print-json detected ...'),
|
("print_json", "Parameter --print-json detected ..."),
|
||||||
('export_csv', 'Parameter --export-csv detected: {}'),
|
("export_csv", "Parameter --export-csv detected: {}"),
|
||||||
('hyperopt_jobs', 'Parameter -j/--job-workers detected: {}'),
|
("hyperopt_jobs", "Parameter -j/--job-workers detected: {}"),
|
||||||
('hyperopt_random_state', 'Parameter --random-state detected: {}'),
|
("hyperopt_random_state", "Parameter --random-state detected: {}"),
|
||||||
('hyperopt_min_trades', 'Parameter --min-trades detected: {}'),
|
("hyperopt_min_trades", "Parameter --min-trades detected: {}"),
|
||||||
('hyperopt_loss', 'Using Hyperopt loss class name: {}'),
|
("hyperopt_loss", "Using Hyperopt loss class name: {}"),
|
||||||
('hyperopt_show_index', 'Parameter -n/--index detected: {}'),
|
("hyperopt_show_index", "Parameter -n/--index detected: {}"),
|
||||||
('hyperopt_list_best', 'Parameter --best detected: {}'),
|
("hyperopt_list_best", "Parameter --best detected: {}"),
|
||||||
('hyperopt_list_profitable', 'Parameter --profitable detected: {}'),
|
("hyperopt_list_profitable", "Parameter --profitable detected: {}"),
|
||||||
('hyperopt_list_min_trades', 'Parameter --min-trades detected: {}'),
|
("hyperopt_list_min_trades", "Parameter --min-trades detected: {}"),
|
||||||
('hyperopt_list_max_trades', 'Parameter --max-trades detected: {}'),
|
("hyperopt_list_max_trades", "Parameter --max-trades detected: {}"),
|
||||||
('hyperopt_list_min_avg_time', 'Parameter --min-avg-time detected: {}'),
|
("hyperopt_list_min_avg_time", "Parameter --min-avg-time detected: {}"),
|
||||||
('hyperopt_list_max_avg_time', 'Parameter --max-avg-time detected: {}'),
|
("hyperopt_list_max_avg_time", "Parameter --max-avg-time detected: {}"),
|
||||||
('hyperopt_list_min_avg_profit', 'Parameter --min-avg-profit detected: {}'),
|
("hyperopt_list_min_avg_profit", "Parameter --min-avg-profit detected: {}"),
|
||||||
('hyperopt_list_max_avg_profit', 'Parameter --max-avg-profit detected: {}'),
|
("hyperopt_list_max_avg_profit", "Parameter --max-avg-profit detected: {}"),
|
||||||
('hyperopt_list_min_total_profit', 'Parameter --min-total-profit detected: {}'),
|
("hyperopt_list_min_total_profit", "Parameter --min-total-profit detected: {}"),
|
||||||
('hyperopt_list_max_total_profit', 'Parameter --max-total-profit detected: {}'),
|
("hyperopt_list_max_total_profit", "Parameter --max-total-profit detected: {}"),
|
||||||
('hyperopt_list_min_objective', 'Parameter --min-objective detected: {}'),
|
("hyperopt_list_min_objective", "Parameter --min-objective detected: {}"),
|
||||||
('hyperopt_list_max_objective', 'Parameter --max-objective detected: {}'),
|
("hyperopt_list_max_objective", "Parameter --max-objective detected: {}"),
|
||||||
('hyperopt_list_no_details', 'Parameter --no-details detected: {}'),
|
("hyperopt_list_no_details", "Parameter --no-details detected: {}"),
|
||||||
('hyperopt_show_no_header', 'Parameter --no-header detected: {}'),
|
("hyperopt_show_no_header", "Parameter --no-header detected: {}"),
|
||||||
('hyperopt_ignore_missing_space', 'Paramter --ignore-missing-space detected: {}'),
|
("hyperopt_ignore_missing_space", "Parameter --ignore-missing-space detected: {}"),
|
||||||
]
|
]
|
||||||
|
|
||||||
self._args_to_config_loop(config, configurations)
|
self._args_to_config_loop(config, configurations)
|
||||||
|
|
||||||
def _process_plot_options(self, config: Config) -> None:
|
def _process_plot_options(self, config: Config) -> None:
|
||||||
|
|
||||||
configurations = [
|
configurations = [
|
||||||
('pairs', 'Using pairs {}'),
|
("pairs", "Using pairs {}"),
|
||||||
('indicators1', 'Using indicators1: {}'),
|
("indicators1", "Using indicators1: {}"),
|
||||||
('indicators2', 'Using indicators2: {}'),
|
("indicators2", "Using indicators2: {}"),
|
||||||
('trade_ids', 'Filtering on trade_ids: {}'),
|
("trade_ids", "Filtering on trade_ids: {}"),
|
||||||
('plot_limit', 'Limiting plot to: {}'),
|
("plot_limit", "Limiting plot to: {}"),
|
||||||
('plot_auto_open', 'Parameter --auto-open detected.'),
|
("plot_auto_open", "Parameter --auto-open detected."),
|
||||||
('trade_source', 'Using trades from: {}'),
|
("trade_source", "Using trades from: {}"),
|
||||||
('prepend_data', 'Prepend detected. Allowing data prepending.'),
|
("prepend_data", "Prepend detected. Allowing data prepending."),
|
||||||
('erase', 'Erase detected. Deleting existing data.'),
|
("erase", "Erase detected. Deleting existing data."),
|
||||||
('no_trades', 'Parameter --no-trades detected.'),
|
("no_trades", "Parameter --no-trades detected."),
|
||||||
('timeframes', 'timeframes --timeframes: {}'),
|
("timeframes", "timeframes --timeframes: {}"),
|
||||||
('days', 'Detected --days: {}'),
|
("days", "Detected --days: {}"),
|
||||||
('include_inactive', 'Detected --include-inactive-pairs: {}'),
|
("include_inactive", "Detected --include-inactive-pairs: {}"),
|
||||||
('download_trades', 'Detected --dl-trades: {}'),
|
("download_trades", "Detected --dl-trades: {}"),
|
||||||
('dataformat_ohlcv', 'Using "{}" to store OHLCV data.'),
|
("dataformat_ohlcv", 'Using "{}" to store OHLCV data.'),
|
||||||
('dataformat_trades', 'Using "{}" to store trades data.'),
|
("dataformat_trades", 'Using "{}" to store trades data.'),
|
||||||
('show_timerange', 'Detected --show-timerange'),
|
("show_timerange", "Detected --show-timerange"),
|
||||||
]
|
]
|
||||||
self._args_to_config_loop(config, configurations)
|
self._args_to_config_loop(config, configurations)
|
||||||
|
|
||||||
def _process_data_options(self, config: Config) -> None:
|
def _process_data_options(self, config: Config) -> None:
|
||||||
self._args_to_config(config, argname='new_pairs_days',
|
self._args_to_config(
|
||||||
logstring='Detected --new-pairs-days: {}')
|
config, argname="new_pairs_days", logstring="Detected --new-pairs-days: {}"
|
||||||
self._args_to_config(config, argname='trading_mode',
|
)
|
||||||
logstring='Detected --trading-mode: {}')
|
self._args_to_config(
|
||||||
config['candle_type_def'] = CandleType.get_default(
|
config, argname="trading_mode", logstring="Detected --trading-mode: {}"
|
||||||
config.get('trading_mode', 'spot') or 'spot')
|
)
|
||||||
config['trading_mode'] = TradingMode(config.get('trading_mode', 'spot') or 'spot')
|
config["candle_type_def"] = CandleType.get_default(
|
||||||
self._args_to_config(config, argname='candle_types',
|
config.get("trading_mode", "spot") or "spot"
|
||||||
logstring='Detected --candle-types: {}')
|
)
|
||||||
|
config["trading_mode"] = TradingMode(config.get("trading_mode", "spot") or "spot")
|
||||||
|
self._args_to_config(
|
||||||
|
config, argname="candle_types", logstring="Detected --candle-types: {}"
|
||||||
|
)
|
||||||
|
|
||||||
def _process_analyze_options(self, config: Config) -> None:
|
def _process_analyze_options(self, config: Config) -> None:
|
||||||
configurations = [
|
configurations = [
|
||||||
('analysis_groups', 'Analysis reason groups: {}'),
|
("analysis_groups", "Analysis reason groups: {}"),
|
||||||
('enter_reason_list', 'Analysis enter tag list: {}'),
|
("enter_reason_list", "Analysis enter tag list: {}"),
|
||||||
('exit_reason_list', 'Analysis exit tag list: {}'),
|
("exit_reason_list", "Analysis exit tag list: {}"),
|
||||||
('indicator_list', 'Analysis indicator list: {}'),
|
("indicator_list", "Analysis indicator list: {}"),
|
||||||
('timerange', 'Filter trades by timerange: {}'),
|
("timerange", "Filter trades by timerange: {}"),
|
||||||
('analysis_rejected', 'Analyse rejected signals: {}'),
|
("analysis_rejected", "Analyse rejected signals: {}"),
|
||||||
('analysis_to_csv', 'Store analysis tables to CSV: {}'),
|
("analysis_to_csv", "Store analysis tables to CSV: {}"),
|
||||||
('analysis_csv_path', 'Path to store analysis CSVs: {}'),
|
("analysis_csv_path", "Path to store analysis CSVs: {}"),
|
||||||
# Lookahead analysis results
|
# Lookahead analysis results
|
||||||
('targeted_trade_amount', 'Targeted Trade amount: {}'),
|
("targeted_trade_amount", "Targeted Trade amount: {}"),
|
||||||
('minimum_trade_amount', 'Minimum Trade amount: {}'),
|
("minimum_trade_amount", "Minimum Trade amount: {}"),
|
||||||
('lookahead_analysis_exportfilename', 'Path to store lookahead-analysis-results: {}'),
|
("lookahead_analysis_exportfilename", "Path to store lookahead-analysis-results: {}"),
|
||||||
('startup_candle', 'Startup candle to be used on recursive analysis: {}'),
|
("startup_candle", "Startup candle to be used on recursive analysis: {}"),
|
||||||
]
|
]
|
||||||
self._args_to_config_loop(config, configurations)
|
self._args_to_config_loop(config, configurations)
|
||||||
|
|
||||||
def _args_to_config_loop(self, config, configurations: List[Tuple[str, str]]) -> None:
|
def _args_to_config_loop(self, config, configurations: List[Tuple[str, str]]) -> None:
|
||||||
|
|
||||||
for argname, logstring in configurations:
|
for argname, logstring in configurations:
|
||||||
self._args_to_config(config, argname=argname, logstring=logstring)
|
self._args_to_config(config, argname=argname, logstring=logstring)
|
||||||
|
|
||||||
def _process_runmode(self, config: Config) -> None:
|
def _process_runmode(self, config: Config) -> None:
|
||||||
|
self._args_to_config(
|
||||||
self._args_to_config(config, argname='dry_run',
|
config,
|
||||||
logstring='Parameter --dry-run detected, '
|
argname="dry_run",
|
||||||
'overriding dry_run to: {} ...')
|
logstring="Parameter --dry-run detected, overriding dry_run to: {} ...",
|
||||||
|
)
|
||||||
|
|
||||||
if not self.runmode:
|
if not self.runmode:
|
||||||
# Handle real mode, infer dry/live from config
|
# Handle real mode, infer dry/live from config
|
||||||
self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE
|
self.runmode = RunMode.DRY_RUN if config.get("dry_run", True) else RunMode.LIVE
|
||||||
logger.info(f"Runmode set to {self.runmode.value}.")
|
logger.info(f"Runmode set to {self.runmode.value}.")
|
||||||
|
|
||||||
config.update({'runmode': self.runmode})
|
config.update({"runmode": self.runmode})
|
||||||
|
|
||||||
def _process_freqai_options(self, config: Config) -> None:
|
def _process_freqai_options(self, config: Config) -> None:
|
||||||
|
self._args_to_config(
|
||||||
|
config, argname="freqaimodel", logstring="Using freqaimodel class name: {}"
|
||||||
|
)
|
||||||
|
|
||||||
self._args_to_config(config, argname='freqaimodel',
|
self._args_to_config(
|
||||||
logstring='Using freqaimodel class name: {}')
|
config, argname="freqaimodel_path", logstring="Using freqaimodel path: {}"
|
||||||
|
)
|
||||||
self._args_to_config(config, argname='freqaimodel_path',
|
|
||||||
logstring='Using freqaimodel path: {}')
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def _args_to_config(self, config: Config, argname: str,
|
def _args_to_config(
|
||||||
logstring: str, logfun: Optional[Callable] = None,
|
self,
|
||||||
deprecated_msg: Optional[str] = None) -> None:
|
config: Config,
|
||||||
|
argname: str,
|
||||||
|
logstring: str,
|
||||||
|
logfun: Optional[Callable] = None,
|
||||||
|
deprecated_msg: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
:param config: Configuration dictionary
|
:param config: Configuration dictionary
|
||||||
:param argname: Argumentname in self.args - will be copied to config dict.
|
:param argname: Argumentname in self.args - will be copied to config dict.
|
||||||
@@ -420,9 +455,11 @@ class Configuration:
|
|||||||
sample: logfun=len (prints the length of the found
|
sample: logfun=len (prints the length of the found
|
||||||
configuration instead of the content)
|
configuration instead of the content)
|
||||||
"""
|
"""
|
||||||
if (argname in self.args and self.args[argname] is not None
|
if (
|
||||||
and self.args[argname] is not False):
|
argname in self.args
|
||||||
|
and self.args[argname] is not None
|
||||||
|
and self.args[argname] is not False
|
||||||
|
):
|
||||||
config.update({argname: self.args[argname]})
|
config.update({argname: self.args[argname]})
|
||||||
if logfun:
|
if logfun:
|
||||||
logger.info(logstring.format(logfun(config[argname])))
|
logger.info(logstring.format(logfun(config[argname])))
|
||||||
@@ -441,7 +478,7 @@ class Configuration:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if "pairs" in config:
|
if "pairs" in config:
|
||||||
config['exchange']['pair_whitelist'] = config['pairs']
|
config["exchange"]["pair_whitelist"] = config["pairs"]
|
||||||
return
|
return
|
||||||
|
|
||||||
if "pairs_file" in self.args and self.args["pairs_file"]:
|
if "pairs_file" in self.args and self.args["pairs_file"]:
|
||||||
@@ -451,19 +488,19 @@ class Configuration:
|
|||||||
# or if pairs file is specified explicitly
|
# or if pairs file is specified explicitly
|
||||||
if not pairs_file.exists():
|
if not pairs_file.exists():
|
||||||
raise OperationalException(f'No pairs file found with path "{pairs_file}".')
|
raise OperationalException(f'No pairs file found with path "{pairs_file}".')
|
||||||
config['pairs'] = load_file(pairs_file)
|
config["pairs"] = load_file(pairs_file)
|
||||||
if isinstance(config['pairs'], list):
|
if isinstance(config["pairs"], list):
|
||||||
config['pairs'].sort()
|
config["pairs"].sort()
|
||||||
return
|
return
|
||||||
|
|
||||||
if 'config' in self.args and self.args['config']:
|
if "config" in self.args and self.args["config"]:
|
||||||
logger.info("Using pairlist from configuration.")
|
logger.info("Using pairlist from configuration.")
|
||||||
config['pairs'] = config.get('exchange', {}).get('pair_whitelist')
|
config["pairs"] = config.get("exchange", {}).get("pair_whitelist")
|
||||||
else:
|
else:
|
||||||
# Fall back to /dl_path/pairs.json
|
# Fall back to /dl_path/pairs.json
|
||||||
pairs_file = config['datadir'] / 'pairs.json'
|
pairs_file = config["datadir"] / "pairs.json"
|
||||||
if pairs_file.exists():
|
if pairs_file.exists():
|
||||||
logger.info(f'Reading pairs file "{pairs_file}".')
|
logger.info(f'Reading pairs file "{pairs_file}".')
|
||||||
config['pairs'] = load_file(pairs_file)
|
config["pairs"] = load_file(pairs_file)
|
||||||
if 'pairs' in config and isinstance(config['pairs'], list):
|
if "pairs" in config and isinstance(config["pairs"], list):
|
||||||
config['pairs'].sort()
|
config["pairs"].sort()
|
||||||
|
|||||||
@@ -12,9 +12,13 @@ from freqtrade.exceptions import ConfigurationError, OperationalException
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def check_conflicting_settings(config: Config,
|
def check_conflicting_settings(
|
||||||
section_old: Optional[str], name_old: str,
|
config: Config,
|
||||||
section_new: Optional[str], name_new: str) -> None:
|
section_old: Optional[str],
|
||||||
|
name_old: str,
|
||||||
|
section_new: Optional[str],
|
||||||
|
name_new: str,
|
||||||
|
) -> None:
|
||||||
section_new_config = config.get(section_new, {}) if section_new else config
|
section_new_config = config.get(section_new, {}) if section_new else config
|
||||||
section_old_config = config.get(section_old, {}) if section_old else config
|
section_old_config = config.get(section_old, {}) if section_old else config
|
||||||
if name_new in section_new_config and name_old in section_old_config:
|
if name_new in section_new_config and name_old in section_old_config:
|
||||||
@@ -29,9 +33,9 @@ def check_conflicting_settings(config: Config,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def process_removed_setting(config: Config,
|
def process_removed_setting(
|
||||||
section1: str, name1: str,
|
config: Config, section1: str, name1: str, section2: Optional[str], name2: str
|
||||||
section2: Optional[str], name2: str) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
:param section1: Removed section
|
:param section1: Removed section
|
||||||
:param name1: Removed setting name
|
:param name1: Removed setting name
|
||||||
@@ -48,10 +52,13 @@ def process_removed_setting(config: Config,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def process_deprecated_setting(config: Config,
|
def process_deprecated_setting(
|
||||||
section_old: Optional[str], name_old: str,
|
config: Config,
|
||||||
section_new: Optional[str], name_new: str
|
section_old: Optional[str],
|
||||||
) -> None:
|
name_old: str,
|
||||||
|
section_new: Optional[str],
|
||||||
|
name_new: str,
|
||||||
|
) -> None:
|
||||||
check_conflicting_settings(config, section_old, name_old, section_new, name_new)
|
check_conflicting_settings(config, section_old, name_old, section_new, name_new)
|
||||||
section_old_config = config.get(section_old, {}) if section_old else config
|
section_old_config = config.get(section_old, {}) if section_old else config
|
||||||
|
|
||||||
@@ -71,57 +78,91 @@ def process_deprecated_setting(config: Config,
|
|||||||
|
|
||||||
|
|
||||||
def process_temporary_deprecated_settings(config: Config) -> None:
|
def process_temporary_deprecated_settings(config: Config) -> None:
|
||||||
|
|
||||||
# Kept for future deprecated / moved settings
|
# Kept for future deprecated / moved settings
|
||||||
# check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal',
|
# check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal',
|
||||||
# 'experimental', 'use_sell_signal')
|
# 'experimental', 'use_sell_signal')
|
||||||
|
|
||||||
process_deprecated_setting(config, 'ask_strategy', 'ignore_buying_expired_candle_after',
|
process_deprecated_setting(
|
||||||
None, 'ignore_buying_expired_candle_after')
|
config,
|
||||||
|
"ask_strategy",
|
||||||
|
"ignore_buying_expired_candle_after",
|
||||||
|
None,
|
||||||
|
"ignore_buying_expired_candle_after",
|
||||||
|
)
|
||||||
|
|
||||||
process_deprecated_setting(config, None, 'forcebuy_enable', None, 'force_entry_enable')
|
process_deprecated_setting(config, None, "forcebuy_enable", None, "force_entry_enable")
|
||||||
|
|
||||||
# New settings
|
# New settings
|
||||||
if config.get('telegram'):
|
if config.get("telegram"):
|
||||||
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell',
|
process_deprecated_setting(
|
||||||
'notification_settings', 'exit')
|
config["telegram"], "notification_settings", "sell", "notification_settings", "exit"
|
||||||
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell_fill',
|
)
|
||||||
'notification_settings', 'exit_fill')
|
process_deprecated_setting(
|
||||||
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell_cancel',
|
config["telegram"],
|
||||||
'notification_settings', 'exit_cancel')
|
"notification_settings",
|
||||||
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy',
|
"sell_fill",
|
||||||
'notification_settings', 'entry')
|
"notification_settings",
|
||||||
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy_fill',
|
"exit_fill",
|
||||||
'notification_settings', 'entry_fill')
|
)
|
||||||
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy_cancel',
|
process_deprecated_setting(
|
||||||
'notification_settings', 'entry_cancel')
|
config["telegram"],
|
||||||
if config.get('webhook'):
|
"notification_settings",
|
||||||
process_deprecated_setting(config, 'webhook', 'webhookbuy', 'webhook', 'webhookentry')
|
"sell_cancel",
|
||||||
process_deprecated_setting(config, 'webhook', 'webhookbuycancel',
|
"notification_settings",
|
||||||
'webhook', 'webhookentrycancel')
|
"exit_cancel",
|
||||||
process_deprecated_setting(config, 'webhook', 'webhookbuyfill',
|
)
|
||||||
'webhook', 'webhookentryfill')
|
process_deprecated_setting(
|
||||||
process_deprecated_setting(config, 'webhook', 'webhooksell', 'webhook', 'webhookexit')
|
config["telegram"], "notification_settings", "buy", "notification_settings", "entry"
|
||||||
process_deprecated_setting(config, 'webhook', 'webhooksellcancel',
|
)
|
||||||
'webhook', 'webhookexitcancel')
|
process_deprecated_setting(
|
||||||
process_deprecated_setting(config, 'webhook', 'webhooksellfill',
|
config["telegram"],
|
||||||
'webhook', 'webhookexitfill')
|
"notification_settings",
|
||||||
|
"buy_fill",
|
||||||
|
"notification_settings",
|
||||||
|
"entry_fill",
|
||||||
|
)
|
||||||
|
process_deprecated_setting(
|
||||||
|
config["telegram"],
|
||||||
|
"notification_settings",
|
||||||
|
"buy_cancel",
|
||||||
|
"notification_settings",
|
||||||
|
"entry_cancel",
|
||||||
|
)
|
||||||
|
if config.get("webhook"):
|
||||||
|
process_deprecated_setting(config, "webhook", "webhookbuy", "webhook", "webhookentry")
|
||||||
|
process_deprecated_setting(
|
||||||
|
config, "webhook", "webhookbuycancel", "webhook", "webhookentrycancel"
|
||||||
|
)
|
||||||
|
process_deprecated_setting(
|
||||||
|
config, "webhook", "webhookbuyfill", "webhook", "webhookentryfill"
|
||||||
|
)
|
||||||
|
process_deprecated_setting(config, "webhook", "webhooksell", "webhook", "webhookexit")
|
||||||
|
process_deprecated_setting(
|
||||||
|
config, "webhook", "webhooksellcancel", "webhook", "webhookexitcancel"
|
||||||
|
)
|
||||||
|
process_deprecated_setting(
|
||||||
|
config, "webhook", "webhooksellfill", "webhook", "webhookexitfill"
|
||||||
|
)
|
||||||
|
|
||||||
# Legacy way - having them in experimental ...
|
# Legacy way - having them in experimental ...
|
||||||
|
|
||||||
process_removed_setting(config, 'experimental', 'use_sell_signal', None, 'use_exit_signal')
|
process_removed_setting(config, "experimental", "use_sell_signal", None, "use_exit_signal")
|
||||||
process_removed_setting(config, 'experimental', 'sell_profit_only', None, 'exit_profit_only')
|
process_removed_setting(config, "experimental", "sell_profit_only", None, "exit_profit_only")
|
||||||
process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal',
|
process_removed_setting(
|
||||||
None, 'ignore_roi_if_entry_signal')
|
config, "experimental", "ignore_roi_if_buy_signal", None, "ignore_roi_if_entry_signal"
|
||||||
|
)
|
||||||
|
|
||||||
process_removed_setting(config, 'ask_strategy', 'use_sell_signal', None, 'use_exit_signal')
|
process_removed_setting(config, "ask_strategy", "use_sell_signal", None, "use_exit_signal")
|
||||||
process_removed_setting(config, 'ask_strategy', 'sell_profit_only', None, 'exit_profit_only')
|
process_removed_setting(config, "ask_strategy", "sell_profit_only", None, "exit_profit_only")
|
||||||
process_removed_setting(config, 'ask_strategy', 'sell_profit_offset',
|
process_removed_setting(
|
||||||
None, 'exit_profit_offset')
|
config, "ask_strategy", "sell_profit_offset", None, "exit_profit_offset"
|
||||||
process_removed_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal',
|
)
|
||||||
None, 'ignore_roi_if_entry_signal')
|
process_removed_setting(
|
||||||
if (config.get('edge', {}).get('enabled', False)
|
config, "ask_strategy", "ignore_roi_if_buy_signal", None, "ignore_roi_if_entry_signal"
|
||||||
and 'capital_available_percentage' in config.get('edge', {})):
|
)
|
||||||
|
if config.get("edge", {}).get(
|
||||||
|
"enabled", False
|
||||||
|
) and "capital_available_percentage" in config.get("edge", {}):
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"DEPRECATED: "
|
"DEPRECATED: "
|
||||||
"Using 'edge.capital_available_percentage' has been deprecated in favor of "
|
"Using 'edge.capital_available_percentage' has been deprecated in favor of "
|
||||||
@@ -129,12 +170,11 @@ def process_temporary_deprecated_settings(config: Config) -> None:
|
|||||||
"'tradable_balance_ratio' and remove 'capital_available_percentage' "
|
"'tradable_balance_ratio' and remove 'capital_available_percentage' "
|
||||||
"from the edge configuration."
|
"from the edge configuration."
|
||||||
)
|
)
|
||||||
if 'ticker_interval' in config:
|
if "ticker_interval" in config:
|
||||||
|
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"DEPRECATED: 'ticker_interval' detected. "
|
"DEPRECATED: 'ticker_interval' detected. "
|
||||||
"Please use 'timeframe' instead of 'ticker_interval."
|
"Please use 'timeframe' instead of 'ticker_interval."
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'protections' in config:
|
if "protections" in config:
|
||||||
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
|
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ def running_in_docker() -> bool:
|
|||||||
"""
|
"""
|
||||||
Check if we are running in a docker container
|
Check if we are running in a docker container
|
||||||
"""
|
"""
|
||||||
return os.environ.get('FT_APP_ENV') == 'docker'
|
return os.environ.get("FT_APP_ENV") == "docker"
|
||||||
|
|||||||
@@ -4,8 +4,14 @@ from pathlib import Path
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from freqtrade.configuration.detect_environment import running_in_docker
|
from freqtrade.configuration.detect_environment import running_in_docker
|
||||||
from freqtrade.constants import (USER_DATA_FILES, USERPATH_FREQAIMODELS, USERPATH_HYPEROPTS,
|
from freqtrade.constants import (
|
||||||
USERPATH_NOTEBOOKS, USERPATH_STRATEGIES, Config)
|
USER_DATA_FILES,
|
||||||
|
USERPATH_FREQAIMODELS,
|
||||||
|
USERPATH_HYPEROPTS,
|
||||||
|
USERPATH_NOTEBOOKS,
|
||||||
|
USERPATH_STRATEGIES,
|
||||||
|
Config,
|
||||||
|
)
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
|
|
||||||
@@ -13,16 +19,15 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def create_datadir(config: Config, datadir: Optional[str] = None) -> Path:
|
def create_datadir(config: Config, datadir: Optional[str] = None) -> Path:
|
||||||
|
|
||||||
folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data")
|
folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data")
|
||||||
if not datadir:
|
if not datadir:
|
||||||
# set datadir
|
# set datadir
|
||||||
exchange_name = config.get('exchange', {}).get('name', '').lower()
|
exchange_name = config.get("exchange", {}).get("name", "").lower()
|
||||||
folder = folder.joinpath(exchange_name)
|
folder = folder.joinpath(exchange_name)
|
||||||
|
|
||||||
if not folder.is_dir():
|
if not folder.is_dir():
|
||||||
folder.mkdir(parents=True)
|
folder.mkdir(parents=True)
|
||||||
logger.info(f'Created data directory: {datadir}')
|
logger.info(f"Created data directory: {datadir}")
|
||||||
return folder
|
return folder
|
||||||
|
|
||||||
|
|
||||||
@@ -34,8 +39,8 @@ def chown_user_directory(directory: Path) -> None:
|
|||||||
if running_in_docker():
|
if running_in_docker():
|
||||||
try:
|
try:
|
||||||
import subprocess
|
import subprocess
|
||||||
subprocess.check_output(
|
|
||||||
['sudo', 'chown', '-R', 'ftuser:', str(directory.resolve())])
|
subprocess.check_output(["sudo", "chown", "-R", "ftuser:", str(directory.resolve())])
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(f"Could not chown {directory}")
|
logger.warning(f"Could not chown {directory}")
|
||||||
|
|
||||||
@@ -50,18 +55,28 @@ def create_userdata_dir(directory: str, create_dir: bool = False) -> Path:
|
|||||||
:param create_dir: Create directory if it does not exist.
|
:param create_dir: Create directory if it does not exist.
|
||||||
:return: Path object containing the directory
|
:return: Path object containing the directory
|
||||||
"""
|
"""
|
||||||
sub_dirs = ["backtest_results", "data", USERPATH_HYPEROPTS, "hyperopt_results", "logs",
|
sub_dirs = [
|
||||||
USERPATH_NOTEBOOKS, "plot", USERPATH_STRATEGIES, USERPATH_FREQAIMODELS]
|
"backtest_results",
|
||||||
|
"data",
|
||||||
|
USERPATH_HYPEROPTS,
|
||||||
|
"hyperopt_results",
|
||||||
|
"logs",
|
||||||
|
USERPATH_NOTEBOOKS,
|
||||||
|
"plot",
|
||||||
|
USERPATH_STRATEGIES,
|
||||||
|
USERPATH_FREQAIMODELS,
|
||||||
|
]
|
||||||
folder = Path(directory)
|
folder = Path(directory)
|
||||||
chown_user_directory(folder)
|
chown_user_directory(folder)
|
||||||
if not folder.is_dir():
|
if not folder.is_dir():
|
||||||
if create_dir:
|
if create_dir:
|
||||||
folder.mkdir(parents=True)
|
folder.mkdir(parents=True)
|
||||||
logger.info(f'Created user-data directory: {folder}')
|
logger.info(f"Created user-data directory: {folder}")
|
||||||
else:
|
else:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"Directory `{folder}` does not exist. "
|
f"Directory `{folder}` does not exist. "
|
||||||
"Please use `freqtrade create-userdir` to create a user directory")
|
"Please use `freqtrade create-userdir` to create a user directory"
|
||||||
|
)
|
||||||
|
|
||||||
# Create required subdirectories
|
# Create required subdirectories
|
||||||
for f in sub_dirs:
|
for f in sub_dirs:
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ def _get_var_typed(val):
|
|||||||
try:
|
try:
|
||||||
return float(val)
|
return float(val)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
if val.lower() in ('t', 'true'):
|
if val.lower() in ("t", "true"):
|
||||||
return True
|
return True
|
||||||
elif val.lower() in ('f', 'false'):
|
elif val.lower() in ("f", "false"):
|
||||||
return False
|
return False
|
||||||
# keep as string
|
# keep as string
|
||||||
return val
|
return val
|
||||||
@@ -32,16 +32,21 @@ def _flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str
|
|||||||
:param prefix: Prefix to consider (usually FREQTRADE__)
|
:param prefix: Prefix to consider (usually FREQTRADE__)
|
||||||
:return: Nested dict based on available and relevant variables.
|
:return: Nested dict based on available and relevant variables.
|
||||||
"""
|
"""
|
||||||
no_convert = ['CHAT_ID', 'PASSWORD']
|
no_convert = ["CHAT_ID", "PASSWORD"]
|
||||||
relevant_vars: Dict[str, Any] = {}
|
relevant_vars: Dict[str, Any] = {}
|
||||||
|
|
||||||
for env_var, val in sorted(env_dict.items()):
|
for env_var, val in sorted(env_dict.items()):
|
||||||
if env_var.startswith(prefix):
|
if env_var.startswith(prefix):
|
||||||
logger.info(f"Loading variable '{env_var}'")
|
logger.info(f"Loading variable '{env_var}'")
|
||||||
key = env_var.replace(prefix, '')
|
key = env_var.replace(prefix, "")
|
||||||
for k in reversed(key.split('__')):
|
for k in reversed(key.split("__")):
|
||||||
val = {k.lower(): _get_var_typed(val)
|
val = {
|
||||||
if not isinstance(val, dict) and k not in no_convert else val}
|
k.lower(): (
|
||||||
|
_get_var_typed(val)
|
||||||
|
if not isinstance(val, dict) and k not in no_convert
|
||||||
|
else val
|
||||||
|
)
|
||||||
|
}
|
||||||
relevant_vars = deep_merge_dicts(val, relevant_vars)
|
relevant_vars = deep_merge_dicts(val, relevant_vars)
|
||||||
return relevant_vars
|
return relevant_vars
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
This module contain functions to load the configuration file
|
This module contain functions to load the configuration file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@@ -25,25 +26,25 @@ def log_config_error_range(path: str, errmsg: str) -> str:
|
|||||||
"""
|
"""
|
||||||
Parses configuration file and prints range around error
|
Parses configuration file and prints range around error
|
||||||
"""
|
"""
|
||||||
if path != '-':
|
if path != "-":
|
||||||
offsetlist = re.findall(r'(?<=Parse\serror\sat\soffset\s)\d+', errmsg)
|
offsetlist = re.findall(r"(?<=Parse\serror\sat\soffset\s)\d+", errmsg)
|
||||||
if offsetlist:
|
if offsetlist:
|
||||||
offset = int(offsetlist[0])
|
offset = int(offsetlist[0])
|
||||||
text = Path(path).read_text()
|
text = Path(path).read_text()
|
||||||
# Fetch an offset of 80 characters around the error line
|
# Fetch an offset of 80 characters around the error line
|
||||||
subtext = text[offset - min(80, offset):offset + 80]
|
subtext = text[offset - min(80, offset) : offset + 80]
|
||||||
segments = subtext.split('\n')
|
segments = subtext.split("\n")
|
||||||
if len(segments) > 3:
|
if len(segments) > 3:
|
||||||
# Remove first and last lines, to avoid odd truncations
|
# Remove first and last lines, to avoid odd truncations
|
||||||
return '\n'.join(segments[1:-1])
|
return "\n".join(segments[1:-1])
|
||||||
else:
|
else:
|
||||||
return subtext
|
return subtext
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def load_file(path: Path) -> Dict[str, Any]:
|
def load_file(path: Path) -> Dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
with path.open('r') as file:
|
with path.open("r") as file:
|
||||||
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
|
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise OperationalException(f'File "{path}" not found!') from None
|
raise OperationalException(f'File "{path}" not found!') from None
|
||||||
@@ -58,25 +59,27 @@ def load_config_file(path: str) -> Dict[str, Any]:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Read config from stdin if requested in the options
|
# Read config from stdin if requested in the options
|
||||||
with Path(path).open() if path != '-' else sys.stdin as file:
|
with Path(path).open() if path != "-" else sys.stdin as file:
|
||||||
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
|
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Config file "{path}" not found!'
|
f'Config file "{path}" not found!'
|
||||||
' Please create a config file or check whether it exists.') from None
|
" Please create a config file or check whether it exists."
|
||||||
|
) from None
|
||||||
except rapidjson.JSONDecodeError as e:
|
except rapidjson.JSONDecodeError as e:
|
||||||
err_range = log_config_error_range(path, str(e))
|
err_range = log_config_error_range(path, str(e))
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
f'{e}\n'
|
f"{e}\nPlease verify the following segment of your configuration:\n{err_range}"
|
||||||
f'Please verify the following segment of your configuration:\n{err_range}'
|
if err_range
|
||||||
if err_range else 'Please verify your configuration file for syntax errors.'
|
else "Please verify your configuration file for syntax errors."
|
||||||
)
|
)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def load_from_files(
|
def load_from_files(
|
||||||
files: List[str], base_path: Optional[Path] = None, level: int = 0) -> Dict[str, Any]:
|
files: List[str], base_path: Optional[Path] = None, level: int = 0
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Recursively load configuration files if specified.
|
Recursively load configuration files if specified.
|
||||||
Sub-files are assumed to be relative to the initial config.
|
Sub-files are assumed to be relative to the initial config.
|
||||||
@@ -90,8 +93,8 @@ def load_from_files(
|
|||||||
files_loaded = []
|
files_loaded = []
|
||||||
# We expect here a list of config filenames
|
# We expect here a list of config filenames
|
||||||
for filename in files:
|
for filename in files:
|
||||||
logger.info(f'Using config: {filename} ...')
|
logger.info(f"Using config: {filename} ...")
|
||||||
if filename == '-':
|
if filename == "-":
|
||||||
# Immediately load stdin and return
|
# Immediately load stdin and return
|
||||||
return load_config_file(filename)
|
return load_config_file(filename)
|
||||||
file = Path(filename)
|
file = Path(filename)
|
||||||
@@ -100,10 +103,11 @@ def load_from_files(
|
|||||||
file = base_path / file
|
file = base_path / file
|
||||||
|
|
||||||
config_tmp = load_config_file(str(file))
|
config_tmp = load_config_file(str(file))
|
||||||
if 'add_config_files' in config_tmp:
|
if "add_config_files" in config_tmp:
|
||||||
config_sub = load_from_files(
|
config_sub = load_from_files(
|
||||||
config_tmp['add_config_files'], file.resolve().parent, level + 1)
|
config_tmp["add_config_files"], file.resolve().parent, level + 1
|
||||||
files_loaded.extend(config_sub.get('config_files', []))
|
)
|
||||||
|
files_loaded.extend(config_sub.get("config_files", []))
|
||||||
config_tmp = deep_merge_dicts(config_tmp, config_sub)
|
config_tmp = deep_merge_dicts(config_tmp, config_sub)
|
||||||
|
|
||||||
files_loaded.insert(0, str(file))
|
files_loaded.insert(0, str(file))
|
||||||
@@ -111,6 +115,6 @@ def load_from_files(
|
|||||||
# Merge config options, overwriting prior values
|
# Merge config options, overwriting prior values
|
||||||
config = deep_merge_dicts(config_tmp, config)
|
config = deep_merge_dicts(config_tmp, config)
|
||||||
|
|
||||||
config['config_files'] = files_loaded
|
config["config_files"] = files_loaded
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
This module contains the argument manager class
|
This module contains the argument manager class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -22,9 +23,13 @@ class TimeRange:
|
|||||||
if *type is None, don't use corresponding startvalue.
|
if *type is None, don't use corresponding startvalue.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, starttype: Optional[str] = None, stoptype: Optional[str] = None,
|
def __init__(
|
||||||
startts: int = 0, stopts: int = 0):
|
self,
|
||||||
|
starttype: Optional[str] = None,
|
||||||
|
stoptype: Optional[str] = None,
|
||||||
|
startts: int = 0,
|
||||||
|
stopts: int = 0,
|
||||||
|
):
|
||||||
self.starttype: Optional[str] = starttype
|
self.starttype: Optional[str] = starttype
|
||||||
self.stoptype: Optional[str] = stoptype
|
self.stoptype: Optional[str] = stoptype
|
||||||
self.startts: int = startts
|
self.startts: int = startts
|
||||||
@@ -48,12 +53,12 @@ class TimeRange:
|
|||||||
Returns a string representation of the timerange as used by parse_timerange.
|
Returns a string representation of the timerange as used by parse_timerange.
|
||||||
Follows the format yyyymmdd-yyyymmdd - leaving out the parts that are not set.
|
Follows the format yyyymmdd-yyyymmdd - leaving out the parts that are not set.
|
||||||
"""
|
"""
|
||||||
start = ''
|
start = ""
|
||||||
stop = ''
|
stop = ""
|
||||||
if startdt := self.startdt:
|
if startdt := self.startdt:
|
||||||
start = startdt.strftime('%Y%m%d')
|
start = startdt.strftime("%Y%m%d")
|
||||||
if stopdt := self.stopdt:
|
if stopdt := self.stopdt:
|
||||||
stop = stopdt.strftime('%Y%m%d')
|
stop = stopdt.strftime("%Y%m%d")
|
||||||
return f"{start}-{stop}"
|
return f"{start}-{stop}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -61,7 +66,7 @@ class TimeRange:
|
|||||||
"""
|
"""
|
||||||
Returns a string representation of the start date
|
Returns a string representation of the start date
|
||||||
"""
|
"""
|
||||||
val = 'unbounded'
|
val = "unbounded"
|
||||||
if (startdt := self.startdt) is not None:
|
if (startdt := self.startdt) is not None:
|
||||||
val = startdt.strftime(DATETIME_PRINT_FORMAT)
|
val = startdt.strftime(DATETIME_PRINT_FORMAT)
|
||||||
return val
|
return val
|
||||||
@@ -71,15 +76,19 @@ class TimeRange:
|
|||||||
"""
|
"""
|
||||||
Returns a string representation of the stop date
|
Returns a string representation of the stop date
|
||||||
"""
|
"""
|
||||||
val = 'unbounded'
|
val = "unbounded"
|
||||||
if (stopdt := self.stopdt) is not None:
|
if (stopdt := self.stopdt) is not None:
|
||||||
val = stopdt.strftime(DATETIME_PRINT_FORMAT)
|
val = stopdt.strftime(DATETIME_PRINT_FORMAT)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
"""Override the default Equals behavior"""
|
"""Override the default Equals behavior"""
|
||||||
return (self.starttype == other.starttype and self.stoptype == other.stoptype
|
return (
|
||||||
and self.startts == other.startts and self.stopts == other.stopts)
|
self.starttype == other.starttype
|
||||||
|
and self.stoptype == other.stoptype
|
||||||
|
and self.startts == other.startts
|
||||||
|
and self.stopts == other.stopts
|
||||||
|
)
|
||||||
|
|
||||||
def subtract_start(self, seconds: int) -> None:
|
def subtract_start(self, seconds: int) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -90,8 +99,9 @@ class TimeRange:
|
|||||||
if self.startts:
|
if self.startts:
|
||||||
self.startts = self.startts - seconds
|
self.startts = self.startts - seconds
|
||||||
|
|
||||||
def adjust_start_if_necessary(self, timeframe_secs: int, startup_candles: int,
|
def adjust_start_if_necessary(
|
||||||
min_date: datetime) -> None:
|
self, timeframe_secs: int, startup_candles: int, min_date: datetime
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Adjust startts by <startup_candles> candles.
|
Adjust startts by <startup_candles> candles.
|
||||||
Applies only if no startup-candles have been available.
|
Applies only if no startup-candles have been available.
|
||||||
@@ -101,13 +111,13 @@ class TimeRange:
|
|||||||
has to be moved
|
has to be moved
|
||||||
:return: None (Modifies the object in place)
|
:return: None (Modifies the object in place)
|
||||||
"""
|
"""
|
||||||
if (not self.starttype or (startup_candles
|
if not self.starttype or (startup_candles and min_date.timestamp() >= self.startts):
|
||||||
and min_date.timestamp() >= self.startts)):
|
|
||||||
# If no startts was defined, or backtest-data starts at the defined backtest-date
|
# If no startts was defined, or backtest-data starts at the defined backtest-date
|
||||||
logger.warning("Moving start-date by %s candles to account for startup time.",
|
logger.warning(
|
||||||
startup_candles)
|
"Moving start-date by %s candles to account for startup time.", startup_candles
|
||||||
|
)
|
||||||
self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles)
|
self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles)
|
||||||
self.starttype = 'date'
|
self.starttype = "date"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_timerange(cls, text: Optional[str]) -> Self:
|
def parse_timerange(cls, text: Optional[str]) -> Self:
|
||||||
@@ -118,16 +128,17 @@ class TimeRange:
|
|||||||
"""
|
"""
|
||||||
if not text:
|
if not text:
|
||||||
return cls(None, None, 0, 0)
|
return cls(None, None, 0, 0)
|
||||||
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
syntax = [
|
||||||
(r'^(\d{8})-$', ('date', None)),
|
(r"^-(\d{8})$", (None, "date")),
|
||||||
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
(r"^(\d{8})-$", ("date", None)),
|
||||||
(r'^-(\d{10})$', (None, 'date')),
|
(r"^(\d{8})-(\d{8})$", ("date", "date")),
|
||||||
(r'^(\d{10})-$', ('date', None)),
|
(r"^-(\d{10})$", (None, "date")),
|
||||||
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
|
(r"^(\d{10})-$", ("date", None)),
|
||||||
(r'^-(\d{13})$', (None, 'date')),
|
(r"^(\d{10})-(\d{10})$", ("date", "date")),
|
||||||
(r'^(\d{13})-$', ('date', None)),
|
(r"^-(\d{13})$", (None, "date")),
|
||||||
(r'^(\d{13})-(\d{13})$', ('date', 'date')),
|
(r"^(\d{13})-$", ("date", None)),
|
||||||
]
|
(r"^(\d{13})-(\d{13})$", ("date", "date")),
|
||||||
|
]
|
||||||
for rex, stype in syntax:
|
for rex, stype in syntax:
|
||||||
# Apply the regular expression to text
|
# Apply the regular expression to text
|
||||||
match = re.match(rex, text)
|
match = re.match(rex, text)
|
||||||
@@ -138,9 +149,12 @@ class TimeRange:
|
|||||||
stop: int = 0
|
stop: int = 0
|
||||||
if stype[0]:
|
if stype[0]:
|
||||||
starts = rvals[index]
|
starts = rvals[index]
|
||||||
if stype[0] == 'date' and len(starts) == 8:
|
if stype[0] == "date" and len(starts) == 8:
|
||||||
start = int(datetime.strptime(starts, '%Y%m%d').replace(
|
start = int(
|
||||||
tzinfo=timezone.utc).timestamp())
|
datetime.strptime(starts, "%Y%m%d")
|
||||||
|
.replace(tzinfo=timezone.utc)
|
||||||
|
.timestamp()
|
||||||
|
)
|
||||||
elif len(starts) == 13:
|
elif len(starts) == 13:
|
||||||
start = int(starts) // 1000
|
start = int(starts) // 1000
|
||||||
else:
|
else:
|
||||||
@@ -148,15 +162,19 @@ class TimeRange:
|
|||||||
index += 1
|
index += 1
|
||||||
if stype[1]:
|
if stype[1]:
|
||||||
stops = rvals[index]
|
stops = rvals[index]
|
||||||
if stype[1] == 'date' and len(stops) == 8:
|
if stype[1] == "date" and len(stops) == 8:
|
||||||
stop = int(datetime.strptime(stops, '%Y%m%d').replace(
|
stop = int(
|
||||||
tzinfo=timezone.utc).timestamp())
|
datetime.strptime(stops, "%Y%m%d")
|
||||||
|
.replace(tzinfo=timezone.utc)
|
||||||
|
.timestamp()
|
||||||
|
)
|
||||||
elif len(stops) == 13:
|
elif len(stops) == 13:
|
||||||
stop = int(stops) // 1000
|
stop = int(stops) // 1000
|
||||||
else:
|
else:
|
||||||
stop = int(stops)
|
stop = int(stops)
|
||||||
if start > stop > 0:
|
if start > stop > 0:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
f'Start date is after stop date for timerange "{text}"')
|
f'Start date is after stop date for timerange "{text}"'
|
||||||
|
)
|
||||||
return cls(stype[0], stype[1], start, stop)
|
return cls(stype[0], stype[1], start, stop)
|
||||||
raise ConfigurationError(f'Incorrect syntax for timerange "{text}"')
|
raise ConfigurationError(f'Incorrect syntax for timerange "{text}"')
|
||||||
|
|||||||
@@ -3,6 +3,4 @@ Module to handle data operations for freqtrade
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# limit what's imported when using `from freqtrade.data import *`
|
# limit what's imported when using `from freqtrade.data import *`
|
||||||
__all__ = [
|
__all__ = ["converter"]
|
||||||
'converter'
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Helpers when analyzing backtest data
|
Helpers when analyzing backtest data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -21,14 +22,35 @@ from freqtrade.types import BacktestHistoryEntryType, BacktestResultType
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Newest format
|
# Newest format
|
||||||
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'max_stake_amount', 'amount',
|
BT_DATA_COLUMNS = [
|
||||||
'open_date', 'close_date', 'open_rate', 'close_rate',
|
"pair",
|
||||||
'fee_open', 'fee_close', 'trade_duration',
|
"stake_amount",
|
||||||
'profit_ratio', 'profit_abs', 'exit_reason',
|
"max_stake_amount",
|
||||||
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
"amount",
|
||||||
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag',
|
"open_date",
|
||||||
'leverage', 'is_short', 'open_timestamp', 'close_timestamp', 'orders'
|
"close_date",
|
||||||
]
|
"open_rate",
|
||||||
|
"close_rate",
|
||||||
|
"fee_open",
|
||||||
|
"fee_close",
|
||||||
|
"trade_duration",
|
||||||
|
"profit_ratio",
|
||||||
|
"profit_abs",
|
||||||
|
"exit_reason",
|
||||||
|
"initial_stop_loss_abs",
|
||||||
|
"initial_stop_loss_ratio",
|
||||||
|
"stop_loss_abs",
|
||||||
|
"stop_loss_ratio",
|
||||||
|
"min_rate",
|
||||||
|
"max_rate",
|
||||||
|
"is_open",
|
||||||
|
"enter_tag",
|
||||||
|
"leverage",
|
||||||
|
"is_short",
|
||||||
|
"open_timestamp",
|
||||||
|
"close_timestamp",
|
||||||
|
"orders",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:
|
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:
|
||||||
@@ -50,15 +72,16 @@ def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> s
|
|||||||
|
|
||||||
if not filename.is_file():
|
if not filename.is_file():
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Directory '{directory}' does not seem to contain backtest statistics yet.")
|
f"Directory '{directory}' does not seem to contain backtest statistics yet."
|
||||||
|
)
|
||||||
|
|
||||||
with filename.open() as file:
|
with filename.open() as file:
|
||||||
data = json_load(file)
|
data = json_load(file)
|
||||||
|
|
||||||
if f'latest_{variant}' not in data:
|
if f"latest_{variant}" not in data:
|
||||||
raise ValueError(f"Invalid '{LAST_BT_RESULT_FN}' format.")
|
raise ValueError(f"Invalid '{LAST_BT_RESULT_FN}' format.")
|
||||||
|
|
||||||
return data[f'latest_{variant}']
|
return data[f"latest_{variant}"]
|
||||||
|
|
||||||
|
|
||||||
def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
|
def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
|
||||||
@@ -71,7 +94,7 @@ def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
|
|||||||
* `directory/.last_result.json` does not exist
|
* `directory/.last_result.json` does not exist
|
||||||
* `directory/.last_result.json` has the wrong content
|
* `directory/.last_result.json` has the wrong content
|
||||||
"""
|
"""
|
||||||
return get_latest_optimize_filename(directory, 'backtest')
|
return get_latest_optimize_filename(directory, "backtest")
|
||||||
|
|
||||||
|
|
||||||
def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
|
def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
|
||||||
@@ -85,14 +108,15 @@ def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
|
|||||||
* `directory/.last_result.json` has the wrong content
|
* `directory/.last_result.json` has the wrong content
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return get_latest_optimize_filename(directory, 'hyperopt')
|
return get_latest_optimize_filename(directory, "hyperopt")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Return default (legacy) pickle filename
|
# Return default (legacy) pickle filename
|
||||||
return 'hyperopt_results.pickle'
|
return "hyperopt_results.pickle"
|
||||||
|
|
||||||
|
|
||||||
def get_latest_hyperopt_file(
|
def get_latest_hyperopt_file(
|
||||||
directory: Union[Path, str], predef_filename: Optional[str] = None) -> Path:
|
directory: Union[Path, str], predef_filename: Optional[str] = None
|
||||||
|
) -> Path:
|
||||||
"""
|
"""
|
||||||
Get latest hyperopt export based on '.last_result.json'.
|
Get latest hyperopt export based on '.last_result.json'.
|
||||||
:param directory: Directory to search for last result
|
:param directory: Directory to search for last result
|
||||||
@@ -107,7 +131,8 @@ def get_latest_hyperopt_file(
|
|||||||
if predef_filename:
|
if predef_filename:
|
||||||
if Path(predef_filename).is_absolute():
|
if Path(predef_filename).is_absolute():
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"--hyperopt-filename expects only the filename, not an absolute path.")
|
"--hyperopt-filename expects only the filename, not an absolute path."
|
||||||
|
)
|
||||||
return directory / predef_filename
|
return directory / predef_filename
|
||||||
return directory / get_latest_hyperopt_filename(directory)
|
return directory / get_latest_hyperopt_filename(directory)
|
||||||
|
|
||||||
@@ -126,7 +151,7 @@ def load_backtest_metadata(filename: Union[Path, str]) -> Dict[str, Any]:
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return {}
|
return {}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise OperationalException('Unexpected error while loading backtest metadata.') from e
|
raise OperationalException("Unexpected error while loading backtest metadata.") from e
|
||||||
|
|
||||||
|
|
||||||
def load_backtest_stats(filename: Union[Path, str]) -> BacktestResultType:
|
def load_backtest_stats(filename: Union[Path, str]) -> BacktestResultType:
|
||||||
@@ -147,7 +172,7 @@ def load_backtest_stats(filename: Union[Path, str]) -> BacktestResultType:
|
|||||||
|
|
||||||
# Legacy list format does not contain metadata.
|
# Legacy list format does not contain metadata.
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
data['metadata'] = load_backtest_metadata(filename)
|
data["metadata"] = load_backtest_metadata(filename)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -159,38 +184,39 @@ def load_and_merge_backtest_result(strategy_name: str, filename: Path, results:
|
|||||||
:param results: dict to merge the result to.
|
:param results: dict to merge the result to.
|
||||||
"""
|
"""
|
||||||
bt_data = load_backtest_stats(filename)
|
bt_data = load_backtest_stats(filename)
|
||||||
k: Literal['metadata', 'strategy']
|
k: Literal["metadata", "strategy"]
|
||||||
for k in ('metadata', 'strategy'): # type: ignore
|
for k in ("metadata", "strategy"): # type: ignore
|
||||||
results[k][strategy_name] = bt_data[k][strategy_name]
|
results[k][strategy_name] = bt_data[k][strategy_name]
|
||||||
results['metadata'][strategy_name]['filename'] = filename.stem
|
results["metadata"][strategy_name]["filename"] = filename.stem
|
||||||
comparison = bt_data['strategy_comparison']
|
comparison = bt_data["strategy_comparison"]
|
||||||
for i in range(len(comparison)):
|
for i in range(len(comparison)):
|
||||||
if comparison[i]['key'] == strategy_name:
|
if comparison[i]["key"] == strategy_name:
|
||||||
results['strategy_comparison'].append(comparison[i])
|
results["strategy_comparison"].append(comparison[i])
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def _get_backtest_files(dirname: Path) -> List[Path]:
|
def _get_backtest_files(dirname: Path) -> List[Path]:
|
||||||
# Weird glob expression here avoids including .meta.json files.
|
# Weird glob expression here avoids including .meta.json files.
|
||||||
return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))))
|
return list(reversed(sorted(dirname.glob("backtest-result-*-[0-9][0-9].json"))))
|
||||||
|
|
||||||
|
|
||||||
def _extract_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]:
|
def _extract_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]:
|
||||||
metadata = load_backtest_metadata(filename)
|
metadata = load_backtest_metadata(filename)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'filename': filename.stem,
|
"filename": filename.stem,
|
||||||
'strategy': s,
|
"strategy": s,
|
||||||
'run_id': v['run_id'],
|
"run_id": v["run_id"],
|
||||||
'notes': v.get('notes', ''),
|
"notes": v.get("notes", ""),
|
||||||
# Backtest "run" time
|
# Backtest "run" time
|
||||||
'backtest_start_time': v['backtest_start_time'],
|
"backtest_start_time": v["backtest_start_time"],
|
||||||
# Backtest timerange
|
# Backtest timerange
|
||||||
'backtest_start_ts': v.get('backtest_start_ts', None),
|
"backtest_start_ts": v.get("backtest_start_ts", None),
|
||||||
'backtest_end_ts': v.get('backtest_end_ts', None),
|
"backtest_end_ts": v.get("backtest_end_ts", None),
|
||||||
'timeframe': v.get('timeframe', None),
|
"timeframe": v.get("timeframe", None),
|
||||||
'timeframe_detail': v.get('timeframe_detail', None),
|
"timeframe_detail": v.get("timeframe_detail", None),
|
||||||
} for s, v in metadata.items()
|
}
|
||||||
|
for s, v in metadata.items()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -218,7 +244,7 @@ def delete_backtest_result(file_abs: Path):
|
|||||||
"""
|
"""
|
||||||
# *.meta.json
|
# *.meta.json
|
||||||
logger.info(f"Deleting backtest result file: {file_abs.name}")
|
logger.info(f"Deleting backtest result file: {file_abs.name}")
|
||||||
file_abs_meta = file_abs.with_suffix('.meta.json')
|
file_abs_meta = file_abs.with_suffix(".meta.json")
|
||||||
file_abs.unlink()
|
file_abs.unlink()
|
||||||
file_abs_meta.unlink()
|
file_abs_meta.unlink()
|
||||||
|
|
||||||
@@ -238,8 +264,19 @@ def update_backtest_metadata(filename: Path, strategy: str, content: Dict[str, A
|
|||||||
file_dump_json(get_backtest_metadata_filename(filename), metadata)
|
file_dump_json(get_backtest_metadata_filename(filename), metadata)
|
||||||
|
|
||||||
|
|
||||||
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
|
def get_backtest_market_change(filename: Path, include_ts: bool = True) -> pd.DataFrame:
|
||||||
min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]:
|
"""
|
||||||
|
Read backtest market change file.
|
||||||
|
"""
|
||||||
|
df = pd.read_feather(filename)
|
||||||
|
if include_ts:
|
||||||
|
df.loc[:, "__date_ts"] = df.loc[:, "date"].astype(np.int64) // 1000 // 1000
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def find_existing_backtest_stats(
|
||||||
|
dirname: Union[Path, str], run_ids: Dict[str, str], min_backtest_date: Optional[datetime] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Find existing backtest stats that match specified run IDs and load them.
|
Find existing backtest stats that match specified run IDs and load them.
|
||||||
:param dirname: pathlib.Path object, or string pointing to the file.
|
:param dirname: pathlib.Path object, or string pointing to the file.
|
||||||
@@ -251,9 +288,9 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
|
|||||||
run_ids = copy(run_ids)
|
run_ids = copy(run_ids)
|
||||||
dirname = Path(dirname)
|
dirname = Path(dirname)
|
||||||
results: Dict[str, Any] = {
|
results: Dict[str, Any] = {
|
||||||
'metadata': {},
|
"metadata": {},
|
||||||
'strategy': {},
|
"strategy": {},
|
||||||
'strategy_comparison': [],
|
"strategy_comparison": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
for filename in _get_backtest_files(dirname):
|
for filename in _get_backtest_files(dirname):
|
||||||
@@ -270,14 +307,14 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if min_backtest_date is not None:
|
if min_backtest_date is not None:
|
||||||
backtest_date = strategy_metadata['backtest_start_time']
|
backtest_date = strategy_metadata["backtest_start_time"]
|
||||||
backtest_date = datetime.fromtimestamp(backtest_date, tz=timezone.utc)
|
backtest_date = datetime.fromtimestamp(backtest_date, tz=timezone.utc)
|
||||||
if backtest_date < min_backtest_date:
|
if backtest_date < min_backtest_date:
|
||||||
# Do not use a cached result for this strategy as first result is too old.
|
# Do not use a cached result for this strategy as first result is too old.
|
||||||
del run_ids[strategy_name]
|
del run_ids[strategy_name]
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if strategy_metadata['run_id'] == run_id:
|
if strategy_metadata["run_id"] == run_id:
|
||||||
del run_ids[strategy_name]
|
del run_ids[strategy_name]
|
||||||
load_and_merge_backtest_result(strategy_name, filename, results)
|
load_and_merge_backtest_result(strategy_name, filename, results)
|
||||||
|
|
||||||
@@ -290,20 +327,20 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
|
|||||||
"""
|
"""
|
||||||
Compatibility support for older backtest data.
|
Compatibility support for older backtest data.
|
||||||
"""
|
"""
|
||||||
df['open_date'] = pd.to_datetime(df['open_date'], utc=True)
|
df["open_date"] = pd.to_datetime(df["open_date"], utc=True)
|
||||||
df['close_date'] = pd.to_datetime(df['close_date'], utc=True)
|
df["close_date"] = pd.to_datetime(df["close_date"], utc=True)
|
||||||
# Compatibility support for pre short Columns
|
# Compatibility support for pre short Columns
|
||||||
if 'is_short' not in df.columns:
|
if "is_short" not in df.columns:
|
||||||
df['is_short'] = False
|
df["is_short"] = False
|
||||||
if 'leverage' not in df.columns:
|
if "leverage" not in df.columns:
|
||||||
df['leverage'] = 1.0
|
df["leverage"] = 1.0
|
||||||
if 'enter_tag' not in df.columns:
|
if "enter_tag" not in df.columns:
|
||||||
df['enter_tag'] = df['buy_tag']
|
df["enter_tag"] = df["buy_tag"]
|
||||||
df = df.drop(['buy_tag'], axis=1)
|
df = df.drop(["buy_tag"], axis=1)
|
||||||
if 'max_stake_amount' not in df.columns:
|
if "max_stake_amount" not in df.columns:
|
||||||
df['max_stake_amount'] = df['stake_amount']
|
df["max_stake_amount"] = df["stake_amount"]
|
||||||
if 'orders' not in df.columns:
|
if "orders" not in df.columns:
|
||||||
df['orders'] = None
|
df["orders"] = None
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
@@ -319,23 +356,25 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
|
|||||||
data = load_backtest_stats(filename)
|
data = load_backtest_stats(filename)
|
||||||
if not isinstance(data, list):
|
if not isinstance(data, list):
|
||||||
# new, nested format
|
# new, nested format
|
||||||
if 'strategy' not in data:
|
if "strategy" not in data:
|
||||||
raise ValueError("Unknown dataformat.")
|
raise ValueError("Unknown dataformat.")
|
||||||
|
|
||||||
if not strategy:
|
if not strategy:
|
||||||
if len(data['strategy']) == 1:
|
if len(data["strategy"]) == 1:
|
||||||
strategy = list(data['strategy'].keys())[0]
|
strategy = list(data["strategy"].keys())[0]
|
||||||
else:
|
else:
|
||||||
raise ValueError("Detected backtest result with more than one strategy. "
|
raise ValueError(
|
||||||
"Please specify a strategy.")
|
"Detected backtest result with more than one strategy. "
|
||||||
|
"Please specify a strategy."
|
||||||
|
)
|
||||||
|
|
||||||
if strategy not in data['strategy']:
|
if strategy not in data["strategy"]:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Strategy {strategy} not available in the backtest result. "
|
f"Strategy {strategy} not available in the backtest result. "
|
||||||
f"Available strategies are '{','.join(data['strategy'].keys())}'"
|
f"Available strategies are '{','.join(data['strategy'].keys())}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
data = data['strategy'][strategy]['trades']
|
data = data["strategy"][strategy]["trades"]
|
||||||
df = pd.DataFrame(data)
|
df = pd.DataFrame(data)
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
df = _load_backtest_data_df_compatibility(df)
|
df = _load_backtest_data_df_compatibility(df)
|
||||||
@@ -343,7 +382,8 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
|
|||||||
else:
|
else:
|
||||||
# old format - only with lists.
|
# old format - only with lists.
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"Backtest-results with only trades data are no longer supported.")
|
"Backtest-results with only trades data are no longer supported."
|
||||||
|
)
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
df = df.sort_values("open_date").reset_index(drop=True)
|
df = df.sort_values("open_date").reset_index(drop=True)
|
||||||
return df
|
return df
|
||||||
@@ -358,23 +398,26 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF
|
|||||||
:return: dataframe with open-counts per time-period in timeframe
|
:return: dataframe with open-counts per time-period in timeframe
|
||||||
"""
|
"""
|
||||||
from freqtrade.exchange import timeframe_to_resample_freq
|
from freqtrade.exchange import timeframe_to_resample_freq
|
||||||
|
|
||||||
timeframe_freq = timeframe_to_resample_freq(timeframe)
|
timeframe_freq = timeframe_to_resample_freq(timeframe)
|
||||||
dates = [pd.Series(pd.date_range(row[1]['open_date'], row[1]['close_date'],
|
dates = [
|
||||||
freq=timeframe_freq))
|
pd.Series(pd.date_range(row[1]["open_date"], row[1]["close_date"], freq=timeframe_freq))
|
||||||
for row in results[['open_date', 'close_date']].iterrows()]
|
for row in results[["open_date", "close_date"]].iterrows()
|
||||||
|
]
|
||||||
deltas = [len(x) for x in dates]
|
deltas = [len(x) for x in dates]
|
||||||
dates = pd.Series(pd.concat(dates).values, name='date')
|
dates = pd.Series(pd.concat(dates).values, name="date")
|
||||||
df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns)
|
df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns)
|
||||||
|
|
||||||
df2 = pd.concat([dates, df2], axis=1)
|
df2 = pd.concat([dates, df2], axis=1)
|
||||||
df2 = df2.set_index('date')
|
df2 = df2.set_index("date")
|
||||||
df_final = df2.resample(timeframe_freq)[['pair']].count()
|
df_final = df2.resample(timeframe_freq)[["pair"]].count()
|
||||||
df_final = df_final.rename({'pair': 'open_trades'}, axis=1)
|
df_final = df_final.rename({"pair": "open_trades"}, axis=1)
|
||||||
return df_final
|
return df_final
|
||||||
|
|
||||||
|
|
||||||
def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
|
def evaluate_result_multi(
|
||||||
max_open_trades: IntOrInf) -> pd.DataFrame:
|
results: pd.DataFrame, timeframe: str, max_open_trades: IntOrInf
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Find overlapping trades by expanding each trade once per period it was open
|
Find overlapping trades by expanding each trade once per period it was open
|
||||||
and then counting overlaps
|
and then counting overlaps
|
||||||
@@ -384,7 +427,7 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
|
|||||||
:return: dataframe with open-counts per time-period in freq
|
:return: dataframe with open-counts per time-period in freq
|
||||||
"""
|
"""
|
||||||
df_final = analyze_trade_parallelism(results, timeframe)
|
df_final = analyze_trade_parallelism(results, timeframe)
|
||||||
return df_final[df_final['open_trades'] > max_open_trades]
|
return df_final[df_final["open_trades"] > max_open_trades]
|
||||||
|
|
||||||
|
|
||||||
def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.DataFrame:
|
def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.DataFrame:
|
||||||
@@ -395,9 +438,9 @@ def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.
|
|||||||
"""
|
"""
|
||||||
df = pd.DataFrame.from_records([t.to_json(True) for t in trades], columns=BT_DATA_COLUMNS)
|
df = pd.DataFrame.from_records([t.to_json(True) for t in trades], columns=BT_DATA_COLUMNS)
|
||||||
if len(df) > 0:
|
if len(df) > 0:
|
||||||
df['close_date'] = pd.to_datetime(df['close_date'], utc=True)
|
df["close_date"] = pd.to_datetime(df["close_date"], utc=True)
|
||||||
df['open_date'] = pd.to_datetime(df['open_date'], utc=True)
|
df["open_date"] = pd.to_datetime(df["open_date"], utc=True)
|
||||||
df['close_rate'] = df['close_rate'].astype('float64')
|
df["close_rate"] = df["close_rate"].astype("float64")
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
@@ -419,8 +462,13 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF
|
|||||||
return trades
|
return trades
|
||||||
|
|
||||||
|
|
||||||
def load_trades(source: str, db_url: str, exportfilename: Path,
|
def load_trades(
|
||||||
no_trades: bool = False, strategy: Optional[str] = None) -> pd.DataFrame:
|
source: str,
|
||||||
|
db_url: str,
|
||||||
|
exportfilename: Path,
|
||||||
|
no_trades: bool = False,
|
||||||
|
strategy: Optional[str] = None,
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on configuration option 'trade_source':
|
Based on configuration option 'trade_source':
|
||||||
* loads data from DB (using `db_url`)
|
* loads data from DB (using `db_url`)
|
||||||
@@ -441,8 +489,9 @@ def load_trades(source: str, db_url: str, exportfilename: Path,
|
|||||||
return load_backtest_data(exportfilename, strategy)
|
return load_backtest_data(exportfilename, strategy)
|
||||||
|
|
||||||
|
|
||||||
def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame,
|
def extract_trades_of_period(
|
||||||
date_index=False) -> pd.DataFrame:
|
dataframe: pd.DataFrame, trades: pd.DataFrame, date_index=False
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Compare trades and backtested pair DataFrames to get trades performed on backtested period
|
Compare trades and backtested pair DataFrames to get trades performed on backtested period
|
||||||
:return: the DataFrame of a trades of period
|
:return: the DataFrame of a trades of period
|
||||||
@@ -451,8 +500,9 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame,
|
|||||||
trades_start = dataframe.index[0]
|
trades_start = dataframe.index[0]
|
||||||
trades_stop = dataframe.index[-1]
|
trades_stop = dataframe.index[-1]
|
||||||
else:
|
else:
|
||||||
trades_start = dataframe.iloc[0]['date']
|
trades_start = dataframe.iloc[0]["date"]
|
||||||
trades_stop = dataframe.iloc[-1]['date']
|
trades_stop = dataframe.iloc[-1]["date"]
|
||||||
trades = trades.loc[(trades['open_date'] >= trades_start) &
|
trades = trades.loc[
|
||||||
(trades['close_date'] <= trades_stop)]
|
(trades["open_date"] >= trades_start) & (trades["close_date"] <= trades_stop)
|
||||||
|
]
|
||||||
return trades
|
return trades
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
from freqtrade.data.converter.converter import (clean_ohlcv_dataframe, convert_ohlcv_format,
|
from freqtrade.data.converter.converter import (
|
||||||
ohlcv_fill_up_missing_data, ohlcv_to_dataframe,
|
clean_ohlcv_dataframe,
|
||||||
order_book_to_dataframe, reduce_dataframe_footprint,
|
convert_ohlcv_format,
|
||||||
trim_dataframe, trim_dataframes)
|
ohlcv_fill_up_missing_data,
|
||||||
|
ohlcv_to_dataframe,
|
||||||
|
order_book_to_dataframe,
|
||||||
|
reduce_dataframe_footprint,
|
||||||
|
trim_dataframe,
|
||||||
|
trim_dataframes,
|
||||||
|
)
|
||||||
from freqtrade.data.converter.orderflow import populate_dataframe_with_trades
|
from freqtrade.data.converter.orderflow import populate_dataframe_with_trades
|
||||||
from freqtrade.data.converter.trade_converter import (convert_trades_format,
|
from freqtrade.data.converter.trade_converter import (
|
||||||
convert_trades_to_ohlcv, trades_convert_types,
|
convert_trades_format,
|
||||||
trades_df_remove_duplicates,
|
convert_trades_to_ohlcv,
|
||||||
trades_dict_to_list, trades_list_to_df,
|
trades_convert_types,
|
||||||
trades_to_ohlcv)
|
trades_df_remove_duplicates,
|
||||||
|
trades_dict_to_list,
|
||||||
|
trades_list_to_df,
|
||||||
|
trades_to_ohlcv,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Functions to convert data from one format to another
|
Functions to convert data from one format to another
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
@@ -15,8 +16,14 @@ from freqtrade.enums import CandleType, TradingMode
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *,
|
def ohlcv_to_dataframe(
|
||||||
fill_missing: bool = True, drop_incomplete: bool = True) -> DataFrame:
|
ohlcv: list,
|
||||||
|
timeframe: str,
|
||||||
|
pair: str,
|
||||||
|
*,
|
||||||
|
fill_missing: bool = True,
|
||||||
|
drop_incomplete: bool = True,
|
||||||
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Converts a list with candle (OHLCV) data (in format returned by ccxt.fetch_ohlcv)
|
Converts a list with candle (OHLCV) data (in format returned by ccxt.fetch_ohlcv)
|
||||||
to a Dataframe
|
to a Dataframe
|
||||||
@@ -32,20 +39,28 @@ def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *,
|
|||||||
cols = DEFAULT_DATAFRAME_COLUMNS
|
cols = DEFAULT_DATAFRAME_COLUMNS
|
||||||
df = DataFrame(ohlcv, columns=cols)
|
df = DataFrame(ohlcv, columns=cols)
|
||||||
|
|
||||||
df['date'] = to_datetime(df['date'], unit='ms', utc=True)
|
df["date"] = to_datetime(df["date"], unit="ms", utc=True)
|
||||||
|
|
||||||
# Some exchanges return int values for Volume and even for OHLC.
|
# Some exchanges return int values for Volume and even for OHLC.
|
||||||
# Convert them since TA-LIB indicators used in the strategy assume floats
|
# Convert them since TA-LIB indicators used in the strategy assume floats
|
||||||
# and fail with exception...
|
# and fail with exception...
|
||||||
df = df.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float',
|
df = df.astype(
|
||||||
'volume': 'float'})
|
dtype={
|
||||||
return clean_ohlcv_dataframe(df, timeframe, pair,
|
"open": "float",
|
||||||
fill_missing=fill_missing,
|
"high": "float",
|
||||||
drop_incomplete=drop_incomplete)
|
"low": "float",
|
||||||
|
"close": "float",
|
||||||
|
"volume": "float",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return clean_ohlcv_dataframe(
|
||||||
|
df, timeframe, pair, fill_missing=fill_missing, drop_incomplete=drop_incomplete
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *,
|
def clean_ohlcv_dataframe(
|
||||||
fill_missing: bool, drop_incomplete: bool) -> DataFrame:
|
data: DataFrame, timeframe: str, pair: str, *, fill_missing: bool, drop_incomplete: bool
|
||||||
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Cleanse a OHLCV dataframe by
|
Cleanse a OHLCV dataframe by
|
||||||
* Grouping it by date (removes duplicate tics)
|
* Grouping it by date (removes duplicate tics)
|
||||||
@@ -60,17 +75,19 @@ def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *,
|
|||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
# group by index and aggregate results to eliminate duplicate ticks
|
# group by index and aggregate results to eliminate duplicate ticks
|
||||||
data = data.groupby(by='date', as_index=False, sort=True).agg({
|
data = data.groupby(by="date", as_index=False, sort=True).agg(
|
||||||
'open': 'first',
|
{
|
||||||
'high': 'max',
|
"open": "first",
|
||||||
'low': 'min',
|
"high": "max",
|
||||||
'close': 'last',
|
"low": "min",
|
||||||
'volume': 'max',
|
"close": "last",
|
||||||
})
|
"volume": "max",
|
||||||
|
}
|
||||||
|
)
|
||||||
# eliminate partial candle
|
# eliminate partial candle
|
||||||
if drop_incomplete:
|
if drop_incomplete:
|
||||||
data.drop(data.tail(1).index, inplace=True)
|
data.drop(data.tail(1).index, inplace=True)
|
||||||
logger.debug('Dropping last candle')
|
logger.debug("Dropping last candle")
|
||||||
|
|
||||||
if fill_missing:
|
if fill_missing:
|
||||||
return ohlcv_fill_up_missing_data(data, timeframe, pair)
|
return ohlcv_fill_up_missing_data(data, timeframe, pair)
|
||||||
@@ -81,37 +98,35 @@ def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *,
|
|||||||
def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) -> DataFrame:
|
def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Fills up missing data with 0 volume rows,
|
Fills up missing data with 0 volume rows,
|
||||||
using the previous close as price for "open", "high" "low" and "close", volume is set to 0
|
using the previous close as price for "open", "high", "low" and "close", volume is set to 0
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from freqtrade.exchange import timeframe_to_resample_freq
|
from freqtrade.exchange import timeframe_to_resample_freq
|
||||||
|
|
||||||
ohlcv_dict = {
|
ohlcv_dict = {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}
|
||||||
'open': 'first',
|
|
||||||
'high': 'max',
|
|
||||||
'low': 'min',
|
|
||||||
'close': 'last',
|
|
||||||
'volume': 'sum'
|
|
||||||
}
|
|
||||||
resample_interval = timeframe_to_resample_freq(timeframe)
|
resample_interval = timeframe_to_resample_freq(timeframe)
|
||||||
# Resample to create "NAN" values
|
# Resample to create "NAN" values
|
||||||
df = dataframe.resample(resample_interval, on='date').agg(ohlcv_dict)
|
df = dataframe.resample(resample_interval, on="date").agg(ohlcv_dict)
|
||||||
|
|
||||||
# Forwardfill close for missing columns
|
# Forwardfill close for missing columns
|
||||||
df['close'] = df['close'].ffill()
|
df["close"] = df["close"].ffill()
|
||||||
# Use close for "open, high, low"
|
# Use close for "open, high, low"
|
||||||
df.loc[:, ['open', 'high', 'low']] = df[['open', 'high', 'low']].fillna(
|
df.loc[:, ["open", "high", "low"]] = df[["open", "high", "low"]].fillna(
|
||||||
value={'open': df['close'],
|
value={
|
||||||
'high': df['close'],
|
"open": df["close"],
|
||||||
'low': df['close'],
|
"high": df["close"],
|
||||||
})
|
"low": df["close"],
|
||||||
|
}
|
||||||
|
)
|
||||||
df.reset_index(inplace=True)
|
df.reset_index(inplace=True)
|
||||||
len_before = len(dataframe)
|
len_before = len(dataframe)
|
||||||
len_after = len(df)
|
len_after = len(df)
|
||||||
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
|
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
|
||||||
if len_before != len_after:
|
if len_before != len_after:
|
||||||
message = (f"Missing data fillup for {pair}, {timeframe}: "
|
message = (
|
||||||
f"before: {len_before} - after: {len_after} - {pct_missing:.2%}")
|
f"Missing data fillup for {pair}, {timeframe}: "
|
||||||
|
f"before: {len_before} - after: {len_after} - {pct_missing:.2%}"
|
||||||
|
)
|
||||||
if pct_missing > 0.01:
|
if pct_missing > 0.01:
|
||||||
logger.info(message)
|
logger.info(message)
|
||||||
else:
|
else:
|
||||||
@@ -120,8 +135,9 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
|||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
def trim_dataframe(df: DataFrame, timerange, *, df_date_col: str = 'date',
|
def trim_dataframe(
|
||||||
startup_candles: int = 0) -> DataFrame:
|
df: DataFrame, timerange, *, df_date_col: str = "date", startup_candles: int = 0
|
||||||
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Trim dataframe based on given timerange
|
Trim dataframe based on given timerange
|
||||||
:param df: Dataframe to trim
|
:param df: Dataframe to trim
|
||||||
@@ -134,15 +150,16 @@ def trim_dataframe(df: DataFrame, timerange, *, df_date_col: str = 'date',
|
|||||||
# Trim candles instead of timeframe in case of given startup_candle count
|
# Trim candles instead of timeframe in case of given startup_candle count
|
||||||
df = df.iloc[startup_candles:, :]
|
df = df.iloc[startup_candles:, :]
|
||||||
else:
|
else:
|
||||||
if timerange.starttype == 'date':
|
if timerange.starttype == "date":
|
||||||
df = df.loc[df[df_date_col] >= timerange.startdt, :]
|
df = df.loc[df[df_date_col] >= timerange.startdt, :]
|
||||||
if timerange.stoptype == 'date':
|
if timerange.stoptype == "date":
|
||||||
df = df.loc[df[df_date_col] <= timerange.stopdt, :]
|
df = df.loc[df[df_date_col] <= timerange.stopdt, :]
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
def trim_dataframes(preprocessed: Dict[str, DataFrame], timerange,
|
def trim_dataframes(
|
||||||
startup_candles: int) -> Dict[str, DataFrame]:
|
preprocessed: Dict[str, DataFrame], timerange, startup_candles: int
|
||||||
|
) -> Dict[str, DataFrame]:
|
||||||
"""
|
"""
|
||||||
Trim startup period from analyzed dataframes
|
Trim startup period from analyzed dataframes
|
||||||
:param preprocessed: Dict of pair: dataframe
|
:param preprocessed: Dict of pair: dataframe
|
||||||
@@ -157,8 +174,9 @@ def trim_dataframes(preprocessed: Dict[str, DataFrame], timerange,
|
|||||||
if not trimed_df.empty:
|
if not trimed_df.empty:
|
||||||
processed[pair] = trimed_df
|
processed[pair] = trimed_df
|
||||||
else:
|
else:
|
||||||
logger.warning(f'{pair} has no data left after adjusting for startup candles, '
|
logger.warning(
|
||||||
f'skipping.')
|
f"{pair} has no data left after adjusting for startup candles, skipping."
|
||||||
|
)
|
||||||
return processed
|
return processed
|
||||||
|
|
||||||
|
|
||||||
@@ -170,19 +188,28 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
|
|||||||
b_sum b_size bids asks a_size a_sum
|
b_sum b_size bids asks a_size a_sum
|
||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
"""
|
"""
|
||||||
cols = ['bids', 'b_size']
|
cols = ["bids", "b_size"]
|
||||||
|
|
||||||
bids_frame = DataFrame(bids, columns=cols)
|
bids_frame = DataFrame(bids, columns=cols)
|
||||||
# add cumulative sum column
|
# add cumulative sum column
|
||||||
bids_frame['b_sum'] = bids_frame['b_size'].cumsum()
|
bids_frame["b_sum"] = bids_frame["b_size"].cumsum()
|
||||||
cols2 = ['asks', 'a_size']
|
cols2 = ["asks", "a_size"]
|
||||||
asks_frame = DataFrame(asks, columns=cols2)
|
asks_frame = DataFrame(asks, columns=cols2)
|
||||||
# add cumulative sum column
|
# add cumulative sum column
|
||||||
asks_frame['a_sum'] = asks_frame['a_size'].cumsum()
|
asks_frame["a_sum"] = asks_frame["a_size"].cumsum()
|
||||||
|
|
||||||
frame = pd.concat([bids_frame['b_sum'], bids_frame['b_size'], bids_frame['bids'],
|
frame = pd.concat(
|
||||||
asks_frame['asks'], asks_frame['a_size'], asks_frame['a_sum']], axis=1,
|
[
|
||||||
keys=['b_sum', 'b_size', 'bids', 'asks', 'a_size', 'a_sum'])
|
bids_frame["b_sum"],
|
||||||
|
bids_frame["b_size"],
|
||||||
|
bids_frame["bids"],
|
||||||
|
asks_frame["asks"],
|
||||||
|
asks_frame["a_size"],
|
||||||
|
asks_frame["a_sum"],
|
||||||
|
],
|
||||||
|
axis=1,
|
||||||
|
keys=["b_sum", "b_size", "bids", "asks", "a_size", "a_sum"],
|
||||||
|
)
|
||||||
# logger.info('order book %s', frame )
|
# logger.info('order book %s', frame )
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
@@ -201,47 +228,51 @@ def convert_ohlcv_format(
|
|||||||
:param erase: Erase source data (does not apply if source and target format are identical)
|
:param erase: Erase source data (does not apply if source and target format are identical)
|
||||||
"""
|
"""
|
||||||
from freqtrade.data.history import get_datahandler
|
from freqtrade.data.history import get_datahandler
|
||||||
src = get_datahandler(config['datadir'], convert_from)
|
|
||||||
trg = get_datahandler(config['datadir'], convert_to)
|
src = get_datahandler(config["datadir"], convert_from)
|
||||||
timeframes = config.get('timeframes', [config.get('timeframe')])
|
trg = get_datahandler(config["datadir"], convert_to)
|
||||||
|
timeframes = config.get("timeframes", [config.get("timeframe")])
|
||||||
logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}")
|
logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}")
|
||||||
|
|
||||||
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', [
|
candle_types = [
|
||||||
c.value for c in CandleType])]
|
CandleType.from_string(ct)
|
||||||
|
for ct in config.get("candle_types", [c.value for c in CandleType])
|
||||||
|
]
|
||||||
logger.info(candle_types)
|
logger.info(candle_types)
|
||||||
paircombs = src.ohlcv_get_available_data(config['datadir'], TradingMode.SPOT)
|
paircombs = src.ohlcv_get_available_data(config["datadir"], TradingMode.SPOT)
|
||||||
paircombs.extend(src.ohlcv_get_available_data(config['datadir'], TradingMode.FUTURES))
|
paircombs.extend(src.ohlcv_get_available_data(config["datadir"], TradingMode.FUTURES))
|
||||||
|
|
||||||
if 'pairs' in config:
|
if "pairs" in config:
|
||||||
# Filter pairs
|
# Filter pairs
|
||||||
paircombs = [comb for comb in paircombs if comb[0] in config['pairs']]
|
paircombs = [comb for comb in paircombs if comb[0] in config["pairs"]]
|
||||||
|
|
||||||
if 'timeframes' in config:
|
if "timeframes" in config:
|
||||||
paircombs = [comb for comb in paircombs if comb[1] in config['timeframes']]
|
paircombs = [comb for comb in paircombs if comb[1] in config["timeframes"]]
|
||||||
paircombs = [comb for comb in paircombs if comb[2] in candle_types]
|
paircombs = [comb for comb in paircombs if comb[2] in candle_types]
|
||||||
|
|
||||||
paircombs = sorted(paircombs, key=lambda x: (x[0], x[1], x[2].value))
|
paircombs = sorted(paircombs, key=lambda x: (x[0], x[1], x[2].value))
|
||||||
|
|
||||||
formatted_paircombs = '\n'.join([f"{pair}, {timeframe}, {candle_type}"
|
formatted_paircombs = "\n".join(
|
||||||
for pair, timeframe, candle_type in paircombs])
|
[f"{pair}, {timeframe}, {candle_type}" for pair, timeframe, candle_type in paircombs]
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(f"Converting candle (OHLCV) data for the following pair combinations:\n"
|
logger.info(
|
||||||
f"{formatted_paircombs}")
|
f"Converting candle (OHLCV) data for the following pair combinations:\n"
|
||||||
|
f"{formatted_paircombs}"
|
||||||
|
)
|
||||||
for pair, timeframe, candle_type in paircombs:
|
for pair, timeframe, candle_type in paircombs:
|
||||||
data = src.ohlcv_load(pair=pair, timeframe=timeframe,
|
data = src.ohlcv_load(
|
||||||
timerange=None,
|
pair=pair,
|
||||||
fill_missing=False,
|
timeframe=timeframe,
|
||||||
drop_incomplete=False,
|
timerange=None,
|
||||||
startup_candles=0,
|
fill_missing=False,
|
||||||
candle_type=candle_type)
|
drop_incomplete=False,
|
||||||
|
startup_candles=0,
|
||||||
|
candle_type=candle_type,
|
||||||
|
)
|
||||||
logger.info(f"Converting {len(data)} {timeframe} {candle_type} candles for {pair}")
|
logger.info(f"Converting {len(data)} {timeframe} {candle_type} candles for {pair}")
|
||||||
if len(data) > 0:
|
if len(data) > 0:
|
||||||
trg.ohlcv_store(
|
trg.ohlcv_store(pair=pair, timeframe=timeframe, data=data, candle_type=candle_type)
|
||||||
pair=pair,
|
|
||||||
timeframe=timeframe,
|
|
||||||
data=data,
|
|
||||||
candle_type=candle_type
|
|
||||||
)
|
|
||||||
if erase and convert_from != convert_to:
|
if erase and convert_from != convert_to:
|
||||||
logger.info(f"Deleting source data for {pair} / {timeframe}")
|
logger.info(f"Deleting source data for {pair} / {timeframe}")
|
||||||
src.ohlcv_purge(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
src.ohlcv_purge(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||||
@@ -254,12 +285,11 @@ def reduce_dataframe_footprint(df: DataFrame) -> DataFrame:
|
|||||||
:return: Dataframe converted to float/int 32s
|
:return: Dataframe converted to float/int 32s
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.debug(f"Memory usage of dataframe is "
|
logger.debug(f"Memory usage of dataframe is {df.memory_usage().sum() / 1024**2:.2f} MB")
|
||||||
f"{df.memory_usage().sum() / 1024**2:.2f} MB")
|
|
||||||
|
|
||||||
df_dtypes = df.dtypes
|
df_dtypes = df.dtypes
|
||||||
for column, dtype in df_dtypes.items():
|
for column, dtype in df_dtypes.items():
|
||||||
if column in ['open', 'high', 'low', 'close', 'volume']:
|
if column in ["open", "high", "low", "close", "volume"]:
|
||||||
continue
|
continue
|
||||||
if dtype == np.float64:
|
if dtype == np.float64:
|
||||||
df_dtypes[column] = np.float32
|
df_dtypes[column] = np.float32
|
||||||
@@ -267,7 +297,6 @@ def reduce_dataframe_footprint(df: DataFrame) -> DataFrame:
|
|||||||
df_dtypes[column] = np.int32
|
df_dtypes[column] = np.int32
|
||||||
df = df.astype(df_dtypes)
|
df = df.astype(df_dtypes)
|
||||||
|
|
||||||
logger.debug(f"Memory usage after optimization is: "
|
logger.debug(f"Memory usage after optimization is: {df.memory_usage().sum() / 1024**2:.2f} MB")
|
||||||
f"{df.memory_usage().sum() / 1024**2:.2f} MB")
|
|
||||||
|
|
||||||
return df
|
return df
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Functions to convert data from one format to another
|
Functions to convert data from one format to another
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
@@ -9,8 +10,13 @@ import pandas as pd
|
|||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TRADES_DTYPES,
|
from freqtrade.constants import (
|
||||||
Config, TradeList)
|
DEFAULT_DATAFRAME_COLUMNS,
|
||||||
|
DEFAULT_TRADES_COLUMNS,
|
||||||
|
TRADES_DTYPES,
|
||||||
|
Config,
|
||||||
|
TradeList,
|
||||||
|
)
|
||||||
from freqtrade.enums import CandleType, TradingMode
|
from freqtrade.enums import CandleType, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
@@ -25,7 +31,7 @@ def trades_df_remove_duplicates(trades: pd.DataFrame) -> pd.DataFrame:
|
|||||||
:param trades: DataFrame with the columns constants.DEFAULT_TRADES_COLUMNS
|
:param trades: DataFrame with the columns constants.DEFAULT_TRADES_COLUMNS
|
||||||
:return: DataFrame with duplicates removed based on the 'timestamp' column
|
:return: DataFrame with duplicates removed based on the 'timestamp' column
|
||||||
"""
|
"""
|
||||||
return trades.drop_duplicates(subset=['timestamp', 'id'])
|
return trades.drop_duplicates(subset=["timestamp", "id"])
|
||||||
|
|
||||||
|
|
||||||
def trades_dict_to_list(trades: List[Dict]) -> TradeList:
|
def trades_dict_to_list(trades: List[Dict]) -> TradeList:
|
||||||
@@ -42,7 +48,7 @@ def trades_convert_types(trades: DataFrame) -> DataFrame:
|
|||||||
Convert Trades dtypes and add 'date' column
|
Convert Trades dtypes and add 'date' column
|
||||||
"""
|
"""
|
||||||
trades = trades.astype(TRADES_DTYPES)
|
trades = trades.astype(TRADES_DTYPES)
|
||||||
trades['date'] = to_datetime(trades['timestamp'], unit='ms', utc=True)
|
trades["date"] = to_datetime(trades["timestamp"], unit="ms", utc=True)
|
||||||
return trades
|
return trades
|
||||||
|
|
||||||
|
|
||||||
@@ -71,13 +77,14 @@ def trades_to_ohlcv(trades: DataFrame, timeframe: str) -> DataFrame:
|
|||||||
:raises: ValueError if no trades are provided
|
:raises: ValueError if no trades are provided
|
||||||
"""
|
"""
|
||||||
from freqtrade.exchange import timeframe_to_resample_freq
|
from freqtrade.exchange import timeframe_to_resample_freq
|
||||||
|
|
||||||
if trades.empty:
|
if trades.empty:
|
||||||
raise ValueError('Trade-list empty.')
|
raise ValueError("Trade-list empty.")
|
||||||
df = trades.set_index('date', drop=True)
|
df = trades.set_index("date", drop=True)
|
||||||
resample_interval = timeframe_to_resample_freq(timeframe)
|
resample_interval = timeframe_to_resample_freq(timeframe)
|
||||||
df_new = df['price'].resample(resample_interval).ohlc()
|
df_new = df["price"].resample(resample_interval).ohlc()
|
||||||
df_new['volume'] = df['amount'].resample(resample_interval).sum()
|
df_new["volume"] = df["amount"].resample(resample_interval).sum()
|
||||||
df_new['date'] = df_new.index
|
df_new["date"] = df_new.index
|
||||||
# Drop 0 volume rows
|
# Drop 0 volume rows
|
||||||
df_new = df_new.dropna()
|
df_new = df_new.dropna()
|
||||||
return df_new.loc[:, DEFAULT_DATAFRAME_COLUMNS]
|
return df_new.loc[:, DEFAULT_DATAFRAME_COLUMNS]
|
||||||
@@ -97,24 +104,27 @@ def convert_trades_to_ohlcv(
|
|||||||
Convert stored trades data to ohlcv data
|
Convert stored trades data to ohlcv data
|
||||||
"""
|
"""
|
||||||
from freqtrade.data.history import get_datahandler
|
from freqtrade.data.history import get_datahandler
|
||||||
|
|
||||||
data_handler_trades = get_datahandler(datadir, data_format=data_format_trades)
|
data_handler_trades = get_datahandler(datadir, data_format=data_format_trades)
|
||||||
data_handler_ohlcv = get_datahandler(datadir, data_format=data_format_ohlcv)
|
data_handler_ohlcv = get_datahandler(datadir, data_format=data_format_ohlcv)
|
||||||
|
|
||||||
logger.info(f"About to convert pairs: '{', '.join(pairs)}', "
|
logger.info(
|
||||||
f"intervals: '{', '.join(timeframes)}' to {datadir}")
|
f"About to convert pairs: '{', '.join(pairs)}', "
|
||||||
|
f"intervals: '{', '.join(timeframes)}' to {datadir}"
|
||||||
|
)
|
||||||
trading_mode = TradingMode.FUTURES if candle_type != CandleType.SPOT else TradingMode.SPOT
|
trading_mode = TradingMode.FUTURES if candle_type != CandleType.SPOT else TradingMode.SPOT
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
trades = data_handler_trades.trades_load(pair, trading_mode)
|
trades = data_handler_trades.trades_load(pair, trading_mode)
|
||||||
for timeframe in timeframes:
|
for timeframe in timeframes:
|
||||||
if erase:
|
if erase:
|
||||||
if data_handler_ohlcv.ohlcv_purge(pair, timeframe, candle_type=candle_type):
|
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, candle_type=candle_type)
|
data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv, candle_type=candle_type)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.warning(f'Could not convert {pair} to OHLCV.')
|
logger.warning(f"Could not convert {pair} to OHLCV.")
|
||||||
|
|
||||||
|
|
||||||
def convert_trades_format(config: Config, convert_from: str, convert_to: str, erase: bool):
|
def convert_trades_format(config: Config, convert_from: str, convert_to: str, erase: bool):
|
||||||
@@ -125,25 +135,27 @@ def convert_trades_format(config: Config, convert_from: str, convert_to: str, er
|
|||||||
: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)
|
||||||
"""
|
"""
|
||||||
if convert_from == 'kraken_csv':
|
if convert_from == "kraken_csv":
|
||||||
if config['exchange']['name'] != 'kraken':
|
if config["exchange"]["name"] != "kraken":
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'Converting from csv is only supported for kraken.'
|
"Converting from csv is only supported for kraken."
|
||||||
'Please refer to the documentation for details about this special mode.'
|
"Please refer to the documentation for details about this special mode."
|
||||||
)
|
)
|
||||||
from freqtrade.data.converter.trade_converter_kraken import import_kraken_trades_from_csv
|
from freqtrade.data.converter.trade_converter_kraken import import_kraken_trades_from_csv
|
||||||
|
|
||||||
import_kraken_trades_from_csv(config, convert_to)
|
import_kraken_trades_from_csv(config, convert_to)
|
||||||
return
|
return
|
||||||
|
|
||||||
from freqtrade.data.history import get_datahandler
|
from freqtrade.data.history import get_datahandler
|
||||||
src = get_datahandler(config['datadir'], convert_from)
|
|
||||||
trg = get_datahandler(config['datadir'], convert_to)
|
|
||||||
|
|
||||||
if 'pairs' not in config:
|
src = get_datahandler(config["datadir"], convert_from)
|
||||||
config['pairs'] = src.trades_get_pairs(config['datadir'])
|
trg = get_datahandler(config["datadir"], convert_to)
|
||||||
|
|
||||||
|
if "pairs" not in config:
|
||||||
|
config["pairs"] = src.trades_get_pairs(config["datadir"])
|
||||||
logger.info(f"Converting trades for {config['pairs']}")
|
logger.info(f"Converting trades for {config['pairs']}")
|
||||||
trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
|
trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT)
|
||||||
for pair in config['pairs']:
|
for pair in config["pairs"]:
|
||||||
data = src.trades_load(pair, trading_mode)
|
data = src.trades_load(pair, trading_mode)
|
||||||
logger.info(f"Converting {len(data)} trades for {pair}")
|
logger.info(f"Converting {len(data)} trades for {pair}")
|
||||||
trg.trades_store(pair, data, trading_mode)
|
trg.trades_store(pair, data, trading_mode)
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ from pathlib import Path
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_TRADES_COLUMNS, Config
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_TRADES_COLUMNS, Config
|
||||||
from freqtrade.data.converter.trade_converter import (trades_convert_types,
|
from freqtrade.data.converter.trade_converter import (
|
||||||
trades_df_remove_duplicates)
|
trades_convert_types,
|
||||||
|
trades_df_remove_duplicates,
|
||||||
|
)
|
||||||
from freqtrade.data.history import get_datahandler
|
from freqtrade.data.history import get_datahandler
|
||||||
from freqtrade.enums import TradingMode
|
from freqtrade.enums import TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
@@ -15,32 +17,33 @@ from freqtrade.resolvers import ExchangeResolver
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
KRAKEN_CSV_TRADE_COLUMNS = ['timestamp', 'price', 'amount']
|
KRAKEN_CSV_TRADE_COLUMNS = ["timestamp", "price", "amount"]
|
||||||
|
|
||||||
|
|
||||||
def import_kraken_trades_from_csv(config: Config, convert_to: str):
|
def import_kraken_trades_from_csv(config: Config, convert_to: str):
|
||||||
"""
|
"""
|
||||||
Import kraken trades from csv
|
Import kraken trades from csv
|
||||||
"""
|
"""
|
||||||
if config['exchange']['name'] != 'kraken':
|
if config["exchange"]["name"] != "kraken":
|
||||||
raise OperationalException('This function is only for the kraken exchange.')
|
raise OperationalException("This function is only for the kraken exchange.")
|
||||||
|
|
||||||
datadir: Path = config['datadir']
|
datadir: Path = config["datadir"]
|
||||||
data_handler = get_datahandler(datadir, data_format=convert_to)
|
data_handler = get_datahandler(datadir, data_format=convert_to)
|
||||||
|
|
||||||
tradesdir: Path = config['datadir'] / 'trades_csv'
|
tradesdir: Path = config["datadir"] / "trades_csv"
|
||||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
# iterate through directories in this directory
|
# iterate through directories in this directory
|
||||||
data_symbols = {p.stem for p in tradesdir.rglob('*.csv')}
|
data_symbols = {p.stem for p in tradesdir.rglob("*.csv")}
|
||||||
|
|
||||||
# create pair/filename mapping
|
# create pair/filename mapping
|
||||||
markets = {
|
markets = {
|
||||||
(m['symbol'], m['altname']) for m in exchange.markets.values()
|
(m["symbol"], m["altname"])
|
||||||
if m.get('altname') in data_symbols
|
for m in exchange.markets.values()
|
||||||
|
if m.get("altname") in data_symbols
|
||||||
}
|
}
|
||||||
logger.info(f"Found csv files for {', '.join(data_symbols)}.")
|
logger.info(f"Found csv files for {', '.join(data_symbols)}.")
|
||||||
|
|
||||||
if pairs_raw := config.get('pairs'):
|
if pairs_raw := config.get("pairs"):
|
||||||
pairs = expand_pairlist(pairs_raw, [m[0] for m in markets])
|
pairs = expand_pairlist(pairs_raw, [m[0] for m in markets])
|
||||||
markets = {m for m in markets if m[0] in pairs}
|
markets = {m for m in markets if m[0] in pairs}
|
||||||
if not markets:
|
if not markets:
|
||||||
@@ -66,18 +69,20 @@ def import_kraken_trades_from_csv(config: Config, convert_to: str):
|
|||||||
trades = pd.concat(dfs, ignore_index=True)
|
trades = pd.concat(dfs, ignore_index=True)
|
||||||
del dfs
|
del dfs
|
||||||
|
|
||||||
trades.loc[:, 'timestamp'] = trades['timestamp'] * 1e3
|
trades.loc[:, "timestamp"] = trades["timestamp"] * 1e3
|
||||||
trades.loc[:, 'cost'] = trades['price'] * trades['amount']
|
trades.loc[:, "cost"] = trades["price"] * trades["amount"]
|
||||||
for col in DEFAULT_TRADES_COLUMNS:
|
for col in DEFAULT_TRADES_COLUMNS:
|
||||||
if col not in trades.columns:
|
if col not in trades.columns:
|
||||||
trades.loc[:, col] = ''
|
trades.loc[:, col] = ""
|
||||||
trades = trades[DEFAULT_TRADES_COLUMNS]
|
trades = trades[DEFAULT_TRADES_COLUMNS]
|
||||||
trades = trades_convert_types(trades)
|
trades = trades_convert_types(trades)
|
||||||
|
|
||||||
trades_df = trades_df_remove_duplicates(trades)
|
trades_df = trades_df_remove_duplicates(trades)
|
||||||
del trades
|
del trades
|
||||||
logger.info(f"{pair}: {len(trades_df)} trades, from "
|
logger.info(
|
||||||
f"{trades_df['date'].min():{DATETIME_PRINT_FORMAT}} to "
|
f"{pair}: {len(trades_df)} trades, from "
|
||||||
f"{trades_df['date'].max():{DATETIME_PRINT_FORMAT}}")
|
f"{trades_df['date'].min():{DATETIME_PRINT_FORMAT}} to "
|
||||||
|
f"{trades_df['date'].max():{DATETIME_PRINT_FORMAT}}"
|
||||||
|
)
|
||||||
|
|
||||||
data_handler.trades_store(pair, trades_df, TradingMode.SPOT)
|
data_handler.trades_store(pair, trades_df, TradingMode.SPOT)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Responsible to provide data to the bot
|
|||||||
including ticker and orderbook data, live and historical candle (OHLCV) data
|
including ticker and orderbook data, live and historical candle (OHLCV) data
|
||||||
Common Interface for bot and strategy to access data.
|
Common Interface for bot and strategy to access data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -12,8 +13,12 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
|
from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWithTimeframes,
|
from freqtrade.constants import (
|
||||||
PairWithTimeframe)
|
FULL_DATAFRAME_THRESHOLD,
|
||||||
|
Config,
|
||||||
|
ListPairsWithTimeframes,
|
||||||
|
PairWithTimeframe,
|
||||||
|
)
|
||||||
from freqtrade.data.history import get_datahandler, load_pair_history
|
from freqtrade.data.history import get_datahandler, load_pair_history
|
||||||
from freqtrade.enums import CandleType, RPCMessageType, RunMode, TradingMode
|
from freqtrade.enums import CandleType, RPCMessageType, RunMode, TradingMode
|
||||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||||
@@ -27,18 +32,17 @@ from freqtrade.util import PeriodicCache
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
NO_EXCHANGE_EXCEPTION = 'Exchange is not available to DataProvider.'
|
NO_EXCHANGE_EXCEPTION = "Exchange is not available to DataProvider."
|
||||||
MAX_DATAFRAME_CANDLES = 1000
|
MAX_DATAFRAME_CANDLES = 1000
|
||||||
|
|
||||||
|
|
||||||
class DataProvider:
|
class DataProvider:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: Config,
|
config: Config,
|
||||||
exchange: Optional[Exchange],
|
exchange: Optional[Exchange],
|
||||||
pairlists=None,
|
pairlists=None,
|
||||||
rpc: Optional[RPCManager] = None
|
rpc: Optional[RPCManager] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._config = config
|
self._config = config
|
||||||
self._exchange = exchange
|
self._exchange = exchange
|
||||||
@@ -49,18 +53,20 @@ class DataProvider:
|
|||||||
self.__slice_date: Optional[datetime] = None
|
self.__slice_date: Optional[datetime] = None
|
||||||
|
|
||||||
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
||||||
self.__producer_pairs_df: Dict[str,
|
self.__producer_pairs_df: Dict[
|
||||||
Dict[PairWithTimeframe, Tuple[DataFrame, datetime]]] = {}
|
str, Dict[PairWithTimeframe, Tuple[DataFrame, datetime]]
|
||||||
|
] = {}
|
||||||
self.__producer_pairs: Dict[str, List[str]] = {}
|
self.__producer_pairs: Dict[str, List[str]] = {}
|
||||||
self._msg_queue: deque = deque()
|
self._msg_queue: deque = deque()
|
||||||
|
|
||||||
self._default_candle_type = self._config.get('candle_type_def', CandleType.SPOT)
|
self._default_candle_type = self._config.get("candle_type_def", CandleType.SPOT)
|
||||||
self._default_timeframe = self._config.get('timeframe', '1h')
|
self._default_timeframe = self._config.get("timeframe", "1h")
|
||||||
|
|
||||||
self.__msg_cache = PeriodicCache(
|
self.__msg_cache = PeriodicCache(
|
||||||
maxsize=1000, ttl=timeframe_to_seconds(self._default_timeframe))
|
maxsize=1000, ttl=timeframe_to_seconds(self._default_timeframe)
|
||||||
|
)
|
||||||
|
|
||||||
self.producers = self._config.get('external_message_consumer', {}).get('producers', [])
|
self.producers = self._config.get("external_message_consumer", {}).get("producers", [])
|
||||||
self.external_data_enabled = len(self.producers) > 0
|
self.external_data_enabled = len(self.producers) > 0
|
||||||
|
|
||||||
def _set_dataframe_max_index(self, limit_index: int):
|
def _set_dataframe_max_index(self, limit_index: int):
|
||||||
@@ -80,11 +86,7 @@ class DataProvider:
|
|||||||
self.__slice_date = limit_date
|
self.__slice_date = limit_date
|
||||||
|
|
||||||
def _set_cached_df(
|
def _set_cached_df(
|
||||||
self,
|
self, pair: str, timeframe: str, dataframe: DataFrame, candle_type: CandleType
|
||||||
pair: str,
|
|
||||||
timeframe: str,
|
|
||||||
dataframe: DataFrame,
|
|
||||||
candle_type: CandleType
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Store cached Dataframe.
|
Store cached Dataframe.
|
||||||
@@ -96,8 +98,7 @@ class DataProvider:
|
|||||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
"""
|
"""
|
||||||
pair_key = (pair, timeframe, candle_type)
|
pair_key = (pair, timeframe, candle_type)
|
||||||
self.__cached_pairs[pair_key] = (
|
self.__cached_pairs[pair_key] = (dataframe, datetime.now(timezone.utc))
|
||||||
dataframe, datetime.now(timezone.utc))
|
|
||||||
|
|
||||||
# For multiple producers we will want to merge the pairlists instead of overwriting
|
# For multiple producers we will want to merge the pairlists instead of overwriting
|
||||||
def _set_producer_pairs(self, pairlist: List[str], producer_name: str = "default"):
|
def _set_producer_pairs(self, pairlist: List[str], producer_name: str = "default"):
|
||||||
@@ -116,12 +117,7 @@ class DataProvider:
|
|||||||
"""
|
"""
|
||||||
return self.__producer_pairs.get(producer_name, []).copy()
|
return self.__producer_pairs.get(producer_name, []).copy()
|
||||||
|
|
||||||
def _emit_df(
|
def _emit_df(self, pair_key: PairWithTimeframe, dataframe: DataFrame, new_candle: bool) -> None:
|
||||||
self,
|
|
||||||
pair_key: PairWithTimeframe,
|
|
||||||
dataframe: DataFrame,
|
|
||||||
new_candle: bool
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Send this dataframe as an ANALYZED_DF message to RPC
|
Send this dataframe as an ANALYZED_DF message to RPC
|
||||||
|
|
||||||
@@ -131,19 +127,21 @@ class DataProvider:
|
|||||||
"""
|
"""
|
||||||
if self.__rpc:
|
if self.__rpc:
|
||||||
msg: RPCAnalyzedDFMsg = {
|
msg: RPCAnalyzedDFMsg = {
|
||||||
'type': RPCMessageType.ANALYZED_DF,
|
"type": RPCMessageType.ANALYZED_DF,
|
||||||
'data': {
|
"data": {
|
||||||
'key': pair_key,
|
"key": pair_key,
|
||||||
'df': dataframe.tail(1),
|
"df": dataframe.tail(1),
|
||||||
'la': datetime.now(timezone.utc)
|
"la": datetime.now(timezone.utc),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
self.__rpc.send_msg(msg)
|
self.__rpc.send_msg(msg)
|
||||||
if new_candle:
|
if new_candle:
|
||||||
self.__rpc.send_msg({
|
self.__rpc.send_msg(
|
||||||
'type': RPCMessageType.NEW_CANDLE,
|
{
|
||||||
'data': pair_key,
|
"type": RPCMessageType.NEW_CANDLE,
|
||||||
})
|
"data": pair_key,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def _replace_external_df(
|
def _replace_external_df(
|
||||||
self,
|
self,
|
||||||
@@ -152,7 +150,7 @@ class DataProvider:
|
|||||||
last_analyzed: datetime,
|
last_analyzed: datetime,
|
||||||
timeframe: str,
|
timeframe: str,
|
||||||
candle_type: CandleType,
|
candle_type: CandleType,
|
||||||
producer_name: str = "default"
|
producer_name: str = "default",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add the pair data to this class from an external source.
|
Add the pair data to this class from an external source.
|
||||||
@@ -178,7 +176,7 @@ class DataProvider:
|
|||||||
last_analyzed: datetime,
|
last_analyzed: datetime,
|
||||||
timeframe: str,
|
timeframe: str,
|
||||||
candle_type: CandleType,
|
candle_type: CandleType,
|
||||||
producer_name: str = "default"
|
producer_name: str = "default",
|
||||||
) -> Tuple[bool, int]:
|
) -> Tuple[bool, int]:
|
||||||
"""
|
"""
|
||||||
Append a candle to the existing external dataframe. The incoming dataframe
|
Append a candle to the existing external dataframe. The incoming dataframe
|
||||||
@@ -204,12 +202,14 @@ class DataProvider:
|
|||||||
last_analyzed=last_analyzed,
|
last_analyzed=last_analyzed,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
candle_type=candle_type,
|
candle_type=candle_type,
|
||||||
producer_name=producer_name
|
producer_name=producer_name,
|
||||||
)
|
)
|
||||||
return (True, 0)
|
return (True, 0)
|
||||||
|
|
||||||
if (producer_name not in self.__producer_pairs_df
|
if (
|
||||||
or pair_key not in self.__producer_pairs_df[producer_name]):
|
producer_name not in self.__producer_pairs_df
|
||||||
|
or pair_key not in self.__producer_pairs_df[producer_name]
|
||||||
|
):
|
||||||
# We don't have data from this producer yet,
|
# We don't have data from this producer yet,
|
||||||
# or we don't have data for this pair_key
|
# or we don't have data for this pair_key
|
||||||
# return False and 1000 for the full df
|
# return False and 1000 for the full df
|
||||||
@@ -220,12 +220,12 @@ class DataProvider:
|
|||||||
# CHECK FOR MISSING CANDLES
|
# CHECK FOR MISSING CANDLES
|
||||||
# Convert the timeframe to a timedelta for pandas
|
# Convert the timeframe to a timedelta for pandas
|
||||||
timeframe_delta: Timedelta = to_timedelta(timeframe)
|
timeframe_delta: Timedelta = to_timedelta(timeframe)
|
||||||
local_last: Timestamp = existing_df.iloc[-1]['date'] # We want the last date from our copy
|
local_last: Timestamp = existing_df.iloc[-1]["date"] # We want the last date from our copy
|
||||||
# We want the first date from the incoming
|
# We want the first date from the incoming
|
||||||
incoming_first: Timestamp = dataframe.iloc[0]['date']
|
incoming_first: Timestamp = dataframe.iloc[0]["date"]
|
||||||
|
|
||||||
# Remove existing candles that are newer than the incoming first candle
|
# Remove existing candles that are newer than the incoming first candle
|
||||||
existing_df1 = existing_df[existing_df['date'] < incoming_first]
|
existing_df1 = existing_df[existing_df["date"] < incoming_first]
|
||||||
|
|
||||||
candle_difference = (incoming_first - local_last) / timeframe_delta
|
candle_difference = (incoming_first - local_last) / timeframe_delta
|
||||||
|
|
||||||
@@ -243,13 +243,13 @@ class DataProvider:
|
|||||||
|
|
||||||
# Everything is good, we appended
|
# Everything is good, we appended
|
||||||
self._replace_external_df(
|
self._replace_external_df(
|
||||||
pair,
|
pair,
|
||||||
appended_df,
|
appended_df,
|
||||||
last_analyzed=last_analyzed,
|
last_analyzed=last_analyzed,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
candle_type=candle_type,
|
candle_type=candle_type,
|
||||||
producer_name=producer_name
|
producer_name=producer_name,
|
||||||
)
|
)
|
||||||
return (True, 0)
|
return (True, 0)
|
||||||
|
|
||||||
def get_producer_df(
|
def get_producer_df(
|
||||||
@@ -257,7 +257,7 @@ class DataProvider:
|
|||||||
pair: str,
|
pair: str,
|
||||||
timeframe: Optional[str] = None,
|
timeframe: Optional[str] = None,
|
||||||
candle_type: Optional[CandleType] = None,
|
candle_type: Optional[CandleType] = None,
|
||||||
producer_name: str = "default"
|
producer_name: str = "default",
|
||||||
) -> Tuple[DataFrame, datetime]:
|
) -> Tuple[DataFrame, datetime]:
|
||||||
"""
|
"""
|
||||||
Get the pair data from producers.
|
Get the pair data from producers.
|
||||||
@@ -292,64 +292,64 @@ class DataProvider:
|
|||||||
"""
|
"""
|
||||||
self._pairlists = pairlists
|
self._pairlists = pairlists
|
||||||
|
|
||||||
def historic_ohlcv(
|
def historic_ohlcv(self, pair: str, timeframe: str, candle_type: str = "") -> DataFrame:
|
||||||
self,
|
|
||||||
pair: str,
|
|
||||||
timeframe: str,
|
|
||||||
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
|
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
||||||
"""
|
"""
|
||||||
_candle_type = CandleType.from_string(
|
_candle_type = (
|
||||||
candle_type) if candle_type != '' else self._config['candle_type_def']
|
CandleType.from_string(candle_type)
|
||||||
|
if candle_type != ""
|
||||||
|
else self._config["candle_type_def"]
|
||||||
|
)
|
||||||
saved_pair: PairWithTimeframe = (pair, str(timeframe), _candle_type)
|
saved_pair: PairWithTimeframe = (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(
|
||||||
'timerange') is None else str(self._config.get('timerange')))
|
None
|
||||||
|
if self._config.get("timerange") is None
|
||||||
|
else str(self._config.get("timerange"))
|
||||||
|
)
|
||||||
|
|
||||||
startup_candles = self.get_required_startup(str(timeframe))
|
startup_candles = self.get_required_startup(str(timeframe))
|
||||||
tf_seconds = timeframe_to_seconds(str(timeframe))
|
tf_seconds = timeframe_to_seconds(str(timeframe))
|
||||||
timerange.subtract_start(tf_seconds * startup_candles)
|
timerange.subtract_start(tf_seconds * startup_candles)
|
||||||
|
|
||||||
logger.info(f"Loading data for {pair} {timeframe} "
|
logger.info(
|
||||||
f"from {timerange.start_fmt} to {timerange.stop_fmt}")
|
f"Loading data for {pair} {timeframe} "
|
||||||
|
f"from {timerange.start_fmt} to {timerange.stop_fmt}"
|
||||||
|
)
|
||||||
|
|
||||||
self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
|
self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
datadir=self._config['datadir'],
|
datadir=self._config["datadir"],
|
||||||
timerange=timerange,
|
timerange=timerange,
|
||||||
data_format=self._config['dataformat_ohlcv'],
|
data_format=self._config["dataformat_ohlcv"],
|
||||||
candle_type=_candle_type,
|
candle_type=_candle_type,
|
||||||
|
|
||||||
)
|
)
|
||||||
return self.__cached_pairs_backtesting[saved_pair].copy()
|
return self.__cached_pairs_backtesting[saved_pair].copy()
|
||||||
|
|
||||||
def get_required_startup(self, timeframe: str) -> int:
|
def get_required_startup(self, timeframe: str) -> int:
|
||||||
freqai_config = self._config.get('freqai', {})
|
freqai_config = self._config.get("freqai", {})
|
||||||
if not freqai_config.get('enabled', False):
|
if not freqai_config.get("enabled", False):
|
||||||
return self._config.get('startup_candle_count', 0)
|
return self._config.get("startup_candle_count", 0)
|
||||||
else:
|
else:
|
||||||
startup_candles = self._config.get('startup_candle_count', 0)
|
startup_candles = self._config.get("startup_candle_count", 0)
|
||||||
indicator_periods = freqai_config['feature_parameters']['indicator_periods_candles']
|
indicator_periods = freqai_config["feature_parameters"]["indicator_periods_candles"]
|
||||||
# make sure the startupcandles is at least the set maximum indicator periods
|
# make sure the startupcandles is at least the set maximum indicator periods
|
||||||
self._config['startup_candle_count'] = max(startup_candles, max(indicator_periods))
|
self._config["startup_candle_count"] = max(startup_candles, max(indicator_periods))
|
||||||
tf_seconds = timeframe_to_seconds(timeframe)
|
tf_seconds = timeframe_to_seconds(timeframe)
|
||||||
train_candles = freqai_config['train_period_days'] * 86400 / tf_seconds
|
train_candles = freqai_config["train_period_days"] * 86400 / tf_seconds
|
||||||
total_candles = int(self._config['startup_candle_count'] + train_candles)
|
total_candles = int(self._config["startup_candle_count"] + train_candles)
|
||||||
logger.info(
|
logger.info(
|
||||||
f'Increasing startup_candle_count for freqai on {timeframe} to {total_candles}')
|
f"Increasing startup_candle_count for freqai on {timeframe} to {total_candles}"
|
||||||
|
)
|
||||||
return total_candles
|
return total_candles
|
||||||
|
|
||||||
def get_pair_dataframe(
|
def get_pair_dataframe(
|
||||||
self,
|
self, pair: str, timeframe: Optional[str] = None, candle_type: str = ""
|
||||||
pair: str,
|
|
||||||
timeframe: Optional[str] = None,
|
|
||||||
candle_type: str = ''
|
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Return pair candle (OHLCV) data, either live or cached historical -- depending
|
Return pair candle (OHLCV) data, either live or cached historical -- depending
|
||||||
@@ -366,13 +366,13 @@ class DataProvider:
|
|||||||
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
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).
|
||||||
timeframe = timeframe or self._config['timeframe']
|
timeframe = timeframe or self._config["timeframe"]
|
||||||
data = self.historic_ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
data = self.historic_ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||||
# Cut date to timeframe-specific date.
|
# Cut date to timeframe-specific date.
|
||||||
# This is necessary to prevent lookahead bias in callbacks through informative pairs.
|
# This is necessary to prevent lookahead bias in callbacks through informative pairs.
|
||||||
if self.__slice_date:
|
if self.__slice_date:
|
||||||
cutoff_date = timeframe_to_prev_date(timeframe, self.__slice_date)
|
cutoff_date = timeframe_to_prev_date(timeframe, self.__slice_date)
|
||||||
data = data.loc[data['date'] < cutoff_date]
|
data = data.loc[data["date"] < cutoff_date]
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).")
|
logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).")
|
||||||
return data
|
return data
|
||||||
@@ -387,7 +387,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, self._config.get('candle_type_def', CandleType.SPOT))
|
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]
|
||||||
@@ -395,7 +395,7 @@ class DataProvider:
|
|||||||
df, date = self.__cached_pairs[pair_key]
|
df, date = self.__cached_pairs[pair_key]
|
||||||
if self.__slice_index is not None:
|
if self.__slice_index is not None:
|
||||||
max_index = self.__slice_index
|
max_index = self.__slice_index
|
||||||
df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES):max_index]
|
df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES) : max_index]
|
||||||
return df, date
|
return df, date
|
||||||
else:
|
else:
|
||||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
||||||
@@ -406,7 +406,7 @@ class DataProvider:
|
|||||||
Get runmode of the bot
|
Get runmode of the bot
|
||||||
can be "live", "dry-run", "backtest", "edgecli", "hyperopt" or "other".
|
can be "live", "dry-run", "backtest", "edgecli", "hyperopt" or "other".
|
||||||
"""
|
"""
|
||||||
return RunMode(self._config.get('runmode', RunMode.OTHER))
|
return RunMode(self._config.get("runmode", RunMode.OTHER))
|
||||||
|
|
||||||
def current_whitelist(self) -> List[str]:
|
def current_whitelist(self) -> List[str]:
|
||||||
"""
|
"""
|
||||||
@@ -434,9 +434,11 @@ class DataProvider:
|
|||||||
|
|
||||||
# Exchange functions
|
# Exchange functions
|
||||||
|
|
||||||
def refresh(self,
|
def refresh(
|
||||||
pairlist: ListPairsWithTimeframes,
|
self,
|
||||||
helping_pairs: Optional[ListPairsWithTimeframes] = None) -> None:
|
pairlist: ListPairsWithTimeframes,
|
||||||
|
helping_pairs: Optional[ListPairsWithTimeframes] = None,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Refresh data, called with each cycle
|
Refresh data, called with each cycle
|
||||||
"""
|
"""
|
||||||
@@ -469,11 +471,7 @@ class DataProvider:
|
|||||||
return list(self._exchange._klines.keys())
|
return list(self._exchange._klines.keys())
|
||||||
|
|
||||||
def ohlcv(
|
def ohlcv(
|
||||||
self,
|
self, pair: str, timeframe: Optional[str] = None, copy: bool = True, candle_type: str = ""
|
||||||
pair: str,
|
|
||||||
timeframe: Optional[str] = None,
|
|
||||||
copy: bool = True,
|
|
||||||
candle_type: str = ''
|
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Get candle (OHLCV) data for the given pair as DataFrame
|
Get candle (OHLCV) data for the given pair as DataFrame
|
||||||
@@ -487,11 +485,13 @@ class DataProvider:
|
|||||||
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):
|
||||||
_candle_type = CandleType.from_string(
|
_candle_type = (
|
||||||
candle_type) if candle_type != '' else self._config['candle_type_def']
|
CandleType.from_string(candle_type)
|
||||||
|
if candle_type != ""
|
||||||
|
else self._config["candle_type_def"]
|
||||||
|
)
|
||||||
return self._exchange.klines(
|
return self._exchange.klines(
|
||||||
(pair, timeframe or self._config['timeframe'], _candle_type),
|
(pair, timeframe or self._config["timeframe"], _candle_type), copy=copy
|
||||||
copy=copy
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return DataFrame()
|
return DataFrame()
|
||||||
@@ -572,7 +572,7 @@ class DataProvider:
|
|||||||
Send custom RPC Notifications from your bot.
|
Send custom RPC Notifications from your bot.
|
||||||
Will not send any bot in modes other than Dry-run or Live.
|
Will not send any bot in modes other than Dry-run or Live.
|
||||||
:param message: Message to be sent. Must be below 4096.
|
:param message: Message to be sent. Must be below 4096.
|
||||||
:param always_send: If False, will send the message only once per candle, and surpress
|
:param always_send: If False, will send the message only once per candle, and suppress
|
||||||
identical messages.
|
identical messages.
|
||||||
Careful as this can end up spaming your chat.
|
Careful as this can end up spaming your chat.
|
||||||
Defaults to False
|
Defaults to False
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ from tabulate import tabulate
|
|||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data,
|
from freqtrade.data.btanalysis import (
|
||||||
load_backtest_stats)
|
get_latest_backtest_filename,
|
||||||
|
load_backtest_data,
|
||||||
|
load_backtest_stats,
|
||||||
|
)
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
|
|
||||||
@@ -18,9 +21,10 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def _load_backtest_analysis_data(backtest_dir: Path, name: str):
|
def _load_backtest_analysis_data(backtest_dir: Path, name: str):
|
||||||
if backtest_dir.is_dir():
|
if backtest_dir.is_dir():
|
||||||
scpf = Path(backtest_dir,
|
scpf = Path(
|
||||||
Path(get_latest_backtest_filename(backtest_dir)).stem + "_" + name + ".pkl"
|
backtest_dir,
|
||||||
)
|
Path(get_latest_backtest_filename(backtest_dir)).stem + "_" + name + ".pkl",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_{name}.pkl")
|
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_{name}.pkl")
|
||||||
|
|
||||||
@@ -53,7 +57,8 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand
|
|||||||
for pair in pairlist:
|
for pair in pairlist:
|
||||||
if pair in signal_candles[strategy_name]:
|
if pair in signal_candles[strategy_name]:
|
||||||
analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators(
|
analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators(
|
||||||
pair, trades, signal_candles[strategy_name][pair])
|
pair, trades, signal_candles[strategy_name][pair]
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Cannot process entry/exit reasons for {strategy_name}: ", e)
|
print(f"Cannot process entry/exit reasons for {strategy_name}: ", e)
|
||||||
|
|
||||||
@@ -64,28 +69,28 @@ def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles:
|
|||||||
buyf = signal_candles
|
buyf = signal_candles
|
||||||
|
|
||||||
if len(buyf) > 0:
|
if len(buyf) > 0:
|
||||||
buyf = buyf.set_index('date', drop=False)
|
buyf = buyf.set_index("date", drop=False)
|
||||||
trades_red = trades.loc[trades['pair'] == pair].copy()
|
trades_red = trades.loc[trades["pair"] == pair].copy()
|
||||||
|
|
||||||
trades_inds = pd.DataFrame()
|
trades_inds = pd.DataFrame()
|
||||||
|
|
||||||
if trades_red.shape[0] > 0 and buyf.shape[0] > 0:
|
if trades_red.shape[0] > 0 and buyf.shape[0] > 0:
|
||||||
for t, v in trades_red.open_date.items():
|
for t, v in trades_red.open_date.items():
|
||||||
allinds = buyf.loc[(buyf['date'] < v)]
|
allinds = buyf.loc[(buyf["date"] < v)]
|
||||||
if allinds.shape[0] > 0:
|
if allinds.shape[0] > 0:
|
||||||
tmp_inds = allinds.iloc[[-1]]
|
tmp_inds = allinds.iloc[[-1]]
|
||||||
|
|
||||||
trades_red.loc[t, 'signal_date'] = tmp_inds['date'].values[0]
|
trades_red.loc[t, "signal_date"] = tmp_inds["date"].values[0]
|
||||||
trades_red.loc[t, 'enter_reason'] = trades_red.loc[t, 'enter_tag']
|
trades_red.loc[t, "enter_reason"] = trades_red.loc[t, "enter_tag"]
|
||||||
tmp_inds.index.rename('signal_date', inplace=True)
|
tmp_inds.index.rename("signal_date", inplace=True)
|
||||||
trades_inds = pd.concat([trades_inds, tmp_inds])
|
trades_inds = pd.concat([trades_inds, tmp_inds])
|
||||||
|
|
||||||
if 'signal_date' in trades_red:
|
if "signal_date" in trades_red:
|
||||||
trades_red['signal_date'] = pd.to_datetime(trades_red['signal_date'], utc=True)
|
trades_red["signal_date"] = pd.to_datetime(trades_red["signal_date"], utc=True)
|
||||||
trades_red.set_index('signal_date', inplace=True)
|
trades_red.set_index("signal_date", inplace=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
trades_red = pd.merge(trades_red, trades_inds, on='signal_date', how='outer')
|
trades_red = pd.merge(trades_red, trades_inds, on="signal_date", how="outer")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
return trades_red
|
return trades_red
|
||||||
@@ -93,138 +98,166 @@ def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles:
|
|||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
|
||||||
def _do_group_table_output(bigdf, glist, csv_path: Path, to_csv=False, ):
|
def _do_group_table_output(
|
||||||
|
bigdf,
|
||||||
|
glist,
|
||||||
|
csv_path: Path,
|
||||||
|
to_csv=False,
|
||||||
|
):
|
||||||
for g in glist:
|
for g in glist:
|
||||||
# 0: summary wins/losses grouped by enter tag
|
# 0: summary wins/losses grouped by enter tag
|
||||||
if g == "0":
|
if g == "0":
|
||||||
group_mask = ['enter_reason']
|
group_mask = ["enter_reason"]
|
||||||
wins = bigdf.loc[bigdf['profit_abs'] >= 0] \
|
wins = (
|
||||||
.groupby(group_mask) \
|
bigdf.loc[bigdf["profit_abs"] >= 0].groupby(group_mask).agg({"profit_abs": ["sum"]})
|
||||||
.agg({'profit_abs': ['sum']})
|
)
|
||||||
|
|
||||||
wins.columns = ['profit_abs_wins']
|
wins.columns = ["profit_abs_wins"]
|
||||||
loss = bigdf.loc[bigdf['profit_abs'] < 0] \
|
loss = (
|
||||||
.groupby(group_mask) \
|
bigdf.loc[bigdf["profit_abs"] < 0].groupby(group_mask).agg({"profit_abs": ["sum"]})
|
||||||
.agg({'profit_abs': ['sum']})
|
)
|
||||||
loss.columns = ['profit_abs_loss']
|
loss.columns = ["profit_abs_loss"]
|
||||||
|
|
||||||
new = bigdf.groupby(group_mask).agg({'profit_abs': [
|
new = bigdf.groupby(group_mask).agg(
|
||||||
'count',
|
{"profit_abs": ["count", lambda x: sum(x > 0), lambda x: sum(x <= 0)]}
|
||||||
lambda x: sum(x > 0),
|
)
|
||||||
lambda x: sum(x <= 0)]})
|
|
||||||
new = pd.concat([new, wins, loss], axis=1).fillna(0)
|
new = pd.concat([new, wins, loss], axis=1).fillna(0)
|
||||||
|
|
||||||
new['profit_tot'] = new['profit_abs_wins'] - abs(new['profit_abs_loss'])
|
new["profit_tot"] = new["profit_abs_wins"] - abs(new["profit_abs_loss"])
|
||||||
new['wl_ratio_pct'] = (new.iloc[:, 1] / new.iloc[:, 0] * 100).fillna(0)
|
new["wl_ratio_pct"] = (new.iloc[:, 1] / new.iloc[:, 0] * 100).fillna(0)
|
||||||
new['avg_win'] = (new['profit_abs_wins'] / new.iloc[:, 1]).fillna(0)
|
new["avg_win"] = (new["profit_abs_wins"] / new.iloc[:, 1]).fillna(0)
|
||||||
new['avg_loss'] = (new['profit_abs_loss'] / new.iloc[:, 2]).fillna(0)
|
new["avg_loss"] = (new["profit_abs_loss"] / new.iloc[:, 2]).fillna(0)
|
||||||
|
|
||||||
new['exp_ratio'] = (
|
new["exp_ratio"] = (
|
||||||
(
|
((1 + (new["avg_win"] / abs(new["avg_loss"]))) * (new["wl_ratio_pct"] / 100)) - 1
|
||||||
(1 + (new['avg_win'] / abs(new['avg_loss']))) * (new['wl_ratio_pct'] / 100)
|
).fillna(0)
|
||||||
) - 1).fillna(0)
|
|
||||||
|
|
||||||
new.columns = ['total_num_buys', 'wins', 'losses',
|
new.columns = [
|
||||||
'profit_abs_wins', 'profit_abs_loss',
|
"total_num_buys",
|
||||||
'profit_tot', 'wl_ratio_pct',
|
"wins",
|
||||||
'avg_win', 'avg_loss', 'exp_ratio']
|
"losses",
|
||||||
|
"profit_abs_wins",
|
||||||
|
"profit_abs_loss",
|
||||||
|
"profit_tot",
|
||||||
|
"wl_ratio_pct",
|
||||||
|
"avg_win",
|
||||||
|
"avg_loss",
|
||||||
|
"exp_ratio",
|
||||||
|
]
|
||||||
|
|
||||||
sortcols = ['total_num_buys']
|
sortcols = ["total_num_buys"]
|
||||||
|
|
||||||
_print_table(new, sortcols, show_index=True, name="Group 0:",
|
_print_table(
|
||||||
to_csv=to_csv, csv_path=csv_path)
|
new, sortcols, show_index=True, name="Group 0:", to_csv=to_csv, csv_path=csv_path
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'],
|
agg_mask = {
|
||||||
'profit_ratio': ['median', 'mean', 'sum']}
|
"profit_abs": ["count", "sum", "median", "mean"],
|
||||||
agg_cols = ['num_buys', 'profit_abs_sum', 'profit_abs_median',
|
"profit_ratio": ["median", "mean", "sum"],
|
||||||
'profit_abs_mean', 'median_profit_pct', 'mean_profit_pct',
|
}
|
||||||
'total_profit_pct']
|
agg_cols = [
|
||||||
sortcols = ['profit_abs_sum', 'enter_reason']
|
"num_buys",
|
||||||
|
"profit_abs_sum",
|
||||||
|
"profit_abs_median",
|
||||||
|
"profit_abs_mean",
|
||||||
|
"median_profit_pct",
|
||||||
|
"mean_profit_pct",
|
||||||
|
"total_profit_pct",
|
||||||
|
]
|
||||||
|
sortcols = ["profit_abs_sum", "enter_reason"]
|
||||||
|
|
||||||
# 1: profit summaries grouped by enter_tag
|
# 1: profit summaries grouped by enter_tag
|
||||||
if g == "1":
|
if g == "1":
|
||||||
group_mask = ['enter_reason']
|
group_mask = ["enter_reason"]
|
||||||
|
|
||||||
# 2: profit summaries grouped by enter_tag and exit_tag
|
# 2: profit summaries grouped by enter_tag and exit_tag
|
||||||
if g == "2":
|
if g == "2":
|
||||||
group_mask = ['enter_reason', 'exit_reason']
|
group_mask = ["enter_reason", "exit_reason"]
|
||||||
|
|
||||||
# 3: profit summaries grouped by pair and enter_tag
|
# 3: profit summaries grouped by pair and enter_tag
|
||||||
if g == "3":
|
if g == "3":
|
||||||
group_mask = ['pair', 'enter_reason']
|
group_mask = ["pair", "enter_reason"]
|
||||||
|
|
||||||
# 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
# 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
||||||
if g == "4":
|
if g == "4":
|
||||||
group_mask = ['pair', 'enter_reason', 'exit_reason']
|
group_mask = ["pair", "enter_reason", "exit_reason"]
|
||||||
|
|
||||||
# 5: profit summaries grouped by exit_tag
|
# 5: profit summaries grouped by exit_tag
|
||||||
if g == "5":
|
if g == "5":
|
||||||
group_mask = ['exit_reason']
|
group_mask = ["exit_reason"]
|
||||||
sortcols = ['exit_reason']
|
sortcols = ["exit_reason"]
|
||||||
|
|
||||||
if group_mask:
|
if group_mask:
|
||||||
new = bigdf.groupby(group_mask).agg(agg_mask).reset_index()
|
new = bigdf.groupby(group_mask).agg(agg_mask).reset_index()
|
||||||
new.columns = group_mask + agg_cols
|
new.columns = group_mask + agg_cols
|
||||||
new['median_profit_pct'] = new['median_profit_pct'] * 100
|
new["median_profit_pct"] = new["median_profit_pct"] * 100
|
||||||
new['mean_profit_pct'] = new['mean_profit_pct'] * 100
|
new["mean_profit_pct"] = new["mean_profit_pct"] * 100
|
||||||
new['total_profit_pct'] = new['total_profit_pct'] * 100
|
new["total_profit_pct"] = new["total_profit_pct"] * 100
|
||||||
|
|
||||||
_print_table(new, sortcols, name=f"Group {g}:",
|
_print_table(new, sortcols, name=f"Group {g}:", to_csv=to_csv, csv_path=csv_path)
|
||||||
to_csv=to_csv, csv_path=csv_path)
|
|
||||||
else:
|
else:
|
||||||
logger.warning("Invalid group mask specified.")
|
logger.warning("Invalid group mask specified.")
|
||||||
|
|
||||||
|
|
||||||
def _do_rejected_signals_output(rejected_signals_df: pd.DataFrame,
|
def _do_rejected_signals_output(
|
||||||
to_csv: bool = False, csv_path=None) -> None:
|
rejected_signals_df: pd.DataFrame, to_csv: bool = False, csv_path=None
|
||||||
cols = ['pair', 'date', 'enter_tag']
|
) -> None:
|
||||||
sortcols = ['date', 'pair', 'enter_tag']
|
cols = ["pair", "date", "enter_tag"]
|
||||||
_print_table(rejected_signals_df[cols],
|
sortcols = ["date", "pair", "enter_tag"]
|
||||||
sortcols,
|
_print_table(
|
||||||
show_index=False,
|
rejected_signals_df[cols],
|
||||||
name="Rejected Signals:",
|
sortcols,
|
||||||
to_csv=to_csv,
|
show_index=False,
|
||||||
csv_path=csv_path)
|
name="Rejected Signals:",
|
||||||
|
to_csv=to_csv,
|
||||||
|
csv_path=csv_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _select_rows_within_dates(df, timerange=None, df_date_col: str = 'date'):
|
def _select_rows_within_dates(df, timerange=None, df_date_col: str = "date"):
|
||||||
if timerange:
|
if timerange:
|
||||||
if timerange.starttype == 'date':
|
if timerange.starttype == "date":
|
||||||
df = df.loc[(df[df_date_col] >= timerange.startdt)]
|
df = df.loc[(df[df_date_col] >= timerange.startdt)]
|
||||||
if timerange.stoptype == 'date':
|
if timerange.stoptype == "date":
|
||||||
df = df.loc[(df[df_date_col] < timerange.stopdt)]
|
df = df.loc[(df[df_date_col] < timerange.stopdt)]
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
def _select_rows_by_tags(df, enter_reason_list, exit_reason_list):
|
def _select_rows_by_tags(df, enter_reason_list, exit_reason_list):
|
||||||
if enter_reason_list and "all" not in enter_reason_list:
|
if enter_reason_list and "all" not in enter_reason_list:
|
||||||
df = df.loc[(df['enter_reason'].isin(enter_reason_list))]
|
df = df.loc[(df["enter_reason"].isin(enter_reason_list))]
|
||||||
|
|
||||||
if exit_reason_list and "all" not in exit_reason_list:
|
if exit_reason_list and "all" not in exit_reason_list:
|
||||||
df = df.loc[(df['exit_reason'].isin(exit_reason_list))]
|
df = df.loc[(df["exit_reason"].isin(exit_reason_list))]
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
def prepare_results(analysed_trades, stratname,
|
def prepare_results(
|
||||||
enter_reason_list, exit_reason_list,
|
analysed_trades, stratname, enter_reason_list, exit_reason_list, timerange=None
|
||||||
timerange=None):
|
):
|
||||||
res_df = pd.DataFrame()
|
res_df = pd.DataFrame()
|
||||||
for pair, trades in analysed_trades[stratname].items():
|
for pair, trades in analysed_trades[stratname].items():
|
||||||
if (trades.shape[0] > 0):
|
if trades.shape[0] > 0:
|
||||||
trades.dropna(subset=['close_date'], inplace=True)
|
trades.dropna(subset=["close_date"], inplace=True)
|
||||||
res_df = pd.concat([res_df, trades], ignore_index=True)
|
res_df = pd.concat([res_df, trades], ignore_index=True)
|
||||||
|
|
||||||
res_df = _select_rows_within_dates(res_df, timerange)
|
res_df = _select_rows_within_dates(res_df, timerange)
|
||||||
|
|
||||||
if res_df is not None and res_df.shape[0] > 0 and ('enter_reason' in res_df.columns):
|
if res_df is not None and res_df.shape[0] > 0 and ("enter_reason" in res_df.columns):
|
||||||
res_df = _select_rows_by_tags(res_df, enter_reason_list, exit_reason_list)
|
res_df = _select_rows_by_tags(res_df, enter_reason_list, exit_reason_list)
|
||||||
|
|
||||||
return res_df
|
return res_df
|
||||||
|
|
||||||
|
|
||||||
def print_results(res_df: pd.DataFrame, analysis_groups: List[str], indicator_list: List[str],
|
def print_results(
|
||||||
csv_path: Path, rejected_signals=None, to_csv=False):
|
res_df: pd.DataFrame,
|
||||||
|
analysis_groups: List[str],
|
||||||
|
indicator_list: List[str],
|
||||||
|
csv_path: Path,
|
||||||
|
rejected_signals=None,
|
||||||
|
to_csv=False,
|
||||||
|
):
|
||||||
if res_df.shape[0] > 0:
|
if res_df.shape[0] > 0:
|
||||||
if analysis_groups:
|
if analysis_groups:
|
||||||
_do_group_table_output(res_df, analysis_groups, to_csv=to_csv, csv_path=csv_path)
|
_do_group_table_output(res_df, analysis_groups, to_csv=to_csv, csv_path=csv_path)
|
||||||
@@ -237,30 +270,31 @@ def print_results(res_df: pd.DataFrame, analysis_groups: List[str], indicator_li
|
|||||||
|
|
||||||
# NB this can be large for big dataframes!
|
# NB this can be large for big dataframes!
|
||||||
if "all" in indicator_list:
|
if "all" in indicator_list:
|
||||||
_print_table(res_df,
|
_print_table(
|
||||||
show_index=False,
|
res_df, show_index=False, name="Indicators:", to_csv=to_csv, csv_path=csv_path
|
||||||
name="Indicators:",
|
)
|
||||||
to_csv=to_csv,
|
|
||||||
csv_path=csv_path)
|
|
||||||
elif indicator_list is not None and indicator_list:
|
elif indicator_list is not None and indicator_list:
|
||||||
available_inds = []
|
available_inds = []
|
||||||
for ind in indicator_list:
|
for ind in indicator_list:
|
||||||
if ind in res_df:
|
if ind in res_df:
|
||||||
available_inds.append(ind)
|
available_inds.append(ind)
|
||||||
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
|
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
|
||||||
_print_table(res_df[ilist],
|
_print_table(
|
||||||
sortcols=['exit_reason'],
|
res_df[ilist],
|
||||||
show_index=False,
|
sortcols=["exit_reason"],
|
||||||
name="Indicators:",
|
show_index=False,
|
||||||
to_csv=to_csv,
|
name="Indicators:",
|
||||||
csv_path=csv_path)
|
to_csv=to_csv,
|
||||||
|
csv_path=csv_path,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print("\\No trades to show")
|
print("\\No trades to show")
|
||||||
|
|
||||||
|
|
||||||
def _print_table(df: pd.DataFrame, sortcols=None, *, show_index=False, name=None,
|
def _print_table(
|
||||||
to_csv=False, csv_path: Path):
|
df: pd.DataFrame, sortcols=None, *, show_index=False, name=None, to_csv=False, csv_path: Path
|
||||||
if (sortcols is not None):
|
):
|
||||||
|
if sortcols is not None:
|
||||||
data = df.sort_values(sortcols)
|
data = df.sort_values(sortcols)
|
||||||
else:
|
else:
|
||||||
data = df
|
data = df
|
||||||
@@ -273,60 +307,64 @@ def _print_table(df: pd.DataFrame, sortcols=None, *, show_index=False, name=None
|
|||||||
if name is not None:
|
if name is not None:
|
||||||
print(name)
|
print(name)
|
||||||
|
|
||||||
print(
|
print(tabulate(data, headers="keys", tablefmt="psql", showindex=show_index))
|
||||||
tabulate(
|
|
||||||
data,
|
|
||||||
headers='keys',
|
|
||||||
tablefmt='psql',
|
|
||||||
showindex=show_index
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def process_entry_exit_reasons(config: Config):
|
def process_entry_exit_reasons(config: Config):
|
||||||
try:
|
try:
|
||||||
analysis_groups = config.get('analysis_groups', [])
|
analysis_groups = config.get("analysis_groups", [])
|
||||||
enter_reason_list = config.get('enter_reason_list', ["all"])
|
enter_reason_list = config.get("enter_reason_list", ["all"])
|
||||||
exit_reason_list = config.get('exit_reason_list', ["all"])
|
exit_reason_list = config.get("exit_reason_list", ["all"])
|
||||||
indicator_list = config.get('indicator_list', [])
|
indicator_list = config.get("indicator_list", [])
|
||||||
do_rejected = config.get('analysis_rejected', False)
|
do_rejected = config.get("analysis_rejected", False)
|
||||||
to_csv = config.get('analysis_to_csv', False)
|
to_csv = config.get("analysis_to_csv", False)
|
||||||
csv_path = Path(config.get('analysis_csv_path', config['exportfilename']))
|
csv_path = Path(config.get("analysis_csv_path", config["exportfilename"]))
|
||||||
if to_csv and not csv_path.is_dir():
|
if to_csv and not csv_path.is_dir():
|
||||||
raise OperationalException(f"Specified directory {csv_path} does not exist.")
|
raise OperationalException(f"Specified directory {csv_path} does not exist.")
|
||||||
|
|
||||||
timerange = TimeRange.parse_timerange(None if config.get(
|
timerange = TimeRange.parse_timerange(
|
||||||
'timerange') is None else str(config.get('timerange')))
|
None if config.get("timerange") is None else str(config.get("timerange"))
|
||||||
|
)
|
||||||
|
|
||||||
backtest_stats = load_backtest_stats(config['exportfilename'])
|
backtest_stats = load_backtest_stats(config["exportfilename"])
|
||||||
|
|
||||||
for strategy_name, results in backtest_stats['strategy'].items():
|
for strategy_name, results in backtest_stats["strategy"].items():
|
||||||
trades = load_backtest_data(config['exportfilename'], strategy_name)
|
trades = load_backtest_data(config["exportfilename"], strategy_name)
|
||||||
|
|
||||||
if trades is not None and not trades.empty:
|
if trades is not None and not trades.empty:
|
||||||
signal_candles = _load_signal_candles(config['exportfilename'])
|
signal_candles = _load_signal_candles(config["exportfilename"])
|
||||||
|
|
||||||
rej_df = None
|
rej_df = None
|
||||||
if do_rejected:
|
if do_rejected:
|
||||||
rejected_signals_dict = _load_rejected_signals(config['exportfilename'])
|
rejected_signals_dict = _load_rejected_signals(config["exportfilename"])
|
||||||
rej_df = prepare_results(rejected_signals_dict, strategy_name,
|
rej_df = prepare_results(
|
||||||
enter_reason_list, exit_reason_list,
|
rejected_signals_dict,
|
||||||
timerange=timerange)
|
strategy_name,
|
||||||
|
enter_reason_list,
|
||||||
|
exit_reason_list,
|
||||||
|
timerange=timerange,
|
||||||
|
)
|
||||||
|
|
||||||
analysed_trades_dict = _process_candles_and_indicators(
|
analysed_trades_dict = _process_candles_and_indicators(
|
||||||
config['exchange']['pair_whitelist'], strategy_name,
|
config["exchange"]["pair_whitelist"], strategy_name, trades, signal_candles
|
||||||
trades, signal_candles)
|
)
|
||||||
|
|
||||||
res_df = prepare_results(analysed_trades_dict, strategy_name,
|
res_df = prepare_results(
|
||||||
enter_reason_list, exit_reason_list,
|
analysed_trades_dict,
|
||||||
timerange=timerange)
|
strategy_name,
|
||||||
|
enter_reason_list,
|
||||||
|
exit_reason_list,
|
||||||
|
timerange=timerange,
|
||||||
|
)
|
||||||
|
|
||||||
print_results(res_df,
|
print_results(
|
||||||
analysis_groups,
|
res_df,
|
||||||
indicator_list,
|
analysis_groups,
|
||||||
rejected_signals=rej_df,
|
indicator_list,
|
||||||
to_csv=to_csv,
|
rejected_signals=rej_df,
|
||||||
csv_path=csv_path)
|
to_csv=to_csv,
|
||||||
|
csv_path=csv_path,
|
||||||
|
)
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|||||||
@@ -5,8 +5,17 @@ Includes:
|
|||||||
* load data for a pair (or a list of pairs) from disk
|
* load data for a pair (or a list of pairs) from disk
|
||||||
* download data from exchange and store to disk
|
* download data from exchange and store to disk
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
from .datahandlers import get_datahandler
|
from .datahandlers import get_datahandler
|
||||||
from .history_utils import (convert_trades_to_ohlcv, download_data_main, get_timerange, load_data,
|
from .history_utils import (
|
||||||
load_pair_history, refresh_backtest_ohlcv_data,
|
convert_trades_to_ohlcv,
|
||||||
refresh_backtest_trades_data, refresh_data, validate_backtest_data)
|
download_data_main,
|
||||||
|
get_timerange,
|
||||||
|
load_data,
|
||||||
|
load_pair_history,
|
||||||
|
refresh_backtest_ohlcv_data,
|
||||||
|
refresh_backtest_trades_data,
|
||||||
|
refresh_data,
|
||||||
|
validate_backtest_data,
|
||||||
|
)
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class FeatherDataHandler(IDataHandler):
|
class FeatherDataHandler(IDataHandler):
|
||||||
|
|
||||||
_columns = DEFAULT_DATAFRAME_COLUMNS
|
_columns = DEFAULT_DATAFRAME_COLUMNS
|
||||||
|
|
||||||
def ohlcv_store(
|
def ohlcv_store(
|
||||||
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType) -> None:
|
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:
|
||||||
@@ -33,11 +33,12 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
self.create_dir_if_needed(filename)
|
self.create_dir_if_needed(filename)
|
||||||
|
|
||||||
data.reset_index(drop=True).loc[:, self._columns].to_feather(
|
data.reset_index(drop=True).loc[:, self._columns].to_feather(
|
||||||
filename, compression_level=9, compression='lz4')
|
filename, compression_level=9, compression="lz4"
|
||||||
|
)
|
||||||
|
|
||||||
def _ohlcv_load(self, pair: str, timeframe: str,
|
def _ohlcv_load(
|
||||||
timerange: Optional[TimeRange], candle_type: CandleType
|
self, pair: str, timeframe: str, 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.
|
||||||
Implements the loading and conversion to a Pandas dataframe.
|
Implements the loading and conversion to a Pandas dataframe.
|
||||||
@@ -50,28 +51,31 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
: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(
|
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type=candle_type)
|
||||||
self._datadir, pair, timeframe, candle_type=candle_type)
|
|
||||||
if not filename.exists():
|
if not filename.exists():
|
||||||
# Fallback mode for 1M files
|
# Fallback mode for 1M files
|
||||||
filename = self._pair_data_filename(
|
filename = self._pair_data_filename(
|
||||||
self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True)
|
self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True
|
||||||
|
)
|
||||||
if not filename.exists():
|
if not filename.exists():
|
||||||
return DataFrame(columns=self._columns)
|
return DataFrame(columns=self._columns)
|
||||||
|
|
||||||
pairdata = read_feather(filename)
|
pairdata = read_feather(filename)
|
||||||
pairdata.columns = self._columns
|
pairdata.columns = self._columns
|
||||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
pairdata = pairdata.astype(
|
||||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
dtype={
|
||||||
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
"open": "float",
|
||||||
|
"high": "float",
|
||||||
|
"low": "float",
|
||||||
|
"close": "float",
|
||||||
|
"volume": "float",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pairdata["date"] = to_datetime(pairdata["date"], unit="ms", utc=True)
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_append(
|
def ohlcv_append(
|
||||||
self,
|
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
|
||||||
pair: str,
|
|
||||||
timeframe: str,
|
|
||||||
data: DataFrame,
|
|
||||||
candle_type: CandleType
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Append data to existing data structures
|
Append data to existing data structures
|
||||||
@@ -92,7 +96,7 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
"""
|
"""
|
||||||
filename = self._pair_trades_filename(self._datadir, pair, trading_mode)
|
filename = self._pair_trades_filename(self._datadir, pair, trading_mode)
|
||||||
self.create_dir_if_needed(filename)
|
self.create_dir_if_needed(filename)
|
||||||
data.reset_index(drop=True).to_feather(filename, compression_level=9, compression='lz4')
|
data.reset_index(drop=True).to_feather(filename, compression_level=9, compression="lz4")
|
||||||
|
|
||||||
def trades_append(self, pair: str, data: DataFrame):
|
def trades_append(self, pair: str, data: DataFrame):
|
||||||
"""
|
"""
|
||||||
@@ -104,7 +108,7 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _trades_load(
|
def _trades_load(
|
||||||
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None
|
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Load a pair from file, either .json.gz or .json
|
Load a pair from file, either .json.gz or .json
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class HDF5DataHandler(IDataHandler):
|
class HDF5DataHandler(IDataHandler):
|
||||||
|
|
||||||
_columns = DEFAULT_DATAFRAME_COLUMNS
|
_columns = DEFAULT_DATAFRAME_COLUMNS
|
||||||
|
|
||||||
def ohlcv_store(
|
def ohlcv_store(
|
||||||
self, pair: str, timeframe: str, data: pd.DataFrame, candle_type: CandleType) -> None:
|
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
|
||||||
@@ -35,13 +35,18 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
self.create_dir_if_needed(filename)
|
self.create_dir_if_needed(filename)
|
||||||
|
|
||||||
_data.loc[:, self._columns].to_hdf(
|
_data.loc[:, self._columns].to_hdf(
|
||||||
filename, key=key, mode='a', complevel=9, complib='blosc',
|
filename,
|
||||||
format='table', data_columns=['date']
|
key=key,
|
||||||
|
mode="a",
|
||||||
|
complevel=9,
|
||||||
|
complib="blosc",
|
||||||
|
format="table",
|
||||||
|
data_columns=["date"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def _ohlcv_load(self, pair: str, timeframe: str,
|
def _ohlcv_load(
|
||||||
timerange: Optional[TimeRange], candle_type: CandleType
|
self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType
|
||||||
) -> pd.DataFrame:
|
) -> 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.
|
||||||
@@ -55,41 +60,40 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
: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(
|
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type=candle_type)
|
||||||
self._datadir,
|
|
||||||
pair,
|
|
||||||
timeframe,
|
|
||||||
candle_type=candle_type
|
|
||||||
)
|
|
||||||
|
|
||||||
if not filename.exists():
|
if not filename.exists():
|
||||||
# Fallback mode for 1M files
|
# Fallback mode for 1M files
|
||||||
filename = self._pair_data_filename(
|
filename = self._pair_data_filename(
|
||||||
self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True)
|
self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True
|
||||||
|
)
|
||||||
if not filename.exists():
|
if not filename.exists():
|
||||||
return pd.DataFrame(columns=self._columns)
|
return pd.DataFrame(columns=self._columns)
|
||||||
where = []
|
where = []
|
||||||
if timerange:
|
if timerange:
|
||||||
if timerange.starttype == 'date':
|
if timerange.starttype == "date":
|
||||||
where.append(f"date >= Timestamp({timerange.startts * 1e9})")
|
where.append(f"date >= Timestamp({timerange.startts * 1e9})")
|
||||||
if timerange.stoptype == 'date':
|
if timerange.stoptype == "date":
|
||||||
where.append(f"date <= Timestamp({timerange.stopts * 1e9})")
|
where.append(f"date <= Timestamp({timerange.stopts * 1e9})")
|
||||||
|
|
||||||
pairdata = pd.read_hdf(filename, key=key, mode="r", where=where)
|
pairdata = pd.read_hdf(filename, key=key, mode="r", where=where)
|
||||||
|
|
||||||
if list(pairdata.columns) != self._columns:
|
if list(pairdata.columns) != self._columns:
|
||||||
raise ValueError("Wrong dataframe format")
|
raise ValueError("Wrong dataframe format")
|
||||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
pairdata = pairdata.astype(
|
||||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
dtype={
|
||||||
|
"open": "float",
|
||||||
|
"high": "float",
|
||||||
|
"low": "float",
|
||||||
|
"close": "float",
|
||||||
|
"volume": "float",
|
||||||
|
}
|
||||||
|
)
|
||||||
pairdata = pairdata.reset_index(drop=True)
|
pairdata = pairdata.reset_index(drop=True)
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_append(
|
def ohlcv_append(
|
||||||
self,
|
self, pair: str, timeframe: str, data: pd.DataFrame, candle_type: CandleType
|
||||||
pair: str,
|
|
||||||
timeframe: str,
|
|
||||||
data: pd.DataFrame,
|
|
||||||
candle_type: CandleType
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Append data to existing data structures
|
Append data to existing data structures
|
||||||
@@ -111,9 +115,13 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
key = self._pair_trades_key(pair)
|
key = self._pair_trades_key(pair)
|
||||||
|
|
||||||
data.to_hdf(
|
data.to_hdf(
|
||||||
self._pair_trades_filename(self._datadir, pair, trading_mode), key=key,
|
self._pair_trades_filename(self._datadir, pair, trading_mode),
|
||||||
mode='a', complevel=9, complib='blosc',
|
key=key,
|
||||||
format='table', data_columns=['timestamp']
|
mode="a",
|
||||||
|
complevel=9,
|
||||||
|
complib="blosc",
|
||||||
|
format="table",
|
||||||
|
data_columns=["timestamp"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def trades_append(self, pair: str, data: pd.DataFrame):
|
def trades_append(self, pair: str, data: pd.DataFrame):
|
||||||
@@ -142,13 +150,13 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
return pd.DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
return pd.DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||||
where = []
|
where = []
|
||||||
if timerange:
|
if timerange:
|
||||||
if timerange.starttype == 'date':
|
if timerange.starttype == "date":
|
||||||
where.append(f"timestamp >= {timerange.startts * 1e3}")
|
where.append(f"timestamp >= {timerange.startts * 1e3}")
|
||||||
if timerange.stoptype == 'date':
|
if timerange.stoptype == "date":
|
||||||
where.append(f"timestamp < {timerange.stopts * 1e3}")
|
where.append(f"timestamp < {timerange.stopts * 1e3}")
|
||||||
|
|
||||||
trades: pd.DataFrame = pd.read_hdf(filename, key=key, mode="r", where=where)
|
trades: pd.DataFrame = pd.read_hdf(filename, key=key, mode="r", where=where)
|
||||||
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
trades[["id", "type"]] = trades[["id", "type"]].replace({np.nan: None})
|
||||||
return trades
|
return trades
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -158,7 +166,7 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str:
|
def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str:
|
||||||
# Escape futures pairs to avoid warnings
|
# Escape futures pairs to avoid warnings
|
||||||
pair_esc = pair.replace(':', '_')
|
pair_esc = pair.replace(":", "_")
|
||||||
return f"{pair_esc}/ohlcv/tf_{timeframe}"
|
return f"{pair_esc}/ohlcv/tf_{timeframe}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ Abstract datahandler interface.
|
|||||||
It's subclasses handle and storing data from disk.
|
It's subclasses handle and storing data from disk.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
@@ -16,8 +17,12 @@ from pandas import DataFrame
|
|||||||
from freqtrade import misc
|
from freqtrade import misc
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DEFAULT_TRADES_COLUMNS, ListPairsWithTimeframes
|
from freqtrade.constants import DEFAULT_TRADES_COLUMNS, ListPairsWithTimeframes
|
||||||
from freqtrade.data.converter import (clean_ohlcv_dataframe, trades_convert_types,
|
from freqtrade.data.converter import (
|
||||||
trades_df_remove_duplicates, trim_dataframe)
|
clean_ohlcv_dataframe,
|
||||||
|
trades_convert_types,
|
||||||
|
trades_df_remove_duplicates,
|
||||||
|
trim_dataframe,
|
||||||
|
)
|
||||||
from freqtrade.enums import CandleType, TradingMode
|
from freqtrade.enums import CandleType, TradingMode
|
||||||
from freqtrade.exchange import timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_seconds
|
||||||
|
|
||||||
@@ -26,8 +31,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class IDataHandler(ABC):
|
class IDataHandler(ABC):
|
||||||
|
_OHLCV_REGEX = r"^([a-zA-Z_\d-]+)\-(\d+[a-zA-Z]{1,2})\-?([a-zA-Z_]*)?(?=\.)"
|
||||||
_OHLCV_REGEX = r'^([a-zA-Z_\d-]+)\-(\d+[a-zA-Z]{1,2})\-?([a-zA-Z_]*)?(?=\.)'
|
|
||||||
|
|
||||||
def __init__(self, datadir: Path) -> None:
|
def __init__(self, datadir: Path) -> None:
|
||||||
self._datadir = datadir
|
self._datadir = datadir
|
||||||
@@ -41,7 +45,8 @@ class IDataHandler(ABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ohlcv_get_available_data(
|
def ohlcv_get_available_data(
|
||||||
cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes:
|
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
|
||||||
@@ -49,17 +54,20 @@ class IDataHandler(ABC):
|
|||||||
:return: List of Tuples of (pair, timeframe, CandleType)
|
:return: List of Tuples of (pair, timeframe, CandleType)
|
||||||
"""
|
"""
|
||||||
if trading_mode == TradingMode.FUTURES:
|
if trading_mode == TradingMode.FUTURES:
|
||||||
datadir = datadir.joinpath('futures')
|
datadir = datadir.joinpath("futures")
|
||||||
_tmp = [
|
_tmp = [
|
||||||
re.search(
|
re.search(cls._OHLCV_REGEX, p.name)
|
||||||
cls._OHLCV_REGEX, p.name
|
for p in datadir.glob(f"*.{cls._get_file_extension()}")
|
||||||
) for p in datadir.glob(f"*.{cls._get_file_extension()}")]
|
]
|
||||||
return [
|
return [
|
||||||
(
|
(
|
||||||
cls.rebuild_pair_from_filename(match[1]),
|
cls.rebuild_pair_from_filename(match[1]),
|
||||||
cls.rebuild_timeframe_from_filename(match[2]),
|
cls.rebuild_timeframe_from_filename(match[2]),
|
||||||
CandleType.from_string(match[3])
|
CandleType.from_string(match[3]),
|
||||||
) for match in _tmp if match and len(match.groups()) > 1]
|
)
|
||||||
|
for match in _tmp
|
||||||
|
if match and len(match.groups()) > 1
|
||||||
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]:
|
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]:
|
||||||
@@ -73,17 +81,20 @@ class IDataHandler(ABC):
|
|||||||
"""
|
"""
|
||||||
candle = ""
|
candle = ""
|
||||||
if candle_type != CandleType.SPOT:
|
if candle_type != CandleType.SPOT:
|
||||||
datadir = datadir.joinpath('futures')
|
datadir = datadir.joinpath("futures")
|
||||||
candle = f"-{candle_type}"
|
candle = f"-{candle_type}"
|
||||||
ext = cls._get_file_extension()
|
ext = cls._get_file_extension()
|
||||||
_tmp = [re.search(r'^(\S+)(?=\-' + timeframe + candle + f'.{ext})', p.name)
|
_tmp = [
|
||||||
for p in datadir.glob(f"*{timeframe}{candle}.{ext}")]
|
re.search(r"^(\S+)(?=\-" + timeframe + candle + f".{ext})", p.name)
|
||||||
|
for p in datadir.glob(f"*{timeframe}{candle}.{ext}")
|
||||||
|
]
|
||||||
# Check if regex found something and only return these results
|
# Check if regex found something and only return these results
|
||||||
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def ohlcv_store(
|
def ohlcv_store(
|
||||||
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType) -> None:
|
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
|
||||||
@@ -93,8 +104,9 @@ class IDataHandler(ABC):
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def ohlcv_data_min_max(self, pair: str, timeframe: str,
|
def ohlcv_data_min_max(
|
||||||
candle_type: CandleType) -> Tuple[datetime, datetime, int]:
|
self, pair: str, timeframe: str, candle_type: CandleType
|
||||||
|
) -> Tuple[datetime, datetime, int]:
|
||||||
"""
|
"""
|
||||||
Returns the min and max timestamp for the given pair and timeframe.
|
Returns the min and max timestamp for the given pair and timeframe.
|
||||||
:param pair: Pair to get min/max for
|
:param pair: Pair to get min/max for
|
||||||
@@ -109,12 +121,12 @@ class IDataHandler(ABC):
|
|||||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
datetime.fromtimestamp(0, tz=timezone.utc),
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
return df.iloc[0]['date'].to_pydatetime(), df.iloc[-1]['date'].to_pydatetime(), len(df)
|
return df.iloc[0]["date"].to_pydatetime(), df.iloc[-1]["date"].to_pydatetime(), len(df)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _ohlcv_load(self, pair: str, timeframe: str, timerange: Optional[TimeRange],
|
def _ohlcv_load(
|
||||||
candle_type: CandleType
|
self, pair: str, timeframe: str, 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.
|
||||||
Implements the loading and conversion to a Pandas dataframe.
|
Implements the loading and conversion to a Pandas dataframe.
|
||||||
@@ -144,11 +156,7 @@ class IDataHandler(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def ohlcv_append(
|
def ohlcv_append(
|
||||||
self,
|
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
|
||||||
pair: str,
|
|
||||||
timeframe: str,
|
|
||||||
data: DataFrame,
|
|
||||||
candle_type: CandleType
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Append data to existing data structures
|
Append data to existing data structures
|
||||||
@@ -166,8 +174,10 @@ class IDataHandler(ABC):
|
|||||||
:return: List of Pairs
|
:return: List of Pairs
|
||||||
"""
|
"""
|
||||||
_ext = cls._get_file_extension()
|
_ext = cls._get_file_extension()
|
||||||
_tmp = [re.search(r'^(\S+)(?=\-trades.' + _ext + ')', p.name)
|
_tmp = [
|
||||||
for p in datadir.glob(f"*trades.{_ext}")]
|
re.search(r"^(\S+)(?=\-trades." + _ext + ")", p.name)
|
||||||
|
for p in datadir.glob(f"*trades.{_ext}")
|
||||||
|
]
|
||||||
# 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 [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
||||||
|
|
||||||
@@ -227,7 +237,7 @@ class IDataHandler(ABC):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def trades_load(
|
def trades_load(
|
||||||
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None
|
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Load a pair from file, either .json.gz or .json
|
Load a pair from file, either .json.gz or .json
|
||||||
@@ -260,7 +270,7 @@ class IDataHandler(ABC):
|
|||||||
pair: str,
|
pair: str,
|
||||||
timeframe: str,
|
timeframe: str,
|
||||||
candle_type: CandleType,
|
candle_type: CandleType,
|
||||||
no_timeframe_modify: bool = False
|
no_timeframe_modify: bool = False,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
pair_s = misc.pair_to_filename(pair)
|
pair_s = misc.pair_to_filename(pair)
|
||||||
candle = ""
|
candle = ""
|
||||||
@@ -268,10 +278,9 @@ class IDataHandler(ABC):
|
|||||||
timeframe = cls.timeframe_to_file(timeframe)
|
timeframe = cls.timeframe_to_file(timeframe)
|
||||||
|
|
||||||
if candle_type != CandleType.SPOT:
|
if candle_type != CandleType.SPOT:
|
||||||
datadir = datadir.joinpath('futures')
|
datadir = datadir.joinpath("futures")
|
||||||
candle = f"-{candle_type}"
|
candle = f"-{candle_type}"
|
||||||
filename = datadir.joinpath(
|
filename = datadir.joinpath(f"{pair_s}-{timeframe}{candle}.{cls._get_file_extension()}")
|
||||||
f'{pair_s}-{timeframe}{candle}.{cls._get_file_extension()}')
|
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -279,14 +288,14 @@ class IDataHandler(ABC):
|
|||||||
pair_s = misc.pair_to_filename(pair)
|
pair_s = misc.pair_to_filename(pair)
|
||||||
if trading_mode == TradingMode.FUTURES:
|
if trading_mode == TradingMode.FUTURES:
|
||||||
# Futures pair ...
|
# Futures pair ...
|
||||||
datadir = datadir.joinpath('futures')
|
datadir = datadir.joinpath("futures")
|
||||||
|
|
||||||
filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}')
|
filename = datadir.joinpath(f"{pair_s}-trades.{cls._get_file_extension()}")
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def timeframe_to_file(timeframe: str):
|
def timeframe_to_file(timeframe: str):
|
||||||
return timeframe.replace('M', 'Mo')
|
return timeframe.replace("M", "Mo")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def rebuild_timeframe_from_filename(timeframe: str) -> str:
|
def rebuild_timeframe_from_filename(timeframe: str) -> str:
|
||||||
@@ -294,7 +303,7 @@ class IDataHandler(ABC):
|
|||||||
converts timeframe from disk to file
|
converts timeframe from disk to file
|
||||||
Replaces mo with M (to avoid problems on case-insensitive filesystems)
|
Replaces mo with M (to avoid problems on case-insensitive filesystems)
|
||||||
"""
|
"""
|
||||||
return re.sub('1mo', '1M', timeframe, flags=re.IGNORECASE)
|
return re.sub("1mo", "1M", timeframe, flags=re.IGNORECASE)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def rebuild_pair_from_filename(pair: str) -> str:
|
def rebuild_pair_from_filename(pair: str) -> str:
|
||||||
@@ -302,18 +311,22 @@ class IDataHandler(ABC):
|
|||||||
Rebuild pair name from filename
|
Rebuild pair name from filename
|
||||||
Assumes a asset name of max. 7 length to also support BTC-PERP and BTC-PERP:USD names.
|
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\d]{1,10})|^([A-Za-z\-]{1,6}))(_)', r'\g<1>/', pair, 1)
|
res = re.sub(r"^(([A-Za-z\d]{1,10})|^([A-Za-z\-]{1,6}))(_)", r"\g<1>/", pair, count=1)
|
||||||
res = re.sub('_', ':', res, 1)
|
res = re.sub("_", ":", res, count=1)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def ohlcv_load(self, pair, timeframe: str,
|
def ohlcv_load(
|
||||||
candle_type: CandleType, *,
|
self,
|
||||||
timerange: Optional[TimeRange] = None,
|
pair,
|
||||||
fill_missing: bool = True,
|
timeframe: str,
|
||||||
drop_incomplete: bool = False,
|
candle_type: CandleType,
|
||||||
startup_candles: int = 0,
|
*,
|
||||||
warn_no_data: bool = True,
|
timerange: Optional[TimeRange] = None,
|
||||||
) -> DataFrame:
|
fill_missing: bool = True,
|
||||||
|
drop_incomplete: bool = False,
|
||||||
|
startup_candles: int = 0,
|
||||||
|
warn_no_data: bool = True,
|
||||||
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Load cached candle (OHLCV) data for the given pair.
|
Load cached candle (OHLCV) data for the given pair.
|
||||||
|
|
||||||
@@ -333,15 +346,12 @@ class IDataHandler(ABC):
|
|||||||
timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles)
|
timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles)
|
||||||
|
|
||||||
pairdf = self._ohlcv_load(
|
pairdf = self._ohlcv_load(
|
||||||
pair,
|
pair, timeframe, timerange=timerange_startup, candle_type=candle_type
|
||||||
timeframe,
|
|
||||||
timerange=timerange_startup,
|
|
||||||
candle_type=candle_type
|
|
||||||
)
|
)
|
||||||
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
|
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, timeframe, candle_type, timerange_startup)
|
self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup)
|
||||||
@@ -350,17 +360,25 @@ class IDataHandler(ABC):
|
|||||||
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.
|
||||||
pairdf = clean_ohlcv_dataframe(pairdf, timeframe,
|
pairdf = clean_ohlcv_dataframe(
|
||||||
pair=pair,
|
pairdf,
|
||||||
fill_missing=fill_missing,
|
timeframe,
|
||||||
drop_incomplete=(drop_incomplete and
|
pair=pair,
|
||||||
enddate == pairdf.iloc[-1]['date']))
|
fill_missing=fill_missing,
|
||||||
|
drop_incomplete=(drop_incomplete and enddate == pairdf.iloc[-1]["date"]),
|
||||||
|
)
|
||||||
self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data)
|
self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data)
|
||||||
return pairdf
|
return pairdf
|
||||||
|
|
||||||
def _check_empty_df(
|
def _check_empty_df(
|
||||||
self, pairdf: DataFrame, pair: str, timeframe: str, candle_type: CandleType,
|
self,
|
||||||
warn_no_data: bool, warn_price: bool = False) -> bool:
|
pairdf: DataFrame,
|
||||||
|
pair: str,
|
||||||
|
timeframe: str,
|
||||||
|
candle_type: CandleType,
|
||||||
|
warn_no_data: bool,
|
||||||
|
warn_price: bool = False,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Warn on empty dataframe
|
Warn on empty dataframe
|
||||||
"""
|
"""
|
||||||
@@ -373,39 +391,55 @@ class IDataHandler(ABC):
|
|||||||
return True
|
return True
|
||||||
elif warn_price:
|
elif warn_price:
|
||||||
candle_price_gap = 0
|
candle_price_gap = 0
|
||||||
if (candle_type in (CandleType.SPOT, CandleType.FUTURES) and
|
if (
|
||||||
not pairdf.empty
|
candle_type in (CandleType.SPOT, CandleType.FUTURES)
|
||||||
and 'close' in pairdf.columns and 'open' in pairdf.columns):
|
and not pairdf.empty
|
||||||
|
and "close" in pairdf.columns
|
||||||
|
and "open" in pairdf.columns
|
||||||
|
):
|
||||||
# Detect gaps between prior close and open
|
# Detect gaps between prior close and open
|
||||||
gaps = ((pairdf['open'] - pairdf['close'].shift(1)) / pairdf['close'].shift(1))
|
gaps = (pairdf["open"] - pairdf["close"].shift(1)) / pairdf["close"].shift(1)
|
||||||
gaps = gaps.dropna()
|
gaps = gaps.dropna()
|
||||||
if len(gaps):
|
if len(gaps):
|
||||||
candle_price_gap = max(abs(gaps))
|
candle_price_gap = max(abs(gaps))
|
||||||
if candle_price_gap > 0.1:
|
if candle_price_gap > 0.1:
|
||||||
logger.info(f"Price jump in {pair}, {timeframe}, {candle_type} between two candles "
|
logger.info(
|
||||||
f"of {candle_price_gap:.2%} detected.")
|
f"Price jump in {pair}, {timeframe}, {candle_type} between two candles "
|
||||||
|
f"of {candle_price_gap:.2%} detected."
|
||||||
|
)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _validate_pairdata(self, pair, pairdata: DataFrame, timeframe: str,
|
def _validate_pairdata(
|
||||||
candle_type: CandleType, timerange: TimeRange):
|
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
|
||||||
:param timerange: Timerange specified for start and end dates
|
:param timerange: Timerange specified for start and end dates
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if timerange.starttype == 'date':
|
if timerange.starttype == "date":
|
||||||
if pairdata.iloc[0]['date'] > timerange.startdt:
|
if pairdata.iloc[0]["date"] > timerange.startdt:
|
||||||
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
|
logger.warning(
|
||||||
f"data starts at {pairdata.iloc[0]['date']:%Y-%m-%d %H:%M:%S}")
|
f"{pair}, {candle_type}, {timeframe}, "
|
||||||
if timerange.stoptype == 'date':
|
f"data starts at {pairdata.iloc[0]['date']:%Y-%m-%d %H:%M:%S}"
|
||||||
if pairdata.iloc[-1]['date'] < timerange.stopdt:
|
)
|
||||||
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
|
if timerange.stoptype == "date":
|
||||||
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")
|
if pairdata.iloc[-1]["date"] < timerange.stopdt:
|
||||||
|
logger.warning(
|
||||||
|
f"{pair}, {candle_type}, {timeframe}, "
|
||||||
|
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}"
|
||||||
|
)
|
||||||
|
|
||||||
def rename_futures_data(
|
def rename_futures_data(
|
||||||
self, pair: str, new_pair: str, timeframe: str, candle_type: CandleType):
|
self, pair: str, new_pair: str, timeframe: str, candle_type: CandleType
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Temporary method to migrate data from old naming to new naming (BTC/USDT -> BTC/USDT:USDT)
|
Temporary method to migrate data from old naming to new naming (BTC/USDT -> BTC/USDT:USDT)
|
||||||
Only used for binance to support the binance futures naming unification.
|
Only used for binance to support the binance futures naming unification.
|
||||||
@@ -431,18 +465,19 @@ class IDataHandler(ABC):
|
|||||||
|
|
||||||
if funding_rate_combs:
|
if funding_rate_combs:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'Migrating {len(funding_rate_combs)} funding fees to correct timeframe.')
|
f"Migrating {len(funding_rate_combs)} funding fees to correct timeframe."
|
||||||
|
)
|
||||||
|
|
||||||
for pair, timeframe, candletype in funding_rate_combs:
|
for pair, timeframe, candletype in funding_rate_combs:
|
||||||
old_name = self._pair_data_filename(self._datadir, pair, timeframe, candletype)
|
old_name = self._pair_data_filename(self._datadir, pair, timeframe, candletype)
|
||||||
new_name = self._pair_data_filename(self._datadir, pair, ff_timeframe, candletype)
|
new_name = self._pair_data_filename(self._datadir, pair, ff_timeframe, candletype)
|
||||||
|
|
||||||
if not Path(old_name).exists():
|
if not Path(old_name).exists():
|
||||||
logger.warning(f'{old_name} does not exist, skipping.')
|
logger.warning(f"{old_name} does not exist, skipping.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if Path(new_name).exists():
|
if Path(new_name).exists():
|
||||||
logger.warning(f'{new_name} already exists, Removing.')
|
logger.warning(f"{new_name} already exists, Removing.")
|
||||||
Path(new_name).unlink()
|
Path(new_name).unlink()
|
||||||
|
|
||||||
Path(old_name).rename(new_name)
|
Path(old_name).rename(new_name)
|
||||||
@@ -457,27 +492,33 @@ def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
|
|||||||
:return: Datahandler class
|
:return: Datahandler class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if datatype == 'json':
|
if datatype == "json":
|
||||||
from .jsondatahandler import JsonDataHandler
|
from .jsondatahandler import JsonDataHandler
|
||||||
|
|
||||||
return JsonDataHandler
|
return JsonDataHandler
|
||||||
elif datatype == 'jsongz':
|
elif datatype == "jsongz":
|
||||||
from .jsondatahandler import JsonGzDataHandler
|
from .jsondatahandler import JsonGzDataHandler
|
||||||
|
|
||||||
return JsonGzDataHandler
|
return JsonGzDataHandler
|
||||||
elif datatype == 'hdf5':
|
elif datatype == "hdf5":
|
||||||
from .hdf5datahandler import HDF5DataHandler
|
from .hdf5datahandler import HDF5DataHandler
|
||||||
|
|
||||||
return HDF5DataHandler
|
return HDF5DataHandler
|
||||||
elif datatype == 'feather':
|
elif datatype == "feather":
|
||||||
from .featherdatahandler import FeatherDataHandler
|
from .featherdatahandler import FeatherDataHandler
|
||||||
|
|
||||||
return FeatherDataHandler
|
return FeatherDataHandler
|
||||||
elif datatype == 'parquet':
|
elif datatype == "parquet":
|
||||||
from .parquetdatahandler import ParquetDataHandler
|
from .parquetdatahandler import ParquetDataHandler
|
||||||
|
|
||||||
return ParquetDataHandler
|
return ParquetDataHandler
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"No datahandler for datatype {datatype} available.")
|
raise ValueError(f"No datahandler for datatype {datatype} available.")
|
||||||
|
|
||||||
|
|
||||||
def get_datahandler(datadir: Path, data_format: Optional[str] = None,
|
def get_datahandler(
|
||||||
data_handler: Optional[IDataHandler] = None) -> IDataHandler:
|
datadir: Path, data_format: Optional[str] = None, data_handler: Optional[IDataHandler] = None
|
||||||
|
) -> IDataHandler:
|
||||||
"""
|
"""
|
||||||
:param datadir: Folder to save data
|
:param datadir: Folder to save data
|
||||||
:param data_format: dataformat to use
|
:param data_format: dataformat to use
|
||||||
@@ -485,6 +526,6 @@ def get_datahandler(datadir: Path, data_format: Optional[str] = None,
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not data_handler:
|
if not data_handler:
|
||||||
HandlerClass = get_datahandlerclass(data_format or 'feather')
|
HandlerClass = get_datahandlerclass(data_format or "feather")
|
||||||
data_handler = HandlerClass(datadir)
|
data_handler = HandlerClass(datadir)
|
||||||
return data_handler
|
return data_handler
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class JsonDataHandler(IDataHandler):
|
class JsonDataHandler(IDataHandler):
|
||||||
|
|
||||||
_use_zip = False
|
_use_zip = False
|
||||||
_columns = DEFAULT_DATAFRAME_COLUMNS
|
_columns = DEFAULT_DATAFRAME_COLUMNS
|
||||||
|
|
||||||
def ohlcv_store(
|
def ohlcv_store(
|
||||||
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType) -> None:
|
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:
|
||||||
@@ -37,16 +37,16 @@ class JsonDataHandler(IDataHandler):
|
|||||||
self.create_dir_if_needed(filename)
|
self.create_dir_if_needed(filename)
|
||||||
_data = data.copy()
|
_data = data.copy()
|
||||||
# Convert date to int
|
# Convert date to int
|
||||||
_data['date'] = _data['date'].astype(np.int64) // 1000 // 1000
|
_data["date"] = _data["date"].astype(np.int64) // 1000 // 1000
|
||||||
|
|
||||||
# Reset index, select only appropriate columns and save as json
|
# Reset index, select only appropriate columns and save as json
|
||||||
_data.reset_index(drop=True).loc[:, self._columns].to_json(
|
_data.reset_index(drop=True).loc[:, self._columns].to_json(
|
||||||
filename, orient="values",
|
filename, orient="values", 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(
|
||||||
timerange: Optional[TimeRange], candle_type: CandleType
|
self, pair: str, timeframe: str, 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.
|
||||||
Implements the loading and conversion to a Pandas dataframe.
|
Implements the loading and conversion to a Pandas dataframe.
|
||||||
@@ -59,31 +59,34 @@ class JsonDataHandler(IDataHandler):
|
|||||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
: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(
|
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type=candle_type)
|
||||||
self._datadir, pair, timeframe, candle_type=candle_type)
|
|
||||||
if not filename.exists():
|
if not filename.exists():
|
||||||
# Fallback mode for 1M files
|
# Fallback mode for 1M files
|
||||||
filename = self._pair_data_filename(
|
filename = self._pair_data_filename(
|
||||||
self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True)
|
self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True
|
||||||
|
)
|
||||||
if not filename.exists():
|
if not filename.exists():
|
||||||
return DataFrame(columns=self._columns)
|
return DataFrame(columns=self._columns)
|
||||||
try:
|
try:
|
||||||
pairdata = read_json(filename, orient='values')
|
pairdata = read_json(filename, orient="values")
|
||||||
pairdata.columns = self._columns
|
pairdata.columns = self._columns
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.error(f"Could not load data for {pair}.")
|
logger.error(f"Could not load data for {pair}.")
|
||||||
return DataFrame(columns=self._columns)
|
return DataFrame(columns=self._columns)
|
||||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
pairdata = pairdata.astype(
|
||||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
dtype={
|
||||||
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
"open": "float",
|
||||||
|
"high": "float",
|
||||||
|
"low": "float",
|
||||||
|
"close": "float",
|
||||||
|
"volume": "float",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pairdata["date"] = to_datetime(pairdata["date"], unit="ms", utc=True)
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_append(
|
def ohlcv_append(
|
||||||
self,
|
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
|
||||||
pair: str,
|
|
||||||
timeframe: str,
|
|
||||||
data: DataFrame,
|
|
||||||
candle_type: CandleType
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Append data to existing data structures
|
Append data to existing data structures
|
||||||
@@ -145,5 +148,4 @@ class JsonDataHandler(IDataHandler):
|
|||||||
|
|
||||||
|
|
||||||
class JsonGzDataHandler(JsonDataHandler):
|
class JsonGzDataHandler(JsonDataHandler):
|
||||||
|
|
||||||
_use_zip = True
|
_use_zip = True
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class ParquetDataHandler(IDataHandler):
|
class ParquetDataHandler(IDataHandler):
|
||||||
|
|
||||||
_columns = DEFAULT_DATAFRAME_COLUMNS
|
_columns = DEFAULT_DATAFRAME_COLUMNS
|
||||||
|
|
||||||
def ohlcv_store(
|
def ohlcv_store(
|
||||||
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType) -> None:
|
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:
|
||||||
@@ -34,9 +34,9 @@ class ParquetDataHandler(IDataHandler):
|
|||||||
|
|
||||||
data.reset_index(drop=True).loc[:, self._columns].to_parquet(filename)
|
data.reset_index(drop=True).loc[:, self._columns].to_parquet(filename)
|
||||||
|
|
||||||
def _ohlcv_load(self, pair: str, timeframe: str,
|
def _ohlcv_load(
|
||||||
timerange: Optional[TimeRange], candle_type: CandleType
|
self, pair: str, timeframe: str, 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.
|
||||||
Implements the loading and conversion to a Pandas dataframe.
|
Implements the loading and conversion to a Pandas dataframe.
|
||||||
@@ -49,28 +49,31 @@ class ParquetDataHandler(IDataHandler):
|
|||||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
: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(
|
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type=candle_type)
|
||||||
self._datadir, pair, timeframe, candle_type=candle_type)
|
|
||||||
if not filename.exists():
|
if not filename.exists():
|
||||||
# Fallback mode for 1M files
|
# Fallback mode for 1M files
|
||||||
filename = self._pair_data_filename(
|
filename = self._pair_data_filename(
|
||||||
self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True)
|
self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True
|
||||||
|
)
|
||||||
if not filename.exists():
|
if not filename.exists():
|
||||||
return DataFrame(columns=self._columns)
|
return DataFrame(columns=self._columns)
|
||||||
|
|
||||||
pairdata = read_parquet(filename)
|
pairdata = read_parquet(filename)
|
||||||
pairdata.columns = self._columns
|
pairdata.columns = self._columns
|
||||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
pairdata = pairdata.astype(
|
||||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
dtype={
|
||||||
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
"open": "float",
|
||||||
|
"high": "float",
|
||||||
|
"low": "float",
|
||||||
|
"close": "float",
|
||||||
|
"volume": "float",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pairdata["date"] = to_datetime(pairdata["date"], unit="ms", utc=True)
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_append(
|
def ohlcv_append(
|
||||||
self,
|
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
|
||||||
pair: str,
|
|
||||||
timeframe: str,
|
|
||||||
data: DataFrame,
|
|
||||||
candle_type: CandleType
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Append data to existing data structures
|
Append data to existing data structures
|
||||||
|
|||||||
@@ -7,11 +7,20 @@ from typing import Dict, List, Optional, Tuple
|
|||||||
from pandas import DataFrame, concat
|
from pandas import DataFrame, concat
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS,
|
from freqtrade.constants import (
|
||||||
DL_DATA_TIMEFRAMES, DOCS_LINK, Config)
|
DATETIME_PRINT_FORMAT,
|
||||||
from freqtrade.data.converter import (clean_ohlcv_dataframe, convert_trades_to_ohlcv,
|
DEFAULT_DATAFRAME_COLUMNS,
|
||||||
ohlcv_to_dataframe, trades_df_remove_duplicates,
|
DL_DATA_TIMEFRAMES,
|
||||||
trades_list_to_df)
|
DOCS_LINK,
|
||||||
|
Config,
|
||||||
|
)
|
||||||
|
from freqtrade.data.converter import (
|
||||||
|
clean_ohlcv_dataframe,
|
||||||
|
convert_trades_to_ohlcv,
|
||||||
|
ohlcv_to_dataframe,
|
||||||
|
trades_df_remove_duplicates,
|
||||||
|
trades_list_to_df,
|
||||||
|
)
|
||||||
from freqtrade.data.history.datahandlers import IDataHandler, get_datahandler
|
from freqtrade.data.history.datahandlers import IDataHandler, get_datahandler
|
||||||
from freqtrade.enums import CandleType, TradingMode
|
from freqtrade.enums import CandleType, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
@@ -25,17 +34,19 @@ from freqtrade.util.migrations import migrate_data
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def load_pair_history(pair: str,
|
def load_pair_history(
|
||||||
timeframe: str,
|
pair: str,
|
||||||
datadir: Path, *,
|
timeframe: str,
|
||||||
timerange: Optional[TimeRange] = None,
|
datadir: Path,
|
||||||
fill_up_missing: bool = True,
|
*,
|
||||||
drop_incomplete: bool = False,
|
timerange: Optional[TimeRange] = None,
|
||||||
startup_candles: int = 0,
|
fill_up_missing: bool = True,
|
||||||
data_format: Optional[str] = None,
|
drop_incomplete: bool = False,
|
||||||
data_handler: Optional[IDataHandler] = None,
|
startup_candles: int = 0,
|
||||||
candle_type: CandleType = CandleType.SPOT
|
data_format: Optional[str] = None,
|
||||||
) -> DataFrame:
|
data_handler: Optional[IDataHandler] = None,
|
||||||
|
candle_type: CandleType = CandleType.SPOT,
|
||||||
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Load cached ohlcv history for the given pair.
|
Load cached ohlcv history for the given pair.
|
||||||
|
|
||||||
@@ -54,27 +65,30 @@ def load_pair_history(pair: str,
|
|||||||
"""
|
"""
|
||||||
data_handler = get_datahandler(datadir, data_format, data_handler)
|
data_handler = get_datahandler(datadir, data_format, data_handler)
|
||||||
|
|
||||||
return data_handler.ohlcv_load(pair=pair,
|
return data_handler.ohlcv_load(
|
||||||
timeframe=timeframe,
|
pair=pair,
|
||||||
timerange=timerange,
|
timeframe=timeframe,
|
||||||
fill_missing=fill_up_missing,
|
timerange=timerange,
|
||||||
drop_incomplete=drop_incomplete,
|
fill_missing=fill_up_missing,
|
||||||
startup_candles=startup_candles,
|
drop_incomplete=drop_incomplete,
|
||||||
candle_type=candle_type,
|
startup_candles=startup_candles,
|
||||||
)
|
candle_type=candle_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_data(datadir: Path,
|
def load_data(
|
||||||
timeframe: str,
|
datadir: Path,
|
||||||
pairs: List[str], *,
|
timeframe: str,
|
||||||
timerange: Optional[TimeRange] = None,
|
pairs: List[str],
|
||||||
fill_up_missing: bool = True,
|
*,
|
||||||
startup_candles: int = 0,
|
timerange: Optional[TimeRange] = None,
|
||||||
fail_without_data: bool = False,
|
fill_up_missing: bool = True,
|
||||||
data_format: str = 'feather',
|
startup_candles: int = 0,
|
||||||
candle_type: CandleType = CandleType.SPOT,
|
fail_without_data: bool = False,
|
||||||
user_futures_funding_rate: Optional[int] = None,
|
data_format: str = "feather",
|
||||||
) -> Dict[str, DataFrame]:
|
candle_type: CandleType = CandleType.SPOT,
|
||||||
|
user_futures_funding_rate: Optional[int] = None,
|
||||||
|
) -> Dict[str, DataFrame]:
|
||||||
"""
|
"""
|
||||||
Load ohlcv history data for a list of pairs.
|
Load ohlcv history data for a list of pairs.
|
||||||
|
|
||||||
@@ -91,18 +105,21 @@ def load_data(datadir: Path,
|
|||||||
"""
|
"""
|
||||||
result: Dict[str, DataFrame] = {}
|
result: Dict[str, DataFrame] = {}
|
||||||
if startup_candles > 0 and timerange:
|
if startup_candles > 0 and timerange:
|
||||||
logger.info(f'Using indicator startup period: {startup_candles} ...')
|
logger.info(f"Using indicator startup period: {startup_candles} ...")
|
||||||
|
|
||||||
data_handler = get_datahandler(datadir, data_format)
|
data_handler = get_datahandler(datadir, data_format)
|
||||||
|
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
hist = load_pair_history(pair=pair, timeframe=timeframe,
|
hist = load_pair_history(
|
||||||
datadir=datadir, timerange=timerange,
|
pair=pair,
|
||||||
fill_up_missing=fill_up_missing,
|
timeframe=timeframe,
|
||||||
startup_candles=startup_candles,
|
datadir=datadir,
|
||||||
data_handler=data_handler,
|
timerange=timerange,
|
||||||
candle_type=candle_type,
|
fill_up_missing=fill_up_missing,
|
||||||
)
|
startup_candles=startup_candles,
|
||||||
|
data_handler=data_handler,
|
||||||
|
candle_type=candle_type,
|
||||||
|
)
|
||||||
if not hist.empty:
|
if not hist.empty:
|
||||||
result[pair] = hist
|
result[pair] = hist
|
||||||
else:
|
else:
|
||||||
@@ -116,14 +133,16 @@ def load_data(datadir: Path,
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def refresh_data(*, datadir: Path,
|
def refresh_data(
|
||||||
timeframe: str,
|
*,
|
||||||
pairs: List[str],
|
datadir: Path,
|
||||||
exchange: Exchange,
|
timeframe: str,
|
||||||
data_format: Optional[str] = None,
|
pairs: List[str],
|
||||||
timerange: Optional[TimeRange] = None,
|
exchange: Exchange,
|
||||||
candle_type: CandleType,
|
data_format: Optional[str] = None,
|
||||||
) -> None:
|
timerange: Optional[TimeRange] = None,
|
||||||
|
candle_type: CandleType,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Refresh ohlcv history data for a list of pairs.
|
Refresh ohlcv history data for a list of pairs.
|
||||||
|
|
||||||
@@ -137,11 +156,17 @@ def refresh_data(*, datadir: Path,
|
|||||||
"""
|
"""
|
||||||
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(
|
||||||
timeframe=timeframe, datadir=datadir,
|
pair=pair,
|
||||||
timerange=timerange, exchange=exchange, data_handler=data_handler,
|
process=process,
|
||||||
candle_type=candle_type)
|
timeframe=timeframe,
|
||||||
|
datadir=datadir,
|
||||||
|
timerange=timerange,
|
||||||
|
exchange=exchange,
|
||||||
|
data_handler=data_handler,
|
||||||
|
candle_type=candle_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _load_cached_data_for_updating(
|
def _load_cached_data_for_updating(
|
||||||
@@ -163,42 +188,49 @@ def _load_cached_data_for_updating(
|
|||||||
start = None
|
start = None
|
||||||
end = None
|
end = None
|
||||||
if timerange:
|
if timerange:
|
||||||
if timerange.starttype == 'date':
|
if timerange.starttype == "date":
|
||||||
start = timerange.startdt
|
start = timerange.startdt
|
||||||
if timerange.stoptype == 'date':
|
if timerange.stoptype == "date":
|
||||||
end = timerange.stopdt
|
end = timerange.stopdt
|
||||||
|
|
||||||
# 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(
|
||||||
timerange=None, fill_missing=False,
|
pair,
|
||||||
drop_incomplete=True, warn_no_data=False,
|
timeframe=timeframe,
|
||||||
candle_type=candle_type)
|
timerange=None,
|
||||||
|
fill_missing=False,
|
||||||
|
drop_incomplete=True,
|
||||||
|
warn_no_data=False,
|
||||||
|
candle_type=candle_type,
|
||||||
|
)
|
||||||
if not data.empty:
|
if not data.empty:
|
||||||
if not prepend and start and start < data.iloc[0]['date']:
|
if not prepend and start and start < data.iloc[0]["date"]:
|
||||||
# Earlier data than existing data requested, redownload all
|
# Earlier data than existing data requested, redownload all
|
||||||
data = DataFrame(columns=DEFAULT_DATAFRAME_COLUMNS)
|
data = DataFrame(columns=DEFAULT_DATAFRAME_COLUMNS)
|
||||||
else:
|
else:
|
||||||
if prepend:
|
if prepend:
|
||||||
end = data.iloc[0]['date']
|
end = data.iloc[0]["date"]
|
||||||
else:
|
else:
|
||||||
start = data.iloc[-1]['date']
|
start = data.iloc[-1]["date"]
|
||||||
start_ms = int(start.timestamp() * 1000) if start else None
|
start_ms = int(start.timestamp() * 1000) if start else None
|
||||||
end_ms = int(end.timestamp() * 1000) if end else None
|
end_ms = int(end.timestamp() * 1000) if end else None
|
||||||
return data, start_ms, end_ms
|
return data, start_ms, end_ms
|
||||||
|
|
||||||
|
|
||||||
def _download_pair_history(pair: str, *,
|
def _download_pair_history(
|
||||||
datadir: Path,
|
pair: str,
|
||||||
exchange: Exchange,
|
*,
|
||||||
timeframe: str = '5m',
|
datadir: Path,
|
||||||
process: str = '',
|
exchange: Exchange,
|
||||||
new_pairs_days: int = 30,
|
timeframe: str = "5m",
|
||||||
data_handler: Optional[IDataHandler] = None,
|
process: str = "",
|
||||||
timerange: Optional[TimeRange] = None,
|
new_pairs_days: int = 30,
|
||||||
candle_type: CandleType,
|
data_handler: Optional[IDataHandler] = None,
|
||||||
erase: bool = False,
|
timerange: Optional[TimeRange] = None,
|
||||||
prepend: bool = False,
|
candle_type: CandleType,
|
||||||
) -> bool:
|
erase: bool = False,
|
||||||
|
prepend: 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
|
||||||
@@ -217,54 +249,71 @@ def _download_pair_history(pair: str, *,
|
|||||||
try:
|
try:
|
||||||
if erase:
|
if erase:
|
||||||
if data_handler.ohlcv_purge(pair, timeframe, candle_type=candle_type):
|
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(f"Deleting existing data for pair {pair}, {timeframe}, {candle_type}.")
|
||||||
|
|
||||||
data, since_ms, until_ms = _load_cached_data_for_updating(
|
data, since_ms, until_ms = _load_cached_data_for_updating(
|
||||||
pair, timeframe, timerange,
|
pair,
|
||||||
|
timeframe,
|
||||||
|
timerange,
|
||||||
data_handler=data_handler,
|
data_handler=data_handler,
|
||||||
candle_type=candle_type,
|
candle_type=candle_type,
|
||||||
prepend=prepend)
|
prepend=prepend,
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(f'({process}) - Download history data for "{pair}", {timeframe}, '
|
logger.info(
|
||||||
f'{candle_type} and store in {datadir}. '
|
f'({process}) - Download history data for "{pair}", {timeframe}, '
|
||||||
f'From {format_ms_time(since_ms) if since_ms else "start"} to '
|
f"{candle_type} and store in {datadir}. "
|
||||||
f'{format_ms_time(until_ms) if until_ms else "now"}'
|
f'From {format_ms_time(since_ms) if since_ms else "start"} to '
|
||||||
)
|
f'{format_ms_time(until_ms) if until_ms else "now"}'
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug("Current Start: %s",
|
logger.debug(
|
||||||
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}"
|
"Current Start: %s",
|
||||||
if not data.empty else 'None')
|
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}" if not data.empty else "None",
|
||||||
logger.debug("Current End: %s",
|
)
|
||||||
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}"
|
logger.debug(
|
||||||
if not data.empty else 'None')
|
"Current End: %s",
|
||||||
|
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}" if not data.empty else "None",
|
||||||
|
)
|
||||||
|
|
||||||
# Default since_ms to 30 days if nothing is given
|
# Default since_ms to 30 days if nothing is given
|
||||||
new_data = exchange.get_historic_ohlcv(pair=pair,
|
new_data = exchange.get_historic_ohlcv(
|
||||||
timeframe=timeframe,
|
pair=pair,
|
||||||
since_ms=since_ms if since_ms else
|
timeframe=timeframe,
|
||||||
int((datetime.now() - timedelta(days=new_pairs_days)
|
since_ms=(
|
||||||
).timestamp()) * 1000,
|
since_ms
|
||||||
is_new_pair=data.empty,
|
if since_ms
|
||||||
candle_type=candle_type,
|
else int((datetime.now() - timedelta(days=new_pairs_days)).timestamp()) * 1000
|
||||||
until_ms=until_ms if until_ms else None
|
),
|
||||||
)
|
is_new_pair=data.empty,
|
||||||
|
candle_type=candle_type,
|
||||||
|
until_ms=until_ms if until_ms else None,
|
||||||
|
)
|
||||||
# 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(
|
||||||
fill_missing=False, drop_incomplete=True)
|
new_data, timeframe, pair, fill_missing=False, drop_incomplete=True
|
||||||
|
)
|
||||||
if data.empty:
|
if data.empty:
|
||||||
data = new_dataframe
|
data = new_dataframe
|
||||||
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(concat([data, new_dataframe], axis=0), timeframe, pair,
|
data = clean_ohlcv_dataframe(
|
||||||
fill_missing=False, drop_incomplete=False)
|
concat([data, new_dataframe], axis=0),
|
||||||
|
timeframe,
|
||||||
|
pair,
|
||||||
|
fill_missing=False,
|
||||||
|
drop_incomplete=False,
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug("New Start: %s",
|
logger.debug(
|
||||||
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}"
|
"New Start: %s",
|
||||||
if not data.empty else 'None')
|
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}" if not data.empty else "None",
|
||||||
logger.debug("New End: %s",
|
)
|
||||||
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}"
|
logger.debug(
|
||||||
if not data.empty else 'None')
|
"New End: %s",
|
||||||
|
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}" if not data.empty else "None",
|
||||||
|
)
|
||||||
|
|
||||||
data_handler.ohlcv_store(pair, timeframe, data=data, candle_type=candle_type)
|
data_handler.ohlcv_store(pair, timeframe, data=data, candle_type=candle_type)
|
||||||
return True
|
return True
|
||||||
@@ -276,13 +325,18 @@ def _download_pair_history(pair: str, *,
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str],
|
def refresh_backtest_ohlcv_data(
|
||||||
datadir: Path, trading_mode: str,
|
exchange: Exchange,
|
||||||
timerange: Optional[TimeRange] = None,
|
pairs: List[str],
|
||||||
new_pairs_days: int = 30, erase: bool = False,
|
timeframes: List[str],
|
||||||
data_format: Optional[str] = None,
|
datadir: Path,
|
||||||
prepend: bool = False,
|
trading_mode: str,
|
||||||
) -> List[str]:
|
timerange: Optional[TimeRange] = None,
|
||||||
|
new_pairs_days: int = 30,
|
||||||
|
erase: bool = False,
|
||||||
|
data_format: Optional[str] = None,
|
||||||
|
prepend: bool = False,
|
||||||
|
) -> 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.
|
||||||
@@ -291,63 +345,77 @@ 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)
|
candle_type = CandleType.get_default(trading_mode)
|
||||||
process = ''
|
process = ""
|
||||||
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)
|
||||||
logger.info(f"Skipping pair {pair}...")
|
logger.info(f"Skipping pair {pair}...")
|
||||||
continue
|
continue
|
||||||
for timeframe in timeframes:
|
for timeframe in timeframes:
|
||||||
|
logger.debug(f"Downloading pair {pair}, {candle_type}, interval {timeframe}.")
|
||||||
logger.debug(f'Downloading pair {pair}, {candle_type}, interval {timeframe}.')
|
process = f"{idx}/{len(pairs)}"
|
||||||
process = f'{idx}/{len(pairs)}'
|
_download_pair_history(
|
||||||
_download_pair_history(pair=pair, process=process,
|
pair=pair,
|
||||||
datadir=datadir, exchange=exchange,
|
process=process,
|
||||||
timerange=timerange, data_handler=data_handler,
|
datadir=datadir,
|
||||||
timeframe=str(timeframe), new_pairs_days=new_pairs_days,
|
exchange=exchange,
|
||||||
candle_type=candle_type,
|
timerange=timerange,
|
||||||
erase=erase, prepend=prepend)
|
data_handler=data_handler,
|
||||||
if trading_mode == 'futures':
|
timeframe=str(timeframe),
|
||||||
|
new_pairs_days=new_pairs_days,
|
||||||
|
candle_type=candle_type,
|
||||||
|
erase=erase,
|
||||||
|
prepend=prepend,
|
||||||
|
)
|
||||||
|
if trading_mode == "futures":
|
||||||
# Predefined candletype (and timeframe) depending on exchange
|
# Predefined candletype (and timeframe) depending on exchange
|
||||||
# Downloads what is necessary to backtest based on futures data.
|
# Downloads what is necessary to backtest based on futures data.
|
||||||
tf_mark = exchange.get_option('mark_ohlcv_timeframe')
|
tf_mark = exchange.get_option("mark_ohlcv_timeframe")
|
||||||
tf_funding_rate = exchange.get_option('funding_fee_timeframe')
|
tf_funding_rate = exchange.get_option("funding_fee_timeframe")
|
||||||
|
|
||||||
fr_candle_type = CandleType.from_string(exchange.get_option('mark_ohlcv_price'))
|
fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price"))
|
||||||
# All exchanges need FundingRate for futures trading.
|
# All exchanges need FundingRate for futures trading.
|
||||||
# The timeframe is aligned to the mark-price timeframe.
|
# The timeframe is aligned to the mark-price timeframe.
|
||||||
combs = ((CandleType.FUNDING_RATE, tf_funding_rate), (fr_candle_type, tf_mark))
|
combs = ((CandleType.FUNDING_RATE, tf_funding_rate), (fr_candle_type, tf_mark))
|
||||||
for candle_type_f, tf in combs:
|
for candle_type_f, tf in combs:
|
||||||
logger.debug(f'Downloading pair {pair}, {candle_type_f}, interval {tf}.')
|
logger.debug(f"Downloading pair {pair}, {candle_type_f}, interval {tf}.")
|
||||||
_download_pair_history(pair=pair, process=process,
|
_download_pair_history(
|
||||||
datadir=datadir, exchange=exchange,
|
pair=pair,
|
||||||
timerange=timerange, data_handler=data_handler,
|
process=process,
|
||||||
timeframe=str(tf), new_pairs_days=new_pairs_days,
|
datadir=datadir,
|
||||||
candle_type=candle_type_f,
|
exchange=exchange,
|
||||||
erase=erase, prepend=prepend)
|
timerange=timerange,
|
||||||
|
data_handler=data_handler,
|
||||||
|
timeframe=str(tf),
|
||||||
|
new_pairs_days=new_pairs_days,
|
||||||
|
candle_type=candle_type_f,
|
||||||
|
erase=erase,
|
||||||
|
prepend=prepend,
|
||||||
|
)
|
||||||
|
|
||||||
return pairs_not_available
|
return pairs_not_available
|
||||||
|
|
||||||
|
|
||||||
def _download_trades_history(exchange: Exchange,
|
def _download_trades_history(
|
||||||
pair: str, *,
|
exchange: Exchange,
|
||||||
new_pairs_days: int = 30,
|
pair: str,
|
||||||
timerange: Optional[TimeRange] = None,
|
*,
|
||||||
data_handler: IDataHandler,
|
new_pairs_days: int = 30,
|
||||||
trading_mode: TradingMode,
|
timerange: Optional[TimeRange] = None,
|
||||||
) -> bool:
|
data_handler: IDataHandler,
|
||||||
|
trading_mode: TradingMode,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Download trade history from the exchange.
|
Download trade history from the exchange.
|
||||||
Appends to previously downloaded trades data.
|
Appends to previously downloaded trades data.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
|
||||||
until = None
|
until = None
|
||||||
since = 0
|
since = 0
|
||||||
if timerange:
|
if timerange:
|
||||||
if timerange.starttype == 'date':
|
if timerange.starttype == "date":
|
||||||
since = timerange.startts * 1000
|
since = timerange.startts * 1000
|
||||||
if timerange.stoptype == 'date':
|
if timerange.stoptype == "date":
|
||||||
until = timerange.stopts * 1000
|
until = timerange.stopts * 1000
|
||||||
|
|
||||||
trades = data_handler.trades_load(pair, trading_mode)
|
trades = data_handler.trades_load(pair, trading_mode)
|
||||||
@@ -356,45 +424,58 @@ def _download_trades_history(exchange: Exchange,
|
|||||||
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
||||||
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
||||||
|
|
||||||
if not trades.empty and since > 0 and since < trades.iloc[0]['timestamp']:
|
if not trades.empty and since > 0 and since < trades.iloc[0]["timestamp"]:
|
||||||
# since is before the first trade
|
# since is before the first trade
|
||||||
logger.info(f"Start ({trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}) earlier than "
|
logger.info(
|
||||||
f"available data. Redownloading trades for {pair}...")
|
f"Start ({trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}) earlier than "
|
||||||
|
f"available data. Redownloading trades for {pair}..."
|
||||||
|
)
|
||||||
trades = trades_list_to_df([])
|
trades = trades_list_to_df([])
|
||||||
|
|
||||||
from_id = trades.iloc[-1]['id'] if not trades.empty else None
|
from_id = trades.iloc[-1]["id"] if not trades.empty else None
|
||||||
if not trades.empty and since < trades.iloc[-1]['timestamp']:
|
if not trades.empty and since < trades.iloc[-1]["timestamp"]:
|
||||||
# Reset since to the last available point
|
# Reset since to the last available point
|
||||||
# - 5 seconds (to ensure we're getting all trades)
|
# - 5 seconds (to ensure we're getting all trades)
|
||||||
since = trades.iloc[-1]['timestamp'] - (5 * 1000)
|
since = trades.iloc[-1]["timestamp"] - (5 * 1000)
|
||||||
logger.info(f"Using last trade date -5s - Downloading trades for {pair} "
|
logger.info(
|
||||||
f"since: {format_ms_time(since)}.")
|
f"Using last trade date -5s - Downloading trades for {pair} "
|
||||||
|
f"since: {format_ms_time(since)}."
|
||||||
|
)
|
||||||
|
|
||||||
if not since:
|
if not since:
|
||||||
since = dt_ts(dt_now() - timedelta(days=new_pairs_days))
|
since = dt_ts(dt_now() - timedelta(days=new_pairs_days))
|
||||||
|
|
||||||
logger.debug("Current Start: %s", 'None' if trades.empty else
|
logger.debug(
|
||||||
f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}")
|
"Current Start: %s",
|
||||||
logger.debug("Current End: %s", 'None' if trades.empty else
|
"None" if trades.empty else f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}",
|
||||||
f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}")
|
)
|
||||||
|
logger.debug(
|
||||||
|
"Current End: %s",
|
||||||
|
"None" if trades.empty else f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}",
|
||||||
|
)
|
||||||
logger.info(f"Current Amount of trades: {len(trades)}")
|
logger.info(f"Current Amount of trades: {len(trades)}")
|
||||||
|
|
||||||
# Default since_ms to 30 days if nothing is given
|
# Default since_ms to 30 days if nothing is given
|
||||||
new_trades = exchange.get_historic_trades(pair=pair,
|
new_trades = exchange.get_historic_trades(
|
||||||
since=since,
|
pair=pair,
|
||||||
until=until,
|
since=since,
|
||||||
from_id=from_id,
|
until=until,
|
||||||
)
|
from_id=from_id,
|
||||||
|
)
|
||||||
new_trades_df = trades_list_to_df(new_trades[1])
|
new_trades_df = trades_list_to_df(new_trades[1])
|
||||||
trades = concat([trades, new_trades_df], axis=0)
|
trades = concat([trades, new_trades_df], axis=0)
|
||||||
# Remove duplicates to make sure we're not storing data we don't need
|
# Remove duplicates to make sure we're not storing data we don't need
|
||||||
trades = trades_df_remove_duplicates(trades)
|
trades = trades_df_remove_duplicates(trades)
|
||||||
data_handler.trades_store(pair, trades, trading_mode)
|
data_handler.trades_store(pair, trades, trading_mode)
|
||||||
|
|
||||||
logger.debug("New Start: %s", 'None' if trades.empty else
|
logger.debug(
|
||||||
f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}")
|
"New Start: %s",
|
||||||
logger.debug("New End: %s", 'None' if trades.empty else
|
"None" if trades.empty else f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}",
|
||||||
f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}")
|
)
|
||||||
|
logger.debug(
|
||||||
|
"New End: %s",
|
||||||
|
"None" if trades.empty else f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}",
|
||||||
|
)
|
||||||
logger.info(f"New Amount of trades: {len(trades)}")
|
logger.info(f"New Amount of trades: {len(trades)}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -405,11 +486,16 @@ def _download_trades_history(exchange: Exchange,
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path,
|
def refresh_backtest_trades_data(
|
||||||
timerange: TimeRange, trading_mode: TradingMode,
|
exchange: Exchange,
|
||||||
new_pairs_days: int = 30,
|
pairs: List[str],
|
||||||
erase: bool = False, data_format: str = 'feather',
|
datadir: Path,
|
||||||
) -> List[str]:
|
timerange: TimeRange,
|
||||||
|
trading_mode: TradingMode,
|
||||||
|
new_pairs_days: int = 30,
|
||||||
|
erase: bool = False,
|
||||||
|
data_format: str = "feather",
|
||||||
|
) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Refresh stored trades data for backtesting and hyperopt operations.
|
Refresh stored trades data for backtesting and hyperopt operations.
|
||||||
Used by freqtrade download-data subcommand.
|
Used by freqtrade download-data subcommand.
|
||||||
@@ -425,15 +511,17 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir:
|
|||||||
|
|
||||||
if erase:
|
if erase:
|
||||||
if data_handler.trades_purge(pair, trading_mode):
|
if data_handler.trades_purge(pair, trading_mode):
|
||||||
logger.info(f'Deleting existing data for pair {pair}.')
|
logger.info(f"Deleting existing data for pair {pair}.")
|
||||||
|
|
||||||
logger.info(f'Downloading trades for pair {pair}.')
|
logger.info(f"Downloading trades for pair {pair}.")
|
||||||
_download_trades_history(exchange=exchange,
|
_download_trades_history(
|
||||||
pair=pair,
|
exchange=exchange,
|
||||||
new_pairs_days=new_pairs_days,
|
pair=pair,
|
||||||
timerange=timerange,
|
new_pairs_days=new_pairs_days,
|
||||||
data_handler=data_handler,
|
timerange=timerange,
|
||||||
trading_mode=trading_mode)
|
data_handler=data_handler,
|
||||||
|
trading_mode=trading_mode,
|
||||||
|
)
|
||||||
return pairs_not_available
|
return pairs_not_available
|
||||||
|
|
||||||
|
|
||||||
@@ -445,15 +533,18 @@ def get_timerange(data: Dict[str, DataFrame]) -> Tuple[datetime, datetime]:
|
|||||||
:return: tuple containing min_date, max_date
|
:return: tuple containing min_date, max_date
|
||||||
"""
|
"""
|
||||||
timeranges = [
|
timeranges = [
|
||||||
(frame['date'].min().to_pydatetime(), frame['date'].max().to_pydatetime())
|
(frame["date"].min().to_pydatetime(), frame["date"].max().to_pydatetime())
|
||||||
for frame in data.values()
|
for frame in data.values()
|
||||||
]
|
]
|
||||||
return (min(timeranges, key=operator.itemgetter(0))[0],
|
return (
|
||||||
max(timeranges, key=operator.itemgetter(1))[1])
|
min(timeranges, key=operator.itemgetter(0))[0],
|
||||||
|
max(timeranges, key=operator.itemgetter(1))[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime,
|
def validate_backtest_data(
|
||||||
max_date: datetime, timeframe_min: int) -> bool:
|
data: DataFrame, pair: str, min_date: datetime, max_date: datetime, timeframe_min: int
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Validates preprocessed backtesting data for missing values and shows warnings about it that.
|
Validates preprocessed backtesting data for missing values and shows warnings about it that.
|
||||||
|
|
||||||
@@ -469,89 +560,111 @@ def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime,
|
|||||||
dflen = len(data)
|
dflen = len(data)
|
||||||
if dflen < expected_frames:
|
if dflen < expected_frames:
|
||||||
found_missing = True
|
found_missing = True
|
||||||
logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values",
|
logger.warning(
|
||||||
pair, expected_frames, dflen, expected_frames - dflen)
|
"%s has missing frames: expected %s, got %s, that's %s missing values",
|
||||||
|
pair,
|
||||||
|
expected_frames,
|
||||||
|
dflen,
|
||||||
|
expected_frames - dflen,
|
||||||
|
)
|
||||||
return found_missing
|
return found_missing
|
||||||
|
|
||||||
|
|
||||||
def download_data_main(config: Config) -> None:
|
def download_data_main(config: Config) -> None:
|
||||||
|
|
||||||
timerange = TimeRange()
|
timerange = TimeRange()
|
||||||
if 'days' in config:
|
if "days" in config:
|
||||||
time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d")
|
time_since = (datetime.now() - timedelta(days=config["days"])).strftime("%Y%m%d")
|
||||||
timerange = TimeRange.parse_timerange(f'{time_since}-')
|
timerange = TimeRange.parse_timerange(f"{time_since}-")
|
||||||
|
|
||||||
if 'timerange' in config:
|
if "timerange" in config:
|
||||||
timerange = timerange.parse_timerange(config['timerange'])
|
timerange = timerange.parse_timerange(config["timerange"])
|
||||||
|
|
||||||
# Remove stake-currency to skip checks which are not relevant for datadownload
|
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||||
config['stake_currency'] = ''
|
config["stake_currency"] = ""
|
||||||
|
|
||||||
pairs_not_available: List[str] = []
|
pairs_not_available: List[str] = []
|
||||||
|
|
||||||
# Init exchange
|
# Init exchange
|
||||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||||
|
|
||||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
available_pairs = [
|
available_pairs = [
|
||||||
p for p in exchange.get_markets(
|
p
|
||||||
tradable_only=True, active_only=not config.get('include_inactive')
|
for p in exchange.get_markets(
|
||||||
).keys()
|
tradable_only=True, active_only=not config.get("include_inactive")
|
||||||
|
).keys()
|
||||||
]
|
]
|
||||||
|
|
||||||
expanded_pairs = dynamic_expand_pairlist(config, available_pairs)
|
expanded_pairs = dynamic_expand_pairlist(config, available_pairs)
|
||||||
if 'timeframes' not in config:
|
if "timeframes" not in config:
|
||||||
config['timeframes'] = DL_DATA_TIMEFRAMES
|
config["timeframes"] = DL_DATA_TIMEFRAMES
|
||||||
|
|
||||||
# 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(expanded_pairs)
|
exchange.validate_pairs(expanded_pairs)
|
||||||
logger.info(f"About to download pairs: {expanded_pairs}, "
|
logger.info(
|
||||||
f"intervals: {config['timeframes']} to {config['datadir']}")
|
f"About to download pairs: {expanded_pairs}, "
|
||||||
|
f"intervals: {config['timeframes']} to {config['datadir']}"
|
||||||
|
)
|
||||||
|
|
||||||
if len(expanded_pairs) == 0:
|
if len(expanded_pairs) == 0:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"No pairs available for download. "
|
"No pairs available for download. "
|
||||||
"Please make sure you're using the correct Pair naming for your selected trade mode. \n"
|
"Please make sure you're using the correct Pair naming for your selected trade mode. \n"
|
||||||
f"More info: {DOCS_LINK}/bot-basics/#pair-naming")
|
f"More info: {DOCS_LINK}/bot-basics/#pair-naming"
|
||||||
|
)
|
||||||
|
|
||||||
for timeframe in config['timeframes']:
|
for timeframe in config["timeframes"]:
|
||||||
exchange.validate_timeframes(timeframe)
|
exchange.validate_timeframes(timeframe)
|
||||||
|
|
||||||
# Start downloading
|
# Start downloading
|
||||||
try:
|
try:
|
||||||
if config.get('download_trades'):
|
if config.get("download_trades"):
|
||||||
pairs_not_available = refresh_backtest_trades_data(
|
pairs_not_available = refresh_backtest_trades_data(
|
||||||
exchange, pairs=expanded_pairs, datadir=config['datadir'],
|
exchange,
|
||||||
timerange=timerange, new_pairs_days=config['new_pairs_days'],
|
pairs=expanded_pairs,
|
||||||
erase=bool(config.get('erase')), data_format=config['dataformat_trades'],
|
datadir=config["datadir"],
|
||||||
trading_mode=config.get('trading_mode', TradingMode.SPOT),
|
timerange=timerange,
|
||||||
)
|
new_pairs_days=config["new_pairs_days"],
|
||||||
|
erase=bool(config.get("erase")),
|
||||||
|
data_format=config["dataformat_trades"],
|
||||||
|
trading_mode=config.get("trading_mode", TradingMode.SPOT),
|
||||||
|
)
|
||||||
|
|
||||||
# Convert downloaded trade data to different timeframes
|
# Convert downloaded trade data to different timeframes
|
||||||
convert_trades_to_ohlcv(
|
convert_trades_to_ohlcv(
|
||||||
pairs=expanded_pairs, timeframes=config['timeframes'],
|
pairs=expanded_pairs,
|
||||||
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
|
timeframes=config["timeframes"],
|
||||||
data_format_ohlcv=config['dataformat_ohlcv'],
|
datadir=config["datadir"],
|
||||||
data_format_trades=config['dataformat_trades'],
|
timerange=timerange,
|
||||||
candle_type=config.get('candle_type_def', CandleType.SPOT),
|
erase=bool(config.get("erase")),
|
||||||
|
data_format_ohlcv=config["dataformat_ohlcv"],
|
||||||
|
data_format_trades=config["dataformat_trades"],
|
||||||
|
candle_type=config.get("candle_type_def", CandleType.SPOT),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if not exchange.get_option('ohlcv_has_history', True):
|
if not exchange.get_option("ohlcv_has_history", True):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"Historic klines not available for {exchange.name}. "
|
f"Historic klines not available for {exchange.name}. "
|
||||||
"Please use `--dl-trades` instead for this exchange "
|
"Please use `--dl-trades` instead for this exchange "
|
||||||
"(will unfortunately take a long time)."
|
"(will unfortunately take a long time)."
|
||||||
)
|
)
|
||||||
migrate_data(config, exchange)
|
migrate_data(config, exchange)
|
||||||
pairs_not_available = refresh_backtest_ohlcv_data(
|
pairs_not_available = refresh_backtest_ohlcv_data(
|
||||||
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
exchange,
|
||||||
datadir=config['datadir'], timerange=timerange,
|
pairs=expanded_pairs,
|
||||||
new_pairs_days=config['new_pairs_days'],
|
timeframes=config["timeframes"],
|
||||||
erase=bool(config.get('erase')), data_format=config['dataformat_ohlcv'],
|
datadir=config["datadir"],
|
||||||
trading_mode=config.get('trading_mode', 'spot'),
|
timerange=timerange,
|
||||||
prepend=config.get('prepend_data', False)
|
new_pairs_days=config["new_pairs_days"],
|
||||||
|
erase=bool(config.get("erase")),
|
||||||
|
data_format=config["dataformat_ohlcv"],
|
||||||
|
trading_mode=config.get("trading_mode", "spot"),
|
||||||
|
prepend=config.get("prepend_data", False),
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
if pairs_not_available:
|
if pairs_not_available:
|
||||||
logger.info(f"Pairs [{','.join(pairs_not_available)}] not available "
|
logger.info(
|
||||||
f"on exchange {exchange.name}.")
|
f"Pairs [{','.join(pairs_not_available)}] not available "
|
||||||
|
f"on exchange {exchange.name}."
|
||||||
|
)
|
||||||
|
|||||||
@@ -30,8 +30,27 @@ def calculate_market_change(data: Dict[str, pd.DataFrame], column: str = "close"
|
|||||||
return float(np.mean(tmp_means))
|
return float(np.mean(tmp_means))
|
||||||
|
|
||||||
|
|
||||||
def combine_dataframes_with_mean(data: Dict[str, pd.DataFrame],
|
def combine_dataframes_by_column(
|
||||||
column: str = "close") -> pd.DataFrame:
|
data: Dict[str, pd.DataFrame], column: str = "close"
|
||||||
|
) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Combine multiple dataframes "column"
|
||||||
|
:param data: Dict of Dataframes, dict key should be pair.
|
||||||
|
:param column: Column in the original dataframes to use
|
||||||
|
:return: DataFrame with the column renamed to the dict key.
|
||||||
|
:raise: ValueError if no data is provided.
|
||||||
|
"""
|
||||||
|
if not data:
|
||||||
|
raise ValueError("No data provided.")
|
||||||
|
df_comb = pd.concat(
|
||||||
|
[data[pair].set_index("date").rename({column: pair}, axis=1)[pair] for pair in data], axis=1
|
||||||
|
)
|
||||||
|
return df_comb
|
||||||
|
|
||||||
|
|
||||||
|
def combined_dataframes_with_rel_mean(
|
||||||
|
data: Dict[str, pd.DataFrame], fromdt: datetime, todt: datetime, column: str = "close"
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Combine multiple dataframes "column"
|
Combine multiple dataframes "column"
|
||||||
:param data: Dict of Dataframes, dict key should be pair.
|
:param data: Dict of Dataframes, dict key should be pair.
|
||||||
@@ -40,16 +59,36 @@ def combine_dataframes_with_mean(data: Dict[str, pd.DataFrame],
|
|||||||
named mean, containing the mean of all pairs.
|
named mean, containing the mean of all pairs.
|
||||||
:raise: ValueError if no data is provided.
|
:raise: ValueError if no data is provided.
|
||||||
"""
|
"""
|
||||||
df_comb = pd.concat([data[pair].set_index('date').rename(
|
df_comb = combine_dataframes_by_column(data, column)
|
||||||
{column: pair}, axis=1)[pair] for pair in data], axis=1)
|
# Trim dataframes to the given timeframe
|
||||||
|
df_comb = df_comb.iloc[(df_comb.index >= fromdt) & (df_comb.index < todt)]
|
||||||
|
df_comb["count"] = df_comb.count(axis=1)
|
||||||
|
df_comb["mean"] = df_comb.mean(axis=1)
|
||||||
|
df_comb["rel_mean"] = df_comb["mean"].pct_change().fillna(0).cumsum()
|
||||||
|
return df_comb[["mean", "rel_mean", "count"]]
|
||||||
|
|
||||||
df_comb['mean'] = df_comb.mean(axis=1)
|
|
||||||
|
def combine_dataframes_with_mean(
|
||||||
|
data: Dict[str, pd.DataFrame], column: str = "close"
|
||||||
|
) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Combine multiple dataframes "column"
|
||||||
|
:param data: Dict of Dataframes, dict key should be pair.
|
||||||
|
:param column: Column in the original dataframes to use
|
||||||
|
:return: DataFrame with the column renamed to the dict key, and a column
|
||||||
|
named mean, containing the mean of all pairs.
|
||||||
|
:raise: ValueError if no data is provided.
|
||||||
|
"""
|
||||||
|
df_comb = combine_dataframes_by_column(data, column)
|
||||||
|
|
||||||
|
df_comb["mean"] = df_comb.mean(axis=1)
|
||||||
|
|
||||||
return df_comb
|
return df_comb
|
||||||
|
|
||||||
|
|
||||||
def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
def create_cum_profit(
|
||||||
timeframe: str) -> pd.DataFrame:
|
df: pd.DataFrame, trades: pd.DataFrame, col_name: str, timeframe: str
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Adds a column `col_name` with the cumulative profit for the given trades array.
|
Adds a column `col_name` with the cumulative profit for the given trades array.
|
||||||
:param df: DataFrame with date index
|
:param df: DataFrame with date index
|
||||||
@@ -62,11 +101,11 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
|||||||
if len(trades) == 0:
|
if len(trades) == 0:
|
||||||
raise ValueError("Trade dataframe empty.")
|
raise ValueError("Trade dataframe empty.")
|
||||||
from freqtrade.exchange import timeframe_to_resample_freq
|
from freqtrade.exchange import timeframe_to_resample_freq
|
||||||
|
|
||||||
timeframe_freq = timeframe_to_resample_freq(timeframe)
|
timeframe_freq = timeframe_to_resample_freq(timeframe)
|
||||||
# Resample to timeframe to make sure trades match candles
|
# Resample to timeframe to make sure trades match candles
|
||||||
_trades_sum = trades.resample(timeframe_freq, on='close_date'
|
_trades_sum = trades.resample(timeframe_freq, on="close_date")[["profit_abs"]].sum()
|
||||||
)[['profit_abs']].sum()
|
df.loc[:, col_name] = _trades_sum["profit_abs"].cumsum()
|
||||||
df.loc[:, col_name] = _trades_sum['profit_abs'].cumsum()
|
|
||||||
# Set first value to 0
|
# Set first value to 0
|
||||||
df.loc[df.iloc[0].name, col_name] = 0
|
df.loc[df.iloc[0].name, col_name] = 0
|
||||||
# FFill to get continuous
|
# FFill to get continuous
|
||||||
@@ -74,29 +113,34 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
|||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_col: str,
|
def _calc_drawdown_series(
|
||||||
starting_balance: float) -> pd.DataFrame:
|
profit_results: pd.DataFrame, *, date_col: str, value_col: str, starting_balance: float
|
||||||
|
) -> pd.DataFrame:
|
||||||
max_drawdown_df = pd.DataFrame()
|
max_drawdown_df = pd.DataFrame()
|
||||||
max_drawdown_df['cumulative'] = profit_results[value_col].cumsum()
|
max_drawdown_df["cumulative"] = profit_results[value_col].cumsum()
|
||||||
max_drawdown_df['high_value'] = max_drawdown_df['cumulative'].cummax()
|
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["drawdown"] = max_drawdown_df["cumulative"] - max_drawdown_df["high_value"]
|
||||||
max_drawdown_df['date'] = profit_results.loc[:, date_col]
|
max_drawdown_df["date"] = profit_results.loc[:, date_col]
|
||||||
if starting_balance:
|
if starting_balance:
|
||||||
cumulative_balance = starting_balance + max_drawdown_df['cumulative']
|
cumulative_balance = starting_balance + max_drawdown_df["cumulative"]
|
||||||
max_balance = starting_balance + max_drawdown_df['high_value']
|
max_balance = starting_balance + max_drawdown_df["high_value"]
|
||||||
max_drawdown_df['drawdown_relative'] = ((max_balance - cumulative_balance) / max_balance)
|
max_drawdown_df["drawdown_relative"] = (max_balance - cumulative_balance) / max_balance
|
||||||
else:
|
else:
|
||||||
# NOTE: This is not completely accurate,
|
# NOTE: This is not completely accurate,
|
||||||
# but might good enough if starting_balance is not available
|
# but might good enough if starting_balance is not available
|
||||||
max_drawdown_df['drawdown_relative'] = (
|
max_drawdown_df["drawdown_relative"] = (
|
||||||
(max_drawdown_df['high_value'] - max_drawdown_df['cumulative'])
|
max_drawdown_df["high_value"] - max_drawdown_df["cumulative"]
|
||||||
/ max_drawdown_df['high_value'])
|
) / max_drawdown_df["high_value"]
|
||||||
return max_drawdown_df
|
return max_drawdown_df
|
||||||
|
|
||||||
|
|
||||||
def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
def calculate_underwater(
|
||||||
value_col: str = 'profit_ratio', starting_balance: float = 0.0
|
trades: pd.DataFrame,
|
||||||
):
|
*,
|
||||||
|
date_col: str = "close_date",
|
||||||
|
value_col: str = "profit_ratio",
|
||||||
|
starting_balance: float = 0.0,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
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)
|
||||||
@@ -110,18 +154,20 @@ def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
|||||||
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 = _calc_drawdown_series(
|
max_drawdown_df = _calc_drawdown_series(
|
||||||
profit_results,
|
profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance
|
||||||
date_col=date_col,
|
)
|
||||||
value_col=value_col,
|
|
||||||
starting_balance=starting_balance)
|
|
||||||
|
|
||||||
return max_drawdown_df
|
return max_drawdown_df
|
||||||
|
|
||||||
|
|
||||||
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
def calculate_max_drawdown(
|
||||||
value_col: str = 'profit_abs', starting_balance: float = 0,
|
trades: pd.DataFrame,
|
||||||
relative: bool = False
|
*,
|
||||||
) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float, float]:
|
date_col: str = "close_date",
|
||||||
|
value_col: str = "profit_abs",
|
||||||
|
starting_balance: float = 0,
|
||||||
|
relative: bool = False,
|
||||||
|
) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float, float]:
|
||||||
"""
|
"""
|
||||||
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)
|
||||||
@@ -137,32 +183,31 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date'
|
|||||||
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 = _calc_drawdown_series(
|
max_drawdown_df = _calc_drawdown_series(
|
||||||
profit_results,
|
profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance
|
||||||
date_col=date_col,
|
|
||||||
value_col=value_col,
|
|
||||||
starting_balance=starting_balance
|
|
||||||
)
|
)
|
||||||
|
|
||||||
idxmin = (
|
idxmin = (
|
||||||
max_drawdown_df['drawdown_relative'].idxmax()
|
max_drawdown_df["drawdown_relative"].idxmax()
|
||||||
if relative else max_drawdown_df['drawdown'].idxmin()
|
if relative
|
||||||
|
else max_drawdown_df["drawdown"].idxmin()
|
||||||
)
|
)
|
||||||
if idxmin == 0:
|
if idxmin == 0:
|
||||||
raise ValueError("No losing trade, therefore no drawdown.")
|
raise ValueError("No losing trade, therefore no drawdown.")
|
||||||
high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col]
|
high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]["high_value"].idxmax(), date_col]
|
||||||
low_date = profit_results.loc[idxmin, date_col]
|
low_date = profit_results.loc[idxmin, date_col]
|
||||||
high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin]
|
high_val = max_drawdown_df.loc[
|
||||||
['high_value'].idxmax(), 'cumulative']
|
max_drawdown_df.iloc[:idxmin]["high_value"].idxmax(), "cumulative"
|
||||||
low_val = max_drawdown_df.loc[idxmin, 'cumulative']
|
]
|
||||||
max_drawdown_rel = max_drawdown_df.loc[idxmin, 'drawdown_relative']
|
low_val = max_drawdown_df.loc[idxmin, "cumulative"]
|
||||||
|
max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
abs(max_drawdown_df.loc[idxmin, 'drawdown']),
|
abs(max_drawdown_df.loc[idxmin, "drawdown"]),
|
||||||
high_date,
|
high_date,
|
||||||
low_date,
|
low_date,
|
||||||
high_val,
|
high_val,
|
||||||
low_val,
|
low_val,
|
||||||
max_drawdown_rel
|
max_drawdown_rel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -178,9 +223,9 @@ def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[f
|
|||||||
raise ValueError("Trade dataframe empty.")
|
raise ValueError("Trade dataframe empty.")
|
||||||
|
|
||||||
csum_df = pd.DataFrame()
|
csum_df = pd.DataFrame()
|
||||||
csum_df['sum'] = trades['profit_abs'].cumsum()
|
csum_df["sum"] = trades["profit_abs"].cumsum()
|
||||||
csum_min = csum_df['sum'].min() + starting_balance
|
csum_min = csum_df["sum"].min() + starting_balance
|
||||||
csum_max = csum_df['sum'].max() + starting_balance
|
csum_max = csum_df["sum"].max() + starting_balance
|
||||||
|
|
||||||
return csum_min, csum_max
|
return csum_min, csum_max
|
||||||
|
|
||||||
@@ -210,28 +255,29 @@ def calculate_expectancy(trades: pd.DataFrame) -> Tuple[float, float]:
|
|||||||
expectancy_ratio = 100
|
expectancy_ratio = 100
|
||||||
|
|
||||||
if len(trades) > 0:
|
if len(trades) > 0:
|
||||||
winning_trades = trades.loc[trades['profit_abs'] > 0]
|
winning_trades = trades.loc[trades["profit_abs"] > 0]
|
||||||
losing_trades = trades.loc[trades['profit_abs'] < 0]
|
losing_trades = trades.loc[trades["profit_abs"] < 0]
|
||||||
profit_sum = winning_trades['profit_abs'].sum()
|
profit_sum = winning_trades["profit_abs"].sum()
|
||||||
loss_sum = abs(losing_trades['profit_abs'].sum())
|
loss_sum = abs(losing_trades["profit_abs"].sum())
|
||||||
nb_win_trades = len(winning_trades)
|
nb_win_trades = len(winning_trades)
|
||||||
nb_loss_trades = len(losing_trades)
|
nb_loss_trades = len(losing_trades)
|
||||||
|
|
||||||
average_win = (profit_sum / nb_win_trades) if nb_win_trades > 0 else 0
|
average_win = (profit_sum / nb_win_trades) if nb_win_trades > 0 else 0
|
||||||
average_loss = (loss_sum / nb_loss_trades) if nb_loss_trades > 0 else 0
|
average_loss = (loss_sum / nb_loss_trades) if nb_loss_trades > 0 else 0
|
||||||
winrate = (nb_win_trades / len(trades))
|
winrate = nb_win_trades / len(trades)
|
||||||
loserate = (nb_loss_trades / len(trades))
|
loserate = nb_loss_trades / len(trades)
|
||||||
|
|
||||||
expectancy = (winrate * average_win) - (loserate * average_loss)
|
expectancy = (winrate * average_win) - (loserate * average_loss)
|
||||||
if (average_loss > 0):
|
if average_loss > 0:
|
||||||
risk_reward_ratio = average_win / average_loss
|
risk_reward_ratio = average_win / average_loss
|
||||||
expectancy_ratio = ((1 + risk_reward_ratio) * winrate) - 1
|
expectancy_ratio = ((1 + risk_reward_ratio) * winrate) - 1
|
||||||
|
|
||||||
return expectancy, expectancy_ratio
|
return expectancy, expectancy_ratio
|
||||||
|
|
||||||
|
|
||||||
def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
def calculate_sortino(
|
||||||
starting_balance: float) -> float:
|
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
|
||||||
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate sortino
|
Calculate sortino
|
||||||
:param trades: DataFrame containing trades (requires columns profit_abs)
|
:param trades: DataFrame containing trades (requires columns profit_abs)
|
||||||
@@ -240,12 +286,12 @@ def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: dateti
|
|||||||
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
total_profit = trades['profit_abs'] / starting_balance
|
total_profit = trades["profit_abs"] / starting_balance
|
||||||
days_period = max(1, (max_date - min_date).days)
|
days_period = max(1, (max_date - min_date).days)
|
||||||
|
|
||||||
expected_returns_mean = total_profit.sum() / days_period
|
expected_returns_mean = total_profit.sum() / days_period
|
||||||
|
|
||||||
down_stdev = np.std(trades.loc[trades['profit_abs'] < 0, 'profit_abs'] / starting_balance)
|
down_stdev = np.std(trades.loc[trades["profit_abs"] < 0, "profit_abs"] / starting_balance)
|
||||||
|
|
||||||
if down_stdev != 0 and not np.isnan(down_stdev):
|
if down_stdev != 0 and not np.isnan(down_stdev):
|
||||||
sortino_ratio = expected_returns_mean / down_stdev * np.sqrt(365)
|
sortino_ratio = expected_returns_mean / down_stdev * np.sqrt(365)
|
||||||
@@ -257,8 +303,9 @@ def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: dateti
|
|||||||
return sortino_ratio
|
return sortino_ratio
|
||||||
|
|
||||||
|
|
||||||
def calculate_sharpe(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
def calculate_sharpe(
|
||||||
starting_balance: float) -> float:
|
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
|
||||||
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate sharpe
|
Calculate sharpe
|
||||||
:param trades: DataFrame containing trades (requires column profit_abs)
|
:param trades: DataFrame containing trades (requires column profit_abs)
|
||||||
@@ -267,7 +314,7 @@ def calculate_sharpe(trades: pd.DataFrame, min_date: datetime, max_date: datetim
|
|||||||
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
total_profit = trades['profit_abs'] / starting_balance
|
total_profit = trades["profit_abs"] / starting_balance
|
||||||
days_period = max(1, (max_date - min_date).days)
|
days_period = max(1, (max_date - min_date).days)
|
||||||
|
|
||||||
expected_returns_mean = total_profit.sum() / days_period
|
expected_returns_mean = total_profit.sum() / days_period
|
||||||
@@ -283,8 +330,9 @@ def calculate_sharpe(trades: pd.DataFrame, min_date: datetime, max_date: datetim
|
|||||||
return sharp_ratio
|
return sharp_ratio
|
||||||
|
|
||||||
|
|
||||||
def calculate_calmar(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
def calculate_calmar(
|
||||||
starting_balance: float) -> float:
|
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
|
||||||
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate calmar
|
Calculate calmar
|
||||||
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
||||||
@@ -293,7 +341,7 @@ def calculate_calmar(trades: pd.DataFrame, min_date: datetime, max_date: datetim
|
|||||||
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
total_profit = trades['profit_abs'].sum() / starting_balance
|
total_profit = trades["profit_abs"].sum() / starting_balance
|
||||||
days_period = max(1, (max_date - min_date).days)
|
days_period = max(1, (max_date - min_date).days)
|
||||||
|
|
||||||
# adding slippage of 0.1% per trade
|
# adding slippage of 0.1% per trade
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# pragma pylint: disable=W0603
|
# pragma pylint: disable=W0603
|
||||||
""" Edge positioning package """
|
"""Edge positioning package"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
@@ -46,48 +47,49 @@ class Edge:
|
|||||||
_cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs
|
_cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs
|
||||||
|
|
||||||
def __init__(self, config: Config, exchange, strategy) -> None:
|
def __init__(self, config: Config, exchange, strategy) -> None:
|
||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
self.exchange = exchange
|
self.exchange = exchange
|
||||||
self.strategy: IStrategy = strategy
|
self.strategy: IStrategy = strategy
|
||||||
|
|
||||||
self.edge_config = self.config.get('edge', {})
|
self.edge_config = self.config.get("edge", {})
|
||||||
self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs
|
self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs
|
||||||
self._final_pairs: list = []
|
self._final_pairs: list = []
|
||||||
|
|
||||||
# checking max_open_trades. it should be -1 as with Edge
|
# checking max_open_trades. it should be -1 as with Edge
|
||||||
# the number of trades is determined by position size
|
# the number of trades is determined by position size
|
||||||
if self.config['max_open_trades'] != float('inf'):
|
if self.config["max_open_trades"] != float("inf"):
|
||||||
logger.critical('max_open_trades should be -1 in config !')
|
logger.critical("max_open_trades should be -1 in config !")
|
||||||
|
|
||||||
if self.config['stake_amount'] != UNLIMITED_STAKE_AMOUNT:
|
if self.config["stake_amount"] != UNLIMITED_STAKE_AMOUNT:
|
||||||
raise OperationalException('Edge works only with unlimited stake amount')
|
raise OperationalException("Edge works only with unlimited stake amount")
|
||||||
|
|
||||||
self._capital_ratio: float = self.config['tradable_balance_ratio']
|
self._capital_ratio: float = self.config["tradable_balance_ratio"]
|
||||||
self._allowed_risk: float = self.edge_config.get('allowed_risk')
|
self._allowed_risk: float = self.edge_config.get("allowed_risk")
|
||||||
self._since_number_of_days: int = self.edge_config.get('calculate_since_number_of_days', 14)
|
self._since_number_of_days: int = self.edge_config.get("calculate_since_number_of_days", 14)
|
||||||
self._last_updated: int = 0 # Timestamp of pairs last updated time
|
self._last_updated: int = 0 # Timestamp of pairs last updated time
|
||||||
self._refresh_pairs = True
|
self._refresh_pairs = True
|
||||||
|
|
||||||
self._stoploss_range_min = float(self.edge_config.get('stoploss_range_min', -0.01))
|
self._stoploss_range_min = float(self.edge_config.get("stoploss_range_min", -0.01))
|
||||||
self._stoploss_range_max = float(self.edge_config.get('stoploss_range_max', -0.05))
|
self._stoploss_range_max = float(self.edge_config.get("stoploss_range_max", -0.05))
|
||||||
self._stoploss_range_step = float(self.edge_config.get('stoploss_range_step', -0.001))
|
self._stoploss_range_step = float(self.edge_config.get("stoploss_range_step", -0.001))
|
||||||
|
|
||||||
# calculating stoploss range
|
# calculating stoploss range
|
||||||
self._stoploss_range = np.arange(
|
self._stoploss_range = np.arange(
|
||||||
self._stoploss_range_min,
|
self._stoploss_range_min, self._stoploss_range_max, self._stoploss_range_step
|
||||||
self._stoploss_range_max,
|
|
||||||
self._stoploss_range_step
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._timerange: TimeRange = TimeRange.parse_timerange(
|
self._timerange: TimeRange = TimeRange.parse_timerange(
|
||||||
f"{(dt_now() - timedelta(days=self._since_number_of_days)).strftime('%Y%m%d')}-")
|
f"{(dt_now() - timedelta(days=self._since_number_of_days)).strftime('%Y%m%d')}-"
|
||||||
if config.get('fee'):
|
)
|
||||||
self.fee = config['fee']
|
if config.get("fee"):
|
||||||
|
self.fee = config["fee"]
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.fee = self.exchange.get_fee(symbol=expand_pairlist(
|
self.fee = self.exchange.get_fee(
|
||||||
self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0])
|
symbol=expand_pairlist(
|
||||||
|
self.config["exchange"]["pair_whitelist"], list(self.exchange.markets)
|
||||||
|
)[0]
|
||||||
|
)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
self.fee = None
|
self.fee = None
|
||||||
|
|
||||||
@@ -95,28 +97,30 @@ class Edge:
|
|||||||
if self.fee is None and pairs:
|
if self.fee is None and pairs:
|
||||||
self.fee = self.exchange.get_fee(pairs[0])
|
self.fee = self.exchange.get_fee(pairs[0])
|
||||||
|
|
||||||
heartbeat = self.edge_config.get('process_throttle_secs')
|
heartbeat = self.edge_config.get("process_throttle_secs")
|
||||||
|
|
||||||
if (self._last_updated > 0) and (
|
if (self._last_updated > 0) and (
|
||||||
self._last_updated + heartbeat > int(dt_now().timestamp())):
|
self._last_updated + heartbeat > int(dt_now().timestamp())
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
data: Dict[str, Any] = {}
|
data: Dict[str, Any] = {}
|
||||||
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
|
logger.info("Using stake_currency: %s ...", self.config["stake_currency"])
|
||||||
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
logger.info("Using local backtesting data (using whitelist in given config) ...")
|
||||||
|
|
||||||
if self._refresh_pairs:
|
if self._refresh_pairs:
|
||||||
timerange_startup = deepcopy(self._timerange)
|
timerange_startup = deepcopy(self._timerange)
|
||||||
timerange_startup.subtract_start(timeframe_to_seconds(
|
timerange_startup.subtract_start(
|
||||||
self.strategy.timeframe) * self.strategy.startup_candle_count)
|
timeframe_to_seconds(self.strategy.timeframe) * self.strategy.startup_candle_count
|
||||||
|
)
|
||||||
refresh_data(
|
refresh_data(
|
||||||
datadir=self.config['datadir'],
|
datadir=self.config["datadir"],
|
||||||
pairs=pairs,
|
pairs=pairs,
|
||||||
exchange=self.exchange,
|
exchange=self.exchange,
|
||||||
timeframe=self.strategy.timeframe,
|
timeframe=self.strategy.timeframe,
|
||||||
timerange=timerange_startup,
|
timerange=timerange_startup,
|
||||||
data_format=self.config['dataformat_ohlcv'],
|
data_format=self.config["dataformat_ohlcv"],
|
||||||
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
|
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||||
)
|
)
|
||||||
# Download informative pairs too
|
# Download informative pairs too
|
||||||
res = defaultdict(list)
|
res = defaultdict(list)
|
||||||
@@ -124,26 +128,27 @@ class Edge:
|
|||||||
res[timeframe].append(pair)
|
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) * self.strategy.startup_candle_count)
|
timeframe_to_seconds(timeframe) * self.strategy.startup_candle_count
|
||||||
|
)
|
||||||
refresh_data(
|
refresh_data(
|
||||||
datadir=self.config['datadir'],
|
datadir=self.config["datadir"],
|
||||||
pairs=inf_pairs,
|
pairs=inf_pairs,
|
||||||
exchange=self.exchange,
|
exchange=self.exchange,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
timerange=timerange_startup,
|
timerange=timerange_startup,
|
||||||
data_format=self.config['dataformat_ohlcv'],
|
data_format=self.config["dataformat_ohlcv"],
|
||||||
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
|
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||||
)
|
)
|
||||||
|
|
||||||
data = load_data(
|
data = load_data(
|
||||||
datadir=self.config['datadir'],
|
datadir=self.config["datadir"],
|
||||||
pairs=pairs,
|
pairs=pairs,
|
||||||
timeframe=self.strategy.timeframe,
|
timeframe=self.strategy.timeframe,
|
||||||
timerange=self._timerange,
|
timerange=self._timerange,
|
||||||
startup_candles=self.strategy.startup_candle_count,
|
startup_candles=self.strategy.startup_candle_count,
|
||||||
data_format=self.config['dataformat_ohlcv'],
|
data_format=self.config["dataformat_ohlcv"],
|
||||||
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
|
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
@@ -152,27 +157,29 @@ class Edge:
|
|||||||
logger.critical("No data found. Edge is stopped ...")
|
logger.critical("No data found. Edge is stopped ...")
|
||||||
return False
|
return False
|
||||||
# Fake run-mode to Edge
|
# Fake run-mode to Edge
|
||||||
prior_rm = self.config['runmode']
|
prior_rm = self.config["runmode"]
|
||||||
self.config['runmode'] = RunMode.EDGE
|
self.config["runmode"] = RunMode.EDGE
|
||||||
preprocessed = self.strategy.advise_all_indicators(data)
|
preprocessed = self.strategy.advise_all_indicators(data)
|
||||||
self.config['runmode'] = prior_rm
|
self.config["runmode"] = prior_rm
|
||||||
|
|
||||||
# Print timeframe
|
# Print timeframe
|
||||||
min_date, max_date = get_timerange(preprocessed)
|
min_date, max_date = get_timerange(preprocessed)
|
||||||
logger.info(f'Measuring data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
|
logger.info(
|
||||||
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
|
f"Measuring data from {min_date.strftime(DATETIME_PRINT_FORMAT)} "
|
||||||
f'({(max_date - min_date).days} days)..')
|
f"up to {max_date.strftime(DATETIME_PRINT_FORMAT)} "
|
||||||
|
f"({(max_date - min_date).days} days).."
|
||||||
|
)
|
||||||
# TODO: Should edge support shorts? needs to be investigated further
|
# TODO: Should edge support shorts? needs to be investigated further
|
||||||
# * (add enter_short exit_short)
|
# * (add enter_short exit_short)
|
||||||
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long']
|
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():
|
||||||
# Sorting dataframe by date and reset index
|
# Sorting dataframe by date and reset index
|
||||||
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.ft_advise_signals(pair_data, {'pair': pair})[headers].copy()
|
df_analyzed = self.strategy.ft_advise_signals(pair_data, {"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)
|
||||||
|
|
||||||
@@ -188,8 +195,9 @@ class Edge:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def stake_amount(self, pair: str, free_capital: float,
|
def stake_amount(
|
||||||
total_capital: float, capital_in_trade: float) -> float:
|
self, pair: str, free_capital: float, total_capital: float, capital_in_trade: float
|
||||||
|
) -> float:
|
||||||
stoploss = self.get_stoploss(pair)
|
stoploss = self.get_stoploss(pair)
|
||||||
available_capital = (total_capital + capital_in_trade) * self._capital_ratio
|
available_capital = (total_capital + capital_in_trade) * self._capital_ratio
|
||||||
allowed_capital_at_risk = available_capital * self._allowed_risk
|
allowed_capital_at_risk = available_capital * self._allowed_risk
|
||||||
@@ -198,14 +206,18 @@ class Edge:
|
|||||||
position_size = min(min(max_position_size, free_capital), available_capital)
|
position_size = min(min(max_position_size, free_capital), available_capital)
|
||||||
if pair in self._cached_pairs:
|
if pair in self._cached_pairs:
|
||||||
logger.info(
|
logger.info(
|
||||||
'winrate: %s, expectancy: %s, position size: %s, pair: %s,'
|
"winrate: %s, expectancy: %s, position size: %s, pair: %s,"
|
||||||
' capital in trade: %s, free capital: %s, total capital: %s,'
|
" capital in trade: %s, free capital: %s, total capital: %s,"
|
||||||
' stoploss: %s, available capital: %s.',
|
" stoploss: %s, available capital: %s.",
|
||||||
self._cached_pairs[pair].winrate,
|
self._cached_pairs[pair].winrate,
|
||||||
self._cached_pairs[pair].expectancy,
|
self._cached_pairs[pair].expectancy,
|
||||||
position_size, pair,
|
position_size,
|
||||||
capital_in_trade, free_capital, total_capital,
|
pair,
|
||||||
stoploss, available_capital
|
capital_in_trade,
|
||||||
|
free_capital,
|
||||||
|
total_capital,
|
||||||
|
stoploss,
|
||||||
|
available_capital,
|
||||||
)
|
)
|
||||||
return round(position_size, 15)
|
return round(position_size, 15)
|
||||||
|
|
||||||
@@ -213,8 +225,10 @@ class Edge:
|
|||||||
if pair in self._cached_pairs:
|
if pair in self._cached_pairs:
|
||||||
return self._cached_pairs[pair].stoploss
|
return self._cached_pairs[pair].stoploss
|
||||||
else:
|
else:
|
||||||
logger.warning(f'Tried to access stoploss of non-existing pair {pair}, '
|
logger.warning(
|
||||||
'strategy stoploss is returned instead.')
|
f"Tried to access stoploss of non-existing pair {pair}, "
|
||||||
|
"strategy stoploss is returned instead."
|
||||||
|
)
|
||||||
return self.strategy.stoploss
|
return self.strategy.stoploss
|
||||||
|
|
||||||
def adjust(self, pairs: List[str]) -> list:
|
def adjust(self, pairs: List[str]) -> list:
|
||||||
@@ -224,8 +238,8 @@ class Edge:
|
|||||||
final = []
|
final = []
|
||||||
for pair, info in self._cached_pairs.items():
|
for pair, info in self._cached_pairs.items():
|
||||||
if (
|
if (
|
||||||
info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2))
|
info.expectancy > float(self.edge_config.get("minimum_expectancy", 0.2))
|
||||||
and info.winrate > float(self.edge_config.get('minimum_winrate', 0.60))
|
and info.winrate > float(self.edge_config.get("minimum_winrate", 0.60))
|
||||||
and pair in pairs
|
and pair in pairs
|
||||||
):
|
):
|
||||||
final.append(pair)
|
final.append(pair)
|
||||||
@@ -234,14 +248,14 @@ class Edge:
|
|||||||
self._final_pairs = final
|
self._final_pairs = final
|
||||||
if self._final_pairs:
|
if self._final_pairs:
|
||||||
logger.info(
|
logger.info(
|
||||||
'Minimum expectancy and minimum winrate are met only for %s,'
|
"Minimum expectancy and minimum winrate are met only for %s,"
|
||||||
' so other pairs are filtered out.',
|
" so other pairs are filtered out.",
|
||||||
self._final_pairs
|
self._final_pairs,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
'Edge removed all pairs as no pair with minimum expectancy '
|
"Edge removed all pairs as no pair with minimum expectancy "
|
||||||
'and minimum winrate was found !'
|
"and minimum winrate was found !"
|
||||||
)
|
)
|
||||||
|
|
||||||
return self._final_pairs
|
return self._final_pairs
|
||||||
@@ -252,14 +266,17 @@ 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(
|
||||||
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60))):
|
self.edge_config.get("minimum_expectancy", 0.2)
|
||||||
final.append({
|
) and info.winrate > float(self.edge_config.get("minimum_winrate", 0.60)):
|
||||||
'Pair': pair,
|
final.append(
|
||||||
'Winrate': info.winrate,
|
{
|
||||||
'Expectancy': info.expectancy,
|
"Pair": pair,
|
||||||
'Stoploss': info.stoploss,
|
"Winrate": info.winrate,
|
||||||
})
|
"Expectancy": info.expectancy,
|
||||||
|
"Stoploss": info.stoploss,
|
||||||
|
}
|
||||||
|
)
|
||||||
return final
|
return final
|
||||||
|
|
||||||
def _fill_calculable_fields(self, result: DataFrame) -> DataFrame:
|
def _fill_calculable_fields(self, result: DataFrame) -> DataFrame:
|
||||||
@@ -279,28 +296,29 @@ class Edge:
|
|||||||
# All returned values are relative, they are defined as ratios.
|
# All returned values are relative, they are defined as ratios.
|
||||||
stake = 0.015
|
stake = 0.015
|
||||||
|
|
||||||
result['trade_duration'] = result['close_date'] - result['open_date']
|
result["trade_duration"] = result["close_date"] - result["open_date"]
|
||||||
|
|
||||||
result['trade_duration'] = result['trade_duration'].map(
|
result["trade_duration"] = result["trade_duration"].map(
|
||||||
lambda x: int(x.total_seconds() / 60))
|
lambda x: int(x.total_seconds() / 60)
|
||||||
|
)
|
||||||
|
|
||||||
# Spends, Takes, Profit, Absolute Profit
|
# Spends, Takes, Profit, Absolute Profit
|
||||||
|
|
||||||
# Buy Price
|
# Buy Price
|
||||||
result['buy_vol'] = stake / result['open_rate'] # How many target are we buying
|
result["buy_vol"] = stake / result["open_rate"] # How many target are we buying
|
||||||
result['buy_fee'] = stake * self.fee
|
result["buy_fee"] = stake * self.fee
|
||||||
result['buy_spend'] = stake + result['buy_fee'] # How much we're spending
|
result["buy_spend"] = stake + result["buy_fee"] # How much we're spending
|
||||||
|
|
||||||
# Sell price
|
# Sell price
|
||||||
result['sell_sum'] = result['buy_vol'] * result['close_rate']
|
result["sell_sum"] = result["buy_vol"] * result["close_rate"]
|
||||||
result['sell_fee'] = result['sell_sum'] * self.fee
|
result["sell_fee"] = result["sell_sum"] * self.fee
|
||||||
result['sell_take'] = result['sell_sum'] - result['sell_fee']
|
result["sell_take"] = result["sell_sum"] - result["sell_fee"]
|
||||||
|
|
||||||
# profit_ratio
|
# profit_ratio
|
||||||
result['profit_ratio'] = (result['sell_take'] - result['buy_spend']) / result['buy_spend']
|
result["profit_ratio"] = (result["sell_take"] - result["buy_spend"]) / result["buy_spend"]
|
||||||
|
|
||||||
# Absolute profit
|
# Absolute profit
|
||||||
result['profit_abs'] = result['sell_take'] - result['buy_spend']
|
result["profit_abs"] = result["sell_take"] - result["buy_spend"]
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -310,8 +328,8 @@ class Edge:
|
|||||||
The calculation will be done per pair and per strategy.
|
The calculation will be done per pair and per strategy.
|
||||||
"""
|
"""
|
||||||
# Removing pairs having less than min_trades_number
|
# Removing pairs having less than min_trades_number
|
||||||
min_trades_number = self.edge_config.get('min_trade_number', 10)
|
min_trades_number = self.edge_config.get("min_trade_number", 10)
|
||||||
results = results.groupby(['pair', 'stoploss']).filter(lambda x: len(x) > min_trades_number)
|
results = results.groupby(["pair", "stoploss"]).filter(lambda x: len(x) > min_trades_number)
|
||||||
###################################
|
###################################
|
||||||
|
|
||||||
# Removing outliers (Only Pumps) from the dataset
|
# Removing outliers (Only Pumps) from the dataset
|
||||||
@@ -319,13 +337,15 @@ class Edge:
|
|||||||
# Then every value more than (standard deviation + 2*average) is out (pump)
|
# Then every value more than (standard deviation + 2*average) is out (pump)
|
||||||
#
|
#
|
||||||
# Removing Pumps
|
# Removing Pumps
|
||||||
if self.edge_config.get('remove_pumps', False):
|
if self.edge_config.get("remove_pumps", False):
|
||||||
results = results[results['profit_abs'] < 2 * results['profit_abs'].std()
|
results = results[
|
||||||
+ results['profit_abs'].mean()]
|
results["profit_abs"]
|
||||||
|
< 2 * results["profit_abs"].std() + results["profit_abs"].mean()
|
||||||
|
]
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
# Removing trades having a duration more than X minutes (set in config)
|
# Removing trades having a duration more than X minutes (set in config)
|
||||||
max_trade_duration = self.edge_config.get('max_trade_duration_minute', 1440)
|
max_trade_duration = self.edge_config.get("max_trade_duration_minute", 1440)
|
||||||
results = results[results.trade_duration < max_trade_duration]
|
results = results[results.trade_duration < max_trade_duration]
|
||||||
#######################################################################
|
#######################################################################
|
||||||
|
|
||||||
@@ -333,44 +353,54 @@ class Edge:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
groupby_aggregator = {
|
groupby_aggregator = {
|
||||||
'profit_abs': [
|
"profit_abs": [
|
||||||
('nb_trades', 'count'), # number of all trades
|
("nb_trades", "count"), # number of all trades
|
||||||
('profit_sum', lambda x: x[x > 0].sum()), # cumulative profit of all winning trades
|
("profit_sum", lambda x: x[x > 0].sum()), # cumulative profit of all winning trades
|
||||||
('loss_sum', lambda x: abs(x[x < 0].sum())), # cumulative loss of all losing trades
|
("loss_sum", lambda x: abs(x[x < 0].sum())), # cumulative loss of all losing trades
|
||||||
('nb_win_trades', lambda x: x[x > 0].count()) # number of winning trades
|
("nb_win_trades", lambda x: x[x > 0].count()), # number of winning trades
|
||||||
],
|
],
|
||||||
'trade_duration': [('avg_trade_duration', 'mean')]
|
"trade_duration": [("avg_trade_duration", "mean")],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Group by (pair and stoploss) by applying above aggregator
|
# Group by (pair and stoploss) by applying above aggregator
|
||||||
df = results.groupby(['pair', 'stoploss'])[['profit_abs', 'trade_duration']].agg(
|
df = (
|
||||||
groupby_aggregator).reset_index(col_level=1)
|
results.groupby(["pair", "stoploss"])[["profit_abs", "trade_duration"]]
|
||||||
|
.agg(groupby_aggregator)
|
||||||
|
.reset_index(col_level=1)
|
||||||
|
)
|
||||||
|
|
||||||
# Dropping level 0 as we don't need it
|
# Dropping level 0 as we don't need it
|
||||||
df.columns = df.columns.droplevel(0)
|
df.columns = df.columns.droplevel(0)
|
||||||
|
|
||||||
# Calculating number of losing trades, average win and average loss
|
# Calculating number of losing trades, average win and average loss
|
||||||
df['nb_loss_trades'] = df['nb_trades'] - df['nb_win_trades']
|
df["nb_loss_trades"] = df["nb_trades"] - df["nb_win_trades"]
|
||||||
df['average_win'] = np.where(df['nb_win_trades'] == 0, 0.0,
|
df["average_win"] = np.where(
|
||||||
df['profit_sum'] / df['nb_win_trades'])
|
df["nb_win_trades"] == 0, 0.0, df["profit_sum"] / df["nb_win_trades"]
|
||||||
df['average_loss'] = np.where(df['nb_loss_trades'] == 0, 0.0,
|
)
|
||||||
df['loss_sum'] / df['nb_loss_trades'])
|
df["average_loss"] = np.where(
|
||||||
|
df["nb_loss_trades"] == 0, 0.0, df["loss_sum"] / df["nb_loss_trades"]
|
||||||
|
)
|
||||||
|
|
||||||
# Win rate = number of profitable trades / number of trades
|
# Win rate = number of profitable trades / number of trades
|
||||||
df['winrate'] = df['nb_win_trades'] / df['nb_trades']
|
df["winrate"] = df["nb_win_trades"] / df["nb_trades"]
|
||||||
|
|
||||||
# risk_reward_ratio = average win / average loss
|
# risk_reward_ratio = average win / average loss
|
||||||
df['risk_reward_ratio'] = df['average_win'] / df['average_loss']
|
df["risk_reward_ratio"] = df["average_win"] / df["average_loss"]
|
||||||
|
|
||||||
# required_risk_reward = (1 / winrate) - 1
|
# required_risk_reward = (1 / winrate) - 1
|
||||||
df['required_risk_reward'] = (1 / df['winrate']) - 1
|
df["required_risk_reward"] = (1 / df["winrate"]) - 1
|
||||||
|
|
||||||
# expectancy = (risk_reward_ratio * winrate) - (lossrate)
|
# expectancy = (risk_reward_ratio * winrate) - (lossrate)
|
||||||
df['expectancy'] = (df['risk_reward_ratio'] * df['winrate']) - (1 - df['winrate'])
|
df["expectancy"] = (df["risk_reward_ratio"] * df["winrate"]) - (1 - df["winrate"])
|
||||||
|
|
||||||
# sort by expectancy and stoploss
|
# sort by expectancy and stoploss
|
||||||
df = df.sort_values(by=['expectancy', 'stoploss'], ascending=False).groupby(
|
df = (
|
||||||
'pair').first().sort_values(by=['expectancy'], ascending=False).reset_index()
|
df.sort_values(by=["expectancy", "stoploss"], ascending=False)
|
||||||
|
.groupby("pair")
|
||||||
|
.first()
|
||||||
|
.sort_values(by=["expectancy"], ascending=False)
|
||||||
|
.reset_index()
|
||||||
|
)
|
||||||
|
|
||||||
final = {}
|
final = {}
|
||||||
for x in df.itertuples():
|
for x in df.itertuples():
|
||||||
@@ -381,17 +411,17 @@ class Edge:
|
|||||||
x.required_risk_reward,
|
x.required_risk_reward,
|
||||||
x.expectancy,
|
x.expectancy,
|
||||||
x.nb_trades,
|
x.nb_trades,
|
||||||
x.avg_trade_duration
|
x.avg_trade_duration,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Returning a list of pairs in order of "expectancy"
|
# Returning a list of pairs in order of "expectancy"
|
||||||
return final
|
return final
|
||||||
|
|
||||||
def _find_trades_for_stoploss_range(self, df, pair: str, stoploss_range) -> list:
|
def _find_trades_for_stoploss_range(self, df, pair: str, stoploss_range) -> list:
|
||||||
buy_column = df['enter_long'].values
|
buy_column = df["enter_long"].values
|
||||||
sell_column = df['exit_long'].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
|
||||||
|
|
||||||
result: list = []
|
result: list = []
|
||||||
for stoploss in stoploss_range:
|
for stoploss in stoploss_range:
|
||||||
@@ -401,8 +431,9 @@ class Edge:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _detect_next_stop_or_sell_point(self, buy_column, sell_column, date_column,
|
def _detect_next_stop_or_sell_point(
|
||||||
ohlc_columns, stoploss, pair: str):
|
self, buy_column, sell_column, date_column, ohlc_columns, stoploss, pair: str
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Iterate through ohlc_columns in order to find the next trade
|
Iterate through ohlc_columns in order to find the next trade
|
||||||
Next trade opens from the first buy signal noticed to
|
Next trade opens from the first buy signal noticed to
|
||||||
@@ -429,27 +460,28 @@ class Edge:
|
|||||||
open_trade_index += 1
|
open_trade_index += 1
|
||||||
|
|
||||||
open_price = ohlc_columns[open_trade_index, 0]
|
open_price = ohlc_columns[open_trade_index, 0]
|
||||||
stop_price = (open_price * (stoploss + 1))
|
stop_price = open_price * (stoploss + 1)
|
||||||
|
|
||||||
# Searching for the index where stoploss is hit
|
# Searching for the index where stoploss is hit
|
||||||
stop_index = utf1st.find_1st(
|
stop_index = utf1st.find_1st(
|
||||||
ohlc_columns[open_trade_index:, 2], stop_price, utf1st.cmp_smaller)
|
ohlc_columns[open_trade_index:, 2], stop_price, utf1st.cmp_smaller
|
||||||
|
)
|
||||||
|
|
||||||
# If we don't find it then we assume stop_index will be far in future (infinite number)
|
# If we don't find it then we assume stop_index will be far in future (infinite number)
|
||||||
if stop_index == -1:
|
if stop_index == -1:
|
||||||
stop_index = float('inf')
|
stop_index = float("inf")
|
||||||
|
|
||||||
# Searching for the index where sell is hit
|
# Searching for the index where sell is hit
|
||||||
sell_index = utf1st.find_1st(sell_column[open_trade_index:], 1, utf1st.cmp_equal)
|
sell_index = utf1st.find_1st(sell_column[open_trade_index:], 1, utf1st.cmp_equal)
|
||||||
|
|
||||||
# If we don't find it then we assume sell_index will be far in future (infinite number)
|
# If we don't find it then we assume sell_index will be far in future (infinite number)
|
||||||
if sell_index == -1:
|
if sell_index == -1:
|
||||||
sell_index = float('inf')
|
sell_index = float("inf")
|
||||||
|
|
||||||
# Check if we don't find any stop or sell point (in that case trade remains open)
|
# Check if we don't find any stop or sell point (in that case trade remains open)
|
||||||
# It is not interesting for Edge to consider it so we simply ignore the trade
|
# It is not interesting for Edge to consider it so we simply ignore the trade
|
||||||
# And stop iterating there is no more entry
|
# And stop iterating there is no more entry
|
||||||
if stop_index == sell_index == float('inf'):
|
if stop_index == sell_index == float("inf"):
|
||||||
break
|
break
|
||||||
|
|
||||||
if stop_index <= sell_index:
|
if stop_index <= sell_index:
|
||||||
@@ -467,17 +499,18 @@ class Edge:
|
|||||||
exit_type = ExitType.EXIT_SIGNAL
|
exit_type = ExitType.EXIT_SIGNAL
|
||||||
exit_price = ohlc_columns[exit_index, 0]
|
exit_price = ohlc_columns[exit_index, 0]
|
||||||
|
|
||||||
trade = {'pair': pair,
|
trade = {
|
||||||
'stoploss': stoploss,
|
"pair": pair,
|
||||||
'profit_ratio': '',
|
"stoploss": stoploss,
|
||||||
'profit_abs': '',
|
"profit_ratio": "",
|
||||||
'open_date': date_column[open_trade_index],
|
"profit_abs": "",
|
||||||
'close_date': date_column[exit_index],
|
"open_date": date_column[open_trade_index],
|
||||||
'trade_duration': '',
|
"close_date": date_column[exit_index],
|
||||||
'open_rate': round(open_price, 15),
|
"trade_duration": "",
|
||||||
'close_rate': round(exit_price, 15),
|
"open_rate": round(open_price, 15),
|
||||||
'exit_type': exit_type
|
"close_rate": round(exit_price, 15),
|
||||||
}
|
"exit_type": exit_type,
|
||||||
|
}
|
||||||
|
|
||||||
result.append(trade)
|
result.append(trade)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from freqtrade.enums.marketstatetype import MarketDirection
|
|||||||
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
||||||
from freqtrade.enums.pricetype import PriceType
|
from freqtrade.enums.pricetype import PriceType
|
||||||
from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType
|
from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType
|
||||||
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADE_MODES, RunMode
|
||||||
from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType
|
from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType
|
||||||
from freqtrade.enums.state import State
|
from freqtrade.enums.state import State
|
||||||
from freqtrade.enums.tradingmode import TradingMode
|
from freqtrade.enums.tradingmode import TradingMode
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class BacktestState(Enum):
|
|||||||
"""
|
"""
|
||||||
Bot application states
|
Bot application states
|
||||||
"""
|
"""
|
||||||
|
|
||||||
STARTUP = 1
|
STARTUP = 1
|
||||||
DATALOAD = 2
|
DATALOAD = 2
|
||||||
ANALYZE = 3
|
ANALYZE = 3
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from enum import Enum
|
|||||||
|
|
||||||
class CandleType(str, Enum):
|
class CandleType(str, Enum):
|
||||||
"""Enum to distinguish candle types"""
|
"""Enum to distinguish candle types"""
|
||||||
|
|
||||||
SPOT = "spot"
|
SPOT = "spot"
|
||||||
FUTURES = "futures"
|
FUTURES = "futures"
|
||||||
MARK = "mark"
|
MARK = "mark"
|
||||||
@@ -17,14 +18,14 @@ class CandleType(str, Enum):
|
|||||||
return f"{self.name.lower()}"
|
return f"{self.name.lower()}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_string(value: str) -> 'CandleType':
|
def from_string(value: str) -> "CandleType":
|
||||||
if not value:
|
if not value:
|
||||||
# Default to spot
|
# Default to spot
|
||||||
return CandleType.SPOT
|
return CandleType.SPOT
|
||||||
return CandleType(value)
|
return CandleType(value)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_default(trading_mode: str) -> 'CandleType':
|
def get_default(trading_mode: str) -> "CandleType":
|
||||||
if trading_mode == 'futures':
|
if trading_mode == "futures":
|
||||||
return CandleType.FUTURES
|
return CandleType.FUTURES
|
||||||
return CandleType.SPOT
|
return CandleType.SPOT
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ class ExitCheckTuple:
|
|||||||
"""
|
"""
|
||||||
NamedTuple for Exit type + reason
|
NamedTuple for Exit type + reason
|
||||||
"""
|
"""
|
||||||
exit_type: ExitType
|
|
||||||
exit_reason: str = ''
|
|
||||||
|
|
||||||
def __init__(self, exit_type: ExitType, exit_reason: str = ''):
|
exit_type: ExitType
|
||||||
|
exit_reason: str = ""
|
||||||
|
|
||||||
|
def __init__(self, exit_type: ExitType, exit_reason: str = ""):
|
||||||
self.exit_type = exit_type
|
self.exit_type = exit_type
|
||||||
self.exit_reason = exit_reason or exit_type.value
|
self.exit_reason = exit_reason or exit_type.value
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class ExitType(Enum):
|
|||||||
"""
|
"""
|
||||||
Enum to distinguish between exit 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"
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ from enum import Enum
|
|||||||
|
|
||||||
|
|
||||||
class HyperoptState(Enum):
|
class HyperoptState(Enum):
|
||||||
""" Hyperopt states """
|
"""Hyperopt states"""
|
||||||
|
|
||||||
STARTUP = 1
|
STARTUP = 1
|
||||||
DATALOAD = 2
|
DATALOAD = 2
|
||||||
INDICATORS = 3
|
INDICATORS = 3
|
||||||
|
|||||||