Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cae8c3a7a8 | |||
| 1fe58c3938 | |||
| 630cdcb99f | |||
| b383654378 | |||
| 0883198f40 |
@@ -1,12 +1,11 @@
|
||||
FROM freqtradeorg/freqtrade:develop_freqairl
|
||||
FROM freqtradeorg/freqtrade:develop
|
||||
|
||||
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 -y install git mercurial 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 \
|
||||
|
||||
@@ -19,24 +19,23 @@
|
||||
"postCreateCommand": "freqtrade create-userdir --userdir user_data/",
|
||||
|
||||
"workspaceFolder": "/workspaces/freqtrade",
|
||||
"customizations": {
|
||||
"settings": {
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"editor.insertSpaces": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"[markdown]": {
|
||||
"files.trimTrailingWhitespace": false,
|
||||
},
|
||||
"python.pythonPath": "/usr/local/bin/python",
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"vscode-icons-team.vscode-icons",
|
||||
],
|
||||
}
|
||||
"settings": {
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"editor.insertSpaces": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"[markdown]": {
|
||||
"files.trimTrailingWhitespace": false,
|
||||
},
|
||||
"python.pythonPath": "/usr/local/bin/python",
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"vscode-icons-team.vscode-icons",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ on:
|
||||
- cron: '0 5 * * 4'
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}"
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
repository-projects: read
|
||||
@@ -77,17 +77,6 @@ jobs:
|
||||
# Allow failure for coveralls
|
||||
coveralls || true
|
||||
|
||||
- name: Check for repository changes
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "Repository is dirty, changes detected:"
|
||||
git status
|
||||
git diff
|
||||
exit 1
|
||||
else
|
||||
echo "Repository is clean, no changes detected."
|
||||
fi
|
||||
|
||||
- name: Backtesting (multi)
|
||||
run: |
|
||||
cp config_examples/config_bittrex.example.json config.json
|
||||
@@ -136,7 +125,6 @@ jobs:
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
|
||||
- name: Cache_dependencies
|
||||
uses: actions/cache@v3
|
||||
@@ -160,8 +148,7 @@ jobs:
|
||||
- name: Installation - macOS
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
# brew update
|
||||
# TODO: Should be the brew upgrade
|
||||
brew update
|
||||
# homebrew fails to update python due to unlinking failures
|
||||
# https://github.com/actions/runner-images/issues/6817
|
||||
rm /usr/local/bin/2to3 || true
|
||||
@@ -187,17 +174,6 @@ jobs:
|
||||
run: |
|
||||
pytest --random-order
|
||||
|
||||
- name: Check for repository changes
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "Repository is dirty, changes detected:"
|
||||
git status
|
||||
git diff
|
||||
exit 1
|
||||
else
|
||||
echo "Repository is clean, no changes detected."
|
||||
fi
|
||||
|
||||
- name: Backtesting
|
||||
run: |
|
||||
cp config_examples/config_bittrex.example.json config.json
|
||||
@@ -261,18 +237,6 @@ jobs:
|
||||
run: |
|
||||
pytest --random-order
|
||||
|
||||
- name: Check for repository changes
|
||||
run: |
|
||||
if (git status --porcelain) {
|
||||
Write-Host "Repository is dirty, changes detected:"
|
||||
git status
|
||||
git diff
|
||||
exit 1
|
||||
}
|
||||
else {
|
||||
Write-Host "Repository is clean, no changes detected."
|
||||
}
|
||||
|
||||
- name: Backtesting
|
||||
run: |
|
||||
cp config_examples/config_bittrex.example.json config.json
|
||||
@@ -338,7 +302,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Documentation build
|
||||
run: |
|
||||
@@ -461,7 +425,7 @@ jobs:
|
||||
python setup.py sdist bdist_wheel
|
||||
|
||||
- name: Publish to PyPI (Test)
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.8
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.1
|
||||
if: (github.event_name == 'release')
|
||||
with:
|
||||
user: __token__
|
||||
@@ -469,7 +433,7 @@ jobs:
|
||||
repository_url: https://test.pypi.org/legacy/
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.8
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.1
|
||||
if: (github.event_name == 'release')
|
||||
with:
|
||||
user: __token__
|
||||
|
||||
@@ -8,17 +8,17 @@ repos:
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.3.0"
|
||||
rev: "v1.0.1"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==5.3.0.6
|
||||
- types-cachetools==5.3.0.4
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.31.0.2
|
||||
- types-tabulate==0.9.0.3
|
||||
- types-python-dateutil==2.8.19.14
|
||||
- SQLAlchemy==2.0.19
|
||||
- types-requests==2.28.11.15
|
||||
- types-tabulate==0.9.0.1
|
||||
- types-python-dateutil==2.8.19.10
|
||||
- SQLAlchemy==2.0.7
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
@@ -30,7 +30,7 @@ repos:
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.0.270'
|
||||
rev: 'v0.0.255'
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11.4-slim-bullseye as base
|
||||
FROM python:3.10.10-slim-bullseye as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
@@ -25,7 +25,7 @@ FROM base as python-deps
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
&& pip install --upgrade pip wheel
|
||||
&& pip install --upgrade pip
|
||||
|
||||
# Install TA-lib
|
||||
COPY build_helpers/* /tmp/
|
||||
|
||||
@@ -210,6 +210,6 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
||||
- [Python >= 3.8](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
- [pip](https://pip.pypa.io/en/stable/installing/)
|
||||
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)
|
||||
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
||||
- [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
||||
- [Docker](https://www.docker.com/products/docker) (Recommended)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,11 +1,21 @@
|
||||
# vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040
|
||||
# Downloads don't work automatically, since the URL is regenerated via javascript.
|
||||
# Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib
|
||||
|
||||
python -m pip install --upgrade pip wheel
|
||||
|
||||
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
||||
|
||||
|
||||
pip install --find-links=build_helpers\ TA-Lib
|
||||
|
||||
if ($pyv -eq '3.8') {
|
||||
pip install build_helpers\TA_Lib-0.4.25-cp38-cp38-win_amd64.whl
|
||||
}
|
||||
if ($pyv -eq '3.9') {
|
||||
pip install build_helpers\TA_Lib-0.4.25-cp39-cp39-win_amd64.whl
|
||||
}
|
||||
if ($pyv -eq '3.10') {
|
||||
pip install build_helpers\TA_Lib-0.4.25-cp310-cp310-win_amd64.whl
|
||||
}
|
||||
if ($pyv -eq '3.11') {
|
||||
pip install build_helpers\TA_Lib-0.4.25-cp311-cp311-win_amd64.whl
|
||||
}
|
||||
pip install -r requirements-dev.txt
|
||||
pip install -e .
|
||||
|
||||
@@ -12,7 +12,6 @@ TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
||||
TAG_PLOT=${TAG}_plot
|
||||
TAG_FREQAI=${TAG}_freqai
|
||||
TAG_FREQAI_RL=${TAG_FREQAI}rl
|
||||
TAG_FREQAI_TORCH=${TAG_FREQAI}torch
|
||||
TAG_PI="${TAG}_pi"
|
||||
|
||||
TAG_ARM=${TAG}_arm
|
||||
@@ -43,9 +42,9 @@ if [ $? -ne 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
|
||||
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
|
||||
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl .
|
||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
|
||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
|
||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl .
|
||||
|
||||
# Tag image for upload and next build step
|
||||
docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
|
||||
@@ -85,10 +84,6 @@ docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI}
|
||||
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM}
|
||||
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL}
|
||||
|
||||
# Create special Torch tag - which is identical to the RL tag.
|
||||
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_TORCH} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM}
|
||||
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_TORCH}
|
||||
|
||||
# copy images to ghcr.io
|
||||
|
||||
alias crane="docker run --rm -i -v $(pwd)/.crane:/home/nonroot/.docker/ gcr.io/go-containerregistry/crane"
|
||||
@@ -98,7 +93,6 @@ chmod a+rwx .crane
|
||||
echo "${GHCR_TOKEN}" | crane auth login ghcr.io -u "${GHCR_USERNAME}" --password-stdin
|
||||
|
||||
crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL}
|
||||
crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_TORCH}
|
||||
crane copy ${IMAGE_NAME}:${TAG_FREQAI} ${GHCR_IMAGE_NAME}:${TAG_FREQAI}
|
||||
crane copy ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT}
|
||||
crane copy ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG}
|
||||
|
||||
@@ -58,9 +58,9 @@ fi
|
||||
# Tag image for upload and next build step
|
||||
docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG
|
||||
|
||||
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
|
||||
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai .
|
||||
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl .
|
||||
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
|
||||
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai .
|
||||
docker build --cache-from freqtrade:${TAG_FREQAI} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl .
|
||||
|
||||
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
|
||||
docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI
|
||||
|
||||
BIN
Binary file not shown.
+1
-10
@@ -6,15 +6,6 @@ services:
|
||||
# image: freqtradeorg/freqtrade:develop
|
||||
# Use plotting image
|
||||
# image: freqtradeorg/freqtrade:develop_plot
|
||||
# # Enable GPU Image and GPU Resources (only relevant for freqAI)
|
||||
# # Make sure to uncomment the whole deploy section
|
||||
# deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: 1
|
||||
# capabilities: [gpu]
|
||||
# Build step - only needed when additional dependencies are needed
|
||||
# build:
|
||||
# context: .
|
||||
@@ -25,7 +16,7 @@ services:
|
||||
- "./user_data:/freqtrade/user_data"
|
||||
# Expose api on port 8080 (localhost only)
|
||||
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
|
||||
# for more information.
|
||||
# before enabling this.
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
# Default command used when running `docker compose up`
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
version: '3'
|
||||
services:
|
||||
freqtrade:
|
||||
image: freqtradeorg/freqtrade:stable_freqaitorch
|
||||
# # Enable GPU Image and GPU Resources
|
||||
# # Make sure to uncomment the whole deploy section
|
||||
# deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: 1
|
||||
# capabilities: [gpu]
|
||||
|
||||
# Build step - only needed when additional dependencies are needed
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: "./docker/Dockerfile.custom"
|
||||
restart: unless-stopped
|
||||
container_name: freqtrade
|
||||
volumes:
|
||||
- "./user_data:/freqtrade/user_data"
|
||||
# Expose api on port 8080 (localhost only)
|
||||
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
|
||||
# for more information.
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
# Default command used when running `docker compose up`
|
||||
command: >
|
||||
trade
|
||||
--logfile /freqtrade/user_data/logs/freqtrade.log
|
||||
--db-url sqlite:////freqtrade/user_data/tradesv3.sqlite
|
||||
--config /freqtrade/user_data/config.json
|
||||
--freqaimodel XGBoostRegressor
|
||||
--strategy FreqaiExampleStrategy
|
||||
@@ -29,7 +29,7 @@ If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl`
|
||||
`user_data/backtest_results` folder.
|
||||
|
||||
To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-analysis` command
|
||||
with `--analysis-groups` option provided with space-separated arguments:
|
||||
with `--analysis-groups` option provided with space-separated arguments (default `0 1 2`):
|
||||
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4 5
|
||||
@@ -39,7 +39,6 @@ This command will read from the last backtesting results. The `--analysis-groups
|
||||
used to specify the various tabular outputs showing the profit fo each group or trade,
|
||||
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
|
||||
* 1: profit summaries grouped by enter_tag
|
||||
* 2: profit summaries grouped by enter_tag and exit_tag
|
||||
* 3: profit summaries grouped by pair and enter_tag
|
||||
@@ -103,22 +102,6 @@ The indicators have to be present in your strategy's main DataFrame (either for
|
||||
timeframe or for informative timeframes) otherwise they will simply be ignored in the script
|
||||
output.
|
||||
|
||||
There are a range of candle and trade-related fields that are included in the analysis so are
|
||||
automatically accessible by including them on the indicator-list, and these include:
|
||||
|
||||
- **open_date :** trade open datetime
|
||||
- **close_date :** trade close datetime
|
||||
- **min_rate :** minimum price seen throughout the position
|
||||
- **max_rate :** maxiumum price seen throughout the position
|
||||
- **open :** signal candle open price
|
||||
- **close :** signal candle close price
|
||||
- **high :** signal candle high price
|
||||
- **low :** signal candle low price
|
||||
- **volume :** signal candle volumne
|
||||
- **profit_ratio :** trade profit ratio
|
||||
- **profit_abs :** absolute profit return of the trade
|
||||
|
||||
|
||||
### Filtering the trade output by date
|
||||
|
||||
To show only trades between dates within your backtested timerange, supply the usual `timerange` option in `YYYYMMDD-[YYYYMMDD]` format:
|
||||
@@ -132,38 +115,3 @@ For example, if your backtest timerange was `20220101-20221231` but you only wan
|
||||
```bash
|
||||
freqtrade backtesting-analysis -c <config.json> --timerange 20220101-20220201
|
||||
```
|
||||
|
||||
### Printing out rejected signals
|
||||
|
||||
Use the `--rejected-signals` option to print out rejected signals.
|
||||
|
||||
```bash
|
||||
freqtrade backtesting-analysis -c <config.json> --rejected-signals
|
||||
```
|
||||
|
||||
### Writing tables to CSV
|
||||
|
||||
Some of the tabular outputs can become large, so printing them out to the terminal is not preferable.
|
||||
Use the `--analysis-to-csv` option to disable printing out of tables to standard out and write them to CSV files.
|
||||
|
||||
```bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv
|
||||
```
|
||||
|
||||
By default this will write one file per output table you specified in the `backtesting-analysis` command, e.g.
|
||||
|
||||
```bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --rejected-signals --analysis-groups 0 1
|
||||
```
|
||||
|
||||
This will write to `user_data/backtest_results`:
|
||||
|
||||
* rejected_signals.csv
|
||||
* group_0.csv
|
||||
* group_1.csv
|
||||
|
||||
To override where the files will be written, also specify the `--analysis-csv-path` option.
|
||||
|
||||
```bash
|
||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --analysis-csv-path another/data/path/
|
||||
```
|
||||
|
||||
@@ -136,7 +136,7 @@ class MyAwesomeStrategy(IStrategy):
|
||||
|
||||
### Dynamic parameters
|
||||
|
||||
Parameters can also be defined dynamically, but must be available to the instance once the [`bot_start()` callback](strategy-callbacks.md#bot-start) has been called.
|
||||
Parameters can also be defined dynamically, but must be available to the instance once the * [`bot_start()` callback](strategy-callbacks.md#bot-start) has been called.
|
||||
|
||||
``` python
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
+9
-14
@@ -274,20 +274,19 @@ A backtesting result will look like that:
|
||||
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
||||
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
|
||||
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
||||
====================================================== LEFT OPEN TRADES REPORT ======================================================
|
||||
| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|
||||
|:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
|
||||
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
||||
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
||||
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
||||
==================== EXIT REASON STATS ====================
|
||||
========================================================= EXIT REASON STATS ==========================================================
|
||||
| Exit Reason | Exits | Wins | Draws | Losses |
|
||||
|:-------------------|--------:|------:|-------:|--------:|
|
||||
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
||||
| stop_loss | 166 | 0 | 0 | 166 |
|
||||
| exit_signal | 56 | 36 | 0 | 20 |
|
||||
| force_exit | 2 | 0 | 0 | 2 |
|
||||
|
||||
====================================================== LEFT OPEN TRADES REPORT ======================================================
|
||||
| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|
||||
|:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
|
||||
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
||||
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
||||
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
||||
================== SUMMARY METRICS ==================
|
||||
| Metric | Value |
|
||||
|-----------------------------+---------------------|
|
||||
@@ -305,7 +304,7 @@ A backtesting result will look like that:
|
||||
| Sharpe | 2.97 |
|
||||
| Calmar | 6.29 |
|
||||
| Profit factor | 1.11 |
|
||||
| Expectancy (Ratio) | -0.15 (-0.05) |
|
||||
| Expectancy | -0.15 |
|
||||
| Avg. stake amount | 0.001 BTC |
|
||||
| Total trade volume | 0.429 BTC |
|
||||
| | |
|
||||
@@ -324,7 +323,6 @@ A backtesting result will look like that:
|
||||
| Days win/draw/lose | 12 / 82 / 25 |
|
||||
| Avg. Duration Winners | 4:23:00 |
|
||||
| Avg. Duration Loser | 6:55:00 |
|
||||
| Max Consecutive Wins / Loss | 3 / 4 |
|
||||
| Rejected Entry signals | 3089 |
|
||||
| Entry/Exit Timeouts | 0 / 0 |
|
||||
| Canceled Trade Entries | 34 |
|
||||
@@ -410,7 +408,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
| Sharpe | 2.97 |
|
||||
| Calmar | 6.29 |
|
||||
| Profit factor | 1.11 |
|
||||
| Expectancy (Ratio) | -0.15 (-0.05) |
|
||||
| Expectancy | -0.15 |
|
||||
| Avg. stake amount | 0.001 BTC |
|
||||
| Total trade volume | 0.429 BTC |
|
||||
| | |
|
||||
@@ -429,7 +427,6 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
| Days win/draw/lose | 12 / 82 / 25 |
|
||||
| Avg. Duration Winners | 4:23:00 |
|
||||
| Avg. Duration Loser | 6:55:00 |
|
||||
| Max Consecutive Wins / Loss | 3 / 4 |
|
||||
| Rejected Entry signals | 3089 |
|
||||
| Entry/Exit Timeouts | 0 / 0 |
|
||||
| Canceled Trade Entries | 34 |
|
||||
@@ -469,7 +466,6 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
- `Best day` / `Worst day`: Best and worst day based on daily profit.
|
||||
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
|
||||
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
||||
- `Max Consecutive Wins / Loss`: Maximum consecutive wins/losses in a row.
|
||||
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
|
||||
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
||||
- `Canceled Trade Entries`: Number of trades that have been canceled by user request via `adjust_entry_price`.
|
||||
@@ -537,7 +533,6 @@ Since backtesting lacks some detailed information about what happens within a ca
|
||||
- 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 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
|
||||
- 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 is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` exit reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes
|
||||
|
||||
+1
-1
@@ -60,10 +60,10 @@ This loop will be repeated again and again until the bot is stopped.
|
||||
|
||||
* Load historic data for configured pairlist.
|
||||
* Calls `bot_start()` once.
|
||||
* Calls `bot_loop_start()` once.
|
||||
* Calculate indicators (calls `populate_indicators()` once per pair).
|
||||
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
|
||||
* Loops per candle simulating entry and exit points.
|
||||
* Calls `bot_loop_start()` strategy callback.
|
||||
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
|
||||
* Calls `adjust_entry_price()` strategy callback for open entry orders.
|
||||
* Check for trade entry signals (`enter_long` / `enter_short` columns).
|
||||
|
||||
+16
-14
@@ -138,7 +138,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String
|
||||
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
|
||||
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
|
||||
| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float.
|
||||
| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account.[More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float.
|
||||
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
|
||||
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
|
||||
@@ -155,25 +155,25 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
|
||||
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
|
||||
| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates) <br>*Defaults to `None`.*<br> **Datatype:** Float
|
||||
| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates) <br>*Defaults to None.*<br> **Datatype:** Float
|
||||
| `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md). <br>*Defaults to `"spot"`.* <br> **Datatype:** String
|
||||
| `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md). <br> **Datatype:** String
|
||||
| `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md). <br>*Defaults to `0.05`.* <br> **Datatype:** Float
|
||||
| | **Unfilled timeout**
|
||||
| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||
| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `"minutes"`.* <br> **Datatype:** String
|
||||
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
|
||||
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
|
||||
| | **Pricing**
|
||||
| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).<br> *Defaults to `"same"`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
|
||||
| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#buy-price-side).<br> *Defaults to `same`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
|
||||
| `entry_pricing.price_last_balance` | **Required.** Interpolate the bidding price. More information [below](#entry-price-without-orderbook-enabled).
|
||||
| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled). <br> *Defaults to `true`.*<br> **Datatype:** Boolean
|
||||
| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean
|
||||
| `entry_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to enter a trade. I.e. a value of 2 will allow the bot to pick the 2nd entry in [Order Book Entry](#entry-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||
| `entry_pricing. check_depth_of_market.enabled` | Do not enter if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `entry_pricing. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio)
|
||||
| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).<br> *Defaults to `"same"`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
|
||||
| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).<br> *Defaults to `same`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
|
||||
| `exit_pricing.price_last_balance` | Interpolate the exiting price. More information [below](#exit-price-without-orderbook-enabled).
|
||||
| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled). <br> *Defaults to `true`.*<br> **Datatype:** Boolean
|
||||
| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean
|
||||
| `exit_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to exit. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Exit](#exit-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
|
||||
| | **TODO**
|
||||
@@ -199,10 +199,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
||||
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
|
||||
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| | **Plugins**
|
||||
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options.
|
||||
@@ -213,7 +213,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
|
||||
| `telegram.reload` | Allow "reload" buttons on telegram messages. <br>*Defaults to `true`.<br> **Datatype:** boolean
|
||||
| `telegram.reload` | Allow "reload" buttons on telegram messages. <br>*Defaults to `True`.<br> **Datatype:** boolean
|
||||
| `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.<br> **Datatype:** dictionary
|
||||
| `telegram.allow_custom_messages` | Enable the sending of Telegram messages from strategies via the dataprovider.send_msg() function. <br> **Datatype:** Boolean
|
||||
| | **Webhook**
|
||||
@@ -682,14 +682,16 @@ To use a proxy for exchange connections - you will have to define the proxies as
|
||||
{
|
||||
"exchange": {
|
||||
"ccxt_config": {
|
||||
"httpsProxy": "http://addr:port",
|
||||
"aiohttp_proxy": "http://addr:port",
|
||||
"proxies": {
|
||||
"http": "http://addr:port",
|
||||
"https": "http://addr:port"
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For more information on available proxy types, please consult the [ccxt proxy documentation](https://docs.ccxt.com/#/README?id=proxy).
|
||||
|
||||
## Next step
|
||||
|
||||
Now you have configured your config.json, the next step is to [start your bot](bot-usage.md).
|
||||
|
||||
+64
-68
@@ -6,7 +6,7 @@ To download data (candles / OHLCV) needed for backtesting and hyperoptimization
|
||||
|
||||
If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes for the last 30 days.
|
||||
Exchange and pairs will come from `config.json` (if specified using `-c/--config`).
|
||||
Without provided configuration, `--exchange` becomes mandatory.
|
||||
Otherwise `--exchange` becomes mandatory.
|
||||
|
||||
You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used.
|
||||
|
||||
@@ -83,47 +83,40 @@ Common arguments:
|
||||
|
||||
```
|
||||
|
||||
!!! Tip "Downloading all data for one quote currency"
|
||||
Often, you'll want to download data for all pairs of a specific quote-currency. In such cases, you can use the following shorthand:
|
||||
`freqtrade download-data --exchange binance --pairs .*/USDT <...>`. The provided "pairs" string will be expanded to contain all active pairs on the exchange.
|
||||
To also download data for inactive (delisted) pairs, add `--include-inactive-pairs` to the command.
|
||||
|
||||
!!! Note "Startup period"
|
||||
`download-data` is a strategy-independent command. The idea is to download a big chunk of data once, and then iteratively increase the amount of data stored.
|
||||
|
||||
For that reason, `download-data` does not care about the "startup-period" defined in a strategy. It's up to the user to download additional days if the backtest should start at a specific point in time (while respecting startup period).
|
||||
|
||||
### Start download
|
||||
### Pairs file
|
||||
|
||||
A very simple command (assuming an available `config.json` file) can look as follows.
|
||||
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
|
||||
If you are using Binance for example:
|
||||
|
||||
- create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
|
||||
- update the `pairs.json` file to contain the currency pairs you are interested in.
|
||||
|
||||
```bash
|
||||
freqtrade download-data --exchange binance
|
||||
mkdir -p user_data/data/binance
|
||||
touch user_data/data/binance/pairs.json
|
||||
```
|
||||
|
||||
This will download historical candle (OHLCV) data for all the currency pairs defined in the configuration.
|
||||
The format of the `pairs.json` file is a simple json list.
|
||||
Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
|
||||
|
||||
Alternatively, specify the pairs directly
|
||||
|
||||
```bash
|
||||
freqtrade download-data --exchange binance --pairs ETH/USDT XRP/USDT BTC/USDT
|
||||
``` json
|
||||
[
|
||||
"ETH/BTC",
|
||||
"ETH/USDT",
|
||||
"BTC/USDT",
|
||||
"XRP/ETH"
|
||||
]
|
||||
```
|
||||
|
||||
or as regex (in this case, to download all active USDT pairs)
|
||||
|
||||
```bash
|
||||
freqtrade download-data --exchange binance --pairs .*/USDT
|
||||
```
|
||||
|
||||
### Other Notes
|
||||
|
||||
* To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
|
||||
* To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.)
|
||||
* To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
|
||||
* To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
|
||||
* To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020.
|
||||
* Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
|
||||
* To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
||||
!!! Tip "Downloading all data for one quote currency"
|
||||
Often, you'll want to download data for all pairs of a specific quote-currency. In such cases, you can use the following shorthand:
|
||||
`freqtrade download-data --exchange binance --pairs .*/USDT <...>`. The provided "pairs" string will be expanded to contain all active pairs on the exchange.
|
||||
To also download data for inactive (delisted) pairs, add `--include-inactive-pairs` to the command.
|
||||
|
||||
??? Note "Permission denied errors"
|
||||
If your configuration directory `user_data` was made by docker, you may get the following error:
|
||||
@@ -138,7 +131,39 @@ freqtrade download-data --exchange binance --pairs .*/USDT
|
||||
sudo chown -R $UID:$GID user_data
|
||||
```
|
||||
|
||||
### Download additional data before the current timerange
|
||||
### Start download
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
freqtrade download-data --exchange binance
|
||||
```
|
||||
|
||||
This will download historical candle (OHLCV) data for all the currency pairs you defined in `pairs.json`.
|
||||
|
||||
Alternatively, specify the pairs directly
|
||||
|
||||
```bash
|
||||
freqtrade download-data --exchange binance --pairs ETH/USDT XRP/USDT BTC/USDT
|
||||
```
|
||||
|
||||
or as regex (to download all active USDT pairs)
|
||||
|
||||
```bash
|
||||
freqtrade download-data --exchange binance --pairs .*/USDT
|
||||
```
|
||||
|
||||
### Other Notes
|
||||
|
||||
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
|
||||
- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.)
|
||||
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
|
||||
- To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
|
||||
- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020.
|
||||
- Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
|
||||
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
||||
|
||||
#### Download additional data before the current timerange
|
||||
|
||||
Assuming you downloaded all data from 2022 (`--timerange 20220101-`) - but you'd now like to also backtest with earlier data.
|
||||
You can do so by using the `--prepend` flag, combined with `--timerange` - specifying an end-date.
|
||||
@@ -213,36 +238,7 @@ Size has been taken from the BTC/USDT 1m spot combination for the timerange spec
|
||||
|
||||
To have a best performance/size mix, we recommend the use of either feather or parquet.
|
||||
|
||||
### Pairs file
|
||||
|
||||
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
|
||||
If you are using Binance for example:
|
||||
|
||||
* create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
|
||||
* update the `pairs.json` file to contain the currency pairs you are interested in.
|
||||
|
||||
```bash
|
||||
mkdir -p user_data/data/binance
|
||||
touch user_data/data/binance/pairs.json
|
||||
```
|
||||
|
||||
The format of the `pairs.json` file is a simple json list.
|
||||
Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
|
||||
|
||||
``` json
|
||||
[
|
||||
"ETH/BTC",
|
||||
"ETH/USDT",
|
||||
"BTC/USDT",
|
||||
"XRP/ETH"
|
||||
]
|
||||
```
|
||||
|
||||
!!! Note
|
||||
The `pairs.json` file is only used when no configuration is loaded (implicitly by naming, or via `--config` flag).
|
||||
You can force the usage of this file via `--pairs-file pairs.json` - however we recommend to use the pairlist from within the configuration, either via `exchange.pair_whitelist` or `pairs` setting in the configuration.
|
||||
|
||||
## Sub-command convert data
|
||||
#### Sub-command convert data
|
||||
|
||||
```
|
||||
usage: freqtrade convert-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
@@ -294,7 +290,7 @@ Common arguments:
|
||||
|
||||
```
|
||||
|
||||
### Example converting data
|
||||
##### Example converting data
|
||||
|
||||
The following command will convert all candle (OHLCV) data available in `~/.freqtrade/data/binance` from json to jsongz, saving diskspace in the process.
|
||||
It'll also remove original json data files (`--erase` parameter).
|
||||
@@ -303,7 +299,7 @@ It'll also remove original json data files (`--erase` parameter).
|
||||
freqtrade convert-data --format-from json --format-to jsongz --datadir ~/.freqtrade/data/binance -t 5m 15m --erase
|
||||
```
|
||||
|
||||
## Sub-command convert trade data
|
||||
#### Sub-command convert trade data
|
||||
|
||||
```
|
||||
usage: freqtrade convert-trade-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
@@ -346,7 +342,7 @@ Common arguments:
|
||||
|
||||
```
|
||||
|
||||
### Example converting trades
|
||||
##### Example converting trades
|
||||
|
||||
The following command will convert all available trade-data in `~/.freqtrade/data/kraken` from jsongz to json.
|
||||
It'll also remove original jsongz data files (`--erase` parameter).
|
||||
@@ -355,7 +351,7 @@ It'll also remove original jsongz data files (`--erase` parameter).
|
||||
freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase
|
||||
```
|
||||
|
||||
## Sub-command trades to ohlcv
|
||||
### Sub-command trades to ohlcv
|
||||
|
||||
When you need to use `--dl-trades` (kraken only) to download data, conversion of trades data to ohlcv data is the last step.
|
||||
This command will allow you to repeat this last step for additional timeframes without re-downloading the data.
|
||||
@@ -404,13 +400,13 @@ Common arguments:
|
||||
|
||||
```
|
||||
|
||||
### Example trade-to-ohlcv conversion
|
||||
#### Example trade-to-ohlcv conversion
|
||||
|
||||
``` bash
|
||||
freqtrade trades-to-ohlcv --exchange kraken -t 5m 1h 1d --pairs BTC/EUR ETH/EUR
|
||||
```
|
||||
|
||||
## Sub-command list-data
|
||||
### Sub-command list-data
|
||||
|
||||
You can get a list of downloaded data using the `list-data` sub-command.
|
||||
|
||||
@@ -455,7 +451,7 @@ Common arguments:
|
||||
|
||||
```
|
||||
|
||||
### Example list-data
|
||||
#### Example list-data
|
||||
|
||||
```bash
|
||||
> freqtrade list-data --userdir ~/.freqtrade/user_data/
|
||||
@@ -469,7 +465,7 @@ ETH/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
|
||||
ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
|
||||
```
|
||||
|
||||
## Trades (tick) data
|
||||
### Trades (tick) data
|
||||
|
||||
By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API.
|
||||
This data can be useful if you need many different timeframes, since it is only downloaded once, and then resampled locally to the desired timeframes.
|
||||
|
||||
+5
-11
@@ -327,18 +327,18 @@ To check how the new exchange behaves, you can use the following snippet:
|
||||
|
||||
``` python
|
||||
import ccxt
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from freqtrade.data.converter import ohlcv_to_dataframe
|
||||
ct = ccxt.binance() # Use the exchange you're testing
|
||||
ct = ccxt.binance()
|
||||
timeframe = "1d"
|
||||
pair = "BTC/USDT" # Make sure to use a pair that exists on that exchange!
|
||||
pair = "XLM/BTC" # Make sure to use a pair that exists on that exchange!
|
||||
raw = ct.fetch_ohlcv(pair, timeframe=timeframe)
|
||||
|
||||
# convert to dataframe
|
||||
df1 = ohlcv_to_dataframe(raw, timeframe, pair=pair, drop_incomplete=False)
|
||||
|
||||
print(df1.tail(1))
|
||||
print(datetime.now(timezone.utc))
|
||||
print(datetime.utcnow())
|
||||
```
|
||||
|
||||
``` output
|
||||
@@ -453,13 +453,7 @@ Once the PR against stable is merged (best right after merging):
|
||||
* Use the button "Draft a new release" in the Github UI (subsection releases).
|
||||
* Use the version-number specified as tag.
|
||||
* Use "stable" as reference (this step comes after the above PR is merged).
|
||||
* Use the above changelog as release comment (as codeblock).
|
||||
* Use the below snippet for the new release
|
||||
|
||||
??? Tip "Release template"
|
||||
````
|
||||
--8<-- "includes/release_template.md"
|
||||
````
|
||||
* Use the above changelog as release comment (as codeblock)
|
||||
|
||||
## Releases
|
||||
|
||||
|
||||
+1
-8
@@ -259,17 +259,10 @@ The configuration parameter `exchange.unknown_fee_rate` can be used to specify t
|
||||
|
||||
Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode.
|
||||
Users with unified accounts (there's no way back) can create a Sub-account which will start as "non-unified", and can therefore use isolated futures.
|
||||
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors
|
||||
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors.
|
||||
|
||||
As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well.
|
||||
|
||||
API Keys for live futures trading (Subaccount on non-unified) must have the following permissions:
|
||||
* Read-write
|
||||
* Contract - Orders
|
||||
* Contract - Positions
|
||||
|
||||
We do strongly recommend to limit all API keys to the IP you're going to use it from.
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
||||
|
||||
@@ -142,13 +142,6 @@ To fix this, redefine order types in the strategy to use "limit" instead of "mar
|
||||
|
||||
The same fix should be applied in the configuration file, if order types are defined in your custom config rather than in the strategy.
|
||||
|
||||
### I'm trying to start the bot live, but get an API permission error
|
||||
|
||||
Errors like `Invalid API-key, IP, or permissions for action` mean exactly what they actually say.
|
||||
Your API key is either invalid (copy/paste error? check for leading/trailing spaces in the config), expired, or the IP you're running the bot from is not enabled in the Exchange's API console.
|
||||
Usually, the permission "Spot Trading" (or the equivalent in the exchange you use) will be necessary.
|
||||
Futures will usually have to be enabled specifically.
|
||||
|
||||
### How do I search the bot logs for something?
|
||||
|
||||
By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility sub-commands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout.
|
||||
|
||||
@@ -43,16 +43,16 @@ The FreqAI strategy requires including the following lines of code in the standa
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
# the model will return all labels created by user in `set_freqai_targets()`
|
||||
# the model will return all labels created by user in `set_freqai_labels()`
|
||||
# (& appended targets), an indication of whether or not the prediction should be accepted,
|
||||
# the target mean/std values for each of the labels created by user in
|
||||
# `set_freqai_targets()` for each training period.
|
||||
# `feature_engineering_*` for each training period.
|
||||
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period, **kwargs) -> DataFrame:
|
||||
def feature_engineering_expand_all(self, dataframe, period, **kwargs):
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
This function will automatically expand the defined features on the config defined
|
||||
@@ -77,7 +77,7 @@ The FreqAI strategy requires including the following lines of code in the standa
|
||||
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame:
|
||||
def feature_engineering_expand_basic(self, dataframe, **kwargs):
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
This function will automatically expand the defined features on the config defined
|
||||
@@ -101,7 +101,7 @@ The FreqAI strategy requires including the following lines of code in the standa
|
||||
dataframe["%-raw_price"] = dataframe["close"]
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame:
|
||||
def feature_engineering_standard(self, dataframe, **kwargs):
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
This optional function will be called once with the dataframe of the base timeframe.
|
||||
@@ -122,7 +122,7 @@ The FreqAI strategy requires including the following lines of code in the standa
|
||||
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
|
||||
return dataframe
|
||||
|
||||
def set_freqai_targets(self, dataframe: DataFrame, **kwargs) -> DataFrame:
|
||||
def set_freqai_targets(self, dataframe, **kwargs):
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
Required function to set the targets for the model.
|
||||
@@ -139,7 +139,6 @@ The FreqAI strategy requires including the following lines of code in the standa
|
||||
/ dataframe["close"]
|
||||
- 1
|
||||
)
|
||||
return dataframe
|
||||
```
|
||||
|
||||
Notice how the `feature_engineering_*()` is where [features](freqai-feature-engineering.md#feature-engineering) are added. Meanwhile `set_freqai_targets()` adds the labels/targets. A full example strategy is available in `templates/FreqaiExampleStrategy.py`.
|
||||
@@ -160,7 +159,7 @@ Below are the values you can expect to include/use inside a typical strategy dat
|
||||
|------------|-------------|
|
||||
| `df['&*']` | Any dataframe column prepended with `&` in `set_freqai_targets()` is treated as a training target (label) inside FreqAI (typically following the naming convention `&-s*`). For example, to predict the close price 40 candles into the future, you would set `df['&-s_close'] = df['close'].shift(-self.freqai_info["feature_parameters"]["label_period_candles"])` with `"label_period_candles": 40` in the config. FreqAI makes the predictions and gives them back under the same key (`df['&-s_close']`) to be used in `populate_entry/exit_trend()`. <br> **Datatype:** Depends on the output of the model.
|
||||
| `df['&*_std/mean']` | Standard deviation and mean values of the defined labels during training (or live tracking with `fit_live_predictions_candles`). Commonly used to understand the rarity of a prediction (use the z-score as shown in `templates/FreqaiExampleStrategy.py` and explained [here](#creating-a-dynamic-target-threshold) to evaluate how often a particular prediction was observed during training or historically with `fit_live_predictions_candles`). <br> **Datatype:** Float.
|
||||
| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, FreqAI will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`. <br> **Datatype:** Integer between -2 and 2.
|
||||
| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, FreqAI will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers()` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`. <br> **Datatype:** Integer between -2 and 2.
|
||||
| `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence FreqAI has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Float.
|
||||
| `df['%*']` | Any dataframe column prepended with `%` in `feature_engineering_*()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md). <br> **Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from FreqAI to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%`. <br> **Datatype:** Depends on the output of the model.
|
||||
|
||||
@@ -237,181 +236,3 @@ If you want to predict multiple targets you must specify all labels in the same
|
||||
df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down')
|
||||
df['&s-up_or_down'] = np.where( df["close"].shift(-100) == df["close"], 'same', df['&s-up_or_down'])
|
||||
```
|
||||
|
||||
## PyTorch Module
|
||||
|
||||
### Quick start
|
||||
|
||||
The easiest way to quickly run a pytorch model is with the following command (for regression task):
|
||||
|
||||
```bash
|
||||
freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel PyTorchMLPRegressor --strategy-path freqtrade/templates
|
||||
```
|
||||
|
||||
!!! Note "Installation/docker"
|
||||
The PyTorch module requires large packages such as `torch`, which should be explicitly requested during `./setup.sh -i` by answering "y" to the question "Do you also want dependencies for freqai-rl or PyTorch (~700mb additional space required) [y/N]?".
|
||||
Users who prefer docker should ensure they use the docker image appended with `_freqaitorch`.
|
||||
We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file.
|
||||
This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available.
|
||||
|
||||
### Structure
|
||||
|
||||
#### Model
|
||||
|
||||
You can construct your own Neural Network architecture in PyTorch by simply defining your `nn.Module` class inside your custom [`IFreqaiModel` file](#using-different-prediction-models) and then using that class in your `def train()` function. Here is an example of logistic regression model implementation using PyTorch (should be used with nn.BCELoss criterion) for classification tasks.
|
||||
|
||||
```python
|
||||
|
||||
class LogisticRegression(nn.Module):
|
||||
def __init__(self, input_size: int):
|
||||
super().__init__()
|
||||
# Define your layers
|
||||
self.linear = nn.Linear(input_size, 1)
|
||||
self.activation = nn.Sigmoid()
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
# Define the forward pass
|
||||
out = self.linear(x)
|
||||
out = self.activation(out)
|
||||
return out
|
||||
|
||||
class MyCoolPyTorchClassifier(BasePyTorchClassifier):
|
||||
"""
|
||||
This is a custom IFreqaiModel showing how a user might setup their own
|
||||
custom Neural Network architecture for their training.
|
||||
"""
|
||||
|
||||
@property
|
||||
def data_convertor(self) -> PyTorchDataConvertor:
|
||||
return DefaultPyTorchDataConvertor(target_tensor_type=torch.float)
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
config = self.freqai_info.get("model_training_parameters", {})
|
||||
self.learning_rate: float = config.get("learning_rate", 3e-4)
|
||||
self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {})
|
||||
self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {})
|
||||
|
||||
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
"""
|
||||
User sets up the training and test data to fit their desired model here
|
||||
:param data_dictionary: the dictionary holding all data for train, test,
|
||||
labels, weights
|
||||
:param dk: The datakitchen object for the current coin/model
|
||||
"""
|
||||
|
||||
class_names = self.get_class_names()
|
||||
self.convert_label_column_to_int(data_dictionary, dk, class_names)
|
||||
n_features = data_dictionary["train_features"].shape[-1]
|
||||
model = LogisticRegression(
|
||||
input_dim=n_features
|
||||
)
|
||||
model.to(self.device)
|
||||
optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate)
|
||||
criterion = torch.nn.CrossEntropyLoss()
|
||||
init_model = self.get_init_model(dk.pair)
|
||||
trainer = PyTorchModelTrainer(
|
||||
model=model,
|
||||
optimizer=optimizer,
|
||||
criterion=criterion,
|
||||
model_meta_data={"class_names": class_names},
|
||||
device=self.device,
|
||||
init_model=init_model,
|
||||
data_convertor=self.data_convertor,
|
||||
**self.trainer_kwargs,
|
||||
)
|
||||
trainer.fit(data_dictionary, self.splits)
|
||||
return trainer
|
||||
|
||||
```
|
||||
|
||||
#### Trainer
|
||||
|
||||
The `PyTorchModelTrainer` performs the idiomatic PyTorch train loop:
|
||||
Define our model, loss function, and optimizer, and then move them to the appropriate device (GPU or CPU). Inside the loop, we iterate through the batches in the dataloader, move the data to the device, compute the prediction and loss, backpropagate, and update the model parameters using the optimizer.
|
||||
|
||||
In addition, the trainer is responsible for the following:
|
||||
- saving and loading the model
|
||||
- converting the data from `pandas.DataFrame` to `torch.Tensor`.
|
||||
|
||||
#### Integration with Freqai module
|
||||
|
||||
Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy.
|
||||
From top to bottom:
|
||||
|
||||
1. `BasePyTorchModel` - Implements the `train` method. all `BasePyTorch*` inherit it. responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device` attribute used by children classes. Sets `model_type` attribute used by the parent class.
|
||||
2. `BasePyTorch*` - Implements the `predict` method. Here, the `*` represents a group of algorithms, such as classifiers or regressors. responsible for data preprocessing, predicting, and postprocessing if needed.
|
||||
3. `PyTorch*Classifier` / `PyTorch*Regressor` - implements the `fit` method. responsible for the main train flaw, where we initialize the trainer and model objects.
|
||||
|
||||

