Merge pull request #2656 from freqtrade/new_release

New release - 2019.11
This commit is contained in:
Matthias 2019-12-15 09:44:10 +01:00 committed by GitHub
commit 7c53dcb0af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
149 changed files with 6596 additions and 2562 deletions

View File

@ -1,6 +1,7 @@
[run] [run]
omit = omit =
scripts/* scripts/*
freqtrade/templates/*
freqtrade/vendor/* freqtrade/vendor/*
freqtrade/__main__.py freqtrade/__main__.py
tests/* tests/*

235
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,235 @@
name: Freqtrade CI
on:
push:
branches:
- master
- develop
- github_actions_tests
tags:
pull_request:
schedule:
- cron: '0 5 * * 4'
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-18.04, macos-latest ]
python-version: [3.7]
steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Cache_dependencies
uses: actions/cache@v1
id: cache
with:
path: ~/dependencies/
key: ${{ runner.os }}-dependencies
- name: pip cache (linux)
uses: actions/cache@preview
if: startsWith(matrix.os, 'ubuntu')
with:
path: ~/.cache/pip
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
- name: pip cache (macOS)
uses: actions/cache@preview
if: startsWith(matrix.os, 'macOS')
with:
path: ~/Library/Caches/pip
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
- name: TA binary *nix
if: steps.cache.outputs.cache-hit != 'true'
run: |
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
- name: Installation - *nix
run: |
python -m pip install --upgrade pip
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include
pip install -r requirements-dev.txt
pip install -e .
- name: Tests
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
COVERALLS_SERVICE_NAME: travis-ci
TRAVIS: "true"
run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
# Allow failure for coveralls
# Fake travis environment to get coveralls working correctly
export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)"
export TRAVIS_BRANCH=${GITHUB_REF#"ref/heads"}
export CI_BRANCH=${GITHUB_REF#"ref/heads"}
echo "${TRAVIS_BRANCH}"
coveralls || true
- name: Backtesting
run: |
cp config.json.example config.json
freqtrade create-userdir --userdir user_data
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
- name: Hyperopt
run: |
cp config.json.example config.json
freqtrade create-userdir --userdir user_data
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt
- name: Flake8
run: |
flake8
- name: Mypy
run: |
mypy freqtrade scripts
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}
job_name: '*Freqtrade CI ${{ matrix.os }}*'
mention: 'here'
mention_if: 'failure'
channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }}
build_windows:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ windows-latest ]
python-version: [3.7]
steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Pip cache (Windows)
uses: actions/cache@preview
if: startsWith(runner.os, 'Windows')
with:
path: ~\AppData\Local\pip\Cache
key: ${{ runner.os }}-pip
restore-keys: ${{ runner.os }}-pip
- name: Installation
run: |
./build_helpers/install_windows.ps1
- name: Tests
run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
- name: Backtesting
run: |
cp config.json.example config.json
freqtrade create-userdir --userdir user_data
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
- name: Hyperopt
run: |
cp config.json.example config.json
freqtrade create-userdir --userdir user_data
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt
- name: Flake8
run: |
flake8
- name: Mypy
run: |
mypy freqtrade scripts
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}
job_name: '*Freqtrade CI windows*'
mention: 'here'
mention_if: 'failure'
channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }}
docs_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Documentation syntax
run: |
./tests/test_docs.sh
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}
job_name: '*Freqtrade Docs*'
channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }}
deploy:
needs: [ build, build_windows, docs_check ]
runs-on: ubuntu-18.04
if: (github.event_name == 'push' || github.event_name == 'schedule') && github.repository == 'freqtrade/freqtrade'
steps:
- uses: actions/checkout@v1
- name: Extract branch name
shell: bash
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
id: extract_branch
- name: Build and test and push docker image
env:
IMAGE_NAME: freqtradeorg/freqtrade
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
run: |
build_helpers/publish_docker.sh
- name: Build raspberry image for ${{ steps.extract_branch.outputs.branch }}_pi
uses: elgohr/Publish-Docker-Github-Action@2.7
with:
name: freqtradeorg/freqtrade:${{ steps.extract_branch.outputs.branch }}_pi
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile.pi
# cache: true
cache: ${{ github.event_name != 'schedule' }}
tag_names: true
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}
job_name: '*Freqtrade CI Deploy*'
mention: 'here'
mention_if: 'failure'
channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }}

View File

@ -0,0 +1,18 @@
name: Update Docker Hub Description
on:
push:
branches:
- master
jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v2.1.0
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKERHUB_REPOSITORY: freqtradeorg/freqtrade

View File

@ -24,31 +24,34 @@ jobs:
script: script:
- pytest --random-order --cov=freqtrade --cov-config=.coveragerc - pytest --random-order --cov=freqtrade --cov-config=.coveragerc
# Allow failure for coveralls # Allow failure for coveralls
- coveralls || true # - coveralls || true
name: pytest name: pytest
- script: - script:
- cp config.json.example config.json - cp config.json.example config.json
- freqtrade --datadir tests/testdata backtesting - freqtrade create-userdir --userdir user_data
- freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
name: backtest name: backtest
- script: - script:
- cp config.json.example config.json - cp config.json.example config.json
- freqtrade --datadir tests/testdata hyperopt -e 5 - freqtrade create-userdir --userdir user_data
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt
name: hyperopt name: hyperopt
- script: flake8 - script: flake8
name: flake8 name: flake8
- script: - script:
# Test Documentation boxes - # Test Documentation boxes -
# !!! <TYPE>: is not allowed! # !!! <TYPE>: is not allowed!
- grep -Er '^!{3}\s\S+:' docs/*; test $? -ne 0 # !!! <TYPE> "title" - Title needs to be quoted!
- grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]' docs/*; test $? -ne 0
name: doc syntax name: doc syntax
- script: mypy freqtrade scripts - script: mypy freqtrade scripts
name: mypy name: mypy
- stage: docker # - stage: docker
if: branch in (master, develop, feat/improve_travis) AND (type in (push, cron)) # if: branch in (master, develop, feat/improve_travis) AND (type in (push, cron))
script: # script:
- build_helpers/publish_docker.sh # - build_helpers/publish_docker.sh
name: "Build and test and push docker image" # name: "Build and test and push docker image"
notifications: notifications:
slack: slack:

View File

@ -24,3 +24,5 @@ RUN pip install numpy --no-cache-dir \
COPY . /freqtrade/ COPY . /freqtrade/
RUN pip install -e . --no-cache-dir RUN pip install -e . --no-cache-dir
ENTRYPOINT ["freqtrade"] ENTRYPOINT ["freqtrade"]
# Default to trade mode
CMD [ "trade" ]

View File

@ -38,3 +38,4 @@ RUN ~/berryconda3/bin/pip install -e . --no-cache-dir
RUN [ "cross-build-end" ] RUN [ "cross-build-end" ]
ENTRYPOINT ["/root/berryconda3/bin/python","./freqtrade/main.py"] ENTRYPOINT ["/root/berryconda3/bin/python","./freqtrade/main.py"]
CMD [ "trade" ]

View File

@ -62,7 +62,6 @@ git checkout develop
For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/latest/installation/). For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/latest/installation/).
## Basic Usage ## Basic Usage
### Bot commands ### Bot commands
@ -106,7 +105,7 @@ optional arguments:
### Telegram RPC commands ### Telegram RPC commands
Telegram is not mandatory. However, this is a great way to control your bot. More details on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/) Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
- `/start`: Starts the trader - `/start`: Starts the trader
- `/stop`: Stops the trader - `/stop`: Stops the trader
@ -129,11 +128,6 @@ The project is currently setup in two main branches:
- `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested. - `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested.
- `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature. - `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature.
## A note on Binance
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore.
## Support ## Support
### Help / Slack ### Help / Slack

Binary file not shown.

View File

@ -0,0 +1,8 @@
# Downloads don't work automatically, since the URL is regenerated via javascript.
# Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib
# Invoke-WebRequest -Uri "https://download.lfd.uci.edu/pythonlibs/xxxxxxx/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -OutFile "TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl"
pip install build_helpers\TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl
pip install -r requirements-dev.txt
pip install -e .

View File

@ -1,17 +1,17 @@
#!/bin/sh #!/bin/sh
# - export TAG=`if [ "$TRAVIS_BRANCH" == "develop" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi`
# Replace / with _ to create a valid tag
TAG=$(echo "${TRAVIS_BRANCH}" | sed -e "s/\//_/")
# Replace / with _ to create a valid tag
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
echo "Running for ${TAG}"
# Add commit and commit_message to docker container # Add commit and commit_message to docker container
echo "${TRAVIS_COMMIT} ${TRAVIS_COMMIT_MESSAGE}" > freqtrade_commit echo "${GITHUB_SHA}" > freqtrade_commit
if [ "${TRAVIS_EVENT_TYPE}" = "cron" ]; then if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
echo "event ${TRAVIS_EVENT_TYPE}: full rebuild - skipping cache" echo "event ${GITHUB_EVENT_NAME}: full rebuild - skipping cache"
docker build -t freqtrade:${TAG} . docker build -t freqtrade:${TAG} .
else else
echo "event ${TRAVIS_EVENT_TYPE}: building with cache" echo "event ${GITHUB_EVENT_NAME}: building with cache"
# Pull last build to avoid rebuilding the whole image # Pull last build to avoid rebuilding the whole image
docker pull ${IMAGE_NAME}:${TAG} docker pull ${IMAGE_NAME}:${TAG}
docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} . docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} .
@ -23,7 +23,7 @@ if [ $? -ne 0 ]; then
fi fi
# Run backtest # Run backtest
docker run --rm -it -v $(pwd)/config.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} --datadir /tests/testdata backtesting docker run --rm -v $(pwd)/config.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy DefaultStrategy
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "failed running backtest" echo "failed running backtest"
@ -38,12 +38,12 @@ if [ $? -ne 0 ]; then
fi fi
# Tag as latest for develop builds # Tag as latest for develop builds
if [ "${TRAVIS_BRANCH}" = "develop" ]; then if [ "${TAG}" = "develop" ]; then
docker tag freqtrade:$TAG ${IMAGE_NAME}:latest docker tag freqtrade:$TAG ${IMAGE_NAME}:latest
fi fi
# Login # Login
echo "$DOCKER_PASS" | docker login -u $DOCKER_USER --password-stdin docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "failed login" echo "failed login"

View File

@ -52,6 +52,9 @@
"DOGE/BTC" "DOGE/BTC"
] ]
}, },
"pairlists": [
{"method": "StaticPairList"}
],
"edge": { "edge": {
"enabled": false, "enabled": false,
"process_throttle_secs": 3600, "process_throttle_secs": 3600,
@ -68,7 +71,7 @@
"remove_pumps": false "remove_pumps": false
}, },
"telegram": { "telegram": {
"enabled": true, "enabled": false,
"token": "your_telegram_token", "token": "your_telegram_token",
"chat_id": "your_telegram_chat_id" "chat_id": "your_telegram_chat_id"
}, },

View File

@ -37,23 +37,29 @@
"rateLimit": 200 "rateLimit": 200
}, },
"pair_whitelist": [ "pair_whitelist": [
"AST/BTC", "ALGO/BTC",
"ETC/BTC", "ATOM/BTC",
"ETH/BTC", "BAT/BTC",
"BCH/BTC",
"BRD/BTC",
"EOS/BTC", "EOS/BTC",
"ETH/BTC",
"IOTA/BTC", "IOTA/BTC",
"LINK/BTC",
"LTC/BTC", "LTC/BTC",
"MTH/BTC", "NEO/BTC",
"NCASH/BTC", "NXS/BTC",
"TNT/BTC",
"XMR/BTC", "XMR/BTC",
"XLM/BTC", "XRP/BTC",
"XRP/BTC" "XTZ/BTC"
], ],
"pair_blacklist": [ "pair_blacklist": [
"BNB/BTC" "BNB/BTC"
] ]
}, },
"pairlists": [
{"method": "StaticPairList"}
],
"edge": { "edge": {
"enabled": false, "enabled": false,
"process_throttle_secs": 3600, "process_throttle_secs": 3600,

View File

@ -50,14 +50,18 @@
"buy": "gtc", "buy": "gtc",
"sell": "gtc" "sell": "gtc"
}, },
"pairlist": { "pairlists": [
{"method": "StaticPairList"},
{
"method": "VolumePairList", "method": "VolumePairList",
"config": {
"number_assets": 20, "number_assets": 20,
"sort_key": "quoteVolume", "sort_key": "quoteVolume",
"precision_filter": false "refresh_period": 1800
}
}, },
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.01
}
],
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
"sandbox": false, "sandbox": false,

View File

@ -38,14 +38,34 @@
"rateLimit": 1000 "rateLimit": 1000
}, },
"pair_whitelist": [ "pair_whitelist": [
"ETH/EUR", "ADA/EUR",
"ATOM/EUR",
"BAT/EUR",
"BCH/EUR",
"BTC/EUR", "BTC/EUR",
"BCH/EUR" "DAI/EUR",
"DASH/EUR",
"EOS/EUR",
"ETC/EUR",
"ETH/EUR",
"LINK/EUR",
"LTC/EUR",
"QTUM/EUR",
"REP/EUR",
"WAVES/EUR",
"XLM/EUR",
"XMR/EUR",
"XRP/EUR",
"XTZ/EUR",
"ZEC/EUR"
], ],
"pair_blacklist": [ "pair_blacklist": [
] ]
}, },
"pairlists": [
{"method": "StaticPairList"}
],
"edge": { "edge": {
"enabled": false, "enabled": false,
"process_throttle_secs": 3600, "process_throttle_secs": 3600,

63
docs/advanced-hyperopt.md Normal file
View File

@ -0,0 +1,63 @@
# Advanced Hyperopt
This page explains some advanced Hyperopt topics that may require higher
coding skills and Python knowledge than creation of an ordinal hyperoptimization
class.
## Creating and using a custom loss function
To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class.
For the sample below, you then need to add the command line parameter `--hyperopt-loss SuperDuperHyperOptLoss` to your hyperopt call so this function is being used.
A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found in [userdata/hyperopts](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_loss.py).
``` python
from freqtrade.optimize.hyperopt import IHyperOptLoss
TARGET_TRADES = 600
EXPECTED_MAX_PROFIT = 3.0
MAX_ACCEPTED_TRADE_DURATION = 300
class SuperDuperHyperOptLoss(IHyperOptLoss):
"""
Defines the default loss function for hyperopt
"""
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
*args, **kwargs) -> float:
"""
Objective function, returns smaller number for better results
This is the legacy algorithm (used until now in freqtrade).
Weights are distributed as follows:
* 0.4 to trade duration
* 0.25: Avoiding trade loss
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
"""
total_profit = results.profit_percent.sum()
trade_duration = results.trade_duration.mean()
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
result = trade_loss + profit_loss + duration_loss
return result
```
Currently, the arguments are:
* `results`: DataFrame containing the result
The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`):
`pair, profit_percent, profit_abs, open_time, close_time, open_index, close_index, trade_duration, open_at_end, open_rate, close_rate, sell_reason`
* `trade_count`: Amount of trades (identical to `len(results)`)
* `min_date`: Start date of the hyperopting TimeFrame
* `min_date`: End date of the hyperopting TimeFrame
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.
!!! Note
This function is called once per iteration - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily.
!!! Note
Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later.

92
docs/advanced-setup.md Normal file
View File

@ -0,0 +1,92 @@
# Advanced Post-installation Tasks
This page explains some advanced tasks and configuration options that can be performed after the bot installation and may be uselful in some environments.
If you do not know what things mentioned here mean, you probably do not need it.
## Configure the bot running as a systemd service
Copy the `freqtrade.service` file to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
!!! Note
Certain systems (like Raspbian) don't load service unit files from the user directory. In this case, copy `freqtrade.service` into `/etc/systemd/user/` (requires superuser permissions).
After that you can start the daemon with:
```bash
systemctl --user start freqtrade
```
For this to be persistent (run when user is logged out) you'll need to enable `linger` for your freqtrade user.
```bash
sudo loginctl enable-linger "$USER"
```
If you run the bot as a service, you can use systemd service manager as a software watchdog monitoring freqtrade bot
state and restarting it in the case of failures. If the `internals.sd_notify` parameter is set to true in the
configuration or the `--sd-notify` command line option is used, the bot will send keep-alive ping messages to systemd
using the sd_notify (systemd notifications) protocol and will also tell systemd its current state (Running or Stopped)
when it changes.
The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd
as the watchdog.
!!! Note
The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container.
## Advanced Logging
On many Linux systems the bot can be configured to send its log messages to `syslog` or `journald` system services. Logging to a remote `syslog` server is also available on Windows. The special values for the `--logfilename` command line option can be used for this.
### Logging to syslog
To send Freqtrade log messages to a local or remote `syslog` service use the `--logfilename` command line option with the value in the following format:
* `--logfilename syslog:<syslog_address>` -- send log messages to `syslog` service using the `<syslog_address>` as the syslog address.
The syslog address can be either a Unix domain socket (socket filename) or a UDP socket specification, consisting of IP address and UDP port, separated by the `:` character.
So, the following are the examples of possible usages:
* `--logfilename syslog:/dev/log` -- log to syslog (rsyslog) using the `/dev/log` socket, suitable for most systems.
* `--logfilename syslog` -- same as above, the shortcut for `/dev/log`.
* `--logfilename syslog:/var/run/syslog` -- log to syslog (rsyslog) using the `/var/run/syslog` socket. Use this on MacOS.
* `--logfilename syslog:localhost:514` -- log to local syslog using UDP socket, if it listens on port 514.
* `--logfilename syslog:<ip>:514` -- log to remote syslog at IP address and port 514. This may be used on Windows for remote logging to an external syslog server.
Log messages are send to `syslog` with the `user` facility. So you can see them with the following commands:
* `tail -f /var/log/user`, or
* install a comprehensive graphical viewer (for instance, 'Log File Viewer' for Ubuntu).
On many systems `syslog` (`rsyslog`) fetches data from `journald` (and vice versa), so both `--logfilename syslog` or `--logfilename journald` can be used and the messages be viewed with both `journalctl` and a syslog viewer utility. You can combine this in any way which suites you better.
For `rsyslog` the messages from the bot can be redirected into a separate dedicated log file. To achieve this, add
```
if $programname startswith "freqtrade" then -/var/log/freqtrade.log
```
to one of the rsyslog configuration files, for example at the end of the `/etc/rsyslog.d/50-default.conf`.
For `syslog` (`rsyslog`), the reduction mode can be switched on. This will reduce the number of repeating messages. For instance, multiple bot Heartbeat messages will be reduced to a single message when nothing else happens with the bot. To achieve this, set in `/etc/rsyslog.conf`:
```
# Filter duplicated messages
$RepeatedMsgReduction on
```
### Logging to journald
This needs the `systemd` python package installed as the dependency, which is not available on Windows. Hence, the whole journald logging functionality is not available for a bot running on Windows.
To send Freqtrade log messages to `journald` system service use the `--logfilename` command line option with the value in the following format:
* `--logfilename journald` -- send log messages to `journald`.
Log messages are send to `journald` with the `user` facility. So you can see them with the following commands:
* `journalctl -f` -- shows Freqtrade log messages sent to `journald` along with other log messages fetched by `journald`.
* `journalctl -f -u freqtrade.service` -- this command can be used when the bot is run as a `systemd` service.
There are many other options in the `journalctl` utility to filter the messages, see manual pages for this utility.
On many systems `syslog` (`rsyslog`) fetches data from `journald` (and vice versa), so both `--logfilename syslog` or `--logfilename journald` can be used and the messages be viewed with both `journalctl` and a syslog viewer utility. You can combine this in any way which suites you better.

View File

@ -11,14 +11,15 @@ Now you have good Buy and Sell strategies and some historic data, you want to te
real data. This is what we call real data. This is what we call
[backtesting](https://en.wikipedia.org/wiki/Backtesting). [backtesting](https://en.wikipedia.org/wiki/Backtesting).
Backtesting will use the crypto-currencies (pairs) from your config file Backtesting will use the crypto-currencies (pairs) from your config file and load ticker data from `user_data/data/<exchange>` by default.
and load ticker data from `user_data/data/<exchange>` by default. If no data is available for the exchange / pair / ticker interval combination, backtesting will ask you to download them first using `freqtrade download-data`.
If no data is available for the exchange / pair / ticker interval combination, backtesting will
ask you to download them first using `freqtrade download-data`.
For details on downloading, please refer to the [Data Downloading](data-download.md) section in the documentation. For details on downloading, please refer to the [Data Downloading](data-download.md) section in the documentation.
The result of backtesting will confirm if your bot has better odds of making a profit than a loss. The result of backtesting will confirm if your bot has better odds of making a profit than a loss.
!!! Tip "Using dynamic pairlists for backtesting"
While using dynamic pairlists during backtesting is not possible, a dynamic pairlist using current data can be generated via the [`test-pairlist`](utils.md#test-pairlist) command, and needs to be specified as `"pair_whitelist"` attribute in the configuration.
### Run a backtesting against the currencies listed in your config file ### Run a backtesting against the currencies listed in your config file
#### With 5 min tickers (Per default) #### With 5 min tickers (Per default)
@ -45,7 +46,7 @@ freqtrade --datadir user_data/data/bittrex-20180101 backtesting
#### With a (custom) strategy file #### With a (custom) strategy file
```bash ```bash
freqtrade -s SampleStrategy backtesting freqtrade backtesting -s SampleStrategy
``` ```
Where `-s SampleStrategy` refers to the class name within the strategy file `sample_strategy.py` found in the `freqtrade/user_data/strategies` directory. Where `-s SampleStrategy` refers to the class name within the strategy file `sample_strategy.py` found in the `freqtrade/user_data/strategies` directory.
@ -72,6 +73,8 @@ The exported trades can be used for [further analysis](#further-backtest-result-
freqtrade backtesting --export trades --export-filename=backtest_samplestrategy.json freqtrade backtesting --export trades --export-filename=backtest_samplestrategy.json
``` ```
Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period).
#### Supplying custom fee value #### Supplying custom fee value
Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt. Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt.

View File

@ -5,20 +5,18 @@ This page explains the different parameters of the bot and how to run it.
!!! Note !!! Note
If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands. If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands.
## Bot commands ## Bot commands
``` ```
usage: freqtrade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] usage: freqtrade [-h] [-V]
[--userdir PATH] [-s NAME] [--strategy-path PATH] {trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit}
[--db-url PATH] [--sd-notify]
{backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit}
... ...
Free, open source crypto trading bot Free, open source crypto trading bot
positional arguments: positional arguments:
{backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit} {trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit}
trade Trade module.
backtesting Backtesting module. backtesting Backtesting module.
edge Edge module. edge Edge module.
hyperopt Hyperopt module. hyperopt Hyperopt module.
@ -32,6 +30,27 @@ positional arguments:
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-V, --version show program's version number and exit
```
### Bot trading commands
```
usage: freqtrade trade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH]
[--db-url PATH] [--sd-notify] [--dry-run]
optional arguments:
-h, --help show this help message and exit
--db-url PATH Override trades database URL, this is useful in custom
deployments (default: `sqlite:///tradesv3.sqlite` for
Live Run mode, `sqlite://` for Dry Run).
--sd-notify Notify systemd service manager.
--dry-run Enforce dry-run for trading (removes Exchange secrets
and simulates trades).
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. --logfile FILE Log to the file specified.
-V, --version show program's version number and exit -V, --version show program's version number and exit
@ -43,15 +62,12 @@ optional arguments:
Path to directory with historical backtesting data. Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH --userdir PATH, --user-data-dir PATH
Path to userdata directory. Path to userdata directory.
-s NAME, --strategy NAME
Specify strategy class name (default:
`DefaultStrategy`).
--strategy-path PATH Specify additional strategy lookup path.
--db-url PATH Override trades database URL, this is useful in custom
deployments (default: `sqlite:///tradesv3.sqlite` for
Live Run mode, `sqlite://` for Dry Run).
--sd-notify Notify systemd service manager.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
``` ```
### How to specify which configuration file be used? ### How to specify which configuration file be used?
@ -60,7 +76,7 @@ The bot allows you to select which configuration file you want to use by means o
the `-c/--config` command line option: the `-c/--config` command line option:
```bash ```bash
freqtrade -c path/far/far/away/config.json freqtrade trade -c path/far/far/away/config.json
``` ```
Per default, the bot loads the `config.json` configuration file from the current Per default, the bot loads the `config.json` configuration file from the current
@ -73,22 +89,22 @@ The bot allows you to use multiple configuration files by specifying multiple
defined in the latter configuration files override parameters with the same name defined in the latter configuration files override parameters with the same name
defined in the previous configuration files specified in the command line earlier. defined in the previous configuration files specified in the command line earlier.
For example, you can make a separate configuration file with your key and secrete For example, you can make a separate configuration file with your key and secret
for the Exchange you use for trading, specify default configuration file with for the Exchange you use for trading, specify default configuration file with
empty key and secrete values while running in the Dry Mode (which does not actually empty key and secret values while running in the Dry Mode (which does not actually
require them): require them):
```bash ```bash
freqtrade -c ./config.json freqtrade trade -c ./config.json
``` ```
and specify both configuration files when running in the normal Live Trade Mode: and specify both configuration files when running in the normal Live Trade Mode:
```bash ```bash
freqtrade -c ./config.json -c path/to/secrets/keys.config.json freqtrade trade -c ./config.json -c path/to/secrets/keys.config.json
``` ```
This could help you hide your private Exchange key and Exchange secrete on you local machine This could help you hide your private Exchange key and Exchange secret on you local machine
by setting appropriate file permissions for the file which contains actual secrets and, additionally, by setting appropriate file permissions for the file which contains actual secrets and, additionally,
prevent unintended disclosure of sensitive private data when you publish examples prevent unintended disclosure of sensitive private data when you publish examples
of your configuration in the project issues or in the Internet. of your configuration in the project issues or in the Internet.
@ -134,7 +150,7 @@ In `user_data/strategies` you have a file `my_awesome_strategy.py` which has
a strategy class called `AwesomeStrategy` to load it: a strategy class called `AwesomeStrategy` to load it:
```bash ```bash
freqtrade --strategy AwesomeStrategy freqtrade trade --strategy AwesomeStrategy
``` ```
If the bot does not find your strategy file, it will display in an error If the bot does not find your strategy file, it will display in an error
@ -149,7 +165,7 @@ This parameter allows you to add an additional strategy lookup path, which gets
checked before the default locations (The passed path must be a directory!): checked before the default locations (The passed path must be a directory!):
```bash ```bash
freqtrade --strategy AwesomeStrategy --strategy-path /some/directory freqtrade trade --strategy AwesomeStrategy --strategy-path /some/directory
``` ```
#### How to install a strategy? #### How to install a strategy?
@ -165,7 +181,7 @@ using `--db-url`. This can also be used to specify a custom database
in production mode. Example command: in production mode. Example command:
```bash ```bash
freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite freqtrade trade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
``` ```
## Backtesting commands ## Backtesting commands
@ -173,8 +189,10 @@ freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
Backtesting also uses the config specified via `-c/--config`. Backtesting also uses the config specified via `-c/--config`.
``` ```
usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--max_open_trades INT] [-d PATH] [--userdir PATH] [-s NAME]
[--strategy-path PATH] [-i TICKER_INTERVAL]
[--timerange TIMERANGE] [--max_open_trades INT]
[--stake_amount STAKE_AMOUNT] [--fee FLOAT] [--stake_amount STAKE_AMOUNT] [--fee FLOAT]
[--eps] [--dmmp] [--eps] [--dmmp]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
@ -211,11 +229,29 @@ optional arguments:
--export EXPORT Export backtest results, argument are: trades. --export EXPORT Export backtest results, argument are: trades.
Example: `--export=trades` Example: `--export=trades`
--export-filename PATH --export-filename PATH
Save backtest results to the file with this filename Save backtest results to the file with this filename.
(default: `user_data/backtest_results/backtest- Requires `--export` to be set as well. Example:
result.json`). Requires `--export` to be set as well. `--export-filename=user_data/backtest_results/backtest
Example: `--export-filename=user_data/backtest_results _today.json`
/backtest_today.json`
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default: `config.json`).
Multiple --config options may be used. Can be set to
`-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
``` ```
@ -223,7 +259,7 @@ optional arguments:
The first time your run Backtesting, you will need to download some historic data first. The first time your run Backtesting, you will need to download some historic data first.
This can be accomplished by using `freqtrade download-data`. This can be accomplished by using `freqtrade download-data`.
Check the corresponding [help page section](backtesting.md#Getting-data-for-backtesting-and-hyperopt) for more details Check the corresponding [Data Downloading](data-download.md) section for more details
## Hyperopt commands ## Hyperopt commands
@ -231,12 +267,14 @@ To optimize your strategy, you can use hyperopt parameter hyperoptimization
to find optimal parameter values for your stategy. to find optimal parameter values for your stategy.
``` ```
usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH]
[-i TICKER_INTERVAL] [--timerange TIMERANGE]
[--max_open_trades INT] [--max_open_trades INT]
[--stake_amount STAKE_AMOUNT] [--fee FLOAT] [--stake_amount STAKE_AMOUNT] [--fee FLOAT]
[--customhyperopt NAME] [--hyperopt-path PATH] [--hyperopt NAME] [--hyperopt-path PATH] [--eps]
[--eps] [-e INT] [-e INT]
[-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [--spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
[--dmmp] [--print-all] [--no-color] [--print-json] [--dmmp] [--print-all] [--no-color] [--print-json]
[-j JOBS] [--random-state INT] [--min-trades INT] [-j JOBS] [--random-state INT] [--min-trades INT]
[--continue] [--hyperopt-loss NAME] [--continue] [--hyperopt-loss NAME]
@ -254,16 +292,15 @@ optional arguments:
Specify stake_amount. Specify stake_amount.
--fee FLOAT Specify fee ratio. Will be applied twice (on trade --fee FLOAT Specify fee ratio. Will be applied twice (on trade
entry and exit). entry and exit).
--customhyperopt NAME --hyperopt NAME Specify hyperopt class name which will be used by the
Specify hyperopt class name (default: bot.
`DefaultHyperOpt`). --hyperopt-path PATH Specify additional lookup path for Hyperopt and
--hyperopt-path PATH Specify additional lookup path for Hyperopts and
Hyperopt Loss functions. Hyperopt Loss functions.
--eps, --enable-position-stacking --eps, --enable-position-stacking
Allow buying the same pair multiple times (position Allow buying the same pair multiple times (position
stacking). stacking).
-e INT, --epochs INT Specify number of epochs (default: 100). -e INT, --epochs INT Specify number of epochs (default: 100).
-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...] --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]
Specify which parameters to hyperopt. Space-separated Specify which parameters to hyperopt. Space-separated
list. Default: `all`. list. Default: `all`.
--dmmp, --disable-max-market-positions --dmmp, --disable-max-market-positions
@ -292,8 +329,27 @@ optional arguments:
generate completely different results, since the generate completely different results, since the
target for optimization is different. Built-in target for optimization is different. Built-in
Hyperopt-loss-functions are: DefaultHyperOptLoss, Hyperopt-loss-functions are: DefaultHyperOptLoss,
OnlyProfitHyperOptLoss, SharpeHyperOptLoss.(default: OnlyProfitHyperOptLoss, SharpeHyperOptLoss (default:
`DefaultHyperOptLoss`). `DefaultHyperOptLoss`).
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default: `config.json`).
Multiple --config options may be used. Can be set to
`-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
``` ```
## Edge commands ## Edge commands
@ -301,7 +357,9 @@ optional arguments:
To know your trade expectancy and winrate against historical data, you can use Edge. To know your trade expectancy and winrate against historical data, you can use Edge.
``` ```
usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH]
[-i TICKER_INTERVAL] [--timerange TIMERANGE]
[--max_open_trades INT] [--stake_amount STAKE_AMOUNT] [--max_open_trades INT] [--stake_amount STAKE_AMOUNT]
[--fee FLOAT] [--stoplosses STOPLOSS_RANGE] [--fee FLOAT] [--stoplosses STOPLOSS_RANGE]
@ -324,6 +382,24 @@ optional arguments:
(without any space). Example: (without any space). Example:
`--stoplosses=-0.01,-0.1,-0.001` `--stoplosses=-0.01,-0.1,-0.001`
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default: `config.json`).
Multiple --config options may be used. Can be set to
`-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
``` ```
To understand edge and how to read the results, please read the [edge documentation](edge.md). To understand edge and how to read the results, please read the [edge documentation](edge.md).

View File

@ -38,85 +38,92 @@ The prevelance for all Options is as follows:
Mandatory parameters are marked as **Required**, which means that they are required to be set in one of the possible ways. Mandatory parameters are marked as **Required**, which means that they are required to be set in one of the possible ways.
| Command | Default | Description | | Command | Description |
|----------|---------|-------------| |----------|-------------|
| `max_open_trades` | 3 | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) | `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades).<br> ***Datatype:*** *Positive integer or -1.*
| `stake_currency` | BTC | **Required.** Crypto-currency used for trading. | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *String*
| `stake_amount` | 0.05 | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. | `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](#understand-stake_amount). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Positive float or `"unlimited"`.*
| `amount_reserve_percent` | 0.05 | Reserve some amount in min pair stake amount. Default is 5%. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals. | `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.*
| `ticker_interval` | [1m, 5m, 15m, 30m, 1h, 1d, ...] | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). | `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *String*
| `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below. | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> ***Datatype:*** *String*
| `dry_run` | true | **Required.** Define if the bot must be in Dry-run or production mode. | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> ***Datatype:*** *Boolean*
| `dry_run_wallet` | 999.9 | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. | `dry_run_wallet` | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. <br> ***Datatype:*** *Float*
| `process_only_new_candles` | false | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> ***Datatype:*** *Boolean*
| `minimal_roi` | See below | Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy). | `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Dict*
| `stoploss` | -0.10 | Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). | `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Float (as ratio)*
| `trailing_stop` | false | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). | `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Boolean*
| `trailing_stop_positive` | 0 | Changes stop-loss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). | `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Float*
| `trailing_stop_positive_offset` | 0 | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). | `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). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> ***Datatype:*** *Float*
| `trailing_only_offset_is_reached` | false | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). | `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*
| `unfilledtimeout.buy` | 10 | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. | `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. <br> ***Datatype:*** *Integer*
| `unfilledtimeout.sell` | 10 | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. | `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. <br> ***Datatype:*** *Integer*
| `bid_strategy.ask_last_balance` | 0.0 | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). | `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance).
| `bid_strategy.use_order_book` | false | Allows buying of pair using the rates in Order Book Bids. | `bid_strategy.use_order_book` | Enable buying using the rates in Order Book Bids. <br> ***Datatype:*** *Boolean*
| `bid_strategy.order_book_top` | 0 | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. | `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. *Defaults to `1`.* <br> ***Datatype:*** *Positive Integer*
| `bid_strategy. check_depth_of_market.enabled` | false | Does not buy if the % difference of buy orders and sell orders is met in Order Book. | `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. <br>*Defaults to `false`.* <br> ***Datatype:*** *Boolean*
| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | 0 | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. | `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. *Defaults to `0`.* <br> ***Datatype:*** *Float (as ratio)*
| `ask_strategy.use_order_book` | false | Allows selling of open traded pair using the rates in Order Book Asks. | `ask_strategy.use_order_book` | Enable selling of open trades using Order Book Asks. <br> ***Datatype:*** *Boolean*
| `ask_strategy.order_book_min` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. | `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> ***Datatype:*** *Positive Integer*
| `ask_strategy.order_book_max` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. | `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> ***Datatype:*** *Positive Integer*
| `ask_strategy.use_sell_signal` | true | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). | `ask_strategy.use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> ***Datatype:*** *Boolean*
| `ask_strategy.sell_profit_only` | false | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). | `ask_strategy.sell_profit_only` | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> ***Datatype:*** *Boolean*
| `ask_strategy.ignore_roi_if_buy_signal` | false | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). | `ask_strategy.ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> ***Datatype:*** *Boolean*
| `order_types` | None | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy). | `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> ***Datatype:*** *Dict*
| `order_time_in_force` | None | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). | `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Dict*
| `exchange.name` | | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). | `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> ***Datatype:*** *String*
| `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. | `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.<br> ***Datatype:*** *Boolean*
| `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.*** | `exchange.key` | API key to use for the exchange. Only required when you are in production mode. **Keep it in secret, do not disclose publicly.** <br> ***Datatype:*** *String*
| `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.*** | `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode. **Keep it in secret, do not disclose publicly.** <br> ***Datatype:*** *String*
| `exchange.password` | '' | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. ***Keep it in secrete, do not disclose publicly.*** | `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. **Keep it in secret, do not disclose publicly.** <br> ***Datatype:*** *String*
| `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). | `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#dynamic-pairlists)). <br> ***Datatype:*** *List*
| `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). | `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)). <br> ***Datatype:*** *List*
| `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular 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) | `exchange.ccxt_config` | Additional CCXT parameters passed to the regular 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` | None | 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) | `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` | 60 | The interval in minutes in which markets are reloaded. | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> ***Datatype:*** *Positive Integer*
| `edge` | false | Please refer to [edge configuration document](edge.md) for detailed explanation. | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation.
| `experimental.block_bad_exchanges` | true | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. | `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*
| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists). | `pairlists` | Define one or more pairlists to be used. [More information below](#dynamic-pairlists). <br>*Defaults to `StaticPairList`.* <br> ***Datatype:*** *List of Dicts*
| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists). | `telegram.enabled` | Enable the usage of Telegram. <br> ***Datatype:*** *Boolean*
| `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram. | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.** <br> ***Datatype:*** *String*
| `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.*** | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.** <br> ***Datatype:*** *String*
| `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.*** | `webhook.enabled` | Enable usage of Webhook notifications <br> ***Datatype:*** *Boolean*
| `webhook.enabled` | false | Enable usage of Webhook notifications | `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> ***Datatype:*** *String*
| `webhook.url` | false | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. | `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. <br> ***Datatype:*** *String*
| `webhook.webhookbuy` | false | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. | `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. <br> ***Datatype:*** *String*
| `webhook.webhooksell` | false | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. | `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. <br> ***Datatype:*** *String*
| `webhook.webhookstatus` | false | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. | `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details. <br> ***Datatype:*** *Boolean*
| `db_url` | `sqlite:///tradesv3.sqlite`| Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`. | `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> ***Datatype:*** *IPv4*
| `initial_state` | running | Defines the initial application state. More information below. | `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details. <br> ***Datatype:*** *Integer between 1024 and 65535*
| `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below. | `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**<br> ***Datatype:*** *String*
| `strategy` | DefaultStrategy | Defines Strategy class to use. | `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**<br> ***Datatype:*** *String*
| `strategy_path` | null | Adds an additional strategy lookup path (must be a directory). | `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances. <br> ***Datatype:*** *String, SQLAlchemy connect string*
| `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. | `initial_state` | Defines the initial application state. More information below. <br>*Defaults to `stopped`.* <br> ***Datatype:*** *Enum, either `stopped` or `running`*
| `internals.heartbeat_interval` | 60 | Print heartbeat message every X seconds. Set to 0 to disable heartbeat messages. | `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. <br> ***Datatype:*** *Boolean*
| `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. | `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> ***Datatype:*** *ClassName*
| `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. | `strategy_path` | Adds an additional strategy lookup path (must be a directory). <br> ***Datatype:*** *String*
| `user_data_dir` | cwd()/user_data | Directory containing user data. Defaults to `./user_data/`. | `internals.process_throttle_secs` | Set the process throttle. Value in second. <br>*Defaults to `5` seconds.* <br> ***Datatype:*** *Positive Integer*
| `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages. <br>*Defaults to `60` seconds.* <br> ***Datatype:*** *Positive Integer or 0*
| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> ***Datatype:*** *Boolean*
| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> ***Datatype:*** *String*
| `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> ***Datatype:*** *String*
### Parameters in the strategy ### Parameters in the strategy
The following parameters can be set in either configuration file or strategy. The following parameters can be set in either configuration file or strategy.
Values set in the configuration file always overwrite values set in the strategy. Values set in the configuration file always overwrite values set in the strategy.
* `ticker_interval`
* `minimal_roi` * `minimal_roi`
* `ticker_interval`
* `stoploss` * `stoploss`
* `trailing_stop` * `trailing_stop`
* `trailing_stop_positive` * `trailing_stop_positive`
* `trailing_stop_positive_offset` * `trailing_stop_positive_offset`
* `trailing_only_offset_is_reached`
* `process_only_new_candles` * `process_only_new_candles`
* `order_types` * `order_types`
* `order_time_in_force` * `order_time_in_force`
* `stake_currency`
* `stake_amount`
* `use_sell_signal` (ask_strategy) * `use_sell_signal` (ask_strategy)
* `sell_profit_only` (ask_strategy) * `sell_profit_only` (ask_strategy)
* `ignore_roi_if_buy_signal` (ask_strategy) * `ignore_roi_if_buy_signal` (ask_strategy)
@ -124,18 +131,22 @@ Values set in the configuration file always overwrite values set in the strategy
### Understand stake_amount ### Understand stake_amount
The `stake_amount` configuration parameter is an amount of crypto-currency your bot will use for each trade. The `stake_amount` configuration parameter is an amount of crypto-currency your bot will use for each trade.
The minimal value is 0.0005. If there is not enough crypto-currency in
the account an exception is generated. The minimal configuration value is 0.0001. Please check your exchange's trading minimums to avoid problems.
This setting works in combination with `max_open_trades`. The maximum capital engaged in trades is `stake_amount * max_open_trades`.
For example, the bot will at most use (0.05 BTC x 3) = 0.15 BTC, assuming a configuration of `max_open_trades=3` and `stake_amount=0.05`.
To allow the bot to trade all the available `stake_currency` in your account set To allow the bot to trade all the available `stake_currency` in your account set
```json ```json
"stake_amount" : "unlimited", "stake_amount" : "unlimited",
``` ```
In this case a trade amount is calclulated as: In this case a trade amount is calculated as:
```python ```python
currency_balanse / (max_open_trades - current_open_trades) currency_balance / (max_open_trades - current_open_trades)
``` ```
### Understand minimal_roi ### Understand minimal_roi
@ -215,6 +226,11 @@ If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
`emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails. `emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails.
The below is the default which is used if this is not configured in either strategy or configuration file. The below is the default which is used if this is not configured in either strategy or configuration file.
Since `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1%.
Calculation example: we bought the asset at 100$.
Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$.
Syntax for Strategy: Syntax for Strategy:
```python ```python
@ -224,7 +240,8 @@ order_types = {
"emergencysell": "market", "emergencysell": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": False, "stoploss_on_exchange": False,
"stoploss_on_exchange_interval": 60 "stoploss_on_exchange_interval": 60,
"stoploss_on_exchange_limit_ratio": 0.99,
} }
``` ```
@ -254,7 +271,7 @@ Configuration:
!!! Note !!! Note
If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order. If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order.
!!! Warning stoploss_on_exchange failures !!! Warning "Warning: stoploss_on_exchange failures"
If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised. If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised.
### Understand order_time_in_force ### Understand order_time_in_force
@ -331,7 +348,7 @@ This configuration enables binance, as well as rate limiting to avoid bans from
Optimal settings for rate limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings. Optimal settings for rate limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step. We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.
#### Advanced FreqTrade Exchange configuration #### Advanced Freqtrade Exchange configuration
Advanced options can be configured using the `_ft_has_params` setting, which will override Defaults and exchange-specific behaviours. Advanced options can be configured using the `_ft_has_params` setting, which will override Defaults and exchange-specific behaviours.
@ -370,6 +387,91 @@ The valid values are:
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT" "BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
``` ```
## Pairlists
Pairlists define the list of pairs that the bot should trade.
There are [`StaticPairList`](#static-pair-list) and dynamic Whitelists available.
[`PrecisionFilter`](#precision-filter) and [`PriceFilter`](#price-pair-filter) act as filters, removing low-value pairs.
All pairlists can be chained, and a combination of all pairlists will become your new whitelist. Pairlists are executed in the sequence they are configured. You should always configure either `StaticPairList` or `DynamicPairList` as starting pairlists.
Inactive markets and blacklisted pairs are always removed from the resulting `pair_whitelist`.
### Available Pairlists
* [`StaticPairList`](#static-pair-list) (default, if not configured differently)
* [`VolumePairList`](#volume-pair-list)
* [`PrecisionFilter`](#precision-filter)
* [`PriceFilter`](#price-pair-filter)
!!! Tip "Testing pairlists"
Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) subcommand to test your configuration quickly.
#### Static Pair List
By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration.
It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.
```json
"pairlists": [
{"method": "StaticPairList"}
],
```
#### Volume Pair List
`VolumePairList` selects `number_assets` top pairs based on `sort_key`, which can be one of `askVolume`, `bidVolume` and `quoteVolume` and defaults to `quoteVolume`.
`VolumePairList` considers outputs of previous pairlists unless it's the first configured pairlist, it does not consider `pair_whitelist`, but selects the top assets from all available markets (with matching stake-currency) on the exchange.
`refresh_period` allows setting the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
```json
"pairlists": [{
"method": "VolumePairList",
"number_assets": 20,
"sort_key": "quoteVolume",
"refresh_period": 1800,
],
```
#### Precision Filter
Filters low-value coins which would not allow setting a stoploss.
#### Price Pair Filter
The `PriceFilter` allows filtering of pairs by price.
Currently, only `low_price_ratio` is implemented, where a raise of 1 price unit (pip) is below the `low_price_ratio` ratio.
This option is disabled by default, and will only apply if set to <> 0.
Calculation example:
Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value.
These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses.
### Full Pairlist example
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting by `quoteVolume` and applies both [`PrecisionFilter`](#precision-filter) and [`PriceFilter`](#price-pair-filter), filtering all assets where 1 priceunit is > 1%.
```json
"exchange": {
"pair_whitelist": [],
"pair_blacklist": ["BNB/BTC"]
},
"pairlists": [
{
"method": "VolumePairList",
"number_assets": 20,
"sort_key": "quoteVolume",
},
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.01}
],
```
## Switch to Dry-run mode ## Switch to Dry-run mode
We recommend starting the bot in the Dry-run mode to see how your bot will We recommend starting the bot in the Dry-run mode to see how your bot will
@ -385,7 +487,7 @@ creating trades on the exchange.
"db_url": "sqlite:///tradesv3.dryrun.sqlite", "db_url": "sqlite:///tradesv3.dryrun.sqlite",
``` ```
3. Remove your Exchange API key and secrete (change them by empty values or fake credentials): 3. Remove your Exchange API key and secret (change them by empty values or fake credentials):
```json ```json
"exchange": { "exchange": {
@ -399,39 +501,6 @@ creating trades on the exchange.
Once you will be happy with your bot performance running in the Dry-run mode, Once you will be happy with your bot performance running in the Dry-run mode,
you can switch it to production mode. you can switch it to production mode.
### Dynamic Pairlists
Dynamic pairlists select pairs for you based on the logic configured.
The bot runs against all pairs (with that stake) on the exchange, and a number of assets
(`number_assets`) is selected based on the selected criteria.
By default, the `StaticPairList` method is used.
The Pairlist method is configured as `pair_whitelist` parameter under the `exchange`
section of the configuration.
**Available Pairlist methods:**
* `StaticPairList`
* It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.
* `VolumePairList`
* It selects `number_assets` top pairs based on `sort_key`, which can be one of
`askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`.
* There is a possibility to filter low-value coins that would not allow setting a stop loss
(set `precision_filter` parameter to `true` for this).
Example:
```json
"pairlist": {
"method": "VolumePairList",
"config": {
"number_assets": 20,
"sort_key": "quoteVolume",
"precision_filter": false
}
},
```
## Switch to production mode ## Switch to production mode
In production mode, the bot will engage your money. Be careful, since a wrong In production mode, the bot will engage your money. Be careful, since a wrong
@ -457,12 +526,14 @@ you run it in production mode.
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5", "secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
... ...
} }
``` ```
!!! Note !!! Note
If you have an exchange API key yet, [see our tutorial](/pre-requisite). If you have an exchange API key yet, [see our tutorial](/pre-requisite).
### Using proxy with FreqTrade You should also make sure to read the [Exchanges](exchanges.md) section of the documentation to be aware of potential configuration details specific to your exchange.
### Using proxy with Freqtrade
To use a proxy with freqtrade, add the kwarg `"aiohttp_trust_env"=true` to the `"ccxt_async_kwargs"` dict in the exchange section of the configuration. To use a proxy with freqtrade, add the kwarg `"aiohttp_trust_env"=true` to the `"ccxt_async_kwargs"` dict in the exchange section of the configuration.
@ -482,14 +553,13 @@ export HTTPS_PROXY="http://addr:port"
freqtrade freqtrade
``` ```
## Embedding Strategies
### Embedding Strategies
FreqTrade provides you with with an easy way to embed the strategy into your configuration file. FreqTrade provides you with with an easy way to embed the strategy into your configuration file.
This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field, This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field,
in your chosen config file. in your chosen config file.
#### Encoding a string as BASE64 ### Encoding a string as BASE64
This is a quick example, how to generate the BASE64 string in python This is a quick example, how to generate the BASE64 string in python

View File

@ -8,7 +8,7 @@ If no additional parameter is specified, freqtrade will download data for `"1m"`
Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Exchange and pairs will come from `config.json` (if specified using `-c/--config`).
Otherwise `--exchange` becomes mandatory. Otherwise `--exchange` becomes mandatory.
!!! Tip Updating existing data !!! Tip "Tip: Updating existing data"
If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data. If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data.
Be carefull though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded. Be carefull though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded.
@ -78,10 +78,8 @@ freqtrade download-data --exchange binance --pairs XRP/ETH ETH/BTC --days 20 --d
!!! Warning !!! Warning
The historic trades are not available during Freqtrade dry-run and live trade modes because all exchanges tested provide this data with a delay of few 100 candles, so it's not suitable for real-time trading. The historic trades are not available during Freqtrade dry-run and live trade modes because all exchanges tested provide this data with a delay of few 100 candles, so it's not suitable for real-time trading.
### Historic Kraken data !!! Note "Kraken user"
Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data.
The Kraken API does only provide 720 historic candles, which is sufficient for FreqTrade dry-run and live trade modes, but is a problem for backtesting.
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
## Next step ## Next step

View File

@ -46,15 +46,18 @@ def test_method_to_test(caplog):
The fastest and easiest way to start up is to use docker-compose.develop which gives developers the ability to start the bot up with all the required dependencies, *without* needing to install any freqtrade specific dependencies on your local machine. The fastest and easiest way to start up is to use docker-compose.develop which gives developers the ability to start the bot up with all the required dependencies, *without* needing to install any freqtrade specific dependencies on your local machine.
#### Install #### Install
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) * [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [docker](https://docs.docker.com/install/) * [docker](https://docs.docker.com/install/)
* [docker-compose](https://docs.docker.com/compose/install/) * [docker-compose](https://docs.docker.com/compose/install/)
#### Starting the bot #### Starting the bot
##### Use the develop dockerfile ##### Use the develop dockerfile
``` bash ``` bash
rm docker-compose.yml && mv docker-compose.develop.yml docker-compose.yml rm docker-compose.yml && mv docker-compose.develop.yml docker-compose.yml
``` ```
#### Docker Compose #### Docker Compose
##### Starting ##### Starting
@ -62,9 +65,11 @@ rm docker-compose.yml && mv docker-compose.develop.yml docker-compose.yml
``` bash ``` bash
docker-compose up docker-compose up
``` ```
![Docker compose up](https://user-images.githubusercontent.com/419355/65456322-47f63a80-de06-11e9-90c6-3c74d1bad0b8.png) ![Docker compose up](https://user-images.githubusercontent.com/419355/65456322-47f63a80-de06-11e9-90c6-3c74d1bad0b8.png)
##### Rebuilding ##### Rebuilding
``` bash ``` bash
docker-compose build docker-compose build
``` ```
@ -77,8 +82,8 @@ that can be effected by `docker-compose up` or `docker-compose run freqtrade_dev
``` bash ``` bash
docker-compose exec freqtrade_develop /bin/bash docker-compose exec freqtrade_develop /bin/bash
``` ```
![image](https://user-images.githubusercontent.com/419355/65456522-ba671a80-de06-11e9-9598-df9ca0d8dcac.png)
![image](https://user-images.githubusercontent.com/419355/65456522-ba671a80-de06-11e9-9598-df9ca0d8dcac.png)
## Modules ## Modules
@ -95,22 +100,22 @@ This is a simple provider, which however serves as a good example on how to star
Next, modify the classname of the provider (ideally align this with the Filename). Next, modify the classname of the provider (ideally align this with the Filename).
The base-class provides the an instance of the bot (`self._freqtrade`), as well as the configuration (`self._config`), and initiates both `_blacklist` and `_whitelist`. The base-class provides an instance of the exchange (`self._exchange`) the pairlist manager (`self._pairlistmanager`), as well as the main configuration (`self._config`), the pairlist dedicated configuration (`self._pairlistconfig`) and the absolute position within the list of pairlists.
```python ```python
self._freqtrade = freqtrade self._exchange = exchange
self._pairlistmanager = pairlistmanager
self._config = config self._config = config
self._whitelist = self._config['exchange']['pair_whitelist'] self._pairlistconfig = pairlistconfig
self._blacklist = self._config['exchange'].get('pair_blacklist', []) self._pairlist_pos = pairlist_pos
``` ```
Now, let's step through the methods which require actions: Now, let's step through the methods which require actions:
#### configuration #### Pairlist configuration
Configuration for PairListProvider is done in the bot configuration file in the element `"pairlist"`. Configuration for PairListProvider is done in the bot configuration file in the element `"pairlist"`.
This Pairlist-object may contain a `"config"` dict with additional configurations for the configured pairlist. This Pairlist-object may contain configurations with additional configurations for the configured pairlist.
By convention, `"number_assets"` is used to specify the maximum number of pairs to keep in the whitelist. Please follow this to ensure a consistent user experience. By convention, `"number_assets"` is used to specify the maximum number of pairs to keep in the whitelist. Please follow this to ensure a consistent user experience.
Additional elements can be configured as needed. `VolumePairList` uses `"sort_key"` to specify the sorting value - however feel free to specify whatever is necessary for your great algorithm to be successfull and dynamic. Additional elements can be configured as needed. `VolumePairList` uses `"sort_key"` to specify the sorting value - however feel free to specify whatever is necessary for your great algorithm to be successfull and dynamic.
@ -120,29 +125,30 @@ Additional elements can be configured as needed. `VolumePairList` uses `"sort_ke
Returns a description used for Telegram messages. Returns a description used for Telegram messages.
This should contain the name of the Provider, as well as a short description containing the number of assets. Please follow the format `"PairlistName - top/bottom X pairs"`. This should contain the name of the Provider, as well as a short description containing the number of assets. Please follow the format `"PairlistName - top/bottom X pairs"`.
#### refresh_pairlist #### filter_pairlist
Override this method and run all calculations needed in this method. Override this method and run all calculations needed in this method.
This is called with each iteration of the bot - so consider implementing caching for compute/network heavy calculations. This is called with each iteration of the bot - so consider implementing caching for compute/network heavy calculations.
Assign the resulting whiteslist to `self._whitelist` and `self._blacklist` respectively. These will then be used to run the bot in this iteration. Pairs with open trades will be added to the whitelist to have the sell-methods run correctly. It get's passed a pairlist (which can be the result of previous pairlists) as well as `tickers`, a pre-fetched version of `get_tickers()`.
Please also run `self._validate_whitelist(pairs)` and to check and remove pairs with inactive markets. This function is available in the Parent class (`StaticPairList`) and should ideally not be overwritten. It must return the resulting pairlist (which may then be passed into the next pairlist filter).
Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the endresult is not shorter than expected.
##### sample ##### sample
``` python ``` python
def refresh_pairlist(self) -> None: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
# Generate dynamic whitelist # Generate dynamic whitelist
pairs = self._gen_pair_whitelist(self._config['stake_currency'], self._sort_key) pairs = self._calculate_pairlist(pairlist, tickers)
# Validate whitelist to only have active market pairs return pairs
self._whitelist = self._validate_whitelist(pairs)[:self._number_pairs]
``` ```
#### _gen_pair_whitelist #### _gen_pair_whitelist
This is a simple method used by `VolumePairList` - however serves as a good example. This is a simple method used by `VolumePairList` - however serves as a good example.
It implements caching (`@cached(TTLCache(maxsize=1, ttl=1800))`) as well as a configuration option to allow different (but similar) strategies to work with the same PairListProvider. In VolumePairList, this implements different methods of sorting, does early validation so only the expected number of pairs is returned.
## Implement a new Exchange (WIP) ## Implement a new Exchange (WIP)
@ -194,24 +200,38 @@ If the day shows the same day, then the last candle can be assumed as incomplete
To keep the jupyter notebooks aligned with the documentation, the following should be ran after updating a example notebook. To keep the jupyter notebooks aligned with the documentation, the following should be ran after updating a example notebook.
``` bash ``` bash
jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace user_data/notebooks/strategy_analysis_example.ipynb jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace freqtrade/templates/strategy_analysis_example.ipynb
jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown user_data/notebooks/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade/templates/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md
``` ```
## Continuous integration
This documents some decisions taken for the CI Pipeline.
* CI runs on all OS variants, Linux (ubuntu), macOS and Windows.
* Docker images are build for the branches `master` and `develop`.
* Raspberry PI Docker images are postfixed with `_pi` - so tags will be `:master_pi` and `develop_pi`.
* Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of.
* Full docker image rebuilds are run once a week via schedule.
* Deployments run on ubuntu.
* ta-lib binaries are contained in the build_helpers directory to avoid fails related to external unavailability.
* All tests must pass for a PR to be merged to `master` or `develop`.
## Creating a release ## Creating a release
This part of the documentation is aimed at maintainers, and shows how to create a release. This part of the documentation is aimed at maintainers, and shows how to create a release.
### Create release branch ### Create release branch
``` bash First, pick a commit that's about one week old (to not include latest additions to releases).
# make sure you're in develop branch
git checkout develop
``` bash
# create new branch # create new branch
git checkout -b new_release git checkout -b new_release <commitid>
``` ```
Determine if crucial bugfixes have been made between this commit and the current state, and eventually cherry-pick these.
* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7-1` should we need to do a second release that month. * Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7-1` should we need to do a second release that month.
* Commit this part * Commit this part
* push that branch to the remote and create a PR against the master branch * push that branch to the remote and create a PR against the master branch
@ -219,23 +239,18 @@ git checkout -b new_release
### Create changelog from git commits ### Create changelog from git commits
!!! Note !!! Note
Make sure that both master and develop are up-todate!. Make sure that the master branch is uptodate!
``` bash ``` bash
# Needs to be done before merging / pulling that branch. # Needs to be done before merging / pulling that branch.
git log --oneline --no-decorate --no-merges master..develop git log --oneline --no-decorate --no-merges master..new_release
``` ```
### Create github release / tag ### Create github release / tag
Once the PR against master is merged (best right after merging): Once the PR against master is merged (best right after merging):
* Use the button "Draft a new release" in the Github UI (subsection releases) * Use the button "Draft a new release" in the Github UI (subsection releases).
* Use the version-number specified as tag. * Use the version-number specified as tag.
* Use "master" as reference (this step comes after the above PR is merged). * Use "master" as reference (this step comes after the above PR is merged).
* Use the above changelog as release comment (as codeblock) * Use the above changelog as release comment (as codeblock).
### After-release
* Update version in develop by postfixing that with `-dev` (`2019.6 -> 2019.6-dev`).
* Create a PR against develop to update that branch.

View File

@ -26,7 +26,7 @@ To update the image, simply run the above commands again and restart your runnin
Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image).
!!! Note Docker image update frequency !!! Note "Docker image update frequency"
The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate. The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate.
In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`.
@ -160,7 +160,7 @@ docker run -d \
-v ~/.freqtrade/config.json:/freqtrade/config.json \ -v ~/.freqtrade/config.json:/freqtrade/config.json \
-v ~/.freqtrade/user_data/:/freqtrade/user_data \ -v ~/.freqtrade/user_data/:/freqtrade/user_data \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy
``` ```
!!! Note !!! Note
@ -170,6 +170,9 @@ docker run -d \
!!! Note !!! Note
All available bot command line parameters can be added to the end of the `docker run` command. All available bot command line parameters can be added to the end of the `docker run` command.
!!! Note
You can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system).
### Monitor your Docker instance ### Monitor your Docker instance
You can use the following commands to monitor and manage your container: You can use the following commands to monitor and manage your container:
@ -199,7 +202,7 @@ docker run -d \
-v ~/.freqtrade/config.json:/freqtrade/config.json \ -v ~/.freqtrade/config.json:/freqtrade/config.json \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
-v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \
freqtrade --strategy AwsomelyProfitableStrategy backtesting freqtrade backtesting --strategy AwsomelyProfitableStrategy
``` ```
Head over to the [Backtesting Documentation](backtesting.md) for more details. Head over to the [Backtesting Documentation](backtesting.md) for more details.

View File

@ -235,7 +235,7 @@ An example of its output:
### Update cached pairs with the latest data ### Update cached pairs with the latest data
Edge requires historic data the same way as backtesting does. Edge requires historic data the same way as backtesting does.
Please refer to the [download section](backtesting.md#Getting-data-for-backtesting-and-hyperopt) of the documentation for details. Please refer to the [Data Downloading](data-download.md) section of the documentation for details.
### Precising stoploss range ### Precising stoploss range

63
docs/exchanges.md Normal file
View File

@ -0,0 +1,63 @@
# Exchange-specific Notes
This page combines common gotchas and informations which are exchange-specific and most likely don't apply to other exchanges.
## Binance
!!! Tip "Stoploss on Exchange"
Binance is currently the only exchange supporting `stoploss_on_exchange`. It provides great advantages, so we recommend to benefit from it.
### Blacklists
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore.
### Binance sites
Binance has been split into 3, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized.
* [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`.
* [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`.
* [binance.je](https://www.binance.je/) - Binance Jersey, trading fiat currencies. Use exchange id: `binanceje`.
## Kraken
### Historic Kraken data
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
## Bittrex
### Restricted markets
Bittrex split its exchange into US and International versions.
The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction.
If you have restricted pairs in your whitelist, you'll get a warning message in the log on Freqtrade startup for each restricted pair.
The warning message will look similar to the following:
``` output
[...] Message: bittrex {"success":false,"message":"RESTRICTED_MARKET","result":null,"explanation":null}"
```
If you're an "International" customer on the Bittrex exchange, then this warning will probably not impact you.
If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your whitelist.
You can get a list of restricted markets by using the following snippet:
``` python
import ccxt
ct = ccxt.bittrex()
_ = ct.load_markets()
res = [ f"{x['MarketCurrency']}/{x['BaseCurrency']}" for x in ct.publicGetMarkets()['result'] if x['IsRestricted']]
print(res)
```
## Random notes for other exchanges
* The Ocean (exchange id: `theocean`) exchange uses Web3 functionality and requires `web3` python package to be installed:
```shell
$ pip3 install web3
```

View File

@ -4,7 +4,7 @@
### The bot does not start ### The bot does not start
Running the bot with `freqtrade --config config.json` does show the output `freqtrade: command not found`. Running the bot with `freqtrade trade --config config.json` does show the output `freqtrade: command not found`.
This could have the following reasons: This could have the following reasons:
@ -48,12 +48,46 @@ You can use the `/forcesell all` command from Telegram.
### I get the message "RESTRICTED_MARKET" ### I get the message "RESTRICTED_MARKET"
Currently known to happen for US Bittrex users. Currently known to happen for US Bittrex users.
Bittrex split its exchange into US and International versions.
The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction.
If you have restricted pairs in your whitelist, you'll get a warning message in the log on FreqTrade startup for each restricted pair. Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information.
If you're an "International" Customer on the Bittrex exchange, then this warning will probably not impact you.
If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your Whitelist. ### 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 subcommands, 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.
* In unix shells, this normally can be done as simple as:
```shell
$ freqtrade --some-options 2>&1 >/dev/null | grep 'something'
```
(note, `2>&1` and `>/dev/null` should be written in this order)
* Bash interpreter also supports so called process substitution syntax, you can grep the log for a string with it as:
```shell
$ freqtrade --some-options 2> >(grep 'something') >/dev/null
```
or
```shell
$ freqtrade --some-options 2> >(grep -v 'something' 1>&2)
```
* You can also write the copy of Freqtrade log messages to a file with the `--logfile` option:
```shell
$ freqtrade --logfile /path/to/mylogfile.log --some-options
```
and then grep it as:
```shell
$ cat /path/to/mylogfile.log | grep 'something'
```
or even on the fly, as the bot works and the logfile grows:
```shell
$ tail -f /path/to/mylogfile.log | grep 'something'
```
from a separate terminal window.
On Windows, the `--logfilename` option is also supported by Freqtrade and you can use the `findstr` command to search the log for the string of interest:
```
> type \path\to\mylogfile.log | findstr "something"
```
## Hyperopt module ## Hyperopt module

View File

@ -15,30 +15,50 @@ To learn how to get data for the pairs and exchange you're interrested in, head
## Prepare Hyperopting ## Prepare Hyperopting
Before we start digging into Hyperopt, we recommend you to take a look at Before we start digging into Hyperopt, we recommend you to take a look at
the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt.py). the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py).
Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy. Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy.
The simplest way to get started is to use `freqtrade new-hyperopt --hyperopt AwesomeHyperopt`.
This will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`.
### Checklist on all tasks / possibilities in hyperopt ### Checklist on all tasks / possibilities in hyperopt
Depending on the space you want to optimize, only some of the below are required: Depending on the space you want to optimize, only some of the below are required:
* fill `populate_indicators` - probably a copy from your strategy
* fill `buy_strategy_generator` - for buy signal optimization * fill `buy_strategy_generator` - for buy signal optimization
* fill `indicator_space` - for buy signal optimzation * fill `indicator_space` - for buy signal optimzation
* fill `sell_strategy_generator` - for sell signal optimization * fill `sell_strategy_generator` - for sell signal optimization
* fill `sell_indicator_space` - for sell signal optimzation * fill `sell_indicator_space` - for sell signal optimzation
Optional, but recommended: !!! Note
`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work.
Optional - can also be loaded from a strategy:
* copy `populate_indicators` from your strategy - otherwise default-strategy will be used
* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used * copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used
* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used * copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used
!!! Note
Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead.
Rarely you may also need to override: Rarely you may also need to override:
* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) * `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default)
* `generate_roi_table` - for custom ROI optimization (if you need more than 4 entries in the ROI table) * `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps)
* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) * `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default)
* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default)
!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss"
You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything (i.e. without creation of a "complete" Hyperopt class with dimensions, parameters, triggers and guards, as described in this document) from the default hyperopt template by relying on your strategy to do most of the calculations.
``` python
# Have a working strategy at hand.
freqtrade new-hyperopt --hyperopt EmptyHyperopt
freqtrade hyperopt --hyperopt EmptyHyperopt --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100
```
### 1. Install a Custom Hyperopt File ### 1. Install a Custom Hyperopt File
@ -156,7 +176,7 @@ that minimizes the value of the [loss function](#loss-functions).
The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators.
When you want to test an indicator that isn't used by the bot currently, remember to When you want to test an indicator that isn't used by the bot currently, remember to
add it to the `populate_indicators()` method in `hyperopt.py`. add it to the `populate_indicators()` method in your custom hyperopt file.
## Loss-functions ## Loss-functions
@ -173,63 +193,7 @@ Currently, the following loss functions are builtin:
* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) * `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration)
* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns) * `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns)
### Creating and using a custom loss function Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation.
To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class.
For the sample below, you then need to add the command line parameter `--hyperopt-loss SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used.
A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_loss.py)
``` python
from freqtrade.optimize.hyperopt import IHyperOptLoss
TARGET_TRADES = 600
EXPECTED_MAX_PROFIT = 3.0
MAX_ACCEPTED_TRADE_DURATION = 300
class SuperDuperHyperOptLoss(IHyperOptLoss):
"""
Defines the default loss function for hyperopt
"""
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
*args, **kwargs) -> float:
"""
Objective function, returns smaller number for better results
This is the legacy algorithm (used until now in freqtrade).
Weights are distributed as follows:
* 0.4 to trade duration
* 0.25: Avoiding trade loss
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
"""
total_profit = results.profit_percent.sum()
trade_duration = results.trade_duration.mean()
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
result = trade_loss + profit_loss + duration_loss
return result
```
Currently, the arguments are:
* `results`: DataFrame containing the result
The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`):
`pair, profit_percent, profit_abs, open_time, close_time, open_index, close_index, trade_duration, open_at_end, open_rate, close_rate, sell_reason`
* `trade_count`: Amount of trades (identical to `len(results)`)
* `min_date`: Start date of the hyperopting TimeFrame
* `min_date`: End date of the hyperopting TimeFrame
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.
!!! Note
This function is called once per iteration - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily.
!!! Note
Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later.
## Execute Hyperopt ## Execute Hyperopt
@ -239,15 +203,15 @@ Because hyperopt tries a lot of combinations to find the best parameters it will
We strongly recommend to use `screen` or `tmux` to prevent any connection loss. We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
```bash ```bash
freqtrade -c config.json hyperopt --customhyperopt <hyperoptname> -e 5000 --spaces all freqtrade hyperopt --config config.json --hyperopt <hyperoptname> -e 5000 --spaces all
``` ```
Use `<hyperoptname>` as the name of the custom hyperopt used. Use `<hyperoptname>` as the name of the custom hyperopt used.
The `-e` flag will set how many evaluations hyperopt will do. We recommend The `-e` option will set how many evaluations hyperopt will do. We recommend
running at least several thousand evaluations. running at least several thousand evaluations.
The `--spaces all` flag determines that all possible parameters should be optimized. Possibilities are listed below. The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below.
!!! Note !!! Note
By default, hyperopt will erase previous results and start from scratch. Continuation can be archived by using `--continue`. By default, hyperopt will erase previous results and start from scratch. Continuation can be archived by using `--continue`.
@ -270,9 +234,17 @@ For example, to use one month of data, pass the following parameter to the hyper
freqtrade hyperopt --timerange 20180401-20180501 freqtrade hyperopt --timerange 20180401-20180501
``` ```
### Running Hyperopt using methods from a strategy
Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided.
```bash
freqtrade hyperopt --strategy SampleStrategy --customhyperopt SampleHyperopt
```
### Running Hyperopt with Smaller Search Space ### Running Hyperopt with Smaller Search Space
Use the `--spaces` argument to limit the search space used by hyperopt. Use the `--spaces` option to limit the search space used by hyperopt.
Letting Hyperopt optimize everything is a huuuuge search space. Often it Letting Hyperopt optimize everything is a huuuuge search space. Often it
might make more sense to start by just searching for initial buy algorithm. might make more sense to start by just searching for initial buy algorithm.
Or maybe you just want to optimize your stoploss or roi table for that awesome Or maybe you just want to optimize your stoploss or roi table for that awesome
@ -285,8 +257,12 @@ Legal values are:
* `sell`: just search for a new sell strategy * `sell`: just search for a new sell strategy
* `roi`: just optimize the minimal profit table for your strategy * `roi`: just optimize the minimal profit table for your strategy
* `stoploss`: search for the best stoploss value * `stoploss`: search for the best stoploss value
* `trailing`: search for the best trailing stop values
* `default`: `all` except `trailing`
* space-separated list of any of the above values for example `--spaces roi stoploss` * space-separated list of any of the above values for example `--spaces roi stoploss`
The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy.
### Position stacking and disabling max market positions ### Position stacking and disabling max market positions
In some situations, you may need to run Hyperopt (and Backtesting) with the In some situations, you may need to run Hyperopt (and Backtesting) with the
@ -341,8 +317,7 @@ So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that t
(dataframe['rsi'] < 29.0) (dataframe['rsi'] < 29.0)
``` ```
Translating your whole hyperopt result as the new buy-signal Translating your whole hyperopt result as the new buy-signal would then look like:
would then look like:
```python ```python
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
@ -361,19 +336,13 @@ You can use the `--print-all` command line option if you would like to see all r
### Understand Hyperopt ROI results ### Understand Hyperopt ROI results
If you are optimizing ROI (i.e. if optimization search-space contains 'all' or 'roi'), your result will look as follows and include a ROI table: If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table:
``` ```
Best result: Best result:
44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367
Buy hyperspace params:
{ 'adx-value': 44,
'rsi-value': 29,
'adx-enabled': False,
'rsi-enabled': True,
'trigger': 'bb_lower'}
ROI table: ROI table:
{ 0: 0.10674, { 0: 0.10674,
21: 0.09158, 21: 0.09158,
@ -397,7 +366,7 @@ As stated in the comment, you can also use it as the value of the `minimal_roi`
#### Default ROI Search Space #### Default ROI Search Space
If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values can vary in the following ranges (for some of the most used ticker intervals, values are rounded to 5 digits after the decimal point): If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values vary in the following ranges (for some of the most used ticker intervals, values are rounded to 5 digits after the decimal point):
| # step | 1m | | 5m | | 1h | | 1d | | | # step | 1m | | 5m | | 1h | | 1d | |
|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|
@ -410,11 +379,11 @@ These ranges should be sufficient in most cases. The minutes in the steps (ROI d
If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default.
Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py). Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
### Understand Hyperopt Stoploss results ### Understand Hyperopt Stoploss results
If you are optimizing stoploss values (i.e. if optimization search-space contains 'all' or 'stoploss'), your result will look as follows and include stoploss: If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss:
``` ```
Best result: Best result:
@ -441,13 +410,51 @@ As stated in the comment, you can also use it as the value of the `stoploss` set
#### Default Stoploss Search Space #### Default Stoploss Search Space
If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace can vary in the range -0.35...-0.02, which is sufficient in most cases. If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace vary in the range -0.35...-0.02, which is sufficient in most cases.
If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default. If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default.
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py). Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
### Validate backtesting results ### Understand Hyperopt Trailing Stop results
If you are optimizing trailing stop values (i.e. if optimization search-space contains 'all' or 'trailing'), your result will look as follows and include trailing stop parameters:
```
Best result:
45/100: 606 trades. Avg profit 1.04%. Total profit 0.31555614 BTC ( 630.48Σ%). Avg duration 150.3 mins. Objective: -1.10161
Trailing stop:
{ 'trailing_only_offset_is_reached': True,
'trailing_stop': True,
'trailing_stop_positive': 0.02001,
'trailing_stop_positive_offset': 0.06038}
```
In order to use these best trailing stop parameters found by Hyperopt in backtesting and for live trades/dry-run, copy-paste them as the values of the corresponding attributes of your custom strategy:
```
# Trailing stop
# These attributes will be overridden if the config file contains corresponding values.
trailing_stop = True
trailing_stop_positive = 0.02001
trailing_stop_positive_offset = 0.06038
trailing_only_offset_is_reached = True
```
As stated in the comment, you can also use it as the values of the corresponding settings in the configuration file.
#### Default Trailing Stop Search Space
If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases.
Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py).
## Show details of Hyperopt results
After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` subcommands. The usage of these subcommands is described in the [Utils](utils.md#list-hyperopt-results) chapter.
## Validate backtesting results
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.

View File

@ -26,24 +26,32 @@ You will need to create API Keys (Usually you get `key` and `secret`) from the E
## Quick start ## Quick start
Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot. Freqtrade provides the Linux/MacOS Easy Installation script to install all dependencies and help you configure the bot.
!!! Note
Python3.6 or higher and the corresponding pip are assumed to be available. The install-script will warn and stop if that's not the case.
```bash
git clone git@github.com:freqtrade/freqtrade.git
cd freqtrade
git checkout develop
./setup.sh --install
```
!!! Note !!! Note
Windows installation is explained [here](#windows). Windows installation is explained [here](#windows).
## Easy Installation - Linux Script The easiest way to install and run Freqtrade is to clone the bot GitHub repository and then run the Easy Installation script, if it's available for your platform.
If you are on Debian, Ubuntu or MacOS freqtrade provides a script to Install, Update, Configure, and Reset your bot. !!! Note "Version considerations"
When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `master` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
!!! Note
Python3.6 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
This can be achieved with the following commands:
```bash
git clone git@github.com:freqtrade/freqtrade.git
cd freqtrade
git checkout master # Optional, see (1)
./setup.sh --install
```
(1) This command switches the cloned repository to the use of the `master` branch. It's not needed if you wish to stay on the `develop` branch. You may later switch between branches at any time with the `git checkout master`/`git checkout develop` commands.
## Easy Installation Script (Linux/MacOS)
If you are on Debian, Ubuntu or MacOS Freqtrade provides the script to install, update, configure and reset the codebase of your bot.
```bash ```bash
$ ./setup.sh $ ./setup.sh
@ -56,25 +64,25 @@ usage:
** --install ** ** --install **
This script will install everything you need to run the bot: With this option, the script will install everything you need to run the bot:
* Mandatory software as: `ta-lib` * Mandatory software as: `ta-lib`
* Setup your virtualenv * Setup your virtualenv
* Configure your `config.json` file * Configure your `config.json` file
This script is a combination of `install script` `--reset`, `--config` This option is a combination of installation tasks, `--reset` and `--config`.
** --update ** ** --update **
Update parameter will pull the last version of your current branch and update your virtualenv. This option will pull the last version of your current branch and update your virtualenv. Run the script with this option periodically to update your bot.
** --reset ** ** --reset **
Reset parameter will hard reset your branch (only if you are on `master` or `develop`) and recreate your virtualenv. This option will hard reset your branch (only if you are on either `master` or `develop`) and recreate your virtualenv.
** --config ** ** --config **
Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`. Use this option to configure the `config.json` configuration file. The script will interactively ask you questions to setup your bot and create your `config.json`.
------ ------
@ -95,29 +103,26 @@ sudo apt-get update
sudo apt-get install build-essential git sudo apt-get install build-essential git
``` ```
#### Raspberry Pi / Raspbian ### Raspberry Pi / Raspbian
Before installing FreqTrade on a Raspberry Pi running the official Raspbian Image, make sure you have at least Python 3.6 installed. The default image only provides Python 3.5. Probably the easiest way to get a recent version of python is [miniconda](https://repo.continuum.io/miniconda/). The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/) from at least September 2019.
This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running.
The following assumes that miniconda3 is installed and available in your environment. Since the last miniconda3 installation file uses python 3.4, we will update to python 3.6 on this installation. Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied.
It's recommended to use (mini)conda for this as installation/compilation of `numpy` and `pandas` takes a long time.
Additional package to install on your Raspbian, `libffi-dev` required by cryptography (from python-telegram-bot).
``` bash ``` bash
conda config --add channels rpi sudo apt-get install python3-venv libatlas-base-dev
conda install python=3.6 git clone https://github.com/freqtrade/freqtrade.git
conda create -n freqtrade python=3.6 cd freqtrade
conda activate freqtrade
conda install pandas numpy
sudo apt install libffi-dev bash setup.sh -i
python3 -m pip install -r requirements-common.txt
python3 -m pip install -e .
``` ```
!!! Note "Installation duration"
Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete.
!!! Note !!! Note
This does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`.
We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine.
### Common ### Common
@ -151,13 +156,13 @@ python3 -m venv .env
source .env/bin/activate source .env/bin/activate
``` ```
#### 3. Install FreqTrade #### 3. Install Freqtrade
Clone the git repository: Clone the git repository:
```bash ```bash
git clone https://github.com/freqtrade/freqtrade.git git clone https://github.com/freqtrade/freqtrade.git
cd freqtrade
``` ```
Optionally checkout the master branch to get the latest stable release: Optionally checkout the master branch to get the latest stable release:
@ -166,59 +171,37 @@ Optionally checkout the master branch to get the latest stable release:
git checkout master git checkout master
``` ```
#### 4. Initialize the configuration #### 4. Install python dependencies
```bash
cd freqtrade
cp config.json.example config.json
```
> *To edit the config please refer to [Bot Configuration](configuration.md).*
#### 5. Install python dependencies
``` bash ``` bash
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install -e . python3 -m pip install -e .
``` ```
#### 5. Initialize the configuration
```bash
# Initialize the user_directory
freqtrade create-userdir --userdir user_data/
cp config.json.example config.json
```
> *To edit the config please refer to [Bot Configuration](configuration.md).*
#### 6. Run the Bot #### 6. Run the Bot
If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins.
```bash ```bash
freqtrade -c config.json freqtrade trade -c config.json
``` ```
*Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. *Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout.
#### 7. [Optional] Configure `freqtrade` as a `systemd` service #### 7. (Optional) Post-installation Tasks
From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup. On Linux, as an optional post-installation task, you may wish to setup the bot to run as a `systemd` service or configure it to send the log messages to the `syslog`/`rsyslog` or `journald` daemons. See [Advanced Logging](advanced-setup.md#advanced-logging) for details.
After that you can start the daemon with:
```bash
systemctl --user start freqtrade
```
For this to be persistent (run when user is logged out) you'll need to enable `linger` for your freqtrade user.
```bash
sudo loginctl enable-linger "$USER"
```
If you run the bot as a service, you can use systemd service manager as a software watchdog monitoring freqtrade bot
state and restarting it in the case of failures. If the `internals.sd_notify` parameter is set to true in the
configuration or the `--sd-notify` command line option is used, the bot will send keep-alive ping messages to systemd
using the sd_notify (systemd notifications) protocol and will also tell systemd its current state (Running or Stopped)
when it changes.
The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd
as the watchdog.
!!! Note
The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container.
------ ------
@ -242,6 +225,12 @@ If that is not available on your system, feel free to try the instructions below
### Install freqtrade manually ### Install freqtrade manually
!!! Note
Make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows.
!!! Hint
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document for more information.
#### Clone the git repository #### Clone the git repository
```bash ```bash

View File

@ -23,13 +23,15 @@ The `freqtrade plot-dataframe` subcommand shows an interactive graph with three
Possible arguments: Possible arguments:
``` ```
usage: freqtrade plot-dataframe [-h] [-p PAIRS [PAIRS ...]] usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [-s NAME]
[--strategy-path PATH] [-p PAIRS [PAIRS ...]]
[--indicators1 INDICATORS1 [INDICATORS1 ...]] [--indicators1 INDICATORS1 [INDICATORS1 ...]]
[--indicators2 INDICATORS2 [INDICATORS2 ...]] [--indicators2 INDICATORS2 [INDICATORS2 ...]]
[--plot-limit INT] [--db-url PATH] [--plot-limit INT] [--db-url PATH]
[--trade-source {DB,file}] [--export EXPORT] [--trade-source {DB,file}] [--export EXPORT]
[--export-filename PATH] [--export-filename PATH]
[--timerange TIMERANGE] [--timerange TIMERANGE] [-i TICKER_INTERVAL]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -62,6 +64,28 @@ optional arguments:
/backtest_today.json` /backtest_today.json`
--timerange TIMERANGE --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
`1d`).
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default: `config.json`).
Multiple --config options may be used. Can be set to
`-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name (default:
`DefaultStrategy`).
--strategy-path PATH Specify additional strategy lookup path.
``` ```
@ -79,11 +103,11 @@ The `-p/--pairs` argument can be used to specify pairs you would like to plot.
Specify custom indicators. Specify custom indicators.
Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices). Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices).
!!! tip !!! Tip
You will almost certainly want to specify a custom strategy! This can be done by adding `-s Classname` / `--strategy ClassName` to the command. You will almost certainly want to specify a custom strategy! This can be done by adding `-s Classname` / `--strategy ClassName` to the command.
``` bash ``` bash
freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH --indicators1 sma ema --indicators2 macd freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --indicators1 sma ema --indicators2 macd
``` ```
### Further usage examples ### Further usage examples
@ -91,25 +115,25 @@ freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH --indicators1 sma
To plot multiple pairs, separate them with a space: To plot multiple pairs, separate them with a space:
``` bash ``` bash
freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH XRP/ETH freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH XRP/ETH
``` ```
To plot a timerange (to zoom in) To plot a timerange (to zoom in)
``` bash ``` bash
freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH --timerange=20180801-20180805 freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --timerange=20180801-20180805
``` ```
To plot trades stored in a database use `--db-url` in combination with `--trade-source DB`: To plot trades stored in a database use `--db-url` in combination with `--trade-source DB`:
``` bash ``` bash
freqtrade --strategy AwesomeStrategy plot-dataframe --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH --trade-source DB freqtrade plot-dataframe --strategy AwesomeStrategy --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH --trade-source DB
``` ```
To plot trades from a backtesting result, use `--export-filename <filename>` To plot trades from a backtesting result, use `--export-filename <filename>`
``` bash ``` bash
freqtrade --strategy AwesomeStrategy plot-dataframe --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH freqtrade plot-dataframe --strategy AwesomeStrategy --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH
``` ```
## Plot profit ## Plot profit
@ -133,10 +157,11 @@ The third graph can be useful to spot outliers, events in pairs that cause profi
Possible options for the `freqtrade plot-profit` subcommand: Possible options for the `freqtrade plot-profit` subcommand:
``` ```
usage: freqtrade plot-profit [-h] [-p PAIRS [PAIRS ...]] usage: freqtrade plot-profit [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]]
[--timerange TIMERANGE] [--export EXPORT] [--timerange TIMERANGE] [--export EXPORT]
[--export-filename PATH] [--db-url PATH] [--export-filename PATH] [--db-url PATH]
[--trade-source {DB,file}] [--trade-source {DB,file}] [-i TICKER_INTERVAL]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -159,6 +184,22 @@ optional arguments:
--trade-source {DB,file} --trade-source {DB,file}
Specify the source for trades (Can be DB or file Specify the source for trades (Can be DB or file
(backtest file)) Default: file (backtest file)) Default: file
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
`1d`).
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default: `config.json`).
Multiple --config options may be used. Can be set to
`-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
``` ```

View File

@ -1,2 +1,2 @@
mkdocs-material==4.4.3 mkdocs-material==4.5.1
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2

View File

@ -16,13 +16,20 @@ Sample configuration:
}, },
``` ```
!!! Danger Security warning !!! Danger "Security warning"
By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot.
!!! Danger Password selection !!! Danger "Password selection"
Please make sure to select a very strong, unique password to protect your bot from unauthorized access. Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly.
This should return the response:
``` output
{"status":"pong"}
```
All other endpoints return sensitive info and require authentication, so are not available through a web browser.
To generate a secure password, either use a password manager, or use the below code snipped. To generate a secure password, either use a password manager, or use the below code snipped.
@ -58,7 +65,7 @@ docker run -d \
-v ~/.freqtrade/user_data/:/freqtrade/user_data \ -v ~/.freqtrade/user_data/:/freqtrade/user_data \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
-p 127.0.0.1:8080:8080 \ -p 127.0.0.1:8080:8080 \
freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy
``` ```
!!! Danger "Security warning" !!! Danger "Security warning"
@ -99,6 +106,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `stop` | | Stops the trader | `stop` | | Stops the trader
| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `reload_conf` | | Reloads the configuration file | `reload_conf` | | Reloads the configuration file
| `show_config` | | Shows part of the current configuration with relevant settings to operation
| `status` | | Lists all open trades | `status` | | Lists all open trades
| `count` | | Displays number of trades used and available | `count` | | Displays number of trades used and available
| `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance | `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
@ -165,6 +173,10 @@ reload_conf
Reload configuration Reload configuration
:returns: json object :returns: json object
show_config
Returns part of the configuration, relevant for trading operations.
:return: json object containing the version
start start
Start the bot if it's in stopped state. Start the bot if it's in stopped state.
:returns: json object :returns: json object

View File

@ -3,74 +3,101 @@
The `stoploss` configuration parameter is loss in percentage that should trigger a sale. The `stoploss` configuration parameter is loss in percentage that should trigger a sale.
For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional. For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional.
Most of the strategy files already include the optimal `stoploss` Most of the strategy files already include the optimal `stoploss` value.
value. This parameter is optional. If you use it in the configuration file, it will take over the
`stoploss` value from the strategy file.
## Stop Loss support !!! Info
All stoploss properties mentioned in this file can be set in the Strategy, or in the configuration. Configuration values will override the strategy values.
## Stop Loss Types
At this stage the bot contains the following stoploss support modes: At this stage the bot contains the following stoploss support modes:
1. static stop loss, defined in either the strategy or configuration. 1. Static stop loss.
2. trailing stop loss, defined in the configuration. 2. Trailing stop loss.
3. trailing stop loss, custom positive loss, defined in configuration. 3. Trailing stop loss, custom positive loss.
4. Trailing stop loss only once the trade has reached a certain offset.
!!! Note Those stoploss modes can be *on exchange* or *off exchange*. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled.
All stoploss properties can be configured in either Strategy or configuration. Configuration values override strategy values.
Those stoploss modes can be *on exchange* or *off exchange*. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfuly. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary.
In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. As an example in case of trailing stoploss if the order is on the exchange and the market is going up then the bot automatically cancels the previous stoploss order and put a new one with a stop value higher than previous one. It is clear that the bot cannot do it every 5 seconds otherwise it gets banned. So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order.
The bot cannot do this every 5 seconds (at each iteration), otherwise it would get banned by the exchange.
So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute).
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
!!! Note !!! Note
Stoploss on exchange is only supported for Binance as of now. Stoploss on exchange is only supported for Binance as of now.
## Static Stop Loss ## Static Stop Loss
This is very simple, basically you define a stop loss of x in your strategy file or alternative in the configuration, which This is very simple, you define a stop loss of x (as a ratio of price, i.e. x * 100% of price). This will try to sell the asset once the loss exceeds the defined loss.
will overwrite the strategy definition. This will basically try to sell your asset, the second the loss exceeds the defined loss.
## Trailing Stop Loss ## Trailing Stop Loss
The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally. The initial value for this is `stoploss`, just as you would define your static Stop loss.
To enable this Feauture all you have to do is to define the configuration element: To enable trailing stoploss:
``` json ``` python
"trailing_stop" : True trailing_stop = True
``` ```
This will now activate an algorithm, which automatically moves your stop loss up every time the price of your asset increases. This will now activate an algorithm, which automatically moves the stop loss up every time the price of your asset increases.
For example, simplified math, For example, simplified math:
* you buy an asset at a price of 100$ * the bot buys an asset at a price of 100$
* your stop loss is defined at 2% * the stop loss is defined at 2%
* which means your stop loss, gets triggered once your asset dropped below 98$ * the stop loss would get triggered once the asset dropps below 98$
* assuming your asset now increases to 102$ * assuming the asset now increases to 102$
* your stop loss, will now be 2% of 102$ or 99.96$ * the stop loss will now be 2% of 102$ or 99.96$
* now your asset drops in value to 101$, your stop loss, will still be 99.96$ * now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$.
basically what this means is that your stop loss will be adjusted to be always be 2% of the highest observed price In summary: The stoploss will be adjusted to be always be 2% of the highest observed price.
### Custom positive loss ### Custom positive stoploss
Due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your profit surpasses a certain percentage, It is also possible to have a default stop loss, when you are in the red with your buy, but once your profit surpasses a certain percentage, the system will utilize a new stop loss, which can have a different value.
the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you have 1.1% profit, For example your default stop loss is 5%, but once you have 1.1% profit, it will be changed to be only a 1% stop loss, which trails the green candles until it goes below them.
it will be changed to be only a 1% stop loss, which trails the green candles until it goes below them.
Both values can be configured in the main configuration file and requires `"trailing_stop": true` to be set to true. Both values require `trailing_stop` to be set to true.
``` json ``` python
"trailing_stop_positive": 0.01, trailing_stop_positive = 0.01
"trailing_stop_positive_offset": 0.011, trailing_stop_positive_offset = 0.011
"trailing_only_offset_is_reached": false
``` ```
The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit. The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit.
Before this, `stoploss` is used for the trailing stoploss.
You should also make sure to have this value (`trailing_stop_positive_offset`) lower than your minimal ROI, otherwise minimal ROI will apply first and sell your trade. Read the [next section](#trailing-only-once-offset-is-reached) to keep stoploss at 5% of the entry point.
!!! Tip
Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade.
### Trailing only once offset is reached
It is also possible to use a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns.
If `"trailing_only_offset_is_reached": true` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`. If `"trailing_only_offset_is_reached": true` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`.
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
```
Simplified example:
``` python
stoploss = 0.05
trailing_stop_positive_offset = 0.03
trailing_only_offset_is_reached = True
```
* the bot buys an asset at a price of 100$
* the stop loss is defined at 5%
* the stop loss will remain at 95% until profit reaches +3%
## Changing stoploss on open trades ## Changing stoploss on open trades

View File

@ -1,4 +1,4 @@
# Optimization # Strategy Customization
This page explains where to customize your strategies, and add new This page explains where to customize your strategies, and add new
indicators. indicators.
@ -7,24 +7,28 @@ indicators.
This is very simple. Copy paste your strategy file into the directory `user_data/strategies`. This is very simple. Copy paste your strategy file into the directory `user_data/strategies`.
Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`: Let assume you have a class called `AwesomeStrategy` in the file `AwesomeStrategy.py`:
1. Move your file into `user_data/strategies` (you should have `user_data/strategies/awesome-strategy.py` 1. Move your file into `user_data/strategies` (you should have `user_data/strategies/AwesomeStrategy.py`
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name) 2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
```bash ```bash
freqtrade --strategy AwesomeStrategy freqtrade trade --strategy AwesomeStrategy
``` ```
## Change your strategy ## Develop your own strategy
The bot includes a default strategy file. However, we recommend you to The bot includes a default strategy file.
use your own file to not have to lose your parameters every time the default Also, several other strategies are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies).
strategy file will be updated on Github. Put your custom strategy file
into the directory `user_data/strategies`.
Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes. You will however most likely have your own idea for a strategy.
`cp user_data/strategies/sample_strategy.py user_data/strategies/awesome-strategy.py` This document intends to help you develop one for yourself.
To get started, use `freqtrade new-strategy --strategy AwesomeStrategy`.
This will create a new strategy file from a template, which will be located under `user_data/strategies/AwesomeStrategy.py`.
!!! Note
This is just a template file, which will most likely not be profitable out of the box.
### Anatomy of a strategy ### Anatomy of a strategy
@ -45,19 +49,19 @@ The current version is 2 - which is also the default when it's not set explicitl
Future versions will require this to be set. Future versions will require this to be set.
```bash ```bash
freqtrade --strategy AwesomeStrategy freqtrade trade --strategy AwesomeStrategy
``` ```
**For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py) **For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py)
file as reference.** file as reference.**
!!! Note Strategies and Backtesting !!! Note "Strategies and Backtesting"
To avoid problems and unexpected differences between Backtesting and dry/live modes, please be aware To avoid problems and unexpected differences between Backtesting and dry/live modes, please be aware
that during backtesting the full time-interval is passed to the `populate_*()` methods at once. that during backtesting the full time-interval is passed to the `populate_*()` methods at once.
It is therefore best to use vectorized operations (across the whole dataframe, not loops) and It is therefore best to use vectorized operations (across the whole dataframe, not loops) and
avoid index referencing (`df.iloc[-1]`), but instead use `df.shift()` to get to the previous candle. avoid index referencing (`df.iloc[-1]`), but instead use `df.shift()` to get to the previous candle.
!!! Warning Using future data !!! Warning "Warning: Using future data"
Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author
needs to take care to avoid having the strategy utilize data from the future. needs to take care to avoid having the strategy utilize data from the future.
Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document. Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document.
@ -114,9 +118,40 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
``` ```
!!! Note "Want more indicator examples?" !!! Note "Want more indicator examples?"
Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py). Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py).
Then uncomment indicators you need. Then uncomment indicators you need.
### Strategy startup period
Most indicators have an instable startup period, in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
To account for this, the strategy can be assigned the `startup_candle_count` attribute.
This should be set to the maximum number of candles that the strategy requires to calculate stable indicators.
In this example strategy, this should be set to 100 (`startup_candle_count = 100`), since the longest needed history is 100 candles.
``` python
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
```
By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt.
!!! Warning
`startup_candle_count` should be below `ohlcv_candle_limit` (which is 500 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations.
#### Example
Let's try to backtest 1 month (January 2019) of 5m candles using the an example strategy with EMA100, as above.
``` bash
freqtrade backtesting --timerange 20190101-20190201 --ticker-interval 5m
```
Assuming `startup_candle_count` is set to 100, backtesting knows it needs 100 candles to generate valid buy signals. It will load data from `20190101 - (100 * 5m)` - which is ~2019-12-31 15:30:00.
If this data is available, indicators will be calculated with this extended timerange. The instable startup period (up to 2019-01-01 00:00:00) will then be removed before starting backtesting.
!!! Note
If data for the startup period is not available, then the timerange will be adjusted to account for this startup period - so Backtesting would start at 2019-01-01 08:30:00.
### Buy signal rules ### Buy signal rules
Edit the method `populate_buy_trend()` in your strategy file to update your buy strategy. Edit the method `populate_buy_trend()` in your strategy file to update your buy strategy.
@ -283,9 +318,9 @@ Please always check the mode of operation to select the correct method to get da
#### Possible options for DataProvider #### Possible options for DataProvider
- `available_pairs` - Property with tuples listing cached pairs with their intervals (pair, interval). - `available_pairs` - Property with tuples listing cached pairs with their intervals (pair, interval).
- `ohlcv(pair, ticker_interval)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame. - `ohlcv(pair, timeframe)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame.
- `historic_ohlcv(pair, ticker_interval)` - Returns historical data stored on disk. - `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk.
- `get_pair_dataframe(pair, ticker_interval)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes). - `get_pair_dataframe(pair, timeframe)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes).
- `orderbook(pair, maximum)` - Returns latest orderbook data for the pair, a dict with bids/asks with a total of `maximum` entries. - `orderbook(pair, maximum)` - Returns latest orderbook data for the pair, a dict with bids/asks with a total of `maximum` entries.
- `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on Market data structure. - `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on Market data structure.
- `runmode` - Property containing the current runmode. - `runmode` - Property containing the current runmode.
@ -296,15 +331,15 @@ Please always check the mode of operation to select the correct method to get da
if self.dp: if self.dp:
inf_pair, inf_timeframe = self.informative_pairs()[0] inf_pair, inf_timeframe = self.informative_pairs()[0]
informative = self.dp.get_pair_dataframe(pair=inf_pair, informative = self.dp.get_pair_dataframe(pair=inf_pair,
ticker_interval=inf_timeframe) timeframe=inf_timeframe)
``` ```
!!! Warning Warning about backtesting !!! Warning "Warning about backtesting"
Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()` Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()`
for the backtesting runmode) provides the full time-range in one go, for the backtesting runmode) provides the full time-range in one go,
so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode). so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode).
!!! Warning Warning in hyperopt !!! Warning "Warning in hyperopt"
This option cannot currently be used during hyperopt. This option cannot currently be used during hyperopt.
#### Orderbook #### Orderbook
@ -374,6 +409,52 @@ if self.wallets:
- `get_used(asset)` - currently tied up balance (open orders) - `get_used(asset)` - currently tied up balance (open orders)
- `get_total(asset)` - total available balance - sum of the 2 above - `get_total(asset)` - total available balance - sum of the 2 above
### Additional data (Trades)
A history of Trades can be retrieved in the strategy by querying the database.
At the top of the file, import Trade.
```python
from freqtrade.persistence import Trade
```
The following example queries for the current pair and trades from today, however other filters can easily be added.
``` python
if self.config['runmode'] in ('live', 'dry_run'):
trades = Trade.get_trades([Trade.pair == metadata['pair'],
Trade.open_date > datetime.utcnow() - timedelta(days=1),
Trade.is_open == False,
]).order_by(Trade.close_date).all()
# Summarize profit for this pair.
curdayprofit = sum(trade.close_profit for trade in trades)
```
Get amount of stake_currency currently invested in Trades:
``` python
if self.config['runmode'] in ('live', 'dry_run'):
total_stakes = Trade.total_open_trades_stakes()
```
Retrieve performance per pair.
Returns a List of dicts per pair.
``` python
if self.config['runmode'] in ('live', 'dry_run'):
performance = Trade.get_overall_performance()
```
Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5% (ratio of 0.015).
``` json
{'pair': "ETH/BTC", 'profit': 0.015, 'count': 5}
```
!!! Warning
Trade history is not available during backtesting or hyperopt.
### Print created dataframe ### Print created dataframe
To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`. To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`.
@ -401,14 +482,14 @@ Printing more than a few rows is also possible (simply use `print(dataframe)` i
### Where can i find a strategy template? ### Where can i find a strategy template?
The strategy template is located in the file The strategy template is located in the file
[user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py). [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py).
### Specify custom strategy location ### Specify custom strategy location
If you want to use a strategy from a different directory you can pass `--strategy-path` If you want to use a strategy from a different directory you can pass `--strategy-path`
```bash ```bash
freqtrade --strategy AwesomeStrategy --strategy-path /some/directory freqtrade trade --strategy AwesomeStrategy --strategy-path /some/directory
``` ```
### Common mistakes when developing strategies ### Common mistakes when developing strategies

View File

@ -10,7 +10,7 @@ from pathlib import Path
# Customize these according to your needs. # Customize these according to your needs.
# Define some constants # Define some constants
ticker_interval = "5m" timeframe = "5m"
# Name of the strategy class # Name of the strategy class
strategy_name = 'SampleStrategy' strategy_name = 'SampleStrategy'
# Path to user data # Path to user data
@ -29,7 +29,7 @@ pair = "BTC_USDT"
from freqtrade.data.history import load_pair_history from freqtrade.data.history import load_pair_history
candles = load_pair_history(datadir=data_location, candles = load_pair_history(datadir=data_location,
ticker_interval=ticker_interval, timeframe=timeframe,
pair=pair) pair=pair)
# Confirm success # Confirm success
@ -107,6 +107,22 @@ trades = load_trades_from_db("sqlite:///tradesv3.sqlite")
trades.groupby("pair")["sell_reason"].value_counts() trades.groupby("pair")["sell_reason"].value_counts()
``` ```
## Analyze the loaded trades for trade parallelism
This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`.
`analyze_trade_parallelism()` returns a timeseries dataframe with an "open_trades" column, specifying the number of open trades for each candle.
```python
from freqtrade.data.btanalysis import analyze_trade_parallelism
# Analyze the above
parallel_trades = analyze_trade_parallelism(trades, '5m')
parallel_trades.plot()
```
## Plot results ## Plot results
Freqtrade offers interactive plotting capabilities based on plotly. Freqtrade offers interactive plotting capabilities based on plotly.

View File

@ -53,6 +53,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/stop` | | Stops the trader | `/stop` | | Stops the trader
| `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `/reload_conf` | | Reloads the configuration file | `/reload_conf` | | Reloads the configuration file
| `/show_config` | | Shows part of the current configuration with relevant settings to operation
| `/status` | | Lists all open trades | `/status` | | Lists all open trades
| `/status table` | | List all open trades in a table format | `/status table` | | List all open trades in a table format
| `/count` | | Displays number of trades used and available | `/count` | | Displays number of trades used and available
@ -93,7 +94,7 @@ Once all positions are sold, run `/stop` to completely stop the bot.
`/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command. `/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command.
!!! warning !!! Warning
The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset. The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
### /status ### /status

View File

@ -2,6 +2,112 @@
Besides the Live-Trade and Dry-Run run modes, the `backtesting`, `edge` and `hyperopt` optimization subcommands, and the `download-data` subcommand which prepares historical data, the bot contains a number of utility subcommands. They are described in this section. Besides the Live-Trade and Dry-Run run modes, the `backtesting`, `edge` and `hyperopt` optimization subcommands, and the `download-data` subcommand which prepares historical data, the bot contains a number of utility subcommands. They are described in this section.
## Create userdir
Creates the directory structure to hold your files for freqtrade.
Will also create strategy and hyperopt examples for you to get started.
Can be used multiple times - using `--reset` will reset the sample strategy and hyperopt files to their default state.
```
usage: freqtrade create-userdir [-h] [--userdir PATH] [--reset]
optional arguments:
-h, --help show this help message and exit
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
--reset Reset sample files to their original state.
```
!!! Warning
Using `--reset` may result in loss of data, since this will overwrite all sample files without asking again.
```
├── backtest_results
├── data
├── hyperopt_results
├── hyperopts
│   ├── sample_hyperopt_advanced.py
│   ├── sample_hyperopt_loss.py
│   └── sample_hyperopt.py
├── notebooks
│   └── strategy_analysis_example.ipynb
├── plot
└── strategies
└── sample_strategy.py
```
## Create new strategy
Creates a new strategy from a template similar to SampleStrategy.
The file will be named inline with your class name, and will not overwrite existing files.
Results will be located in `user_data/strategies/<strategyclassname>.py`.
``` output
usage: freqtrade new-strategy [-h] [--userdir PATH] [-s NAME]
[--template {full,minimal}]
optional arguments:
-h, --help show this help message and exit
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--template {full,minimal}
Use a template which is either `minimal` or `full`
(containing multiple sample indicators). Default:
`full`.
```
### Sample usage of new-strategy
```bash
freqtrade new-strategy --strategy AwesomeStrategy
```
With custom user directory
```bash
freqtrade new-strategy --userdir ~/.freqtrade/ --strategy AwesomeStrategy
```
## Create new hyperopt
Creates a new hyperopt from a template similar to SampleHyperopt.
The file will be named inline with your class name, and will not overwrite existing files.
Results will be located in `user_data/hyperopts/<classname>.py`.
``` output
usage: freqtrade new-hyperopt [-h] [--userdir PATH] [--hyperopt NAME]
[--template {full,minimal}]
optional arguments:
-h, --help show this help message and exit
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
--hyperopt NAME Specify hyperopt class name which will be used by the
bot.
--template {full,minimal}
Use a template which is either `minimal` or `full`
(containing multiple sample indicators). Default:
`full`.
```
### Sample usage of new-hyperopt
```bash
freqtrade new-hyperopt --hyperopt AwesomeHyperopt
```
With custom user directory
```bash
freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt
```
## List Exchanges ## List Exchanges
Use the `list-exchanges` subcommand to see the exchanges available for the bot. Use the `list-exchanges` subcommand to see the exchanges available for the bot.
@ -124,3 +230,100 @@ $ freqtrade -c config_binance.json list-pairs --all --base BTC ETH --quote USDT
``` ```
$ freqtrade list-markets --exchange kraken --all $ freqtrade list-markets --exchange kraken --all
``` ```
## Test pairlist
Use the `test-pairlist` subcommand to test the configuration of [dynamic pairlists](configuration.md#pairlists).
Requires a configuration with specified `pairlists` attribute.
Can be used to generate static pairlists to be used during backtesting / hyperopt.
```
usage: freqtrade test-pairlist [-h] [-c PATH]
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
[-1] [--print-json]
optional arguments:
-h, --help show this help message and exit
-c PATH, --config PATH
Specify configuration file (default: `config.json`).
Multiple --config options may be used. Can be set to
`-` to read config from stdin.
--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]
Specify quote currency(-ies). Space-separated list.
-1, --one-column Print output in one column.
--print-json Print list of pairs or market symbols in JSON format.
```
### Examples
Show whitelist when using a [dynamic pairlist](configuration.md#pairlists).
```
freqtrade test-pairlist --config config.json --quote USDT BTC
```
## List Hyperopt results
You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` subcommand.
```
usage: freqtrade hyperopt-list [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [--best]
[--profitable] [--no-color] [--print-json]
[--no-details]
optional arguments:
-h, --help show this help message and exit
--best Select only best epochs.
--profitable Select only profitable epochs.
--no-color Disable colorization of hyperopt results. May be
useful if you are redirecting output to a file.
--print-json Print best result detailization in JSON format.
--no-details Do not print best epoch details.
```
### Examples
List all results, print details of the best result at the end:
```
freqtrade hyperopt-list
```
List only epochs with positive profit. Do not print the details of the best epoch, so that the list can be iterated in a script:
```
freqtrade hyperopt-list --profitable --no-details
```
## Show details of Hyperopt results
You can show the details of any hyperoptimization epoch previously evaluated by the Hyperopt module with the `hyperopt-show` subcommand.
```
usage: freqtrade hyperopt-show [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [--best]
[--profitable] [-n INT] [--print-json]
[--no-header]
optional arguments:
-h, --help show this help message and exit
--best Select only best epochs.
--profitable Select only profitable epochs.
-n INT, --index INT Specify the index of the epoch to print details for.
--print-json Print best result detailization in JSON format.
--no-header Do not print epoch details header.
```
### Examples
Print details for the epoch 168 (the number of the epoch is shown by the `hyperopt-list` subcommand or by Hyperopt itself during hyperoptimization run):
```
freqtrade hyperopt-show -n 168
```
Prints JSON data with details for the last best epoch (i.e., the best of all epochs):
```
freqtrade hyperopt-show --best -n -1 --print-json --no-header
```

View File

@ -6,7 +6,7 @@ After=network.target
# Set WorkingDirectory and ExecStart to your file paths accordingly # Set WorkingDirectory and ExecStart to your file paths accordingly
# NOTE: %h will be resolved to /home/<username> # NOTE: %h will be resolved to /home/<username>
WorkingDirectory=%h/freqtrade WorkingDirectory=%h/freqtrade
ExecStart=/usr/bin/freqtrade ExecStart=/usr/bin/freqtrade trade
Restart=on-failure Restart=on-failure
[Install] [Install]

View File

@ -6,7 +6,7 @@ After=network.target
# Set WorkingDirectory and ExecStart to your file paths accordingly # Set WorkingDirectory and ExecStart to your file paths accordingly
# NOTE: %h will be resolved to /home/<username> # NOTE: %h will be resolved to /home/<username>
WorkingDirectory=%h/freqtrade WorkingDirectory=%h/freqtrade
ExecStart=/usr/bin/freqtrade --sd-notify ExecStart=/usr/bin/freqtrade trade --sd-notify
Restart=always Restart=always
#Restart=on-failure #Restart=on-failure

View File

@ -1,5 +1,5 @@
""" FreqTrade bot """ """ FreqTrade bot """
__version__ = '2019.10' __version__ = '2019.11'
if __version__ == 'develop': if __version__ == 'develop':

View File

@ -1,4 +1,5 @@
from freqtrade.configuration.arguments import Arguments # noqa: F401 from freqtrade.configuration.arguments import Arguments # noqa: F401
from freqtrade.configuration.check_exchange import check_exchange, remove_credentials # noqa: F401
from freqtrade.configuration.timerange import TimeRange # noqa: F401 from freqtrade.configuration.timerange import TimeRange # noqa: F401
from freqtrade.configuration.configuration import Configuration # noqa: F401 from freqtrade.configuration.configuration import Configuration # noqa: F401
from freqtrade.configuration.config_validation import validate_config_consistency # noqa: F401 from freqtrade.configuration.config_validation import validate_config_consistency # noqa: F401

View File

@ -13,7 +13,7 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat
ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_STRATEGY = ["strategy", "strategy_path"]
ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["db_url", "sd_notify"] ARGS_TRADE = ["db_url", "sd_notify", "dry_run"]
ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange",
"max_open_trades", "stake_amount", "fee"] "max_open_trades", "stake_amount", "fee"]
@ -37,35 +37,44 @@ ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column",
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all"] "print_csv", "base_currencies", "quote_currencies", "list_pairs_all"]
ARGS_CREATE_USERDIR = ["user_data_dir"] ARGS_TEST_PAIRLIST = ["config", "quote_currencies", "print_one_column", "list_pairs_print_json"]
ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange",
"timeframes", "erase"] "timeframes", "erase"]
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
"trade_source", "export", "exportfilename", "timerange", "ticker_interval"] "db_url", "trade_source", "export", "exportfilename",
"timerange", "ticker_interval"]
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
"trade_source", "ticker_interval"] "trade_source", "ticker_interval"]
NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs", ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", "print_colorized",
"plot-dataframe", "plot-profit"] "print_json", "hyperopt_list_no_details"]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges"] ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
"print_json", "hyperopt_show_no_header"]
NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs",
"hyperopt-list", "hyperopt-show", "plot-dataframe", "plot-profit"]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"]
class Arguments: class Arguments:
""" """
Arguments Class. Manage the arguments received by the cli Arguments Class. Manage the arguments received by the cli
""" """
def __init__(self, args: Optional[List[str]]) -> None: def __init__(self, args: Optional[List[str]]) -> None:
self.args = args self.args = args
self._parsed_arg: Optional[argparse.Namespace] = None self._parsed_arg: Optional[argparse.Namespace] = None
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
def _load_args(self) -> None:
self._build_args(optionlist=ARGS_MAIN)
self._build_subcommands()
def get_parsed_arg(self) -> Dict[str, Any]: def get_parsed_arg(self) -> Dict[str, Any]:
""" """
@ -73,7 +82,7 @@ class Arguments:
:return: List[str] List of arguments :return: List[str] List of arguments
""" """
if self._parsed_arg is None: if self._parsed_arg is None:
self._load_args() self._build_subcommands()
self._parsed_arg = self._parse_args() self._parsed_arg = self._parse_args()
return vars(self._parsed_arg) return vars(self._parsed_arg)
@ -84,22 +93,17 @@ class Arguments:
""" """
parsed_arg = self.parser.parse_args(self.args) parsed_arg = self.parser.parse_args(self.args)
# When no config is provided, but a config exists, use that configuration!
subparser = parsed_arg.subparser if 'subparser' in parsed_arg else None
# Workaround issue in argparse with action='append' and default value # Workaround issue in argparse with action='append' and default value
# (see https://bugs.python.org/issue16399) # (see https://bugs.python.org/issue16399)
# Allow no-config for certain commands (like downloading / plotting) # Allow no-config for certain commands (like downloading / plotting)
if (parsed_arg.config is None if ('config' in parsed_arg and parsed_arg.config is None and
and subparser not in NO_CONF_ALLOWED ((Path.cwd() / constants.DEFAULT_CONFIG).is_file() or
and ((Path.cwd() / constants.DEFAULT_CONFIG).is_file() not ('command' in parsed_arg and parsed_arg.command in NO_CONF_REQURIED))):
or (subparser not in NO_CONF_REQURIED))):
parsed_arg.config = [constants.DEFAULT_CONFIG] parsed_arg.config = [constants.DEFAULT_CONFIG]
return parsed_arg return parsed_arg
def _build_args(self, optionlist, parser=None): def _build_args(self, optionlist, parser):
parser = parser or self.parser
for val in optionlist: for val in optionlist:
opt = AVAILABLE_CLI_OPTIONS[val] opt = AVAILABLE_CLI_OPTIONS[val]
@ -110,38 +114,82 @@ class Arguments:
Builds and attaches all subcommands. Builds and attaches all subcommands.
:return: None :return: None
""" """
# Build shared arguments (as group Common Options)
_common_parser = argparse.ArgumentParser(add_help=False)
group = _common_parser.add_argument_group("Common arguments")
self._build_args(optionlist=ARGS_COMMON, parser=group)
_strategy_parser = argparse.ArgumentParser(add_help=False)
strategy_group = _strategy_parser.add_argument_group("Strategy arguments")
self._build_args(optionlist=ARGS_STRATEGY, parser=strategy_group)
# Build main command
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
self._build_args(optionlist=['version'], parser=self.parser)
from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge
from freqtrade.utils import (start_create_userdir, start_download_data, from freqtrade.utils import (start_create_userdir, start_download_data,
start_list_exchanges, start_list_timeframes, start_hyperopt_list, start_hyperopt_show,
start_list_markets) start_list_exchanges, start_list_markets,
start_new_hyperopt, start_new_strategy,
start_list_timeframes, start_test_pairlist, start_trading)
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
subparsers = self.parser.add_subparsers(dest='subparser') subparsers = self.parser.add_subparsers(dest='command',
# Use custom message when no subhandler is added
# shown from `main.py`
# required=True
)
# Add trade subcommand
trade_cmd = subparsers.add_parser('trade', help='Trade module.',
parents=[_common_parser, _strategy_parser])
trade_cmd.set_defaults(func=start_trading)
self._build_args(optionlist=ARGS_TRADE, parser=trade_cmd)
# Add backtesting subcommand # Add backtesting subcommand
backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.',
parents=[_common_parser, _strategy_parser])
backtesting_cmd.set_defaults(func=start_backtesting) backtesting_cmd.set_defaults(func=start_backtesting)
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
# Add edge subcommand # Add edge subcommand
edge_cmd = subparsers.add_parser('edge', help='Edge module.') edge_cmd = subparsers.add_parser('edge', help='Edge module.',
parents=[_common_parser, _strategy_parser])
edge_cmd.set_defaults(func=start_edge) edge_cmd.set_defaults(func=start_edge)
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd) self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
# Add hyperopt subcommand # Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.',
parents=[_common_parser, _strategy_parser],
)
hyperopt_cmd.set_defaults(func=start_hyperopt) hyperopt_cmd.set_defaults(func=start_hyperopt)
self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd) self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd)
# add create-userdir subcommand # add create-userdir subcommand
create_userdir_cmd = subparsers.add_parser('create-userdir', create_userdir_cmd = subparsers.add_parser('create-userdir',
help="Create user-data directory.") help="Create user-data directory.",
)
create_userdir_cmd.set_defaults(func=start_create_userdir) create_userdir_cmd.set_defaults(func=start_create_userdir)
self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_cmd) self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_cmd)
# add new-strategy subcommand
build_strategy_cmd = subparsers.add_parser('new-strategy',
help="Create new strategy")
build_strategy_cmd.set_defaults(func=start_new_strategy)
self._build_args(optionlist=ARGS_BUILD_STRATEGY, parser=build_strategy_cmd)
# add new-hyperopt subcommand
build_hyperopt_cmd = subparsers.add_parser('new-hyperopt',
help="Create new hyperopt")
build_hyperopt_cmd.set_defaults(func=start_new_hyperopt)
self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_cmd)
# Add list-exchanges subcommand # Add list-exchanges subcommand
list_exchanges_cmd = subparsers.add_parser( list_exchanges_cmd = subparsers.add_parser(
'list-exchanges', 'list-exchanges',
help='Print available exchanges.' help='Print available exchanges.',
parents=[_common_parser],
) )
list_exchanges_cmd.set_defaults(func=start_list_exchanges) list_exchanges_cmd.set_defaults(func=start_list_exchanges)
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
@ -149,7 +197,8 @@ class Arguments:
# Add list-timeframes subcommand # Add list-timeframes subcommand
list_timeframes_cmd = subparsers.add_parser( list_timeframes_cmd = subparsers.add_parser(
'list-timeframes', 'list-timeframes',
help='Print available ticker intervals (timeframes) for the exchange.' help='Print available ticker intervals (timeframes) for the exchange.',
parents=[_common_parser],
) )
list_timeframes_cmd.set_defaults(func=start_list_timeframes) list_timeframes_cmd.set_defaults(func=start_list_timeframes)
self._build_args(optionlist=ARGS_LIST_TIMEFRAMES, parser=list_timeframes_cmd) self._build_args(optionlist=ARGS_LIST_TIMEFRAMES, parser=list_timeframes_cmd)
@ -157,7 +206,8 @@ class Arguments:
# Add list-markets subcommand # Add list-markets subcommand
list_markets_cmd = subparsers.add_parser( list_markets_cmd = subparsers.add_parser(
'list-markets', 'list-markets',
help='Print markets on exchange.' help='Print markets on exchange.',
parents=[_common_parser],
) )
list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False)) list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False))
self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_markets_cmd) self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_markets_cmd)
@ -165,24 +215,34 @@ class Arguments:
# Add list-pairs subcommand # Add list-pairs subcommand
list_pairs_cmd = subparsers.add_parser( list_pairs_cmd = subparsers.add_parser(
'list-pairs', 'list-pairs',
help='Print pairs on exchange.' help='Print pairs on exchange.',
parents=[_common_parser],
) )
list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True)) list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True))
self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_pairs_cmd) self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_pairs_cmd)
# Add test-pairlist subcommand
test_pairlist_cmd = subparsers.add_parser(
'test-pairlist',
help='Test your pairlist configuration.',
)
test_pairlist_cmd.set_defaults(func=start_test_pairlist)
self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd)
# Add download-data subcommand # Add download-data subcommand
download_data_cmd = subparsers.add_parser( download_data_cmd = subparsers.add_parser(
'download-data', 'download-data',
help='Download backtesting data.' help='Download backtesting data.',
parents=[_common_parser],
) )
download_data_cmd.set_defaults(func=start_download_data) download_data_cmd.set_defaults(func=start_download_data)
self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd) self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd)
# Add Plotting subcommand # Add Plotting subcommand
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
plot_dataframe_cmd = subparsers.add_parser( plot_dataframe_cmd = subparsers.add_parser(
'plot-dataframe', 'plot-dataframe',
help='Plot candles with indicators.' help='Plot candles with indicators.',
parents=[_common_parser, _strategy_parser],
) )
plot_dataframe_cmd.set_defaults(func=start_plot_dataframe) plot_dataframe_cmd.set_defaults(func=start_plot_dataframe)
self._build_args(optionlist=ARGS_PLOT_DATAFRAME, parser=plot_dataframe_cmd) self._build_args(optionlist=ARGS_PLOT_DATAFRAME, parser=plot_dataframe_cmd)
@ -190,7 +250,26 @@ class Arguments:
# Plot profit # Plot profit
plot_profit_cmd = subparsers.add_parser( plot_profit_cmd = subparsers.add_parser(
'plot-profit', 'plot-profit',
help='Generate plot showing profits.' help='Generate plot showing profits.',
parents=[_common_parser],
) )
plot_profit_cmd.set_defaults(func=start_plot_profit) plot_profit_cmd.set_defaults(func=start_plot_profit)
self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd) self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd)
# Add hyperopt-list subcommand
hyperopt_list_cmd = subparsers.add_parser(
'hyperopt-list',
help='List Hyperopt results',
parents=[_common_parser],
)
hyperopt_list_cmd.set_defaults(func=start_hyperopt_list)
self._build_args(optionlist=ARGS_HYPEROPT_LIST, parser=hyperopt_list_cmd)
# Add hyperopt-show subcommand
hyperopt_show_cmd = subparsers.add_parser(
'hyperopt-show',
help='Show details of Hyperopt results',
parents=[_common_parser],
)
hyperopt_show_cmd.set_defaults(func=start_hyperopt_show)
self._build_args(optionlist=ARGS_HYPEROPT_SHOW, parser=hyperopt_show_cmd)

View File

@ -10,6 +10,19 @@ from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def remove_credentials(config: Dict[str, Any]):
"""
Removes exchange keys from the configuration and specifies dry-run
Used for backtesting / hyperopt / edge and utils.
Modifies the input dict!
"""
config['exchange']['key'] = ''
config['exchange']['secret'] = ''
config['exchange']['password'] = ''
config['exchange']['uid'] = ''
config['dry_run'] = True
def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
""" """
Check if the exchange name in the config file is supported by Freqtrade Check if the exchange name in the config file is supported by Freqtrade
@ -21,7 +34,8 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
and thus is not known for the Freqtrade at all. and thus is not known for the Freqtrade at all.
""" """
if config['runmode'] in [RunMode.PLOT] and not config.get('exchange', {}).get('name'): if (config['runmode'] in [RunMode.PLOT, RunMode.UTIL_NO_EXCHANGE, RunMode.OTHER]
and not config.get('exchange', {}).get('name')):
# Skip checking exchange in plot mode, since it requires no exchange # Skip checking exchange in plot mode, since it requires no exchange
return True return True
logger.info("Checking exchange...") logger.info("Checking exchange...")

View File

@ -18,6 +18,18 @@ def check_int_positive(value: str) -> int:
return uint return uint
def check_int_nonzero(value: str) -> int:
try:
uint = int(value)
if uint == 0:
raise ValueError
except ValueError:
raise argparse.ArgumentTypeError(
f"{value} is invalid for this parameter, should be a non-zero integer value"
)
return uint
class Arg: class Arg:
# Optional CLI arguments # Optional CLI arguments
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -36,7 +48,8 @@ AVAILABLE_CLI_OPTIONS = {
), ),
"logfile": Arg( "logfile": Arg(
'--logfile', '--logfile',
help='Log to the file specified.', help="Log to the file specified. Special values are: 'syslog', 'journald'. "
"See the documentation for more details.",
metavar='FILE', metavar='FILE',
), ),
"version": Arg( "version": Arg(
@ -62,12 +75,16 @@ AVAILABLE_CLI_OPTIONS = {
help='Path to userdata directory.', help='Path to userdata directory.',
metavar='PATH', metavar='PATH',
), ),
"reset": Arg(
'--reset',
help='Reset sample files to their original state.',
action='store_true',
),
# Main options # Main options
"strategy": Arg( "strategy": Arg(
'-s', '--strategy', '-s', '--strategy',
help='Specify strategy class name (default: `%(default)s`).', help='Specify strategy class name which will be used by the bot.',
metavar='NAME', metavar='NAME',
default='DefaultStrategy',
), ),
"strategy_path": Arg( "strategy_path": Arg(
'--strategy-path', '--strategy-path',
@ -86,6 +103,11 @@ AVAILABLE_CLI_OPTIONS = {
help='Notify systemd service manager.', help='Notify systemd service manager.',
action='store_true', action='store_true',
), ),
"dry_run": Arg(
'--dry-run',
help='Enforce dry-run for trading (removes Exchange secrets and simulates trades).',
action='store_true',
),
# Optimize common # Optimize common
"ticker_interval": Arg( "ticker_interval": Arg(
'-i', '--ticker-interval', '-i', '--ticker-interval',
@ -136,7 +158,7 @@ AVAILABLE_CLI_OPTIONS = {
), ),
"exportfilename": Arg( "exportfilename": Arg(
'--export-filename', '--export-filename',
help='Save backtest results to the file with this filename (default: `%(default)s`). ' help='Save backtest results to the file with this filename. '
'Requires `--export` to be set as well. ' 'Requires `--export` to be set as well. '
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`', 'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
metavar='PATH', metavar='PATH',
@ -156,14 +178,13 @@ AVAILABLE_CLI_OPTIONS = {
), ),
# Hyperopt # Hyperopt
"hyperopt": Arg( "hyperopt": Arg(
'--customhyperopt', '--hyperopt',
help='Specify hyperopt class name (default: `%(default)s`).', help='Specify hyperopt class name which will be used by the bot.',
metavar='NAME', metavar='NAME',
default=constants.DEFAULT_HYPEROPT,
), ),
"hyperopt_path": Arg( "hyperopt_path": Arg(
'--hyperopt-path', '--hyperopt-path',
help='Specify additional lookup path for Hyperopts and Hyperopt Loss functions.', help='Specify additional lookup path for Hyperopt and Hyperopt Loss functions.',
metavar='PATH', metavar='PATH',
), ),
"epochs": Arg( "epochs": Arg(
@ -174,12 +195,11 @@ AVAILABLE_CLI_OPTIONS = {
default=constants.HYPEROPT_EPOCH, default=constants.HYPEROPT_EPOCH,
), ),
"spaces": Arg( "spaces": Arg(
'-s', '--spaces', '--spaces',
help='Specify which parameters to hyperopt. Space-separated list. ' help='Specify which parameters to hyperopt. Space-separated list.',
'Default: `%(default)s`.', choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'default'],
choices=['all', 'buy', 'sell', 'roi', 'stoploss'],
nargs='+', nargs='+',
default='all', default='default',
), ),
"print_all": Arg( "print_all": Arg(
'--print-all', '--print-all',
@ -331,6 +351,14 @@ AVAILABLE_CLI_OPTIONS = {
help='Clean all existing data for the selected exchange/pairs/timeframes.', help='Clean all existing data for the selected exchange/pairs/timeframes.',
action='store_true', action='store_true',
), ),
# Templating options
"template": Arg(
'--template',
help='Use a template which is either `minimal` or '
'`full` (containing multiple sample indicators). Default: `%(default)s`.',
choices=['full', 'minimal'],
default='full',
),
# Plot dataframe # Plot dataframe
"indicators1": Arg( "indicators1": Arg(
'--indicators1', '--indicators1',
@ -361,4 +389,31 @@ AVAILABLE_CLI_OPTIONS = {
choices=["DB", "file"], choices=["DB", "file"],
default="file", default="file",
), ),
# hyperopt-list, hyperopt-show
"hyperopt_list_profitable": Arg(
'--profitable',
help='Select only profitable epochs.',
action='store_true',
),
"hyperopt_list_best": Arg(
'--best',
help='Select only best epochs.',
action='store_true',
),
"hyperopt_list_no_details": Arg(
'--no-details',
help='Do not print best epoch details.',
action='store_true',
),
"hyperopt_show_index": Arg(
'-n', '--index',
help='Specify the index of the epoch to print details for.',
type=check_int_nonzero,
metavar='INT',
),
"hyperopt_show_no_header": Arg(
'--no-header',
help='Do not print epoch details header.',
action='store_true',
),
} }

View File

@ -5,7 +5,7 @@ from jsonschema import Draft4Validator, validators
from jsonschema.exceptions import ValidationError, best_match from jsonschema.exceptions import ValidationError, best_match
from freqtrade import constants, OperationalException from freqtrade import constants, OperationalException
from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -61,9 +61,15 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None:
:param conf: Config in JSON format :param conf: Config in JSON format
:return: Returns None if everything is ok, otherwise throw an OperationalException :return: Returns None if everything is ok, otherwise throw an OperationalException
""" """
# validating trailing stoploss # validating trailing stoploss
_validate_trailing_stoploss(conf) _validate_trailing_stoploss(conf)
_validate_edge(conf) _validate_edge(conf)
_validate_whitelist(conf)
# validate configuration before returning
logger.info('Validating configuration ...')
validate_config_schema(conf)
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
@ -111,3 +117,29 @@ def _validate_edge(conf: Dict[str, Any]) -> None:
"Edge and VolumePairList are incompatible, " "Edge and VolumePairList are incompatible, "
"Edge will override whatever pairs VolumePairlist selects." "Edge will override whatever pairs VolumePairlist selects."
) )
def _validate_whitelist(conf: Dict[str, Any]) -> None:
"""
Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does.
"""
if conf.get('runmode', RunMode.OTHER) in [RunMode.OTHER, RunMode.PLOT,
RunMode.UTIL_NO_EXCHANGE, RunMode.UTIL_EXCHANGE]:
return
for pl in conf.get('pairlists', [{'method': 'StaticPairList'}]):
if (pl.get('method') == 'StaticPairList'
and not conf.get('exchange', {}).get('pair_whitelist')):
raise OperationalException("StaticPairList requires pair_whitelist to be set.")
if pl.get('method') == 'StaticPairList':
stake = conf['stake_currency']
invalid_pairs = []
for pair in conf['exchange'].get('pair_whitelist'):
if not pair.endswith(f'/{stake}'):
invalid_pairs.append(pair)
if invalid_pairs:
raise OperationalException(
f"Stake-currency '{stake}' not compatible with pair-whitelist. "
f"Please remove the following pairs: {invalid_pairs}")

View File

@ -9,15 +9,13 @@ from typing import Any, Callable, Dict, List, Optional
from freqtrade import OperationalException, constants from freqtrade import OperationalException, constants
from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.check_exchange import check_exchange
from freqtrade.configuration.config_validation import (validate_config_consistency,
validate_config_schema)
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
from freqtrade.configuration.directory_operations import (create_datadir, from freqtrade.configuration.directory_operations import (create_datadir,
create_userdata_dir) create_userdata_dir)
from freqtrade.configuration.load_config import load_config_file from freqtrade.configuration.load_config import load_config_file
from freqtrade.loggers import setup_logging from freqtrade.loggers import setup_logging
from freqtrade.misc import deep_merge_dicts, json_load from freqtrade.misc import deep_merge_dicts, json_load
from freqtrade.state import RunMode from freqtrade.state import RunMode, TRADING_MODES, NON_UTIL_MODES
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -81,9 +79,8 @@ class Configuration:
if 'ask_strategy' not in config: if 'ask_strategy' not in config:
config['ask_strategy'] = {} config['ask_strategy'] = {}
# validate configuration before returning if 'pairlists' not in config:
logger.info('Validating configuration ...') config['pairlists'] = []
validate_config_schema(config)
return config return config
@ -93,19 +90,21 @@ class Configuration:
:return: Configuration dictionary :return: Configuration dictionary
""" """
# Load all configs # Load all configs
config: Dict[str, Any] = self.load_from_files(self.args["config"]) config: Dict[str, Any] = self.load_from_files(self.args.get("config", []))
# Keep a copy of the original configuration file # Keep a copy of the original configuration file
config['original_config'] = deepcopy(config) config['original_config'] = deepcopy(config)
self._process_runmode(config)
self._process_common_options(config) self._process_common_options(config)
self._process_trading_options(config)
self._process_optimize_options(config) self._process_optimize_options(config)
self._process_plot_options(config) self._process_plot_options(config)
self._process_runmode(config)
# Check if the exchange set by the user is supported # Check if the exchange set by the user is supported
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
@ -113,8 +112,6 @@ class Configuration:
process_temporary_deprecated_settings(config) process_temporary_deprecated_settings(config)
validate_config_consistency(config)
return config return config
def _process_logging_options(self, config: Dict[str, Any]) -> None: def _process_logging_options(self, config: Dict[str, Any]) -> None:
@ -130,21 +127,9 @@ class Configuration:
setup_logging(config) setup_logging(config)
def _process_common_options(self, config: Dict[str, Any]) -> None: def _process_trading_options(self, config: Dict[str, Any]) -> None:
if config['runmode'] not in TRADING_MODES:
self._process_logging_options(config) return
# Set strategy if not specified in config and or if it's non default
if self.args.get("strategy") != constants.DEFAULT_STRATEGY or not config.get('strategy'):
config.update({'strategy': self.args.get("strategy")})
self._args_to_config(config, argname='strategy_path',
logstring='Using additional Strategy lookup path: {}')
if ('db_url' in self.args and self.args["db_url"] and
self.args["db_url"] != constants.DEFAULT_DB_PROD_URL):
config.update({'db_url': self.args["db_url"]})
logger.info('Parameter --db-url detected ...')
if config.get('dry_run', False): if config.get('dry_run', False):
logger.info('Dry run is enabled') logger.info('Dry run is enabled')
@ -158,17 +143,33 @@ class Configuration:
logger.info(f'Using DB: "{config["db_url"]}"') logger.info(f'Using DB: "{config["db_url"]}"')
def _process_common_options(self, config: Dict[str, Any]) -> None:
self._process_logging_options(config)
# Set strategy if not specified in config and or if it's non default
if self.args.get("strategy") or not config.get('strategy'):
config.update({'strategy': self.args.get("strategy")})
self._args_to_config(config, argname='strategy_path',
logstring='Using additional Strategy lookup path: {}')
if ('db_url' in self.args and self.args["db_url"] and
self.args["db_url"] != constants.DEFAULT_DB_PROD_URL):
config.update({'db_url': self.args["db_url"]})
logger.info('Parameter --db-url detected ...')
if config.get('forcebuy_enable', False): if config.get('forcebuy_enable', False):
logger.warning('`forcebuy` RPC message enabled.') logger.warning('`forcebuy` RPC message enabled.')
# Setting max_open_trades to infinite if -1
if config.get('max_open_trades') == -1:
config['max_open_trades'] = float('inf')
# Support for sd_notify # Support for sd_notify
if 'sd_notify' in self.args and self.args["sd_notify"]: if 'sd_notify' in self.args and self.args["sd_notify"]:
config['internals'].update({'sd_notify': True}) config['internals'].update({'sd_notify': True})
self._args_to_config(config, argname='dry_run',
logstring='Parameter --dry-run detected, '
'overriding dry_run to: {} ...')
def _process_datadir_options(self, config: Dict[str, Any]) -> None: def _process_datadir_options(self, config: Dict[str, Any]) -> None:
""" """
Extract information for sys.argv and load directory configurations Extract information for sys.argv and load directory configurations
@ -179,6 +180,9 @@ class Configuration:
config['exchange']['name'] = self.args["exchange"] config['exchange']['name'] = self.args["exchange"]
logger.info(f"Using exchange {config['exchange']['name']}") logger.info(f"Using exchange {config['exchange']['name']}")
if 'pair_whitelist' not in config['exchange']:
config['exchange']['pair_whitelist'] = []
if 'user_data_dir' in self.args and self.args["user_data_dir"]: if 'user_data_dir' in self.args and self.args["user_data_dir"]:
config.update({'user_data_dir': self.args["user_data_dir"]}) config.update({'user_data_dir': self.args["user_data_dir"]})
elif 'user_data_dir' not in config: elif 'user_data_dir' not in config:
@ -209,6 +213,10 @@ class Configuration:
self._args_to_config(config, argname='position_stacking', self._args_to_config(config, argname='position_stacking',
logstring='Parameter --enable-position-stacking detected ...') logstring='Parameter --enable-position-stacking detected ...')
# Setting max_open_trades to infinite if -1
if config.get('max_open_trades') == -1:
config['max_open_trades'] = float('inf')
if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]: if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]:
config.update({'use_max_market_positions': False}) config.update({'use_max_market_positions': False})
logger.info('Parameter --disable-max-market-positions detected ...') logger.info('Parameter --disable-max-market-positions detected ...')
@ -217,7 +225,7 @@ class Configuration:
config.update({'max_open_trades': self.args["max_open_trades"]}) config.update({'max_open_trades': self.args["max_open_trades"]})
logger.info('Parameter --max_open_trades detected, ' logger.info('Parameter --max_open_trades detected, '
'overriding max_open_trades to: %s ...', config.get('max_open_trades')) 'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
else: elif config['runmode'] in NON_UTIL_MODES:
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
self._args_to_config(config, argname='stake_amount', self._args_to_config(config, argname='stake_amount',
@ -292,6 +300,21 @@ class Configuration:
self._args_to_config(config, argname='hyperopt_loss', self._args_to_config(config, argname='hyperopt_loss',
logstring='Using Hyperopt loss class name: {}') logstring='Using Hyperopt loss class name: {}')
self._args_to_config(config, argname='hyperopt_show_index',
logstring='Parameter -n/--index detected: {}')
self._args_to_config(config, argname='hyperopt_list_best',
logstring='Parameter --best detected: {}')
self._args_to_config(config, argname='hyperopt_list_profitable',
logstring='Parameter --profitable detected: {}')
self._args_to_config(config, argname='hyperopt_list_no_details',
logstring='Parameter --no-details detected: {}')
self._args_to_config(config, argname='hyperopt_show_no_header',
logstring='Parameter --no-header detected: {}')
def _process_plot_options(self, config: Dict[str, Any]) -> None: def _process_plot_options(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='pairs', self._args_to_config(config, argname='pairs',

View File

@ -57,3 +57,26 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
'experimental', 'sell_profit_only') 'experimental', 'sell_profit_only')
process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal', process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal',
'experimental', 'ignore_roi_if_buy_signal') 'experimental', 'ignore_roi_if_buy_signal')
if not config.get('pairlists') and not config.get('pairlists'):
config['pairlists'] = [{'method': 'StaticPairList'}]
logger.warning(
"DEPRECATED: "
"Pairlists must be defined explicitly in the future."
"Defaulting to StaticPairList for now.")
if config.get('pairlist', {}).get("method") == 'VolumePairList':
logger.warning(
"DEPRECATED: "
f"Using VolumePairList in pairlist is deprecated and must be moved to pairlists. "
"Please refer to the docs on configuration details")
pl = {'method': 'VolumePairList'}
pl.update(config.get('pairlist', {}).get('config'))
config['pairlists'].append(pl)
if config.get('pairlist', {}).get('config', {}).get('precision_filter'):
logger.warning(
"DEPRECATED: "
f"Using precision_filter setting is deprecated and has been replaced by"
"PrecisionFilter. Please refer to the docs on configuration details")
config['pairlists'].append({'method': 'PrecisionFilter'})

View File

@ -1,8 +1,10 @@
import logging import logging
from typing import Any, Dict, Optional import shutil
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.constants import USER_DATA_FILES
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,7 +33,8 @@ def create_userdata_dir(directory: str, create_dir=False) -> Path:
:param create_dir: Create directory if it does not exist. :param create_dir: Create directory if it does not exist.
:return: Path object containing the directory :return: Path object containing the directory
""" """
sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "plot", "strategies", ] sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "notebooks",
"plot", "strategies", ]
folder = Path(directory) folder = Path(directory)
if not folder.is_dir(): if not folder.is_dir():
if create_dir: if create_dir:
@ -48,3 +51,26 @@ def create_userdata_dir(directory: str, create_dir=False) -> Path:
if not subfolder.is_dir(): if not subfolder.is_dir():
subfolder.mkdir(parents=False) subfolder.mkdir(parents=False)
return folder return folder
def copy_sample_files(directory: Path, overwrite: bool = False) -> None:
"""
Copy files from templates to User data directory.
:param directory: Directory to copy data to
:param overwrite: Overwrite existing sample files
"""
if not directory.is_dir():
raise OperationalException(f"Directory `{directory}` does not exist.")
sourcedir = Path(__file__).parents[1] / "templates"
for source, target in USER_DATA_FILES.items():
targetdir = directory / target
if not targetdir.is_dir():
raise OperationalException(f"Directory `{targetdir}` does not exist.")
targetfile = targetdir / source
if targetfile.exists():
if not overwrite:
logger.warning(f"File `{targetfile}` exists already, not deploying sample file.")
continue
else:
logger.warning(f"File `{targetfile}` exists already, overwriting.")
shutil.copy(str(sourcedir / source), str(targetfile))

View File

@ -1,11 +1,14 @@
""" """
This module contains the argument manager class This module contains the argument manager class
""" """
import logging
import re import re
from typing import Optional from typing import Optional
import arrow import arrow
logger = logging.getLogger(__name__)
class TimeRange: class TimeRange:
""" """
@ -27,6 +30,34 @@ class TimeRange:
return (self.starttype == other.starttype and self.stoptype == other.stoptype return (self.starttype == other.starttype and self.stoptype == other.stoptype
and self.startts == other.startts and self.stopts == other.stopts) and self.startts == other.startts and self.stopts == other.stopts)
def subtract_start(self, seconds) -> None:
"""
Subtracts <seconds> from startts if startts is set.
:param seconds: Seconds to subtract from starttime
:return: None (Modifies the object in place)
"""
if self.startts:
self.startts = self.startts - seconds
def adjust_start_if_necessary(self, timeframe_secs: int, startup_candles: int,
min_date: arrow.Arrow) -> None:
"""
Adjust startts by <startup_candles> candles.
Applies only if no startup-candles have been available.
:param timeframe_secs: Ticker timeframe in seconds e.g. `timeframe_to_seconds('5m')`
:param startup_candles: Number of candles to move start-date forward
:param min_date: Minimum data date loaded. Key kriterium to decide if start-time
has to be moved
:return: None (Modifies the object in place)
"""
if (not self.starttype or (startup_candles
and min_date.timestamp >= self.startts)):
# If no startts was defined, or backtest-data starts at the defined backtest-date
logger.warning("Moving start-date by %s candles to account for startup time.",
startup_candles)
self.startts = (min_date.timestamp + timeframe_secs * startup_candles)
self.starttype = 'date'
@staticmethod @staticmethod
def parse_timerange(text: Optional[str]): def parse_timerange(text: Optional[str]):
""" """

View File

@ -6,11 +6,8 @@ bot constants
DEFAULT_CONFIG = 'config.json' DEFAULT_CONFIG = 'config.json'
DEFAULT_EXCHANGE = 'bittrex' DEFAULT_EXCHANGE = 'bittrex'
PROCESS_THROTTLE_SECS = 5 # sec PROCESS_THROTTLE_SECS = 5 # sec
DEFAULT_TICKER_INTERVAL = 5 # min
HYPEROPT_EPOCH = 100 # epochs HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec RETRY_TIMEOUT = 30 # sec
DEFAULT_STRATEGY = 'DefaultStrategy'
DEFAULT_HYPEROPT = 'DefaultHyperOpt'
DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite://' DEFAULT_DB_DRYRUN_URL = 'sqlite://'
@ -20,11 +17,23 @@ REQUIRED_ORDERTIF = ['buy', 'sell']
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTYPE_POSSIBILITIES = ['limit', 'market']
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'PriceFilter']
DRY_RUN_WALLET = 999.9 DRY_RUN_WALLET = 999.9
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
TICKER_INTERVALS = [ USERPATH_HYPEROPTS = 'hyperopts'
USERPATH_STRATEGY = 'strategies'
# Soure files with destination directories within user-directory
USER_DATA_FILES = {
'sample_strategy.py': USERPATH_STRATEGY,
'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS,
'sample_hyperopt_loss.py': USERPATH_HYPEROPTS,
'sample_hyperopt.py': USERPATH_HYPEROPTS,
'strategy_analysis_example.ipynb': 'notebooks',
}
TIMEFRAMES = [
'1m', '3m', '5m', '15m', '30m', '1m', '3m', '5m', '15m', '30m',
'1h', '2h', '4h', '6h', '8h', '12h', '1h', '2h', '4h', '6h', '8h', '12h',
'1d', '3d', '1w', '1d', '3d', '1w',
@ -56,13 +65,13 @@ MINIMAL_CONFIG = {
CONF_SCHEMA = { CONF_SCHEMA = {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'max_open_trades': {'type': 'integer', 'minimum': -1}, 'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1},
'ticker_interval': {'type': 'string', 'enum': TICKER_INTERVALS}, 'ticker_interval': {'type': 'string', 'enum': TIMEFRAMES},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']},
'stake_amount': { 'stake_amount': {
"type": ["number", "string"], 'type': ['number', 'string'],
"minimum": 0.0005, 'minimum': 0.0001,
"pattern": UNLIMITED_STAKE_AMOUNT 'pattern': UNLIMITED_STAKE_AMOUNT
}, },
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
'dry_run': {'type': 'boolean'}, 'dry_run': {'type': 'boolean'},
@ -84,8 +93,8 @@ CONF_SCHEMA = {
'unfilledtimeout': { 'unfilledtimeout': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'buy': {'type': 'number', 'minimum': 3}, 'buy': {'type': 'number', 'minimum': 1},
'sell': {'type': 'number', 'minimum': 10} 'sell': {'type': 'number', 'minimum': 1}
} }
}, },
'bid_strategy': { 'bid_strategy': {
@ -97,7 +106,7 @@ CONF_SCHEMA = {
'maximum': 1, 'maximum': 1,
'exclusiveMaximum': False, 'exclusiveMaximum': False,
'use_order_book': {'type': 'boolean'}, 'use_order_book': {'type': 'boolean'},
'order_book_top': {'type': 'number', 'maximum': 20, 'minimum': 1}, 'order_book_top': {'type': 'integer', 'maximum': 20, 'minimum': 1},
'check_depth_of_market': { 'check_depth_of_market': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
@ -113,8 +122,8 @@ CONF_SCHEMA = {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'use_order_book': {'type': 'boolean'}, 'use_order_book': {'type': 'boolean'},
'order_book_min': {'type': 'number', 'minimum': 1}, 'order_book_min': {'type': 'integer', 'minimum': 1},
'order_book_max': {'type': 'number', 'minimum': 1, 'maximum': 50}, 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50},
'use_sell_signal': {'type': 'boolean'}, 'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'}, 'sell_profit_only': {'type': 'boolean'},
'ignore_roi_if_buy_signal': {'type': 'boolean'} 'ignore_roi_if_buy_signal': {'type': 'boolean'}
@ -151,13 +160,16 @@ CONF_SCHEMA = {
'block_bad_exchanges': {'type': 'boolean'} 'block_bad_exchanges': {'type': 'boolean'}
} }
}, },
'pairlist': { 'pairlists': {
'type': 'array',
'items': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS}, 'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS},
'config': {'type': 'object'} 'config': {'type': 'object'}
}, },
'required': ['method'] 'required': ['method'],
}
}, },
'telegram': { 'telegram': {
'type': 'object', 'type': 'object',
@ -184,8 +196,8 @@ CONF_SCHEMA = {
'listen_ip_address': {'format': 'ipv4'}, 'listen_ip_address': {'format': 'ipv4'},
'listen_port': { 'listen_port': {
'type': 'integer', 'type': 'integer',
"minimum": 1024, 'minimum': 1024,
"maximum": 65535 'maximum': 65535
}, },
'username': {'type': 'string'}, 'username': {'type': 'string'},
'password': {'type': 'string'}, 'password': {'type': 'string'},
@ -198,7 +210,7 @@ CONF_SCHEMA = {
'internals': { 'internals': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'process_throttle_secs': {'type': 'number'}, 'process_throttle_secs': {'type': 'integer'},
'interval': {'type': 'integer'}, 'interval': {'type': 'integer'},
'sd_notify': {'type': 'boolean'}, 'sd_notify': {'type': 'boolean'},
} }
@ -235,37 +247,37 @@ CONF_SCHEMA = {
'ccxt_config': {'type': 'object'}, 'ccxt_config': {'type': 'object'},
'ccxt_async_config': {'type': 'object'} 'ccxt_async_config': {'type': 'object'}
}, },
'required': ['name', 'pair_whitelist'] 'required': ['name']
}, },
'edge': { 'edge': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
"enabled": {'type': 'boolean'}, 'enabled': {'type': 'boolean'},
"process_throttle_secs": {'type': 'integer', 'minimum': 600}, 'process_throttle_secs': {'type': 'integer', 'minimum': 600},
"calculate_since_number_of_days": {'type': 'integer'}, 'calculate_since_number_of_days': {'type': 'integer'},
"allowed_risk": {'type': 'number'}, 'allowed_risk': {'type': 'number'},
"capital_available_percentage": {'type': 'number'}, 'capital_available_percentage': {'type': 'number'},
"stoploss_range_min": {'type': 'number'}, 'stoploss_range_min': {'type': 'number'},
"stoploss_range_max": {'type': 'number'}, 'stoploss_range_max': {'type': 'number'},
"stoploss_range_step": {'type': 'number'}, 'stoploss_range_step': {'type': 'number'},
"minimum_winrate": {'type': 'number'}, 'minimum_winrate': {'type': 'number'},
"minimum_expectancy": {'type': 'number'}, 'minimum_expectancy': {'type': 'number'},
"min_trade_number": {'type': 'number'}, 'min_trade_number': {'type': 'number'},
"max_trade_duration_minute": {'type': 'integer'}, 'max_trade_duration_minute': {'type': 'integer'},
"remove_pumps": {'type': 'boolean'} 'remove_pumps': {'type': 'boolean'}
}, },
'required': ['process_throttle_secs', 'allowed_risk', 'capital_available_percentage'] 'required': ['process_throttle_secs', 'allowed_risk', 'capital_available_percentage']
} }
}, },
'anyOf': [
{'required': ['exchange']}
],
'required': [ 'required': [
'exchange',
'max_open_trades', 'max_open_trades',
'stake_currency', 'stake_currency',
'stake_amount', 'stake_amount',
'dry_run', 'dry_run',
'bid_strategy', 'bid_strategy',
'unfilledtimeout', 'unfilledtimeout',
'stoploss',
'minimal_roi',
] ]
} }

View File

@ -7,7 +7,7 @@ from typing import Dict
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import pytz from datetime import timezone
from freqtrade import persistence from freqtrade import persistence
from freqtrade.misc import json_load from freqtrade.misc import json_load
@ -52,16 +52,18 @@ def load_backtest_data(filename) -> pd.DataFrame:
return df return df
def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int) -> pd.DataFrame: def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataFrame:
""" """
Find overlapping trades by expanding each trade once per period it was open Find overlapping trades by expanding each trade once per period it was open
and then counting overlaps and then counting overlaps.
:param results: Results Dataframe - can be loaded :param results: Results Dataframe - can be loaded
:param freq: Frequency used for the backtest :param timeframe: Timeframe used for backtest
:param max_open_trades: parameter max_open_trades used during backtest run :return: dataframe with open-counts per time-period in timeframe
:return: dataframe with open-counts per time-period in freq
""" """
dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, freq=freq)) from freqtrade.exchange import timeframe_to_minutes
timeframe_min = timeframe_to_minutes(timeframe)
dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time,
freq=f"{timeframe_min}min"))
for row in results[['open_time', 'close_time']].iterrows()] for row in results[['open_time', 'close_time']].iterrows()]
deltas = [len(x) for x in dates] deltas = [len(x) for x in dates]
dates = pd.Series(pd.concat(dates).values, name='date') dates = pd.Series(pd.concat(dates).values, name='date')
@ -69,8 +71,23 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int
df2 = pd.concat([dates, df2], axis=1) df2 = pd.concat([dates, df2], axis=1)
df2 = df2.set_index('date') df2 = df2.set_index('date')
df_final = df2.resample(freq)[['pair']].count() df_final = df2.resample(f"{timeframe_min}min")[['pair']].count()
return df_final[df_final['pair'] > max_open_trades] df_final = df_final.rename({'pair': 'open_trades'}, axis=1)
return df_final
def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
max_open_trades: int) -> pd.DataFrame:
"""
Find overlapping trades by expanding each trade once per period it was open
and then counting overlaps
:param results: Results Dataframe - can be loaded
:param timeframe: Frequency used for the backtest
:param max_open_trades: parameter max_open_trades used during backtest run
:return: dataframe with open-counts per time-period in freq
"""
df_final = analyze_trade_parallelism(results, timeframe)
return df_final[df_final['open_trades'] > max_open_trades]
def load_trades_from_db(db_url: str) -> pd.DataFrame: def load_trades_from_db(db_url: str) -> pd.DataFrame:
@ -89,8 +106,8 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
"stop_loss", "initial_stop_loss", "strategy", "ticker_interval"] "stop_loss", "initial_stop_loss", "strategy", "ticker_interval"]
trades = pd.DataFrame([(t.pair, trades = pd.DataFrame([(t.pair,
t.open_date.replace(tzinfo=pytz.UTC), t.open_date.replace(tzinfo=timezone.utc),
t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None, t.close_date.replace(tzinfo=timezone.utc) if t.close_date else None,
t.calc_profit(), t.calc_profit_percent(), t.calc_profit(), t.calc_profit_percent(),
t.open_rate, t.close_rate, t.amount, t.open_rate, t.close_rate, t.amount,
(round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2) (round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2)
@ -106,7 +123,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
t.stop_loss, t.initial_stop_loss, t.stop_loss, t.initial_stop_loss,
t.strategy, t.ticker_interval t.strategy, t.ticker_interval
) )
for t in Trade.query.all()], for t in Trade.get_trades().all()],
columns=columns) columns=columns)
return trades return trades
@ -161,9 +178,9 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
:return: Returns df with one additional column, col_name, containing the cumulative profit. :return: Returns df with one additional column, col_name, containing the cumulative profit.
""" """
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
ticker_minutes = timeframe_to_minutes(timeframe) timeframe_minutes = timeframe_to_minutes(timeframe)
# Resample to ticker_interval to make sure trades match candles # Resample to timeframe to make sure trades match candles
_trades_sum = trades.resample(f'{ticker_minutes}min', on='close_time')[['profitperc']].sum() _trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_time')[['profitperc']].sum()
df.loc[:, col_name] = _trades_sum.cumsum() df.loc[:, col_name] = _trades_sum.cumsum()
# Set first value to 0 # Set first value to 0
df.loc[df.iloc[0].name, col_name] = 0 df.loc[df.iloc[0].name, col_name] = 0

View File

@ -10,13 +10,13 @@ from pandas import DataFrame, to_datetime
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def parse_ticker_dataframe(ticker: list, ticker_interval: str, pair: str, *, def parse_ticker_dataframe(ticker: list, timeframe: str, pair: str, *,
fill_missing: bool = True, fill_missing: bool = True,
drop_incomplete: bool = True) -> DataFrame: drop_incomplete: bool = True) -> DataFrame:
""" """
Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe
:param ticker: ticker list, as returned by exchange.async_get_candle_history :param ticker: ticker list, as returned by exchange.async_get_candle_history
:param ticker_interval: ticker_interval (e.g. 5m). Used to fill up eventual missing data :param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data
:param pair: Pair this data is for (used to warn if fillup was necessary) :param pair: Pair this data is for (used to warn if fillup was necessary)
:param fill_missing: fill up missing candles with 0 candles :param fill_missing: fill up missing candles with 0 candles
(see ohlcv_fill_up_missing_data for details) (see ohlcv_fill_up_missing_data for details)
@ -52,12 +52,12 @@ def parse_ticker_dataframe(ticker: list, ticker_interval: str, pair: str, *,
logger.debug('Dropping last candle') logger.debug('Dropping last candle')
if fill_missing: if fill_missing:
return ohlcv_fill_up_missing_data(frame, ticker_interval, pair) return ohlcv_fill_up_missing_data(frame, timeframe, pair)
else: else:
return frame return frame
def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str, pair: str) -> DataFrame: def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) -> DataFrame:
""" """
Fills up missing data with 0 volume rows, Fills up missing data with 0 volume rows,
using the previous close as price for "open", "high" "low" and "close", volume is set to 0 using the previous close as price for "open", "high" "low" and "close", volume is set to 0
@ -72,7 +72,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str, pair:
'close': 'last', 'close': 'last',
'volume': 'sum' 'volume': 'sum'
} }
ticker_minutes = timeframe_to_minutes(ticker_interval) ticker_minutes = timeframe_to_minutes(timeframe)
# Resample to create "NAN" values # Resample to create "NAN" values
df = dataframe.resample(f'{ticker_minutes}min', on='date').agg(ohlc_dict) df = dataframe.resample(f'{ticker_minutes}min', on='date').agg(ohlc_dict)

View File

@ -37,52 +37,53 @@ class DataProvider:
@property @property
def available_pairs(self) -> List[Tuple[str, str]]: def available_pairs(self) -> List[Tuple[str, str]]:
""" """
Return a list of tuples containing pair, ticker_interval for which data is currently cached. Return a list of tuples containing (pair, timeframe) for which data is currently cached.
Should be whitelist + open trades. Should be whitelist + open trades.
""" """
return list(self._exchange._klines.keys()) return list(self._exchange._klines.keys())
def ohlcv(self, pair: str, ticker_interval: str = None, copy: bool = True) -> DataFrame: def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame:
""" """
Get ohlcv data for the given pair as DataFrame Get ohlcv data for the given pair as DataFrame
Please use the `available_pairs` method to verify which pairs are currently cached. Please use the `available_pairs` method to verify which pairs are currently cached.
:param pair: pair to get the data for :param pair: pair to get the data for
:param ticker_interval: ticker interval to get data for :param timeframe: Ticker timeframe to get data for
:param copy: copy dataframe before returning if True. :param copy: copy dataframe before returning if True.
Use False only for read-only operations (where the dataframe is not modified) Use False only for read-only operations (where the dataframe is not modified)
""" """
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
return self._exchange.klines((pair, ticker_interval or self._config['ticker_interval']), return self._exchange.klines((pair, timeframe or self._config['ticker_interval']),
copy=copy) copy=copy)
else: else:
return DataFrame() return DataFrame()
def historic_ohlcv(self, pair: str, ticker_interval: str = None) -> DataFrame: def historic_ohlcv(self, pair: str, timeframe: str = None) -> DataFrame:
""" """
Get stored historic ohlcv data Get stored historic ohlcv data
:param pair: pair to get the data for :param pair: pair to get the data for
:param ticker_interval: ticker interval to get data for :param timeframe: timeframe to get data for
""" """
return load_pair_history(pair=pair, return load_pair_history(pair=pair,
ticker_interval=ticker_interval or self._config['ticker_interval'], timeframe=timeframe or self._config['ticker_interval'],
datadir=Path(self._config['datadir']) datadir=Path(self._config['datadir'])
) )
def get_pair_dataframe(self, pair: str, ticker_interval: str = None) -> DataFrame: def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame:
""" """
Return pair ohlcv data, either live or cached historical -- depending Return pair ohlcv data, either live or cached historical -- depending
on the runmode. on the runmode.
:param pair: pair to get the data for :param pair: pair to get the data for
:param ticker_interval: ticker interval to get data for :param timeframe: timeframe to get data for
:return: Dataframe for this pair
""" """
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
# Get live ohlcv data. # Get live ohlcv data.
data = self.ohlcv(pair=pair, ticker_interval=ticker_interval) data = self.ohlcv(pair=pair, timeframe=timeframe)
else: else:
# Get historic ohlcv data (cached on disk). # Get historic ohlcv data (cached on disk).
data = self.historic_ohlcv(pair=pair, ticker_interval=ticker_interval) data = self.historic_ohlcv(pair=pair, timeframe=timeframe)
if len(data) == 0: if len(data) == 0:
logger.warning(f"No data found for ({pair}, {ticker_interval}).") logger.warning(f"No data found for ({pair}, {timeframe}).")
return data return data
def market(self, pair: str) -> Optional[Dict[str, Any]]: def market(self, pair: str) -> Optional[Dict[str, Any]]:

View File

@ -8,7 +8,8 @@ Includes:
import logging import logging
import operator import operator
from datetime import datetime from copy import deepcopy
from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
@ -18,7 +19,7 @@ from pandas import DataFrame
from freqtrade import OperationalException, misc from freqtrade import OperationalException, misc
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.converter import parse_ticker_dataframe, trades_to_ohlcv from freqtrade.data.converter import parse_ticker_dataframe, trades_to_ohlcv
from freqtrade.exchange import Exchange, timeframe_to_minutes from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_seconds
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -49,13 +50,30 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
return tickerlist[start_index:stop_index] return tickerlist[start_index:stop_index]
def load_tickerdata_file(datadir: Path, pair: str, ticker_interval: str, def trim_dataframe(df: DataFrame, timerange: TimeRange, df_date_col: str = 'date') -> DataFrame:
"""
Trim dataframe based on given timerange
:param df: Dataframe to trim
:param timerange: timerange (use start and end date if available)
:param: df_date_col: Column in the dataframe to use as Date column
:return: trimmed dataframe
"""
if timerange.starttype == 'date':
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
df = df.loc[df[df_date_col] >= start, :]
if timerange.stoptype == 'date':
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
df = df.loc[df[df_date_col] <= stop, :]
return df
def load_tickerdata_file(datadir: Path, pair: str, timeframe: str,
timerange: Optional[TimeRange] = None) -> Optional[list]: timerange: Optional[TimeRange] = None) -> Optional[list]:
""" """
Load a pair from file, either .json.gz or .json Load a pair from file, either .json.gz or .json
:return: tickerlist or None if unsuccessful :return: tickerlist or None if unsuccessful
""" """
filename = pair_data_filename(datadir, pair, ticker_interval) filename = pair_data_filename(datadir, pair, timeframe)
pairdata = misc.file_load_json(filename) pairdata = misc.file_load_json(filename)
if not pairdata: if not pairdata:
return [] return []
@ -66,11 +84,11 @@ def load_tickerdata_file(datadir: Path, pair: str, ticker_interval: str,
def store_tickerdata_file(datadir: Path, pair: str, def store_tickerdata_file(datadir: Path, pair: str,
ticker_interval: str, data: list, is_zip: bool = False): timeframe: str, data: list, is_zip: bool = False):
""" """
Stores tickerdata to file Stores tickerdata to file
""" """
filename = pair_data_filename(datadir, pair, ticker_interval) filename = pair_data_filename(datadir, pair, timeframe)
misc.file_dump_json(filename, data, is_zip=is_zip) misc.file_dump_json(filename, data, is_zip=is_zip)
@ -107,18 +125,19 @@ def _validate_pairdata(pair, pairdata, timerange: TimeRange):
def load_pair_history(pair: str, def load_pair_history(pair: str,
ticker_interval: str, timeframe: str,
datadir: Path, datadir: Path,
timerange: Optional[TimeRange] = None, timerange: Optional[TimeRange] = None,
refresh_pairs: bool = False, refresh_pairs: bool = False,
exchange: Optional[Exchange] = None, exchange: Optional[Exchange] = None,
fill_up_missing: bool = True, fill_up_missing: bool = True,
drop_incomplete: bool = True drop_incomplete: bool = True,
startup_candles: int = 0,
) -> DataFrame: ) -> DataFrame:
""" """
Loads cached ticker history for the given pair. Loads cached ticker history for the given pair.
:param pair: Pair to load data for :param pair: Pair to load data for
:param ticker_interval: Ticker-interval (e.g. "5m") :param timeframe: Ticker timeframe (e.g. "5m")
:param datadir: Path to the data storage location. :param datadir: Path to the data storage location.
:param timerange: Limit data to be loaded to this timerange :param timerange: Limit data to be loaded to this timerange
:param refresh_pairs: Refresh pairs from exchange. :param refresh_pairs: Refresh pairs from exchange.
@ -126,65 +145,88 @@ def load_pair_history(pair: str,
:param exchange: Exchange object (needed when using "refresh_pairs") :param exchange: Exchange object (needed when using "refresh_pairs")
:param fill_up_missing: Fill missing values with "No action"-candles :param fill_up_missing: Fill missing values with "No action"-candles
:param drop_incomplete: Drop last candle assuming it may be incomplete. :param drop_incomplete: Drop last candle assuming it may be incomplete.
:return: DataFrame with ohlcv data :param startup_candles: Additional candles to load at the start of the period
:return: DataFrame with ohlcv data, or empty DataFrame
""" """
timerange_startup = deepcopy(timerange)
if startup_candles > 0 and timerange_startup:
timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles)
# The user forced the refresh of pairs # The user forced the refresh of pairs
if refresh_pairs: if refresh_pairs:
download_pair_history(datadir=datadir, download_pair_history(datadir=datadir,
exchange=exchange, exchange=exchange,
pair=pair, pair=pair,
ticker_interval=ticker_interval, timeframe=timeframe,
timerange=timerange) timerange=timerange)
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) pairdata = load_tickerdata_file(datadir, pair, timeframe, timerange=timerange_startup)
if pairdata: if pairdata:
if timerange: if timerange_startup:
_validate_pairdata(pair, pairdata, timerange) _validate_pairdata(pair, pairdata, timerange_startup)
return parse_ticker_dataframe(pairdata, ticker_interval, pair=pair, return parse_ticker_dataframe(pairdata, timeframe, pair=pair,
fill_missing=fill_up_missing, fill_missing=fill_up_missing,
drop_incomplete=drop_incomplete) drop_incomplete=drop_incomplete)
else: else:
logger.warning( logger.warning(
f'No history data for pair: "{pair}", interval: {ticker_interval}. ' f'No history data for pair: "{pair}", timeframe: {timeframe}. '
'Use `freqtrade download-data` to download the data' 'Use `freqtrade download-data` to download the data'
) )
return None return DataFrame()
def load_data(datadir: Path, def load_data(datadir: Path,
ticker_interval: str, timeframe: str,
pairs: List[str], pairs: List[str],
refresh_pairs: bool = False, refresh_pairs: bool = False,
exchange: Optional[Exchange] = None, exchange: Optional[Exchange] = None,
timerange: Optional[TimeRange] = None, timerange: Optional[TimeRange] = None,
fill_up_missing: bool = True, fill_up_missing: bool = True,
startup_candles: int = 0,
fail_without_data: bool = False
) -> Dict[str, DataFrame]: ) -> Dict[str, DataFrame]:
""" """
Loads ticker history data for a list of pairs Loads ticker history data for a list of pairs
:return: dict(<pair>:<tickerlist>) :param datadir: Path to the data storage location.
:param timeframe: Ticker Timeframe (e.g. "5m")
:param pairs: List of pairs to load
:param refresh_pairs: Refresh pairs from exchange.
(Note: Requires exchange to be passed as well.)
:param exchange: Exchange object (needed when using "refresh_pairs")
:param timerange: Limit data to be loaded to this timerange
:param fill_up_missing: Fill missing values with "No action"-candles
:param startup_candles: Additional candles to load at the start of the period
:param fail_without_data: Raise OperationalException if no data is found.
:return: dict(<pair>:<Dataframe>)
TODO: refresh_pairs is still used by edge to keep the data uptodate. TODO: refresh_pairs is still used by edge to keep the data uptodate.
This should be replaced in the future. Instead, writing the current candles to disk This should be replaced in the future. Instead, writing the current candles to disk
from dataprovider should be implemented, as this would avoid loading ohlcv data twice. from dataprovider should be implemented, as this would avoid loading ohlcv data twice.
exchange and refresh_pairs are then not needed here nor in load_pair_history. exchange and refresh_pairs are then not needed here nor in load_pair_history.
""" """
result: Dict[str, DataFrame] = {} result: Dict[str, DataFrame] = {}
if startup_candles > 0 and timerange:
logger.info(f'Using indicator startup period: {startup_candles} ...')
for pair in pairs: for pair in pairs:
hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, hist = load_pair_history(pair=pair, timeframe=timeframe,
datadir=datadir, timerange=timerange, datadir=datadir, timerange=timerange,
refresh_pairs=refresh_pairs, refresh_pairs=refresh_pairs,
exchange=exchange, exchange=exchange,
fill_up_missing=fill_up_missing) fill_up_missing=fill_up_missing,
if hist is not None: startup_candles=startup_candles)
if not hist.empty:
result[pair] = hist result[pair] = hist
if fail_without_data and not result:
raise OperationalException("No data found. Terminating.")
return result return result
def pair_data_filename(datadir: Path, pair: str, ticker_interval: str) -> Path: def pair_data_filename(datadir: Path, pair: str, timeframe: str) -> Path:
pair_s = pair.replace("/", "_") pair_s = pair.replace("/", "_")
filename = datadir.joinpath(f'{pair_s}-{ticker_interval}.json') filename = datadir.joinpath(f'{pair_s}-{timeframe}.json')
return filename return filename
@ -194,7 +236,7 @@ def pair_trades_filename(datadir: Path, pair: str) -> Path:
return filename return filename
def _load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: str, def _load_cached_data_for_updating(datadir: Path, pair: str, timeframe: str,
timerange: Optional[TimeRange]) -> Tuple[List[Any], timerange: Optional[TimeRange]) -> Tuple[List[Any],
Optional[int]]: Optional[int]]:
""" """
@ -212,12 +254,12 @@ def _load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: st
if timerange.starttype == 'date': if timerange.starttype == 'date':
since_ms = timerange.startts * 1000 since_ms = timerange.startts * 1000
elif timerange.stoptype == 'line': elif timerange.stoptype == 'line':
num_minutes = timerange.stopts * timeframe_to_minutes(ticker_interval) num_minutes = timerange.stopts * timeframe_to_minutes(timeframe)
since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000
# read the cached file # read the cached file
# Intentionally don't pass timerange in - since we need to load the full dataset. # Intentionally don't pass timerange in - since we need to load the full dataset.
data = load_tickerdata_file(datadir, pair, ticker_interval) data = load_tickerdata_file(datadir, pair, timeframe)
# remove the last item, could be incomplete candle # remove the last item, could be incomplete candle
if data: if data:
data.pop() data.pop()
@ -238,18 +280,18 @@ def _load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: st
def download_pair_history(datadir: Path, def download_pair_history(datadir: Path,
exchange: Optional[Exchange], exchange: Optional[Exchange],
pair: str, pair: str,
ticker_interval: str = '5m', timeframe: str = '5m',
timerange: Optional[TimeRange] = None) -> bool: timerange: Optional[TimeRange] = None) -> bool:
""" """
Download the latest ticker intervals from the exchange for the pair passed in parameters Download latest candles from the exchange for the pair and timeframe passed in parameters
The data is downloaded starting from the last correct ticker interval data that The data is downloaded starting from the last correct data that
exists in a cache. If timerange starts earlier than the data in the cache, exists in a cache. If timerange starts earlier than the data in the cache,
the full data will be redownloaded the full data will be redownloaded
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
:param pair: pair to download :param pair: pair to download
:param ticker_interval: ticker interval :param timeframe: Ticker Timeframe (e.g 5m)
:param timerange: range of time to download :param timerange: range of time to download
:return: bool with success state :return: bool with success state
""" """
@ -260,17 +302,17 @@ def download_pair_history(datadir: Path,
try: try:
logger.info( logger.info(
f'Download history data for pair: "{pair}", interval: {ticker_interval} ' f'Download history data for pair: "{pair}", timeframe: {timeframe} '
f'and store in {datadir}.' f'and store in {datadir}.'
) )
data, since_ms = _load_cached_data_for_updating(datadir, pair, ticker_interval, timerange) data, since_ms = _load_cached_data_for_updating(datadir, pair, timeframe, timerange)
logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None')
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
# Default since_ms to 30 days if nothing is given # Default since_ms to 30 days if nothing is given
new_data = exchange.get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval, new_data = exchange.get_historic_ohlcv(pair=pair, timeframe=timeframe,
since_ms=since_ms if since_ms since_ms=since_ms if since_ms
else else
int(arrow.utcnow().shift( int(arrow.utcnow().shift(
@ -280,12 +322,12 @@ def download_pair_history(datadir: Path,
logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))
logger.debug("New End: %s", misc.format_ms_time(data[-1][0])) logger.debug("New End: %s", misc.format_ms_time(data[-1][0]))
store_tickerdata_file(datadir, pair, ticker_interval, data=data) store_tickerdata_file(datadir, pair, timeframe, data=data)
return True return True
except Exception as e: except Exception as e:
logger.error( logger.error(
f'Failed to download history data for pair: "{pair}", interval: {ticker_interval}. ' f'Failed to download history data for pair: "{pair}", timeframe: {timeframe}. '
f'Error: {e}' f'Error: {e}'
) )
return False return False
@ -305,17 +347,17 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
pairs_not_available.append(pair) pairs_not_available.append(pair)
logger.info(f"Skipping pair {pair}...") logger.info(f"Skipping pair {pair}...")
continue continue
for ticker_interval in timeframes: for timeframe in timeframes:
dl_file = pair_data_filename(dl_path, pair, ticker_interval) dl_file = pair_data_filename(dl_path, pair, timeframe)
if erase and dl_file.exists(): if erase and dl_file.exists():
logger.info( logger.info(
f'Deleting existing data for pair {pair}, interval {ticker_interval}.') f'Deleting existing data for pair {pair}, interval {timeframe}.')
dl_file.unlink() dl_file.unlink()
logger.info(f'Downloading pair {pair}, interval {ticker_interval}.') logger.info(f'Downloading pair {pair}, interval {timeframe}.')
download_pair_history(datadir=dl_path, exchange=exchange, download_pair_history(datadir=dl_path, exchange=exchange,
pair=pair, ticker_interval=str(ticker_interval), pair=pair, timeframe=str(timeframe),
timerange=timerange) timerange=timerange)
return pairs_not_available return pairs_not_available
@ -421,7 +463,7 @@ def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]
def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime, def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime,
max_date: datetime, ticker_interval_mins: int) -> bool: max_date: datetime, timeframe_mins: int) -> bool:
""" """
Validates preprocessed backtesting data for missing values and shows warnings about it that. Validates preprocessed backtesting data for missing values and shows warnings about it that.
@ -429,10 +471,10 @@ def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime,
:param pair: pair used for log output. :param pair: pair used for log output.
:param min_date: start-date of the data :param min_date: start-date of the data
:param max_date: end-date of the data :param max_date: end-date of the data
:param ticker_interval_mins: ticker interval in minutes :param timeframe_mins: ticker Timeframe in minutes
""" """
# total difference in minutes / interval-minutes # total difference in minutes / timeframe-minutes
expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins) expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_mins)
found_missing = False found_missing = False
dflen = len(data) dflen = len(data)
if dflen < expected_frames: if dflen < expected_frames:

View File

@ -97,10 +97,11 @@ class Edge:
data = history.load_data( data = history.load_data(
datadir=Path(self.config['datadir']), datadir=Path(self.config['datadir']),
pairs=pairs, pairs=pairs,
ticker_interval=self.strategy.ticker_interval, timeframe=self.strategy.ticker_interval,
refresh_pairs=self._refresh_pairs, refresh_pairs=self._refresh_pairs,
exchange=self.exchange, exchange=self.exchange,
timerange=self._timerange timerange=self._timerange,
startup_candles=self.strategy.startup_candle_count,
) )
if not data: if not data:

View File

@ -1,4 +1,5 @@
from freqtrade.exchange.exchange import Exchange, MAP_EXCHANGE_CHILDCLASS # noqa: F401 from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS # noqa: F401
from freqtrade.exchange.exchange import Exchange # noqa: F401
from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401 from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401
is_exchange_bad, is_exchange_bad,
is_exchange_known_ccxt, is_exchange_known_ccxt,
@ -14,3 +15,4 @@ from freqtrade.exchange.exchange import (market_is_active, # noqa: F401
symbol_is_pair) symbol_is_pair)
from freqtrade.exchange.kraken import Kraken # noqa: F401 from freqtrade.exchange.kraken import Kraken # noqa: F401
from freqtrade.exchange.binance import Binance # noqa: F401 from freqtrade.exchange.binance import Binance # noqa: F401
from freqtrade.exchange.bibox import Bibox # noqa: F401

View File

@ -0,0 +1,22 @@
""" Bibox exchange subclass """
import logging
from typing import Dict
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__)
class Bibox(Exchange):
"""
Bibox 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.
"""
# fetchCurrencies API point requires authentication for Bibox,
# so switch it off for Freqtrade load_markets()
_ccxt_config: Dict = {"has": {"fetchCurrencies": False}}

View File

@ -0,0 +1,124 @@
import logging
from freqtrade import DependencyException, TemporaryError
logger = logging.getLogger(__name__)
API_RETRY_COUNT = 4
BAD_EXCHANGES = {
"bitmex": "Various reasons.",
"bitstamp": "Does not provide history. "
"Details in https://github.com/freqtrade/freqtrade/issues/1983",
"hitbtc": "This API cannot be used with Freqtrade. "
"Use `hitbtc2` exchange id to access this exchange.",
**dict.fromkeys([
'adara',
'anxpro',
'bigone',
'coinbase',
'coinexchange',
'coinmarketcap',
'lykke',
'xbtce',
], "Does not provide timeframes. ccxt fetchOHLCV: False"),
**dict.fromkeys([
'bcex',
'bit2c',
'bitbay',
'bitflyer',
'bitforex',
'bithumb',
'bitso',
'bitstamp1',
'bl3p',
'braziliex',
'btcbox',
'btcchina',
'btctradeim',
'btctradeua',
'bxinth',
'chilebit',
'coincheck',
'coinegg',
'coinfalcon',
'coinfloor',
'coingi',
'coinmate',
'coinone',
'coinspot',
'coolcoin',
'crypton',
'deribit',
'exmo',
'exx',
'flowbtc',
'foxbit',
'fybse',
# 'hitbtc',
'ice3x',
'independentreserve',
'indodax',
'itbit',
'lakebtc',
'latoken',
'liquid',
'livecoin',
'luno',
'mixcoins',
'negociecoins',
'nova',
'paymium',
'southxchange',
'stronghold',
'surbitcoin',
'therock',
'tidex',
'vaultoro',
'vbtc',
'virwox',
'yobit',
'zaif',
], "Does not provide timeframes. ccxt fetchOHLCV: emulated"),
}
MAP_EXCHANGE_CHILDCLASS = {
'binanceus': 'binance',
'binanceje': 'binance',
}
def retrier_async(f):
async def wrapper(*args, **kwargs):
count = kwargs.pop('count', API_RETRY_COUNT)
try:
return await f(*args, **kwargs)
except (TemporaryError, DependencyException) as ex:
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
if count > 0:
count -= 1
kwargs.update({'count': count})
logger.warning('retrying %s() still for %s times', f.__name__, count)
return await wrapper(*args, **kwargs)
else:
logger.warning('Giving up retrying: %s()', f.__name__)
raise ex
return wrapper
def retrier(f):
def wrapper(*args, **kwargs):
count = kwargs.pop('count', API_RETRY_COUNT)
try:
return f(*args, **kwargs)
except (TemporaryError, DependencyException) as ex:
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
if count > 0:
count -= 1
kwargs.update({'count': count})
logger.warning('retrying %s() still for %s times', f.__name__, count)
return wrapper(*args, **kwargs)
else:
logger.warning('Giving up retrying: %s()', f.__name__)
raise ex
return wrapper

View File

@ -14,141 +14,25 @@ from typing import Any, Dict, List, Optional, Tuple
import arrow import arrow
import ccxt import ccxt
import ccxt.async_support as ccxt_async import ccxt.async_support as ccxt_async
from ccxt.base.decimal_to_precision import ROUND_UP, ROUND_DOWN from ccxt.base.decimal_to_precision import ROUND_DOWN, ROUND_UP
from pandas import DataFrame from pandas import DataFrame
from freqtrade import (DependencyException, InvalidOrderException, from freqtrade import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError, constants) OperationalException, TemporaryError, constants)
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
from freqtrade.misc import deep_merge_dicts from freqtrade.misc import deep_merge_dicts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
API_RETRY_COUNT = 4
BAD_EXCHANGES = {
"bitmex": "Various reasons.",
"bitstamp": "Does not provide history. "
"Details in https://github.com/freqtrade/freqtrade/issues/1983",
"hitbtc": "This API cannot be used with Freqtrade. "
"Use `hitbtc2` exchange id to access this exchange.",
**dict.fromkeys([
'adara',
'anxpro',
'bigone',
'coinbase',
'coinexchange',
'coinmarketcap',
'lykke',
'xbtce',
], "Does not provide timeframes. ccxt fetchOHLCV: False"),
**dict.fromkeys([
'bcex',
'bit2c',
'bitbay',
'bitflyer',
'bitforex',
'bithumb',
'bitso',
'bitstamp1',
'bl3p',
'braziliex',
'btcbox',
'btcchina',
'btctradeim',
'btctradeua',
'bxinth',
'chilebit',
'coincheck',
'coinegg',
'coinfalcon',
'coinfloor',
'coingi',
'coinmate',
'coinone',
'coinspot',
'coolcoin',
'crypton',
'deribit',
'exmo',
'exx',
'flowbtc',
'foxbit',
'fybse',
# 'hitbtc',
'ice3x',
'independentreserve',
'indodax',
'itbit',
'lakebtc',
'latoken',
'liquid',
'livecoin',
'luno',
'mixcoins',
'negociecoins',
'nova',
'paymium',
'southxchange',
'stronghold',
'surbitcoin',
'therock',
'tidex',
'vaultoro',
'vbtc',
'virwox',
'yobit',
'zaif',
], "Does not provide timeframes. ccxt fetchOHLCV: emulated"),
}
MAP_EXCHANGE_CHILDCLASS = {
'binanceus': 'binance',
'binanceje': 'binance',
}
def retrier_async(f):
async def wrapper(*args, **kwargs):
count = kwargs.pop('count', API_RETRY_COUNT)
try:
return await f(*args, **kwargs)
except (TemporaryError, DependencyException) as ex:
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
if count > 0:
count -= 1
kwargs.update({'count': count})
logger.warning('retrying %s() still for %s times', f.__name__, count)
return await wrapper(*args, **kwargs)
else:
logger.warning('Giving up retrying: %s()', f.__name__)
raise ex
return wrapper
def retrier(f):
def wrapper(*args, **kwargs):
count = kwargs.pop('count', API_RETRY_COUNT)
try:
return f(*args, **kwargs)
except (TemporaryError, DependencyException) as ex:
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
if count > 0:
count -= 1
kwargs.update({'count': count})
logger.warning('retrying %s() still for %s times', f.__name__, count)
return wrapper(*args, **kwargs)
else:
logger.warning('Giving up retrying: %s()', f.__name__)
raise ex
return wrapper
class Exchange: class Exchange:
_config: Dict = {} _config: Dict = {}
# Parameters to add directly to ccxt sync/async initialization.
_ccxt_config: Dict = {}
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement) # Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
_params: Dict = {} _params: Dict = {}
@ -210,10 +94,17 @@ class Exchange:
self._trades_pagination_arg = self._ft_has['trades_pagination_arg'] self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
# Initialize ccxt objects # Initialize ccxt objects
ccxt_config = self._ccxt_config.copy()
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}),
ccxt_config)
self._api = self._init_ccxt( self._api = self._init_ccxt(
exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config')) exchange_config, ccxt_kwargs=ccxt_config)
ccxt_async_config = self._ccxt_config.copy()
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}),
ccxt_async_config)
self._api_async = self._init_ccxt( self._api_async = self._init_ccxt(
exchange_config, ccxt_async, ccxt_kwargs=exchange_config.get('ccxt_async_config')) exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config)
logger.info('Using Exchange "%s"', self.name) logger.info('Using Exchange "%s"', self.name)
@ -228,6 +119,7 @@ class Exchange:
self.validate_pairs(config['exchange']['pair_whitelist']) self.validate_pairs(config['exchange']['pair_whitelist'])
self.validate_ordertypes(config.get('order_types', {})) self.validate_ordertypes(config.get('order_types', {}))
self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {}))
self.validate_required_startup_candles(config.get('startup_candle_count', 0))
# Converts the interval provided in minutes in config to seconds # Converts the interval provided in minutes in config to seconds
self.markets_refresh_interval: int = exchange_config.get( self.markets_refresh_interval: int = exchange_config.get(
@ -443,6 +335,16 @@ class Exchange:
raise OperationalException( raise OperationalException(
f'Time in force policies are not supported for {self.name} yet.') f'Time in force policies are not supported for {self.name} yet.')
def validate_required_startup_candles(self, startup_candles) -> None:
"""
Checks if required startup_candles is more than ohlcv_candle_limit.
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
"""
if startup_candles + 5 > self._ft_has['ohlcv_candle_limit']:
raise OperationalException(
f"This strategy requires {startup_candles} candles to start. "
f"{self.name} only provides {self._ft_has['ohlcv_candle_limit']}.")
def exchange_has(self, endpoint: str) -> bool: def exchange_has(self, endpoint: str) -> bool:
""" """
Checks if exchange implements a specific API endpoint. Checks if exchange implements a specific API endpoint.
@ -644,40 +546,40 @@ class Exchange:
logger.info("returning cached ticker-data for %s", pair) logger.info("returning cached ticker-data for %s", pair)
return self._cached_ticker[pair] return self._cached_ticker[pair]
def get_historic_ohlcv(self, pair: str, ticker_interval: str, def get_historic_ohlcv(self, pair: str, timeframe: str,
since_ms: int) -> List: since_ms: int) -> List:
""" """
Gets candle history using asyncio and returns the list of candles. Gets candle history using asyncio and returns the list of candles.
Handles all async doing. Handles all async doing.
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call. Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call.
:param pair: Pair to download :param pair: Pair to download
:param ticker_interval: Interval to get :param timeframe: Ticker Timeframe to get
:param since_ms: Timestamp in milliseconds to get history from :param since_ms: Timestamp in milliseconds to get history from
:returns List of tickers :returns List of tickers
""" """
return asyncio.get_event_loop().run_until_complete( return asyncio.get_event_loop().run_until_complete(
self._async_get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval, self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
since_ms=since_ms)) since_ms=since_ms))
async def _async_get_historic_ohlcv(self, pair: str, async def _async_get_historic_ohlcv(self, pair: str,
ticker_interval: str, timeframe: str,
since_ms: int) -> List: since_ms: int) -> List:
one_call = timeframe_to_msecs(ticker_interval) * self._ohlcv_candle_limit one_call = timeframe_to_msecs(timeframe) * self._ohlcv_candle_limit
logger.debug( logger.debug(
"one_call: %s msecs (%s)", "one_call: %s msecs (%s)",
one_call, one_call,
arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True) arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True)
) )
input_coroutines = [self._async_get_candle_history( input_coroutines = [self._async_get_candle_history(
pair, ticker_interval, since) for since in pair, timeframe, since) for since in
range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] range(since_ms, arrow.utcnow().timestamp * 1000, one_call)]
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True) tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
# Combine tickers # Combine tickers
data: List = [] data: List = []
for p, ticker_interval, ticker in tickers: for p, timeframe, ticker in tickers:
if p == pair: if p == pair:
data.extend(ticker) data.extend(ticker)
# Sort data again after extending the result - above calls return in "async order" # Sort data again after extending the result - above calls return in "async order"
@ -697,14 +599,14 @@ class Exchange:
input_coroutines = [] input_coroutines = []
# Gather coroutines to run # Gather coroutines to run
for pair, ticker_interval in set(pair_list): for pair, timeframe in set(pair_list):
if (not ((pair, ticker_interval) in self._klines) if (not ((pair, timeframe) in self._klines)
or self._now_is_time_to_refresh(pair, ticker_interval)): or self._now_is_time_to_refresh(pair, timeframe)):
input_coroutines.append(self._async_get_candle_history(pair, ticker_interval)) input_coroutines.append(self._async_get_candle_history(pair, timeframe))
else: else:
logger.debug( logger.debug(
"Using cached ohlcv data for pair %s, interval %s ...", "Using cached ohlcv data for pair %s, timeframe %s ...",
pair, ticker_interval pair, timeframe
) )
tickers = asyncio.get_event_loop().run_until_complete( tickers = asyncio.get_event_loop().run_until_complete(
@ -716,40 +618,40 @@ class Exchange:
logger.warning("Async code raised an exception: %s", res.__class__.__name__) logger.warning("Async code raised an exception: %s", res.__class__.__name__)
continue continue
pair = res[0] pair = res[0]
ticker_interval = res[1] timeframe = res[1]
ticks = res[2] ticks = res[2]
# keeping last candle time as last refreshed time of the pair # keeping last candle time as last refreshed time of the pair
if ticks: if ticks:
self._pairs_last_refresh_time[(pair, ticker_interval)] = ticks[-1][0] // 1000 self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
# keeping parsed dataframe in cache # keeping parsed dataframe in cache
self._klines[(pair, ticker_interval)] = parse_ticker_dataframe( self._klines[(pair, timeframe)] = parse_ticker_dataframe(
ticks, ticker_interval, pair=pair, fill_missing=True, ticks, timeframe, pair=pair, fill_missing=True,
drop_incomplete=self._ohlcv_partial_candle) drop_incomplete=self._ohlcv_partial_candle)
return tickers return tickers
def _now_is_time_to_refresh(self, pair: str, ticker_interval: str) -> bool: def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool:
# Calculating ticker interval in seconds # Calculating ticker interval in seconds
interval_in_sec = timeframe_to_seconds(ticker_interval) interval_in_sec = timeframe_to_seconds(timeframe)
return not ((self._pairs_last_refresh_time.get((pair, ticker_interval), 0) return not ((self._pairs_last_refresh_time.get((pair, timeframe), 0)
+ interval_in_sec) >= arrow.utcnow().timestamp) + interval_in_sec) >= arrow.utcnow().timestamp)
@retrier_async @retrier_async
async def _async_get_candle_history(self, pair: str, ticker_interval: str, async def _async_get_candle_history(self, pair: str, timeframe: str,
since_ms: Optional[int] = None) -> Tuple[str, str, List]: since_ms: Optional[int] = None) -> Tuple[str, str, List]:
""" """
Asynchronously gets candle histories using fetch_ohlcv Asynchronously gets candle histories using fetch_ohlcv
returns tuple: (pair, ticker_interval, ohlcv_list) returns tuple: (pair, timeframe, ohlcv_list)
""" """
try: try:
# fetch ohlcv asynchronously # fetch ohlcv asynchronously
s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else '' s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
logger.debug( logger.debug(
"Fetching pair %s, interval %s, since %s %s...", "Fetching pair %s, interval %s, since %s %s...",
pair, ticker_interval, since_ms, s pair, timeframe, since_ms, s
) )
data = await self._api_async.fetch_ohlcv(pair, timeframe=ticker_interval, data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe,
since=since_ms) since=since_ms)
# Because some exchange sort Tickers ASC and other DESC. # Because some exchange sort Tickers ASC and other DESC.
@ -761,9 +663,9 @@ class Exchange:
data = sorted(data, key=lambda x: x[0]) data = sorted(data, key=lambda x: x[0])
except IndexError: except IndexError:
logger.exception("Error loading %s. Result was %s.", pair, data) logger.exception("Error loading %s. Result was %s.", pair, data)
return pair, ticker_interval, [] return pair, timeframe, []
logger.debug("Done fetching pair %s, interval %s ...", pair, ticker_interval) logger.debug("Done fetching pair %s, interval %s ...", pair, timeframe)
return pair, ticker_interval, data return pair, timeframe, data
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
@ -910,7 +812,6 @@ class Exchange:
Handles all async doing. Handles all async doing.
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call. Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call.
:param pair: Pair to download :param pair: Pair to download
:param ticker_interval: Interval to get
:param since: Timestamp in milliseconds to get history from :param since: Timestamp in milliseconds to get history from
:param until: Timestamp in milliseconds. Defaults to current timestamp if not defined. :param until: Timestamp in milliseconds. Defaults to current timestamp if not defined.
:param from_id: Download data starting with ID (if id is known) :param from_id: Download data starting with ID (if id is known)
@ -983,6 +884,22 @@ class Exchange:
@retrier @retrier
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
"""
Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.
The "since" argument passed in is coming from the database and is in UTC,
as timezone-native datetime object.
From the python documentation:
> Naive datetime instances are assumed to represent local time
Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the
transformation from local timezone to UTC.
This works for timezones UTC+ since then the result will contain trades from a few hours
instead of from the last 5 seconds, however fails for UTC- timezones,
since we're then asking for trades with a "since" argument in the future.
:param order_id order_id: Order-id as given when creating the order
:param pair: Pair the order is for
:param since: datetime object of the order creation time. Assumes object is in UTC.
"""
if self._config['dry_run']: if self._config['dry_run']:
return [] return []
if not self.exchange_has('fetchMyTrades'): if not self.exchange_has('fetchMyTrades'):
@ -990,7 +907,8 @@ class Exchange:
try: try:
# Allow 5s offset to catch slight time offsets (discovered in #1185) # Allow 5s offset to catch slight time offsets (discovered in #1185)
# since needs to be int in milliseconds # since needs to be int in milliseconds
my_trades = self._api.fetch_my_trades(pair, int((since.timestamp() - 5) * 1000)) my_trades = self._api.fetch_my_trades(
pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000))
matched_trades = [trade for trade in my_trades if trade['order'] == order_id] matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
return matched_trades return matched_trades
@ -1049,27 +967,27 @@ def available_exchanges(ccxt_module=None) -> List[str]:
return [x for x in exchanges if not is_exchange_bad(x)] return [x for x in exchanges if not is_exchange_bad(x)]
def timeframe_to_seconds(ticker_interval: str) -> int: def timeframe_to_seconds(timeframe: str) -> int:
""" """
Translates the timeframe interval value written in the human readable Translates the timeframe interval value written in the human readable
form ('1m', '5m', '1h', '1d', '1w', etc.) to the number form ('1m', '5m', '1h', '1d', '1w', etc.) to the number
of seconds for one timeframe interval. of seconds for one timeframe interval.
""" """
return ccxt.Exchange.parse_timeframe(ticker_interval) return ccxt.Exchange.parse_timeframe(timeframe)
def timeframe_to_minutes(ticker_interval: str) -> int: def timeframe_to_minutes(timeframe: str) -> int:
""" """
Same as timeframe_to_seconds, but returns minutes. Same as timeframe_to_seconds, but returns minutes.
""" """
return ccxt.Exchange.parse_timeframe(ticker_interval) // 60 return ccxt.Exchange.parse_timeframe(timeframe) // 60
def timeframe_to_msecs(ticker_interval: str) -> int: def timeframe_to_msecs(timeframe: str) -> int:
""" """
Same as timeframe_to_seconds, but returns milliseconds. Same as timeframe_to_seconds, but returns milliseconds.
""" """
return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 return ccxt.Exchange.parse_timeframe(timeframe) * 1000
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime: def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:

View File

@ -20,9 +20,9 @@ from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.resolvers import (ExchangeResolver, PairListResolver, from freqtrade.resolvers import ExchangeResolver, StrategyResolver
StrategyResolver)
from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.rpc import RPCManager, RPCMessageType
from freqtrade.pairlist.pairlistmanager import PairListManager
from freqtrade.state import State from freqtrade.state import State
from freqtrade.strategy.interface import IStrategy, SellType from freqtrade.strategy.interface import IStrategy, SellType
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
@ -70,14 +70,13 @@ class FreqtradeBot:
# Attach Wallets to Strategy baseclass # Attach Wallets to Strategy baseclass
IStrategy.wallets = self.wallets IStrategy.wallets = self.wallets
pairlistname = self.config.get('pairlist', {}).get('method', 'StaticPairList') self.pairlists = PairListManager(self.exchange, self.config)
self.pairlists = PairListResolver(pairlistname, self, self.config).pairlist
# Initializing Edge only if enabled # Initializing Edge only if enabled
self.edge = Edge(self.config, self.exchange, self.strategy) if \ self.edge = Edge(self.config, self.exchange, self.strategy) if \
self.config.get('edge', {}).get('enabled', False) else None self.config.get('edge', {}).get('enabled', False) else None
self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] self.active_pair_whitelist = self._refresh_whitelist()
persistence.init(self.config.get('db_url', None), persistence.init(self.config.get('db_url', None),
clean_open_orders=self.config.get('dry_run', False)) clean_open_orders=self.config.get('dry_run', False))
@ -123,21 +122,10 @@ class FreqtradeBot:
# Check whether markets have to be reloaded # Check whether markets have to be reloaded
self.exchange._reload_markets() self.exchange._reload_markets()
# Refresh whitelist
self.pairlists.refresh_pairlist()
self.active_pair_whitelist = self.pairlists.whitelist
# Calculating Edge positioning
if self.edge:
self.edge.calculate()
self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist)
# Query trades from persistence layer # Query trades from persistence layer
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
# Extend active-pair whitelist with pairs from open trades self.active_pair_whitelist = self._refresh_whitelist(trades)
# It ensures that tickers are downloaded for open trades
self._extend_whitelist_with_trades(self.active_pair_whitelist, trades)
# Refreshing candles # Refreshing candles
self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist), self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist),
@ -150,7 +138,6 @@ class FreqtradeBot:
if len(trades) < self.config['max_open_trades']: if len(trades) < self.config['max_open_trades']:
self.process_maybe_execute_buys() self.process_maybe_execute_buys()
if 'unfilledtimeout' in self.config:
# Check and handle any timed out open orders # Check and handle any timed out open orders
self.check_handle_timedout() self.check_handle_timedout()
Trade.session.flush() Trade.session.flush()
@ -160,11 +147,24 @@ class FreqtradeBot:
logger.info(f"Bot heartbeat. PID={getpid()}") logger.info(f"Bot heartbeat. PID={getpid()}")
self._heartbeat_msg = arrow.utcnow().timestamp self._heartbeat_msg = arrow.utcnow().timestamp
def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]): def _refresh_whitelist(self, trades: List[Trade] = []) -> List[str]:
""" """
Extend whitelist with pairs from open trades Refresh whitelist from pairlist or edge and extend it with trades.
""" """
whitelist.extend([trade.pair for trade in trades if trade.pair not in whitelist]) # Refresh whitelist
self.pairlists.refresh_pairlist()
_whitelist = self.pairlists.whitelist
# Calculating Edge positioning
if self.edge:
self.edge.calculate()
_whitelist = self.edge.adjust(_whitelist)
if trades:
# Extend active-pair whitelist with pairs from open trades
# It ensures that tickers are downloaded for open trades
_whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist])
return _whitelist
def _create_pair_whitelist(self, pairs: List[str]) -> List[Tuple[str, str]]: def _create_pair_whitelist(self, pairs: List[str]) -> List[Tuple[str, str]]:
""" """
@ -266,7 +266,11 @@ class FreqtradeBot:
amount_reserve_percent += self.strategy.stoploss amount_reserve_percent += self.strategy.stoploss
# it should not be more than 50% # it should not be more than 50%
amount_reserve_percent = max(amount_reserve_percent, 0.5) amount_reserve_percent = max(amount_reserve_percent, 0.5)
return min(min_stake_amounts) / amount_reserve_percent
# 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 max(min_stake_amounts) / amount_reserve_percent
def create_trades(self) -> bool: def create_trades(self) -> bool:
""" """
@ -317,7 +321,6 @@ class FreqtradeBot:
(bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0): (bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0):
if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market): if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market):
buycount += self.execute_buy(_pair, stake_amount) buycount += self.execute_buy(_pair, stake_amount)
else:
continue continue
buycount += self.execute_buy(_pair, stake_amount) buycount += self.execute_buy(_pair, stake_amount)
@ -632,8 +635,8 @@ class FreqtradeBot:
Force-sells the pair (using EmergencySell reason) in case of Problems creating the order. Force-sells the pair (using EmergencySell reason) in case of Problems creating the order.
:return: True if the order succeeded, and False in case of problems. :return: True if the order succeeded, and False in case of problems.
""" """
# Limit price threshold: As limit price should always be below price # Limit price threshold: As limit price should always be below stop-price
LIMIT_PRICE_PCT = 0.99 LIMIT_PRICE_PCT = self.strategy.order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
try: try:
stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount, stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount,
@ -755,23 +758,28 @@ class FreqtradeBot:
return True return True
return False return False
def _check_timed_out(self, side: str, order: dict) -> bool:
"""
Check if timeout is active, and if the order is still open and timed out
"""
timeout = self.config.get('unfilledtimeout', {}).get(side)
ordertime = arrow.get(order['datetime']).datetime
if timeout is not None:
timeout_threshold = arrow.utcnow().shift(minutes=-timeout).datetime
return (order['status'] == 'open' and order['side'] == side
and ordertime < timeout_threshold)
return False
def check_handle_timedout(self) -> None: def check_handle_timedout(self) -> None:
""" """
Check if any orders are timed out and cancel if neccessary Check if any orders are timed out and cancel if neccessary
:param timeoutvalue: Number of minutes until order is considered timed out :param timeoutvalue: Number of minutes until order is considered timed out
:return: None :return: None
""" """
buy_timeout = self.config['unfilledtimeout']['buy']
sell_timeout = self.config['unfilledtimeout']['sell']
buy_timeout_threshold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
sell_timeout_threshold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all(): for trade in Trade.get_open_order_trades():
try: try:
# FIXME: Somehow the query above returns results
# where the open_order_id is in fact None.
# This is probably because the record got
# updated via /forcesell in a different thread.
if not trade.open_order_id: if not trade.open_order_id:
continue continue
order = self.exchange.get_order(trade.open_order_id, trade.pair) order = self.exchange.get_order(trade.open_order_id, trade.pair)
@ -781,23 +789,20 @@ class FreqtradeBot:
trade, trade,
traceback.format_exc()) traceback.format_exc())
continue continue
ordertime = arrow.get(order['datetime']).datetime
# Check if trade is still actually open # Check if trade is still actually open
if float(order['remaining']) == 0.0: if float(order.get('remaining', 0.0)) == 0.0:
self.wallets.update() self.wallets.update()
continue continue
if ((order['side'] == 'buy' and order['status'] == 'canceled') if ((order['side'] == 'buy' and order['status'] == 'canceled')
or (order['status'] == 'open' or (self._check_timed_out('buy', order))):
and order['side'] == 'buy' and ordertime < buy_timeout_threshold)):
self.handle_timedout_limit_buy(trade, order) self.handle_timedout_limit_buy(trade, order)
self.wallets.update() self.wallets.update()
elif ((order['side'] == 'sell' and order['status'] == 'canceled') elif ((order['side'] == 'sell' and order['status'] == 'canceled')
or (order['status'] == 'open' or (self._check_timed_out('sell', order))):
and order['side'] == 'sell' and ordertime < sell_timeout_threshold)):
self.handle_timedout_limit_sell(trade, order) self.handle_timedout_limit_sell(trade, order)
self.wallets.update() self.wallets.update()
@ -812,7 +817,8 @@ class FreqtradeBot:
}) })
def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool: def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool:
"""Buy timeout - cancel order """
Buy timeout - cancel order
:return: True if order was fully cancelled :return: True if order was fully cancelled
""" """
reason = "cancelled due to timeout" reason = "cancelled due to timeout"
@ -823,18 +829,22 @@ class FreqtradeBot:
corder = order corder = order
reason = "canceled on Exchange" reason = "canceled on Exchange"
if corder['remaining'] == corder['amount']: if corder.get('remaining', order['remaining']) == order['amount']:
# if trade is not partially completed, just delete the trade # if trade is not partially completed, just delete the trade
self.handle_buy_order_full_cancel(trade, reason) self.handle_buy_order_full_cancel(trade, reason)
return True return True
# if trade is partially complete, edit the stake details for the trade # if trade is partially complete, edit the stake details for the trade
# and close the order # and close the order
trade.amount = corder['amount'] - corder['remaining'] # cancel_order may not contain the full order dict, so we need to fallback
# to the order dict aquired before cancelling.
# we need to fall back to the values from order if corder does not contain these keys.
trade.amount = order['amount'] - corder.get('remaining', order['remaining'])
trade.stake_amount = trade.amount * trade.open_rate trade.stake_amount = trade.amount * trade.open_rate
# verify if fees were taken from amount to avoid problems during selling # verify if fees were taken from amount to avoid problems during selling
try: try:
new_amount = self.get_real_amount(trade, corder, trade.amount) new_amount = self.get_real_amount(trade, corder if 'fee' in corder else order,
trade.amount)
if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC): if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC):
trade.amount = new_amount trade.amount = new_amount
# Fee was applied, so set to 0 # Fee was applied, so set to 0

View File

@ -1,9 +1,12 @@
import logging import logging
import sys import sys
from logging.handlers import RotatingFileHandler from logging import Formatter
from logging.handlers import RotatingFileHandler, SysLogHandler
from typing import Any, Dict, List from typing import Any, Dict, List
from freqtrade import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,11 +36,39 @@ def setup_logging(config: Dict[str, Any]) -> None:
# Log level # Log level
verbosity = config['verbosity'] verbosity = config['verbosity']
# Log to stdout, not stderr # Log to stderr
log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)] log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stderr)]
if config.get('logfile'): logfile = config.get('logfile')
log_handlers.append(RotatingFileHandler(config['logfile'], if logfile:
s = logfile.split(':')
if s[0] == 'syslog':
# Address can be either a string (socket filename) for Unix domain socket or
# a tuple (hostname, port) for UDP socket.
# Address can be omitted (i.e. simple 'syslog' used as the value of
# config['logfilename']), which defaults to '/dev/log', applicable for most
# of the systems.
address = (s[1], int(s[2])) if len(s) > 2 else s[1] if len(s) > 1 else '/dev/log'
handler = SysLogHandler(address=address)
# No datetime field for logging into syslog, to allow syslog
# to perform reduction of repeating messages if this is set in the
# syslog config. The messages should be equal for this.
handler.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
log_handlers.append(handler)
elif s[0] == 'journald':
try:
from systemd.journal import JournaldLogHandler
except ImportError:
raise OperationalException("You need the systemd python package be installed in "
"order to use logging to journald.")
handler = JournaldLogHandler()
# No datetime field for logging into journald, to allow syslog
# to perform reduction of repeating messages if this is set in the
# syslog config. The messages should be equal for this.
handler.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
log_handlers.append(handler)
else:
log_handlers.append(RotatingFileHandler(logfile,
maxBytes=1024 * 1024, # 1Mb maxBytes=1024 * 1024, # 1Mb
backupCount=10)) backupCount=10))

View File

@ -15,7 +15,6 @@ from typing import Any, List
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.configuration import Arguments from freqtrade.configuration import Arguments
from freqtrade.worker import Worker
logger = logging.getLogger('freqtrade') logger = logging.getLogger('freqtrade')
@ -28,21 +27,23 @@ def main(sysargv: List[str] = None) -> None:
""" """
return_code: Any = 1 return_code: Any = 1
worker = None
try: try:
arguments = Arguments(sysargv) arguments = Arguments(sysargv)
args = arguments.get_parsed_arg() args = arguments.get_parsed_arg()
# A subcommand has been issued. # Call subcommand.
# Means if Backtesting or Hyperopt have been called we exit the bot
if 'func' in args: if 'func' in args:
args['func'](args) return_code = args['func'](args)
# TODO: fetch return_code as returned by the command function here
return_code = 0
else: else:
# Load and run worker # No subcommand was issued.
worker = Worker(args) raise OperationalException(
worker.run() "Usage of Freqtrade requires a subcommand to be specified.\n"
"To have the previous behavior (bot executing trades in live/dry-run modes, "
"depending on the value of the `dry_run` setting in the config), run freqtrade "
"as `freqtrade trade [options...]`.\n"
"To see the full list of options available, please use "
"`freqtrade --help` or `freqtrade <command> --help`."
)
except SystemExit as e: except SystemExit as e:
return_code = e return_code = e
@ -55,8 +56,6 @@ def main(sysargv: List[str] = None) -> None:
except Exception: except Exception:
logger.exception('Fatal exception!') logger.exception('Fatal exception!')
finally: finally:
if worker:
worker.exit()
sys.exit(return_code) sys.exit(return_code)

View File

@ -127,3 +127,16 @@ def round_dict(d, n):
def plural(num, singular: str, plural: str = None) -> str: def plural(num, singular: str, plural: str = None) -> str:
return singular if (num == 1 or num == -1) else plural or singular + 's' return singular if (num == 1 or num == -1) else plural or singular + 's'
def render_template(templatefile: str, arguments: dict = {}):
from jinja2 import Environment, PackageLoader, select_autoescape
env = Environment(
loader=PackageLoader('freqtrade', 'templates'),
autoescape=select_autoescape(['html', 'xml'])
)
template = env.get_template(templatefile)
return template.render(**arguments)

View File

@ -78,7 +78,7 @@ def start_hyperopt(args: Dict[str, Any]) -> None:
except Timeout: except Timeout:
logger.info("Another running instance of freqtrade Hyperopt detected.") logger.info("Another running instance of freqtrade Hyperopt detected.")
logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. " logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. "
"Hyperopt module is resource hungry. Please run your Hyperopts sequentially " "Hyperopt module is resource hungry. Please run your Hyperopt sequentially "
"or on separate machines.") "or on separate machines.")
logger.info("Quitting now.") logger.info("Quitting now.")
# TODO: return False here in order to help freqtrade to exit # TODO: return False here in order to help freqtrade to exit

View File

@ -10,18 +10,19 @@ from pathlib import Path
from typing import Any, Dict, List, NamedTuple, Optional from typing import Any, Dict, List, NamedTuple, Optional
from pandas import DataFrame from pandas import DataFrame
from tabulate import tabulate
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.configuration import TimeRange from freqtrade.configuration import (TimeRange, remove_credentials,
validate_config_consistency)
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.misc import file_dump_json from freqtrade.misc import file_dump_json
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.strategy.interface import IStrategy, SellType from freqtrade.strategy.interface import IStrategy, SellType
from tabulate import tabulate
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -57,11 +58,7 @@ class Backtesting:
self.config = config self.config = config
# Reset keys for backtesting # Reset keys for backtesting
self.config['exchange']['key'] = '' remove_credentials(self.config)
self.config['exchange']['secret'] = ''
self.config['exchange']['password'] = ''
self.config['exchange']['uid'] = ''
self.config['dry_run'] = True
self.strategylist: List[IStrategy] = [] self.strategylist: List[IStrategy] = []
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
@ -79,17 +76,21 @@ class Backtesting:
stratconf = deepcopy(self.config) stratconf = deepcopy(self.config)
stratconf['strategy'] = strat stratconf['strategy'] = strat
self.strategylist.append(StrategyResolver(stratconf).strategy) self.strategylist.append(StrategyResolver(stratconf).strategy)
validate_config_consistency(stratconf)
else: else:
# No strategy list specified, only one strategy # No strategy list specified, only one strategy
self.strategylist.append(StrategyResolver(self.config).strategy) self.strategylist.append(StrategyResolver(self.config).strategy)
validate_config_consistency(self.config)
if "ticker_interval" not in self.config: if "ticker_interval" not in self.config:
raise OperationalException("Ticker-interval needs to be set in either configuration " raise OperationalException("Ticker-interval needs to be set in either configuration "
"or as cli argument `--ticker-interval 5m`") "or as cli argument `--ticker-interval 5m`")
self.ticker_interval = str(self.config.get('ticker_interval')) self.timeframe = str(self.config.get('ticker_interval'))
self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval) self.timeframe_mins = timeframe_to_minutes(self.timeframe)
# Get maximum required startup period
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
# Load one (first) strategy # Load one (first) strategy
self._set_strategy(self.strategylist[0]) self._set_strategy(self.strategylist[0])
@ -103,6 +104,31 @@ class Backtesting:
# And the regular "stoploss" function would not apply to that case # And the regular "stoploss" function would not apply to that case
self.strategy.order_types['stoploss_on_exchange'] = False self.strategy.order_types['stoploss_on_exchange'] = False
def load_bt_data(self):
timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
data = history.load_data(
datadir=Path(self.config['datadir']),
pairs=self.config['exchange']['pair_whitelist'],
timeframe=self.timeframe,
timerange=timerange,
startup_candles=self.required_startup,
fail_without_data=True,
)
min_date, max_date = history.get_timeframe(data)
logger.info(
'Loading data from %s up to %s (%s days)..',
min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days
)
# Adjust startts forward if not enough data is available
timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe),
self.required_startup, min_date)
return data, timerange
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame, def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame,
skip_nan: bool = False) -> str: skip_nan: bool = False) -> str:
""" """
@ -218,7 +244,8 @@ class Backtesting:
ticker: Dict = {} ticker: Dict = {}
# Create ticker dict # Create ticker dict
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run pair_data.loc[:, 'buy'] = 0 # cleanup from previous run
pair_data.loc[:, 'sell'] = 0 # cleanup from previous run
ticker_data = self.strategy.advise_sell( ticker_data = self.strategy.advise_sell(
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
@ -351,7 +378,7 @@ class Backtesting:
lock_pair_until: Dict = {} lock_pair_until: Dict = {}
# Indexes per pair, so some pairs are allowed to have a missing start. # Indexes per pair, so some pairs are allowed to have a missing start.
indexes: Dict = {} indexes: Dict = {}
tmp = start_date + timedelta(minutes=self.ticker_interval_mins) tmp = start_date + timedelta(minutes=self.timeframe_mins)
# Loop timerange and get candle for each pair at that point in time # Loop timerange and get candle for each pair at that point in time
while tmp < end_date: while tmp < end_date:
@ -403,7 +430,7 @@ class Backtesting:
lock_pair_until[pair] = end_date.datetime lock_pair_until[pair] = end_date.datetime
# Move time one configured time_interval ahead. # Move time one configured time_interval ahead.
tmp += timedelta(minutes=self.ticker_interval_mins) tmp += timedelta(minutes=self.timeframe_mins)
return DataFrame.from_records(trades, columns=BacktestResult._fields) return DataFrame.from_records(trades, columns=BacktestResult._fields)
def start(self) -> None: def start(self) -> None:
@ -412,39 +439,18 @@ class Backtesting:
:return: None :return: None
""" """
data: Dict[str, Any] = {} data: Dict[str, Any] = {}
pairs = self.config['exchange']['pair_whitelist']
logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
logger.info('Using stake_amount: %s ...', self.config['stake_amount']) logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
data = history.load_data(
datadir=Path(self.config['datadir']),
pairs=pairs,
ticker_interval=self.ticker_interval,
timerange=timerange,
)
if not data:
logger.critical("No data found. Terminating.")
return
# Use max_open_trades in backtesting, except --disable-max-market-positions is set # Use max_open_trades in backtesting, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True): if self.config.get('use_max_market_positions', True):
max_open_trades = self.config['max_open_trades'] max_open_trades = self.config['max_open_trades']
else: else:
logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...') logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
max_open_trades = 0 max_open_trades = 0
data, timerange = self.load_bt_data()
all_results = {} all_results = {}
min_date, max_date = history.get_timeframe(data)
logger.info(
'Backtesting with data from %s up to %s (%s days)..',
min_date.isoformat(),
max_date.isoformat(),
(max_date - min_date).days
)
for strat in self.strategylist: for strat in self.strategylist:
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
self._set_strategy(strat) self._set_strategy(strat)
@ -452,6 +458,15 @@ class Backtesting:
# need to reprocess data every time to populate signals # need to reprocess data every time to populate signals
preprocessed = self.strategy.tickerdata_to_dataframe(data) preprocessed = self.strategy.tickerdata_to_dataframe(data)
# Trim startup period from analyzed dataframe
for pair, df in preprocessed.items():
preprocessed[pair] = history.trim_dataframe(df, timerange)
min_date, max_date = history.get_timeframe(preprocessed)
logger.info(
'Backtesting with data from %s up to %s (%s days)..',
min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days
)
# Execute backtest and print results # Execute backtest and print results
all_results[self.strategy.get_strategy_name()] = self.backtest( all_results[self.strategy.get_strategy_name()] = self.backtest(
{ {

View File

@ -4,12 +4,14 @@
This module contains the edge backtesting interface This module contains the edge backtesting interface
""" """
import logging import logging
from typing import Dict, Any from typing import Any, Dict
from tabulate import tabulate
from freqtrade import constants
from freqtrade.edge import Edge
from freqtrade.configuration import TimeRange from tabulate import tabulate
from freqtrade import constants
from freqtrade.configuration import (TimeRange, remove_credentials,
validate_config_consistency)
from freqtrade.edge import Edge
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
@ -29,15 +31,13 @@ class EdgeCli:
self.config = config self.config = config
# Reset keys for edge # Reset keys for edge
self.config['exchange']['key'] = '' remove_credentials(self.config)
self.config['exchange']['secret'] = ''
self.config['exchange']['password'] = ''
self.config['exchange']['uid'] = ''
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
self.config['dry_run'] = True
self.exchange = Exchange(self.config) self.exchange = Exchange(self.config)
self.strategy = StrategyResolver(self.config).strategy self.strategy = StrategyResolver(self.config).strategy
validate_config_consistency(self.config)
self.edge = Edge(config, self.exchange, self.strategy) self.edge = Edge(config, self.exchange, self.strategy)
# Set refresh_pairs to false for edge-cli (it must be true for edge) # Set refresh_pairs to false for edge-cli (it must be true for edge)
self.edge._refresh_pairs = False self.edge._refresh_pairs = False

View File

@ -4,9 +4,9 @@
This module contains the hyperopt logic This module contains the hyperopt logic
""" """
import locale
import logging import logging
import sys import sys
from collections import OrderedDict from collections import OrderedDict
from operator import itemgetter from operator import itemgetter
from pathlib import Path from pathlib import Path
@ -14,23 +14,23 @@ from pprint import pprint
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import rapidjson import rapidjson
from colorama import init as colorama_init
from colorama import Fore, Style from colorama import Fore, Style
from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count from colorama import init as colorama_init
from joblib import (Parallel, cpu_count, delayed, dump, load,
wrap_non_picklable_objects)
from pandas import DataFrame from pandas import DataFrame
from skopt import Optimizer from skopt import Optimizer
from skopt.space import Dimension from skopt.space import Dimension
from freqtrade.configuration import TimeRange from freqtrade import OperationalException
from freqtrade.data.history import load_data, get_timeframe from freqtrade.data.history import get_timeframe, trim_dataframe
from freqtrade.misc import round_dict from freqtrade.misc import plural, round_dict
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F4 from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F4
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver,
HyperOptResolver)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -55,10 +55,10 @@ class Hyperopt:
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
self.config = config self.config = config
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt
self.backtesting = Backtesting(self.config) self.backtesting = Backtesting(self.config)
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt
self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
@ -75,6 +75,8 @@ class Hyperopt:
else: else:
logger.info("Continuing on previous hyperopt results.") logger.info("Continuing on previous hyperopt results.")
self.num_trials_saved = 0
# Previous evaluations # Previous evaluations
self.trials: List = [] self.trials: List = []
@ -103,6 +105,10 @@ class Hyperopt:
self.config['ask_strategy'] = {} self.config['ask_strategy'] = {}
self.config['ask_strategy']['use_sell_signal'] = True self.config['ask_strategy']['use_sell_signal'] = True
self.print_all = self.config.get('print_all', False)
self.print_colorized = self.config.get('print_colorized', False)
self.print_json = self.config.get('print_json', False)
@staticmethod @staticmethod
def get_lock_filename(config) -> str: def get_lock_filename(config) -> str:
@ -118,125 +124,179 @@ class Hyperopt:
logger.info(f"Removing `{p}`.") logger.info(f"Removing `{p}`.")
p.unlink() p.unlink()
def get_args(self, params): def _get_params_dict(self, raw_params: List[Any]) -> Dict:
dimensions = self.dimensions dimensions: List[Dimension] = self.dimensions
# Ensure the number of dimensions match # Ensure the number of dimensions match
# the number of parameters in the list x. # the number of parameters in the list.
if len(params) != len(dimensions): if len(raw_params) != len(dimensions):
raise ValueError('Mismatch in number of search-space dimensions. ' raise ValueError('Mismatch in number of search-space dimensions.')
f'len(dimensions)=={len(dimensions)} and len(x)=={len(params)}')
# Create a dict where the keys are the names of the dimensions # Return a dict where the keys are the names of the dimensions
# and the values are taken from the list of parameters x. # and the values are taken from the list of parameters.
arg_dict = {dim.name: value for dim, value in zip(dimensions, params)} return {d.name: v for d, v in zip(dimensions, raw_params)}
return arg_dict
def save_trials(self) -> None: def save_trials(self, final: bool = False) -> None:
""" """
Save hyperopt trials to file Save hyperopt trials to file
""" """
if self.trials: num_trials = len(self.trials)
logger.info("Saving %d evaluations to '%s'", len(self.trials), self.trials_file) if num_trials > self.num_trials_saved:
logger.info(f"Saving {num_trials} {plural(num_trials, 'epoch')}.")
dump(self.trials, self.trials_file) dump(self.trials, self.trials_file)
self.num_trials_saved = num_trials
if final:
logger.info(f"{num_trials} {plural(num_trials, 'epoch')} "
f"saved to '{self.trials_file}'.")
def read_trials(self) -> List: @staticmethod
def _read_trials(trials_file) -> List:
""" """
Read hyperopt trials file Read hyperopt trials file
""" """
logger.info("Reading Trials from '%s'", self.trials_file) logger.info("Reading Trials from '%s'", trials_file)
trials = load(self.trials_file) trials = load(trials_file)
self.trials_file.unlink()
return trials return trials
def log_trials_result(self) -> None: def _get_params_details(self, params: Dict) -> Dict:
""" """
Display Best hyperopt result Return the params for each space
""" """
results = sorted(self.trials, key=itemgetter('loss')) result: Dict = {}
best_result = results[0]
params = best_result['params']
log_str = self.format_results_logstring(best_result)
print(f"\nBest result:\n\n{log_str}\n")
if self.config.get('print_json'):
result_dict: Dict = {}
if self.has_space('buy') or self.has_space('sell'):
result_dict['params'] = {}
if self.has_space('buy'): if self.has_space('buy'):
result_dict['params'].update({p.name: params.get(p.name) result['buy'] = {p.name: params.get(p.name)
for p in self.hyperopt_space('buy')}) for p in self.hyperopt_space('buy')}
if self.has_space('sell'): if self.has_space('sell'):
result_dict['params'].update({p.name: params.get(p.name) result['sell'] = {p.name: params.get(p.name)
for p in self.hyperopt_space('sell')}) for p in self.hyperopt_space('sell')}
if self.has_space('roi'): if self.has_space('roi'):
result['roi'] = self.custom_hyperopt.generate_roi_table(params)
if self.has_space('stoploss'):
result['stoploss'] = {p.name: params.get(p.name)
for p in self.hyperopt_space('stoploss')}
if self.has_space('trailing'):
result['trailing'] = {p.name: params.get(p.name)
for p in self.hyperopt_space('trailing')}
return result
@staticmethod
def print_epoch_details(results, total_epochs, print_json: bool,
no_header: bool = False, header_str: str = None) -> None:
"""
Display details of the hyperopt result
"""
params = results.get('params_details', {})
# Default header string
if header_str is None:
header_str = "Best result"
if not no_header:
explanation_str = Hyperopt._format_explanation_string(results, total_epochs)
print(f"\n{header_str}:\n\n{explanation_str}\n")
if print_json:
result_dict: Dict = {}
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
Hyperopt._params_update_for_json(result_dict, params, s)
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
else:
Hyperopt._params_pretty_print(params, 'buy', "Buy hyperspace params:")
Hyperopt._params_pretty_print(params, 'sell', "Sell hyperspace params:")
Hyperopt._params_pretty_print(params, 'roi', "ROI table:")
Hyperopt._params_pretty_print(params, 'stoploss', "Stoploss:")
Hyperopt._params_pretty_print(params, 'trailing', "Trailing stop:")
@staticmethod
def _params_update_for_json(result_dict, params, space: str):
if space in params:
space_params = Hyperopt._space_params(params, space)
if space in ['buy', 'sell']:
result_dict.setdefault('params', {}).update(space_params)
elif space == 'roi':
# Convert keys in min_roi dict to strings because # Convert keys in min_roi dict to strings because
# rapidjson cannot dump dicts with integer keys... # rapidjson cannot dump dicts with integer keys...
# OrderedDict is used to keep the numeric order of the items # OrderedDict is used to keep the numeric order of the items
# in the dict. # in the dict.
result_dict['minimal_roi'] = OrderedDict( result_dict['minimal_roi'] = OrderedDict(
(str(k), v) for k, v in self.custom_hyperopt.generate_roi_table(params).items() (str(k), v) for k, v in space_params.items()
) )
if self.has_space('stoploss'): else: # 'stoploss', 'trailing'
result_dict['stoploss'] = params.get('stoploss') result_dict.update(space_params)
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
else:
if self.has_space('buy'):
print('Buy hyperspace params:')
pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')},
indent=4)
if self.has_space('sell'):
print('Sell hyperspace params:')
pprint({p.name: params.get(p.name) for p in self.hyperopt_space('sell')},
indent=4)
if self.has_space('roi'):
print("ROI table:")
# Round printed values to 5 digits after the decimal point
pprint(round_dict(self.custom_hyperopt.generate_roi_table(params), 5), indent=4)
if self.has_space('stoploss'):
# Also round to 5 digits after the decimal point
print(f"Stoploss: {round(params.get('stoploss'), 5)}")
def log_results(self, results) -> None: @staticmethod
def _params_pretty_print(params, space: str, header: str):
if space in params:
space_params = Hyperopt._space_params(params, space, 5)
if space == 'stoploss':
print(header, space_params.get('stoploss'))
else:
print(header)
pprint(space_params, indent=4)
@staticmethod
def _space_params(params, space: str, r: int = None) -> Dict:
d = params[space]
# Round floats to `r` digits after the decimal point if requested
return round_dict(d, r) if r else d
@staticmethod
def is_best_loss(results, current_best_loss) -> bool:
return results['loss'] < current_best_loss
def print_results(self, results) -> None:
""" """
Log results if it is better than any previous evaluation Log results if it is better than any previous evaluation
""" """
print_all = self.config.get('print_all', False) is_best = results['is_best']
is_best_loss = results['loss'] < self.current_best_loss if not self.print_all:
if print_all or is_best_loss: # Print '\n' after each 100th epoch to separate dots from the log messages.
if is_best_loss: # Otherwise output is messy on a terminal.
self.current_best_loss = results['loss'] print('.', end='' if results['current_epoch'] % 100 != 0 else None) # type: ignore
log_str = self.format_results_logstring(results)
# Colorize output
if self.config.get('print_colorized', False):
if results['total_profit'] > 0:
log_str = Fore.GREEN + log_str
if print_all and is_best_loss:
log_str = Style.BRIGHT + log_str
if print_all:
print(log_str)
else:
print('\n' + log_str)
else:
print('.', end='')
sys.stdout.flush() sys.stdout.flush()
def format_results_logstring(self, results) -> str: if self.print_all or is_best:
# Output human-friendly index here (starting from 1) if not self.print_all:
current = results['current_epoch'] + 1 # Separate the results explanation string from dots
total = self.total_epochs print("\n")
res = results['results_explanation'] self.print_results_explanation(results, self.total_epochs, self.print_all,
loss = results['loss'] self.print_colorized)
log_str = f'{current:5d}/{total}: {res} Objective: {loss:.5f}'
log_str = f'*{log_str}' if results['is_initial_point'] else f' {log_str}' @staticmethod
return log_str def print_results_explanation(results, total_epochs, highlight_best: bool,
print_colorized: bool) -> None:
"""
Log results explanation string
"""
explanation_str = Hyperopt._format_explanation_string(results, total_epochs)
# Colorize output
if print_colorized:
if results['total_profit'] > 0:
explanation_str = Fore.GREEN + explanation_str
if highlight_best and results['is_best']:
explanation_str = Style.BRIGHT + explanation_str
print(explanation_str)
@staticmethod
def _format_explanation_string(results, total_epochs) -> str:
return (("*" if results['is_initial_point'] else " ") +
f"{results['current_epoch']:5d}/{total_epochs}: " +
f"{results['results_explanation']} " +
f"Objective: {results['loss']:.5f}")
def has_space(self, space: str) -> bool: def has_space(self, space: str) -> bool:
""" """
Tell if a space value is contained in the configuration Tell if the space value is contained in the configuration
""" """
# The 'trailing' space is not included in the 'default' set of spaces
if space == 'trailing':
return any(s in self.config['spaces'] for s in [space, 'all']) return any(s in self.config['spaces'] for s in [space, 'all'])
else:
return any(s in self.config['spaces'] for s in [space, 'all', 'default'])
def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]:
""" """
@ -246,47 +306,66 @@ class Hyperopt:
for all hyperspaces used. for all hyperspaces used.
""" """
spaces: List[Dimension] = [] spaces: List[Dimension] = []
if space == 'buy' or (space is None and self.has_space('buy')): if space == 'buy' or (space is None and self.has_space('buy')):
logger.debug("Hyperopt has 'buy' space") logger.debug("Hyperopt has 'buy' space")
spaces += self.custom_hyperopt.indicator_space() spaces += self.custom_hyperopt.indicator_space()
if space == 'sell' or (space is None and self.has_space('sell')): if space == 'sell' or (space is None and self.has_space('sell')):
logger.debug("Hyperopt has 'sell' space") logger.debug("Hyperopt has 'sell' space")
spaces += self.custom_hyperopt.sell_indicator_space() spaces += self.custom_hyperopt.sell_indicator_space()
if space == 'roi' or (space is None and self.has_space('roi')): if space == 'roi' or (space is None and self.has_space('roi')):
logger.debug("Hyperopt has 'roi' space") logger.debug("Hyperopt has 'roi' space")
spaces += self.custom_hyperopt.roi_space() spaces += self.custom_hyperopt.roi_space()
if space == 'stoploss' or (space is None and self.has_space('stoploss')): if space == 'stoploss' or (space is None and self.has_space('stoploss')):
logger.debug("Hyperopt has 'stoploss' space") logger.debug("Hyperopt has 'stoploss' space")
spaces += self.custom_hyperopt.stoploss_space() spaces += self.custom_hyperopt.stoploss_space()
if space == 'trailing' or (space is None and self.has_space('trailing')):
logger.debug("Hyperopt has 'trailing' space")
spaces += self.custom_hyperopt.trailing_space()
return spaces return spaces
def generate_optimizer(self, _params: Dict, iteration=None) -> Dict: def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict:
""" """
Used Optimize function. Called once per epoch to optimize whatever is configured. Used Optimize function. Called once per epoch to optimize whatever is configured.
Keep this function as optimized as possible! Keep this function as optimized as possible!
""" """
params = self.get_args(_params) params_dict = self._get_params_dict(raw_params)
params_details = self._get_params_details(params_dict)
if self.has_space('roi'): if self.has_space('roi'):
self.backtesting.strategy.minimal_roi = \ self.backtesting.strategy.minimal_roi = \
self.custom_hyperopt.generate_roi_table(params) self.custom_hyperopt.generate_roi_table(params_dict)
if self.has_space('buy'): if self.has_space('buy'):
self.backtesting.strategy.advise_buy = \ self.backtesting.strategy.advise_buy = \
self.custom_hyperopt.buy_strategy_generator(params) self.custom_hyperopt.buy_strategy_generator(params_dict)
if self.has_space('sell'): if self.has_space('sell'):
self.backtesting.strategy.advise_sell = \ self.backtesting.strategy.advise_sell = \
self.custom_hyperopt.sell_strategy_generator(params) self.custom_hyperopt.sell_strategy_generator(params_dict)
if self.has_space('stoploss'): if self.has_space('stoploss'):
self.backtesting.strategy.stoploss = params['stoploss'] self.backtesting.strategy.stoploss = params_dict['stoploss']
if self.has_space('trailing'):
self.backtesting.strategy.trailing_stop = params_dict['trailing_stop']
self.backtesting.strategy.trailing_stop_positive = \
params_dict['trailing_stop_positive']
self.backtesting.strategy.trailing_stop_positive_offset = \
params_dict['trailing_stop_positive_offset']
self.backtesting.strategy.trailing_only_offset_is_reached = \
params_dict['trailing_only_offset_is_reached']
processed = load(self.tickerdata_pickle) processed = load(self.tickerdata_pickle)
min_date, max_date = get_timeframe(processed) min_date, max_date = get_timeframe(processed)
results = self.backtesting.backtest( backtesting_results = self.backtesting.backtest(
{ {
'stake_amount': self.config['stake_amount'], 'stake_amount': self.config['stake_amount'],
'processed': processed, 'processed': processed,
@ -296,56 +375,63 @@ class Hyperopt:
'end_date': max_date, 'end_date': max_date,
} }
) )
results_explanation = self.format_results(results) return self._get_results_dict(backtesting_results, min_date, max_date,
params_dict, params_details)
trade_count = len(results.index) def _get_results_dict(self, backtesting_results, min_date, max_date,
total_profit = results.profit_abs.sum() params_dict, params_details):
results_metrics = self._calculate_results_metrics(backtesting_results)
results_explanation = self._format_results_explanation_string(results_metrics)
trade_count = results_metrics['trade_count']
total_profit = results_metrics['total_profit']
# If this evaluation contains too short amount of trades to be # If this evaluation contains too short amount of trades to be
# interesting -- consider it as 'bad' (assigned max. loss value) # interesting -- consider it as 'bad' (assigned max. loss value)
# in order to cast this hyperspace point away from optimization # in order to cast this hyperspace point away from optimization
# path. We do not want to optimize 'hodl' strategies. # path. We do not want to optimize 'hodl' strategies.
if trade_count < self.config['hyperopt_min_trades']: loss: float = MAX_LOSS
return { if trade_count >= self.config['hyperopt_min_trades']:
'loss': MAX_LOSS, loss = self.calculate_loss(results=backtesting_results, trade_count=trade_count,
'params': params,
'results_explanation': results_explanation,
'total_profit': total_profit,
}
loss = self.calculate_loss(results=results, trade_count=trade_count,
min_date=min_date.datetime, max_date=max_date.datetime) min_date=min_date.datetime, max_date=max_date.datetime)
return { return {
'loss': loss, 'loss': loss,
'params': params, 'params_dict': params_dict,
'params_details': params_details,
'results_metrics': results_metrics,
'results_explanation': results_explanation, 'results_explanation': results_explanation,
'total_profit': total_profit, 'total_profit': total_profit,
} }
def format_results(self, results: DataFrame) -> str: def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict:
return {
'trade_count': len(backtesting_results.index),
'avg_profit': backtesting_results.profit_percent.mean() * 100.0,
'total_profit': backtesting_results.profit_abs.sum(),
'profit': backtesting_results.profit_percent.sum() * 100.0,
'duration': backtesting_results.trade_duration.mean(),
}
def _format_results_explanation_string(self, results_metrics: Dict) -> str:
""" """
Return the formatted results explanation in a string Return the formatted results explanation in a string
""" """
trades = len(results.index)
avg_profit = results.profit_percent.mean() * 100.0
total_profit = results.profit_abs.sum()
stake_cur = self.config['stake_currency'] stake_cur = self.config['stake_currency']
profit = results.profit_percent.sum() * 100.0 return (f"{results_metrics['trade_count']:6d} trades. "
duration = results.trade_duration.mean() f"Avg profit {results_metrics['avg_profit']: 6.2f}%. "
f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} "
f"({results_metrics['profit']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). "
f"Avg duration {results_metrics['duration']:5.1f} mins."
).encode(locale.getpreferredencoding(), 'replace').decode('utf-8')
return (f'{trades:6d} trades. Avg profit {avg_profit: 5.2f}%. ' def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer:
f'Total profit {total_profit: 11.8f} {stake_cur} '
f'({profit: 7.2f}Σ%). Avg duration {duration:5.1f} mins.')
def get_optimizer(self, dimensions, cpu_count) -> Optimizer:
return Optimizer( return Optimizer(
dimensions, dimensions,
base_estimator="ET", base_estimator="ET",
acq_optimizer="auto", acq_optimizer="auto",
n_initial_points=INITIAL_POINTS, n_initial_points=INITIAL_POINTS,
acq_optimizer_kwargs={'n_jobs': cpu_count}, acq_optimizer_kwargs={'n_jobs': cpu_count},
random_state=self.config.get('hyperopt_random_state', None) random_state=self.config.get('hyperopt_random_state', None),
) )
def fix_optimizer_models_list(self): def fix_optimizer_models_list(self):
@ -369,56 +455,51 @@ class Hyperopt:
return parallel(delayed( return parallel(delayed(
wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked) wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked)
def load_previous_results(self): @staticmethod
""" read trials file if we have one """ def load_previous_results(trials_file) -> List:
if self.trials_file.is_file() and self.trials_file.stat().st_size > 0: """
self.trials = self.read_trials() Load data for epochs from the file if we have one
logger.info( """
'Loaded %d previous evaluations from disk.', trials: List = []
len(self.trials) if trials_file.is_file() and trials_file.stat().st_size > 0:
) trials = Hyperopt._read_trials(trials_file)
if trials[0].get('is_best') is None:
raise OperationalException(
"The file with Hyperopt results is incompatible with this version "
"of Freqtrade and cannot be loaded.")
logger.info(f"Loaded {len(trials)} previous evaluations from disk.")
return trials
def start(self) -> None: def start(self) -> None:
timerange = TimeRange.parse_timerange(None if self.config.get( data, timerange = self.backtesting.load_bt_data()
'timerange') is None else str(self.config.get('timerange')))
data = load_data(
datadir=Path(self.config['datadir']),
pairs=self.config['exchange']['pair_whitelist'],
ticker_interval=self.backtesting.ticker_interval,
timerange=timerange
)
if not data: preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data)
logger.critical("No data found. Terminating.")
return
# Trim startup period from analyzed dataframe
for pair, df in preprocessed.items():
preprocessed[pair] = trim_dataframe(df, timerange)
min_date, max_date = get_timeframe(data) min_date, max_date = get_timeframe(data)
logger.info( logger.info(
'Hyperopting with data from %s up to %s (%s days)..', 'Hyperopting with data from %s up to %s (%s days)..',
min_date.isoformat(), min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days
max_date.isoformat(),
(max_date - min_date).days
) )
preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data)
dump(preprocessed, self.tickerdata_pickle) dump(preprocessed, self.tickerdata_pickle)
# We don't need exchange instance anymore while running hyperopt # We don't need exchange instance anymore while running hyperopt
self.backtesting.exchange = None # type: ignore self.backtesting.exchange = None # type: ignore
self.load_previous_results() self.trials = self.load_previous_results(self.trials_file)
cpus = cpu_count() cpus = cpu_count()
logger.info(f"Found {cpus} CPU cores. Let's make them scream!") logger.info(f"Found {cpus} CPU cores. Let's make them scream!")
config_jobs = self.config.get('hyperopt_jobs', -1) config_jobs = self.config.get('hyperopt_jobs', -1)
logger.info(f'Number of parallel jobs set as: {config_jobs}') logger.info(f'Number of parallel jobs set as: {config_jobs}')
self.dimensions = self.hyperopt_space() self.dimensions: List[Dimension] = self.hyperopt_space()
self.opt = self.get_optimizer(self.dimensions, config_jobs) self.opt = self.get_optimizer(self.dimensions, config_jobs)
if self.config.get('print_colorized', False): if self.print_colorized:
colorama_init(autoreset=True) colorama_init(autoreset=True)
try: try:
@ -432,15 +513,38 @@ class Hyperopt:
self.opt.tell(asked, [v['loss'] for v in f_val]) self.opt.tell(asked, [v['loss'] for v in f_val])
self.fix_optimizer_models_list() self.fix_optimizer_models_list()
for j in range(jobs): for j in range(jobs):
current = i * jobs + j # Use human-friendly indexes here (starting from 1)
current = i * jobs + j + 1
val = f_val[j] val = f_val[j]
val['current_epoch'] = current val['current_epoch'] = current
val['is_initial_point'] = current < INITIAL_POINTS val['is_initial_point'] = current <= INITIAL_POINTS
self.log_results(val)
self.trials.append(val)
logger.debug(f"Optimizer epoch evaluated: {val}") logger.debug(f"Optimizer epoch evaluated: {val}")
is_best = self.is_best_loss(val, self.current_best_loss)
# This value is assigned here and not in the optimization method
# to keep proper order in the list of results. That's because
# evaluations can take different time. Here they are aligned in the
# order they will be shown to the user.
val['is_best'] = is_best
self.print_results(val)
if is_best:
self.current_best_loss = val['loss']
self.trials.append(val)
# Save results after each best epoch and every 100 epochs
if is_best or current % 100 == 0:
self.save_trials()
except KeyboardInterrupt: except KeyboardInterrupt:
print('User interrupted..') print('User interrupted..')
self.save_trials() self.save_trials(final=True)
self.log_trials_result()
if self.trials:
sorted_trials = sorted(self.trials, key=itemgetter('loss'))
results = sorted_trials[0]
self.print_epoch_details(results, self.total_epochs, self.print_json)
else:
# This is printed when Ctrl+C is pressed quickly, before first epochs have
# a chance to be evaluated.
print("No epochs evaluated yet, no best result.")

View File

@ -1,15 +1,14 @@
""" """
IHyperOpt interface IHyperOpt interface
This module defines the interface to apply for hyperopts This module defines the interface to apply for hyperopt
""" """
import logging import logging
import math import math
from abc import ABC, abstractmethod from abc import ABC
from typing import Dict, Any, Callable, List from typing import Dict, Any, Callable, List
from pandas import DataFrame from skopt.space import Categorical, Dimension, Integer, Real
from skopt.space import Dimension, Integer, Real
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
@ -28,8 +27,8 @@ def _format_exception_message(method: str, space: str) -> str:
class IHyperOpt(ABC): class IHyperOpt(ABC):
""" """
Interface for freqtrade hyperopts Interface for freqtrade hyperopt
Defines the mandatory structure must follow any custom hyperopts Defines the mandatory structure must follow any custom hyperopt
Class attributes you can use: Class attributes you can use:
ticker_interval -> int: value of the ticker interval to use for the strategy ticker_interval -> int: value of the ticker interval to use for the strategy
@ -42,15 +41,6 @@ class IHyperOpt(ABC):
# Assign ticker_interval to be used in hyperopt # Assign ticker_interval to be used in hyperopt
IHyperOpt.ticker_interval = str(config['ticker_interval']) IHyperOpt.ticker_interval = str(config['ticker_interval'])
@staticmethod
@abstractmethod
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Populate indicators that will be used in the Buy and Sell strategy.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe().
:return: A Dataframe with all mandatory indicators for the strategies.
"""
@staticmethod @staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable: def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
""" """
@ -116,10 +106,10 @@ class IHyperOpt(ABC):
roi_t_alpha = 1.0 roi_t_alpha = 1.0
roi_p_alpha = 1.0 roi_p_alpha = 1.0
ticker_interval_mins = timeframe_to_minutes(IHyperOpt.ticker_interval) timeframe_mins = timeframe_to_minutes(IHyperOpt.ticker_interval)
# We define here limits for the ROI space parameters automagically adapted to the # We define here limits for the ROI space parameters automagically adapted to the
# ticker_interval used by the bot: # timeframe used by the bot:
# #
# * 'roi_t' (limits for the time intervals in the ROI tables) components # * 'roi_t' (limits for the time intervals in the ROI tables) components
# are scaled linearly. # are scaled linearly.
@ -127,8 +117,8 @@ class IHyperOpt(ABC):
# #
# The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space() # The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space()
# method for the 5m ticker interval. # method for the 5m ticker interval.
roi_t_scale = ticker_interval_mins / 5 roi_t_scale = timeframe_mins / 5
roi_p_scale = math.log1p(ticker_interval_mins) / math.log1p(5) roi_p_scale = math.log1p(timeframe_mins) / math.log1p(5)
roi_limits = { roi_limits = {
'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha), 'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha),
'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha), 'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha),
@ -184,6 +174,27 @@ class IHyperOpt(ABC):
Real(-0.35, -0.02, name='stoploss'), Real(-0.35, -0.02, name='stoploss'),
] ]
@staticmethod
def trailing_space() -> List[Dimension]:
"""
Create a trailing stoploss space.
You may override it in your custom Hyperopt class.
"""
return [
# It was decided to always set trailing_stop is to True if the 'trailing' hyperspace
# is used. Otherwise hyperopt will vary other parameters that won't have effect if
# trailing_stop is set False.
# This parameter is included into the hyperspace dimensions rather than assigning
# it explicitly in the code in order to have it printed in the results along with
# other 'trailing' hyperspace parameters.
Categorical([True], name='trailing_stop'),
Real(0.02, 0.35, name='trailing_stop_positive'),
Real(0.01, 0.1, name='trailing_stop_positive_offset'),
Categorical([True, False], name='trailing_only_offset_is_reached'),
]
# This is needed for proper unpickling the class attribute ticker_interval # This is needed for proper unpickling the class attribute ticker_interval
# which is set to the actual value by the resolver. # which is set to the actual value by the resolver.
# Why do I still need such shamanic mantras in modern python? # Why do I still need such shamanic mantras in modern python?

View File

@ -1,6 +1,6 @@
""" """
IHyperOptLoss interface IHyperOptLoss interface
This module defines the interface for the loss-function for hyperopts This module defines the interface for the loss-function for hyperopt
""" """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@ -11,7 +11,7 @@ from pandas import DataFrame
class IHyperOptLoss(ABC): class IHyperOptLoss(ABC):
""" """
Interface for freqtrade hyperopts Loss functions. Interface for freqtrade hyperopt Loss functions.
Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.) Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.)
""" """
ticker_interval: str ticker_interval: str

View File

@ -5,22 +5,31 @@ Provides lists as configured in config.json
""" """
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod, abstractproperty
from typing import List from copy import deepcopy
from typing import Dict, List
from freqtrade.exchange import market_is_active from freqtrade.exchange import market_is_active
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class IPairList(ABC): class IPairList(ABC):
def __init__(self, freqtrade, config: dict) -> None: def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
self._freqtrade = freqtrade pairlist_pos: int) -> None:
"""
:param exchange: Exchange instance
:param pairlistmanager: Instanciating Pairlist manager
:param config: Global bot configuration
:param pairlistconfig: Configuration for this pairlist - can be empty.
:param pairlist_pos: Position of the filter in the pairlist-filter-list
"""
self._exchange = exchange
self._pairlistmanager = pairlistmanager
self._config = config self._config = config
self._whitelist = self._config['exchange']['pair_whitelist'] self._pairlistconfig = pairlistconfig
self._blacklist = self._config['exchange'].get('pair_blacklist', []) self._pairlist_pos = pairlist_pos
@property @property
def name(self) -> str: def name(self) -> str:
@ -30,21 +39,13 @@ class IPairList(ABC):
""" """
return self.__class__.__name__ return self.__class__.__name__
@property @abstractproperty
def whitelist(self) -> List[str]: def needstickers(self) -> bool:
""" """
Has the current whitelist Boolean property defining if tickers are necessary.
-> no need to overwrite in subclasses If no Pairlist requries tickers, an empty List is passed
as tickers argument to filter_pairlist
""" """
return self._whitelist
@property
def blacklist(self) -> List[str]:
"""
Has the current blacklist
-> no need to overwrite in subclasses
"""
return self._blacklist
@abstractmethod @abstractmethod
def short_desc(self) -> str: def short_desc(self) -> str:
@ -54,36 +55,62 @@ class IPairList(ABC):
""" """
@abstractmethod @abstractmethod
def refresh_pairlist(self) -> None: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
""" """
Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary
-> Please overwrite in subclasses -> Please overwrite in subclasses
:param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist
""" """
def _validate_whitelist(self, whitelist: List[str]) -> List[str]: @staticmethod
def verify_blacklist(pairlist: List[str], blacklist: List[str]) -> List[str]:
"""
Verify and remove items from pairlist - returning a filtered pairlist.
"""
for pair in deepcopy(pairlist):
if pair in blacklist:
logger.warning(f"Pair {pair} in your blacklist. Removing it from whitelist...")
pairlist.remove(pair)
return pairlist
def _verify_blacklist(self, pairlist: List[str]) -> List[str]:
"""
Proxy method to verify_blacklist for easy access for child classes.
"""
return IPairList.verify_blacklist(pairlist, self._pairlistmanager.blacklist)
def _whitelist_for_active_markets(self, pairlist: List[str]) -> List[str]:
""" """
Check available markets and remove pair from whitelist if necessary Check available markets and remove pair from whitelist if necessary
:param whitelist: the sorted list of pairs the user might want to trade :param whitelist: the sorted list of pairs the user might want to trade
:return: the list of pairs the user wants to trade without those unavailable or :return: the list of pairs the user wants to trade without those unavailable or
black_listed black_listed
""" """
markets = self._freqtrade.exchange.markets markets = self._exchange.markets
sanitized_whitelist = set() sanitized_whitelist: List[str] = []
for pair in whitelist: for pair in pairlist:
# pair is not in the generated dynamic market, or in the blacklist ... ignore it # pair is not in the generated dynamic market or has the wrong stake currency
if (pair in self.blacklist or pair not in markets if pair not in markets:
or not pair.endswith(self._config['stake_currency'])):
logger.warning(f"Pair {pair} is not compatible with exchange " logger.warning(f"Pair {pair} is not compatible with exchange "
f"{self._freqtrade.exchange.name} or contained in " f"{self._exchange.name}. Removing it from whitelist..")
f"your blacklist. Removing it from whitelist..")
continue continue
if not pair.endswith(self._config['stake_currency']):
logger.warning(f"Pair {pair} is not compatible with your stake currency "
f"{self._config['stake_currency']}. Removing it from whitelist..")
continue
# Check if market is active # Check if market is active
market = markets[pair] market = markets[pair]
if not market_is_active(market): if not market_is_active(market):
logger.info(f"Ignoring {pair} from whitelist. Market is not active.") logger.info(f"Ignoring {pair} from whitelist. Market is not active.")
continue continue
sanitized_whitelist.add(pair) if pair not in sanitized_whitelist:
sanitized_whitelist.append(pair)
sanitized_whitelist = self._verify_blacklist(sanitized_whitelist)
# We need to remove pairs that are unknown # We need to remove pairs that are unknown
return list(sanitized_whitelist) return sanitized_whitelist

View File

@ -0,0 +1,63 @@
import logging
from copy import deepcopy
from typing import Dict, List
from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__)
class PrecisionFilter(IPairList):
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requries tickers, an empty List is passed
as tickers argument to filter_pairlist
"""
return True
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
"""
return f"{self.name} - Filtering untradable pairs."
def _validate_precision_filter(self, ticker: dict, stoploss: float) -> bool:
"""
Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very
low value pairs.
:param ticker: ticker dict as returned from ccxt.load_markets()
:param stoploss: stoploss value as set in the configuration
(already cleaned to be 1 - stoploss)
:return: True if the pair can stay, false if it should be removed
"""
stop_price = ticker['ask'] * stoploss
# Adjust stop-prices to precision
sp = self._exchange.symbol_price_prec(ticker["symbol"], stop_price)
stop_gap_price = self._exchange.symbol_price_prec(ticker["symbol"], stop_price * 0.99)
logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}")
if sp <= stop_gap_price:
logger.info(f"Removed {ticker['symbol']} from whitelist, "
f"because stop price {sp} would be <= stop limit {stop_gap_price}")
return False
return True
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
"""
Filters and sorts pairlists and assigns and returns them again.
"""
stoploss = None
if self._config.get('stoploss') is not None:
# Precalculate sanitized stoploss value to avoid recalculation for every pair
stoploss = 1 - abs(self._config.get('stoploss'))
# Copy list since we're modifying this list
for p in deepcopy(pairlist):
ticker = tickers.get(p)
# Filter out assets which would not allow setting a stoploss
if not ticker or (stoploss and not self._validate_precision_filter(ticker, stoploss)):
pairlist.remove(p)
continue
return pairlist

View File

@ -0,0 +1,69 @@
import logging
from copy import deepcopy
from typing import Dict, List
from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__)
class PriceFilter(IPairList):
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
pairlist_pos: int) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0)
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requries tickers, an empty List is passed
as tickers argument to filter_pairlist
"""
return True
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
"""
return f"{self.name} - Filtering pairs priced below {self._low_price_ratio * 100}%."
def _validate_ticker_lowprice(self, ticker) -> bool:
"""
Check if if one price-step (pip) is > than a certain barrier.
:param ticker: ticker dict as returned from ccxt.load_markets()
:param precision: Precision
:return: True if the pair can stay, false if it should be removed
"""
precision = self._exchange.markets[ticker['symbol']]['precision']['price']
compare = ticker['last'] + 1 / pow(10, precision)
changeperc = (compare - ticker['last']) / ticker['last']
if changeperc > self._low_price_ratio:
logger.info(f"Removed {ticker['symbol']} from whitelist, "
f"because 1 unit is {changeperc * 100:.3f}%")
return False
return True
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
"""
Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary
:param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist
"""
# Copy list since we're modifying this list
for p in deepcopy(pairlist):
ticker = tickers.get(p)
if not ticker:
pairlist.remove(p)
# Filter out assets which would not allow setting a stoploss
if self._low_price_ratio and not self._validate_ticker_lowprice(ticker):
pairlist.remove(p)
return pairlist

View File

@ -5,6 +5,7 @@ Provides lists as configured in config.json
""" """
import logging import logging
from typing import Dict, List
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
@ -13,18 +14,28 @@ logger = logging.getLogger(__name__)
class StaticPairList(IPairList): class StaticPairList(IPairList):
def __init__(self, freqtrade, config: dict) -> None: @property
super().__init__(freqtrade, config) def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requries tickers, an empty List is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str: def short_desc(self) -> str:
""" """
Short whitelist method description - used for startup-messages Short whitelist method description - used for startup-messages
-> Please overwrite in subclasses -> Please overwrite in subclasses
""" """
return f"{self.name}: {self.whitelist}" return f"{self.name}"
def refresh_pairlist(self) -> None: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
""" """
Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary
:param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist
""" """
self._whitelist = self._validate_whitelist(self._config['exchange']['pair_whitelist']) return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist'])

View File

@ -5,11 +5,12 @@ Provides lists as configured in config.json
""" """
import logging import logging
from typing import List from datetime import datetime
from cachetools import TTLCache, cached from typing import Dict, List
from freqtrade.pairlist.IPairList import IPairList
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume'] SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume']
@ -17,18 +18,19 @@ SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume']
class VolumePairList(IPairList): class VolumePairList(IPairList):
def __init__(self, freqtrade, config: dict) -> None: def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
super().__init__(freqtrade, config) pairlist_pos: int) -> None:
self._whitelistconf = self._config.get('pairlist', {}).get('config') super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if 'number_assets' not in self._whitelistconf:
if 'number_assets' not in self._pairlistconfig:
raise OperationalException( raise OperationalException(
f'`number_assets` not specified. Please check your configuration ' f'`number_assets` not specified. Please check your configuration '
'for "pairlist.config.number_assets"') 'for "pairlist.config.number_assets"')
self._number_pairs = self._whitelistconf['number_assets'] self._number_pairs = self._pairlistconfig['number_assets']
self._sort_key = self._whitelistconf.get('sort_key', 'quoteVolume') self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume')
self._precision_filter = self._whitelistconf.get('precision_filter', False) self.refresh_period = self._pairlistconfig.get('refresh_period', 1800)
if not self._freqtrade.exchange.exchange_has('fetchTickers'): if not self._exchange.exchange_has('fetchTickers'):
raise OperationalException( raise OperationalException(
'Exchange does not support dynamic whitelist.' 'Exchange does not support dynamic whitelist.'
'Please edit your config and restart the bot' 'Please edit your config and restart the bot'
@ -36,6 +38,16 @@ class VolumePairList(IPairList):
if not self._validate_keys(self._sort_key): if not self._validate_keys(self._sort_key):
raise OperationalException( raise OperationalException(
f'key {self._sort_key} not in {SORT_VALUES}') f'key {self._sort_key} not in {SORT_VALUES}')
self._last_refresh = 0
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requries tickers, an empty List is passed
as tickers argument to filter_pairlist
"""
return True
def _validate_keys(self, key): def _validate_keys(self, key):
return key in SORT_VALUES return key in SORT_VALUES
@ -43,54 +55,54 @@ class VolumePairList(IPairList):
def short_desc(self) -> str: def short_desc(self) -> str:
""" """
Short whitelist method description - used for startup-messages Short whitelist method description - used for startup-messages
-> Please overwrite in subclasses
""" """
return f"{self.name} - top {self._whitelistconf['number_assets']} volume pairs." return f"{self.name} - top {self._pairlistconfig['number_assets']} volume pairs."
def refresh_pairlist(self) -> None: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
""" """
Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively Filters and sorts pairlist and returns the whitelist again.
-> Please overwrite in subclasses Called on each bot iteration - please use internal caching if necessary
:param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist
""" """
# Generate dynamic whitelist # Generate dynamic whitelist
self._whitelist = self._gen_pair_whitelist( if self._last_refresh + self.refresh_period < datetime.now().timestamp():
self._config['stake_currency'], self._sort_key)[:self._number_pairs] self._last_refresh = int(datetime.now().timestamp())
return self._gen_pair_whitelist(pairlist,
tickers,
self._config['stake_currency'],
self._sort_key,
)
else:
return pairlist
@cached(TTLCache(maxsize=1, ttl=1800)) def _gen_pair_whitelist(self, pairlist, tickers, base_currency: str, key: str) -> List[str]:
def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]:
""" """
Updates the whitelist with with a dynamically generated list Updates the whitelist with with a dynamically generated list
:param base_currency: base currency as str :param base_currency: base currency as str
:param key: sort key (defaults to 'quoteVolume') :param key: sort key (defaults to 'quoteVolume')
:param tickers: Tickers (from exchange.get_tickers()).
:return: List of pairs :return: List of pairs
""" """
tickers = self._freqtrade.exchange.get_tickers() if self._pairlist_pos == 0:
# If VolumePairList is the first in the list, use fresh pairlist
# check length so that we make sure that '/' is actually in the string # check length so that we make sure that '/' is actually in the string
tickers = [v for k, v in tickers.items() filtered_tickers = [v for k, v in tickers.items()
if (len(k.split('/')) == 2 and k.split('/')[1] == base_currency if (len(k.split('/')) == 2 and k.split('/')[1] == base_currency
and v[key] is not None)] and v[key] is not None)]
sorted_tickers = sorted(tickers, reverse=True, key=lambda t: t[key]) else:
# If other pairlist is in front, use the incomming pairlist.
filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[key])
# Validate whitelist to only have active market pairs # Validate whitelist to only have active market pairs
valid_pairs = self._validate_whitelist([s['symbol'] for s in sorted_tickers]) pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers])
valid_tickers = [t for t in sorted_tickers if t["symbol"] in valid_pairs] pairs = self._verify_blacklist(pairs)
# Limit to X number of pairs
if self._freqtrade.strategy.stoploss is not None and self._precision_filter: pairs = pairs[:self._number_pairs]
logger.info(f"Searching {self._number_pairs} pairs: {pairs}")
stop_prices = [self._freqtrade.get_target_bid(t["symbol"], t)
* (1 - abs(self._freqtrade.strategy.stoploss)) for t in valid_tickers]
rates = [sp * 0.99 for sp in stop_prices]
logger.debug("\n".join([f"{sp} : {r}" for sp, r in zip(stop_prices[:10], rates[:10])]))
for i, t in enumerate(valid_tickers):
sp = self._freqtrade.exchange.symbol_price_prec(t["symbol"], stop_prices[i])
r = self._freqtrade.exchange.symbol_price_prec(t["symbol"], rates[i])
logger.debug(f"{t['symbol']} - {sp} : {r}")
if sp <= r:
logger.info(f"Removed {t['symbol']} from whitelist, "
f"because stop price {sp} would be <= stop limit {r}")
valid_tickers.remove(t)
pairs = [s['symbol'] for s in valid_tickers]
logger.info(f"Searching pairs: {self._whitelist}")
return pairs return pairs

View File

@ -0,0 +1,95 @@
"""
Static List provider
Provides lists as configured in config.json
"""
from cachetools import TTLCache, cached
import logging
from typing import Dict, List
from freqtrade import OperationalException
from freqtrade.pairlist.IPairList import IPairList
from freqtrade.resolvers import PairListResolver
logger = logging.getLogger(__name__)
class PairListManager():
def __init__(self, exchange, config: dict) -> None:
self._exchange = exchange
self._config = config
self._whitelist = self._config['exchange'].get('pair_whitelist')
self._blacklist = self._config['exchange'].get('pair_blacklist', [])
self._pairlists: List[IPairList] = []
self._tickers_needed = False
for pl in self._config.get('pairlists', None):
if 'method' not in pl:
logger.warning(f"No method in {pl}")
continue
pairl = PairListResolver(pl.get('method'),
exchange=exchange,
pairlistmanager=self,
config=config,
pairlistconfig=pl,
pairlist_pos=len(self._pairlists)
).pairlist
self._tickers_needed = pairl.needstickers or self._tickers_needed
self._pairlists.append(pairl)
if not self._pairlists:
raise OperationalException("No Pairlist defined!")
@property
def whitelist(self) -> List[str]:
"""
Has the current whitelist
"""
return self._whitelist
@property
def blacklist(self) -> List[str]:
"""
Has the current blacklist
-> no need to overwrite in subclasses
"""
return self._blacklist
@property
def name_list(self) -> List[str]:
"""
Get list of loaded pairlists names
"""
return [p.name for p in self._pairlists]
def short_desc(self) -> List[Dict]:
"""
List of short_desc for each pairlist
"""
return [{p.name: p.short_desc()} for p in self._pairlists]
@cached(TTLCache(maxsize=1, ttl=1800))
def _get_cached_tickers(self):
return self._exchange.get_tickers()
def refresh_pairlist(self) -> None:
"""
Run pairlist through all configured pairlists.
"""
pairlist = self._whitelist.copy()
# tickers should be cached to avoid calling the exchange on each call.
tickers: Dict = {}
if self._tickers_needed:
tickers = self._get_cached_tickers()
# Process all pairlists in chain
for pl in self._pairlists:
pairlist = pl.filter_pairlist(pairlist, tickers)
# Validation against blacklist happens after the pairlists to ensure blacklist is respected.
pairlist = IPairList.verify_blacklist(pairlist, self.blacklist)
self._whitelist = pairlist

View File

@ -8,17 +8,16 @@ from typing import Any, Dict, List, Optional
import arrow import arrow
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
create_engine, inspect) create_engine, desc, func, inspect)
from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.exc import NoSuchModuleError
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Query
from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker from sqlalchemy.orm.session import sessionmaker
from sqlalchemy import func
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from freqtrade import OperationalException from freqtrade import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -52,9 +51,11 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
raise OperationalException(f"Given value for db_url: '{db_url}' " raise OperationalException(f"Given value for db_url: '{db_url}' "
f"is no valid database URL! (See {_SQL_DOCS_URL})") f"is no valid database URL! (See {_SQL_DOCS_URL})")
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope
Trade.session = session() # Scoped sessions proxy requests to the appropriate thread-local session.
Trade.query = session.query_property() # We should use the scoped_session object - not a seperately initialized version
Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Trade.query = Trade.session.query_property()
_DECL_BASE.metadata.create_all(engine) _DECL_BASE.metadata.create_all(engine)
check_migrate(engine) check_migrate(engine)
@ -393,6 +394,37 @@ class Trade(_DECL_BASE):
profit_percent = (close_trade_price / open_trade_price) - 1 profit_percent = (close_trade_price / open_trade_price) - 1
return float(f"{profit_percent:.8f}") return float(f"{profit_percent:.8f}")
@staticmethod
def get_trades(trade_filter=None) -> Query:
"""
Helper function to query Trades using filters.
:param trade_filter: Optional filter to apply to trades
Can be either a Filter object, or a List of filters
e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])`
e.g. `(trade_filter=Trade.id == trade_id)`
:return: unsorted query object
"""
if trade_filter is not None:
if not isinstance(trade_filter, list):
trade_filter = [trade_filter]
return Trade.query.filter(*trade_filter)
else:
return Trade.query
@staticmethod
def get_open_trades() -> List[Any]:
"""
Query trades from persistence layer
"""
return Trade.get_trades(Trade.is_open.is_(True)).all()
@staticmethod
def get_open_order_trades():
"""
Returns all open trades
"""
return Trade.get_trades(Trade.open_order_id.isnot(None)).all()
@staticmethod @staticmethod
def total_open_trades_stakes() -> float: def total_open_trades_stakes() -> float:
""" """
@ -405,11 +437,38 @@ class Trade(_DECL_BASE):
return total_open_stake_amount or 0 return total_open_stake_amount or 0
@staticmethod @staticmethod
def get_open_trades() -> List[Any]: def get_overall_performance() -> List[Dict[str, Any]]:
""" """
Query trades from persistence layer Returns List of dicts containing all Trades, including profit and trade count
""" """
return Trade.query.filter(Trade.is_open.is_(True)).all() pair_rates = Trade.session.query(
Trade.pair,
func.sum(Trade.close_profit).label('profit_sum'),
func.count(Trade.pair).label('count')
).filter(Trade.is_open.is_(False))\
.group_by(Trade.pair) \
.order_by(desc('profit_sum')) \
.all()
return [
{
'pair': pair,
'profit': rate,
'count': count
}
for pair, rate, count in pair_rates
]
@staticmethod
def get_best_pair():
"""
Get best pair with closed trade.
"""
best_pair = Trade.session.query(
Trade.pair, func.sum(Trade.close_profit).label('profit_sum')
).filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(desc('profit_sum')).first()
return best_pair
@staticmethod @staticmethod
def stoploss_reinitialization(desired_stoploss): def stoploss_reinitialization(desired_stoploss):

View File

@ -39,7 +39,7 @@ def init_plotscript(config):
tickers = history.load_data( tickers = history.load_data(
datadir=Path(str(config.get("datadir"))), datadir=Path(str(config.get("datadir"))),
pairs=pairs, pairs=pairs,
ticker_interval=config.get('ticker_interval', '5m'), timeframe=config.get('ticker_interval', '5m'),
timerange=timerange, timerange=timerange,
) )
@ -47,7 +47,7 @@ def init_plotscript(config):
db_url=config.get('db_url'), db_url=config.get('db_url'),
exportfilename=config.get('exportfilename'), exportfilename=config.get('exportfilename'),
) )
trades = history.trim_dataframe(trades, timerange, 'open_time')
return {"tickers": tickers, return {"tickers": tickers,
"trades": trades, "trades": trades,
"pairs": pairs, "pairs": pairs,
@ -300,12 +300,12 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame],
return fig return fig
def generate_plot_filename(pair, ticker_interval) -> str: def generate_plot_filename(pair, timeframe) -> str:
""" """
Generate filenames per pair/ticker_interval to be used for storing plots Generate filenames per pair/timeframe to be used for storing plots
""" """
pair_name = pair.replace("/", "_") pair_name = pair.replace("/", "_")
file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' file_name = 'freqtrade-plot-' + pair_name + '-' + timeframe + '.html'
logger.info('Generate plot file for %s', pair) logger.info('Generate plot file for %s', pair)
@ -316,8 +316,9 @@ def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False
""" """
Generate a plot html file from pre populated fig plotly object Generate a plot html file from pre populated fig plotly object
:param fig: Plotly Figure to plot :param fig: Plotly Figure to plot
:param pair: Pair to plot (used as filename and Plot title) :param filename: Name to store the file as
:param ticker_interval: Used as part of the filename :param directory: Directory to store the file in
:param auto_open: Automatically open files saved
:return: None :return: None
""" """
directory.mkdir(parents=True, exist_ok=True) directory.mkdir(parents=True, exist_ok=True)
@ -376,12 +377,14 @@ def plot_profit(config: Dict[str, Any]) -> None:
in helping out to find a good algorithm. in helping out to find a good algorithm.
""" """
plot_elements = init_plotscript(config) plot_elements = init_plotscript(config)
trades = load_trades(config['trade_source'], trades = plot_elements['trades']
db_url=str(config.get('db_url')),
exportfilename=str(config.get('exportfilename')),
)
# Filter trades to relevant pairs # Filter trades to relevant pairs
trades = trades[trades['pair'].isin(plot_elements["pairs"])] # Remove open pairs - we don't know the profit yet so can't calculate profit for these.
# Also, If only one open pair is left, then the profit-generation would fail.
trades = trades[(trades['pair'].isin(plot_elements["pairs"]))
& (~trades['close_time'].isnull())
]
# Create an average close price of all the pairs that were involved. # Create an average close price of all the pairs that were involved.
# this could be useful to gauge the overall market trend # this could be useful to gauge the overall market trend
fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"],

View File

@ -1,14 +1,14 @@
# pragma pylint: disable=attribute-defined-outside-init # pragma pylint: disable=attribute-defined-outside-init
""" """
This module load custom hyperopts This module load custom hyperopt
""" """
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional, Dict from typing import Optional, Dict
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.constants import DEFAULT_HYPEROPT, DEFAULT_HYPEROPT_LOSS from freqtrade.constants import DEFAULT_HYPEROPT_LOSS, USERPATH_HYPEROPTS
from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_interface import IHyperOpt
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
@ -20,7 +20,6 @@ class HyperOptResolver(IResolver):
""" """
This class contains all the logic to load custom hyperopt class This class contains all the logic to load custom hyperopt class
""" """
__slots__ = ['hyperopt'] __slots__ = ['hyperopt']
def __init__(self, config: Dict) -> None: def __init__(self, config: Dict) -> None:
@ -28,12 +27,18 @@ class HyperOptResolver(IResolver):
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary :param config: configuration dictionary
""" """
if not config.get('hyperopt'):
raise OperationalException("No Hyperopt set. Please use `--hyperopt` to specify "
"the Hyperopt class to use.")
hyperopt_name = config['hyperopt']
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt
hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT
self.hyperopt = self._load_hyperopt(hyperopt_name, config, self.hyperopt = self._load_hyperopt(hyperopt_name, config,
extra_dir=config.get('hyperopt_path')) extra_dir=config.get('hyperopt_path'))
if not hasattr(self.hyperopt, 'populate_indicators'):
logger.warning("Hyperopt class does not provide populate_indicators() method. "
"Using populate_indicators from the strategy.")
if not hasattr(self.hyperopt, 'populate_buy_trend'): if not hasattr(self.hyperopt, 'populate_buy_trend'):
logger.warning("Hyperopt class does not provide populate_buy_trend() method. " logger.warning("Hyperopt class does not provide populate_buy_trend() method. "
"Using populate_buy_trend from the strategy.") "Using populate_buy_trend from the strategy.")
@ -53,7 +58,7 @@ class HyperOptResolver(IResolver):
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = self.build_search_paths(config, current_path=current_path, abs_paths = self.build_search_paths(config, current_path=current_path,
user_subdir='hyperopts', extra_dir=extra_dir) user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir)
hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt, hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt,
object_name=hyperopt_name, kwargs={'config': config}) object_name=hyperopt_name, kwargs={'config': config})
@ -69,27 +74,28 @@ class HyperOptLossResolver(IResolver):
""" """
This class contains all the logic to load custom hyperopt loss class This class contains all the logic to load custom hyperopt loss class
""" """
__slots__ = ['hyperoptloss'] __slots__ = ['hyperoptloss']
def __init__(self, config: Dict = None) -> None: def __init__(self, config: Dict) -> None:
""" """
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary or None :param config: configuration dictionary
""" """
config = config or {}
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt # Verify the hyperopt_loss is in the configuration, otherwise fallback to the
hyperopt_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS # default hyperopt loss
hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS
self.hyperoptloss = self._load_hyperoptloss( self.hyperoptloss = self._load_hyperoptloss(
hyperopt_name, config, extra_dir=config.get('hyperopt_path')) hyperoptloss_name, config, extra_dir=config.get('hyperopt_path'))
# Assign ticker_interval to be used in hyperopt # Assign ticker_interval to be used in hyperopt
self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval']) self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'): if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'):
raise OperationalException( raise OperationalException(
f"Found hyperopt {hyperopt_name} does not implement `hyperopt_loss_function`.") f"Found HyperoptLoss class {hyperoptloss_name} does not "
"implement `hyperopt_loss_function`.")
def _load_hyperoptloss( def _load_hyperoptloss(
self, hyper_loss_name: str, config: Dict, self, hyper_loss_name: str, config: Dict,
@ -104,7 +110,7 @@ class HyperOptLossResolver(IResolver):
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = self.build_search_paths(config, current_path=current_path, abs_paths = self.build_search_paths(config, current_path=current_path,
user_subdir='hyperopts', extra_dir=extra_dir) user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir)
hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss, hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss,
object_name=hyper_loss_name) object_name=hyper_loss_name)

View File

@ -17,13 +17,13 @@ class IResolver:
This class contains all the logic to load custom classes This class contains all the logic to load custom classes
""" """
def build_search_paths(self, config, current_path: Path, user_subdir: str, def build_search_paths(self, config, current_path: Path, user_subdir: Optional[str] = None,
extra_dir: Optional[str] = None) -> List[Path]: extra_dir: Optional[str] = None) -> List[Path]:
abs_paths = [ abs_paths: List[Path] = [current_path]
config['user_data_dir'].joinpath(user_subdir),
current_path, if user_subdir:
] abs_paths.insert(0, config['user_data_dir'].joinpath(user_subdir))
if extra_dir: if extra_dir:
# Add extra directory to the top of the search paths # Add extra directory to the top of the search paths

View File

@ -20,13 +20,18 @@ class PairListResolver(IResolver):
__slots__ = ['pairlist'] __slots__ = ['pairlist']
def __init__(self, pairlist_name: str, freqtrade, config: dict) -> None: def __init__(self, pairlist_name: str, exchange, pairlistmanager,
config: dict, pairlistconfig: dict, pairlist_pos: int) -> None:
""" """
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary or None :param config: configuration dictionary or None
""" """
self.pairlist = self._load_pairlist(pairlist_name, config, kwargs={'freqtrade': freqtrade, self.pairlist = self._load_pairlist(pairlist_name, config,
'config': config}) kwargs={'exchange': exchange,
'pairlistmanager': pairlistmanager,
'config': config,
'pairlistconfig': pairlistconfig,
'pairlist_pos': pairlist_pos})
def _load_pairlist( def _load_pairlist(
self, pairlist_name: str, config: dict, kwargs: dict) -> IPairList: self, pairlist_name: str, config: dict, kwargs: dict) -> IPairList:
@ -40,7 +45,7 @@ class PairListResolver(IResolver):
current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
abs_paths = self.build_search_paths(config, current_path=current_path, abs_paths = self.build_search_paths(config, current_path=current_path,
user_subdir='pairlist', extra_dir=None) user_subdir=None, extra_dir=None)
pairlist = self._load_object(paths=abs_paths, object_type=IPairList, pairlist = self._load_object(paths=abs_paths, object_type=IPairList,
object_name=pairlist_name, kwargs=kwargs) object_name=pairlist_name, kwargs=kwargs)

View File

@ -32,8 +32,11 @@ class StrategyResolver(IResolver):
""" """
config = config or {} config = config or {}
# Verify the strategy is in the configuration, otherwise fallback to the default strategy if not config.get('strategy'):
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY raise OperationalException("No strategy set. Please use `--strategy` to specify "
"the strategy class to use.")
strategy_name = config['strategy']
self.strategy: IStrategy = self._load_strategy(strategy_name, self.strategy: IStrategy = self._load_strategy(strategy_name,
config=config, config=config,
extra_dir=config.get('strategy_path')) extra_dir=config.get('strategy_path'))
@ -57,6 +60,7 @@ class StrategyResolver(IResolver):
("order_time_in_force", None, False), ("order_time_in_force", None, False),
("stake_currency", None, False), ("stake_currency", None, False),
("stake_amount", None, False), ("stake_amount", None, False),
("startup_candle_count", None, False),
("use_sell_signal", True, True), ("use_sell_signal", True, True),
("sell_profit_only", False, True), ("sell_profit_only", False, True),
("ignore_roi_if_buy_signal", False, True), ("ignore_roi_if_buy_signal", False, True),
@ -125,7 +129,8 @@ class StrategyResolver(IResolver):
current_path = Path(__file__).parent.parent.joinpath('strategy').resolve() current_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
abs_paths = self.build_search_paths(config, current_path=current_path, abs_paths = self.build_search_paths(config, current_path=current_path,
user_subdir='strategies', extra_dir=extra_dir) user_subdir=constants.USERPATH_STRATEGY,
extra_dir=extra_dir)
if ":" in strategy_name: if ":" in strategy_name:
logger.info("loading base64 encoded strategy") logger.info("loading base64 encoded strategy")

View File

@ -169,6 +169,10 @@ class ApiServer(RPC):
view_func=self._status, methods=['GET']) view_func=self._status, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/version', 'version', self.app.add_url_rule(f'{BASE_URI}/version', 'version',
view_func=self._version, methods=['GET']) view_func=self._version, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/show_config', 'show_config',
view_func=self._show_config, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/ping', 'ping',
view_func=self._ping, methods=['GET'])
# Combined actions and infos # Combined actions and infos
self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist,
@ -224,6 +228,13 @@ class ApiServer(RPC):
msg = self._rpc_stopbuy() msg = self._rpc_stopbuy()
return self.rest_dump(msg) return self.rest_dump(msg)
@rpc_catch_errors
def _ping(self):
"""
simple poing version
"""
return self.rest_dump({"status": "pong"})
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
def _version(self): def _version(self):
@ -232,6 +243,14 @@ class ApiServer(RPC):
""" """
return self.rest_dump({"version": __version__}) return self.rest_dump({"version": __version__})
@require_login
@rpc_catch_errors
def _show_config(self):
"""
Prints the bot's version
"""
return self.rest_dump(self._rpc_show_config())
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
def _reload_conf(self): def _reload_conf(self):
@ -265,7 +284,7 @@ class ApiServer(RPC):
stats = self._rpc_daily_profit(timescale, stats = self._rpc_daily_profit(timescale,
self._config['stake_currency'], self._config['stake_currency'],
self._config['fiat_display_currency'] self._config.get('fiat_display_currency', '')
) )
return self.rest_dump(stats) return self.rest_dump(stats)
@ -293,7 +312,7 @@ class ApiServer(RPC):
logger.info("LocalRPC - Profit Command Called") logger.info("LocalRPC - Profit Command Called")
stats = self._rpc_trade_statistics(self._config['stake_currency'], stats = self._rpc_trade_statistics(self._config['stake_currency'],
self._config['fiat_display_currency'] self._config.get('fiat_display_currency')
) )
return self.rest_dump(stats) return self.rest_dump(stats)
@ -321,8 +340,11 @@ class ApiServer(RPC):
Returns the current status of the trades in json format Returns the current status of the trades in json format
""" """
try:
results = self._rpc_trade_status() results = self._rpc_trade_status()
return self.rest_dump(results) return self.rest_dump(results)
except RPCException:
return self.rest_dump([])
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -332,7 +354,8 @@ class ApiServer(RPC):
Returns the current status of the trades in json format Returns the current status of the trades in json format
""" """
results = self._rpc_balance(self._config.get('fiat_display_currency', '')) results = self._rpc_balance(self._config['stake_currency'],
self._config.get('fiat_display_currency', ''))
return self.rest_dump(results) return self.rest_dump(results)
@require_login @require_login

View File

@ -3,17 +3,15 @@ This module contains class to define a RPC communications
""" """
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from datetime import timedelta, datetime, date from datetime import date, datetime, timedelta
from decimal import Decimal
from enum import Enum from enum import Enum
from typing import Dict, Any, List, Optional from math import isnan
from typing import Any, Dict, List, Optional, Tuple
import arrow import arrow
import sqlalchemy as sql from numpy import NAN, mean
from numpy import mean, NAN
from pandas import DataFrame
from freqtrade import TemporaryError, DependencyException from freqtrade import DependencyException, TemporaryError
from freqtrade.misc import shorten_date from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
@ -82,6 +80,29 @@ class RPC:
def send_msg(self, msg: Dict[str, str]) -> None: def send_msg(self, msg: Dict[str, str]) -> None:
""" Sends a message to all registered rpc modules """ """ Sends a message to all registered rpc modules """
def _rpc_show_config(self) -> Dict[str, Any]:
"""
Return a dict of config options.
Explicitly does NOT return the full config to avoid leakage of sensitive
information via rpc.
"""
config = self._freqtrade.config
val = {
'dry_run': config.get('dry_run', False),
'stake_currency': config['stake_currency'],
'stake_amount': config['stake_amount'],
'minimal_roi': config['minimal_roi'].copy(),
'stoploss': config['stoploss'],
'trailing_stop': config['trailing_stop'],
'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
'ticker_interval': config['ticker_interval'],
'exchange': config['exchange']['name'],
'strategy': config['strategy'],
}
return val
def _rpc_trade_status(self) -> List[Dict[str, Any]]: def _rpc_trade_status(self) -> List[Dict[str, Any]]:
""" """
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
@ -118,7 +139,7 @@ class RPC:
results.append(trade_dict) results.append(trade_dict)
return results return results
def _rpc_status_table(self) -> DataFrame: def _rpc_status_table(self, stake_currency, fiat_display_currency: str) -> Tuple[List, List]:
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
if not trades: if not trades:
raise RPCException('no active order') raise RPCException('no active order')
@ -131,17 +152,28 @@ class RPC:
except DependencyException: except DependencyException:
current_rate = NAN current_rate = NAN
trade_perc = (100 * trade.calc_profit_percent(current_rate)) trade_perc = (100 * trade.calc_profit_percent(current_rate))
trade_profit = trade.calc_profit(current_rate)
profit_str = f'{trade_perc:.2f}%'
if self._fiat_converter:
fiat_profit = self._fiat_converter.convert_amount(
trade_profit,
stake_currency,
fiat_display_currency
)
if fiat_profit and not isnan(fiat_profit):
profit_str += f" ({fiat_profit:.2f})"
trades_list.append([ trades_list.append([
trade.id, trade.id,
trade.pair, trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
f'{trade_perc:.2f}%' profit_str
]) ])
profitcol = "Profit"
if self._fiat_converter:
profitcol += " (" + fiat_display_currency + ")"
columns = ['ID', 'Pair', 'Since', 'Profit'] columns = ['ID', 'Pair', 'Since', profitcol]
df_statuses = DataFrame.from_records(trades_list, columns=columns) return trades_list, columns
df_statuses = df_statuses.set_index(columns[0])
return df_statuses
def _rpc_daily_profit( def _rpc_daily_profit(
self, timescale: int, self, timescale: int,
@ -154,12 +186,11 @@ class RPC:
for day in range(0, timescale): for day in range(0, timescale):
profitday = today - timedelta(days=day) profitday = today - timedelta(days=day)
trades = Trade.query \ trades = Trade.get_trades(trade_filter=[
.filter(Trade.is_open.is_(False)) \ Trade.is_open.is_(False),
.filter(Trade.close_date >= profitday)\ Trade.close_date >= profitday,
.filter(Trade.close_date < (profitday + timedelta(days=1)))\ Trade.close_date < (profitday + timedelta(days=1))
.order_by(Trade.close_date)\ ]).order_by(Trade.close_date).all()
.all()
curdayprofit = sum(trade.calc_profit() for trade in trades) curdayprofit = sum(trade.calc_profit() for trade in trades)
profit_days[profitday] = { profit_days[profitday] = {
'amount': f'{curdayprofit:.8f}', 'amount': f'{curdayprofit:.8f}',
@ -192,7 +223,7 @@ class RPC:
def _rpc_trade_statistics( def _rpc_trade_statistics(
self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
""" Returns cumulative profit statistics """ """ Returns cumulative profit statistics """
trades = Trade.query.order_by(Trade.id).all() trades = Trade.get_trades().order_by(Trade.id).all()
profit_all_coin = [] profit_all_coin = []
profit_all_perc = [] profit_all_perc = []
@ -221,15 +252,11 @@ class RPC:
profit_percent = trade.calc_profit_percent(rate=current_rate) profit_percent = trade.calc_profit_percent(rate=current_rate)
profit_all_coin.append( profit_all_coin.append(
trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)) trade.calc_profit(rate=trade.close_rate or current_rate)
) )
profit_all_perc.append(profit_percent) profit_all_perc.append(profit_percent)
best_pair = Trade.session.query( best_pair = Trade.get_best_pair()
Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum')
).filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(sql.text('profit_sum DESC')).first()
if not best_pair: if not best_pair:
raise RPCException('no closed trade') raise RPCException('no closed trade')
@ -270,34 +297,42 @@ class RPC:
'best_rate': round(bp_rate * 100, 2), 'best_rate': round(bp_rate * 100, 2),
} }
def _rpc_balance(self, fiat_display_currency: str) -> Dict: def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
""" Returns current account balance per crypto """ """ Returns current account balance per crypto """
output = [] output = []
total = 0.0 total = 0.0
for coin, balance in self._freqtrade.exchange.get_balances().items(): try:
if not balance['total']: tickers = self._freqtrade.exchange.get_tickers()
except (TemporaryError, DependencyException):
raise RPCException('Error getting current tickers.')
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
if not balance.total:
continue continue
if coin == 'BTC': est_stake: float = 0
if coin == stake_currency:
rate = 1.0 rate = 1.0
est_stake = balance.total
else: else:
try: try:
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, "BTC") pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
if pair.startswith("BTC"): rate = tickers.get(pair, {}).get('bid', None)
rate = 1.0 / self._freqtrade.get_sell_rate(pair, False) if rate:
else: if pair.startswith(stake_currency):
rate = self._freqtrade.get_sell_rate(pair, False) rate = 1.0 / rate
est_stake = rate * balance.total
except (TemporaryError, DependencyException): except (TemporaryError, DependencyException):
logger.warning(f" Could not get rate for pair {coin}.") logger.warning(f" Could not get rate for pair {coin}.")
continue continue
est_btc: float = rate * balance['total'] total = total + (est_stake or 0)
total = total + est_btc
output.append({ output.append({
'currency': coin, 'currency': coin,
'free': balance['free'] if balance['free'] is not None else 0, 'free': balance.free if balance.free is not None else 0,
'balance': balance['total'] if balance['total'] is not None else 0, 'balance': balance.total if balance.total is not None else 0,
'used': balance['used'] if balance['used'] is not None else 0, 'used': balance.used if balance.used is not None else 0,
'est_btc': est_btc, 'est_stake': est_stake or 0,
'stake': stake_currency,
}) })
if total == 0.0: if total == 0.0:
if self._freqtrade.config.get('dry_run', False): if self._freqtrade.config.get('dry_run', False):
@ -389,11 +424,8 @@ class RPC:
return {'result': 'Created sell orders for all open trades.'} return {'result': 'Created sell orders for all open trades.'}
# Query for trade # Query for trade
trade = Trade.query.filter( trade = Trade.get_trades(
sql.and_( trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
Trade.id == trade_id,
Trade.is_open.is_(True)
)
).first() ).first()
if not trade: if not trade:
logger.warning('forcesell: Invalid argument received') logger.warning('forcesell: Invalid argument received')
@ -423,7 +455,7 @@ class RPC:
# check if valid pair # check if valid pair
# check if pair already has an open pair # check if pair already has an open pair
trade = Trade.query.filter(Trade.is_open.is_(True)).filter(Trade.pair.is_(pair)).first() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first()
if trade: if trade:
raise RPCException(f'position for {pair} already open - id: {trade.id}') raise RPCException(f'position for {pair} already open - id: {trade.id}')
@ -432,28 +464,20 @@ class RPC:
# execute buy # execute buy
if self._freqtrade.execute_buy(pair, stakeamount, price): if self._freqtrade.execute_buy(pair, stakeamount, price):
trade = Trade.query.filter(Trade.is_open.is_(True)).filter(Trade.pair.is_(pair)).first() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first()
return trade return trade
else: else:
return None return None
def _rpc_performance(self) -> List[Dict]: def _rpc_performance(self) -> List[Dict[str, Any]]:
""" """
Handler for performance. Handler for performance.
Shows a performance statistic from finished trades Shows a performance statistic from finished trades
""" """
pair_rates = Trade.get_overall_performance()
pair_rates = Trade.session.query(Trade.pair, # Round and convert to %
sql.func.sum(Trade.close_profit).label('profit_sum'), [x.update({'profit': round(x['profit'] * 100, 2)}) for x in pair_rates]
sql.func.count(Trade.pair).label('count')) \ return pair_rates
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(sql.text('profit_sum DESC')) \
.all()
return [
{'pair': pair, 'profit': round(rate * 100, 2), 'count': count}
for pair, rate, count in pair_rates
]
def _rpc_count(self) -> Dict[str, float]: def _rpc_count(self) -> Dict[str, float]:
""" Returns the number of trades running """ """ Returns the number of trades running """
@ -469,7 +493,7 @@ class RPC:
def _rpc_whitelist(self) -> Dict: def _rpc_whitelist(self) -> Dict:
""" Returns the currently active whitelist""" """ Returns the currently active whitelist"""
res = {'method': self._freqtrade.pairlists.name, res = {'method': self._freqtrade.pairlists.name_list,
'length': len(self._freqtrade.active_pair_whitelist), 'length': len(self._freqtrade.active_pair_whitelist),
'whitelist': self._freqtrade.active_pair_whitelist 'whitelist': self._freqtrade.active_pair_whitelist
} }
@ -484,7 +508,7 @@ class RPC:
and pair not in self._freqtrade.pairlists.blacklist): and pair not in self._freqtrade.pairlists.blacklist):
self._freqtrade.pairlists.blacklist.append(pair) self._freqtrade.pairlists.blacklist.append(pair)
res = {'method': self._freqtrade.pairlists.name, res = {'method': self._freqtrade.pairlists.name_list,
'length': len(self._freqtrade.pairlists.blacklist), 'length': len(self._freqtrade.pairlists.blacklist),
'blacklist': self._freqtrade.pairlists.blacklist, 'blacklist': self._freqtrade.pairlists.blacklist,
} }

View File

@ -95,6 +95,7 @@ class Telegram(RPC):
CommandHandler('daily', self._daily), CommandHandler('daily', self._daily),
CommandHandler('count', self._count), CommandHandler('count', self._count),
CommandHandler('reload_conf', self._reload_conf), CommandHandler('reload_conf', self._reload_conf),
CommandHandler('show_config', self._show_config),
CommandHandler('stopbuy', self._stopbuy), CommandHandler('stopbuy', self._stopbuy),
CommandHandler('whitelist', self._whitelist), CommandHandler('whitelist', self._whitelist),
CommandHandler('blacklist', self._blacklist), CommandHandler('blacklist', self._blacklist),
@ -234,8 +235,9 @@ class Telegram(RPC):
:return: None :return: None
""" """
try: try:
df_statuses = self._rpc_status_table() statlist, head = self._rpc_status_table(self._config['stake_currency'],
message = tabulate(df_statuses, headers='keys', tablefmt='simple') self._config.get('fiat_display_currency', ''))
message = tabulate(statlist, headers=head, tablefmt='simple')
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML) self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e)) self._send_msg(str(e))
@ -323,15 +325,16 @@ class Telegram(RPC):
def _balance(self, update: Update, context: CallbackContext) -> None: def _balance(self, update: Update, context: CallbackContext) -> None:
""" Handler for /balance """ """ Handler for /balance """
try: try:
result = self._rpc_balance(self._config.get('fiat_display_currency', '')) result = self._rpc_balance(self._config['stake_currency'],
self._config.get('fiat_display_currency', ''))
output = '' output = ''
for currency in result['currencies']: for currency in result['currencies']:
if currency['est_btc'] > 0.0001: if currency['est_stake'] > 0.0001:
curr_output = "*{currency}:*\n" \ curr_output = "*{currency}:*\n" \
"\t`Available: {free: .8f}`\n" \ "\t`Available: {free: .8f}`\n" \
"\t`Balance: {balance: .8f}`\n" \ "\t`Balance: {balance: .8f}`\n" \
"\t`Pending: {used: .8f}`\n" \ "\t`Pending: {used: .8f}`\n" \
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency) "\t`Est. {stake}: {est_stake: .8f}`\n".format(**currency)
else: else:
curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency)
@ -549,6 +552,7 @@ class Telegram(RPC):
"*/balance:* `Show account balance per currency`\n" \ "*/balance:* `Show account balance per currency`\n" \
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \ "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \
"*/reload_conf:* `Reload configuration file` \n" \ "*/reload_conf:* `Reload configuration file` \n" \
"*/show_config:* `Show running configuration` \n" \
"*/whitelist:* `Show current whitelist` \n" \ "*/whitelist:* `Show current whitelist` \n" \
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \ "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \
"to the blacklist.` \n" \ "to the blacklist.` \n" \
@ -569,6 +573,26 @@ class Telegram(RPC):
""" """
self._send_msg('*Version:* `{}`'.format(__version__)) self._send_msg('*Version:* `{}`'.format(__version__))
@authorized_only
def _show_config(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /show_config.
Show config information information
:param bot: telegram bot
:param update: message update
:return: None
"""
val = self._rpc_show_config()
self._send_msg(
f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n"
f"*Exchange:* `{val['exchange']}`\n"
f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
f"*Minimum ROI:* `{val['minimal_roi']}`\n"
f"*{'Trailing ' if val['trailing_stop'] else ''}Stoploss:* `{val['stoploss']}`\n"
f"*Ticker Interval:* `{val['ticker_interval']}`\n"
f"*Strategy:* `{val['strategy']}`'"
)
def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
""" """
Send given markdown message Send given markdown message

View File

@ -25,5 +25,12 @@ class RunMode(Enum):
BACKTEST = "backtest" BACKTEST = "backtest"
EDGE = "edge" EDGE = "edge"
HYPEROPT = "hyperopt" HYPEROPT = "hyperopt"
UTIL_EXCHANGE = "util_exchange"
UTIL_NO_EXCHANGE = "util_no_exchange"
PLOT = "plot" PLOT = "plot"
OTHER = "other" # Used for plotting scripts and test OTHER = "other"
TRADING_MODES = [RunMode.LIVE, RunMode.DRY_RUN]
OPTIMIZE_MODES = [RunMode.BACKTEST, RunMode.EDGE, RunMode.HYPEROPT]
NON_UTIL_MODES = TRADING_MODES + OPTIMIZE_MODES

View File

@ -39,6 +39,9 @@ class DefaultStrategy(IStrategy):
'stoploss_on_exchange': False 'stoploss_on_exchange': False
} }
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 20
# Optional time in force for orders # Optional time in force for orders
order_time_in_force = { order_time_in_force = {
'buy': 'gtc', 'buy': 'gtc',
@ -105,9 +108,6 @@ class DefaultStrategy(IStrategy):
# EMA - Exponential Moving Average # EMA - Exponential Moving Average
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
# SMA - Simple Moving Average
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
return dataframe return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

View File

@ -103,11 +103,14 @@ class IStrategy(ABC):
# run "populate_indicators" only for new candle # run "populate_indicators" only for new candle
process_only_new_candles: bool = False process_only_new_candles: bool = False
# Count of candles the strategy requires before producing valid signals
startup_candle_count: int = 0
# Class level variables (intentional) containing # Class level variables (intentional) containing
# the dataprovider (dp) (access to other candles, historic data, ...) # the dataprovider (dp) (access to other candles, historic data, ...)
# and wallets - access to the current balance. # and wallets - access to the current balance.
dp: DataProvider dp: Optional[DataProvider] = None
wallets: Wallets wallets: Optional[Wallets] = None
def __init__(self, config: dict) -> None: def __init__(self, config: dict) -> None:
self.config = config self.config = config
@ -421,6 +424,7 @@ class IStrategy(ABC):
def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
""" """
Creates a dataframe and populates indicators for given ticker data Creates a dataframe and populates indicators for given ticker data
Used by optimize operations only, not during dry / live runs.
""" """
return {pair: self.advise_indicators(pair_data, {'pair': pair}) return {pair: self.advise_indicators(pair_data, {'pair': pair})
for pair, pair_data in tickerdata.items()} for pair, pair_data in tickerdata.items()}

View File

@ -0,0 +1,127 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# --- Do not remove these libs ---
from functools import reduce
from typing import Any, Callable, Dict, List
import numpy as np # noqa
import pandas as pd # noqa
from pandas import DataFrame
from skopt.space import Categorical, Dimension, Integer, Real # noqa
from freqtrade.optimize.hyperopt_interface import IHyperOpt
# --------------------------------
# Add your lib to import here
import talib.abstract as ta # noqa
import freqtrade.vendor.qtpylib.indicators as qtpylib
class {{ hyperopt }}(IHyperOpt):
"""
This is a Hyperopt template to get you started.
More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md
You should:
- Add any lib you need to build your hyperopt.
You must keep:
- The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator.
The roi_space, generate_roi_table, stoploss_space methods are no longer required to be
copied in every custom hyperopt. However, you may override them if you need the
'roi' and the 'stoploss' spaces that differ from the defaults offered by Freqtrade.
Sample implementation of these methods can be found in
https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py
"""
@staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the buy strategy parameters to be used by Hyperopt.
"""
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Buy strategy Hyperopt will build and use.
"""
conditions = []
# GUARDS AND TRENDS
{{ buy_guards | indent(12) }}
# TRIGGERS
if 'trigger' in params:
if params['trigger'] == 'bb_lower':
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if params['trigger'] == 'macd_cross_signal':
conditions.append(qtpylib.crossed_above(
dataframe['macd'], dataframe['macdsignal']
))
if params['trigger'] == 'sar_reversal':
conditions.append(qtpylib.crossed_above(
dataframe['close'], dataframe['sar']
))
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'buy'] = 1
return dataframe
return populate_buy_trend
@staticmethod
def indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching buy strategy parameters.
"""
return [
{{ buy_space | indent(12) }}
]
@staticmethod
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the sell strategy parameters to be used by Hyperopt.
"""
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Sell strategy Hyperopt will build and use.
"""
conditions = []
# GUARDS AND TRENDS
{{ sell_guards | indent(12) }}
# TRIGGERS
if 'sell-trigger' in params:
if params['sell-trigger'] == 'sell-bb_upper':
conditions.append(dataframe['close'] > dataframe['bb_upperband'])
if params['sell-trigger'] == 'sell-macd_cross_signal':
conditions.append(qtpylib.crossed_above(
dataframe['macdsignal'], dataframe['macd']
))
if params['sell-trigger'] == 'sell-sar_reversal':
conditions.append(qtpylib.crossed_above(
dataframe['sar'], dataframe['close']
))
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'sell'] = 1
return dataframe
return populate_sell_trend
@staticmethod
def sell_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching sell strategy parameters.
"""
return [
{{ sell_space | indent(12) }}
]

View File

@ -0,0 +1,138 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# --- Do not remove these libs ---
import numpy as np # noqa
import pandas as pd # noqa
from pandas import DataFrame
from freqtrade.strategy.interface import IStrategy
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
class {{ strategy }}(IStrategy):
"""
This is a strategy template to get you started.
More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md
You can:
:return: a Dataframe with all mandatory indicators for the strategies
- Rename the class name (Do not forget to update class_name)
- Add any methods you want to build your strategy
- Add any lib you need to build your strategy
You must keep:
- the lib in the section "Do not remove these libs"
- the prototype for the methods: minimal_roi, stoploss, populate_indicators, populate_buy_trend,
populate_sell_trend, hyperopt_space, buy_strategy_generator
"""
# Strategy interface version - allow new iterations of the strategy interface.
# Check the documentation or the Sample strategy to get the latest version.
INTERFACE_VERSION = 2
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi".
minimal_roi = {
"60": 0.01,
"30": 0.02,
"0": 0.04
}
# Optimal stoploss designed for the strategy.
# This attribute will be overridden if the config file contains "stoploss".
stoploss = -0.10
# Trailing stoploss
trailing_stop = False
# trailing_stop_positive = 0.01
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
# Optimal ticker interval for the strategy.
ticker_interval = '5m'
# Run "populate_indicators()" only for new candle.
process_only_new_candles = False
# These values can be overridden in the "ask_strategy" section in the config.
use_sell_signal = True
sell_profit_only = False
ignore_roi_if_buy_signal = False
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 20
# Optional order type mapping.
order_types = {
'buy': 'limit',
'sell': 'limit',
'stoploss': 'market',
'stoploss_on_exchange': False
}
# Optional order time in force.
order_time_in_force = {
'buy': 'gtc',
'sell': 'gtc'
}
def informative_pairs(self):
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
These pair/interval combinations are non-tradeable, unless they are part
of the whitelist as well.
For more information, please consult the documentation
:return: List of tuples in the format (pair, interval)
Sample: return [("ETH/USDT", "5m"),
("BTC/USDT", "15m"),
]
"""
return []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
"""
{{ indicators | indent(8) }}
return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column
"""
dataframe.loc[
(
{{ buy_trend | indent(16) }}
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column
"""
dataframe.loc[
(
{{ sell_trend | indent(16) }}
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'sell'] = 1
return dataframe

View File

@ -1,19 +1,23 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# --- Do not remove these libs ---
from functools import reduce from functools import reduce
from typing import Any, Callable, Dict, List from typing import Any, Callable, Dict, List
from datetime import datetime
import numpy as np import numpy as np # noqa
import talib.abstract as ta import pandas as pd # noqa
from pandas import DataFrame from pandas import DataFrame
from skopt.space import Categorical, Dimension, Integer, Real from skopt.space import Categorical, Dimension, Integer, Real # noqa
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_interface import IHyperOpt
# --------------------------------
# Add your lib to import here
import talib.abstract as ta # noqa
import freqtrade.vendor.qtpylib.indicators as qtpylib
class SampleHyperOpts(IHyperOpt):
class SampleHyperOpt(IHyperOpt):
""" """
This is a sample Hyperopt to inspire you. This is a sample Hyperopt to inspire you.
Feel free to customize it. Feel free to customize it.
@ -34,34 +38,6 @@ class SampleHyperOpts(IHyperOpt):
Sample implementation of these methods can be found in Sample implementation of these methods can be found in
https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py
""" """
@staticmethod
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Add several indicators needed for buy and sell strategies defined below.
"""
# ADX
dataframe['adx'] = ta.ADX(dataframe)
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
# MFI
dataframe['mfi'] = ta.MFI(dataframe)
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
# Stochastic Fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
# Minus-DI
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_upperband'] = bollinger['upper']
# SAR
dataframe['sar'] = ta.SAR(dataframe)
return dataframe
@staticmethod @staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable: def buy_strategy_generator(params: Dict[str, Any]) -> Callable:

View File

@ -1,20 +1,23 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# --- Do not remove these libs ---
from functools import reduce from functools import reduce
from math import exp
from typing import Any, Callable, Dict, List from typing import Any, Callable, Dict, List
from datetime import datetime
import numpy as np# noqa F401 import numpy as np # noqa
import talib.abstract as ta import pandas as pd # noqa
from pandas import DataFrame from pandas import DataFrame
from skopt.space import Categorical, Dimension, Integer, Real from skopt.space import Categorical, Dimension, Integer, Real # noqa
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_interface import IHyperOpt
# --------------------------------
# Add your lib to import here
import talib.abstract as ta # noqa
import freqtrade.vendor.qtpylib.indicators as qtpylib
class AdvancedSampleHyperOpts(IHyperOpt):
class AdvancedSampleHyperOpt(IHyperOpt):
""" """
This is a sample hyperopt to inspire you. This is a sample hyperopt to inspire you.
Feel free to customize it. Feel free to customize it.
@ -37,6 +40,9 @@ class AdvancedSampleHyperOpts(IHyperOpt):
""" """
@staticmethod @staticmethod
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
This method can also be loaded from the strategy, if it doesn't exist in the hyperopt class.
"""
dataframe['adx'] = ta.ADX(dataframe) dataframe['adx'] = ta.ADX(dataframe)
macd = ta.MACD(dataframe) macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd'] dataframe['macd'] = macd['macd']
@ -227,10 +233,33 @@ class AdvancedSampleHyperOpts(IHyperOpt):
Real(-0.5, -0.02, name='stoploss'), Real(-0.5, -0.02, name='stoploss'),
] ]
@staticmethod
def trailing_space() -> List[Dimension]:
"""
Create a trailing stoploss space.
You may override it in your custom Hyperopt class.
"""
return [
# It was decided to always set trailing_stop is to True if the 'trailing' hyperspace
# is used. Otherwise hyperopt will vary other parameters that won't have effect if
# trailing_stop is set False.
# This parameter is included into the hyperspace dimensions rather than assigning
# it explicitly in the code in order to have it printed in the results along with
# other 'trailing' hyperspace parameters.
Categorical([True], name='trailing_stop'),
Real(0.02, 0.35, name='trailing_stop_positive'),
Real(0.01, 0.1, name='trailing_stop_positive_offset'),
Categorical([True, False], name='trailing_only_offset_is_reached'),
]
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators. Should be a copy of from strategy Based on TA indicators.
must align to populate_indicators in this file Can be a copy of the corresponding method from the strategy,
or will be loaded from the strategy.
Must align to populate_indicators used (either from this File, or from the strategy)
Only used when --spaces does not include buy Only used when --spaces does not include buy
""" """
dataframe.loc[ dataframe.loc[
@ -246,8 +275,10 @@ class AdvancedSampleHyperOpts(IHyperOpt):
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators. Should be a copy of from strategy Based on TA indicators.
must align to populate_indicators in this file Can be a copy of the corresponding method from the strategy,
or will be loaded from the strategy.
Must align to populate_indicators used (either from this File, or from the strategy)
Only used when --spaces does not include sell Only used when --spaces does not include sell
""" """
dataframe.loc[ dataframe.loc[

View File

@ -1,13 +1,16 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# --- Do not remove these libs --- # --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy import numpy as np # noqa
import pandas as pd # noqa
from pandas import DataFrame from pandas import DataFrame
# --------------------------------
from freqtrade.strategy.interface import IStrategy
# --------------------------------
# Add your lib to import here # Add your lib to import here
import talib.abstract as ta import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
import numpy # noqa
# This class is a sample. Feel free to customize it. # This class is a sample. Feel free to customize it.
@ -59,6 +62,9 @@ class SampleStrategy(IStrategy):
sell_profit_only = False sell_profit_only = False
ignore_roi_if_buy_signal = False ignore_roi_if_buy_signal = False
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 20
# Optional order type mapping. # Optional order type mapping.
order_types = { order_types = {
'buy': 'limit', 'buy': 'limit',
@ -104,15 +110,20 @@ class SampleStrategy(IStrategy):
# RSI # RSI
dataframe['rsi'] = ta.RSI(dataframe) dataframe['rsi'] = ta.RSI(dataframe)
"""
# ADX # ADX
dataframe['adx'] = ta.ADX(dataframe) dataframe['adx'] = ta.ADX(dataframe)
# Awesome oscillator # # Aroon, Aroon Oscillator
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) # aroon = ta.AROON(dataframe)
# dataframe['aroonup'] = aroon['aroonup']
# dataframe['aroondown'] = aroon['aroondown']
# dataframe['aroonosc'] = ta.AROONOSC(dataframe)
# Commodity Channel Index: values Oversold:<-100, Overbought:>100 # # Awesome oscillator
dataframe['cci'] = ta.CCI(dataframe) # dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
# # Commodity Channel Index: values Oversold:<-100, Overbought:>100
# dataframe['cci'] = ta.CCI(dataframe)
# MACD # MACD
macd = ta.MACD(dataframe) macd = ta.MACD(dataframe)
@ -123,40 +134,39 @@ class SampleStrategy(IStrategy):
# MFI # MFI
dataframe['mfi'] = ta.MFI(dataframe) dataframe['mfi'] = ta.MFI(dataframe)
# Minus Directional Indicator / Movement # # Minus Directional Indicator / Movement
dataframe['minus_dm'] = ta.MINUS_DM(dataframe) # dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe) # dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Plus Directional Indicator / Movement # # Plus Directional Indicator / Movement
dataframe['plus_dm'] = ta.PLUS_DM(dataframe) # dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
dataframe['plus_di'] = ta.PLUS_DI(dataframe) # dataframe['plus_di'] = ta.PLUS_DI(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe) # dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# ROC # # ROC
dataframe['roc'] = ta.ROC(dataframe) # dataframe['roc'] = ta.ROC(dataframe)
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) # # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
rsi = 0.1 * (dataframe['rsi'] - 50) # rsi = 0.1 * (dataframe['rsi'] - 50)
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1) # dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1)
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) # # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) # dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# Stoch # # Stoch
stoch = ta.STOCH(dataframe) # stoch = ta.STOCH(dataframe)
dataframe['slowd'] = stoch['slowd'] # dataframe['slowd'] = stoch['slowd']
dataframe['slowk'] = stoch['slowk'] # dataframe['slowk'] = stoch['slowk']
# Stoch fast # Stoch fast
stoch_fast = ta.STOCHF(dataframe) stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd'] dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk'] dataframe['fastk'] = stoch_fast['fastk']
# Stoch RSI # # Stoch RSI
stoch_rsi = ta.STOCHRSI(dataframe) # stoch_rsi = ta.STOCHRSI(dataframe)
dataframe['fastd_rsi'] = stoch_rsi['fastd'] # dataframe['fastd_rsi'] = stoch_rsi['fastd']
dataframe['fastk_rsi'] = stoch_rsi['fastk'] # dataframe['fastk_rsi'] = stoch_rsi['fastk']
"""
# Overlap Studies # Overlap Studies
# ------------------------------------ # ------------------------------------
@ -167,21 +177,19 @@ class SampleStrategy(IStrategy):
dataframe['bb_middleband'] = bollinger['mid'] dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper'] dataframe['bb_upperband'] = bollinger['upper']
""" # # EMA - Exponential Moving Average
# EMA - Exponential Moving Average # dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) # dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) # dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) # dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) # dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# # SMA - Simple Moving Average
# dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
# SAR Parabol # SAR Parabol
dataframe['sar'] = ta.SAR(dataframe) dataframe['sar'] = ta.SAR(dataframe)
# SMA - Simple Moving Average
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
"""
# TEMA - Triple Exponential Moving Average # TEMA - Triple Exponential Moving Average
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
@ -194,65 +202,57 @@ class SampleStrategy(IStrategy):
# Pattern Recognition - Bullish candlestick patterns # Pattern Recognition - Bullish candlestick patterns
# ------------------------------------ # ------------------------------------
""" # # Hammer: values [0, 100]
# Hammer: values [0, 100] # dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) # # Inverted Hammer: values [0, 100]
# Inverted Hammer: values [0, 100] # dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) # # Dragonfly Doji: values [0, 100]
# Dragonfly Doji: values [0, 100] # dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) # # Piercing Line: values [0, 100]
# Piercing Line: values [0, 100] # dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] # # Morningstar: values [0, 100]
# Morningstar: values [0, 100] # dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] # # Three White Soldiers: values [0, 100]
# Three White Soldiers: values [0, 100] # dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
"""
# Pattern Recognition - Bearish candlestick patterns # Pattern Recognition - Bearish candlestick patterns
# ------------------------------------ # ------------------------------------
""" # # Hanging Man: values [0, 100]
# Hanging Man: values [0, 100] # dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) # # Shooting Star: values [0, 100]
# Shooting Star: values [0, 100] # dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) # # Gravestone Doji: values [0, 100]
# Gravestone Doji: values [0, 100] # dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) # # Dark Cloud Cover: values [0, 100]
# Dark Cloud Cover: values [0, 100] # dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) # # Evening Doji Star: values [0, 100]
# Evening Doji Star: values [0, 100] # dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) # # Evening Star: values [0, 100]
# Evening Star: values [0, 100] # dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
"""
# Pattern Recognition - Bullish/Bearish candlestick patterns # Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------ # ------------------------------------
""" # # Three Line Strike: values [0, -100, 100]
# Three Line Strike: values [0, -100, 100] # dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) # # Spinning Top: values [0, -100, 100]
# Spinning Top: values [0, -100, 100] # dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] # # Engulfing: values [0, -100, 100]
# Engulfing: values [0, -100, 100] # dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] # # Harami: values [0, -100, 100]
# Harami: values [0, -100, 100] # dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] # # Three Outside Up/Down: values [0, -100, 100]
# Three Outside Up/Down: values [0, -100, 100] # dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] # # Three Inside Up/Down: values [0, -100, 100]
# Three Inside Up/Down: values [0, -100, 100] # dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
"""
# Chart type # # Chart type
# ------------------------------------ # # ------------------------------------
""" # # Heikinashi stategy
# Heikinashi stategy # heikinashi = qtpylib.heikinashi(dataframe)
heikinashi = qtpylib.heikinashi(dataframe) # dataframe['ha_open'] = heikinashi['open']
dataframe['ha_open'] = heikinashi['open'] # dataframe['ha_close'] = heikinashi['close']
dataframe['ha_close'] = heikinashi['close'] # dataframe['ha_high'] = heikinashi['high']
dataframe['ha_high'] = heikinashi['high'] # dataframe['ha_low'] = heikinashi['low']
dataframe['ha_low'] = heikinashi['low']
"""
# Retrieve best bid and best ask from the orderbook # Retrieve best bid and best ask from the orderbook
# ------------------------------------ # ------------------------------------

View File

@ -26,7 +26,7 @@
"# Customize these according to your needs.\n", "# Customize these according to your needs.\n",
"\n", "\n",
"# Define some constants\n", "# Define some constants\n",
"ticker_interval = \"5m\"\n", "timeframe = \"5m\"\n",
"# Name of the strategy class\n", "# Name of the strategy class\n",
"strategy_name = 'SampleStrategy'\n", "strategy_name = 'SampleStrategy'\n",
"# Path to user data\n", "# Path to user data\n",
@ -49,7 +49,7 @@
"from freqtrade.data.history import load_pair_history\n", "from freqtrade.data.history import load_pair_history\n",
"\n", "\n",
"candles = load_pair_history(datadir=data_location,\n", "candles = load_pair_history(datadir=data_location,\n",
" ticker_interval=ticker_interval,\n", " timeframe=timeframe,\n",
" pair=pair)\n", " pair=pair)\n",
"\n", "\n",
"# Confirm success\n", "# Confirm success\n",
@ -68,9 +68,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"metadata": { "metadata": {},
"scrolled": true
},
"outputs": [], "outputs": [],
"source": [ "source": [
"# Load strategy using values set above\n", "# Load strategy using values set above\n",
@ -169,6 +167,31 @@
"trades.groupby(\"pair\")[\"sell_reason\"].value_counts()" "trades.groupby(\"pair\")[\"sell_reason\"].value_counts()"
] ]
}, },
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Analyze the loaded trades for trade parallelism\n",
"This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`.\n",
"\n",
"`analyze_trade_parallelism()` returns a timeseries dataframe with an \"open_trades\" column, specifying the number of open trades for each candle."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from freqtrade.data.btanalysis import analyze_trade_parallelism\n",
"\n",
"# Analyze the above\n",
"parallel_trades = analyze_trade_parallelism(trades, '5m')\n",
"\n",
"\n",
"parallel_trades.plot()"
]
},
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},

View File

@ -0,0 +1,3 @@
(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30
(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising

View File

@ -0,0 +1 @@
(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30

View File

@ -0,0 +1,8 @@
if params.get('mfi-enabled'):
conditions.append(dataframe['mfi'] < params['mfi-value'])
if params.get('fastd-enabled'):
conditions.append(dataframe['fastd'] < params['fastd-value'])
if params.get('adx-enabled'):
conditions.append(dataframe['adx'] > params['adx-value'])
if params.get('rsi-enabled'):
conditions.append(dataframe['rsi'] < params['rsi-value'])

View File

@ -0,0 +1,2 @@
if params.get('rsi-enabled'):
conditions.append(dataframe['rsi'] < params['rsi-value'])

View File

@ -0,0 +1,9 @@
Integer(10, 25, name='mfi-value'),
Integer(15, 45, name='fastd-value'),
Integer(20, 50, name='adx-value'),
Integer(20, 40, name='rsi-value'),
Categorical([True, False], name='mfi-enabled'),
Categorical([True, False], name='fastd-enabled'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')

View File

@ -0,0 +1,3 @@
Integer(20, 40, name='rsi-value'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')

View File

@ -0,0 +1,8 @@
if params.get('sell-mfi-enabled'):
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
if params.get('sell-fastd-enabled'):
conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
if params.get('sell-adx-enabled'):
conditions.append(dataframe['adx'] < params['sell-adx-value'])
if params.get('sell-rsi-enabled'):
conditions.append(dataframe['rsi'] > params['sell-rsi-value'])

View File

@ -0,0 +1,2 @@
if params.get('sell-rsi-enabled'):
conditions.append(dataframe['rsi'] > params['sell-rsi-value'])

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