|
||||
|
||||
#### Full example
|
||||
|
||||
Building a PyTorch regressor using MLP (multilayer perceptron) model, MSELoss criterion, and AdamW optimizer.
|
||||
|
||||
```python
|
||||
class PyTorchMLPRegressor(BasePyTorchRegressor):
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
config = self.freqai_info.get("model_training_parameters", {})
|
||||
self.learning_rate: float = config.get("learning_rate", 3e-4)
|
||||
self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {})
|
||||
self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {})
|
||||
|
||||
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
n_features = data_dictionary["train_features"].shape[-1]
|
||||
model = PyTorchMLPModel(
|
||||
input_dim=n_features,
|
||||
output_dim=1,
|
||||
**self.model_kwargs
|
||||
)
|
||||
model.to(self.device)
|
||||
optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate)
|
||||
criterion = torch.nn.MSELoss()
|
||||
init_model = self.get_init_model(dk.pair)
|
||||
trainer = PyTorchModelTrainer(
|
||||
model=model,
|
||||
optimizer=optimizer,
|
||||
criterion=criterion,
|
||||
device=self.device,
|
||||
init_model=init_model,
|
||||
target_tensor_type=torch.float,
|
||||
**self.trainer_kwargs,
|
||||
)
|
||||
trainer.fit(data_dictionary)
|
||||
return trainer
|
||||
```
|
||||
|
||||
Here we create a `PyTorchMLPRegressor` class that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel`, where the former implements the `predict` method that is suitable for our regression task, and the latter implements the train method.
|
||||
|
||||
??? Note "Setting Class Names for Classifiers"
|
||||
When using classifiers, the user must declare the class names (or targets) by overriding the `IFreqaiModel.class_names` attribute. This is achieved by setting `self.freqai.class_names` in the FreqAI strategy inside the `set_freqai_targets` method.
|
||||
|
||||
For example, if you are using a binary classifier to predict price movements as up or down, you can set the class names as follows:
|
||||
```python
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
|
||||
self.freqai.class_names = ["down", "up"]
|
||||
dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) >
|
||||
dataframe["close"], 'up', 'down')
|
||||
|
||||
return dataframe
|
||||
```
|
||||
To see a full example, you can refer to the [classifier test strategy class](https://github.com/freqtrade/freqtrade/blob/develop/tests/strategy/strats/freqai_test_classifier.py).
|
||||
|
||||
|
||||
#### Improving performance with `torch.compile()`
|
||||
|
||||
Torch provides a `torch.compile()` method that can be used to improve performance for specific GPU hardware. More details can be found [here](https://pytorch.org/tutorials/intermediate/torch_compile_tutorial.html). In brief, you simply wrap your `model` in `torch.compile()`:
|
||||
|
||||
|
||||
```python
|
||||
model = PyTorchMLPModel(
|
||||
input_dim=n_features,
|
||||
output_dim=1,
|
||||
**self.model_kwargs
|
||||
)
|
||||
model.to(self.device)
|
||||
model = torch.compile(model)
|
||||
```
|
||||
|
||||
Then proceed to use the model as normal. Keep in mind that doing this will remove eager execution, which means errors and tracebacks will not be informative.
|
||||
|
||||
@@ -6,8 +6,8 @@ Low level feature engineering is performed in the user strategy within a set of
|
||||
|
||||
| Function | Description |
|
||||
|---------------|-------------|
|
||||
| `feature_engineering_expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
||||
| `feature_engineering_expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`.
|
||||
| `feature_engineering__expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
||||
| `feature_engineering__expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`.
|
||||
| `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week).
|
||||
| `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals.
|
||||
|
||||
@@ -16,7 +16,7 @@ Meanwhile, high level feature engineering is handled within `"feature_parameters
|
||||
It is advisable to start from the template `feature_engineering_*` functions in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy:
|
||||
|
||||
```python
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame:
|
||||
def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs):
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
This function will automatically expand the defined features on the config defined
|
||||
@@ -67,7 +67,7 @@ It is advisable to start from the template `feature_engineering_*` functions in
|
||||
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame:
|
||||
def feature_engineering_expand_basic(self, dataframe, metadata, **kwargs):
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
This function will automatically expand the defined features on the config defined
|
||||
@@ -96,7 +96,7 @@ It is advisable to start from the template `feature_engineering_*` functions in
|
||||
dataframe["%-raw_price"] = dataframe["close"]
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame:
|
||||
def feature_engineering_standard(self, dataframe, metadata, **kwargs):
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
This optional function will be called once with the dataframe of the base timeframe.
|
||||
@@ -122,7 +122,7 @@ It is advisable to start from the template `feature_engineering_*` functions in
|
||||
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
|
||||
return dataframe
|
||||
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame:
|
||||
def set_freqai_targets(self, dataframe, metadata, **kwargs):
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
Required function to set the targets for the model.
|
||||
@@ -180,18 +180,16 @@ You can ask for each of the defined features to be included also for informative
|
||||
|
||||
In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
|
||||
$= 3 * 3 * 3 * 2 * 2 = 108$.
|
||||
|
||||
!!! note "Learn more about creative feature engineering"
|
||||
Check out our [medium article](https://emergentmethods.medium.com/freqai-from-price-to-prediction-6fadac18b665) geared toward helping users learn how to creatively engineer features.
|
||||
|
||||
### Gain finer control over `feature_engineering_*` functions with `metadata`
|
||||
|
||||
All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc.
|
||||
### Gain finer control over `feature_engineering_*` functions with `metadata`
|
||||
|
||||
```python
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame:
|
||||
if metadata["tf"] == "1h":
|
||||
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
||||
All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc.
|
||||
|
||||
```py
|
||||
def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs):
|
||||
if metadata["tf"] == "1h":
|
||||
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
||||
```
|
||||
|
||||
This will block `ta.ROC()` from being added to any timeframes other than `"1h"`.
|
||||
@@ -212,7 +210,41 @@ Another example, where the user wants to use live metrics from the trade databas
|
||||
|
||||
You need to set the standard dictionary in the config so that FreqAI can return proper dataframe shapes. These values will likely be overridden by the prediction model, but in the case where the model has yet to set them, or needs a default initial value, the pre-set values are what will be returned.
|
||||
|
||||
### Weighting features for temporal importance
|
||||
## Feature normalization
|
||||
|
||||
FreqAI is strict when it comes to data normalization. The train features, $X^{train}$, are always normalized to [-1, 1] using a shifted min-max normalization:
|
||||
|
||||
$$X^{train}_{norm} = 2 * \frac{X^{train} - X^{train}.min()}{X^{train}.max() - X^{train}.min()} - 1$$
|
||||
|
||||
All other data (test data and unseen prediction data in dry/live/backtest) is always automatically normalized to the training feature space according to industry standards. FreqAI stores all the metadata required to ensure that test and prediction features will be properly normalized and that predictions are properly denormalized. For this reason, it is not recommended to eschew industry standards and modify FreqAI internals - however - advanced users can do so by inheriting `train()` in their custom `IFreqaiModel` and using their own normalization functions.
|
||||
|
||||
## 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.
|
||||
|
||||
## Inlier metric
|
||||
|
||||
The `inlier_metric` is a metric aimed at quantifying how similar the features of a data point are to the most recent historical data points.
|
||||
|
||||
You define the lookback window by setting `inlier_metric_window` and FreqAI computes the distance between the present time point and each of the previous `inlier_metric_window` lookback points. A Weibull function is fit to each of the lookback distributions and its cumulative distribution function (CDF) is used to produce a quantile for each lookback point. The `inlier_metric` is then computed for each time point as the average of the corresponding lookback quantiles. The figure below explains the concept for an `inlier_metric_window` of 5.
|
||||
|
||||

|
||||
|
||||
FreqAI adds the `inlier_metric` to the training features and hence gives the model access to a novel type of temporal information.
|
||||
|
||||
This function does **not** remove outliers from the data set.
|
||||
|
||||
## Weighting features for temporal importance
|
||||
|
||||
FreqAI allows you to set a `weight_factor` to weight recent data more strongly than past data via an exponential function:
|
||||
|
||||
@@ -222,103 +254,13 @@ where $W_i$ is the weight of data point $i$ in a total set of $n$ data points. B
|
||||
|
||||

|
||||
|
||||
## Building the data pipeline
|
||||
|
||||
By default, FreqAI builds a dynamic pipeline based on user congfiguration settings. The default settings are robust and designed to work with a variety of methods. These two steps are a `MinMaxScaler(-1,1)` and a `VarianceThreshold` which removes any column that has 0 variance. Users can activate other steps with more configuration parameters. For example if users add `use_SVM_to_remove_outliers: true` to the `freqai` config, then FreqAI will automatically add the [`SVMOutlierExtractor`](#identifying-outliers-using-a-support-vector-machine-svm) to the pipeline. Likewise, users can add `principal_component_analysis: true` to the `freqai` config to activate PCA. The [DissimilarityIndex](#identifying-outliers-with-the-dissimilarity-index-di) is activated with `DI_threshold: 1`. Finally, noise can also be added to the data with `noise_standard_deviation: 0.1`. Finally, users can add [DBSCAN](#identifying-outliers-with-dbscan) outlier removal with `use_DBSCAN_to_remove_outliers: true`.
|
||||
|
||||
!!! note "More information available"
|
||||
Please review the [parameter table](freqai-parameter-table.md) for more information on these parameters.
|
||||
|
||||
|
||||
### Customizing the pipeline
|
||||
|
||||
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"
|
||||
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.
|
||||
|
||||
```python
|
||||
from datasieve.transforms import SKLearnWrapper, DissimilarityIndex
|
||||
from datasieve.pipeline import Pipeline
|
||||
from sklearn.preprocessing import QuantileTransformer, StandardScaler
|
||||
from freqai.base_models import BaseRegressionModel
|
||||
|
||||
|
||||
class MyFreqaiModel(BaseRegressionModel):
|
||||
"""
|
||||
Some cool custom model
|
||||
"""
|
||||
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
"""
|
||||
My custom fit function
|
||||
"""
|
||||
model = cool_model.fit()
|
||||
return model
|
||||
|
||||
def define_data_pipeline(self) -> Pipeline:
|
||||
"""
|
||||
User defines their custom feature pipeline here (if they wish)
|
||||
"""
|
||||
feature_pipeline = Pipeline([
|
||||
('qt', SKLearnWrapper(QuantileTransformer(output_distribution='normal'))),
|
||||
('di', ds.DissimilarityIndex(di_threshold=1))
|
||||
])
|
||||
|
||||
return feature_pipeline
|
||||
|
||||
def define_label_pipeline(self) -> Pipeline:
|
||||
"""
|
||||
User defines their custom label pipeline here (if they wish)
|
||||
"""
|
||||
label_pipeline = Pipeline([
|
||||
('qt', SKLearnWrapper(StandardScaler())),
|
||||
])
|
||||
|
||||
return label_pipeline
|
||||
```
|
||||
|
||||
Here, you are defining the exact pipeline that will be used for your feature set during training and prediction. You can use *most* SKLearn transformation steps by wrapping them in the `SKLearnWrapper` class as shown above. In addition, you can use any of the transformations available in the [`DataSieve` library](https://github.com/emergentmethods/datasieve).
|
||||
|
||||
You can easily add your own transformation by creating a class that inherits from the datasieve `BaseTransform` and implementing your `fit()`, `transform()` and `inverse_transform()` methods:
|
||||
|
||||
```python
|
||||
from datasieve.transforms.base_transform import BaseTransform
|
||||
# import whatever else you need
|
||||
|
||||
class MyCoolTransform(BaseTransform):
|
||||
def __init__(self, **kwargs):
|
||||
self.param1 = kwargs.get('param1', 1)
|
||||
|
||||
def fit(self, X, y=None, sample_weight=None, feature_list=None, **kwargs):
|
||||
# do something with X, y, sample_weight, or/and feature_list
|
||||
return X, y, sample_weight, feature_list
|
||||
|
||||
def transform(self, X, y=None, sample_weight=None,
|
||||
feature_list=None, outlier_check=False, **kwargs):
|
||||
# do something with X, y, sample_weight, or/and feature_list
|
||||
return X, y, sample_weight, feature_list
|
||||
|
||||
def inverse_transform(self, X, y=None, sample_weight=None, feature_list=None, **kwargs):
|
||||
# do/dont do something with X, y, sample_weight, or/and feature_list
|
||||
return X, y, sample_weight, feature_list
|
||||
```
|
||||
|
||||
!!! note "Hint"
|
||||
You can define this custom class in the same file as your `IFreqaiModel`.
|
||||
|
||||
### Migrating a custom `IFreqaiModel` to the new Pipeline
|
||||
|
||||
If you have created your own custom `IFreqaiModel` with a custom `train()`/`predict()` function, *and* you still rely on `data_cleaning_train/predict()`, then you will need to migrate to the new pipeline. If your model does *not* rely on `data_cleaning_train/predict()`, then you do not need to worry about this migration.
|
||||
|
||||
More details about the migration can be found [here](strategy_migration.md#freqai---new-data-pipeline).
|
||||
|
||||
## Outlier detection
|
||||
|
||||
Equity and crypto markets suffer from a high level of non-patterned noise in the form of outlier data points. FreqAI implements a variety of methods to identify such outliers and hence mitigate risk.
|
||||
|
||||
### Identifying outliers with the Dissimilarity Index (DI)
|
||||
|
||||
The Dissimilarity Index (DI) aims to quantify the uncertainty associated with each prediction made by the model.
|
||||
The Dissimilarity Index (DI) aims to quantify the uncertainty associated with each prediction made by the model.
|
||||
|
||||
You can tell FreqAI to remove outlier data points from the training/test data sets using the DI by including the following statement in the config:
|
||||
|
||||
@@ -330,7 +272,7 @@ You can tell FreqAI to remove outlier data points from the training/test data se
|
||||
}
|
||||
```
|
||||
|
||||
Which will add `DissimilarityIndex` step to your `feature_pipeline` and set the threshold to 1. The DI allows predictions which are outliers (not existent in the model feature space) to be thrown out due to low levels of certainty. To do so, FreqAI measures the distance between each training data point (feature vector), $X_{a}$, and all other training data points:
|
||||
The DI allows predictions which are outliers (not existent in the model feature space) to be thrown out due to low levels of certainty. To do so, FreqAI measures the distance between each training data point (feature vector), $X_{a}$, and all other training data points:
|
||||
|
||||
$$ d_{ab} = \sqrt{\sum_{j=1}^p(X_{a,j}-X_{b,j})^2} $$
|
||||
|
||||
@@ -364,9 +306,9 @@ You can tell FreqAI to remove outlier data points from the training/test data se
|
||||
}
|
||||
```
|
||||
|
||||
Which will add `SVMOutlierExtractor` step to your `feature_pipeline`. The SVM will be trained on the training data and any data point that the SVM deems to be beyond the feature space will be removed.
|
||||
The SVM will be trained on the training data and any data point that the SVM deems to be beyond the feature space will be removed.
|
||||
|
||||
You can elect to provide additional parameters for the SVM, such as `shuffle`, and `nu` via the `feature_parameters.svm_params` dictionary in the config.
|
||||
FreqAI uses `sklearn.linear_model.SGDOneClassSVM` (details are available on scikit-learn's webpage [here](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDOneClassSVM.html) (external website)) and you can elect to provide additional parameters for the SVM, such as `shuffle`, and `nu`.
|
||||
|
||||
The parameter `shuffle` is by default set to `False` to ensure consistent results. If it is set to `True`, running the SVM multiple times on the same data set might result in different outcomes due to `max_iter` being to low for the algorithm to reach the demanded `tol`. Increasing `max_iter` solves this issue but causes the procedure to take longer time.
|
||||
|
||||
@@ -384,7 +326,7 @@ You can configure FreqAI to use DBSCAN to cluster and remove outliers from the t
|
||||
}
|
||||
```
|
||||
|
||||
Which will add the `DataSieveDBSCAN` step to your `feature_pipeline`. This is an unsupervised machine learning algorithm that clusters data without needing to know how many clusters there should be.
|
||||
DBSCAN is an unsupervised machine learning algorithm that clusters data without needing to know how many clusters there should be.
|
||||
|
||||
Given a number of data points $N$, and a distance $\varepsilon$, DBSCAN clusters the data set by setting all data points that have $N-1$ other data points within a distance of $\varepsilon$ as *core points*. A data point that is within a distance of $\varepsilon$ from a *core point* but that does not have $N-1$ other data points within a distance of $\varepsilon$ from itself is considered an *edge point*. A cluster is then the collection of *core points* and *edge points*. Data points that have no other data points at a distance $<\varepsilon$ are considered outliers. The figure below shows a cluster with $N = 3$.
|
||||
|
||||
|
||||
@@ -18,10 +18,9 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
||||
| `purge_old_models` | Number of models to keep on disk (not relevant to backtesting). Default is 2, which means that dry/live runs will keep the latest 2 models on disk. Setting to 0 keeps all models. This parameter also accepts a boolean to maintain backwards compatibility. <br> **Datatype:** Integer. <br> Default: `2`.
|
||||
| `save_backtest_models` | Save models to disk when running backtesting. Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when you wish to tune entry/exit parameters). Saving backtesting models to disk also allows to use the same model files for starting a dry/live instance with the same model `identifier`. <br> **Datatype:** Boolean. <br> Default: `False` (no models are saved).
|
||||
| `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)). <br> **Datatype:** Positive integer.
|
||||
| `continual_learning` | Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning (more information can be found [here](freqai-running.md#continual-learning)). Beware that this is currently a naive approach to incremental learning, and it has a high probability of overfitting/getting stuck in local minima while the market moves away from your model. We have the connections here primarily for experimental purposes and so that it is ready for more mature approaches to continual learning in chaotic systems like the crypto market. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
| `continual_learning` | Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning (more information can be found [here](freqai-running.md#continual-learning)). <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
| `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file. <br> **Datatype:** Boolean. <br> Default: `False`
|
||||
| `data_kitchen_thread_count` | <br> Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.). This has no impact on the number of threads used for training. If user does not set it (default), FreqAI will use max number of threads - 2 (leaving 1 physical core available for Freqtrade bot and FreqUI) <br> **Datatype:** Positive integer.
|
||||
| `activate_tensorboard` | <br> Indicate whether or not to activate tensorboard for the tensorboard enabled modules (currently Reinforcment Learning, XGBoost, Catboost, and PyTorch). Tensorboard needs Torch installed, which means you will need the torch/RL docker image or you need to answer "yes" to the install question about whether or not you wish to install Torch. <br> **Datatype:** Boolean. <br> Default: `True`.
|
||||
|
||||
### Feature parameters
|
||||
|
||||
@@ -42,11 +41,12 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
||||
| `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.
|
||||
| `svm_params` | All parameters available in Sklearn's `SGDOneClassSVM()`. See details about some select parameters [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Dictionary.
|
||||
| `use_DBSCAN_to_remove_outliers` | Cluster data using the DBSCAN algorithm to identify and remove outliers from training and prediction data. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan). <br> **Datatype:** Boolean.
|
||||
| `inlier_metric_window` | If set, FreqAI adds an `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`, i.e., the number of previous time points to compare the current candle to. Details of how the `inlier_metric` is computed can be found [here](freqai-feature-engineering.md#inlier-metric). <br> **Datatype:** Integer. <br> Default: `0`.
|
||||
| `noise_standard_deviation` | If set, FreqAI adds noise to the training features with the aim of preventing overfitting. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in FreqAI is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation). <br> **Datatype:** Integer. <br> Default: `0`.
|
||||
| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset. <br> **Datatype:** Float. <br> Default: `30`.
|
||||
| `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it. <br> **Datatype:** Boolean. <br> Default: `False` (no reversal).
|
||||
| `shuffle_after_split` | Split the data into train and test sets, and then shuffle both sets individually. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.<br> **Datatype:** Integer. <br> Default: `0`.
|
||||
| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.<br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
|
||||
### Data split parameters
|
||||
|
||||
@@ -85,28 +85,6 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
||||
| `net_arch` | Network architecture which is well described in [`stable_baselines3` doc](https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html#examples). In summary: `[<shared layers>, dict(vf=[<non-shared value network layers>], pi=[<non-shared policy network layers>])]`. By default this is set to `[128, 128]`, which defines 2 shared hidden layers with 128 units each.
|
||||
| `randomize_starting_position` | Randomize the starting point of each episode to avoid overfitting. <br> **Datatype:** bool. <br> Default: `False`.
|
||||
| `drop_ohlc_from_features` | Do not include the normalized ohlc data in the feature set passed to the agent during training (ohlc will still be used for driving the environment in all cases) <br> **Datatype:** Boolean. <br> **Default:** `False`
|
||||
| `progress_bar` | Display a progress bar with the current progress, elapsed time and estimated remaining time. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
|
||||
### PyTorch parameters
|
||||
|
||||
#### general
|
||||
|
||||
| Parameter | Description |
|
||||
|------------|-------------|
|
||||
| | **Model training parameters within the `freqai.model_training_parameters` sub dictionary**
|
||||
| `learning_rate` | Learning rate to be passed to the optimizer. <br> **Datatype:** float. <br> Default: `3e-4`.
|
||||
| `model_kwargs` | Parameters to be passed to the model class. <br> **Datatype:** dict. <br> Default: `{}`.
|
||||
| `trainer_kwargs` | Parameters to be passed to the trainer class. <br> **Datatype:** dict. <br> Default: `{}`.
|
||||
|
||||
#### trainer_kwargs
|
||||
|
||||
| Parameter | Description |
|
||||
|------------|-------------|
|
||||
| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary**
|
||||
| `max_iters` | The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. <br> **Datatype:** int. <br> Default: `100`.
|
||||
| `batch_size` | The size of the batches to use during training.. <br> **Datatype:** int. <br> Default: `64`.
|
||||
| `max_n_eval_batches` | The maximum number batches to use for evaluation.. <br> **Datatype:** int, optional. <br> Default: `None`.
|
||||
|
||||
|
||||
### Additional parameters
|
||||
|
||||
@@ -114,5 +92,5 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
||||
|------------|-------------|
|
||||
| | **Extraneous parameters**
|
||||
| `freqai.keras` | If the selected model makes use of Keras (typical for TensorFlow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
| `freqai.conv_width` | The width of a neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
|
||||
| `freqai.conv_width` | The width of a convolutional neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
|
||||
| `freqai.reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
|
||||
@@ -37,7 +37,7 @@ freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --con
|
||||
where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `feature_engineering_*` as a typical Regressor. The difference lies in the creation of the targets, Reinforcement Learning doesn't require them. However, FreqAI requires a default (neutral) value to be set in the action column:
|
||||
|
||||
```python
|
||||
def set_freqai_targets(self, dataframe, **kwargs) -> DataFrame:
|
||||
def set_freqai_targets(self, dataframe, **kwargs):
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
Required function to set the targets for the model.
|
||||
@@ -53,19 +53,17 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from
|
||||
# For RL, there are no direct targets to set. This is filler (neutral)
|
||||
# until the agent sends an action.
|
||||
dataframe["&-action"] = 0
|
||||
return dataframe
|
||||
```
|
||||
|
||||
Most of the function remains the same as for typical Regressors, however, the function below shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
|
||||
Most of the function remains the same as for typical Regressors, however, the function above shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
|
||||
|
||||
```python
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame:
|
||||
def feature_engineering_standard(self, dataframe, **kwargs):
|
||||
# The following features are necessary for RL models
|
||||
dataframe[f"%-raw_close"] = dataframe["close"]
|
||||
dataframe[f"%-raw_open"] = dataframe["open"]
|
||||
dataframe[f"%-raw_high"] = dataframe["high"]
|
||||
dataframe[f"%-raw_low"] = dataframe["low"]
|
||||
return dataframe
|
||||
```
|
||||
|
||||
Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action.
|
||||
@@ -135,104 +133,92 @@ Parameter details can be found [here](freqai-parameter-table.md), but in general
|
||||
|
||||
## Creating a custom reward function
|
||||
|
||||
!!! danger "Not for production"
|
||||
Warning!
|
||||
The reward function provided with the Freqtrade source code is a showcase of functionality designed to show/test as many possible environment control features as possible. It is also designed to run quickly on small computers. This is a benchmark, it is *not* for live production. Please beware that you will need to create your own custom_reward() function or use a template built by other users outside of the Freqtrade source code.
|
||||
|
||||
As you begin to modify the strategy and the prediction model, you will quickly realize some important differences between the Reinforcement Learner and the Regressors/Classifiers. Firstly, the strategy does not set a target value (no labels!). Instead, you set the `calculate_reward()` function inside the `MyRLEnv` class (see below). A default `calculate_reward()` is provided inside `prediction_models/ReinforcementLearner.py` to demonstrate the necessary building blocks for creating rewards, but this is *not* designed for production. Users *must* create their own custom reinforcement learning model class or use a pre-built one from outside the Freqtrade source code and save it to `user_data/freqaimodels`. It is inside the `calculate_reward()` where creative theories about the market can be expressed. For example, you can reward your agent when it makes a winning trade, and penalize the agent when it makes a losing trade. Or perhaps, you wish to reward the agent for entering trades, and penalize the agent for sitting in trades too long. Below we show examples of how these rewards are all calculated:
|
||||
|
||||
!!! note "Hint"
|
||||
The best reward functions are ones that are continuously differentiable, and well scaled. In other words, adding a single large negative penalty to a rare event is not a good idea, and the neural net will not be able to learn that function. Instead, it is better to add a small negative penalty to a common event. This will help the agent learn faster. Not only this, but you can help improve the continuity of your rewards/penalties by having them scale with severity according to some linear/exponential functions. In other words, you'd slowly scale the penalty as the duration of the trade increases. This is better than a single large penalty occuring at a single point in time.
|
||||
As you begin to modify the strategy and the prediction model, you will quickly realize some important differences between the Reinforcement Learner and the Regressors/Classifiers. Firstly, the strategy does not set a target value (no labels!). Instead, you set the `calculate_reward()` function inside the `MyRLEnv` class (see below). A default `calculate_reward()` is provided inside `prediction_models/ReinforcementLearner.py` to demonstrate the necessary building blocks for creating rewards, but users are encouraged to create their own custom reinforcement learning model class (see below) and save it to `user_data/freqaimodels`. It is inside the `calculate_reward()` where creative theories about the market can be expressed. For example, you can reward your agent when it makes a winning trade, and penalize the agent when it makes a losing trade. Or perhaps, you wish to reward the agent for entering trades, and penalize the agent for sitting in trades too long. Below we show examples of how these rewards are all calculated:
|
||||
|
||||
```python
|
||||
from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
|
||||
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions
|
||||
from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
|
||||
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions
|
||||
|
||||
|
||||
class MyCoolRLModel(ReinforcementLearner):
|
||||
"""
|
||||
User created RL prediction model.
|
||||
|
||||
Save this file to `freqtrade/user_data/freqaimodels`
|
||||
|
||||
then use it with:
|
||||
|
||||
freqtrade trade --freqaimodel MyCoolRLModel --config config.json --strategy SomeCoolStrat
|
||||
|
||||
Here the users can override any of the functions
|
||||
available in the `IFreqaiModel` inheritance tree. Most importantly for RL, this
|
||||
is where the user overrides `MyRLEnv` (see below), to define custom
|
||||
`calculate_reward()` function, or to override any other parts of the environment.
|
||||
|
||||
This class also allows users to override any other part of the IFreqaiModel tree.
|
||||
For example, the user can override `def fit()` or `def train()` or `def predict()`
|
||||
to take fine-tuned control over these processes.
|
||||
|
||||
Another common override may be `def data_cleaning_predict()` where the user can
|
||||
take fine-tuned control over the data handling pipeline.
|
||||
"""
|
||||
class MyRLEnv(Base5ActionRLEnv):
|
||||
class MyCoolRLModel(ReinforcementLearner):
|
||||
"""
|
||||
User made custom environment. This class inherits from BaseEnvironment and gym.env.
|
||||
Users can override any functions from those parent classes. Here is an example
|
||||
of a user customized `calculate_reward()` function.
|
||||
User created RL prediction model.
|
||||
|
||||
Warning!
|
||||
This is function is a showcase of functionality designed to show as many possible
|
||||
environment control features as possible. It is also designed to run quickly
|
||||
on small computers. This is a benchmark, it is *not* for live production.
|
||||
Save this file to `freqtrade/user_data/freqaimodels`
|
||||
|
||||
then use it with:
|
||||
|
||||
freqtrade trade --freqaimodel MyCoolRLModel --config config.json --strategy SomeCoolStrat
|
||||
|
||||
Here the users can override any of the functions
|
||||
available in the `IFreqaiModel` inheritance tree. Most importantly for RL, this
|
||||
is where the user overrides `MyRLEnv` (see below), to define custom
|
||||
`calculate_reward()` function, or to override any other parts of the environment.
|
||||
|
||||
This class also allows users to override any other part of the IFreqaiModel tree.
|
||||
For example, the user can override `def fit()` or `def train()` or `def predict()`
|
||||
to take fine-tuned control over these processes.
|
||||
|
||||
Another common override may be `def data_cleaning_predict()` where the user can
|
||||
take fine-tuned control over the data handling pipeline.
|
||||
"""
|
||||
def calculate_reward(self, action: int) -> float:
|
||||
# first, penalize if the action is not valid
|
||||
if not self._is_valid(action):
|
||||
return -2
|
||||
pnl = self.get_unrealized_profit()
|
||||
class MyRLEnv(Base5ActionRLEnv):
|
||||
"""
|
||||
User made custom environment. This class inherits from BaseEnvironment and gym.env.
|
||||
Users can override any functions from those parent classes. Here is an example
|
||||
of a user customized `calculate_reward()` function.
|
||||
"""
|
||||
def calculate_reward(self, action: int) -> float:
|
||||
# first, penalize if the action is not valid
|
||||
if not self._is_valid(action):
|
||||
return -2
|
||||
pnl = self.get_unrealized_profit()
|
||||
|
||||
factor = 100
|
||||
factor = 100
|
||||
|
||||
pair = self.pair.replace(':', '')
|
||||
pair = self.pair.replace(':', '')
|
||||
|
||||
# you can use feature values from dataframe
|
||||
# Assumes the shifted RSI indicator has been generated in the strategy.
|
||||
rsi_now = self.raw_features[f"%-rsi-period_10_shift-1_{pair}_"
|
||||
f"{self.config['timeframe']}"].iloc[self._current_tick]
|
||||
# you can use feature values from dataframe
|
||||
# Assumes the shifted RSI indicator has been generated in the strategy.
|
||||
rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{pair}_"
|
||||
f"{self.config['timeframe']}"].iloc[self._current_tick]
|
||||
|
||||
# reward agent for entering trades
|
||||
if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
|
||||
and self._position == Positions.Neutral):
|
||||
if rsi_now < 40:
|
||||
factor = 40 / rsi_now
|
||||
else:
|
||||
factor = 1
|
||||
return 25 * factor
|
||||
# reward agent for entering trades
|
||||
if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
|
||||
and self._position == Positions.Neutral):
|
||||
if rsi_now < 40:
|
||||
factor = 40 / rsi_now
|
||||
else:
|
||||
factor = 1
|
||||
return 25 * factor
|
||||
|
||||
# discourage agent from not entering trades
|
||||
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
||||
return -1
|
||||
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
||||
trade_duration = self._current_tick - self._last_trade_tick
|
||||
if trade_duration <= max_trade_duration:
|
||||
factor *= 1.5
|
||||
elif trade_duration > max_trade_duration:
|
||||
factor *= 0.5
|
||||
# discourage sitting in position
|
||||
if self._position in (Positions.Short, Positions.Long) and \
|
||||
action == Actions.Neutral.value:
|
||||
return -1 * trade_duration / max_trade_duration
|
||||
# close long
|
||||
if action == Actions.Long_exit.value and self._position == Positions.Long:
|
||||
if pnl > self.profit_aim * self.rr:
|
||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||
return float(pnl * factor)
|
||||
# close short
|
||||
if action == Actions.Short_exit.value and self._position == Positions.Short:
|
||||
if pnl > self.profit_aim * self.rr:
|
||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||
return float(pnl * factor)
|
||||
return 0.
|
||||
# discourage agent from not entering trades
|
||||
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
||||
return -1
|
||||
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
||||
trade_duration = self._current_tick - self._last_trade_tick
|
||||
if trade_duration <= max_trade_duration:
|
||||
factor *= 1.5
|
||||
elif trade_duration > max_trade_duration:
|
||||
factor *= 0.5
|
||||
# discourage sitting in position
|
||||
if self._position in (Positions.Short, Positions.Long) and \
|
||||
action == Actions.Neutral.value:
|
||||
return -1 * trade_duration / max_trade_duration
|
||||
# close long
|
||||
if action == Actions.Long_exit.value and self._position == Positions.Long:
|
||||
if pnl > self.profit_aim * self.rr:
|
||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||
return float(pnl * factor)
|
||||
# close short
|
||||
if action == Actions.Short_exit.value and self._position == Positions.Short:
|
||||
if pnl > self.profit_aim * self.rr:
|
||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||
return float(pnl * factor)
|
||||
return 0.
|
||||
```
|
||||
|
||||
## Using Tensorboard
|
||||
### Using Tensorboard
|
||||
|
||||
Reinforcement Learning models benefit from tracking training metrics. FreqAI has integrated Tensorboard to allow users to track training and evaluation performance across all coins and across all retrainings. Tensorboard is activated via the following command:
|
||||
|
||||
@@ -245,30 +231,32 @@ where `unique-id` is the `identifier` set in the `freqai` configuration file. Th
|
||||
|
||||

|
||||
|
||||
## Custom logging
|
||||
|
||||
### Custom logging
|
||||
|
||||
FreqAI also provides a built in episodic summary logger called `self.tensorboard_log` for adding custom information to the Tensorboard log. By default, this function is already called once per step inside the environment to record the agent actions. All values accumulated for all steps in a single episode are reported at the conclusion of each episode, followed by a full reset of all metrics to 0 in preparation for the subsequent episode.
|
||||
|
||||
|
||||
`self.tensorboard_log` can also be used anywhere inside the environment, for example, it can be added to the `calculate_reward` function to collect more detailed information about how often various parts of the reward were called:
|
||||
|
||||
```python
|
||||
class MyRLEnv(Base5ActionRLEnv):
|
||||
"""
|
||||
User made custom environment. This class inherits from BaseEnvironment and gym.env.
|
||||
Users can override any functions from those parent classes. Here is an example
|
||||
of a user customized `calculate_reward()` function.
|
||||
"""
|
||||
def calculate_reward(self, action: int) -> float:
|
||||
if not self._is_valid(action):
|
||||
self.tensorboard_log("invalid")
|
||||
return -2
|
||||
```py
|
||||
class MyRLEnv(Base5ActionRLEnv):
|
||||
"""
|
||||
User made custom environment. This class inherits from BaseEnvironment and gym.env.
|
||||
Users can override any functions from those parent classes. Here is an example
|
||||
of a user customized `calculate_reward()` function.
|
||||
"""
|
||||
def calculate_reward(self, action: int) -> float:
|
||||
if not self._is_valid(action):
|
||||
self.tensorboard_log("invalid")
|
||||
return -2
|
||||
|
||||
```
|
||||
|
||||
!!! Note
|
||||
The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)`. In this case the metric values are not incremented.
|
||||
|
||||
## Choosing a base environment
|
||||
### Choosing a base environment
|
||||
|
||||
FreqAI provides three base environments, `Base3ActionRLEnvironment`, `Base4ActionEnvironment` and `Base5ActionEnvironment`. As the names imply, the environments are customized for agents that can select from 3, 4 or 5 actions. The `Base3ActionEnvironment` is the simplest, the agent can select from hold, long, or short. This environment can also be used for long-only bots (it automatically follows the `can_short` flag from the strategy), where long is the enter condition and short is the exit condition. Meanwhile, in the `Base4ActionEnvironment`, the agent can enter long, enter short, hold neutral, or exit position. Finally, in the `Base5ActionEnvironment`, the agent has the same actions as Base4, but instead of a single exit action, it separates exit long and exit short. The main changes stemming from the environment selection include:
|
||||
|
||||
|
||||
+1
-18
@@ -128,12 +128,6 @@ The FreqAI specific parameter `label_period_candles` defines the offset (number
|
||||
|
||||
You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `False` which means that all new models are trained from scratch, without input from previous models.
|
||||
|
||||
???+ danger "Continual learning enforces a constant parameter space"
|
||||
Since `continual_learning` means that the model parameter space *cannot* change between trainings, `principal_component_analysis` is automatically disabled when `continual_learning` is enabled. Hint: PCA changes the parameter space and the number of features, learn more about PCA [here](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis).
|
||||
|
||||
???+ danger "Experimental functionality"
|
||||
Beware that this is currently a naive approach to incremental learning, and it has a high probability of overfitting/getting stuck in local minima while the market moves away from your model. We have the mechanics available in FreqAI primarily for experimental purposes and so that it is ready for more mature approaches to continual learning in chaotic systems like the crypto market.
|
||||
|
||||
## Hyperopt
|
||||
|
||||
You can hyperopt using the same command as for [typical Freqtrade hyperopt](hyperopt.md):
|
||||
@@ -161,14 +155,7 @@ This specific hyperopt would help you understand the appropriate `DI_values` for
|
||||
|
||||
## Using Tensorboard
|
||||
|
||||
!!! note "Availability"
|
||||
FreqAI includes tensorboard for a variety of models, including XGBoost, all PyTorch models, Reinforcement Learning, and Catboost. If you would like to see Tensorboard integrated into another model type, please open an issue on the [Freqtrade GitHub](https://github.com/freqtrade/freqtrade/issues)
|
||||
|
||||
!!! danger "Requirements"
|
||||
Tensorboard logging requires the FreqAI torch installation/docker image.
|
||||
|
||||
|
||||
The easiest way to use tensorboard is to ensure `freqai.activate_tensorboard` is set to `True` (default setting) in your configuration file, run FreqAI, then open a separate shell and run:
|
||||
CatBoost models benefit from tracking training metrics via Tensorboard. You can take advantage of the FreqAI integration to track training and evaluation performance across all coins and across all retrainings. Tensorboard is activated via the following command:
|
||||
|
||||
```bash
|
||||
cd freqtrade
|
||||
@@ -178,7 +165,3 @@ tensorboard --logdir user_data/models/unique-id
|
||||
where `unique-id` is the `identifier` set in the `freqai` configuration file. This command must be run in a separate shell if you wish to view the output in your browser at 127.0.0.1:6060 (6060 is the default port used by Tensorboard).
|
||||
|
||||

|
||||
|
||||
|
||||
!!! note "Deactivate for improved performance"
|
||||
Tensorboard logging can slow down training and should be deactivated for production use.
|
||||
|
||||
+7
-16
@@ -32,10 +32,7 @@ The easiest way to quickly test FreqAI is to run it in dry mode with the followi
|
||||
freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel LightGBMRegressor --strategy-path freqtrade/templates
|
||||
```
|
||||
|
||||
You will see the boot-up process of automatic data downloading, followed by simultaneous training and trading.
|
||||
|
||||
!!! danger "Not for production"
|
||||
The example strategy provided with the Freqtrade source code is designed for showcasing/testing a wide variety of FreqAI features. It is also designed to run on small computers so that it can be used as a benchmark between developers and users. It is *not* designed to be run in production.
|
||||
You will see the boot-up process of automatic data downloading, followed by simultaneous training and trading.
|
||||
|
||||
An example strategy, prediction model, and config to use as a starting points can be found in
|
||||
`freqtrade/templates/FreqaiExampleStrategy.py`, `freqtrade/freqai/prediction_models/LightGBMRegressor.py`, and
|
||||
@@ -72,14 +69,15 @@ pip install -r requirements-freqai.txt
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Catboost will not be installed on low-powered arm devices (raspberry), since it does not provide wheels for this platform.
|
||||
Catboost will not be installed on arm devices (raspberry, Mac M1, ARM based VPS, ...), since it does not provide wheels for this platform.
|
||||
|
||||
!!! Note "python 3.11"
|
||||
Some dependencies (Catboost, Torch) currently don't support python 3.11. Freqtrade therefore only supports python 3.10 for these models/dependencies.
|
||||
Tests involving these dependencies are skipped on 3.11.
|
||||
|
||||
### Usage with docker
|
||||
|
||||
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices. If you would like to use PyTorch or Reinforcement learning, you should use the torch or RL tags, `image: freqtradeorg/freqtrade:develop_freqaitorch`, `image: freqtradeorg/freqtrade:develop_freqairl`.
|
||||
|
||||
!!! note "docker-compose-freqai.yml"
|
||||
We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file. This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available.
|
||||
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices.
|
||||
|
||||
### FreqAI position in open-source machine learning landscape
|
||||
|
||||
@@ -107,13 +105,6 @@ This is for performance reasons - FreqAI relies on making quick predictions/retr
|
||||
it needs to download all the training data at the beginning of a dry/live instance. FreqAI stores and appends
|
||||
new candles automatically for future retrains. This means that if new pairs arrive later in the dry run due to a volume pairlist, it will not have the data ready. However, FreqAI does work with the `ShufflePairlist` or a `VolumePairlist` which keeps the total pairlist constant (but reorders the pairs according to volume).
|
||||
|
||||
## Additional learning materials
|
||||
|
||||
Here we compile some external materials that provide deeper looks into various components of FreqAI:
|
||||
|
||||
- [Real-time head-to-head: Adaptive modeling of financial market data using XGBoost and CatBoost](https://emergentmethods.medium.com/real-time-head-to-head-adaptive-modeling-of-financial-market-data-using-xgboost-and-catboost-995a115a7495)
|
||||
- [FreqAI - from price to prediction](https://emergentmethods.medium.com/freqai-from-price-to-prediction-6fadac18b665)
|
||||
|
||||
## Credits
|
||||
|
||||
FreqAI is developed by a group of individuals who all contribute specific skillsets to the project.
|
||||
|
||||
@@ -184,8 +184,6 @@ The RemotePairList is defined in the pairlists section of the configuration sett
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "RemotePairList",
|
||||
"mode": "whitelist",
|
||||
"processing_mode": "filter",
|
||||
"pairlist_url": "https://example.com/pairlist",
|
||||
"number_assets": 10,
|
||||
"refresh_period": 1800,
|
||||
@@ -196,14 +194,6 @@ The RemotePairList is defined in the pairlists section of the configuration sett
|
||||
]
|
||||
```
|
||||
|
||||
The optional `mode` option specifies if the pairlist should be used as a `blacklist` or as a `whitelist`. The default value is "whitelist".
|
||||
|
||||
The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append".
|
||||
|
||||
In "filter" mode, the retrieved pairlist is used as a filter. Only the pairs present in both the original pairlist and the retrieved pairlist are included in the final pairlist. Other pairs are filtered out.
|
||||
|
||||
In "append" mode, the retrieved pairlist is added to the original pairlist. All pairs from both lists are included in the final pairlist without any filtering.
|
||||
|
||||
The `pairlist_url` option specifies the URL of the remote server where the pairlist is located, or the path to a local file (if file:/// is prepended). This allows the user to use either a remote server or a local file as the source for the pairlist.
|
||||
|
||||
The user is responsible for providing a server or local file that returns a JSON object with the following structure:
|
||||
@@ -211,7 +201,7 @@ The user is responsible for providing a server or local file that returns a JSON
|
||||
```json
|
||||
{
|
||||
"pairs": ["XRP/USDT", "ETH/USDT", "LTC/USDT"],
|
||||
"refresh_period": 1800
|
||||
"refresh_period": 1800,
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ The below example assumes a timeframe of 1 hour:
|
||||
* Locks each pair after selling for an additional 5 candles (`CooldownPeriod`), giving other pairs a chance to get filled.
|
||||
* Stops trading for 4 hours (`4 * 1h candles`) if the last 2 days (`48 * 1h candles`) had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`).
|
||||
* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (`24 * 1h candles`) limit (`StoplossGuard`).
|
||||
* Locks all pairs that had 2 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`).
|
||||
* Locks all pairs that had 4 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`).
|
||||
* Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades.
|
||||
|
||||
``` python
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
## Highlighted changes
|
||||
|
||||
- ...
|
||||
|
||||
### How to update
|
||||
|
||||
As always, you can update your bot using one of the following commands:
|
||||
|
||||
#### docker-compose
|
||||
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
#### Installation via setup script
|
||||
|
||||
```
|
||||
# Deactivate venv and run
|
||||
./setup.sh --update
|
||||
```
|
||||
|
||||
#### Plain native installation
|
||||
|
||||
```
|
||||
git pull
|
||||
pip install -U -r requirements.txt
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Expand full changelog</summary>
|
||||
|
||||
```
|
||||
<Paste your changelog here>
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -1,11 +0,0 @@
|
||||
This section will highlight a few projects from members of the community.
|
||||
!!! Note
|
||||
The projects below are for the most part not maintained by the freqtrade , therefore use your own caution before using them.
|
||||
|
||||
- [Example freqtrade strategies](https://github.com/freqtrade/freqtrade-strategies/)
|
||||
- [FrequentHippo - Grafana dashboard with dry/live runs and backtests](http://frequenthippo.ddns.net:3000/) (by hippocritical).
|
||||
- [Online pairlist generator](https://remotepairlist.com/) (by Blood4rc).
|
||||
- [Freqtrade Backtesting Project](https://bt.robot.co.network/) (by Blood4rc).
|
||||
- [Freqtrade analysis notebook](https://github.com/froggleston/freqtrade_analysis_notebook) (by Froggleston).
|
||||
- [TUI for freqtrade](https://github.com/froggleston/freqtrade-frogtrade9000) (by Froggleston).
|
||||
- [Bot Academy](https://botacademy.ddns.net/) (by stash86) - Blog about crypto bot projects.
|
||||
@@ -63,10 +63,6 @@ Exchanges confirmed working by the community:
|
||||
- [X] [Bitvavo](https://bitvavo.com/)
|
||||
- [X] [Kucoin](https://www.kucoin.com/)
|
||||
|
||||
## Community showcase
|
||||
|
||||
--8<-- "includes/showcase.md"
|
||||
|
||||
## Requirements
|
||||
|
||||
### Hardware requirements
|
||||
|
||||
@@ -30,6 +30,12 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
|
||||
!!! Warning "Up-to-date clock"
|
||||
The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges.
|
||||
|
||||
!!! Error "Running setup.py install for gym did not run successfully."
|
||||
If you get an error related with gym we suggest you to downgrade setuptools it to version 65.5.0 you can do it with the following command:
|
||||
```bash
|
||||
pip install setuptools==65.5.0
|
||||
```
|
||||
|
||||
------
|
||||
|
||||
## Requirements
|
||||
@@ -46,7 +52,7 @@ These requirements apply to both [Script Installation](#script-installation) and
|
||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
||||
* [TA-Lib](https://ta-lib.github.io/ta-lib-python/) (install instructions [below](#install-ta-lib))
|
||||
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions [below](#install-ta-lib))
|
||||
|
||||
### Install code
|
||||
|
||||
@@ -204,7 +210,7 @@ sudo ./build_helpers/install_ta-lib.sh
|
||||
|
||||
##### TA-Lib manual installation
|
||||
|
||||
[Official installation guide](https://ta-lib.github.io/ta-lib-python/install.html)
|
||||
Official webpage: https://mrjbq7.github.io/ta-lib/install.html
|
||||
|
||||
```bash
|
||||
wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
||||
@@ -236,7 +242,6 @@ source .env/bin/activate
|
||||
|
||||
```bash
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install -r requirements.txt
|
||||
python3 -m pip install -e .
|
||||
```
|
||||
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
# Lookahead analysis
|
||||
|
||||
This page explains how to validate your strategy in terms of look ahead bias.
|
||||
|
||||
Checking look ahead bias is the bane of any strategy since it is sometimes very easy to introduce backtest bias -
|
||||
but very hard to detect.
|
||||
|
||||
Backtesting initializes all timestamps at once and calculates all indicators in the beginning.
|
||||
This means that if your indicators or entry/exit signals could look into future candles and falsify your backtest.
|
||||
|
||||
Lookahead-analysis requires historic data to be available.
|
||||
To learn how to get data for the pairs and exchange you're interested in,
|
||||
head over to the [Data Downloading](data-download.md) section of the documentation.
|
||||
|
||||
This command is built upon backtesting since it internally chains backtests and pokes at the strategy to provoke it to show look ahead bias.
|
||||
This is done by not looking at the strategy itself - but at the results it returned.
|
||||
The results are things like changed indicator-values and moved entries/exits compared to the full backtest.
|
||||
|
||||
You can use commands of [Backtesting](backtesting.md).
|
||||
It also supports the lookahead-analysis of freqai strategies.
|
||||
|
||||
- `--cache` is forced to "none".
|
||||
- `--max-open-trades` is forced to be at least equal to the number of pairs.
|
||||
- `--dry-run-wallet` is forced to be basically infinite.
|
||||
|
||||
## Lookahead-analysis command reference
|
||||
|
||||
```
|
||||
usage: freqtrade lookahead-analysis [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[-d PATH] [--userdir PATH] [-s NAME]
|
||||
[--strategy-path PATH]
|
||||
[--recursive-strategy-search]
|
||||
[--freqaimodel NAME]
|
||||
[--freqaimodel-path PATH] [-i TIMEFRAME]
|
||||
[--timerange TIMERANGE]
|
||||
[--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
|
||||
[--max-open-trades INT]
|
||||
[--stake-amount STAKE_AMOUNT]
|
||||
[--fee FLOAT] [-p PAIRS [PAIRS ...]]
|
||||
[--enable-protections]
|
||||
[--dry-run-wallet DRY_RUN_WALLET]
|
||||
[--timeframe-detail TIMEFRAME_DETAIL]
|
||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||
[--export {none,trades,signals}]
|
||||
[--export-filename PATH]
|
||||
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||
[--cache {none,day,week,month}]
|
||||
[--freqai-backtest-live-models]
|
||||
[--minimum-trade-amount INT]
|
||||
[--targeted-trade-amount INT]
|
||||
[--lookahead-analysis-exportfilename LOOKAHEAD_ANALYSIS_EXPORTFILENAME]
|
||||
|
||||
options:
|
||||
--minimum-trade-amount INT
|
||||
Minimum trade amount for lookahead-analysis
|
||||
--targeted-trade-amount INT
|
||||
Targeted trade amount for lookahead analysis
|
||||
--lookahead-analysis-exportfilename LOOKAHEAD_ANALYSIS_EXPORTFILENAME
|
||||
Use this csv-filename to store lookahead-analysis-
|
||||
results
|
||||
```
|
||||
|
||||
!!! Note ""
|
||||
The above Output was reduced to options `lookahead-analysis` adds on top of regular backtesting commands.
|
||||
|
||||
### Summary
|
||||
|
||||
Checks a given strategy for look ahead bias via lookahead-analysis
|
||||
Look ahead bias means that the backtest uses data from future candles thereby not making it viable beyond backtesting
|
||||
and producing false hopes for the one backtesting.
|
||||
|
||||
### Introduction
|
||||
|
||||
Many strategies - without the programmer knowing - have fallen prey to look ahead bias.
|
||||
|
||||
Any backtest will populate the full dataframe including all time stamps at the beginning.
|
||||
If the programmer is not careful or oblivious how things work internally
|
||||
(which sometimes can be really hard to find out) then it will just look into the future making the strategy amazing
|
||||
but not realistic.
|
||||
|
||||
This command is made to try to verify the validity in the form of the aforementioned look ahead bias.
|
||||
|
||||
### How does the command work?
|
||||
|
||||
It will start with a backtest of all pairs to generate a baseline for indicators and entries/exits.
|
||||
After the backtest ran, it will look if the `minimum-trade-amount` is met
|
||||
and if not cancel the lookahead-analysis for this strategy.
|
||||
|
||||
After setting the baseline it will then do additional runs for every entry and exit separately.
|
||||
When a verification-backtest is done, it will compare the indicators as the signal (either entry or exit) and report the bias.
|
||||
After all signals have been verified or falsified a result-table will be generated for the user to see.
|
||||
|
||||
### Caveats
|
||||
|
||||
- `lookahead-analysis` can only verify / falsify the trades it calculated and verified.
|
||||
If the strategy has many different signals / signal types, it's up to you to select appropriate parameters to ensure that all signals have triggered at least once. Not triggered signals will not have been verified.
|
||||
This could lead to a false-negative (the strategy will then be reported as non-biased).
|
||||
- `lookahead-analysis` has access to everything that backtesting has too.
|
||||
Please don't provoke any configs like enabling position stacking.
|
||||
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` amount and neither leftover money in your wallet.
|
||||
@@ -42,14 +42,14 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect
|
||||
| `producers` | **Required.** List of producers <br> **Datatype:** Array.
|
||||
| `producers.name` | **Required.** Name of this producer. This name must be used in calls to `get_producer_pairs()` and `get_producer_df()` if more than one producer is used.<br> **Datatype:** string
|
||||
| `producers.host` | **Required.** The hostname or IP address from your producer.<br> **Datatype:** string
|
||||
| `producers.port` | **Required.** The port matching the above host.<br>*Defaults to `8080`.*<br> **Datatype:** Integer
|
||||
| `producers.port` | **Required.** The port matching the above host.<br> **Datatype:** string
|
||||
| `producers.secure` | **Optional.** Use ssl in websockets connection. Default False.<br> **Datatype:** string
|
||||
| `producers.ws_token` | **Required.** `ws_token` as configured on the producer.<br> **Datatype:** string
|
||||
| | **Optional settings**
|
||||
| `wait_timeout` | Timeout until we ping again if no message is received. <br>*Defaults to `300`.*<br> **Datatype:** Integer - in seconds.
|
||||
| `ping_timeout` | Ping timeout <br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
||||
| `wait_timeout` | Ping timeout <br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
||||
| `sleep_time` | Sleep time before retrying to connect.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
||||
| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.<br>*Defaults to `false`.*<br> **Datatype:** Boolean.
|
||||
| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
||||
| `message_size_limit` | Size limit per message<br>*Defaults to `8`.*<br> **Datatype:** Integer - Megabytes.
|
||||
|
||||
Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
markdown==3.3.7
|
||||
mkdocs==1.4.3
|
||||
mkdocs-material==9.1.19
|
||||
mkdocs==1.4.2
|
||||
mkdocs-material==9.1.3
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.1
|
||||
pymdown-extensions==9.10
|
||||
jinja2==3.1.2
|
||||
|
||||
+4
-3
@@ -9,6 +9,9 @@ This same command can also be used to update freqUI, should there be a new relea
|
||||
|
||||
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`).
|
||||
|
||||
!!! info "Alpha release"
|
||||
FreqUI is still considered an alpha release - if you encounter bugs or inconsistencies please open a [FreqUI issue](https://github.com/freqtrade/frequi/issues/new/choose).
|
||||
|
||||
!!! 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.
|
||||
|
||||
@@ -134,9 +137,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
|
||||
| `reload_config` | Reloads the configuration file.
|
||||
| `trades` | List last trades. Limited to 500 trades per call.
|
||||
| `trade/<tradeid>` | Get specific trade.
|
||||
| `trade/<tradeid>` | DELETE - Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
||||
| `trade/<tradeid>/open-order` | DELETE - Cancel open order for this trade.
|
||||
| `trade/<tradeid>/reload` | GET - Reload a trade from the Exchange. Only works in live, and can potentially help recover a trade that was manually sold on the exchange.
|
||||
| `delete_trade <trade_id>` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
||||
| `show_config` | Shows part of the current configuration with relevant settings to operation.
|
||||
| `logs` | Shows last log messages.
|
||||
| `status` | Lists all open trades.
|
||||
|
||||
+9
-16
@@ -23,22 +23,10 @@ These modes can be configured with these values:
|
||||
'stoploss_on_exchange_limit_ratio': 0.99
|
||||
```
|
||||
|
||||
Stoploss on exchange is only supported for the following exchanges, and not all exchanges support both stop-limit and stop-market.
|
||||
The Order-type will be ignored if only one mode is available.
|
||||
|
||||
| Exchange | stop-loss type |
|
||||
|----------|-------------|
|
||||
| Binance | limit |
|
||||
| Binance Futures | market, limit |
|
||||
| Huobi | limit |
|
||||
| kraken | market, limit |
|
||||
| Gate | limit |
|
||||
| Okx | limit |
|
||||
| Kucoin | stop-limit, stop-market|
|
||||
|
||||
!!! Note "Tight stoploss"
|
||||
<ins>Do not set too low/tight stoploss value when using stop loss on exchange!</ins>
|
||||
If set to low/tight you will have greater risk of missing fill on the order and stoploss will not work.
|
||||
!!! Note
|
||||
Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), Gate (stop-limit), and Kucoin (stop-limit and stop-market) as of now.
|
||||
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
||||
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
|
||||
|
||||
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
|
||||
|
||||
@@ -209,6 +197,11 @@ You can also keep a static stoploss until the offset is reached, and then trail
|
||||
If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`.
|
||||
This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset.
|
||||
|
||||
``` python
|
||||
trailing_stop_positive_offset = 0.011
|
||||
trailing_only_offset_is_reached = True
|
||||
```
|
||||
|
||||
Configuration (offset is buy-price + 3%):
|
||||
|
||||
``` python
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Advanced Strategies
|
||||
|
||||
This page explains some advanced concepts available for strategies.
|
||||
If you're just getting started, please familiarize yourself with the [Freqtrade basics](bot-basics.md) and methods described in [Strategy Customization](strategy-customization.md) first.
|
||||
If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation and with the [Freqtrade basics](bot-basics.md) first.
|
||||
|
||||
The call sequence of the methods described here is covered under [bot execution logic](bot-basics.md#bot-execution-logic). Those docs are also helpful in deciding which method is most suitable for your customisation needs.
|
||||
[Freqtrade basics](bot-basics.md) describes in which sequence each method described below is called, which can be helpful to understand which method to use for your custom needs.
|
||||
|
||||
!!! Note
|
||||
Callback methods should *only* be implemented if a strategy uses them.
|
||||
All callback methods described below should only be implemented in a strategy if they are actually used.
|
||||
|
||||
!!! Tip
|
||||
Start off with a strategy template containing all available callback methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced`
|
||||
You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced`
|
||||
|
||||
## Storing information
|
||||
|
||||
Storing information can be accomplished by creating a new dictionary within the strategy class.
|
||||
|
||||
The name of the variable can be chosen at will, but should be prefixed with `custom_` to avoid naming collisions with predefined strategy variables.
|
||||
The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
|
||||
|
||||
```python
|
||||
class AwesomeStrategy(IStrategy):
|
||||
@@ -227,8 +227,8 @@ for val in self.buy_ema_short.range:
|
||||
f'ema_short_{val}': ta.EMA(dataframe, timeperiod=val)
|
||||
}))
|
||||
|
||||
# Combine all dataframes, and reassign the original dataframe column
|
||||
dataframe = pd.concat(frames, axis=1)
|
||||
# Append columns to existing dataframe
|
||||
merged_frame = 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.
|
||||
|
||||
@@ -43,7 +43,7 @@ class AwesomeStrategy(IStrategy):
|
||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||
# Assign this to the class by using self.*
|
||||
# can then be used by populate_* methods
|
||||
self.custom_remote_data = requests.get('https://some_remote_source.example.com')
|
||||
self.cust_remote_data = requests.get('https://some_remote_source.example.com')
|
||||
|
||||
```
|
||||
|
||||
@@ -51,8 +51,7 @@ During hyperopt, this runs only once at startup.
|
||||
|
||||
## Bot loop start
|
||||
|
||||
A simple callback which is called once at the start of every bot throttling iteration in dry/live mode (roughly every 5
|
||||
seconds, unless configured differently) or once per candle in backtest/hyperopt mode.
|
||||
A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently).
|
||||
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
|
||||
|
||||
``` python
|
||||
@@ -62,12 +61,11 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
|
||||
def bot_loop_start(self, **kwargs) -> None:
|
||||
"""
|
||||
Called at the start of the bot iteration (one loop).
|
||||
Might be used to perform pair-independent tasks
|
||||
(e.g. gather some remote resource for comparison)
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
"""
|
||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||
@@ -352,7 +350,7 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
# Convert absolute price to percentage relative to current_rate
|
||||
if stoploss_price < current_rate:
|
||||
return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short)
|
||||
return (stoploss_price / current_rate) - 1
|
||||
|
||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||
return 1
|
||||
@@ -750,7 +748,7 @@ class DigDeeperStrategy(IStrategy):
|
||||
# Hope you have a deep wallet!
|
||||
try:
|
||||
# This returns first order stake size
|
||||
stake_amount = filled_entries[0].stake_amount
|
||||
stake_amount = filled_entries[0].cost
|
||||
# This then calculates current safety order size
|
||||
stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
|
||||
return stake_amount
|
||||
|
||||
@@ -342,12 +342,16 @@ The above configuration would therefore mean:
|
||||
|
||||
The calculation does include fees.
|
||||
|
||||
To disable ROI completely, set it to an empty dictionary:
|
||||
To disable ROI completely, set it to an insanely high number:
|
||||
|
||||
```python
|
||||
minimal_roi = {}
|
||||
minimal_roi = {
|
||||
"0": 100
|
||||
}
|
||||
```
|
||||
|
||||
While technically not completely disabled, this would exit once the trade reaches 10000% Profit.
|
||||
|
||||
To use times based on candle duration (timeframe), the following snippet can be handy.
|
||||
This will allow you to change the timeframe for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...)
|
||||
|
||||
|
||||
@@ -578,7 +578,7 @@ def populate_any_indicators(
|
||||
Features will now expand automatically. As such, the expansion loops, as well as the `{pair}` / `{timeframe}` parts will need to be removed.
|
||||
|
||||
``` python linenums="1"
|
||||
def feature_engineering_expand_all(self, dataframe, period, **kwargs) -> DataFrame::
|
||||
def feature_engineering_expand_all(self, dataframe, period, **kwargs):
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
This function will automatically expand the defined features on the config defined
|
||||
@@ -638,7 +638,7 @@ Features will now expand automatically. As such, the expansion loops, as well as
|
||||
Basic features. Make sure to remove the `{pair}` part from your features.
|
||||
|
||||
``` python linenums="1"
|
||||
def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame::
|
||||
def feature_engineering_expand_basic(self, dataframe, **kwargs):
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
This function will automatically expand the defined features on the config defined
|
||||
@@ -673,7 +673,7 @@ Basic features. Make sure to remove the `{pair}` part from your features.
|
||||
### FreqAI - feature engineering standard
|
||||
|
||||
``` python linenums="1"
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame:
|
||||
def feature_engineering_standard(self, dataframe, **kwargs):
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
This optional function will be called once with the dataframe of the base timeframe.
|
||||
@@ -704,7 +704,7 @@ Basic features. Make sure to remove the `{pair}` part from your features.
|
||||
Targets now get their own, dedicated method.
|
||||
|
||||
``` python linenums="1"
|
||||
def set_freqai_targets(self, dataframe: DataFrame, **kwargs) -> DataFrame:
|
||||
def set_freqai_targets(self, dataframe, **kwargs):
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
Required function to set the targets for the model.
|
||||
@@ -728,86 +728,3 @@ Targets now get their own, dedicated method.
|
||||
|
||||
return dataframe
|
||||
```
|
||||
|
||||
|
||||
### FreqAI - New data Pipeline
|
||||
|
||||
If you have created your own custom `IFreqaiModel` with a custom `train()`/`predict()` function, *and* you still rely on `data_cleaning_train/predict()`, then you will need to migrate to the new pipeline. If your model does *not* rely on `data_cleaning_train/predict()`, then you do not need to worry about this migration. That means that this migration guide is relevant for a very small percentage of power-users. If you stumbled upon this guide by mistake, feel free to inquire in depth about your problem in the Freqtrade discord server.
|
||||
|
||||
The conversion involves first removing `data_cleaning_train/predict()` and replacing them with a `define_data_pipeline()` and `define_label_pipeline()` function to your `IFreqaiModel` class:
|
||||
|
||||
```python linenums="1" hl_lines="11-14 47-49 55-57"
|
||||
class MyCoolFreqaiModel(BaseRegressionModel):
|
||||
"""
|
||||
Some cool custom IFreqaiModel you made before Freqtrade version 2023.6
|
||||
"""
|
||||
def train(
|
||||
self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs
|
||||
) -> Any:
|
||||
|
||||
# ... your custom stuff
|
||||
|
||||
# Remove these lines
|
||||
# data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered)
|
||||
# self.data_cleaning_train(dk)
|
||||
# data_dictionary = dk.normalize_data(data_dictionary)
|
||||
# (1)
|
||||
|
||||
# Add these lines. Now we control the pipeline fit/transform ourselves
|
||||
dd = dk.make_train_test_datasets(features_filtered, labels_filtered)
|
||||
dk.feature_pipeline = self.define_data_pipeline(threads=dk.thread_count)
|
||||
dk.label_pipeline = self.define_label_pipeline(threads=dk.thread_count)
|
||||
|
||||
(dd["train_features"],
|
||||
dd["train_labels"],
|
||||
dd["train_weights"]) = dk.feature_pipeline.fit_transform(dd["train_features"],
|
||||
dd["train_labels"],
|
||||
dd["train_weights"])
|
||||
|
||||
(dd["test_features"],
|
||||
dd["test_labels"],
|
||||
dd["test_weights"]) = dk.feature_pipeline.transform(dd["test_features"],
|
||||
dd["test_labels"],
|
||||
dd["test_weights"])
|
||||
|
||||
dd["train_labels"], _, _ = dk.label_pipeline.fit_transform(dd["train_labels"])
|
||||
dd["test_labels"], _, _ = dk.label_pipeline.transform(dd["test_labels"])
|
||||
|
||||
# ... your custom code
|
||||
|
||||
return model
|
||||
|
||||
def predict(
|
||||
self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs
|
||||
) -> Tuple[DataFrame, npt.NDArray[np.int_]]:
|
||||
|
||||
# ... your custom stuff
|
||||
|
||||
# Remove these lines:
|
||||
# self.data_cleaning_predict(dk)
|
||||
# (2)
|
||||
|
||||
# Add these lines:
|
||||
dk.data_dictionary["prediction_features"], outliers, _ = dk.feature_pipeline.transform(
|
||||
dk.data_dictionary["prediction_features"], outlier_check=True)
|
||||
|
||||
# Remove this line
|
||||
# pred_df = dk.denormalize_labels_from_metadata(pred_df)
|
||||
# (3)
|
||||
|
||||
# Replace with these lines
|
||||
pred_df, _, _ = dk.label_pipeline.inverse_transform(pred_df)
|
||||
if self.freqai_info.get("DI_threshold", 0) > 0:
|
||||
dk.DI_values = dk.feature_pipeline["di"].di_values
|
||||
else:
|
||||
dk.DI_values = np.zeros(outliers.shape[0])
|
||||
dk.do_predict = outliers
|
||||
|
||||
# ... your custom code
|
||||
return (pred_df, dk.do_predict)
|
||||
```
|
||||
|
||||
|
||||
1. Data normalization and cleaning is now homogenized with the new pipeline definition. This is created in the new `define_data_pipeline()` and `define_label_pipeline()` functions. The `data_cleaning_train()` and `data_cleaning_predict()` functions are no longer used. You can override `define_data_pipeline()` to create your own custom pipeline if you wish.
|
||||
2. Data normalization and cleaning is now homogenized with the new pipeline definition. This is created in the new `define_data_pipeline()` and `define_label_pipeline()` functions. The `data_cleaning_train()` and `data_cleaning_predict()` functions are no longer used. You can override `define_data_pipeline()` to create your own custom pipeline if you wish.
|
||||
3. Data denormalization is done with the new pipeline. Replace this with the lines below.
|
||||
|
||||
+2
-10
@@ -187,13 +187,11 @@ official commands. You can ask at any moment for help with `/help`.
|
||||
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True)
|
||||
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True)
|
||||
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
||||
| `/reload_trade <trade_id>` | Reload a trade from the Exchange. Only works in live, and can potentially help recover a trade that was manually sold on the exchange.
|
||||
| `/cancel_open_order <trade_id> | /coo <trade_id>` | Cancel an open order for a trade.
|
||||
| **Metrics** |
|
||||
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
||||
| `/performance` | Show performance of each finished trade grouped by pair
|
||||
| `/balance` | Show bot managed balance per currency
|
||||
| `/balance full` | Show account balance per currency
|
||||
| `/balance` | Show account balance per currency
|
||||
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
|
||||
| `/weekly <n>` | Shows profit or loss per week, over the last n weeks (n defaults to 8)
|
||||
| `/monthly <n>` | Shows profit or loss per month, over the last n months (n defaults to 6)
|
||||
@@ -204,6 +202,7 @@ official commands. You can ask at any moment for help with `/help`.
|
||||
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||
| `/edge` | Show validated pairs by Edge if it is enabled.
|
||||
|
||||
|
||||
## Telegram commands in action
|
||||
|
||||
Below, example of Telegram message you will receive for each command.
|
||||
@@ -280,26 +279,19 @@ Return a summary of your profit/loss and performance.
|
||||
> ∙ `33.095 EUR`
|
||||
>
|
||||
> **Total Trade Count:** `138`
|
||||
> **Bot started:** `2022-07-11 18:40:44`
|
||||
> **First Trade opened:** `3 days ago`
|
||||
> **Latest Trade opened:** `2 minutes ago`
|
||||
> **Avg. Duration:** `2:33:45`
|
||||
> **Best Performing:** `PAY/BTC: 50.23%`
|
||||
> **Trading volume:** `0.5 BTC`
|
||||
> **Profit factor:** `1.04`
|
||||
> **Win / Loss:** `102 / 36`
|
||||
> **Winrate:** `73.91%`
|
||||
> **Expectancy (Ratio):** `4.87 (1.66)`
|
||||
> **Max Drawdown:** `9.23% (0.01255 BTC)`
|
||||
|
||||
The relative profit of `1.2%` is the average profit per trade.
|
||||
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
|
||||
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
||||
Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
|
||||
Expectancy corresponds to the average return per currency unit at risk, i.e. the winrate and the risk-reward ratio (the average gain of winning trades compared to the average loss of losing trades).
|
||||
Expectancy Ratio is expected profit or loss of a subsequent trade based on the performance of all past trades.
|
||||
Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
||||
Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date.
|
||||
|
||||
### /forceexit <trade_id>
|
||||
|
||||
|
||||
@@ -141,8 +141,7 @@ Most properties here can be None as they are dependant on the exchange response.
|
||||
`amount` | float | Amount in base currency
|
||||
`filled` | float | Filled amount (in base currency)
|
||||
`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.*)
|
||||
`stake_amount` | float | Stake amount used for this order. *Added in 2023.7.*
|
||||
`cost` | float | Cost of the order - usually average * filled
|
||||
`order_date` | datetime | Order creation date **use `order_date_utc` instead**
|
||||
`order_date_utc` | datetime | Order creation date (in UTC)
|
||||
`order_fill_date` | datetime | Order fill date **use `order_fill_utc` instead**
|
||||
|
||||
+6
-17
@@ -723,9 +723,6 @@ usage: freqtrade backtesting-analysis [-h] [-v] [--logfile FILE] [-V]
|
||||
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
|
||||
[--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]]
|
||||
[--timerange YYYYMMDD-[YYYYMMDD]]
|
||||
[--rejected]
|
||||
[--analysis-to-csv]
|
||||
[--analysis-csv-path PATH]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
@@ -739,27 +736,19 @@ optional arguments:
|
||||
pair and enter_tag, 4: by pair, enter_ and exit_tag
|
||||
(this can get quite large)
|
||||
--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]
|
||||
Space separated list of entry signals to analyse.
|
||||
Default: all. e.g. 'entry_tag_a entry_tag_b'
|
||||
Comma separated list of entry signals to analyse.
|
||||
Default: all. e.g. 'entry_tag_a,entry_tag_b'
|
||||
--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]
|
||||
Space separated list of exit signals to analyse.
|
||||
Comma separated list of exit signals to analyse.
|
||||
Default: all. e.g.
|
||||
'exit_tag_a roi stop_loss trailing_stop_loss'
|
||||
'exit_tag_a,roi,stop_loss,trailing_stop_loss'
|
||||
--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]
|
||||
Space separated list of indicators to analyse. e.g.
|
||||
'close rsi bb_lowerband profit_abs'
|
||||
Comma separated list of indicators to analyse. e.g.
|
||||
'close,rsi,bb_lowerband,profit_abs'
|
||||
--timerange YYYYMMDD-[YYYYMMDD]
|
||||
Timerange to filter trades for analysis,
|
||||
start inclusive, end exclusive. e.g.
|
||||
20220101-20220201
|
||||
--rejected
|
||||
Print out rejected trades table
|
||||
--analysis-to-csv
|
||||
Write out tables to individual CSVs, by default to
|
||||
'user_data/backtest_results' unless '--analysis-csv-path' is given.
|
||||
--analysis-csv-path [PATH]
|
||||
Optional path where individual CSVs will be written. If not used,
|
||||
CSVs will be written to 'user_data/backtest_results'.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
||||
@@ -80,18 +80,12 @@ When using the Form-Encoded or JSON-Encoded configuration you can configure any
|
||||
|
||||
The result would be a POST request with e.g. `Status: running` body and `Content-Type: text/plain` header.
|
||||
|
||||
## Additional configurations
|
||||
|
||||
The `webhook.retries` parameter can be set for the maximum number of retries the webhook request should attempt if it is unsuccessful (i.e. HTTP response status is not 200). By default this is set to `0` which is disabled. An additional `webhook.retry_delay` parameter can be set to specify the time in seconds between retry attempts. By default this is set to `0.1` (i.e. 100ms). Note that increasing the number of retries or retry delay may slow down the trader if there are connectivity issues with the webhook.
|
||||
You can also specify `webhook.timeout` - which defines how long the bot will wait until it assumes the other host as unresponsive (defaults to 10s).
|
||||
|
||||
Example configuration for retries:
|
||||
Optional parameters are available to enable automatic retries for webhook messages. The `webhook.retries` parameter can be set for the maximum number of retries the webhook request should attempt if it is unsuccessful (i.e. HTTP response status is not 200). By default this is set to `0` which is disabled. An additional `webhook.retry_delay` parameter can be set to specify the time in seconds between retry attempts. By default this is set to `0.1` (i.e. 100ms). Note that increasing the number of retries or retry delay may slow down the trader if there are connectivity issues with the webhook. Example configuration for retries:
|
||||
|
||||
```json
|
||||
"webhook": {
|
||||
"enabled": true,
|
||||
"url": "https://<YOURHOOKURL>",
|
||||
"timeout": 10,
|
||||
"retries": 3,
|
||||
"retry_delay": 0.2,
|
||||
"status": {
|
||||
@@ -115,8 +109,6 @@ Custom messages can be sent to Webhook endpoints via the `self.dp.send_msg()` fu
|
||||
|
||||
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
|
||||
|
||||
## Webhook Message types
|
||||
|
||||
### Entry
|
||||
|
||||
The fields in `webhook.entry` are filled when the bot executes a long/short. Parameters are filled using string.format.
|
||||
|
||||
@@ -24,9 +24,9 @@ git clone https://github.com/freqtrade/freqtrade.git
|
||||
|
||||
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
||||
|
||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.8, 3.9, 3.10 and 3.11) and for 64bit Windows.
|
||||
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
|
||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.25-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version).
|
||||
|
||||
Freqtrade provides these dependencies for the latest 3 Python versions (3.8, 3.9, 3.10 and 3.11) and for 64bit Windows.
|
||||
Other versions must be downloaded from the above link.
|
||||
|
||||
``` powershell
|
||||
@@ -45,6 +45,8 @@ freqtrade
|
||||
The above installation script assumes you're using powershell on a 64bit windows.
|
||||
Commands for the legacy CMD windows console may differ.
|
||||
|
||||
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222)
|
||||
|
||||
### Error during installation on Windows
|
||||
|
||||
``` bash
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2023.7'
|
||||
__version__ = '2023.3.dev'
|
||||
|
||||
if 'dev' in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -19,8 +19,7 @@ from freqtrade.commands.list_commands import (start_list_exchanges, start_list_f
|
||||
start_list_markets, start_list_strategies,
|
||||
start_list_timeframes, start_show_trades)
|
||||
from freqtrade.commands.optimize_commands import (start_backtesting, start_backtesting_show,
|
||||
start_edge, start_hyperopt,
|
||||
start_lookahead_analysis)
|
||||
start_edge, start_hyperopt)
|
||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
||||
from freqtrade.commands.strategy_utils_commands import start_strategy_update
|
||||
|
||||
Executable → Regular
+8
-24
@@ -46,7 +46,7 @@ ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column", "print_coloriz
|
||||
|
||||
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"]
|
||||
|
||||
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list", "backtest_breakdown"]
|
||||
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list"]
|
||||
|
||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
||||
|
||||
@@ -67,7 +67,8 @@ ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
||||
|
||||
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"]
|
||||
|
||||
@@ -105,8 +106,7 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
|
||||
"disableparamexport", "backtest_breakdown"]
|
||||
|
||||
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list",
|
||||
"exit_reason_list", "indicator_list", "timerange",
|
||||
"analysis_rejected", "analysis_to_csv", "analysis_csv_path"]
|
||||
"exit_reason_list", "indicator_list", "timerange"]
|
||||
|
||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||
"list-markets", "list-pairs", "list-strategies", "list-freqaimodels",
|
||||
@@ -116,11 +116,7 @@ NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list
|
||||
|
||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||
|
||||
ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_search"]
|
||||
|
||||
ARGS_LOOKAHEAD_ANALYSIS = [
|
||||
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"]
|
||||
ARGS_STRATEGY_UTILS = ["strategy_list", "strategy_path", "recursive_strategy_search"]
|
||||
|
||||
|
||||
class Arguments:
|
||||
@@ -204,9 +200,8 @@ class Arguments:
|
||||
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_show_trades, start_strategy_update,
|
||||
start_new_config, start_new_strategy, start_plot_dataframe,
|
||||
start_plot_profit, start_show_trades, start_strategy_update,
|
||||
start_test_pairlist, start_trading, start_webserver)
|
||||
|
||||
subparsers = self.parser.add_subparsers(dest='command',
|
||||
@@ -455,15 +450,4 @@ class Arguments:
|
||||
'files to the current version',
|
||||
parents=[_common_parser])
|
||||
strategy_updater_cmd.set_defaults(func=start_strategy_update)
|
||||
self._build_args(optionlist=ARGS_STRATEGY_UPDATER, parser=strategy_updater_cmd)
|
||||
|
||||
# Add lookahead_analysis subcommand
|
||||
lookahead_analayis_cmd = subparsers.add_parser(
|
||||
'lookahead-analysis',
|
||||
help="Check for potential look ahead bias.",
|
||||
parents=[_common_parser, _strategy_parser])
|
||||
|
||||
lookahead_analayis_cmd.set_defaults(func=start_lookahead_analysis)
|
||||
|
||||
self._build_args(optionlist=ARGS_LOOKAHEAD_ANALYSIS,
|
||||
parser=lookahead_analayis_cmd)
|
||||
self._build_args(optionlist=ARGS_STRATEGY_UTILS, parser=strategy_updater_cmd)
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import Any, Dict, List
|
||||
|
||||
from questionary import Separator, prompt
|
||||
|
||||
from freqtrade.configuration.detect_environment import running_in_docker
|
||||
from freqtrade.configuration.directory_operations import chown_user_directory
|
||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
||||
from freqtrade.exceptions import OperationalException
|
||||
@@ -180,7 +179,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"name": "api_server_listen_addr",
|
||||
"message": ("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",
|
||||
"when": lambda x: x['api_server']
|
||||
},
|
||||
{
|
||||
|
||||
Executable → Regular
+11
-41
@@ -381,7 +381,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"candle_types": Arg(
|
||||
'--candle-types',
|
||||
help='Select candle type to convert. Defaults to all available types.',
|
||||
help='Select candle type to use',
|
||||
choices=[c.value for c in CandleType],
|
||||
nargs='+',
|
||||
),
|
||||
@@ -450,12 +450,14 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"exchange": Arg(
|
||||
'--exchange',
|
||||
help='Exchange name. Only valid if no config is provided.',
|
||||
help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). '
|
||||
f'Only valid if no config is provided.',
|
||||
),
|
||||
"timeframes": Arg(
|
||||
'-t', '--timeframes',
|
||||
help='Specify which tickers to download. Space-separated list. '
|
||||
'Default: `1m 5m`.',
|
||||
default=['1m', '5m'],
|
||||
nargs='+',
|
||||
),
|
||||
"prepend_data": Arg(
|
||||
@@ -634,45 +636,30 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"4: by pair, enter_ and exit_tag (this can get quite large), "
|
||||
"5: by exit_tag"),
|
||||
nargs='+',
|
||||
default=[],
|
||||
default=['0', '1', '2'],
|
||||
choices=['0', '1', '2', '3', '4', '5'],
|
||||
),
|
||||
"enter_reason_list": Arg(
|
||||
"--enter-reason-list",
|
||||
help=("Space separated list of entry signals to analyse. Default: all. "
|
||||
"e.g. 'entry_tag_a entry_tag_b'"),
|
||||
help=("Comma separated list of entry signals to analyse. Default: all. "
|
||||
"e.g. 'entry_tag_a,entry_tag_b'"),
|
||||
nargs='+',
|
||||
default=['all'],
|
||||
),
|
||||
"exit_reason_list": Arg(
|
||||
"--exit-reason-list",
|
||||
help=("Space separated list of exit signals to analyse. Default: all. "
|
||||
"e.g. 'exit_tag_a roi stop_loss trailing_stop_loss'"),
|
||||
help=("Comma separated list of exit signals to analyse. Default: all. "
|
||||
"e.g. 'exit_tag_a,roi,stop_loss,trailing_stop_loss'"),
|
||||
nargs='+',
|
||||
default=['all'],
|
||||
),
|
||||
"indicator_list": Arg(
|
||||
"--indicator-list",
|
||||
help=("Space separated list of indicators to analyse. "
|
||||
"e.g. 'close rsi bb_lowerband profit_abs'"),
|
||||
help=("Comma separated list of indicators to analyse. "
|
||||
"e.g. 'close,rsi,bb_lowerband,profit_abs'"),
|
||||
nargs='+',
|
||||
default=[],
|
||||
),
|
||||
"analysis_rejected": Arg(
|
||||
'--rejected-signals',
|
||||
help='Analyse rejected signals',
|
||||
action='store_true',
|
||||
),
|
||||
"analysis_to_csv": Arg(
|
||||
'--analysis-to-csv',
|
||||
help='Save selected analysis tables to individual CSVs',
|
||||
action='store_true',
|
||||
),
|
||||
"analysis_csv_path": Arg(
|
||||
'--analysis-csv-path',
|
||||
help=("Specify a path to save the analysis CSVs "
|
||||
"if --analysis-to-csv is enabled. Default: user_data/basktesting_results/"),
|
||||
),
|
||||
"freqaimodel": Arg(
|
||||
'--freqaimodel',
|
||||
help='Specify a custom freqaimodels.',
|
||||
@@ -688,21 +675,4 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
help='Run backtest with ready models.',
|
||||
action='store_true'
|
||||
),
|
||||
"minimum_trade_amount": Arg(
|
||||
'--minimum-trade-amount',
|
||||
help='Minimum trade amount for lookahead-analysis',
|
||||
type=check_int_positive,
|
||||
metavar='INT',
|
||||
),
|
||||
"targeted_trade_amount": Arg(
|
||||
'--targeted-trade-amount',
|
||||
help='Targeted trade amount for lookahead analysis',
|
||||
type=check_int_positive,
|
||||
metavar='INT',
|
||||
),
|
||||
"lookahead_analysis_exportfilename": Arg(
|
||||
'--lookahead-analysis-exportfilename',
|
||||
help="Use this csv-filename to store lookahead-analysis-results",
|
||||
type=str
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import logging
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from typing import Any, Dict
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List
|
||||
|
||||
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, Config
|
||||
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
||||
from freqtrade.data.history import convert_trades_to_ohlcv, download_data_main
|
||||
from freqtrade.enums import RunMode, TradingMode
|
||||
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
|
||||
refresh_backtest_trades_data)
|
||||
from freqtrade.enums import CandleType, RunMode, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.exchange import market_is_active, timeframe_to_minutes
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
||||
|
||||
@@ -18,7 +20,7 @@ from freqtrade.util.binance_mig import migrate_binance_futures_data
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _check_data_config_download_sanity(config: Config) -> None:
|
||||
def _data_download_sanity(config: Config) -> None:
|
||||
if 'days' in config and 'timerange' in config:
|
||||
raise OperationalException("--days and --timerange are mutually exclusive. "
|
||||
"You can only specify one or the other.")
|
||||
@@ -35,14 +37,78 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||
|
||||
_check_data_config_download_sanity(config)
|
||||
_data_download_sanity(config)
|
||||
timerange = TimeRange()
|
||||
if 'days' in config:
|
||||
time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d")
|
||||
timerange = TimeRange.parse_timerange(f'{time_since}-')
|
||||
|
||||
if 'timerange' in config:
|
||||
timerange = timerange.parse_timerange(config['timerange'])
|
||||
|
||||
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||
config['stake_currency'] = ''
|
||||
|
||||
pairs_not_available: List[str] = []
|
||||
|
||||
# Init exchange
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
||||
markets = [p for p, m in exchange.markets.items() if market_is_active(m)
|
||||
or config.get('include_inactive')]
|
||||
|
||||
expanded_pairs = dynamic_expand_pairlist(config, markets)
|
||||
|
||||
# Manual validations of relevant settings
|
||||
if not config['exchange'].get('skip_pair_validation', False):
|
||||
exchange.validate_pairs(expanded_pairs)
|
||||
logger.info(f"About to download pairs: {expanded_pairs}, "
|
||||
f"intervals: {config['timeframes']} to {config['datadir']}")
|
||||
|
||||
for timeframe in config['timeframes']:
|
||||
exchange.validate_timeframes(timeframe)
|
||||
|
||||
try:
|
||||
download_data_main(config)
|
||||
|
||||
if config.get('download_trades'):
|
||||
if config.get('trading_mode') == 'futures':
|
||||
raise OperationalException("Trade download not supported for futures.")
|
||||
pairs_not_available = refresh_backtest_trades_data(
|
||||
exchange, pairs=expanded_pairs, datadir=config['datadir'],
|
||||
timerange=timerange, new_pairs_days=config['new_pairs_days'],
|
||||
erase=bool(config.get('erase')), data_format=config['dataformat_trades'])
|
||||
|
||||
# Convert downloaded trade data to different timeframes
|
||||
convert_trades_to_ohlcv(
|
||||
pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
|
||||
data_format_ohlcv=config['dataformat_ohlcv'],
|
||||
data_format_trades=config['dataformat_trades'],
|
||||
)
|
||||
else:
|
||||
if not exchange.get_option('ohlcv_has_history', True):
|
||||
raise OperationalException(
|
||||
f"Historic klines not available for {exchange.name}. "
|
||||
"Please use `--dl-trades` instead for this exchange "
|
||||
"(will unfortunately take a long time)."
|
||||
)
|
||||
migrate_binance_futures_data(config)
|
||||
pairs_not_available = refresh_backtest_ohlcv_data(
|
||||
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||
datadir=config['datadir'], timerange=timerange,
|
||||
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)
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
sys.exit("SIGINT received, aborting ...")
|
||||
|
||||
finally:
|
||||
if pairs_not_available:
|
||||
logger.info(f"Pairs [{','.join(pairs_not_available)}] not available "
|
||||
f"on exchange {exchange.name}.")
|
||||
|
||||
|
||||
def start_convert_trades(args: Dict[str, Any]) -> None:
|
||||
|
||||
@@ -57,11 +123,9 @@ def start_convert_trades(args: Dict[str, Any]) -> None:
|
||||
raise OperationalException(
|
||||
"Downloading data requires a list of pairs. "
|
||||
"Please check the documentation on how to configure this.")
|
||||
if 'timeframes' not in config:
|
||||
config['timeframes'] = DL_DATA_TIMEFRAMES
|
||||
|
||||
# Init exchange
|
||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
||||
# Manual validations of relevant settings
|
||||
if not config['exchange'].get('skip_pair_validation', False):
|
||||
exchange.validate_pairs(config['pairs'])
|
||||
@@ -88,10 +152,11 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
if ohlcv:
|
||||
migrate_binance_futures_data(config)
|
||||
convert_ohlcv_format(config,
|
||||
convert_from=args['format_from'],
|
||||
convert_to=args['format_to'],
|
||||
erase=args['erase'])
|
||||
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', ['spot'])]
|
||||
for candle_type in candle_types:
|
||||
convert_ohlcv_format(config,
|
||||
convert_from=args['format_from'], convert_to=args['format_to'],
|
||||
erase=args['erase'], candle_type=candle_type)
|
||||
else:
|
||||
convert_trades_format(config,
|
||||
convert_from=args['format_from'], convert_to=args['format_to'],
|
||||
@@ -139,14 +204,11 @@ def start_list_data(args: Dict[str, Any]) -> None:
|
||||
pair, timeframe, candle_type,
|
||||
*dhc.ohlcv_data_min_max(pair, timeframe, candle_type)
|
||||
) for pair, timeframe, candle_type in paircombs]
|
||||
|
||||
print(tabulate([
|
||||
(pair, timeframe, candle_type,
|
||||
start.strftime(DATETIME_PRINT_FORMAT),
|
||||
end.strftime(DATETIME_PRINT_FORMAT))
|
||||
for pair, timeframe, candle_type, start, end in sorted(
|
||||
paircombs1,
|
||||
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2]))
|
||||
for pair, timeframe, candle_type, start, end in paircombs1
|
||||
],
|
||||
headers=("Pair", "Timeframe", "Type", 'From', 'To'),
|
||||
tablefmt='psql', stralign='right'))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import csv
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any, Dict, List, Union
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import rapidjson
|
||||
from colorama import Fore, Style
|
||||
@@ -11,10 +11,9 @@ from tabulate import tabulate
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import list_available_exchanges, market_is_active
|
||||
from freqtrade.exchange import market_is_active, validate_exchanges
|
||||
from freqtrade.misc import parse_db_uri_for_logging, plural
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.types import ValidExchangesType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -26,42 +25,18 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
exchanges = list_available_exchanges(args['list_exchanges_all'])
|
||||
exchanges = validate_exchanges(args['list_exchanges_all'])
|
||||
|
||||
if args['print_one_column']:
|
||||
print('\n'.join([e['name'] for e in exchanges]))
|
||||
print('\n'.join([e[0] for e in exchanges]))
|
||||
else:
|
||||
headers = {
|
||||
'name': 'Exchange name',
|
||||
'supported': 'Supported',
|
||||
'trade_modes': 'Markets',
|
||||
'comment': 'Reason',
|
||||
}
|
||||
headers.update({'valid': 'Valid'} if args['list_exchanges_all'] else {})
|
||||
|
||||
def build_entry(exchange: ValidExchangesType, valid: bool):
|
||||
valid_entry = {'valid': exchange['valid']} if valid else {}
|
||||
result: Dict[str, Union[str, bool]] = {
|
||||
'name': exchange['name'],
|
||||
**valid_entry,
|
||||
'supported': 'Official' if exchange['supported'] else '',
|
||||
'trade_modes': ', '.join(
|
||||
(f"{a['margin_mode']} " if a['margin_mode'] else '') + a['trading_mode']
|
||||
for a in exchange['trade_modes']
|
||||
),
|
||||
'comment': exchange['comment'],
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
if args['list_exchanges_all']:
|
||||
print("All exchanges supported by the ccxt library:")
|
||||
exchanges = [build_entry(e, True) for e in exchanges]
|
||||
else:
|
||||
print("Exchanges available for Freqtrade:")
|
||||
exchanges = [build_entry(e, False) for e in exchanges if e['valid'] is not False]
|
||||
exchanges = [e for e in exchanges if e[1] is not False]
|
||||
|
||||
print(tabulate(exchanges, headers=headers, ))
|
||||
print(tabulate(exchanges, headers=['Exchange name', 'Valid', 'reason']))
|
||||
|
||||
|
||||
def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
|
||||
@@ -139,7 +114,7 @@ def start_list_timeframes(args: Dict[str, Any]) -> None:
|
||||
config['timeframe'] = None
|
||||
|
||||
# Init exchange
|
||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
||||
|
||||
if args['print_one_column']:
|
||||
print('\n'.join(exchange.timeframes))
|
||||
@@ -158,7 +133,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||
|
||||
# Init exchange
|
||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
||||
|
||||
# By default only active pairs/markets are to be shown
|
||||
active_only = not args.get('list_pairs_all', False)
|
||||
|
||||
@@ -132,15 +132,3 @@ def start_edge(args: Dict[str, Any]) -> None:
|
||||
# Initialize Edge object
|
||||
edge_cli = EdgeCli(config)
|
||||
edge_cli.start()
|
||||
|
||||
|
||||
def start_lookahead_analysis(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Start the backtest bias tester script
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.optimize.lookahead_analysis_helpers import LookaheadAnalysisSubFunctions
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
LookaheadAnalysisSubFunctions.start(config)
|
||||
|
||||
@@ -18,7 +18,7 @@ def start_test_pairlist(args: Dict[str, Any]) -> None:
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||
|
||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
||||
|
||||
quote_currencies = args.get('quote_currencies')
|
||||
if not quote_currencies:
|
||||
|
||||
@@ -174,7 +174,7 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None:
|
||||
return
|
||||
|
||||
for pl in conf.get('pairlists', [{'method': 'StaticPairList'}]):
|
||||
if (isinstance(pl, dict) and pl.get('method') == 'StaticPairList'
|
||||
if (pl.get('method') == 'StaticPairList'
|
||||
and not conf.get('exchange', {}).get('pair_whitelist')):
|
||||
raise OperationalException("StaticPairList requires pair_whitelist to be set.")
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ class Configuration:
|
||||
# This will override the strategy configuration
|
||||
self._args_to_config(config, argname='timeframe',
|
||||
logstring='Parameter -i/--timeframe detected ... '
|
||||
'Using timeframe: {} ...')
|
||||
'Using timeframe: {} ...')
|
||||
|
||||
self._args_to_config(config, argname='position_stacking',
|
||||
logstring='Parameter --enable-position-stacking detected ...')
|
||||
@@ -300,9 +300,6 @@ class Configuration:
|
||||
self._args_to_config(config, argname='hyperoptexportfilename',
|
||||
logstring='Using hyperopt file: {}')
|
||||
|
||||
self._args_to_config(config, argname='lookahead_analysis_exportfilename',
|
||||
logstring='Saving lookahead analysis results into {} ...')
|
||||
|
||||
self._args_to_config(config, argname='epochs',
|
||||
logstring='Parameter --epochs detected ... '
|
||||
'Will run Hyperopt with for {} epochs ...'
|
||||
@@ -468,28 +465,6 @@ class Configuration:
|
||||
self._args_to_config(config, argname='timerange',
|
||||
logstring='Filter trades by timerange: {}')
|
||||
|
||||
self._args_to_config(config, argname='analysis_rejected',
|
||||
logstring='Analyse rejected signals: {}')
|
||||
|
||||
self._args_to_config(config, argname='analysis_to_csv',
|
||||
logstring='Store analysis tables to CSV: {}')
|
||||
|
||||
self._args_to_config(config, argname='analysis_csv_path',
|
||||
logstring='Path to store analysis CSVs: {}')
|
||||
|
||||
self._args_to_config(config, argname='analysis_csv_path',
|
||||
logstring='Path to store analysis CSVs: {}')
|
||||
|
||||
# Lookahead analysis results
|
||||
self._args_to_config(config, argname='targeted_trade_amount',
|
||||
logstring='Targeted Trade amount: {}')
|
||||
|
||||
self._args_to_config(config, argname='minimum_trade_amount',
|
||||
logstring='Minimum Trade amount: {}')
|
||||
|
||||
self._args_to_config(config, argname='lookahead_analysis_exportfilename',
|
||||
logstring='Path to store lookahead-analysis-results: {}')
|
||||
|
||||
def _process_runmode(self, config: Config) -> None:
|
||||
|
||||
self._args_to_config(config, argname='dry_run',
|
||||
@@ -568,7 +543,6 @@ class Configuration:
|
||||
# Fall back to /dl_path/pairs.json
|
||||
pairs_file = config['datadir'] / 'pairs.json'
|
||||
if pairs_file.exists():
|
||||
logger.info(f'Reading pairs file "{pairs_file}".')
|
||||
config['pairs'] = load_file(pairs_file)
|
||||
if 'pairs' in config and isinstance(config['pairs'], list):
|
||||
config['pairs'].sort()
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import os
|
||||
|
||||
|
||||
def running_in_docker() -> bool:
|
||||
"""
|
||||
Check if we are running in a docker container
|
||||
"""
|
||||
return os.environ.get('FT_APP_ENV') == 'docker'
|
||||
@@ -3,7 +3,6 @@ import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from freqtrade.configuration.detect_environment import running_in_docker
|
||||
from freqtrade.constants import (USER_DATA_FILES, USERPATH_FREQAIMODELS, USERPATH_HYPEROPTS,
|
||||
USERPATH_NOTEBOOKS, USERPATH_STRATEGIES, Config)
|
||||
from freqtrade.exceptions import OperationalException
|
||||
@@ -31,7 +30,8 @@ def chown_user_directory(directory: Path) -> None:
|
||||
Use Sudo to change permissions of the home-directory if necessary
|
||||
Only applies when running in docker!
|
||||
"""
|
||||
if running_in_docker():
|
||||
import os
|
||||
if os.environ.get('FT_APP_ENV') == 'docker':
|
||||
try:
|
||||
import subprocess
|
||||
subprocess.check_output(
|
||||
|
||||
@@ -6,7 +6,7 @@ import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from typing_extensions import Self
|
||||
import arrow
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
from freqtrade.exceptions import OperationalException
|
||||
@@ -109,15 +109,15 @@ class TimeRange:
|
||||
self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles)
|
||||
self.starttype = 'date'
|
||||
|
||||
@classmethod
|
||||
def parse_timerange(cls, text: Optional[str]) -> Self:
|
||||
@staticmethod
|
||||
def parse_timerange(text: Optional[str]) -> 'TimeRange':
|
||||
"""
|
||||
Parse the value of the argument --timerange to determine what is the range desired
|
||||
:param text: value from --timerange
|
||||
:return: Start and End range period
|
||||
"""
|
||||
if not text:
|
||||
return cls(None, None, 0, 0)
|
||||
if text is None:
|
||||
return TimeRange(None, None, 0, 0)
|
||||
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
||||
(r'^(\d{8})-$', ('date', None)),
|
||||
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
||||
@@ -139,8 +139,7 @@ class TimeRange:
|
||||
if stype[0]:
|
||||
starts = rvals[index]
|
||||
if stype[0] == 'date' and len(starts) == 8:
|
||||
start = int(datetime.strptime(starts, '%Y%m%d').replace(
|
||||
tzinfo=timezone.utc).timestamp())
|
||||
start = arrow.get(starts, 'YYYYMMDD').int_timestamp
|
||||
elif len(starts) == 13:
|
||||
start = int(starts) // 1000
|
||||
else:
|
||||
@@ -149,8 +148,7 @@ class TimeRange:
|
||||
if stype[1]:
|
||||
stops = rvals[index]
|
||||
if stype[1] == 'date' and len(stops) == 8:
|
||||
stop = int(datetime.strptime(stops, '%Y%m%d').replace(
|
||||
tzinfo=timezone.utc).timestamp())
|
||||
stop = arrow.get(stops, 'YYYYMMDD').int_timestamp
|
||||
elif len(stops) == 13:
|
||||
stop = int(stops) // 1000
|
||||
else:
|
||||
@@ -158,5 +156,5 @@ class TimeRange:
|
||||
if start > stop > 0:
|
||||
raise OperationalException(
|
||||
f'Start date is after stop date for timerange "{text}"')
|
||||
return cls(stype[0], stype[1], start, stop)
|
||||
return TimeRange(stype[0], stype[1], start, stop)
|
||||
raise OperationalException(f'Incorrect syntax for timerange "{text}"')
|
||||
|
||||
+7
-18
@@ -8,8 +8,8 @@ from typing import Any, Dict, List, Literal, Tuple
|
||||
from freqtrade.enums import CandleType, PriceType, RPCMessageType
|
||||
|
||||
|
||||
DOCS_LINK = "https://www.freqtrade.io/en/stable"
|
||||
DEFAULT_CONFIG = 'config.json'
|
||||
DEFAULT_EXCHANGE = 'bittrex'
|
||||
PROCESS_THROTTLE_SECS = 5 # sec
|
||||
HYPEROPT_EPOCH = 100 # epochs
|
||||
RETRY_TIMEOUT = 30 # sec
|
||||
@@ -36,10 +36,9 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', '
|
||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
||||
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
||||
AVAILABLE_PROTECTIONS = ['CooldownPeriod',
|
||||
'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
||||
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5', 'feather']
|
||||
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['parquet']
|
||||
AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
||||
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5']
|
||||
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['feather', 'parquet']
|
||||
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
||||
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
||||
BACKTEST_CACHE_DEFAULT = 'day'
|
||||
@@ -64,8 +63,6 @@ USERPATH_FREQAIMODELS = 'freqaimodels'
|
||||
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
||||
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
|
||||
FULL_DATAFRAME_THRESHOLD = 100
|
||||
CUSTOM_TAG_MAX_LENGTH = 255
|
||||
DL_DATA_TIMEFRAMES = ['1m', '5m']
|
||||
|
||||
ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||
|
||||
@@ -112,8 +109,6 @@ MINIMAL_CONFIG = {
|
||||
}
|
||||
}
|
||||
|
||||
__MESSAGE_TYPE_DICT: Dict[str, Dict[str, str]] = {x: {'type': 'object'} for x in RPCMessageType}
|
||||
|
||||
# Required json-schema for user specified config
|
||||
CONF_SCHEMA = {
|
||||
'type': 'object',
|
||||
@@ -151,6 +146,7 @@ CONF_SCHEMA = {
|
||||
'patternProperties': {
|
||||
'^[0-9.]+$': {'type': 'number'}
|
||||
},
|
||||
'minProperties': 1
|
||||
},
|
||||
'amount_reserve_percent': {'type': 'number', 'minimum': 0.0, 'maximum': 0.5},
|
||||
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True, 'minimum': -1},
|
||||
@@ -166,9 +162,6 @@ CONF_SCHEMA = {
|
||||
'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
|
||||
'margin_mode': {'type': 'string', 'enum': MARGIN_MODES},
|
||||
'reduce_df_footprint': {'type': 'boolean', 'default': False},
|
||||
'minimum_trade_amount': {'type': 'number', 'default': 10},
|
||||
'targeted_trade_amount': {'type': 'number', 'default': 20},
|
||||
'lookahead_analysis_exportfilename': {'type': 'string'},
|
||||
'liquidation_buffer': {'type': 'number', 'minimum': 0.0, 'maximum': 0.99},
|
||||
'backtest_breakdown': {
|
||||
'type': 'array',
|
||||
@@ -356,8 +349,7 @@ CONF_SCHEMA = {
|
||||
'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'},
|
||||
'retries': {'type': 'integer', 'minimum': 0},
|
||||
'retry_delay': {'type': 'number', 'minimum': 0},
|
||||
**__MESSAGE_TYPE_DICT,
|
||||
# **{x: {'type': 'object'} for x in RPCMessageType},
|
||||
**dict([(x, {'type': 'object'}) for x in RPCMessageType]),
|
||||
# Below -> Deprecated
|
||||
'webhookentry': {'type': 'object'},
|
||||
'webhookentrycancel': {'type': 'object'},
|
||||
@@ -605,8 +597,7 @@ CONF_SCHEMA = {
|
||||
"model_type": {"type": "string", "default": "PPO"},
|
||||
"policy_type": {"type": "string", "default": "MlpPolicy"},
|
||||
"net_arch": {"type": "array", "default": [128, 128]},
|
||||
"randomize_starting_position": {"type": "boolean", "default": False},
|
||||
"progress_bar": {"type": "boolean", "default": True},
|
||||
"randomize_startinng_position": {"type": "boolean", "default": False},
|
||||
"model_reward_parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -696,6 +687,4 @@ BidAsk = Literal['bid', 'ask']
|
||||
OBLiteral = Literal['asks', 'bids']
|
||||
|
||||
Config = Dict[str, Any]
|
||||
# Exchange part of the configuration.
|
||||
ExchangeConfig = Dict[str, Any]
|
||||
IntOrInf = float
|
||||
|
||||
@@ -170,7 +170,6 @@ def load_and_merge_backtest_result(strategy_name: str, filename: Path, results:
|
||||
|
||||
|
||||
def _get_backtest_files(dirname: Path) -> List[Path]:
|
||||
# Weird glob expression here avoids including .meta.json files.
|
||||
return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))))
|
||||
|
||||
|
||||
@@ -185,7 +184,7 @@ def get_backtest_resultlist(dirname: Path):
|
||||
continue
|
||||
for s, v in metadata.items():
|
||||
results.append({
|
||||
'filename': filename.stem,
|
||||
'filename': filename.name,
|
||||
'strategy': s,
|
||||
'run_id': v['run_id'],
|
||||
'backtest_start_time': v['backtest_start_time'],
|
||||
@@ -194,17 +193,6 @@ def get_backtest_resultlist(dirname: Path):
|
||||
return results
|
||||
|
||||
|
||||
def delete_backtest_result(file_abs: Path):
|
||||
"""
|
||||
Delete backtest result file and corresponding metadata file.
|
||||
"""
|
||||
# *.meta.json
|
||||
logger.info(f"Deleting backtest result file: {file_abs.name}")
|
||||
file_abs_meta = file_abs.with_suffix('.meta.json')
|
||||
file_abs.unlink()
|
||||
file_abs_meta.unlink()
|
||||
|
||||
|
||||
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
|
||||
min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -223,6 +211,7 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
|
||||
'strategy_comparison': [],
|
||||
}
|
||||
|
||||
# Weird glob expression here avoids including .meta.json files.
|
||||
for filename in _get_backtest_files(dirname):
|
||||
metadata = load_backtest_metadata(filename)
|
||||
if not metadata:
|
||||
@@ -257,8 +246,14 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
Compatibility support for older backtest data.
|
||||
"""
|
||||
df['open_date'] = pd.to_datetime(df['open_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,
|
||||
infer_datetime_format=True
|
||||
)
|
||||
df['close_date'] = pd.to_datetime(df['close_date'],
|
||||
utc=True,
|
||||
infer_datetime_format=True
|
||||
)
|
||||
# Compatibility support for pre short Columns
|
||||
if 'is_short' not in df.columns:
|
||||
df['is_short'] = False
|
||||
|
||||
+36
-48
@@ -11,7 +11,7 @@ import pandas as pd
|
||||
from pandas import DataFrame, to_datetime
|
||||
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, Config, TradeList
|
||||
from freqtrade.enums import CandleType, TradingMode
|
||||
from freqtrade.enums import CandleType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -34,7 +34,7 @@ def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *,
|
||||
cols = DEFAULT_DATAFRAME_COLUMNS
|
||||
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, infer_datetime_format=True)
|
||||
|
||||
# Some exchanges return int values for Volume and even for OHLC.
|
||||
# Convert them since TA-LIB indicators used in the strategy assume floats
|
||||
@@ -96,14 +96,8 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
||||
'volume': 'sum'
|
||||
}
|
||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
||||
resample_interval = f'{timeframe_minutes}min'
|
||||
if timeframe_minutes >= 43200 and timeframe_minutes < 525600:
|
||||
# Monthly candles need special treatment to stick to the 1st of the month
|
||||
resample_interval = f'{timeframe}S'
|
||||
elif timeframe_minutes > 43200:
|
||||
resample_interval = timeframe
|
||||
# Resample to create "NAN" values
|
||||
df = dataframe.resample(resample_interval, on='date').agg(ohlcv_dict)
|
||||
df = dataframe.resample(f'{timeframe_minutes}min', on='date').agg(ohlcv_dict)
|
||||
|
||||
# Forwardfill close for missing columns
|
||||
df['close'] = df['close'].fillna(method='ffill')
|
||||
@@ -128,7 +122,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
||||
return df
|
||||
|
||||
|
||||
def trim_dataframe(df: DataFrame, timerange, *, df_date_col: str = 'date',
|
||||
def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date',
|
||||
startup_candles: int = 0) -> DataFrame:
|
||||
"""
|
||||
Trim dataframe based on given timerange
|
||||
@@ -270,6 +264,7 @@ def convert_ohlcv_format(
|
||||
convert_from: str,
|
||||
convert_to: str,
|
||||
erase: bool,
|
||||
candle_type: CandleType
|
||||
):
|
||||
"""
|
||||
Convert OHLCV from one format to another
|
||||
@@ -277,6 +272,7 @@ def convert_ohlcv_format(
|
||||
:param convert_from: Source format
|
||||
:param convert_to: Target format
|
||||
:param erase: Erase source data (does not apply if source and target format are identical)
|
||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||
"""
|
||||
from freqtrade.data.history.idatahandler import get_datahandler
|
||||
src = get_datahandler(config['datadir'], convert_from)
|
||||
@@ -284,45 +280,37 @@ def convert_ohlcv_format(
|
||||
timeframes = config.get('timeframes', [config.get('timeframe')])
|
||||
logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}")
|
||||
|
||||
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', [
|
||||
c.value for c in CandleType])]
|
||||
logger.info(candle_types)
|
||||
paircombs = src.ohlcv_get_available_data(config['datadir'], TradingMode.SPOT)
|
||||
paircombs.extend(src.ohlcv_get_available_data(config['datadir'], TradingMode.FUTURES))
|
||||
|
||||
if 'pairs' in config:
|
||||
# Filter pairs
|
||||
paircombs = [comb for comb in paircombs if comb[0] in config['pairs']]
|
||||
|
||||
if 'timeframes' in config:
|
||||
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 = sorted(paircombs, key=lambda x: (x[0], x[1], x[2].value))
|
||||
|
||||
formatted_paircombs = '\n'.join([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"
|
||||
f"{formatted_paircombs}")
|
||||
for pair, timeframe, candle_type in paircombs:
|
||||
data = src.ohlcv_load(pair=pair, timeframe=timeframe,
|
||||
timerange=None,
|
||||
fill_missing=False,
|
||||
drop_incomplete=False,
|
||||
startup_candles=0,
|
||||
candle_type=candle_type)
|
||||
logger.info(f"Converting {len(data)} {timeframe} {candle_type} candles for {pair}")
|
||||
if len(data) > 0:
|
||||
trg.ohlcv_store(
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
data=data,
|
||||
if 'pairs' not in config:
|
||||
config['pairs'] = []
|
||||
# Check timeframes or fall back to timeframe.
|
||||
for timeframe in timeframes:
|
||||
config['pairs'].extend(src.ohlcv_get_pairs(
|
||||
config['datadir'],
|
||||
timeframe,
|
||||
candle_type=candle_type
|
||||
)
|
||||
if erase and convert_from != convert_to:
|
||||
logger.info(f"Deleting source data for {pair} / {timeframe}")
|
||||
src.ohlcv_purge(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||
))
|
||||
config['pairs'] = sorted(set(config['pairs']))
|
||||
logger.info(f"Converting candle (OHLCV) data for {config['pairs']}")
|
||||
|
||||
for timeframe in timeframes:
|
||||
for pair in config['pairs']:
|
||||
data = src.ohlcv_load(pair=pair, timeframe=timeframe,
|
||||
timerange=None,
|
||||
fill_missing=False,
|
||||
drop_incomplete=False,
|
||||
startup_candles=0,
|
||||
candle_type=candle_type)
|
||||
logger.info(f"Converting {len(data)} {timeframe} {candle_type} candles for {pair}")
|
||||
if len(data) > 0:
|
||||
trg.ohlcv_store(
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
data=data,
|
||||
candle_type=candle_type
|
||||
)
|
||||
if erase and convert_from != convert_to:
|
||||
logger.info(f"Deleting source data for {pair} / {timeframe}")
|
||||
src.ohlcv_purge(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||
|
||||
|
||||
def reduce_dataframe_footprint(df: DataFrame) -> DataFrame:
|
||||
|
||||
@@ -21,7 +21,6 @@ from freqtrade.exchange import Exchange, timeframe_to_seconds
|
||||
from freqtrade.exchange.types import OrderBook
|
||||
from freqtrade.misc import append_candles_to_dataframe
|
||||
from freqtrade.rpc import RPCManager
|
||||
from freqtrade.rpc.rpc_types import RPCAnalyzedDFMsg
|
||||
from freqtrade.util import PeriodicCache
|
||||
|
||||
|
||||
@@ -119,7 +118,8 @@ class DataProvider:
|
||||
:param new_candle: This is a new candle
|
||||
"""
|
||||
if self.__rpc:
|
||||
msg: RPCAnalyzedDFMsg = {
|
||||
self.__rpc.send_msg(
|
||||
{
|
||||
'type': RPCMessageType.ANALYZED_DF,
|
||||
'data': {
|
||||
'key': pair_key,
|
||||
@@ -127,7 +127,7 @@ class DataProvider:
|
||||
'la': datetime.now(timezone.utc)
|
||||
}
|
||||
}
|
||||
self.__rpc.send_msg(msg)
|
||||
)
|
||||
if new_candle:
|
||||
self.__rpc.send_msg({
|
||||
'type': RPCMessageType.NEW_CANDLE,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import joblib
|
||||
import pandas as pd
|
||||
@@ -16,31 +15,22 @@ from freqtrade.exceptions import OperationalException
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _load_backtest_analysis_data(backtest_dir: Path, name: str):
|
||||
def _load_signal_candles(backtest_dir: Path):
|
||||
if backtest_dir.is_dir():
|
||||
scpf = Path(backtest_dir,
|
||||
Path(get_latest_backtest_filename(backtest_dir)).stem + "_" + name + ".pkl"
|
||||
Path(get_latest_backtest_filename(backtest_dir)).stem + "_signals.pkl"
|
||||
)
|
||||
else:
|
||||
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_{name}.pkl")
|
||||
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_signals.pkl")
|
||||
|
||||
try:
|
||||
with scpf.open("rb") as scp:
|
||||
loaded_data = joblib.load(scp)
|
||||
logger.info(f"Loaded {name} candles: {str(scpf)}")
|
||||
signal_candles = joblib.load(scp)
|
||||
logger.info(f"Loaded signal candles: {str(scpf)}")
|
||||
except Exception as e:
|
||||
logger.error(f"Cannot load {name} data from pickled results: ", e)
|
||||
return None
|
||||
logger.error("Cannot load signal candles from pickled results: ", e)
|
||||
|
||||
return loaded_data
|
||||
|
||||
|
||||
def _load_rejected_signals(backtest_dir: Path):
|
||||
return _load_backtest_analysis_data(backtest_dir, "rejected")
|
||||
|
||||
|
||||
def _load_signal_candles(backtest_dir: Path):
|
||||
return _load_backtest_analysis_data(backtest_dir, "signals")
|
||||
return signal_candles
|
||||
|
||||
|
||||
def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_candles):
|
||||
@@ -53,7 +43,9 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand
|
||||
for pair in pairlist:
|
||||
if pair in signal_candles[strategy_name]:
|
||||
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:
|
||||
print(f"Cannot process entry/exit reasons for {strategy_name}: ", e)
|
||||
|
||||
@@ -93,7 +85,7 @@ def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles:
|
||||
return pd.DataFrame()
|
||||
|
||||
|
||||
def _do_group_table_output(bigdf, glist, csv_path: Path, to_csv=False, ):
|
||||
def _do_group_table_output(bigdf, glist):
|
||||
for g in glist:
|
||||
# 0: summary wins/losses grouped by enter tag
|
||||
if g == "0":
|
||||
@@ -124,8 +116,7 @@ def _do_group_table_output(bigdf, glist, csv_path: Path, to_csv=False, ):
|
||||
|
||||
sortcols = ['total_num_buys']
|
||||
|
||||
_print_table(new, sortcols, show_index=True, name="Group 0:",
|
||||
to_csv=to_csv, csv_path=csv_path)
|
||||
_print_table(new, sortcols, show_index=True)
|
||||
|
||||
else:
|
||||
agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'],
|
||||
@@ -163,24 +154,11 @@ def _do_group_table_output(bigdf, glist, csv_path: Path, to_csv=False, ):
|
||||
new['mean_profit_pct'] = new['mean_profit_pct'] * 100
|
||||
new['total_profit_pct'] = new['total_profit_pct'] * 100
|
||||
|
||||
_print_table(new, sortcols, name=f"Group {g}:",
|
||||
to_csv=to_csv, csv_path=csv_path)
|
||||
_print_table(new, sortcols)
|
||||
else:
|
||||
logger.warning("Invalid group mask specified.")
|
||||
|
||||
|
||||
def _do_rejected_signals_output(rejected_signals_df: pd.DataFrame,
|
||||
to_csv: bool = False, csv_path=None) -> None:
|
||||
cols = ['pair', 'date', 'enter_tag']
|
||||
sortcols = ['date', 'pair', 'enter_tag']
|
||||
_print_table(rejected_signals_df[cols],
|
||||
sortcols,
|
||||
show_index=False,
|
||||
name="Rejected Signals:",
|
||||
to_csv=to_csv,
|
||||
csv_path=csv_path)
|
||||
|
||||
|
||||
def _select_rows_within_dates(df, timerange=None, df_date_col: str = 'date'):
|
||||
if timerange:
|
||||
if timerange.starttype == 'date':
|
||||
@@ -214,64 +192,38 @@ def prepare_results(analysed_trades, stratname,
|
||||
return res_df
|
||||
|
||||
|
||||
def print_results(res_df: pd.DataFrame, analysis_groups: List[str], indicator_list: List[str],
|
||||
csv_path: Path, rejected_signals=None, to_csv=False):
|
||||
def print_results(res_df, analysis_groups, indicator_list):
|
||||
if res_df.shape[0] > 0:
|
||||
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)
|
||||
|
||||
if rejected_signals is not None:
|
||||
if rejected_signals.empty:
|
||||
print("There were no rejected signals.")
|
||||
else:
|
||||
_do_rejected_signals_output(rejected_signals, to_csv=to_csv, csv_path=csv_path)
|
||||
|
||||
# NB this can be large for big dataframes!
|
||||
if "all" in indicator_list:
|
||||
_print_table(res_df,
|
||||
show_index=False,
|
||||
name="Indicators:",
|
||||
to_csv=to_csv,
|
||||
csv_path=csv_path)
|
||||
elif indicator_list is not None and indicator_list:
|
||||
print(res_df)
|
||||
elif indicator_list is not None:
|
||||
available_inds = []
|
||||
for ind in indicator_list:
|
||||
if ind in res_df:
|
||||
available_inds.append(ind)
|
||||
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
|
||||
_print_table(res_df[ilist],
|
||||
sortcols=['exit_reason'],
|
||||
show_index=False,
|
||||
name="Indicators:",
|
||||
to_csv=to_csv,
|
||||
csv_path=csv_path)
|
||||
_print_table(res_df[ilist], sortcols=['exit_reason'], show_index=False)
|
||||
else:
|
||||
print("\\No trades to show")
|
||||
|
||||
|
||||
def _print_table(df: pd.DataFrame, sortcols=None, *, show_index=False, name=None,
|
||||
to_csv=False, csv_path: Path):
|
||||
def _print_table(df, sortcols=None, show_index=False):
|
||||
if (sortcols is not None):
|
||||
data = df.sort_values(sortcols)
|
||||
else:
|
||||
data = df
|
||||
|
||||
if to_csv:
|
||||
safe_name = Path(csv_path, name.lower().replace(" ", "_").replace(":", "") + ".csv")
|
||||
data.to_csv(safe_name)
|
||||
print(f"Saved {name} to {safe_name}")
|
||||
else:
|
||||
if name is not None:
|
||||
print(name)
|
||||
|
||||
print(
|
||||
tabulate(
|
||||
data,
|
||||
headers='keys',
|
||||
tablefmt='psql',
|
||||
showindex=show_index
|
||||
)
|
||||
print(
|
||||
tabulate(
|
||||
data,
|
||||
headers='keys',
|
||||
tablefmt='psql',
|
||||
showindex=show_index
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def process_entry_exit_reasons(config: Config):
|
||||
@@ -280,11 +232,6 @@ def process_entry_exit_reasons(config: Config):
|
||||
enter_reason_list = config.get('enter_reason_list', ["all"])
|
||||
exit_reason_list = config.get('exit_reason_list', ["all"])
|
||||
indicator_list = config.get('indicator_list', [])
|
||||
do_rejected = config.get('analysis_rejected', False)
|
||||
to_csv = config.get('analysis_to_csv', False)
|
||||
csv_path = Path(config.get('analysis_csv_path', config['exportfilename']))
|
||||
if to_csv and not csv_path.is_dir():
|
||||
raise OperationalException(f"Specified directory {csv_path} does not exist.")
|
||||
|
||||
timerange = TimeRange.parse_timerange(None if config.get(
|
||||
'timerange') is None else str(config.get('timerange')))
|
||||
@@ -294,16 +241,8 @@ def process_entry_exit_reasons(config: Config):
|
||||
for strategy_name, results in backtest_stats['strategy'].items():
|
||||
trades = load_backtest_data(config['exportfilename'], strategy_name)
|
||||
|
||||
if trades is not None and not trades.empty:
|
||||
if not trades.empty:
|
||||
signal_candles = _load_signal_candles(config['exportfilename'])
|
||||
|
||||
rej_df = None
|
||||
if do_rejected:
|
||||
rejected_signals_dict = _load_rejected_signals(config['exportfilename'])
|
||||
rej_df = prepare_results(rejected_signals_dict, strategy_name,
|
||||
enter_reason_list, exit_reason_list,
|
||||
timerange=timerange)
|
||||
|
||||
analysed_trades_dict = _process_candles_and_indicators(
|
||||
config['exchange']['pair_whitelist'], strategy_name,
|
||||
trades, signal_candles)
|
||||
@@ -314,10 +253,7 @@ def process_entry_exit_reasons(config: Config):
|
||||
|
||||
print_results(res_df,
|
||||
analysis_groups,
|
||||
indicator_list,
|
||||
rejected_signals=rej_df,
|
||||
to_csv=to_csv,
|
||||
csv_path=csv_path)
|
||||
indicator_list)
|
||||
|
||||
except ValueError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@@ -6,7 +6,7 @@ Includes:
|
||||
* download data from exchange and store to disk
|
||||
"""
|
||||
# flake8: noqa: F401
|
||||
from .history_utils import (convert_trades_to_ohlcv, download_data_main, get_timerange, load_data,
|
||||
load_pair_history, refresh_backtest_ohlcv_data,
|
||||
refresh_backtest_trades_data, refresh_data, validate_backtest_data)
|
||||
from .history_utils import (convert_trades_to_ohlcv, get_timerange, load_data, load_pair_history,
|
||||
refresh_backtest_ohlcv_data, refresh_backtest_trades_data, refresh_data,
|
||||
validate_backtest_data)
|
||||
from .idatahandler import get_datahandler
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Optional
|
||||
from pandas import DataFrame, read_feather, to_datetime
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList
|
||||
from freqtrade.enums import CandleType
|
||||
|
||||
from .idatahandler import IDataHandler
|
||||
@@ -63,7 +63,10 @@ class FeatherDataHandler(IDataHandler):
|
||||
pairdata.columns = self._columns
|
||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
||||
pairdata['date'] = to_datetime(pairdata['date'],
|
||||
unit='ms',
|
||||
utc=True,
|
||||
infer_datetime_format=True)
|
||||
return pairdata
|
||||
|
||||
def ohlcv_append(
|
||||
@@ -89,11 +92,12 @@ class FeatherDataHandler(IDataHandler):
|
||||
:param data: List of Lists containing trade data,
|
||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||
"""
|
||||
filename = self._pair_trades_filename(self._datadir, pair)
|
||||
self.create_dir_if_needed(filename)
|
||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
||||
|
||||
tradesdata = DataFrame(data, columns=DEFAULT_TRADES_COLUMNS)
|
||||
tradesdata.to_feather(filename, compression_level=9, compression='lz4')
|
||||
raise NotImplementedError()
|
||||
# array = pa.array(data)
|
||||
# array
|
||||
# feather.write_feather(data, filename)
|
||||
|
||||
def trades_append(self, pair: str, data: TradeList):
|
||||
"""
|
||||
@@ -112,13 +116,14 @@ class FeatherDataHandler(IDataHandler):
|
||||
:param timerange: Timerange to load trades for - currently not implemented
|
||||
:return: List of trades
|
||||
"""
|
||||
filename = self._pair_trades_filename(self._datadir, pair)
|
||||
if not filename.exists():
|
||||
return []
|
||||
raise NotImplementedError()
|
||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
||||
# tradesdata = misc.file_load_json(filename)
|
||||
|
||||
tradesdata = read_feather(filename)
|
||||
# if not tradesdata:
|
||||
# return []
|
||||
|
||||
return tradesdata.values.tolist()
|
||||
# return tradesdata
|
||||
|
||||
@classmethod
|
||||
def _get_file_extension(cls):
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import logging
|
||||
import operator
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame, concat
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS,
|
||||
DL_DATA_TIMEFRAMES, Config)
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
||||
from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe,
|
||||
trades_remove_duplicates, trades_to_ohlcv)
|
||||
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||
from freqtrade.util import format_ms_time
|
||||
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
||||
from freqtrade.misc import format_ms_time
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -230,18 +228,16 @@ def _download_pair_history(pair: str, *,
|
||||
)
|
||||
|
||||
logger.debug("Current Start: %s",
|
||||
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}"
|
||||
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}}"
|
||||
if not data.empty else 'None')
|
||||
f"{data.iloc[-1]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None')
|
||||
|
||||
# Default since_ms to 30 days if nothing is given
|
||||
new_data = exchange.get_historic_ohlcv(pair=pair,
|
||||
timeframe=timeframe,
|
||||
since_ms=since_ms if since_ms else
|
||||
int((datetime.now() - timedelta(days=new_pairs_days)
|
||||
).timestamp()) * 1000,
|
||||
arrow.utcnow().shift(
|
||||
days=-new_pairs_days).int_timestamp * 1000,
|
||||
is_new_pair=data.empty,
|
||||
candle_type=candle_type,
|
||||
until_ms=until_ms if until_ms else None
|
||||
@@ -257,12 +253,10 @@ def _download_pair_history(pair: str, *,
|
||||
data = clean_ohlcv_dataframe(concat([data, new_dataframe], axis=0), timeframe, pair,
|
||||
fill_missing=False, drop_incomplete=False)
|
||||
|
||||
logger.debug("New Start: %s",
|
||||
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}"
|
||||
if not data.empty else 'None')
|
||||
logger.debug("New Start: %s",
|
||||
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}}"
|
||||
if not data.empty else 'None')
|
||||
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)
|
||||
return True
|
||||
@@ -297,7 +291,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
||||
continue
|
||||
for timeframe in timeframes:
|
||||
|
||||
logger.debug(f'Downloading pair {pair}, {candle_type}, interval {timeframe}.')
|
||||
logger.info(f'Downloading pair {pair}, interval {timeframe}.')
|
||||
process = f'{idx}/{len(pairs)}'
|
||||
_download_pair_history(pair=pair, process=process,
|
||||
datadir=datadir, exchange=exchange,
|
||||
@@ -355,7 +349,7 @@ def _download_trades_history(exchange: Exchange,
|
||||
trades = []
|
||||
|
||||
if not since:
|
||||
since = int((datetime.now() - timedelta(days=new_pairs_days)).timestamp()) * 1000
|
||||
since = arrow.utcnow().shift(days=-new_pairs_days).int_timestamp * 1000
|
||||
|
||||
from_id = trades[-1][1] if trades else None
|
||||
if trades and since < trades[-1][0]:
|
||||
@@ -486,79 +480,3 @@ def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime,
|
||||
logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values",
|
||||
pair, expected_frames, dflen, expected_frames - dflen)
|
||||
return found_missing
|
||||
|
||||
|
||||
def download_data_main(config: Config) -> None:
|
||||
|
||||
timerange = TimeRange()
|
||||
if 'days' in config:
|
||||
time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d")
|
||||
timerange = TimeRange.parse_timerange(f'{time_since}-')
|
||||
|
||||
if 'timerange' in config:
|
||||
timerange = timerange.parse_timerange(config['timerange'])
|
||||
|
||||
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||
config['stake_currency'] = ''
|
||||
|
||||
pairs_not_available: List[str] = []
|
||||
|
||||
# Init exchange
|
||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||
available_pairs = [
|
||||
p for p in exchange.get_markets(
|
||||
tradable_only=True, active_only=not config.get('include_inactive')
|
||||
).keys()
|
||||
]
|
||||
|
||||
expanded_pairs = dynamic_expand_pairlist(config, available_pairs)
|
||||
if 'timeframes' not in config:
|
||||
config['timeframes'] = DL_DATA_TIMEFRAMES
|
||||
|
||||
# Manual validations of relevant settings
|
||||
if not config['exchange'].get('skip_pair_validation', False):
|
||||
exchange.validate_pairs(expanded_pairs)
|
||||
logger.info(f"About to download pairs: {expanded_pairs}, "
|
||||
f"intervals: {config['timeframes']} to {config['datadir']}")
|
||||
|
||||
for timeframe in config['timeframes']:
|
||||
exchange.validate_timeframes(timeframe)
|
||||
|
||||
# Start downloading
|
||||
try:
|
||||
if config.get('download_trades'):
|
||||
if config.get('trading_mode') == 'futures':
|
||||
raise OperationalException("Trade download not supported for futures.")
|
||||
pairs_not_available = refresh_backtest_trades_data(
|
||||
exchange, pairs=expanded_pairs, datadir=config['datadir'],
|
||||
timerange=timerange, new_pairs_days=config['new_pairs_days'],
|
||||
erase=bool(config.get('erase')), data_format=config['dataformat_trades'])
|
||||
|
||||
# Convert downloaded trade data to different timeframes
|
||||
convert_trades_to_ohlcv(
|
||||
pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
|
||||
data_format_ohlcv=config['dataformat_ohlcv'],
|
||||
data_format_trades=config['dataformat_trades'],
|
||||
)
|
||||
else:
|
||||
if not exchange.get_option('ohlcv_has_history', True):
|
||||
raise OperationalException(
|
||||
f"Historic klines not available for {exchange.name}. "
|
||||
"Please use `--dl-trades` instead for this exchange "
|
||||
"(will unfortunately take a long time)."
|
||||
)
|
||||
migrate_binance_futures_data(config)
|
||||
pairs_not_available = refresh_backtest_ohlcv_data(
|
||||
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||
datadir=config['datadir'], timerange=timerange,
|
||||
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:
|
||||
if pairs_not_available:
|
||||
logger.info(f"Pairs [{','.join(pairs_not_available)}] not available "
|
||||
f"on exchange {exchange.name}.")
|
||||
|
||||
@@ -75,7 +75,10 @@ class JsonDataHandler(IDataHandler):
|
||||
return DataFrame(columns=self._columns)
|
||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
||||
pairdata['date'] = to_datetime(pairdata['date'],
|
||||
unit='ms',
|
||||
utc=True,
|
||||
infer_datetime_format=True)
|
||||
return pairdata
|
||||
|
||||
def ohlcv_append(
|
||||
|
||||
@@ -62,7 +62,10 @@ class ParquetDataHandler(IDataHandler):
|
||||
pairdata.columns = self._columns
|
||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
||||
pairdata['date'] = to_datetime(pairdata['date'],
|
||||
unit='ms',
|
||||
utc=True,
|
||||
infer_datetime_format=True)
|
||||
return pairdata
|
||||
|
||||
def ohlcv_append(
|
||||
|
||||
+18
-21
@@ -194,35 +194,32 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo
|
||||
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
|
||||
|
||||
|
||||
def calculate_expectancy(trades: pd.DataFrame) -> Tuple[float, float]:
|
||||
def calculate_expectancy(trades: pd.DataFrame) -> float:
|
||||
"""
|
||||
Calculate expectancy
|
||||
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
||||
:return: expectancy, expectancy_ratio
|
||||
:return: expectancy
|
||||
"""
|
||||
if len(trades) == 0:
|
||||
return 0
|
||||
|
||||
expectancy = 0
|
||||
expectancy_ratio = 100
|
||||
expectancy = 1
|
||||
|
||||
if len(trades) > 0:
|
||||
winning_trades = trades.loc[trades['profit_abs'] > 0]
|
||||
losing_trades = trades.loc[trades['profit_abs'] < 0]
|
||||
profit_sum = winning_trades['profit_abs'].sum()
|
||||
loss_sum = abs(losing_trades['profit_abs'].sum())
|
||||
nb_win_trades = len(winning_trades)
|
||||
nb_loss_trades = len(losing_trades)
|
||||
profit_sum = trades.loc[trades['profit_abs'] > 0, 'profit_abs'].sum()
|
||||
loss_sum = abs(trades.loc[trades['profit_abs'] < 0, 'profit_abs'].sum())
|
||||
nb_win_trades = len(trades.loc[trades['profit_abs'] > 0])
|
||||
nb_loss_trades = len(trades.loc[trades['profit_abs'] < 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
|
||||
winrate = (nb_win_trades / len(trades))
|
||||
loserate = (nb_loss_trades / len(trades))
|
||||
if (nb_win_trades > 0) and (nb_loss_trades > 0):
|
||||
average_win = profit_sum / nb_win_trades
|
||||
average_loss = loss_sum / nb_loss_trades
|
||||
risk_reward_ratio = average_win / average_loss
|
||||
winrate = nb_win_trades / len(trades)
|
||||
expectancy = ((1 + risk_reward_ratio) * winrate) - 1
|
||||
elif nb_win_trades == 0:
|
||||
expectancy = 0
|
||||
|
||||
expectancy = (winrate * average_win) - (loserate * average_loss)
|
||||
if (average_loss > 0):
|
||||
risk_reward_ratio = average_win / average_loss
|
||||
expectancy_ratio = ((1 + risk_reward_ratio) * winrate) - 1
|
||||
|
||||
return expectancy, expectancy_ratio
|
||||
return expectancy
|
||||
|
||||
|
||||
def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, List, NamedTuple
|
||||
|
||||
import arrow
|
||||
import numpy as np
|
||||
import utils_find_1st as utf1st
|
||||
from pandas import DataFrame
|
||||
@@ -18,7 +18,6 @@ from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_seconds
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.util import dt_now
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -80,8 +79,8 @@ class Edge:
|
||||
self._stoploss_range_step
|
||||
)
|
||||
|
||||
self._timerange: TimeRange = TimeRange.parse_timerange(
|
||||
f"{(dt_now() - timedelta(days=self._since_number_of_days)).strftime('%Y%m%d')}-")
|
||||
self._timerange: TimeRange = TimeRange.parse_timerange("%s-" % arrow.now().shift(
|
||||
days=-1 * self._since_number_of_days).format('YYYYMMDD'))
|
||||
if config.get('fee'):
|
||||
self.fee = config['fee']
|
||||
else:
|
||||
@@ -98,7 +97,7 @@ class Edge:
|
||||
heartbeat = self.edge_config.get('process_throttle_secs')
|
||||
|
||||
if (self._last_updated > 0) and (
|
||||
self._last_updated + heartbeat > int(dt_now().timestamp())):
|
||||
self._last_updated + heartbeat > arrow.utcnow().int_timestamp):
|
||||
return False
|
||||
|
||||
data: Dict[str, Any] = {}
|
||||
@@ -172,7 +171,13 @@ class Edge:
|
||||
pair_data = pair_data.sort_values(by=['date'])
|
||||
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.advise_exit(
|
||||
dataframe=self.strategy.advise_entry(
|
||||
dataframe=pair_data,
|
||||
metadata={'pair': pair}
|
||||
),
|
||||
metadata={'pair': pair}
|
||||
)[headers].copy()
|
||||
|
||||
trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)
|
||||
|
||||
@@ -184,7 +189,7 @@ class Edge:
|
||||
# Fill missing, calculable columns, profit, duration , abs etc.
|
||||
trades_df = self._fill_calculable_fields(DataFrame(trades))
|
||||
self._cached_pairs = self._process_expectancy(trades_df)
|
||||
self._last_updated = int(dt_now().timestamp())
|
||||
self._last_updated = arrow.utcnow().int_timestamp
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ class ExitType(Enum):
|
||||
EMERGENCY_EXIT = "emergency_exit"
|
||||
CUSTOM_EXIT = "custom_exit"
|
||||
PARTIAL_EXIT = "partial_exit"
|
||||
SOLD_ON_EXCHANGE = "sold_on_exchange"
|
||||
NONE = ""
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MarginMode(str, Enum):
|
||||
class MarginMode(Enum):
|
||||
"""
|
||||
Enum to distinguish between
|
||||
cross margin/futures margin_mode and
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
# flake8: noqa: F401
|
||||
# isort: off
|
||||
from freqtrade.exchange.common import remove_exchange_credentials, MAP_EXCHANGE_CHILDCLASS
|
||||
from freqtrade.exchange.common import remove_credentials, MAP_EXCHANGE_CHILDCLASS
|
||||
from freqtrade.exchange.exchange import Exchange
|
||||
# isort: on
|
||||
from freqtrade.exchange.binance import Binance
|
||||
from freqtrade.exchange.bitpanda import Bitpanda
|
||||
from freqtrade.exchange.bittrex import Bittrex
|
||||
from freqtrade.exchange.bitvavo import Bitvavo
|
||||
from freqtrade.exchange.bybit import Bybit
|
||||
from freqtrade.exchange.coinbasepro import Coinbasepro
|
||||
from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision,
|
||||
amount_to_contracts, amount_to_precision,
|
||||
available_exchanges, ccxt_exchanges,
|
||||
contracts_to_amount, date_minus_candles,
|
||||
is_exchange_known_ccxt, list_available_exchanges,
|
||||
from freqtrade.exchange.exchange_utils import (amount_to_contract_precision, amount_to_contracts,
|
||||
amount_to_precision, available_exchanges,
|
||||
ccxt_exchanges, contracts_to_amount,
|
||||
date_minus_candles, is_exchange_known_ccxt,
|
||||
market_is_active, price_to_precision,
|
||||
timeframe_to_minutes, timeframe_to_msecs,
|
||||
timeframe_to_next_date, timeframe_to_prev_date,
|
||||
timeframe_to_seconds, validate_exchange)
|
||||
timeframe_to_seconds, validate_exchange,
|
||||
validate_exchanges)
|
||||
from freqtrade.exchange.gate import Gate
|
||||
from freqtrade.exchange.hitbtc import Hitbtc
|
||||
from freqtrade.exchange.huobi import Huobi
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
""" Binance exchange subclass """
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
import ccxt
|
||||
|
||||
from freqtrade.constants import BuySell
|
||||
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange
|
||||
@@ -34,7 +36,6 @@ class Binance(Exchange):
|
||||
"tickers_have_price": False,
|
||||
"floor_leverage": True,
|
||||
"stop_price_type_field": "workingType",
|
||||
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
|
||||
"stop_price_type_value_mapping": {
|
||||
PriceType.LAST: "CONTRACT_PRICE",
|
||||
PriceType.MARK: "MARK_PRICE",
|
||||
@@ -48,6 +49,26 @@ class Binance(Exchange):
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||
]
|
||||
|
||||
def _get_params(
|
||||
self,
|
||||
side: BuySell,
|
||||
ordertype: str,
|
||||
leverage: float,
|
||||
reduceOnly: bool,
|
||||
time_in_force: str = 'GTC',
|
||||
) -> Dict:
|
||||
params = super()._get_params(side, ordertype, leverage, reduceOnly, time_in_force)
|
||||
if (
|
||||
time_in_force == 'PO'
|
||||
and ordertype != 'market'
|
||||
and self.trading_mode == TradingMode.SPOT
|
||||
# Only spot can do post only orders
|
||||
):
|
||||
params.pop('timeInForce')
|
||||
params['postOnly'] = True
|
||||
|
||||
return params
|
||||
|
||||
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
|
||||
tickers = super().get_tickers(symbols=symbols, cached=cached)
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
@@ -66,7 +87,7 @@ class Binance(Exchange):
|
||||
"""
|
||||
try:
|
||||
if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']:
|
||||
position_side = self._api.fapiPrivateGetPositionSideDual()
|
||||
position_side = self._api.fapiPrivateGetPositionsideDual()
|
||||
self._log_exchange_response('position_side_setting', position_side)
|
||||
assets_margin = self._api.fapiPrivateGetMultiAssetsMargin()
|
||||
self._log_exchange_response('multi_asset_margin', assets_margin)
|
||||
@@ -105,9 +126,8 @@ class Binance(Exchange):
|
||||
if x and x[3] and x[3][0] and x[3][0][0] > since_ms:
|
||||
# Set starting date to first available candle.
|
||||
since_ms = x[3][0][0]
|
||||
logger.info(
|
||||
f"Candle-data for {pair} available starting with "
|
||||
f"{datetime.fromtimestamp(since_ms // 1000, tz=timezone.utc).isoformat()}.")
|
||||
logger.info(f"Candle-data for {pair} available starting with "
|
||||
f"{arrow.get(since_ms // 1000).isoformat()}.")
|
||||
|
||||
return await super()._async_get_historic_ohlcv(
|
||||
pair=pair,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +0,0 @@
|
||||
"""Kucoin exchange subclass."""
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Bitvavo(Exchange):
|
||||
"""Bitvavo exchange class.
|
||||
|
||||
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||
|
||||
Please note that this exchange is not included in the list of exchanges
|
||||
officially supported by the Freqtrade development team. So some features
|
||||
may still not work as expected.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
"ohlcv_candle_limit": 1440,
|
||||
}
|
||||
@@ -28,7 +28,7 @@ class Bybit(Exchange):
|
||||
|
||||
_ft_has: Dict = {
|
||||
"ohlcv_candle_limit": 200,
|
||||
"ohlcv_has_history": True,
|
||||
"ohlcv_has_history": False,
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
"ohlcv_has_history": True,
|
||||
|
||||
@@ -4,7 +4,6 @@ import time
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, Optional, TypeVar, cast, overload
|
||||
|
||||
from freqtrade.constants import ExchangeConfig
|
||||
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
|
||||
@@ -85,22 +84,20 @@ EXCHANGE_HAS_OPTIONAL = [
|
||||
# 'fetchPositions', # Futures trading
|
||||
# 'fetchLeverageTiers', # Futures initialization
|
||||
# 'fetchMarketLeverageTiers', # Futures initialization
|
||||
# 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance...
|
||||
]
|
||||
|
||||
|
||||
def remove_exchange_credentials(exchange_config: ExchangeConfig, dry_run: bool) -> None:
|
||||
def remove_credentials(config) -> None:
|
||||
"""
|
||||
Removes exchange keys from the configuration and specifies dry-run
|
||||
Used for backtesting / hyperopt / edge and utils.
|
||||
Modifies the input dict!
|
||||
"""
|
||||
if dry_run:
|
||||
exchange_config['key'] = ''
|
||||
exchange_config['apiKey'] = ''
|
||||
exchange_config['secret'] = ''
|
||||
exchange_config['password'] = ''
|
||||
exchange_config['uid'] = ''
|
||||
if config.get('dry_run', False):
|
||||
config['exchange']['key'] = ''
|
||||
config['exchange']['secret'] = ''
|
||||
config['exchange']['password'] = ''
|
||||
config['exchange']['uid'] = ''
|
||||
|
||||
|
||||
def calculate_backoff(retrycount, max_retries):
|
||||
|
||||
+134
-208
@@ -11,6 +11,7 @@ from math import floor
|
||||
from threading import Lock
|
||||
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
|
||||
|
||||
import arrow
|
||||
import ccxt
|
||||
import ccxt.async_support as ccxt_async
|
||||
from cachetools import TTLCache
|
||||
@@ -19,30 +20,27 @@ from dateutil import parser
|
||||
from pandas import DataFrame, concat
|
||||
|
||||
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk,
|
||||
BuySell, Config, EntryExit, ExchangeConfig,
|
||||
ListPairsWithTimeframes, MakerTaker, OBLiteral, PairWithTimeframe)
|
||||
BuySell, Config, EntryExit, ListPairsWithTimeframes, MakerTaker,
|
||||
OBLiteral, PairWithTimeframe)
|
||||
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
|
||||
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
||||
from freqtrade.enums.pricetype import PriceType
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||
InvalidOrderException, OperationalException, PricingError,
|
||||
RetryableOrderError, TemporaryError)
|
||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_exchange_credentials,
|
||||
retrier, retrier_async)
|
||||
from freqtrade.exchange.exchange_utils import (ROUND, ROUND_DOWN, ROUND_UP, CcxtModuleType,
|
||||
amount_to_contract_precision, amount_to_contracts,
|
||||
amount_to_precision, contracts_to_amount,
|
||||
date_minus_candles, is_exchange_known_ccxt,
|
||||
market_is_active, price_to_precision,
|
||||
timeframe_to_minutes, timeframe_to_msecs,
|
||||
timeframe_to_next_date, timeframe_to_prev_date,
|
||||
timeframe_to_seconds)
|
||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_credentials, retrier,
|
||||
retrier_async)
|
||||
from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contract_precision,
|
||||
amount_to_contracts, amount_to_precision,
|
||||
contracts_to_amount, date_minus_candles,
|
||||
is_exchange_known_ccxt, market_is_active,
|
||||
price_to_precision, timeframe_to_minutes,
|
||||
timeframe_to_msecs, timeframe_to_next_date,
|
||||
timeframe_to_prev_date, timeframe_to_seconds)
|
||||
from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers
|
||||
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
|
||||
safe_value_fallback2)
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.util import dt_from_ts, dt_now
|
||||
from freqtrade.util.datetime_helpers import dt_humanize, dt_ts
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -61,7 +59,6 @@ class Exchange:
|
||||
# or by specifying them in the configuration.
|
||||
_ft_has_default: Dict = {
|
||||
"stoploss_on_exchange": False,
|
||||
"stop_price_param": "stopPrice",
|
||||
"order_time_in_force": ["GTC"],
|
||||
"ohlcv_params": {},
|
||||
"ohlcv_candle_limit": 500,
|
||||
@@ -80,10 +77,9 @@ class Exchange:
|
||||
"mark_ohlcv_price": "mark",
|
||||
"mark_ohlcv_timeframe": "8h",
|
||||
"ccxt_futures_name": "swap",
|
||||
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
|
||||
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
||||
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
||||
# Override createMarketBuyOrderRequiresPrice where ccxt has it wrong
|
||||
"marketOrderRequiresPrice": False,
|
||||
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
|
||||
}
|
||||
_ft_has: Dict = {}
|
||||
_ft_has_futures: Dict = {}
|
||||
@@ -92,8 +88,8 @@ class Exchange:
|
||||
# TradingMode.SPOT always supported and not required in this list
|
||||
]
|
||||
|
||||
def __init__(self, config: Config, *, exchange_config: Optional[ExchangeConfig] = None,
|
||||
validate: bool = True, load_leverage_tiers: bool = False) -> None:
|
||||
def __init__(self, config: Config, validate: bool = True,
|
||||
load_leverage_tiers: bool = False) -> None:
|
||||
"""
|
||||
Initializes this module with the given config,
|
||||
it does basic validation whether the specified exchange and pairs are valid.
|
||||
@@ -107,7 +103,8 @@ class Exchange:
|
||||
# Lock event loop. This is necessary to avoid race-conditions when using force* commands
|
||||
# Due to funding fee fetching.
|
||||
self._loop_lock = Lock()
|
||||
self.loop = self._init_async_loop()
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self._config: Config = {}
|
||||
|
||||
self._config.update(config)
|
||||
@@ -131,13 +128,13 @@ class Exchange:
|
||||
|
||||
# Holds all open sell orders for dry_run
|
||||
self._dry_run_open_orders: Dict[str, Any] = {}
|
||||
remove_credentials(config)
|
||||
|
||||
if config['dry_run']:
|
||||
logger.info('Instance is running with dry_run enabled')
|
||||
logger.info(f"Using CCXT {ccxt.__version__}")
|
||||
exchange_conf: Dict[str, Any] = exchange_config if exchange_config else config['exchange']
|
||||
remove_exchange_credentials(exchange_conf, config.get('dry_run', False))
|
||||
self.log_responses = exchange_conf.get('log_responses', False)
|
||||
exchange_config = config['exchange']
|
||||
self.log_responses = exchange_config.get('log_responses', False)
|
||||
|
||||
# Leverage properties
|
||||
self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
|
||||
@@ -152,8 +149,8 @@ class Exchange:
|
||||
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
|
||||
if exchange_conf.get('_ft_has_params'):
|
||||
self._ft_has = deep_merge_dicts(exchange_conf.get('_ft_has_params'),
|
||||
if exchange_config.get('_ft_has_params'):
|
||||
self._ft_has = deep_merge_dicts(exchange_config.get('_ft_has_params'),
|
||||
self._ft_has)
|
||||
logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
|
||||
|
||||
@@ -165,18 +162,18 @@ class Exchange:
|
||||
|
||||
# Initialize ccxt objects
|
||||
ccxt_config = self._ccxt_config
|
||||
ccxt_config = deep_merge_dicts(exchange_conf.get('ccxt_config', {}), ccxt_config)
|
||||
ccxt_config = deep_merge_dicts(exchange_conf.get('ccxt_sync_config', {}), ccxt_config)
|
||||
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config)
|
||||
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config)
|
||||
|
||||
self._api = self._init_ccxt(exchange_conf, ccxt_kwargs=ccxt_config)
|
||||
self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config)
|
||||
|
||||
ccxt_async_config = self._ccxt_config
|
||||
ccxt_async_config = deep_merge_dicts(exchange_conf.get('ccxt_config', {}),
|
||||
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}),
|
||||
ccxt_async_config)
|
||||
ccxt_async_config = deep_merge_dicts(exchange_conf.get('ccxt_async_config', {}),
|
||||
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}),
|
||||
ccxt_async_config)
|
||||
self._api_async = self._init_ccxt(
|
||||
exchange_conf, ccxt_async, ccxt_kwargs=ccxt_async_config)
|
||||
exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config)
|
||||
|
||||
logger.info(f'Using Exchange "{self.name}"')
|
||||
self.required_candle_call_count = 1
|
||||
@@ -189,8 +186,8 @@ class Exchange:
|
||||
self._startup_candle_count, config.get('timeframe', ''))
|
||||
|
||||
# Converts the interval provided in minutes in config to seconds
|
||||
self.markets_refresh_interval: int = exchange_conf.get(
|
||||
"markets_refresh_interval", 60) * 60 * 1000
|
||||
self.markets_refresh_interval: int = exchange_config.get(
|
||||
"markets_refresh_interval", 60) * 60
|
||||
|
||||
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
|
||||
self.fill_leverage_tiers()
|
||||
@@ -208,13 +205,6 @@ class Exchange:
|
||||
and self._api_async.session):
|
||||
logger.debug("Closing async ccxt session.")
|
||||
self.loop.run_until_complete(self._api_async.close())
|
||||
if self.loop and not self.loop.is_closed():
|
||||
self.loop.close()
|
||||
|
||||
def _init_async_loop(self) -> asyncio.AbstractEventLoop:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop
|
||||
|
||||
def validate_config(self, config):
|
||||
# Check if timeframe is available
|
||||
@@ -300,7 +290,7 @@ class Exchange:
|
||||
return list((self._api.timeframes or {}).keys())
|
||||
|
||||
@property
|
||||
def markets(self) -> Dict[str, Any]:
|
||||
def markets(self) -> Dict:
|
||||
"""exchange ccxt markets"""
|
||||
if not self._markets:
|
||||
logger.info("Markets were not loaded. Loading them now..")
|
||||
@@ -490,7 +480,7 @@ class Exchange:
|
||||
try:
|
||||
self._markets = self._api.load_markets(params={})
|
||||
self._load_async_markets()
|
||||
self._last_markets_refresh = dt_ts()
|
||||
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
||||
if self._ft_has['needs_trading_fees']:
|
||||
self._trading_fees = self.fetch_trading_fees()
|
||||
|
||||
@@ -501,14 +491,15 @@ class Exchange:
|
||||
"""Reload markets both sync and async if refresh interval has passed """
|
||||
# Check whether markets have to be reloaded
|
||||
if (self._last_markets_refresh > 0) and (
|
||||
self._last_markets_refresh + self.markets_refresh_interval > dt_ts()):
|
||||
self._last_markets_refresh + self.markets_refresh_interval
|
||||
> arrow.utcnow().int_timestamp):
|
||||
return None
|
||||
logger.debug("Performing scheduled market reload..")
|
||||
try:
|
||||
self._markets = self._api.load_markets(reload=True, params={})
|
||||
# Also reload async markets to avoid issues with newly listed pairs
|
||||
self._load_async_markets(reload=True)
|
||||
self._last_markets_refresh = dt_ts()
|
||||
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
||||
self.fill_leverage_tiers()
|
||||
except ccxt.BaseError:
|
||||
logger.exception("Could not reload markets.")
|
||||
@@ -739,14 +730,12 @@ class Exchange:
|
||||
"""
|
||||
return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
|
||||
|
||||
def price_to_precision(self, pair: str, price: float, *, rounding_mode: int = ROUND) -> float:
|
||||
def price_to_precision(self, pair: str, price: float) -> float:
|
||||
"""
|
||||
Returns the price rounded to the precision the Exchange accepts.
|
||||
The default price_rounding_mode in conf is ROUND.
|
||||
For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
|
||||
Returns the price rounded up to the precision the Exchange accepts.
|
||||
Rounds up
|
||||
"""
|
||||
return price_to_precision(price, self.get_precision_price(pair),
|
||||
self.precisionMode, rounding_mode=rounding_mode)
|
||||
return price_to_precision(price, self.get_precision_price(pair), self.precisionMode)
|
||||
|
||||
def price_get_one_pip(self, pair: str, price: float) -> float:
|
||||
"""
|
||||
@@ -769,12 +758,12 @@ class Exchange:
|
||||
return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage)
|
||||
|
||||
def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
|
||||
max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max', leverage)
|
||||
max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max')
|
||||
if max_stake_amount is None:
|
||||
# * Should never be executed
|
||||
raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
|
||||
'never set max_stake_amount to None')
|
||||
return max_stake_amount
|
||||
return max_stake_amount / leverage
|
||||
|
||||
def _get_stake_amount_limit(
|
||||
self,
|
||||
@@ -792,41 +781,43 @@ class Exchange:
|
||||
except KeyError:
|
||||
raise ValueError(f"Can't get market information for symbol {pair}")
|
||||
|
||||
if isMin:
|
||||
# reserve some percent defined in config (5% default) + stoploss
|
||||
margin_reserve: float = 1.0 + self._config.get('amount_reserve_percent',
|
||||
DEFAULT_AMOUNT_RESERVE_PERCENT)
|
||||
stoploss_reserve = (
|
||||
margin_reserve / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
|
||||
)
|
||||
# it should not be more than 50%
|
||||
stoploss_reserve = max(min(stoploss_reserve, 1.5), 1)
|
||||
else:
|
||||
margin_reserve = 1.0
|
||||
stoploss_reserve = 1.0
|
||||
|
||||
stake_limits = []
|
||||
limits = market['limits']
|
||||
if (limits['cost'][limit] is not None):
|
||||
stake_limits.append(
|
||||
self._contracts_to_amount(pair, limits['cost'][limit]) * stoploss_reserve
|
||||
self._contracts_to_amount(
|
||||
pair,
|
||||
limits['cost'][limit]
|
||||
)
|
||||
)
|
||||
|
||||
if (limits['amount'][limit] is not None):
|
||||
stake_limits.append(
|
||||
self._contracts_to_amount(pair, limits['amount'][limit]) * price * margin_reserve
|
||||
self._contracts_to_amount(
|
||||
pair,
|
||||
limits['amount'][limit] * price
|
||||
)
|
||||
)
|
||||
|
||||
if not stake_limits:
|
||||
return None if isMin else float('inf')
|
||||
|
||||
# reserve some percent defined in config (5% default) + stoploss
|
||||
amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent',
|
||||
DEFAULT_AMOUNT_RESERVE_PERCENT)
|
||||
amount_reserve_percent = (
|
||||
amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
|
||||
)
|
||||
# it should not be more than 50%
|
||||
amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1)
|
||||
|
||||
# The value returned should satisfy both limits: for amount (base currency) and
|
||||
# for cost (quote, stake currency), so max() is used here.
|
||||
# See also #2575 at github.
|
||||
return self._get_stake_amount_considering_leverage(
|
||||
max(stake_limits) if isMin else min(stake_limits),
|
||||
max(stake_limits) * amount_reserve_percent,
|
||||
leverage or 1.0
|
||||
)
|
||||
) if isMin else min(stake_limits)
|
||||
|
||||
def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float:
|
||||
"""
|
||||
@@ -842,8 +833,7 @@ class Exchange:
|
||||
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
rate: float, leverage: float, params: Dict = {},
|
||||
stop_loss: bool = False) -> Dict[str, Any]:
|
||||
now = dt_now()
|
||||
order_id = f'dry_run_{side}_{now.timestamp()}'
|
||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
||||
# Rounding here must respect to contract sizes
|
||||
_amount = self._contracts_to_amount(
|
||||
pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)))
|
||||
@@ -858,8 +848,8 @@ class Exchange:
|
||||
'side': side,
|
||||
'filled': 0,
|
||||
'remaining': _amount,
|
||||
'datetime': now.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||
'timestamp': dt_ts(now),
|
||||
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||
'status': "open",
|
||||
'fee': None,
|
||||
'info': {},
|
||||
@@ -867,7 +857,7 @@ class Exchange:
|
||||
}
|
||||
if stop_loss:
|
||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||
dry_order[self._ft_has['stop_price_param']] = dry_order["price"]
|
||||
dry_order["stopPrice"] = dry_order["price"]
|
||||
# Workaround to avoid filling stoploss orders immediately
|
||||
dry_order["ft_order_type"] = "stoploss"
|
||||
orderbook: Optional[OrderBook] = None
|
||||
@@ -890,7 +880,7 @@ class Exchange:
|
||||
'filled': _amount,
|
||||
'remaining': 0.0,
|
||||
'status': "closed",
|
||||
'cost': (dry_order['amount'] * average)
|
||||
'cost': (dry_order['amount'] * average) / leverage
|
||||
})
|
||||
# market orders will always incurr taker fees
|
||||
dry_order = self.add_dry_order_fee(pair, dry_order, 'taker')
|
||||
@@ -1019,7 +1009,7 @@ class Exchange:
|
||||
from freqtrade.persistence import Order
|
||||
order = Order.order_by_id(order_id)
|
||||
if order:
|
||||
ccxt_order = order.to_ccxt_object(self._ft_has['stop_price_param'])
|
||||
ccxt_order = order.to_ccxt_object()
|
||||
self._dry_run_open_orders[order_id] = ccxt_order
|
||||
return ccxt_order
|
||||
# Gracefully handle errors with dry-run orders.
|
||||
@@ -1048,13 +1038,6 @@ class Exchange:
|
||||
params.update({'reduceOnly': True})
|
||||
return params
|
||||
|
||||
def _order_needs_price(self, ordertype: str) -> bool:
|
||||
return (
|
||||
ordertype != 'market'
|
||||
or self._api.options.get("createMarketBuyOrderRequiresPrice", False)
|
||||
or self._ft_has.get('marketOrderRequiresPrice', False)
|
||||
)
|
||||
|
||||
def create_order(
|
||||
self,
|
||||
*,
|
||||
@@ -1077,7 +1060,8 @@ class Exchange:
|
||||
try:
|
||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
||||
needs_price = self._order_needs_price(ordertype)
|
||||
needs_price = (ordertype != 'market'
|
||||
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
||||
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
||||
|
||||
if not reduceOnly:
|
||||
@@ -1120,11 +1104,11 @@ class Exchange:
|
||||
"""
|
||||
if not self._ft_has.get('stoploss_on_exchange'):
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
price_param = self._ft_has['stop_price_param']
|
||||
|
||||
return (
|
||||
order.get(price_param, None) is None
|
||||
or ((side == "sell" and stop_loss > float(order[price_param])) or
|
||||
(side == "buy" and stop_loss < float(order[price_param])))
|
||||
order.get('stopPrice', None) is None
|
||||
or ((side == "sell" and stop_loss > float(order['stopPrice'])) or
|
||||
(side == "buy" and stop_loss < float(order['stopPrice'])))
|
||||
)
|
||||
|
||||
def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
|
||||
@@ -1147,8 +1131,8 @@ class Exchange:
|
||||
else:
|
||||
limit_rate = stop_price * (2 - limit_price_pct)
|
||||
|
||||
bad_stop_price = ((stop_price < limit_rate) if side ==
|
||||
"sell" else (stop_price > limit_rate))
|
||||
bad_stop_price = ((stop_price <= limit_rate) if side ==
|
||||
"sell" else (stop_price >= limit_rate))
|
||||
# Ensure rate is less than stop price
|
||||
if bad_stop_price:
|
||||
# This can for example happen if the stop / liquidation price is set to 0
|
||||
@@ -1164,8 +1148,8 @@ class Exchange:
|
||||
|
||||
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
|
||||
params = self._params.copy()
|
||||
# Verify if stopPrice works for your exchange, else configure stop_price_param
|
||||
params.update({self._ft_has['stop_price_param']: stop_price})
|
||||
# Verify if stopPrice works for your exchange!
|
||||
params.update({'stopPrice': stop_price})
|
||||
return params
|
||||
|
||||
@retrier(retries=0)
|
||||
@@ -1191,12 +1175,12 @@ class Exchange:
|
||||
|
||||
user_order_type = order_types.get('stoploss', 'market')
|
||||
ordertype, user_order_type = self._get_stop_order_type(user_order_type)
|
||||
round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP
|
||||
stop_price_norm = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
|
||||
|
||||
stop_price_norm = self.price_to_precision(pair, stop_price)
|
||||
limit_rate = None
|
||||
if user_order_type == 'limit':
|
||||
limit_rate = self._get_stop_limit_rate(stop_price, order_types, side)
|
||||
limit_rate = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
|
||||
limit_rate = self.price_to_precision(pair, limit_rate)
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
@@ -1432,47 +1416,6 @@ class Exchange:
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier(retries=0)
|
||||
def fetch_orders(self, pair: str, since: datetime) -> List[Dict]:
|
||||
"""
|
||||
Fetch all orders for a pair "since"
|
||||
:param pair: Pair for the query
|
||||
:param since: Starting time for the query
|
||||
"""
|
||||
if self._config['dry_run']:
|
||||
return []
|
||||
|
||||
def fetch_orders_emulate() -> List[Dict]:
|
||||
orders = []
|
||||
if self.exchange_has('fetchClosedOrders'):
|
||||
orders = self._api.fetch_closed_orders(pair, since=since_ms)
|
||||
if self.exchange_has('fetchOpenOrders'):
|
||||
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
|
||||
orders.extend(orders_open)
|
||||
return orders
|
||||
|
||||
try:
|
||||
since_ms = int((since.timestamp() - 10) * 1000)
|
||||
if self.exchange_has('fetchOrders'):
|
||||
try:
|
||||
orders: List[Dict] = self._api.fetch_orders(pair, since=since_ms)
|
||||
except ccxt.NotSupported:
|
||||
# Some exchanges don't support fetchOrders
|
||||
# attempt to fetch open and closed orders separately
|
||||
orders = fetch_orders_emulate()
|
||||
else:
|
||||
orders = fetch_orders_emulate()
|
||||
self._log_exchange_response('fetch_orders', orders)
|
||||
orders = [self._order_contracts_to_amount(o) for o in orders]
|
||||
return orders
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not fetch positions due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def fetch_trading_fees(self) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -1661,18 +1604,39 @@ class Exchange:
|
||||
|
||||
price_side = self._get_price_side(side, is_short, conf_strategy)
|
||||
|
||||
price_side_word = price_side.capitalize()
|
||||
|
||||
if conf_strategy.get('use_order_book', False):
|
||||
|
||||
order_book_top = conf_strategy.get('order_book_top', 1)
|
||||
if order_book is None:
|
||||
order_book = self.fetch_l2_order_book(pair, order_book_top)
|
||||
rate = self._get_rate_from_ob(pair, side, order_book, name, price_side,
|
||||
order_book_top)
|
||||
logger.debug('order_book %s', order_book)
|
||||
# top 1 = index 0
|
||||
try:
|
||||
obside: OBLiteral = 'bids' if price_side == 'bid' else 'asks'
|
||||
rate = order_book[obside][order_book_top - 1][0]
|
||||
except (IndexError, KeyError) as e:
|
||||
logger.warning(
|
||||
f"{pair} - {name} Price at location {order_book_top} from orderbook "
|
||||
f"could not be determined. Orderbook: {order_book}"
|
||||
)
|
||||
raise PricingError from e
|
||||
logger.debug(f"{pair} - {name} price from orderbook {price_side_word}"
|
||||
f"side - top {order_book_top} order book {side} rate {rate:.8f}")
|
||||
else:
|
||||
logger.debug(f"Using Last {price_side.capitalize()} / Last Price")
|
||||
logger.debug(f"Using Last {price_side_word} / Last Price")
|
||||
if ticker is None:
|
||||
ticker = self.fetch_ticker(pair)
|
||||
rate = self._get_rate_from_ticker(side, ticker, conf_strategy, price_side)
|
||||
ticker_rate = ticker[price_side]
|
||||
if ticker['last'] and ticker_rate:
|
||||
if side == 'entry' and ticker_rate > ticker['last']:
|
||||
balance = conf_strategy.get('price_last_balance', 0.0)
|
||||
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
|
||||
elif side == 'exit' and ticker_rate < ticker['last']:
|
||||
balance = conf_strategy.get('price_last_balance', 0.0)
|
||||
ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
|
||||
rate = ticker_rate
|
||||
|
||||
if rate is None:
|
||||
raise PricingError(f"{name}-Rate for {pair} was empty.")
|
||||
@@ -1681,43 +1645,6 @@ class Exchange:
|
||||
|
||||
return rate
|
||||
|
||||
def _get_rate_from_ticker(self, side: EntryExit, ticker: Ticker, conf_strategy: Dict[str, Any],
|
||||
price_side: BidAsk) -> Optional[float]:
|
||||
"""
|
||||
Get rate from ticker.
|
||||
"""
|
||||
ticker_rate = ticker[price_side]
|
||||
if ticker['last'] and ticker_rate:
|
||||
if side == 'entry' and ticker_rate > ticker['last']:
|
||||
balance = conf_strategy.get('price_last_balance', 0.0)
|
||||
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
|
||||
elif side == 'exit' and ticker_rate < ticker['last']:
|
||||
balance = conf_strategy.get('price_last_balance', 0.0)
|
||||
ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
|
||||
rate = ticker_rate
|
||||
return rate
|
||||
|
||||
def _get_rate_from_ob(self, pair: str, side: EntryExit, order_book: OrderBook, name: str,
|
||||
price_side: BidAsk, order_book_top: int) -> float:
|
||||
"""
|
||||
Get rate from orderbook
|
||||
:raises: PricingError if rate could not be determined.
|
||||
"""
|
||||
logger.debug('order_book %s', order_book)
|
||||
# top 1 = index 0
|
||||
try:
|
||||
obside: OBLiteral = 'bids' if price_side == 'bid' else 'asks'
|
||||
rate = order_book[obside][order_book_top - 1][0]
|
||||
except (IndexError, KeyError) as e:
|
||||
logger.warning(
|
||||
f"{pair} - {name} Price at location {order_book_top} from orderbook "
|
||||
f"could not be determined. Orderbook: {order_book}"
|
||||
)
|
||||
raise PricingError from e
|
||||
logger.debug(f"{pair} - {name} price from orderbook {price_side.capitalize()}"
|
||||
f"side - top {order_book_top} order book {side} rate {rate:.8f}")
|
||||
return rate
|
||||
|
||||
def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]:
|
||||
entry_rate = None
|
||||
exit_rate = None
|
||||
@@ -1858,6 +1785,9 @@ class Exchange:
|
||||
if fee_curr is None:
|
||||
return None
|
||||
fee_cost = float(fee['cost'])
|
||||
if self._ft_has['fee_cost_in_contracts']:
|
||||
# Convert cost via "contracts" conversion
|
||||
fee_cost = self._contracts_to_amount(symbol, fee['cost'])
|
||||
|
||||
# Calculate fee based on order details
|
||||
if fee_curr == self.get_pair_base_currency(symbol):
|
||||
@@ -1943,11 +1873,11 @@ class Exchange:
|
||||
logger.debug(
|
||||
"one_call: %s msecs (%s)",
|
||||
one_call,
|
||||
dt_humanize(dt_now() - timedelta(milliseconds=one_call), only_distance=True)
|
||||
arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True)
|
||||
)
|
||||
input_coroutines = [self._async_get_candle_history(
|
||||
pair, timeframe, candle_type, since) for since in
|
||||
range(since_ms, until_ms or dt_ts(), one_call)]
|
||||
range(since_ms, until_ms or (arrow.utcnow().int_timestamp * 1000), one_call)]
|
||||
|
||||
data: List = []
|
||||
# Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
|
||||
@@ -2130,7 +2060,7 @@ class Exchange:
|
||||
"""
|
||||
try:
|
||||
# Fetch OHLCV asynchronously
|
||||
s = '(' + dt_from_ts(since_ms).isoformat() + ') ' if since_ms is not None else ''
|
||||
s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
|
||||
logger.debug(
|
||||
"Fetching pair %s, %s, interval %s, since %s %s...",
|
||||
pair, candle_type, timeframe, since_ms, s
|
||||
@@ -2220,7 +2150,7 @@ class Exchange:
|
||||
logger.debug(
|
||||
"Fetching trades for pair %s, since %s %s...",
|
||||
pair, since,
|
||||
'(' + dt_from_ts(since).isoformat() + ') ' if since is not None else ''
|
||||
'(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else ''
|
||||
)
|
||||
trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
|
||||
trades = self._trades_contracts_to_amount(trades)
|
||||
@@ -2429,12 +2359,12 @@ class Exchange:
|
||||
# Must fetch the leverage tiers for each market separately
|
||||
# * This is slow(~45s) on Okx, makes ~90 api calls to load all linear swap markets
|
||||
markets = self.markets
|
||||
symbols = []
|
||||
|
||||
symbols = [
|
||||
symbol for symbol, market in markets.items()
|
||||
for symbol, market in markets.items():
|
||||
if (self.market_is_future(market)
|
||||
and market['quote'] == self._config['stake_currency'])
|
||||
]
|
||||
and market['quote'] == self._config['stake_currency']):
|
||||
symbols.append(symbol)
|
||||
|
||||
tiers: Dict[str, List[Dict]] = {}
|
||||
|
||||
@@ -2454,26 +2384,25 @@ class Exchange:
|
||||
else:
|
||||
logger.info("Using cached leverage_tiers.")
|
||||
|
||||
async def gather_results(input_coro):
|
||||
async def gather_results():
|
||||
return await asyncio.gather(*input_coro, return_exceptions=True)
|
||||
|
||||
for input_coro in chunks(coros, 100):
|
||||
|
||||
with self._loop_lock:
|
||||
results = self.loop.run_until_complete(gather_results(input_coro))
|
||||
results = self.loop.run_until_complete(gather_results())
|
||||
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning(f"Leverage tier exception: {repr(res)}")
|
||||
continue
|
||||
symbol, tier = res
|
||||
tiers[symbol] = tier
|
||||
for symbol, res in results:
|
||||
tiers[symbol] = res
|
||||
if len(coros) > 0:
|
||||
self.cache_leverage_tiers(tiers, self._config['stake_currency'])
|
||||
logger.info(f"Done initializing {len(symbols)} markets.")
|
||||
|
||||
return tiers
|
||||
return {}
|
||||
else:
|
||||
return {}
|
||||
else:
|
||||
return {}
|
||||
|
||||
def cache_leverage_tiers(self, tiers: Dict[str, List[Dict]], stake_currency: str) -> None:
|
||||
|
||||
@@ -2489,17 +2418,14 @@ class Exchange:
|
||||
def load_cached_leverage_tiers(self, stake_currency: str) -> Optional[Dict[str, List[Dict]]]:
|
||||
filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json"
|
||||
if filename.is_file():
|
||||
try:
|
||||
tiers = file_load_json(filename)
|
||||
updated = tiers.get('updated')
|
||||
if updated:
|
||||
updated_dt = parser.parse(updated)
|
||||
if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4):
|
||||
logger.info("Cached leverage tiers are outdated. Will update.")
|
||||
return None
|
||||
return tiers['data']
|
||||
except Exception:
|
||||
logger.exception("Error loading cached leverage tiers. Refreshing.")
|
||||
tiers = file_load_json(filename)
|
||||
updated = tiers.get('updated')
|
||||
if updated:
|
||||
updated_dt = parser.parse(updated)
|
||||
if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4):
|
||||
logger.info("Cached leverage tiers are outdated. Will update.")
|
||||
return None
|
||||
return tiers['data']
|
||||
return None
|
||||
|
||||
def fill_leverage_tiers(self) -> None:
|
||||
@@ -2954,8 +2880,8 @@ class Exchange:
|
||||
if nominal_value >= tier['minNotional']:
|
||||
return (tier['maintenanceMarginRate'], tier['maintAmt'])
|
||||
|
||||
raise ExchangeError("nominal value can not be lower than 0")
|
||||
raise OperationalException("nominal value can not be lower than 0")
|
||||
# The lowest notional_floor for any pair in fetch_leverage_tiers is always 0 because it
|
||||
# describes the min amt for a tier, and the lowest tier will always go down to 0
|
||||
else:
|
||||
raise ExchangeError(f"Cannot get maintenance ratio using {self.name}")
|
||||
raise OperationalException(f"Cannot get maintenance ratio using {self.name}")
|
||||
|
||||
@@ -2,18 +2,14 @@
|
||||
Exchange support utils
|
||||
"""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import ceil, floor
|
||||
from math import ceil
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import ccxt
|
||||
from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE,
|
||||
TRUNCATE, decimal_to_precision)
|
||||
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision
|
||||
|
||||
from freqtrade.exchange.common import (BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
|
||||
SUPPORTED_EXCHANGES)
|
||||
from freqtrade.types import ValidExchangesType
|
||||
from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED
|
||||
from freqtrade.util import FtPrecise
|
||||
from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts
|
||||
|
||||
|
||||
CcxtModuleType = Any
|
||||
@@ -57,41 +53,14 @@ def validate_exchange(exchange: str) -> Tuple[bool, str]:
|
||||
return True, ''
|
||||
|
||||
|
||||
def _build_exchange_list_entry(
|
||||
exchange_name: str, exchangeClasses: Dict[str, Any]) -> ValidExchangesType:
|
||||
valid, comment = validate_exchange(exchange_name)
|
||||
result: ValidExchangesType = {
|
||||
'name': exchange_name,
|
||||
'valid': valid,
|
||||
'supported': exchange_name.lower() in SUPPORTED_EXCHANGES,
|
||||
'comment': comment,
|
||||
'trade_modes': [{'trading_mode': 'spot', 'margin_mode': ''}],
|
||||
}
|
||||
if resolved := exchangeClasses.get(exchange_name.lower()):
|
||||
supported_modes = [{'trading_mode': 'spot', 'margin_mode': ''}] + [
|
||||
{'trading_mode': tm.value, 'margin_mode': mm.value}
|
||||
for tm, mm in resolved['class']._supported_trading_mode_margin_pairs
|
||||
]
|
||||
result.update({
|
||||
'trade_modes': supported_modes,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def list_available_exchanges(all_exchanges: bool) -> List[ValidExchangesType]:
|
||||
def validate_exchanges(all_exchanges: bool) -> List[Tuple[str, bool, str]]:
|
||||
"""
|
||||
:return: List of tuples with exchangename, valid, reason.
|
||||
"""
|
||||
exchanges = ccxt_exchanges() if all_exchanges else available_exchanges()
|
||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||
|
||||
subclassed = {e['name'].lower(): e for e in ExchangeResolver.search_all_objects({}, False)}
|
||||
|
||||
exchanges_valid: List[ValidExchangesType] = [
|
||||
_build_exchange_list_entry(e, subclassed) for e in exchanges
|
||||
exchanges_valid = [
|
||||
(e, *validate_exchange(e)) for e in exchanges
|
||||
]
|
||||
|
||||
return exchanges_valid
|
||||
|
||||
|
||||
@@ -129,8 +98,9 @@ def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> d
|
||||
if not date:
|
||||
date = datetime.now(timezone.utc)
|
||||
|
||||
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_DOWN) // 1000
|
||||
return dt_from_ts(new_timestamp)
|
||||
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
|
||||
ROUND_DOWN) // 1000
|
||||
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
||||
|
||||
|
||||
def timeframe_to_next_date(timeframe: str, date: Optional[datetime] = None) -> datetime:
|
||||
@@ -142,8 +112,9 @@ def timeframe_to_next_date(timeframe: str, date: Optional[datetime] = None) -> d
|
||||
"""
|
||||
if not date:
|
||||
date = datetime.now(timezone.utc)
|
||||
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_UP) // 1000
|
||||
return dt_from_ts(new_timestamp)
|
||||
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
|
||||
ROUND_UP) // 1000
|
||||
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
||||
|
||||
|
||||
def date_minus_candles(
|
||||
@@ -248,51 +219,35 @@ def amount_to_contract_precision(
|
||||
return amount
|
||||
|
||||
|
||||
def price_to_precision(
|
||||
price: float,
|
||||
price_precision: Optional[float],
|
||||
precisionMode: Optional[int],
|
||||
*,
|
||||
rounding_mode: int = ROUND,
|
||||
) -> float:
|
||||
def price_to_precision(price: float, price_precision: Optional[float],
|
||||
precisionMode: Optional[int]) -> float:
|
||||
"""
|
||||
Returns the price rounded to the precision the Exchange accepts.
|
||||
Returns the price rounded up to the precision the Exchange accepts.
|
||||
Partial Re-implementation of ccxt internal method decimal_to_precision(),
|
||||
which does not support rounding up.
|
||||
For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
|
||||
|
||||
which does not support rounding up
|
||||
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
|
||||
align with amount_to_precision().
|
||||
!!! Rounds up
|
||||
:param price: price to convert
|
||||
:param price_precision: price precision to use. Used from markets[pair]['precision']['price']
|
||||
:param precisionMode: precision mode to use. Should be used from precisionMode
|
||||
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
|
||||
:param rounding_mode: rounding mode to use. Defaults to ROUND
|
||||
:return: price rounded up to the precision the Exchange accepts
|
||||
|
||||
"""
|
||||
if price_precision is not None and precisionMode is not None:
|
||||
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
|
||||
# precision=price_precision,
|
||||
# counting_mode=self.precisionMode,
|
||||
# ))
|
||||
if precisionMode == TICK_SIZE:
|
||||
if rounding_mode == ROUND:
|
||||
ticks = price / price_precision
|
||||
rounded_ticks = round(ticks)
|
||||
return rounded_ticks * price_precision
|
||||
precision = FtPrecise(price_precision)
|
||||
price_str = FtPrecise(price)
|
||||
missing = price_str % precision
|
||||
if not missing == FtPrecise("0"):
|
||||
return round(float(str(price_str - missing + precision)), 14)
|
||||
return price
|
||||
elif precisionMode in (SIGNIFICANT_DIGITS, DECIMAL_PLACES):
|
||||
ndigits = round(price_precision)
|
||||
if rounding_mode == ROUND:
|
||||
return round(price, ndigits)
|
||||
ticks = price * (10**ndigits)
|
||||
if rounding_mode == ROUND_UP:
|
||||
return ceil(ticks) / (10**ndigits)
|
||||
if rounding_mode == TRUNCATE:
|
||||
return int(ticks) / (10**ndigits)
|
||||
if rounding_mode == ROUND_DOWN:
|
||||
return floor(ticks) / (10**ndigits)
|
||||
raise ValueError(f"Unknown rounding_mode {rounding_mode}")
|
||||
raise ValueError(f"Unknown precisionMode {precisionMode}")
|
||||
price = round(float(str(price_str - missing + precision)), 14)
|
||||
else:
|
||||
symbol_prec = price_precision
|
||||
big_price = price * pow(10, symbol_prec)
|
||||
price = ceil(big_price) / pow(10, symbol_prec)
|
||||
return price
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from freqtrade.constants import BuySell
|
||||
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.misc import safe_value_fallback2
|
||||
|
||||
@@ -27,12 +28,13 @@ class Gate(Exchange):
|
||||
"order_time_in_force": ['GTC', 'IOC'],
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
"stoploss_on_exchange": True,
|
||||
"marketOrderRequiresPrice": True,
|
||||
}
|
||||
|
||||
_ft_has_futures: Dict = {
|
||||
"needs_trading_fees": True,
|
||||
"marketOrderRequiresPrice": False,
|
||||
"tickers_have_bid_ask": False,
|
||||
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
|
||||
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
||||
"stop_price_type_field": "price_type",
|
||||
"stop_price_type_value_mapping": {
|
||||
PriceType.LAST: 0,
|
||||
@@ -48,6 +50,14 @@ class Gate(Exchange):
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||
]
|
||||
|
||||
def validate_ordertypes(self, order_types: Dict) -> None:
|
||||
|
||||
if self.trading_mode != TradingMode.FUTURES:
|
||||
if any(v == 'market' for k, v in order_types.items()):
|
||||
raise OperationalException(
|
||||
f'Exchange {self.name} does not support market orders.')
|
||||
super().validate_stop_ordertypes(order_types)
|
||||
|
||||
def _get_params(
|
||||
self,
|
||||
side: BuySell,
|
||||
|
||||
@@ -12,7 +12,6 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, Invali
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_utils import ROUND_DOWN, ROUND_UP
|
||||
from freqtrade.exchange.types import Tickers
|
||||
|
||||
|
||||
@@ -110,7 +109,6 @@ class Kraken(Exchange):
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
params.update({'reduceOnly': True})
|
||||
|
||||
round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP
|
||||
if order_types.get('stoploss', 'market') == 'limit':
|
||||
ordertype = "stop-loss-limit"
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
@@ -118,11 +116,11 @@ class Kraken(Exchange):
|
||||
limit_rate = stop_price * limit_price_pct
|
||||
else:
|
||||
limit_rate = stop_price * (2 - limit_price_pct)
|
||||
params['price2'] = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
|
||||
params['price2'] = self.price_to_precision(pair, limit_rate)
|
||||
else:
|
||||
ordertype = "stop-loss"
|
||||
|
||||
stop_price = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
|
||||
stop_price = self.price_to_precision(pair, stop_price)
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
|
||||
+33
-39
@@ -28,10 +28,10 @@ class Okx(Exchange):
|
||||
"funding_fee_timeframe": "8h",
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
"stoploss_on_exchange": True,
|
||||
"stop_price_param": "stopLossPrice",
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
"tickers_have_quoteVolume": False,
|
||||
"fee_cost_in_contracts": True,
|
||||
"stop_price_type_field": "slTriggerPxType",
|
||||
"stop_price_type_value_mapping": {
|
||||
PriceType.LAST: "last",
|
||||
@@ -124,20 +124,6 @@ class Okx(Exchange):
|
||||
params['posSide'] = self._get_posSide(side, reduceOnly)
|
||||
return params
|
||||
|
||||
def __fetch_leverage_already_set(self, pair: str, leverage: float, side: BuySell) -> bool:
|
||||
try:
|
||||
res_lev = self._api.fetch_leverage(symbol=pair, params={
|
||||
"mgnMode": self.margin_mode.value,
|
||||
"posSide": self._get_posSide(side, False),
|
||||
})
|
||||
self._log_exchange_response('get_leverage', res_lev)
|
||||
already_set = all(float(x['lever']) == leverage for x in res_lev['data'])
|
||||
return already_set
|
||||
|
||||
except ccxt.BaseError:
|
||||
# Assume all errors as "not set yet"
|
||||
return False
|
||||
|
||||
@retrier
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
|
||||
@@ -154,11 +140,8 @@ class Okx(Exchange):
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
already_set = self.__fetch_leverage_already_set(pair, leverage, side)
|
||||
if not already_set:
|
||||
raise TemporaryError(
|
||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}'
|
||||
) from e
|
||||
raise TemporaryError(
|
||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@@ -179,28 +162,28 @@ class Okx(Exchange):
|
||||
return pair_tiers[-1]['maxNotional'] / leverage
|
||||
|
||||
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
|
||||
params = super()._get_stop_params(side, ordertype, stop_price)
|
||||
|
||||
params = self._params.copy()
|
||||
# Verify if stopPrice works for your exchange!
|
||||
params.update({'stopLossPrice': stop_price})
|
||||
|
||||
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
|
||||
params['tdMode'] = self.margin_mode.value
|
||||
params['posSide'] = self._get_posSide(side, True)
|
||||
return params
|
||||
|
||||
def _convert_stop_order(self, pair: str, order_id: str, order: Dict) -> Dict:
|
||||
if (
|
||||
order['status'] == 'closed'
|
||||
and (real_order_id := order.get('info', {}).get('ordId')) is not None
|
||||
):
|
||||
# Once a order triggered, we fetch the regular followup order.
|
||||
order_reg = self.fetch_order(real_order_id, pair)
|
||||
self._log_exchange_response('fetch_stoploss_order1', order_reg)
|
||||
order_reg['id_stop'] = order_reg['id']
|
||||
order_reg['id'] = order_id
|
||||
order_reg['type'] = 'stoploss'
|
||||
order_reg['status_stop'] = 'triggered'
|
||||
return order_reg
|
||||
order = self._order_contracts_to_amount(order)
|
||||
order['type'] = 'stoploss'
|
||||
return order
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
||||
"""
|
||||
OKX uses non-default stoploss price naming.
|
||||
"""
|
||||
if not self._ft_has.get('stoploss_on_exchange'):
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
return (
|
||||
order.get('stopLossPrice', None) is None
|
||||
or ((side == "sell" and stop_loss > float(order['stopLossPrice'])) or
|
||||
(side == "buy" and stop_loss < float(order['stopLossPrice'])))
|
||||
)
|
||||
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
@@ -210,7 +193,7 @@ class Okx(Exchange):
|
||||
params1 = {'stop': True}
|
||||
order_reg = self._api.fetch_order(order_id, pair, params=params1)
|
||||
self._log_exchange_response('fetch_stoploss_order', order_reg)
|
||||
return self._convert_stop_order(pair, order_id, order_reg)
|
||||
return order_reg
|
||||
except ccxt.OrderNotFound:
|
||||
pass
|
||||
params2 = {'stop': True, 'ordType': 'conditional'}
|
||||
@@ -221,7 +204,18 @@ class Okx(Exchange):
|
||||
orders_f = [order for order in orders if order['id'] == order_id]
|
||||
if orders_f:
|
||||
order = orders_f[0]
|
||||
return self._convert_stop_order(pair, order_id, order)
|
||||
if (order['status'] == 'closed'
|
||||
and (real_order_id := order.get('info', {}).get('ordId')) is not None):
|
||||
# Once a order triggered, we fetch the regular followup order.
|
||||
order_reg = self.fetch_order(real_order_id, pair)
|
||||
self._log_exchange_response('fetch_stoploss_order1', order_reg)
|
||||
order_reg['id_stop'] = order_reg['id']
|
||||
order_reg['id'] = order_id
|
||||
order_reg['type'] = 'stoploss'
|
||||
order_reg['status_stop'] = 'triggered'
|
||||
return order_reg
|
||||
order['type'] = 'stoploss'
|
||||
return order
|
||||
except ccxt.BaseError:
|
||||
pass
|
||||
raise RetryableOrderError(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from enum import Enum
|
||||
|
||||
from gymnasium import spaces
|
||||
from gym import spaces
|
||||
|
||||
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
||||
|
||||
@@ -66,7 +66,7 @@ class Base3ActionRLEnv(BaseEnvironment):
|
||||
elif action == Actions.Sell.value and not self.can_short:
|
||||
self._update_total_profit()
|
||||
self._position = Positions.Neutral
|
||||
trade_type = "exit"
|
||||
trade_type = "neutral"
|
||||
self._last_trade_tick = None
|
||||
else:
|
||||
print("case not defined")
|
||||
@@ -74,7 +74,7 @@ class Base3ActionRLEnv(BaseEnvironment):
|
||||
if trade_type is not None:
|
||||
self.trade_history.append(
|
||||
{'price': self.current_price(), 'index': self._current_tick,
|
||||
'type': trade_type, 'profit': self.get_unrealized_profit()})
|
||||
'type': trade_type})
|
||||
|
||||
if (self._total_profit < self.max_drawdown or
|
||||
self._total_unrealized_profit < self.max_drawdown):
|
||||
@@ -94,12 +94,9 @@ class Base3ActionRLEnv(BaseEnvironment):
|
||||
|
||||
observation = self._get_observation()
|
||||
|
||||
# user can play with time if they want
|
||||
truncated = False
|
||||
|
||||
self._update_history(info)
|
||||
|
||||
return observation, step_reward, self._done, truncated, info
|
||||
return observation, step_reward, self._done, info
|
||||
|
||||
def is_tradesignal(self, action: int) -> bool:
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from enum import Enum
|
||||
|
||||
from gymnasium import spaces
|
||||
from gym import spaces
|
||||
|
||||
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
||||
|
||||
@@ -52,6 +52,16 @@ class Base4ActionRLEnv(BaseEnvironment):
|
||||
|
||||
trade_type = None
|
||||
if self.is_tradesignal(action):
|
||||
"""
|
||||
Action: Neutral, position: Long -> Close Long
|
||||
Action: Neutral, position: Short -> Close Short
|
||||
|
||||
Action: Long, position: Neutral -> Open Long
|
||||
Action: Long, position: Short -> Close Short and Open Long
|
||||
|
||||
Action: Short, position: Neutral -> Open Short
|
||||
Action: Short, position: Long -> Close Long and Open Short
|
||||
"""
|
||||
|
||||
if action == Actions.Neutral.value:
|
||||
self._position = Positions.Neutral
|
||||
@@ -59,16 +69,16 @@ class Base4ActionRLEnv(BaseEnvironment):
|
||||
self._last_trade_tick = None
|
||||
elif action == Actions.Long_enter.value:
|
||||
self._position = Positions.Long
|
||||
trade_type = "enter_long"
|
||||
trade_type = "long"
|
||||
self._last_trade_tick = self._current_tick
|
||||
elif action == Actions.Short_enter.value:
|
||||
self._position = Positions.Short
|
||||
trade_type = "enter_short"
|
||||
trade_type = "short"
|
||||
self._last_trade_tick = self._current_tick
|
||||
elif action == Actions.Exit.value:
|
||||
self._update_total_profit()
|
||||
self._position = Positions.Neutral
|
||||
trade_type = "exit"
|
||||
trade_type = "neutral"
|
||||
self._last_trade_tick = None
|
||||
else:
|
||||
print("case not defined")
|
||||
@@ -76,7 +86,7 @@ class Base4ActionRLEnv(BaseEnvironment):
|
||||
if trade_type is not None:
|
||||
self.trade_history.append(
|
||||
{'price': self.current_price(), 'index': self._current_tick,
|
||||
'type': trade_type, 'profit': self.get_unrealized_profit()})
|
||||
'type': trade_type})
|
||||
|
||||
if (self._total_profit < self.max_drawdown or
|
||||
self._total_unrealized_profit < self.max_drawdown):
|
||||
@@ -96,12 +106,9 @@ class Base4ActionRLEnv(BaseEnvironment):
|
||||
|
||||
observation = self._get_observation()
|
||||
|
||||
# user can play with time if they want
|
||||
truncated = False
|
||||
|
||||
self._update_history(info)
|
||||
|
||||
return observation, step_reward, self._done, truncated, info
|
||||
return observation, step_reward, self._done, info
|
||||
|
||||
def is_tradesignal(self, action: int) -> bool:
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from enum import Enum
|
||||
|
||||
from gymnasium import spaces
|
||||
from gym import spaces
|
||||
|
||||
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
||||
|
||||
@@ -53,6 +53,16 @@ class Base5ActionRLEnv(BaseEnvironment):
|
||||
|
||||
trade_type = None
|
||||
if self.is_tradesignal(action):
|
||||
"""
|
||||
Action: Neutral, position: Long -> Close Long
|
||||
Action: Neutral, position: Short -> Close Short
|
||||
|
||||
Action: Long, position: Neutral -> Open Long
|
||||
Action: Long, position: Short -> Close Short and Open Long
|
||||
|
||||
Action: Short, position: Neutral -> Open Short
|
||||
Action: Short, position: Long -> Close Long and Open Short
|
||||
"""
|
||||
|
||||
if action == Actions.Neutral.value:
|
||||
self._position = Positions.Neutral
|
||||
@@ -60,21 +70,21 @@ class Base5ActionRLEnv(BaseEnvironment):
|
||||
self._last_trade_tick = None
|
||||
elif action == Actions.Long_enter.value:
|
||||
self._position = Positions.Long
|
||||
trade_type = "enter_long"
|
||||
trade_type = "long"
|
||||
self._last_trade_tick = self._current_tick
|
||||
elif action == Actions.Short_enter.value:
|
||||
self._position = Positions.Short
|
||||
trade_type = "enter_short"
|
||||
trade_type = "short"
|
||||
self._last_trade_tick = self._current_tick
|
||||
elif action == Actions.Long_exit.value:
|
||||
self._update_total_profit()
|
||||
self._position = Positions.Neutral
|
||||
trade_type = "exit_long"
|
||||
trade_type = "neutral"
|
||||
self._last_trade_tick = None
|
||||
elif action == Actions.Short_exit.value:
|
||||
self._update_total_profit()
|
||||
self._position = Positions.Neutral
|
||||
trade_type = "exit_short"
|
||||
trade_type = "neutral"
|
||||
self._last_trade_tick = None
|
||||
else:
|
||||
print("case not defined")
|
||||
@@ -82,7 +92,7 @@ class Base5ActionRLEnv(BaseEnvironment):
|
||||
if trade_type is not None:
|
||||
self.trade_history.append(
|
||||
{'price': self.current_price(), 'index': self._current_tick,
|
||||
'type': trade_type, 'profit': self.get_unrealized_profit()})
|
||||
'type': trade_type})
|
||||
|
||||
if (self._total_profit < self.max_drawdown or
|
||||
self._total_unrealized_profit < self.max_drawdown):
|
||||
@@ -101,12 +111,10 @@ class Base5ActionRLEnv(BaseEnvironment):
|
||||
)
|
||||
|
||||
observation = self._get_observation()
|
||||
# user can play with time if they want
|
||||
truncated = False
|
||||
|
||||
self._update_history(info)
|
||||
|
||||
return observation, step_reward, self._done, truncated, info
|
||||
return observation, step_reward, self._done, info
|
||||
|
||||
def is_tradesignal(self, action: int) -> bool:
|
||||
"""
|
||||
|
||||
@@ -2,13 +2,13 @@ import logging
|
||||
import random
|
||||
from abc import abstractmethod
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Type, Union
|
||||
from typing import Optional, Type, Union
|
||||
|
||||
import gymnasium as gym
|
||||
import gym
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from gymnasium import spaces
|
||||
from gymnasium.utils import seeding
|
||||
from gym import spaces
|
||||
from gym.utils import seeding
|
||||
from pandas import DataFrame
|
||||
|
||||
|
||||
@@ -127,23 +127,12 @@ class BaseEnvironment(gym.Env):
|
||||
self.history: dict = {}
|
||||
self.trade_history: list = []
|
||||
|
||||
def get_attr(self, attr: str):
|
||||
"""
|
||||
Returns the attribute of the environment
|
||||
:param attr: attribute to return
|
||||
:return: attribute
|
||||
"""
|
||||
return getattr(self, attr)
|
||||
|
||||
@abstractmethod
|
||||
def set_action_space(self):
|
||||
"""
|
||||
Unique to the environment action count. Must be inherited.
|
||||
"""
|
||||
|
||||
def action_masks(self) -> List[bool]:
|
||||
return [self._is_valid(action.value) for action in self.actions]
|
||||
|
||||
def seed(self, seed: int = 1):
|
||||
self.np_random, seed = seeding.np_random(seed)
|
||||
return [seed]
|
||||
@@ -183,7 +172,7 @@ class BaseEnvironment(gym.Env):
|
||||
def reset_tensorboard_log(self):
|
||||
self.tensorboard_metrics = {}
|
||||
|
||||
def reset(self, seed=None):
|
||||
def reset(self):
|
||||
"""
|
||||
Reset is called at the beginning of every episode
|
||||
"""
|
||||
@@ -214,7 +203,7 @@ class BaseEnvironment(gym.Env):
|
||||
self.close_trade_profit = []
|
||||
self._total_unrealized_profit = 1
|
||||
|
||||
return self._get_observation(), self.history
|
||||
return self._get_observation()
|
||||
|
||||
@abstractmethod
|
||||
def step(self, action: int):
|
||||
@@ -309,12 +298,6 @@ class BaseEnvironment(gym.Env):
|
||||
"""
|
||||
An example reward function. This is the one function that users will likely
|
||||
wish to inject their own creativity into.
|
||||
|
||||
Warning!
|
||||
This is function is a showcase of functionality designed to show as many possible
|
||||
environment control features as possible. It is also designed to run quickly
|
||||
on small computers. This is a benchmark, it is *not* for live production.
|
||||
|
||||
:param action: int = The action made by the agent for the current candle.
|
||||
:return:
|
||||
float = the reward to give to the agent for current step (used for optimization
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user