Merge pull request #1578 from freqtrade/release/0.18.1

Release/0.18.1
This commit is contained in:
Matthias 2019-02-21 12:33:09 +01:00 committed by GitHub
commit 905beef8a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 2968 additions and 1247 deletions

View File

@ -21,6 +21,7 @@ search: False
requirements: requirements:
- requirements.txt - requirements.txt
- requirements-dev.txt - requirements-dev.txt
- requirements-plot.txt
# configure the branch prefix the bot is using # configure the branch prefix the bot is using

8
.readthedocs.yml Normal file
View File

@ -0,0 +1,8 @@
# .readthedocs.yml
build:
image: latest
python:
version: 3.6
setup_py_install: false

View File

@ -1,7 +1,7 @@
sudo: true sudo: true
os: os:
- linux - linux
dist: trusty dist: xenial
language: python language: python
python: python:
- 3.6 - 3.6
@ -17,9 +17,9 @@ addons:
- libdw-dev - libdw-dev
- binutils-dev - binutils-dev
install: install:
- ./build_helpers/install_ta-lib.sh - cd build_helpers && ./install_ta-lib.sh; cd ..
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH - export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
- pip install --upgrade flake8 coveralls pytest-random-order pytest-asyncio mypy - pip install --upgrade pytest-random-order
- pip install -r requirements-dev.txt - pip install -r requirements-dev.txt
- pip install -e . - pip install -e .
jobs: jobs:
@ -27,7 +27,6 @@ jobs:
- stage: tests - stage: tests
script: script:
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ - pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
- coveralls
name: pytest name: pytest
- script: - script:
- cp config.json.example config.json - cp config.json.example config.json
@ -48,11 +47,13 @@ jobs:
- 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"
after_success:
- coveralls
notifications: notifications:
slack: slack:
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q= secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=
cache: cache:
pip: True
directories: directories:
- $HOME/.cache/pip - /usr/local/lib
- ta-lib

View File

@ -14,6 +14,10 @@ Few pointers for contributions:
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
## Getting started
Best start by reading the [documentation](https://www.freqtrade.io/) to get a feel for what is possible with the bot, or head straight to the [Developer-documentation](https://www.freqtrade.io/en/latest/developer/) (WIP) which should help you getting started.
## Before sending the PR: ## Before sending the PR:
### 1. Run unit tests ### 1. Run unit tests
@ -41,12 +45,6 @@ pytest freqtrade/tests/test_<file_name>.py::test_<method_name>
### 2. Test if your code is PEP8 compliant ### 2. Test if your code is PEP8 compliant
#### Install packages
```bash
pip3.6 install flake8 coveralls
```
#### Run Flake8 #### Run Flake8
```bash ```bash
@ -60,22 +58,12 @@ Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using
### 3. Test if all type-hints are correct ### 3. Test if all type-hints are correct
#### Install packages
``` bash
pip3.6 install mypy
```
#### Run mypy #### Run mypy
``` bash ``` bash
mypy freqtrade mypy freqtrade
``` ```
## Getting started
Best start by reading the [documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) to get a feel for what is possible with the bot, or head straight to the [Developer-documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/developer.md) (WIP) which should help you getting started.
## (Core)-Committer Guide ## (Core)-Committer Guide
### Process: Pull Requests ### Process: Pull Requests

View File

@ -1,4 +1,4 @@
FROM python:3.7.0-slim-stretch FROM python:3.7.2-slim-stretch
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install curl build-essential \ && apt-get -y install curl build-essential \

View File

@ -1,10 +1,11 @@
# freqtrade # Freqtrade
[![Build Status](https://travis-ci.org/freqtrade/freqtrade.svg?branch=develop)](https://travis-ci.org/freqtrade/freqtrade) [![Build Status](https://travis-ci.org/freqtrade/freqtrade.svg?branch=develop)](https://travis-ci.org/freqtrade/freqtrade)
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
Simple High frequency trading bot for crypto currencies designed to support multi exchanges and be controlled via Telegram. Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png) ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
@ -27,48 +28,27 @@ hesitate to read the source code and understand the mechanism of this bot.
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](#a-note-on-binance)) - [X] [Binance](https://www.binance.com/) ([*Note for binance users](#a-note-on-binance))
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ - [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
## Documentation
We invite you to read the bot documentation to ensure you understand how the bot is working.
Please find the complete documentation on our [website](https://www.freqtrade.io).
## Features ## Features
- [x] **Based on Python 3.6+**: For botting on any operating system - Windows, macOS and Linux - [x] **Based on Python 3.6+**: For botting on any operating system - Windows, macOS and Linux.
- [x] **Persistence**: Persistence is achieved through sqlite - [x] **Persistence**: Persistence is achieved through sqlite.
- [x] **Dry-run**: Run the bot without playing money. - [x] **Dry-run**: Run the bot without playing money.
- [x] **Backtesting**: Run a simulation of your buy/sell strategy. - [x] **Backtesting**: Run a simulation of your buy/sell strategy.
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. - [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://github.com/freqtrade/freqtrade/blob/develop/docs/edge.md) - [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/latest/edge/).
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists. - [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists.
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid. - [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
- [x] **Manageable via Telegram**: Manage the bot with Telegram - [x] **Manageable via Telegram**: Manage the bot with Telegram.
- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat. - [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat.
- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss. - [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss.
- [x] **Performance status report**: Provide a performance status of your current trades. - [x] **Performance status report**: Provide a performance status of your current trades.
## Table of Contents
- [Quick start](#quick-start)
- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md)
- [Edge](https://github.com/freqtrade/freqtrade/blob/develop/docs/edge.md)
- [Basic Usage](#basic-usage)
- [Bot commands](#bot-commands)
- [Telegram RPC commands](#telegram-rpc-commands)
- [Support](#support)
- [Help](#help--slack)
- [Bugs](#bugs--issues)
- [Feature Requests](#feature-requests)
- [Pull Requests](#pull-requests)
- [Requirements](#requirements)
- [Min hardware required](#min-hardware-required)
- [Software requirements](#software-requirements)
- [Wanna help?](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
- [Dev - getting started](https://github.com/freqtrade/freqtrade/blob/develop/docs/developer.md) (WIP)
## Quick start ## Quick start
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot. Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
@ -80,63 +60,52 @@ git checkout develop
./setup.sh --install ./setup.sh --install
``` ```
_Windows installation is explained in [Installation doc](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)_ For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/latest/installation/).
## Documentation
We invite you to read the bot documentation to ensure you understand how the bot is working.
- [Index](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
- [Bot usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md)
- [How to run the bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
- [How to use Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
- [How to use Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
## Basic Usage ## Basic Usage
### Bot commands ### Bot commands
```bash ```
usage: main.py [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME] usage: main.py [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
[--strategy-path PATH] [--dynamic-whitelist [INT]] [--strategy-path PATH] [--customhyperopt NAME]
[--dry-run-db] [--dynamic-whitelist [INT]] [--db-url PATH]
{backtesting,hyperopt} ... {backtesting,edge,hyperopt} ...
Simple High Frequency Trading Bot for crypto currencies Free, open source crypto trading bot
positional arguments: positional arguments:
{backtesting,hyperopt} {backtesting,edge,hyperopt}
backtesting backtesting module backtesting backtesting module
edge edge module
hyperopt hyperopt module hyperopt hyperopt module
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-v, --verbose be verbose -v, --verbose verbose mode (-vv for more, -vvv to get all messages)
--version show program's version number and exit --version show program\'s version number and exit
-c PATH, --config PATH -c PATH, --config PATH
specify configuration file (default: config.json) specify configuration file (default: config.json)
-d PATH, --datadir PATH -d PATH, --datadir PATH
path to backtest data (default: path to backtest data
freqtrade/tests/testdata
-s NAME, --strategy NAME -s NAME, --strategy NAME
specify strategy class name (default: DefaultStrategy) specify strategy class name (default: DefaultStrategy)
--strategy-path PATH specify additional strategy lookup path --strategy-path PATH specify additional strategy lookup path
--customhyperopt NAME
specify hyperopt class name (default:
DefaultHyperOpts)
--dynamic-whitelist [INT] --dynamic-whitelist [INT]
dynamically generate and update whitelist based on 24h dynamically generate and update whitelist based on 24h
BaseVolume (Default 20 currencies) BaseVolume (default: 20) DEPRECATED.
--dry-run-db Force dry run to use a local DB --db-url PATH Override trades database URL, this is useful if
"tradesv3.dry_run.sqlite" instead of memory DB. Work dry_run is enabled or in custom deployments (default:
only if dry_run is enabled. None)
``` ```
### 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://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) 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/)
- `/start`: Starts the trader - `/start`: Starts the trader
- `/stop`: Stops the trader - `/stop`: Stops the trader
@ -227,4 +196,3 @@ To run this bot we recommend you a cloud instance with a minimum of:
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) - [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) - [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
- [Docker](https://www.docker.com/products/docker) (Recommended) - [Docker](https://www.docker.com/products/docker) (Recommended)

View File

@ -1,4 +1,4 @@
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then if [ ! -f "/usr/local/lib/libta_lib.a" ]; then
tar zxvf ta-lib-0.4.0-src.tar.gz tar zxvf ta-lib-0.4.0-src.tar.gz
cd ta-lib \ cd ta-lib \
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
@ -7,7 +7,5 @@ if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
&& which sudo && sudo make install || make install \ && which sudo && sudo make install || make install \
&& cd .. && cd ..
else else
echo "TA-lib already installed, skipping download and build." echo "TA-lib already installed, skipping installation"
cd ta-lib && sudo make install && cd ..
fi fi

View File

@ -13,7 +13,7 @@ if [ "${TRAVIS_EVENT_TYPE}" = "cron" ]; then
else else
echo "event ${TRAVIS_EVENT_TYPE}: building with cache" echo "event ${TRAVIS_EVENT_TYPE}: building with cache"
# Pull last build to avoid rebuilding the whole image # Pull last build to avoid rebuilding the whole image
docker pull ${REPO}:${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} .
fi fi

View File

@ -3,6 +3,7 @@
"stake_currency": "BTC", "stake_currency": "BTC",
"stake_amount": 0.05, "stake_amount": 0.05,
"fiat_display_currency": "USD", "fiat_display_currency": "USD",
"amount_reserve_percent" : 0.05,
"dry_run": false, "dry_run": false,
"ticker_interval": "5m", "ticker_interval": "5m",
"trailing_stop": false, "trailing_stop": false,
@ -37,7 +38,8 @@
"buy": "limit", "buy": "limit",
"sell": "limit", "sell": "limit",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": "false" "stoploss_on_exchange": "false",
"stoploss_on_exchange_interval": 60
}, },
"order_time_in_force": { "order_time_in_force": {
"buy": "gtc", "buy": "gtc",

View File

@ -3,11 +3,6 @@
This page explains how to validate your strategy performance by using This page explains how to validate your strategy performance by using
Backtesting. Backtesting.
## Table of Contents
- [Test your strategy with Backtesting](#test-your-strategy-with-backtesting)
- [Understand the backtesting result](#understand-the-backtesting-result)
## Test your strategy with Backtesting ## Test your strategy with Backtesting
Now you have good Buy and Sell strategies, you want to test it against Now you have good Buy and Sell strategies, you want to test it against
@ -171,53 +166,65 @@ The most important in the backtesting is to understand the result.
A backtesting result will look like that: A backtesting result will look like that:
``` ```
======================================== BACKTESTING REPORT ========================================= ========================================================= BACKTESTING REPORT ========================================================
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss | | pair | buy count | avg profit % | cum profit % | tot profit BTC | tot profit % | avg duration | profit | loss |
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:| |:---------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:|
| ETH/BTC | 44 | 0.18 | 0.00159118 | 50.9 | 44 | 0 | | ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 | 21 |
| LTC/BTC | 27 | 0.10 | 0.00051931 | 103.1 | 26 | 1 | | ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 | 8 |
| ETC/BTC | 24 | 0.05 | 0.00022434 | 166.0 | 22 | 2 | | BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 | 14 |
| DASH/BTC | 29 | 0.18 | 0.00103223 | 192.2 | 29 | 0 | | DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 | 7 |
| ZEC/BTC | 65 | -0.02 | -0.00020621 | 202.7 | 62 | 3 | | ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 | 10 |
| XLM/BTC | 35 | 0.02 | 0.00012877 | 242.4 | 32 | 3 | | EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 | 20 |
| BCH/BTC | 12 | 0.62 | 0.00149284 | 50.0 | 12 | 0 | | ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 | 15 |
| POWR/BTC | 21 | 0.26 | 0.00108215 | 134.8 | 21 | 0 | | ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 | 17 |
| ADA/BTC | 54 | -0.19 | -0.00205202 | 191.3 | 47 | 7 | | IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 | 18 |
| XMR/BTC | 24 | -0.43 | -0.00206013 | 120.6 | 20 | 4 | | LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 | 9 |
| TOTAL | 335 | 0.03 | 0.00175246 | 157.9 | 315 | 20 | | LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 | 21 |
2018-06-13 06:57:27,347 - freqtrade.optimize.backtesting - INFO - | NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 | 7 |
====================================== LEFT OPEN TRADES REPORT ====================================== | NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 | 13 |
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss | | REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 | 5 |
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:| | XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 | 9 |
| ETH/BTC | 3 | 0.16 | 0.00009619 | 25.0 | 3 | 0 | | XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 | 11 |
| LTC/BTC | 1 | -1.00 | -0.00020118 | 1085.0 | 0 | 1 | | XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 | 23 |
| ETC/BTC | 2 | -1.80 | -0.00071933 | 1092.5 | 0 | 2 | | ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 | 15 |
| DASH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 | | TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 |
| ZEC/BTC | 3 | -4.27 | -0.00256826 | 1301.7 | 0 | 3 | ========================================================= SELL REASON STATS =========================================================
| XLM/BTC | 3 | -1.11 | -0.00066744 | 965.0 | 0 | 3 | | Sell Reason | Count |
| BCH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 | |:-------------------|--------:|
| POWR/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 | | trailing_stop_loss | 205 |
| ADA/BTC | 7 | -3.58 | -0.00503604 | 850.0 | 0 | 7 | | stop_loss | 166 |
| XMR/BTC | 4 | -3.79 | -0.00303456 | 291.2 | 0 | 4 | | sell_signal | 56 |
| TOTAL | 23 | -2.63 | -0.01213062 | 750.4 | 3 | 20 | | force_sell | 2 |
====================================================== LEFT OPEN TRADES REPORT ======================================================
| pair | buy count | avg profit % | cum profit % | tot profit BTC | tot profit % | avg duration | profit | loss |
|:---------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:|
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 | 0 |
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 | 0 |
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 | 0 |
``` ```
The 1st table will contain all trades the bot made. The 1st table will contain all trades the bot made.
The 2nd table will contain all trades the bot had to `forcesell` at the end of the backtest period to prsent a full picture. The 2nd table will contain a recap of sell reasons.
The 3rd table will contain all trades the bot had to `forcesell` at the end of the backtest period to present a full picture.
These trades are also included in the first table, but are extracted separately for clarity. These trades are also included in the first table, but are extracted separately for clarity.
The last line will give you the overall performance of your strategy, The last line will give you the overall performance of your strategy,
here: here:
``` ```
TOTAL 419 -0.41 -0.00348593 52.9 | TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 |
``` ```
We understand the bot has made `419` trades for an average duration of We understand the bot has made `429` trades for an average duration of
`52.9` min, with a performance of `-0.41%` (loss), that means it has `4:12:00`, with a performance of `76.20%` (profit), that means it has
lost a total of `-0.00348593 BTC`. earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC.
The column `avg profit %` shows the average profit for all trades made while the column `cum profit %` sums all the profits/losses.
The column `tot profit %` shows instead the total profit % in relation to allocated capital
(`max_open_trades * stake_amount`). In the above results we have `max_open_trades=2 stake_amount=0.005` in config
so `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`.
As you will see your strategy performance will be influenced by your buy As you will see your strategy performance will be influenced by your buy
strategy, your sell strategy, and also by the `minimal_roi` and strategy, your sell strategy, and also by the `minimal_roi` and
@ -256,15 +263,15 @@ There will be an additional table comparing win/losses of the different strategi
Detailed output for all strategies one after the other will be available, so make sure to scroll up. Detailed output for all strategies one after the other will be available, so make sure to scroll up.
``` ```
=================================================== Strategy Summary ==================================================== =========================================================== Strategy Summary ===========================================================
| Strategy | buy count | avg profit % | cum profit % | total profit ETH | avg duration | profit | loss | | Strategy | buy count | avg profit % | cum profit % | tot profit BTC | tot profit % | avg duration | profit | loss |
|:-----------|------------:|---------------:|---------------:|-------------------:|:----------------|---------:|-------:| |:------------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:|
| Strategy1 | 19 | -0.76 | -14.39 | -0.01440287 | 15:48:00 | 15 | 4 | | Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 |
| Strategy2 | 6 | -2.73 | -16.40 | -0.01641299 | 1 day, 14:12:00 | 3 | 3 | | Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 825 |
``` ```
## Next step ## Next step
Great, your strategy is profitable. What if the bot can give your the Great, your strategy is profitable. What if the bot can give your the
optimal parameters to use for your strategy? optimal parameters to use for your strategy?
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md)

View File

@ -1,28 +1,8 @@
# Bot Optimization # Optimization
This page explains where to customize your strategies, and add new This page explains where to customize your strategies, and add new
indicators. indicators.
## Table of Contents
- [Install a custom strategy file](#install-a-custom-strategy-file)
- [Customize your strategy](#change-your-strategy)
- [Anatomy of a strategy](#anatomy-of-a-strategy)
- [Customize indicators](#customize-indicators)
- [Buy signal rules](#buy-signal-rules)
- [Sell signal rules](#sell-signal-rules)
- [Minimal ROI](#minimal-roi)
- [Stoploss](#stoploss)
- [Ticker interval](#ticker-interval)
- [Metadata dict](#metadata-dict)
- [Where is the default strategy](#where-is-the-default-strategy)
- [Specify custom strategy location](#specify-custom-strategy-location)
- [Further strategy ideas](#further-strategy-ideas)
- [Where is the default strategy](#where-is-the-default-strategy)
Since the version `0.16.0` the bot allows using custom strategy file.
## Install a custom strategy file ## Install a custom strategy file
This is very simple. Copy paste your strategy file into the folder This is very simple. Copy paste your strategy file into the folder
@ -67,6 +47,12 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py) **For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
file as reference.** file as reference.**
!!! Note: Strategies and Backtesting
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.
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.
### Customize Indicators ### Customize Indicators
Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file. Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
@ -118,9 +104,9 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
return dataframe return dataframe
``` ```
#### Want more indicator examples
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py). !!! Note "Want more indicator examples?"
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).<br/>
Then uncomment indicators you need. Then uncomment indicators you need.
### Buy signal rules ### Buy signal rules
@ -199,10 +185,9 @@ minimal_roi = {
The above configuration would therefore mean: The above configuration would therefore mean:
- Sell whenever 4% profit was reached - Sell whenever 4% profit was reached
- Sell after 20 minutes when 2% profit was reached - Sell when 2% profit was reached (in effect after 20 minutes)
- Sell after 20 minutes when 2% profit was reached - Sell when 1% profit was reached (in effect after 30 minutes)
- Sell after 30 minutes when 1% profit was reached - Sell when trade is non-loosing (in effect after 40 minutes)
- Sell after 40 minutes when the trade is non-loosing (no profit)
The calculation does include fees. The calculation does include fees.
@ -237,12 +222,129 @@ This is the set of candles the bot should download and use for the analysis.
Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work. Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work.
Please note that the same buy/sell signals may work with one interval, but not the other. Please note that the same buy/sell signals may work with one interval, but not the other.
This setting is accessible within the strategy by using `self.ticker_interval`.
### Metadata dict ### Metadata dict
The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information. The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information.
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`. Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
The Metadata-dict should not be modified and does not persist information across multiple calls.
Instead, have a look at the section [Storing information](#Storing-information)
### Storing information
Storing information can be accomplished by crating a new dictionary within the strategy class.
The name of the variable can be choosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
```python
class Awesomestrategy(IStrategy):
# Create custom dictionary
cust_info = {}
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Check if the entry already exists
if "crosstime" in self.cust_info[metadata["pair"]:
self.cust_info[metadata["pair"]["crosstime"] += 1
else:
self.cust_info[metadata["pair"]["crosstime"] = 1
```
!!! Warning:
The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash.
!!! Note:
If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
### Additional data (DataProvider)
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
!!!Note:
The DataProvier is currently not available during backtesting / hyperopt, but this is planned for the future.
All methods return `None` in case of failure (do not raise an exception).
Please always check if the `DataProvider` is available to avoid failures during backtesting.
#### Possible options for DataProvider
- `available_pairs` - Property with tuples listing cached pairs with their intervals. (pair, interval)
- `ohlcv(pair, ticker_interval)` - Currently cached ticker data for all pairs in the whitelist, returns DataFrame or empty DataFrame
- `historic_ohlcv(pair, ticker_interval)` - Data stored on disk
- `runmode` - Property containing the current runmode.
#### ohlcv / historic_ohlcv
``` python
if self.dp:
if dp.runmode == 'live':
if ('ETH/BTC', ticker_interval) in self.dp.available_pairs:
data_eth = self.dp.ohlcv(pair='ETH/BTC',
ticker_interval=ticker_interval)
else:
# Get historic ohlcv data (cached on disk).
history_eth = self.dp.historic_ohlcv(pair='ETH/BTC',
ticker_interval='1h')
```
!!! Warning: Warning about backtesting
Be carefull when using dataprovider in backtesting. `historic_ohlcv()` 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).
#### Available Pairs
``` python
if self.dp:
for pair, ticker in self.dp.available_pairs:
print(f"available {pair}, {ticker}")
```
#### Get data for non-tradeable pairs
Data for additional, informative pairs (reference pairs) can be beneficial for some strategies.
Ohlcv data for these pairs will be downloaded as part of the regular whitelist refresh process and is available via `DataProvider` just as other pairs (see above).
These parts will **not** be traded unless they are also specified in the pair whitelist, or have been selected by Dynamic Whitelisting.
The pairs need to be specified as tuples in the format `("pair", "interval")`, with pair as the first and time interval as the second argument.
Sample:
``` python
def informative_pairs(self):
return [("ETH/USDT", "5m"),
("BTC/TUSD", "15m"),
]
```
!!! Warning:
As these pairs will be refreshed as part of the regular whitelist refresh, it's best to keep this list short.
All intervals and all pairs can be specified as long as they are available (and active) on the used exchange.
It is however better to use resampling to longer time-intervals when possible
to avoid hammering the exchange with too many requests and risk beeing blocked.
### Additional data - Wallets
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
!!!NOTE:
Wallets is not available during backtesting / hyperopt.
Please always check if `Wallets` is available to avoid failures during backtesting.
``` python
if self.wallets:
free_eth = self.wallets.get_free('ETH')
used_eth = self.wallets.get_used('ETH')
total_eth = self.wallets.get_total('ETH')
```
#### Possible options for Wallets
- `get_free(asset)` - currently available balance to trade
- `get_used(asset)` - currently tied up balance (open orders)
- `get_total(asset)` - total available balance - sum of the 2 above
### Where is the default strategy? ### Where is the default strategy?
The default buy strategy is located in the file The default buy strategy is located in the file
@ -267,4 +369,4 @@ We also got a *strategy-sharing* channel in our [Slack community](https://join.s
## Next step ## Next step
Now you have a perfect strategy you probably want to backtest it. Now you have a perfect strategy you probably want to backtest it.
Your next step is to learn [How to use the Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md). Your next step is to learn [How to use the Backtesting](backtesting.md).

View File

@ -1,32 +1,28 @@
# Bot usage # Start the bot
This page explains the difference parameters of the bot and how to run it. This page explains the different parameters of the bot and how to run it.
## Table of Contents
- [Bot commands](#bot-commands)
- [Backtesting commands](#backtesting-commands)
- [Hyperopt commands](#hyperopt-commands)
## Bot commands ## Bot commands
``` ```
usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME] usage: main.py [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
[--strategy-path PATH] [--dynamic-whitelist [INT]] [--strategy-path PATH] [--customhyperopt NAME]
[--db-url PATH] [--dynamic-whitelist [INT]] [--db-url PATH]
{backtesting,hyperopt} ... {backtesting,edge,hyperopt} ...
Simple High Frequency Trading Bot for crypto currencies Free, open source crypto trading bot
positional arguments: positional arguments:
{backtesting,hyperopt} {backtesting,edge,hyperopt}
backtesting backtesting module backtesting backtesting module
edge edge module
hyperopt hyperopt module hyperopt hyperopt module
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-v, --verbose be verbose -v, --verbose verbose mode (-vv for more, -vvv to get all messages)
--version show program's version number and exit --version show program\'s version number and exit
-c PATH, --config PATH -c PATH, --config PATH
specify configuration file (default: config.json) specify configuration file (default: config.json)
-d PATH, --datadir PATH -d PATH, --datadir PATH
@ -34,12 +30,15 @@ optional arguments:
-s NAME, --strategy NAME -s NAME, --strategy NAME
specify strategy class name (default: DefaultStrategy) specify strategy class name (default: DefaultStrategy)
--strategy-path PATH specify additional strategy lookup path --strategy-path PATH specify additional strategy lookup path
--customhyperopt NAME
specify hyperopt class name (default:
DefaultHyperOpts)
--dynamic-whitelist [INT] --dynamic-whitelist [INT]
dynamically generate and update whitelist based on 24h dynamically generate and update whitelist based on 24h
BaseVolume (default: 20) DEPRECATED BaseVolume (default: 20) DEPRECATED.
--db-url PATH Override trades database URL, this is useful if --db-url PATH Override trades database URL, this is useful if
dry_run is enabled or in custom deployments (default: dry_run is enabled or in custom deployments (default:
sqlite:///tradesv3.sqlite) None)
``` ```
### How to use a different config file? ### How to use a different config file?
@ -51,7 +50,7 @@ default, the bot will load the file `./config.json`
python3 ./freqtrade/main.py -c path/far/far/away/config.json python3 ./freqtrade/main.py -c path/far/far/away/config.json
``` ```
### How to use --strategy? ### How to use **--strategy**?
This parameter will allow you to load your custom strategy class. This parameter will allow you to load your custom strategy class.
Per default without `--strategy` or `-s` the bot will load the Per default without `--strategy` or `-s` the bot will load the
@ -74,7 +73,7 @@ message the reason (File not found, or errors in your code).
Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md). Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
### How to use --strategy-path? ### How to use **--strategy-path**?
This parameter allows you to add an additional strategy lookup path, which gets This parameter allows you to add an additional strategy lookup path, which gets
checked before the default locations (The passed path must be a folder!): checked before the default locations (The passed path must be a folder!):
@ -87,9 +86,10 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/fol
This is very simple. Copy paste your strategy file into the folder This is very simple. Copy paste your strategy file into the folder
`user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it. `user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it.
### How to use --dynamic-whitelist? ### How to use **--dynamic-whitelist**?
> Dynamic-whitelist is deprecated. Please move your configurations to the configuration as outlined [here](docs/configuration.md#Dynamic-Pairlists) !!! danger "DEPRECATED"
Dynamic-whitelist is deprecated. Please move your configurations to the configuration as outlined [here](/configuration/#dynamic-pairlists)
Per default `--dynamic-whitelist` will retrieve the 20 currencies based Per default `--dynamic-whitelist` will retrieve the 20 currencies based
on BaseVolume. This value can be changed when you run the script. on BaseVolume. This value can be changed when you run the script.
@ -113,7 +113,7 @@ python3 ./freqtrade/main.py --dynamic-whitelist 30
negative value (e.g -2), `--dynamic-whitelist` will use the default negative value (e.g -2), `--dynamic-whitelist` will use the default
value (20). value (20).
### How to use --db-url? ### How to use **--db-url**?
When you run the bot in Dry-run mode, per default no transactions are When you run the bot in Dry-run mode, per default no transactions are
stored in a database. If you want to store your bot actions in a DB stored in a database. If you want to store your bot actions in a DB
@ -129,8 +129,8 @@ python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.s
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] [--eps] [--dmmp] usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
[--timerange TIMERANGE] [-l] [-r] [--eps] [--dmmp] [-l] [-r]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export EXPORT] [--export-filename PATH] [--export EXPORT] [--export-filename PATH]
@ -138,6 +138,8 @@ optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
specify ticker interval (1m, 5m, 30m, 1h, 1d) specify ticker interval (1m, 5m, 30m, 1h, 1d)
--timerange TIMERANGE
specify what timerange of data to use.
--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)
@ -145,8 +147,6 @@ optional arguments:
Disable applying `max_open_trades` during backtest Disable applying `max_open_trades` during backtest
(same as setting `max_open_trades` to a very high (same as setting `max_open_trades` to a very high
number) number)
--timerange TIMERANGE
specify what timerange of data to use.
-l, --live using live data -l, --live using live data
-r, --refresh-pairs-cached -r, --refresh-pairs-cached
refresh the pairs files in tests/testdata with the refresh the pairs files in tests/testdata with the
@ -167,18 +167,18 @@ optional arguments:
filename=user_data/backtest_data/backtest_today.json filename=user_data/backtest_data/backtest_today.json
(default: user_data/backtest_data/backtest- (default: user_data/backtest_data/backtest-
result.json) result.json)
``` ```
### How to use --refresh-pairs-cached parameter? ### How to use **--refresh-pairs-cached** parameter?
The first time your run Backtesting, it will take the pairs you have The first time your run Backtesting, it will take the pairs you have
set in your config file and download data from Bittrex. set in your config file and download data from Bittrex.
If for any reason you want to update your data set, you use If for any reason you want to update your data set, you use
`--refresh-pairs-cached` to force Backtesting to update the data it has. `--refresh-pairs-cached` to force Backtesting to update the data it has.
**Use it only if you want to update your data set. You will not be able
to come back to the previous version.** !!! Note
Use it only if you want to update your data set. You will not be able to come back to the previous version.
To test your strategy with latest data, we recommend continuing using To test your strategy with latest data, we recommend continuing using
the parameter `-l` or `--live`. the parameter `-l` or `--live`.
@ -250,4 +250,4 @@ in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.
## Next step ## Next step
The optimal strategy of the bot will change with time depending of the market trends. The next step is to The optimal strategy of the bot will change with time depending of the market trends. The next step is to
[optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md). [optimize your bot](bot-optimization.md).

View File

@ -2,12 +2,6 @@
This page explains how to configure your `config.json` file. This page explains how to configure your `config.json` file.
## Table of Contents
- [Bot commands](#bot-commands)
- [Backtesting commands](#backtesting-commands)
- [Hyperopt commands](#hyperopt-commands)
## Setup config.json ## Setup config.json
We recommend to copy and use the `config.json.example` as a template We recommend to copy and use the `config.json.example` as a template
@ -15,70 +9,98 @@ for your bot configuration.
The table below will list all configuration parameters. The table below will list all configuration parameters.
| Command | Default | Mandatory | Description | Mandatory Parameters are marked as **Required**.
|----------|---------|----------|-------------|
| `max_open_trades` | 3 | Yes | Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades)
| `stake_currency` | BTC | Yes | Crypto-currency used for trading.
| `stake_amount` | 0.05 | Yes | 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 avaliable balance.
| `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below.
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
| `process_only_new_candles` | false | No | 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. Can be set either in Configuration or in the strategy.
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file.
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
| `trailing_stop` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file).
| `trailing_stop_positve` | 0 | No | Changes stop-loss once profit has been reached.
| `trailing_stop_positve_offset` | 0 | No | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive.
| `unfilledtimeout.buy` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled.
| `unfilledtimeout.sell` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled.
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
| `bid_strategy.use_order_book` | false | No | Allows buying of pair using the rates in Order Book Bids.
| `bid_strategy.order_book_top` | 0 | No | Bot will use the top N rate in Order Book Bids. Ie. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids.
| `bid_strategy. check_depth_of_market.enabled` | false | No | Does not buy if the % difference of buy orders and sell orders is met in Order Book.
| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | 0 | No | 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.
| `ask_strategy.use_order_book` | false | No | Allows selling of open traded pair using the rates in Order Book Asks.
| `ask_strategy.order_book_min` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
| `ask_strategy.order_book_max` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
| `order_types` | None | No | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types).
| `order_time_in_force` | None | No | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force).
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
| `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
| `exchange.ccxt_rate_limit` | True | No | DEPRECATED!! Have CCXT handle Exchange rate limits. Depending on the exchange, having this to false can lead to temporary bans from the exchange.
| `exchange.ccxt_config` | None | No | 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_async_config` | None | No | 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)
| `edge` | false | No | Please refer to [edge configuration document](edge.md) for detailed explanation.
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
| `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision.
| `experimental.ignore_roi_if_buy_signal` | false | No | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`
| `pairlist.method` | StaticPairList | No | Use Static whitelist. [More information below](#dynamic-pairlists).
| `pairlist.config` | None | No | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists).
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
| `webhook.enabled` | false | No | Enable usage of Webhook notifications
| `webhook.url` | false | No | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
| `webhook.webhookbuy` | false | No | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
| `webhook.webhooksell` | false | No | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
| `webhook.webhookstatus` | false | No | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
| `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
| `initial_state` | running | No | Defines the initial application state. More information below.
| `forcebuy_enable` | false | No | Enables the RPC Commands to force a buy. More information below.
| `strategy` | DefaultStrategy | No | Defines Strategy class to use.
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
The definition of each config parameters is in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205). | Command | Default | 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)
| `stake_currency` | BTC | **Required.** Crypto-currency used for trading.
| `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.
| `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.
| `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-strategy).
| `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below.
| `dry_run` | true | **Required.** Define if the bot must be in Dry-run or production mode.
| `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-strategy).
| `minimal_roi` | See below | Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-strategy).
| `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-strategy).
| `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-strategy).
| `trailing_stop_positive` | 0 | Changes stop-loss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-strategy).
| `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-strategy).
| `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.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.
| `bid_strategy.ask_last_balance` | 0.0 | **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.order_book_top` | 0 | Bot will use the top N rate in Order Book Bids. Ie. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids.
| `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.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.
| `ask_strategy.use_order_book` | false | Allows selling of open traded pair using the rates in Order Book Asks.
| `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_max` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
| `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-strategy).
| `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-strategy).
| `exchange.name` | bittrex | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
| `exchange.key` | key | API key to use for the exchange. Only required when you are in production mode.
| `exchange.secret` | secret | API secret to use for the exchange. Only required when you are in production mode.
| `exchange.pair_whitelist` | [] | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
| `exchange.pair_blacklist` | [] | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
| `exchange.ccxt_rate_limit` | True | DEPRECATED!! Have CCXT handle Exchange rate limits. Depending on the exchange, having this to false can lead to temporary bans from the exchange.
| `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_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)
| `edge` | false | Please refer to [edge configuration document](edge.md) for detailed explanation.
| `experimental.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-strategy).
| `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-strategy).
| `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-strategy).
| `pairlist.method` | StaticPairList | Use Static whitelist. [More information below](#dynamic-pairlists).
| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists).
| `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram.
| `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
| `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
| `webhook.enabled` | false | Enable usage of Webhook notifications
| `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` | 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` | 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` | false | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
| `db_url` | `sqlite:///tradesv3.sqlite`| Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
| `initial_state` | running | Defines the initial application state. More information below.
| `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below.
| `strategy` | DefaultStrategy | Defines Strategy class to use.
| `strategy_path` | null | Adds an additional strategy lookup path (must be a folder).
| `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second.
### Parameters in strategy
The following parameters can be set in either configuration or strategy.
Values in the configuration are always overwriting values set in the strategy.
* `minimal_roi`
* `ticker_interval`
* `stoploss`
* `trailing_stop`
* `trailing_stop_positive`
* `trailing_stop_positive_offset`
* `process_only_new_candles`
* `order_types`
* `order_time_in_force`
* `use_sell_signal` (experimental)
* `sell_profit_only` (experimental)
* `ignore_roi_if_buy_signal` (experimental)
### Understand stake_amount ### Understand stake_amount
`stake_amount` is an amount of crypto-currency your bot will use for each trade. `stake_amount` 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 minimal value is 0.0005. If there is not enough crypto-currency in
the account an exception is generated. the account an exception is generated.
To allow the bot to trade all the avaliable `stake_currency` in your account set `stake_amount` = `unlimited`. To allow the bot to trade all the available `stake_currency` in your account set
In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`.
```json
"stake_amount" : "unlimited",
```
In this case a trade amount is calclulated as:
```python
currency_balanse / (max_open_trades - current_open_trades)
```
### Understand minimal_roi ### Understand minimal_roi
@ -86,7 +108,7 @@ In this case a trade amount is calclulated as `currency_balanse / (max_open_trad
in minutes and the value is the minimum ROI in percent. in minutes and the value is the minimum ROI in percent.
See the example below: See the example below:
``` ```json
"minimal_roi": { "minimal_roi": {
"40": 0.0, # Sell after 40 minutes if the profit is not negative "40": 0.0, # Sell after 40 minutes if the profit is not negative
"30": 0.01, # Sell after 30 minutes if there is at least 1% profit "30": 0.01, # Sell after 30 minutes if there is at least 1% profit
@ -144,10 +166,10 @@ end up paying more then would probably have been necessary.
### Understand order_types ### Understand order_types
`order_types` contains a dict mapping order-types to market-types as well as stoploss on or off exchange type. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled. `order_types` contains a dict mapping order-types to market-types as well as stoploss on or off exchange type and stoploss on exchange update interval in seconds. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled. In case stoploss on exchange and `trailing_stop` are both set, then the bot will use `stoploss_on_exchange_interval` to check it periodically and update it if necessary (e.x. in case of trailing stoploss).
This can be set in the configuration or in the strategy. Configuration overwrites strategy configurations. This can be set in the configuration or in the strategy. Configuration overwrites strategy configurations.
If this is configured, all 4 values (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`) need to be present, otherwise the bot warn about it and will fail to start. If this is configured, all 4 values (`"buy"`, `"sell"`, `"stoploss"` and `"stoploss_on_exchange"`) need to be present, otherwise the bot warn about it and will fail to start.
The below is the default which is used if this is not configured in either Strategy or configuration. The below is the default which is used if this is not configured in either Strategy or configuration.
```python ```python
@ -155,15 +177,20 @@ The below is the default which is used if this is not configured in either Strat
"buy": "limit", "buy": "limit",
"sell": "limit", "sell": "limit",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": False "stoploss_on_exchange": False,
"stoploss_on_exchange_interval": 60
}, },
``` ```
**NOTE**: Not all exchanges support "market" orders. !!! Note
Not all exchanges support "market" orders.
The following message will be shown if your exchange does not support market orders: `"Exchange <yourexchange> does not support market orders."` The following message will be shown if your exchange does not support market orders: `"Exchange <yourexchange> does not support market orders."`
!!! Note
stoploss on exchange interval is not mandatory. Do not change it's value if you are unsure of what you are doing. For more information about how stoploss works please read [the stoploss documentation](stoploss.md).
### Understand order_time_in_force ### Understand order_time_in_force
Order time in force defines the policy by which the order is executed on the exchange. Three commonly used time in force are:<br/> `order_time_in_force` defines the policy by which the order is executed on the exchange. Three commonly used time in force are:<br/>
**GTC (Goog Till Canceled):** **GTC (Goog Till Canceled):**
This is most of the time the default time in force. It means the order will remain on exchange till it is canceled by user. It can be fully or partially fulfilled. If partially fulfilled, the remaining will stay on the exchange till cancelled.<br/> This is most of the time the default time in force. It means the order will remain on exchange till it is canceled by user. It can be fully or partially fulfilled. If partially fulfilled, the remaining will stay on the exchange till cancelled.<br/>
**FOK (Full Or Kill):** **FOK (Full Or Kill):**
@ -179,7 +206,9 @@ possible values are: `gtc` (default), `fok` or `ioc`.<br/>
"sell": "gtc" "sell": "gtc"
}, },
``` ```
**NOTE**: This is an ongoing work. For now it is supported only for binance and only for buy orders. Please don't change the default value unless you know what you are doing.<br/>
!!! Warning
This is an ongoing work. For now it is supported only for binance and only for buy orders. Please don't change the default value unless you know what you are doing.
### What values for exchange.name? ### What values for exchange.name?
@ -198,9 +227,15 @@ Feel free to test other exchanges and submit your PR to improve the bot.
### What values for fiat_display_currency? ### What values for fiat_display_currency?
`fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram. `fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram.
The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD". The valid values are:<br/>
In addition to central bank currencies, a range of cryto currencies are supported. ```json
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT". "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD"
```
In addition to FIAT currencies, a range of cryto currencies are supported.
The valid values are:
```json
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
```
## Switch to dry-run mode ## Switch to dry-run mode
@ -209,14 +244,12 @@ behave and how is the performance of your strategy. In Dry-run mode the
bot does not engage your money. It only runs a live simulation without bot does not engage your money. It only runs a live simulation without
creating trades. creating trades.
### To switch your bot in Dry-run mode:
1. Edit your `config.json` file 1. Edit your `config.json` file
2. Switch dry-run to true and specify db_url for a persistent db 2. Switch dry-run to true and specify db_url for a persistent db
```json ```json
"dry_run": true, "dry_run": true,
"db_url": "sqlite///tradesv3.dryrun.sqlite", "db_url": "sqlite:///tradesv3.dryrun.sqlite",
``` ```
3. Remove your Exchange API key (change them by fake api credentials) 3. Remove your Exchange API key (change them by fake api credentials)
@ -238,9 +271,9 @@ production mode.
Dynamic pairlists select pairs for you based on the logic configured. 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. 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*, a Static Pairlist is used (configured as `"pair_whitelist"` under the `"exchange"` section of this configuration). By default, a Static Pairlist is used (configured as `"pair_whitelist"` under the `"exchange"` section of this configuration).
#### Available Pairlist methods **Available Pairlist methods:**
* `"StaticPairList"` * `"StaticPairList"`
* uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist` * uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`
@ -266,15 +299,15 @@ you run it in production mode.
### To switch your bot in production mode ### To switch your bot in production mode
1. Edit your `config.json` file **Edit your `config.json` file.**
2. Switch dry-run to false and don't forget to adapt your database URL if set **Switch dry-run to false and don't forget to adapt your database URL if set:**
```json ```json
"dry_run": false, "dry_run": false,
``` ```
3. Insert your Exchange API key (change them by fake api keys) **Insert your Exchange API key (change them by fake api keys):**
```json ```json
"exchange": { "exchange": {
@ -285,8 +318,8 @@ you run it in production mode.
} }
``` ```
!!! Note
If you have not your Bittrex API key yet, [see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md). If you have an exchange API key yet, [see our tutorial](/pre-requisite).
### Using proxy with FreqTrade ### Using proxy with FreqTrade
@ -337,4 +370,4 @@ Please ensure that 'NameOfStrategy' is identical to the strategy name!
## Next step ## Next step
Now you have configured your config.json, the next step is to [start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md). Now you have configured your config.json, the next step is to [start your bot](bot-usage.md).

View File

@ -4,8 +4,20 @@ This page is intended for developers of FreqTrade, people who want to contribute
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) where you can ask questions. All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) where you can ask questions.
## Documentation
## Module Documentation is available at [https://freqtrade.io](https://www.freqtrade.io/) and needs to be provided with every new feature PR.
Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/extensions/admonition/).
## Developer setup
To configure a development environment, use best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ".
Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -r requirements-dev.txt`.
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
## Modules
### Dynamic Pairlist ### Dynamic Pairlist
@ -68,3 +80,38 @@ Please also run `self._validate_whitelist(pairs)` and to check and remove pairs
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. 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.
## Creating a release
This part of the documentation is aimed at maintainers, and shows how to create a release.
### create release branch
``` bash
# make sure you're in develop branch
git checkout develop
# create new branch
git checkout -b new_release
```
* edit `freqtrade/__init__.py` and add the desired version (for example `0.18.0`)
* Commit this part
* push that branch to the remote and create a PR
### create changelog from git commits
``` bash
# Needs to be done before merging / pulling that branch.
git log --oneline --no-decorate --no-merges master..develop
```
### Create github release / tag
* Use the version-number specified as tag.
* Use "master" as reference (this step comes after the above PR is merged).
* use the above changelog as release comment (as codeblock)
### After-release
* update version in develop to next valid version and postfix that with `-dev` (`0.18.0 -> 0.18.1-dev`)

View File

@ -2,15 +2,11 @@
This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss. This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss.
**NOTICE:** Edge positioning is not compatible with dynamic whitelist. it overrides dynamic whitelist. !!! Warning
**NOTICE2:** Edge won't consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else will be ignored in its calculation. Edge positioning is not compatible with dynamic whitelist. it overrides dynamic whitelist.
## Table of Contents !!! Note
Edge won't consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else will be ignored in its calculation.
- [Introduction](#introduction)
- [How does it work?](#how-does-it-work?)
- [Configurations](#configurations)
- [Running Edge independently](#running-edge-independently)
## Introduction ## Introduction
Trading is all about probability. No one can claim that he has a strategy working all the time. You have to assume that sometimes you lose.<br/><br/> Trading is all about probability. No one can claim that he has a strategy working all the time. You have to assume that sometimes you lose.<br/><br/>
@ -28,7 +24,7 @@ The answer comes to two factors:
Means over X trades what is the percentage of winning trades to total number of trades (note that we don't consider how much you gained but only If you won or not). Means over X trades what is the percentage of winning trades to total number of trades (note that we don't consider how much you gained but only If you won or not).
`W = (Number of winning trades) / (Number of losing trades)` `W = (Number of winning trades) / (Total number of trades)`
### Risk Reward Ratio ### Risk Reward Ratio
Risk Reward Ratio is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose: Risk Reward Ratio is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose:

View File

@ -27,7 +27,7 @@ like pauses. You can stop your bot, adjust settings and start it again.
#### I want to improve the bot with a new strategy #### I want to improve the bot with a new strategy
That's great. We have a nice backtesting and hyperoptimizing setup. See That's great. We have a nice backtesting and hyperoptimizing setup. See
the tutorial [here|Testing-new-strategies-with-Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands). the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands).
#### Is there a setting to only SELL the coins being held and not #### Is there a setting to only SELL the coins being held and not
perform anymore BUYS? perform anymore BUYS?
@ -68,4 +68,3 @@ but it will give the idea. With only these triggers and guards there is
already 8*10^9*10 evaluations. A roughly total of 80 billion evals. already 8*10^9*10 evaluations. A roughly total of 80 billion evals.
Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th
of the search space. of the search space.

View File

@ -6,43 +6,50 @@ algorithms included in the `scikit-optimize` package to accomplish this. The
search will burn all your CPU cores, make your laptop sound like a fighter jet search will burn all your CPU cores, make your laptop sound like a fighter jet
and still take a long time. and still take a long time.
*Note:* Hyperopt will crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) !!! Bug
Hyperopt will crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133)
## Table of Contents
- [Prepare your Hyperopt](#prepare-hyperopt)
- [Configure your Guards and Triggers](#configure-your-guards-and-triggers)
- [Solving a Mystery](#solving-a-mystery)
- [Adding New Indicators](#adding-new-indicators)
- [Execute Hyperopt](#execute-hyperopt)
- [Understand the hyperopt result](#understand-the-hyperopt-result)
## Prepare Hyperopting ## Prepare Hyperopting
Before we start digging in Hyperopt, we recommend you to take a look at Before we start digging into Hyperopt, we recommend you to take a look at
an example hyperopt file located into [user_data/hyperopts/](https://github.com/gcarq/freqtrade/blob/develop/user_data/hyperopts/test_hyperopt.py) an example hyperopt file located into [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/test_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.
### Checklist on all tasks / possibilities in hyperopt
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 `indicator_space` - for buy signal optimzation
* fill `sell_strategy_generator` - for sell signal optimization
* fill `sell_indicator_space` - for sell signal optimzation
* fill `roi_space` - for ROI optimization
* fill `generate_roi_table` - for ROI optimization (if you need more than 3 entries)
* fill `stoploss_space` - stoploss optimization
* Optional but recommended
* 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
### 1. Install a Custom Hyperopt File ### 1. Install a Custom Hyperopt File
This is very simple. Put your hyperopt file into the folder
`user_data/hyperopts`. Put your hyperopt file into the folder`user_data/hyperopts`.
Let assume you want a hyperopt file `awesome_hyperopt.py`: Let assume you want a hyperopt file `awesome_hyperopt.py`:
1. Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py` Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py`
### 2. Configure your Guards and Triggers ### 2. Configure your Guards and Triggers
There are two places you need to change in your hyperopt file to add a
new buy hyperopt for testing: There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing:
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/test_hyperopt.py#L230-L251).
- Inside [indicator_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/test_hyperopt.py#L207-L223). - Inside `indicator_space()` - the parameters hyperopt shall be optimizing.
- Inside `populate_buy_trend()` - applying the parameters.
There you have two different types of indicators: 1. `guards` and 2. `triggers`. There you have two different types of indicators: 1. `guards` and 2. `triggers`.
1. Guards are conditions like "never buy if ADX < 10", or never buy if 1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10.
current price is over EMA10. 2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower bollinger band".
2. Triggers are ones that actually trigger buy in specific moment, like
"buy when EMA5 crosses over EMA10" or "buy when close price touches lower
bollinger band".
Hyperoptimization will, for each eval round, pick one trigger and possibly Hyperoptimization will, for each eval round, pick one trigger and possibly
multiple guards. The constructed strategy will be something like multiple guards. The constructed strategy will be something like
@ -53,6 +60,17 @@ If you have updated the buy strategy, ie. changed the contents of
`populate_buy_trend()` method you have to update the `guards` and `populate_buy_trend()` method you have to update the `guards` and
`triggers` hyperopts must use. `triggers` hyperopts must use.
#### Sell optimization
Similar to the buy-signal above, sell-signals can also be optimized.
Place the corresponding settings into the following methods
* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing.
* Inside `populate_sell_trend()` - applying the parameters.
The configuration and rules are the same than for buy signals.
To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`.
## Solving a Mystery ## Solving a Mystery
Let's say you are curious: should you use MACD crossings or lower Bollinger Let's say you are curious: should you use MACD crossings or lower Bollinger
@ -63,7 +81,7 @@ mystery.
We will start by defining a search space: We will start by defining a search space:
``` ```python
def indicator_space() -> List[Dimension]: def indicator_space() -> List[Dimension]:
""" """
Define your Hyperopt space for searching strategy parameters Define your Hyperopt space for searching strategy parameters
@ -86,7 +104,7 @@ one we call `trigger` and use it to decide which buy trigger we want to use.
So let's write the buy strategy using these values: So let's write the buy strategy using these values:
``` ``` python
def populate_buy_trend(dataframe: DataFrame) -> DataFrame: def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
conditions = [] conditions = []
# GUARDS AND TRENDS # GUARDS AND TRENDS
@ -96,6 +114,7 @@ So let's write the buy strategy using these values:
conditions.append(dataframe['rsi'] < params['rsi-value']) conditions.append(dataframe['rsi'] < params['rsi-value'])
# TRIGGERS # TRIGGERS
if 'trigger' in params:
if params['trigger'] == 'bb_lower': if params['trigger'] == 'bb_lower':
conditions.append(dataframe['close'] < dataframe['bb_lowerband']) conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if params['trigger'] == 'macd_cross_signal': if params['trigger'] == 'macd_cross_signal':
@ -133,15 +152,19 @@ 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
python3 ./freqtrade/main.py -s <strategyname> --hyperopt <hyperoptname> -c config.json hyperopt -e 5000 python3 ./freqtrade/main.py --hyperopt <hyperoptname> -c config.json hyperopt -e 5000 --spaces all
``` ```
Use `<strategyname>` and `<hyperoptname>` as the names of the custom strategy Use `<hyperoptname>` as the name of the custom hyperopt used.
(only required for generating sells) and the custom hyperopt used.
The `-e` flag will set how many evaluations hyperopt will do. We recommend The `-e` flag 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.
!!! Warning
When switching parameters or changing configuration options, the file `user_data/hyperopt_results.pickle` should be removed. It's used to be able to continue interrupted calculations, but does not detect changes to settings or the hyperopt file.
### Execute Hyperopt with Different Ticker-Data Source ### Execute Hyperopt with Different Ticker-Data Source
If you would like to hyperopt parameters using an alternate ticker data that If you would like to hyperopt parameters using an alternate ticker data that
@ -170,6 +193,7 @@ Legal values are:
- `all`: optimize everything - `all`: optimize everything
- `buy`: just search for a new buy strategy - `buy`: just search for a new buy 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
- 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`
@ -183,7 +207,11 @@ Given the following result from hyperopt:
Best result: Best result:
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
with values: with values:
{'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'bb_lower'} { 'adx-value': 44,
'rsi-value': 29,
'adx-enabled': False,
'rsi-enabled': True,
'trigger': 'bb_lower'}
``` ```
You should understand this result like: You should understand this result like:
@ -223,9 +251,24 @@ If you are optimizing ROI, you're result will look as follows and include a ROI
Best result: Best result:
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
with values: with values:
{'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'bb_lower', 'roi_t1': 40, 'roi_t2': 57, 'roi_t3': 21, 'roi_p1': 0.03634636907306948, 'roi_p2': 0.055237357937802885, 'roi_p3': 0.015163796015548354, 'stoploss': -0.37996664668703606} { 'adx-value': 44,
'rsi-value': 29,
'adx-enabled': false,
'rsi-enabled': True,
'trigger': 'bb_lower',
'roi_t1': 40,
'roi_t2': 57,
'roi_t3': 21,
'roi_p1': 0.03634636907306948,
'roi_p2': 0.055237357937802885,
'roi_p3': 0.015163796015548354,
'stoploss': -0.37996664668703606
}
ROI table: ROI table:
{0: 0.10674752302642071, 21: 0.09158372701087236, 78: 0.03634636907306948, 118: 0} { 0: 0.10674752302642071,
21: 0.09158372701087236,
78: 0.03634636907306948,
118: 0}
``` ```
This would translate to the following ROI table: This would translate to the following ROI table:
@ -245,9 +288,10 @@ Once the optimized strategy has been implemented into your strategy, you should
To archive the same results (number of trades, ...) than during hyperopt, please use the command line flag `--disable-max-market-positions`. To archive the same results (number of trades, ...) than during hyperopt, please use the command line flag `--disable-max-market-positions`.
This setting is the default for hyperopt for speed reasons. You can overwrite this in the configuration by setting `"position_stacking"=false` or by changing the relevant line in your hyperopt file [here](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L283). This setting is the default for hyperopt for speed reasons. You can overwrite this in the configuration by setting `"position_stacking"=false` or by changing the relevant line in your hyperopt file [here](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L283).
!!! Note:
Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality.
## Next Step ## Next Step
Now you have a perfect bot and want to control it from Telegram. Your Now you have a perfect bot and want to control it from Telegram. Your
next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md). next step is to learn the [Telegram usage](telegram-usage.md).

BIN
docs/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,39 +1,67 @@
# freqtrade documentation # Freqtrade
[![Build Status](https://travis-ci.org/freqtrade/freqtrade.svg?branch=develop)](https://travis-ci.org/freqtrade/freqtrade)
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
Welcome to freqtrade documentation. Please feel free to contribute to <!-- Place this tag where you want the button to render. -->
this documentation if you see it became outdated by sending us a <a class="github-button" href="https://github.com/freqtrade/freqtrade" data-icon="octicon-star" data-size="large" aria-label="Star freqtrade/freqtrade on GitHub">Star</a>
Pull-request. Do not hesitate to reach us on <!-- Place this tag where you want the button to render. -->
[Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) <a class="github-button" href="https://github.com/freqtrade/freqtrade/fork" data-icon="octicon-repo-forked" data-size="large" aria-label="Fork freqtrade/freqtrade on GitHub">Fork</a>
if you do not find the answer to your questions. <!-- Place this tag where you want the button to render. -->
<a class="github-button" href="https://github.com/freqtrade/freqtrade/archive/master.zip" data-icon="octicon-cloud-download" data-size="large" aria-label="Download freqtrade/freqtrade on GitHub">Download</a>
<!-- Place this tag where you want the button to render. -->
<a class="github-button" href="https://github.com/freqtrade" data-size="large" aria-label="Follow @freqtrade on GitHub">Follow @freqtrade</a>
## Introduction
Freqtrade is a cryptocurrency trading bot written in Python.
## Table of Contents !!! Danger "DISCLAIMER"
This software is for educational purposes only. Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
- [Pre-requisite](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md) Always start by running a trading bot in Dry-run and do not engage money before you understand how it works and what profit/loss you should expect.
- [Setup your Bittrex account](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
- [Setup your Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot) We strongly recommend you to have coding and Python knowledge. Do not hesitate to read the source code and understand the mechanism of this bot.
- [Bot Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
- [Install with Docker (all platforms)](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#docker)
- [Install on Linux Ubuntu](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#21-linux---ubuntu-1604) ## Features
- [Install on MacOS](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#23-macos-installation) - Based on Python 3.6+: For botting on any operating system - Windows, macOS and Linux
- [Install on Windows](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#windows) - Persistence: Persistence is achieved through sqlite
- [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) - Dry-run: Run the bot without playing money.
- [Bot usage (Start your bot)](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md) - Backtesting: Run a simulation of your buy/sell strategy.
- [Bot commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands) - Strategy Optimization by machine learning: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
- [Backtesting commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands) - Edge position sizing Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. Learn more
- [Hyperopt commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands) - Whitelist crypto-currencies: Select which crypto-currency you want to trade or use dynamic whitelists.
- [Edge commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#edge-commands) - Blacklist crypto-currencies: Select which crypto-currency you want to avoid.
- [Bot Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md) - Manageable via Telegram: Manage the bot with Telegram
- [Change your strategy](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#change-your-strategy) - Display profit/loss in fiat: Display your profit/loss in 33 fiat.
- [Add more Indicator](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#add-more-indicator) - Daily summary of profit/loss: Provide a daily summary of your profit/loss.
- [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) - Performance status report: Provide a performance status of your current trades.
- [Edge positioning](https://github.com/freqtrade/freqtrade/blob/develop/docs/edge.md)
- [Find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
- [Control the bot with telegram](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md) ## Requirements
- [Receive notifications via webhook](https://github.com/freqtrade/freqtrade/blob/develop/docs/webhook-config.md) ### Uptodate clock
- [Contribute to the project](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) The clock must be accurate, syncronized to a NTP server very frequently to avoid problems with communication to the exchanges.
- [How to contribute](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
- [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) ### Hardware requirements
- [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md) To run this bot we recommend you a cloud instance with a minimum of:
- [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md)
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md) - 2GB RAM
- [Developer Docs](https://github.com/freqtrade/freqtrade/blob/develop/docs/developer.md) - 1GB disk space
- 2vCPU
### Software requirements
- Python 3.6.x
- pip
- git
- TA-Lib
- virtualenv (Recommended)
- Docker (Recommended)
## Support
Help / Slack
For any questions not covered by the documentation or for further information about the bot, we encourage you to join our slack channel.
Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) to join Slack channel.
## Ready to try?
Begin by reading our installation guide [here](installation).

View File

@ -1,25 +1,69 @@
# Installation # Installation
This page explains how to prepare your environment for running the bot. This page explains how to prepare your environment for running the bot.
To understand how to set up the bot please read the [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) page. ## Prerequisite
Before running your bot in production you will need to setup few
external API. In production mode, the bot required valid Bittrex API
credentials and a Telegram bot (optional but recommended).
## Table of Contents - [Setup your exchange account](#setup-your-exchange-account)
- [Backtesting commands](#setup-your-telegram-bot)
* [Table of Contents](#table-of-contents) ### Setup your exchange account
* [Easy Installation - Linux Script](#easy-installation---linux-script) *To be completed, please feel free to complete this section.*
* [Automatic Installation - Docker](#automatic-installation---docker)
* [Custom Linux MacOS Installation](#custom-installation)
- [Requirements](#requirements)
- [Linux - Ubuntu 16.04](#linux---ubuntu-1604)
- [MacOS](#macos)
- [Setup Config and virtual env](#setup-config-and-virtual-env)
* [Windows](#windows)
<!-- /TOC --> ### Setup your Telegram bot
The only things you need is a working Telegram bot and its API token.
Below we explain how to create your Telegram Bot, and how to get your
Telegram user id.
------ ### 1. Create your Telegram bot
**1.1. Start a chat with https://telegram.me/BotFather**
**1.2. Send the message `/newbot`. ** *BotFather response:*
```
Alright, a new bot. How are we going to call it? Please choose a name for your bot.
```
**1.3. Choose the public name of your bot (e.x. `Freqtrade bot`)**
*BotFather response:*
```
Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot.
```
**1.4. Choose the name id of your bot (e.x "`My_own_freqtrade_bot`")**
**1.5. Father bot will return you the token (API key)**<br/>
Copy it and keep it you will use it for the config parameter `token`.
*BotFather response:*
```hl_lines="4"
Done! Congratulations on your new bot. You will find it at t.me/My_own_freqtrade_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this.
Use this token to access the HTTP API:
521095879:AAEcEZEL7ADJ56FtG_qD0bQJSKETbXCBCi0
For a description of the Bot API, see this page: https://core.telegram.org/bots/api
```
**1.6. Don't forget to start the conversation with your bot, by clicking /START button**
### 2. Get your user id
**2.1. Talk to https://telegram.me/userinfobot**
**2.2. Get your "Id", you will use it for the config parameter
`chat_id`.**
<hr/>
## Quick start
Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot.
```bash
git clone git@github.com:freqtrade/freqtrade.git
cd freqtrade
git checkout develop
./setup.sh --install
```
!!! Note
Windows installation is explained [here](#windows).
<hr/>
## Easy Installation - Linux Script ## Easy Installation - Linux Script
If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot. If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot.
@ -33,7 +77,7 @@ usage:
-c,--config Easy config generator (Will override your existing file). -c,--config Easy config generator (Will override your existing file).
``` ```
### --install ** --install **
This script will install everything you need to run the bot: This script will install everything you need to run the bot:
@ -43,15 +87,15 @@ This script will install everything you need to run the bot:
This script is a combination of `install script` `--reset`, `--config` This script is a combination of `install script` `--reset`, `--config`
### --update ** --update **
Update parameter will pull the last version of your current branch and update your virtualenv. Update parameter will pull the last version of your current branch and update your virtualenv.
### --reset ** --reset **
Reset parameter will hard reset your branch (only if you are on `master` or `develop`) and recreate your virtualenv. Reset parameter will hard reset your branch (only if you are on `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`. Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`.
@ -69,33 +113,39 @@ Once you have Docker installed, simply create the config file (e.g. `config.json
### 1. Prepare the Bot ### 1. Prepare the Bot
#### 1.1. Clone the git repository **1.1. Clone the git repository**
Linux/Mac/Windows with WSL
```bash ```bash
git clone https://github.com/freqtrade/freqtrade.git git clone https://github.com/freqtrade/freqtrade.git
``` ```
#### 1.2. (Optional) Checkout the develop branch Windows with docker
```bash
git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git
```
**1.2. (Optional) Checkout the develop branch**
```bash ```bash
git checkout develop git checkout develop
``` ```
#### 1.3. Go into the new directory **1.3. Go into the new directory**
```bash ```bash
cd freqtrade cd freqtrade
``` ```
#### 1.4. Copy `config.json.example` to `config.json` **1.4. Copy `config.json.example` to `config.json`**
```bash ```bash
cp -n config.json.example config.json cp -n config.json.example config.json
``` ```
> To edit the config please refer to the [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) page. > To edit the config please refer to the [Bot Configuration](configuration.md) page.
#### 1.5. Create your database file *(optional - the bot will create it if it is missing)* **1.5. Create your database file *(optional - the bot will create it if it is missing)**
Production Production
@ -115,7 +165,7 @@ Either use the prebuilt image from docker hub - or build the image yourself if y
Branches / tags available can be checked out on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). Branches / tags available can be checked out on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/).
#### 2.1. Download the docker image **2.1. Download the docker image**
Pull the image from docker hub and (optionally) change the name of the image Pull the image from docker hub and (optionally) change the name of the image
@ -127,7 +177,7 @@ docker tag freqtradeorg/freqtrade:develop freqtrade
To update the image, simply run the above commands again and restart your running container. To update the image, simply run the above commands again and restart your running container.
#### 2.2. Build the Docker image **2.2. Build the Docker image**
```bash ```bash
cd freqtrade cd freqtrade
@ -164,7 +214,7 @@ There is known issue in OSX Docker versions after 17.09.1, whereby /etc/localtim
docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
``` ```
More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396) More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396).
In this example, the database will be created inside the docker instance and will be lost when you will refresh your image. In this example, the database will be created inside the docker instance and will be lost when you will refresh your image.
@ -172,7 +222,7 @@ In this example, the database will be created inside the docker instance and wil
To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem).
#### 5.1. Move your config file and database **5.1. Move your config file and database**
```bash ```bash
mkdir ~/.freqtrade mkdir ~/.freqtrade
@ -180,7 +230,7 @@ mv config.json ~/.freqtrade
mv tradesv3.sqlite ~/.freqtrade mv tradesv3.sqlite ~/.freqtrade
``` ```
#### 5.2. Run the docker image **5.2. Run the docker image**
```bash ```bash
docker run -d \ docker run -d \
@ -191,7 +241,8 @@ docker run -d \
freqtrade --db-url sqlite:///tradesv3.sqlite freqtrade --db-url sqlite:///tradesv3.sqlite
``` ```
*Note*: db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used. !!! Note
db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used.
To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite` To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite`
### 6. Monitor your Docker instance ### 6. Monitor your Docker instance
@ -208,7 +259,8 @@ docker start freqtrade
For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/).
*Note*: You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. !!! Note
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
### 7. Backtest with docker ### 7. Backtest with docker
@ -225,16 +277,17 @@ docker run -d \
freqtrade --strategy AwsomelyProfitableStrategy backtesting freqtrade --strategy AwsomelyProfitableStrategy backtesting
``` ```
Head over to the [Backtesting Documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) for more details. Head over to the [Backtesting Documentation](backtesting.md) for more details.
*Note*: Additional parameters can be appended after the image name (`freqtrade` in the above example). !!! Note
Additional parameters can be appended after the image name (`freqtrade` in the above example).
------ ------
## Custom Installation ## Custom Installation
We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros. We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros.
OS Specific steps are listed first, the [common](#common) section below is necessary for all systems. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems.
### Requirements ### Requirements
@ -286,7 +339,7 @@ python3 -m pip install -e .
brew install python3 git wget brew install python3 git wget
``` ```
### common ### Common
#### 1. Install TA-Lib #### 1. Install TA-Lib
@ -304,11 +357,13 @@ cd ..
rm -rf ./ta-lib* rm -rf ./ta-lib*
``` ```
*Note*: An already downloaded version of ta-lib is included in the repository, as the sourceforge.net source seems to have problems frequently. !!! Note
An already downloaded version of ta-lib is included in the repository, as the sourceforge.net source seems to have problems frequently.
#### 2. Setup your Python virtual environment (virtualenv) #### 2. Setup your Python virtual environment (virtualenv)
*Note*: This step is optional but strongly recommended to keep your system organized !!! Note
This step is optional but strongly recommended to keep your system organized
```bash ```bash
python3 -m venv .env python3 -m venv .env
@ -337,7 +392,7 @@ cd freqtrade
cp config.json.example config.json cp config.json.example config.json
``` ```
> *To edit the config please refer to [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md).* > *To edit the config please refer to [Bot Configuration](configuration.md).*
#### 5. Install python dependencies #### 5. Install python dependencies
@ -396,7 +451,7 @@ copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of inofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib0.4.17cp36cp36mwin32.whl` (make sure to use the version matching your python version) As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib0.4.17cp36cp36mwin32.whl` (make sure to use the version matching your python version)
```cmd ```cmd
>cd \path\freqtrade-develop >cd \path\freqtrade-develop
@ -426,4 +481,4 @@ The easiest way is to download install Microsoft Visual Studio Community [here](
--- ---
Now you have an environment ready, the next step is Now you have an environment ready, the next step is
[Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)... [Bot Configuration](configuration.md).

52
docs/partials/header.html Normal file
View File

@ -0,0 +1,52 @@
<header class="md-header" data-md-component="header">
<nav class="md-header-nav md-grid">
<div class="md-flex">
<div class="md-flex__cell md-flex__cell--shrink">
<a href="{{ config.site_url | default(nav.homepage.url, true) | url }}" title="{{ config.site_name }}"
class="md-header-nav__button md-logo">
{% if config.theme.logo.icon %}
<i class="md-icon">{{ config.theme.logo.icon }}</i>
{% else %}
<img src="{{ config.theme.logo | url }}" width="24" height="24">
{% endif %}
</a>
</div>
<div class="md-flex__cell md-flex__cell--shrink">
<label class="md-icon md-icon--menu md-header-nav__button" for="__drawer"></label>
</div>
<div class="md-flex__cell md-flex__cell--stretch">
<div class="md-flex__ellipsis md-header-nav__title" data-md-component="title">
{% block site_name %}
{% if config.site_name == page.title %}
{{ config.site_name }}
{% else %}
<span class="md-header-nav__topic">
{{ config.site_name }}
</span>
<span class="md-header-nav__topic">
{{ page.title }}
</span>
{% endif %}
{% endblock %}
</div>
</div>
<div class="md-flex__cell md-flex__cell--shrink">
{% block search_box %}
{% if "search" in config["plugins"] %}
<label class="md-icon md-icon--search md-header-nav__button" for="__search"></label>
{% include "partials/search.html" %}
{% endif %}
{% endblock %}
</div>
{% if config.repo_url %}
<div class="md-flex__cell md-flex__cell--shrink">
<div class="md-header-nav__source">
{% include "partials/source.html" %}
</div>
</div>
{% endif %}
</div>
</nav>
<!-- Place this tag in your head or just before your close body tag. -->
<script async defer src="https://buttons.github.io/buttons.js"></script>
</header>

View File

@ -1,10 +1,6 @@
# Plotting # Plotting
This page explains how to plot prices, indicator, profits. This page explains how to plot prices, indicator, profits.
## Table of Contents
- [Plot price and indicators](#plot-price-and-indicators)
- [Plot profit](#plot-profit)
## Installation ## Installation
Plotting scripts use Plotly library. Install/upgrade it with: Plotting scripts use Plotly library. Install/upgrade it with:
@ -19,7 +15,7 @@ At least version 2.3.0 is required.
Usage for the price plotter: Usage for the price plotter:
``` ```
script/plot_dataframe.py [-h] [-p pair] [--live] script/plot_dataframe.py [-h] [-p pairs] [--live]
``` ```
Example Example
@ -27,11 +23,16 @@ Example
python scripts/plot_dataframe.py -p BTC/ETH python scripts/plot_dataframe.py -p BTC/ETH
``` ```
The `-p` pair argument, can be used to specify what The `-p` pairs argument, can be used to specify
pair you would like to plot. pairs you would like to plot.
**Advanced use** **Advanced use**
To plot multiple pairs, separate them with a comma:
```
python scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH
```
To plot the current live price use the `--live` flag: To plot the current live price use the `--live` flag:
``` ```
python scripts/plot_dataframe.py -p BTC/ETH --live python scripts/plot_dataframe.py -p BTC/ETH --live

View File

@ -1,48 +0,0 @@
# Pre-requisite
Before running your bot in production you will need to setup few
external API. In production mode, the bot required valid Bittrex API
credentials and a Telegram bot (optional but recommended).
## Table of Contents
- [Setup your Bittrex account](#setup-your-bittrex-account)
- [Backtesting commands](#setup-your-telegram-bot)
## Setup your Bittrex account
*To be completed, please feel free to complete this section.*
## Setup your Telegram bot
The only things you need is a working Telegram bot and its API token.
Below we explain how to create your Telegram Bot, and how to get your
Telegram user id.
### 1. Create your Telegram bot
**1.1. Start a chat with https://telegram.me/BotFather**
**1.2. Send the message** `/newbot`
*BotFather response:*
```
Alright, a new bot. How are we going to call it? Please choose a name for your bot.
```
**1.3. Choose the public name of your bot (e.g "`Freqtrade bot`")**
*BotFather response:*
```
Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot.
```
**1.4. Choose the name id of your bot (e.g "`My_own_freqtrade_bot`")**
**1.5. Father bot will return you the token (API key)**
Copy it and keep it you will use it for the config parameter `token`.
*BotFather response:*
```
Done! Congratulations on your new bot. You will find it at t.me/My_own_freqtrade_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this.
Use this token to access the HTTP API:
521095879:AAEcEZEL7ADJ56FtG_qD0bQJSKETbXCBCi0
For a description of the Bot API, see this page: https://core.telegram.org/bots/api
```
**1.6. Don't forget to start the conversation with your bot, by clicking /START button**
### 2. Get your user id
**2.1. Talk to https://telegram.me/userinfobot**
**2.2. Get your "Id", you will use it for the config parameter
`chat_id`.**

View File

@ -0,0 +1 @@
mkdocs-material==3.1.0

View File

@ -2,9 +2,20 @@
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, defined in either the strategy or configuration.
2. trailing stop loss, defined in the configuration 2. trailing stop loss, defined in the configuration.
3. trailing stop loss, custom positive loss, defined in configuration 3. trailing stop loss, custom positive loss, defined in configuration.
!!! Note
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. 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).
!!! Note
Stoploss on exchange is only supported for Binance as of now.
## Static Stop Loss ## Static Stop Loss

View File

@ -2,9 +2,9 @@
This page explains how to command your bot with Telegram. This page explains how to command your bot with Telegram.
## Pre-requisite ## Prerequisite
To control your bot with Telegram, you need first to To control your bot with Telegram, you need first to
[set up a Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md) [set up a Telegram bot](installation.md)
and add your Telegram API keys into your config file. and add your Telegram API keys into your config file.
## Telegram commands ## Telegram commands
@ -146,4 +146,3 @@ Day Profit BTC Profit USD
## /version ## /version
> **Version:** `0.14.3` > **Version:** `0.14.3`

View File

@ -1,5 +1,5 @@
""" FreqTrade bot """ """ FreqTrade bot """
__version__ = '0.18.0' __version__ = '0.18.1'
class DependencyException(BaseException): class DependencyException(BaseException):

View File

@ -272,7 +272,7 @@ class Arguments(object):
'-s', '--spaces', '-s', '--spaces',
help='Specify which parameters to hyperopt. Space separate list. \ help='Specify which parameters to hyperopt. Space separate list. \
Default: %(default)s', Default: %(default)s',
choices=['all', 'buy', 'roi', 'stoploss'], choices=['all', 'buy', 'sell', 'roi', 'stoploss'],
default='all', default='all',
nargs='+', nargs='+',
dest='spaces', dest='spaces',
@ -352,9 +352,9 @@ class Arguments(object):
Parses given arguments for scripts. Parses given arguments for scripts.
""" """
self.parser.add_argument( self.parser.add_argument(
'-p', '--pair', '-p', '--pairs',
help='Show profits for only this pairs. Pairs are comma-separated.', help='Show profits for only this pairs. Pairs are comma-separated.',
dest='pair', dest='pairs',
default=None default=None
) )

View File

@ -12,6 +12,7 @@ from jsonschema import Draft4Validator, validate
from jsonschema.exceptions import ValidationError, best_match from jsonschema.exceptions import ValidationError, best_match
from freqtrade import OperationalException, constants from freqtrade import OperationalException, constants
from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -34,9 +35,10 @@ class Configuration(object):
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
""" """
def __init__(self, args: Namespace) -> None: def __init__(self, args: Namespace, runmode: RunMode = None) -> None:
self.args = args self.args = args
self.config: Optional[Dict[str, Any]] = None self.config: Optional[Dict[str, Any]] = None
self.runmode = runmode
def load_config(self) -> Dict[str, Any]: def load_config(self) -> Dict[str, Any]:
""" """
@ -68,6 +70,13 @@ class Configuration(object):
# Load Hyperopt # Load Hyperopt
config = self._load_hyperopt_config(config) config = self._load_hyperopt_config(config)
# Set runmode
if not self.runmode:
# Handle real mode, infer dry/live from config
self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE
config.update({'runmode': self.runmode})
return config return config
def _load_config_file(self, path: str) -> Dict[str, Any]: def _load_config_file(self, path: str) -> Dict[str, Any]:
@ -124,9 +133,6 @@ class Configuration(object):
if self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL: if self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL:
config.update({'db_url': self.args.db_url}) config.update({'db_url': self.args.db_url})
logger.info('Parameter --db-url detected ...') logger.info('Parameter --db-url detected ...')
else:
# Set default here
config.update({'db_url': constants.DEFAULT_DB_PROD_URL})
if config.get('dry_run', False): if config.get('dry_run', False):
logger.info('Dry run is enabled') logger.info('Dry run is enabled')
@ -152,13 +158,16 @@ class Configuration(object):
return config return config
def _create_default_datadir(self, config: Dict[str, Any]) -> str: def _create_datadir(self, config: Dict[str, Any], datadir: Optional[str] = None) -> str:
if not datadir:
# set datadir
exchange_name = config.get('exchange', {}).get('name').lower() exchange_name = config.get('exchange', {}).get('name').lower()
default_path = os.path.join('user_data', 'data', exchange_name) datadir = os.path.join('user_data', 'data', exchange_name)
if not os.path.isdir(default_path):
os.makedirs(default_path) if not os.path.isdir(datadir):
logger.info(f'Created data directory: {default_path}') os.makedirs(datadir)
return default_path logger.info(f'Created data directory: {datadir}')
return datadir
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]: def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
""" """
@ -198,9 +207,9 @@ class Configuration(object):
# If --datadir is used we add it to the configuration # If --datadir is used we add it to the configuration
if 'datadir' in self.args and self.args.datadir: if 'datadir' in self.args and self.args.datadir:
config.update({'datadir': self.args.datadir}) config.update({'datadir': self._create_datadir(config, self.args.datadir)})
else: else:
config.update({'datadir': self._create_default_datadir(config)}) config.update({'datadir': self._create_datadir(config, None)})
logger.info('Using data folder: %s ...', config.get('datadir')) logger.info('Using data folder: %s ...', config.get('datadir'))
# If -r/--refresh-pairs-cached is used we add it to the configuration # If -r/--refresh-pairs-cached is used we add it to the configuration

View File

@ -13,6 +13,7 @@ DEFAULT_HYPEROPT = 'DefaultHyperOpts'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite://' DEFAULT_DB_DRYRUN_URL = 'sqlite://'
UNLIMITED_STAKE_AMOUNT = 'unlimited' UNLIMITED_STAKE_AMOUNT = 'unlimited'
DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05
REQUIRED_ORDERTIF = ['buy', 'sell'] 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']
@ -112,7 +113,8 @@ CONF_SCHEMA = {
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss_on_exchange': {'type': 'boolean'} 'stoploss_on_exchange': {'type': 'boolean'},
'stoploss_on_exchange_interval': {'type': 'number'}
}, },
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] 'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
}, },

View File

@ -5,13 +5,19 @@ import logging
import pandas as pd import pandas as pd
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
from freqtrade.constants import TICKER_INTERVAL_MINUTES
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def parse_ticker_dataframe(ticker: list) -> DataFrame: def parse_ticker_dataframe(ticker: list, ticker_interval: str,
fill_missing: 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 fill_missing: fill up missing candles with 0 candles
(see ohlcv_fill_up_missing_data for details)
:return: DataFrame :return: DataFrame
""" """
logger.debug("Parsing tickerlist to dataframe") logger.debug("Parsing tickerlist to dataframe")
@ -23,6 +29,12 @@ def parse_ticker_dataframe(ticker: list) -> DataFrame:
utc=True, utc=True,
infer_datetime_format=True) infer_datetime_format=True)
# Some exchanges return int values for volume and even for ohlc.
# Convert them since TA-LIB indicators used in the strategy assume floats
# and fail with exception...
frame = frame.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float',
'volume': 'float'})
# group by index and aggregate results to eliminate duplicate ticks # group by index and aggregate results to eliminate duplicate ticks
frame = frame.groupby(by='date', as_index=False, sort=True).agg({ frame = frame.groupby(by='date', as_index=False, sort=True).agg({
'open': 'first', 'open': 'first',
@ -33,9 +45,43 @@ def parse_ticker_dataframe(ticker: list) -> DataFrame:
}) })
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
logger.debug('Dropping last candle') logger.debug('Dropping last candle')
if fill_missing:
return ohlcv_fill_up_missing_data(frame, ticker_interval)
else:
return frame return frame
def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str) -> DataFrame:
"""
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
"""
ohlc_dict = {
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
}
tick_mins = TICKER_INTERVAL_MINUTES[ticker_interval]
# Resample to create "NAN" values
df = dataframe.resample(f'{tick_mins}min', on='date').agg(ohlc_dict)
# Forwardfill close for missing columns
df['close'] = df['close'].fillna(method='ffill')
# Use close for "open, high, low"
df.loc[:, ['open', 'high', 'low']] = df[['open', 'high', 'low']].fillna(
value={'open': df['close'],
'high': df['close'],
'low': df['close'],
})
df.reset_index(inplace=True)
logger.debug(f"Missing data fillup: before: {len(dataframe)} - after: {len(df)}")
return df
def order_book_to_dataframe(bids: list, asks: list) -> DataFrame: def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
""" """
Gets order book list, returns dataframe with below format per suggested by creslin Gets order book list, returns dataframe with below format per suggested by creslin

View File

@ -0,0 +1,97 @@
"""
Dataprovider
Responsible to provide data to the bot
including Klines, tickers, historic data
Common Interface for bot and strategy to access data.
"""
import logging
from pathlib import Path
from typing import List, Tuple
from pandas import DataFrame
from freqtrade.data.history import load_pair_history
from freqtrade.exchange import Exchange
from freqtrade.state import RunMode
logger = logging.getLogger(__name__)
class DataProvider(object):
def __init__(self, config: dict, exchange: Exchange) -> None:
self._config = config
self._exchange = exchange
def refresh(self,
pairlist: List[Tuple[str, str]],
helping_pairs: List[Tuple[str, str]] = None) -> None:
"""
Refresh data, called with each cycle
"""
if helping_pairs:
self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs)
else:
self._exchange.refresh_latest_ohlcv(pairlist)
@property
def available_pairs(self) -> List[Tuple[str, str]]:
"""
Return a list of tuples containing pair, tick_interval for which data is currently cached.
Should be whitelist + open trades.
"""
return list(self._exchange._klines.keys())
def ohlcv(self, pair: str, tick_interval: str = None, copy: bool = True) -> DataFrame:
"""
get ohlcv data for the given pair as DataFrame
Please check `available_pairs` to verify which pairs are currently cached.
:param pair: pair to get the data for
:param tick_interval: ticker_interval to get pair for
:param copy: copy dataframe before returning.
Use false only for RO operations (where the dataframe is not modified)
"""
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
if tick_interval:
pairtick = (pair, tick_interval)
else:
pairtick = (pair, self._config['ticker_interval'])
return self._exchange.klines(pairtick, copy=copy)
else:
return DataFrame()
def historic_ohlcv(self, pair: str, ticker_interval: str) -> DataFrame:
"""
get stored historic ohlcv data
:param pair: pair to get the data for
:param tick_interval: ticker_interval to get pair for
"""
return load_pair_history(pair=pair,
ticker_interval=ticker_interval,
refresh_pairs=False,
datadir=Path(self._config['datadir']) if self._config.get(
'datadir') else None
)
def ticker(self, pair: str):
"""
Return last ticker data
"""
# TODO: Implement me
pass
def orderbook(self, pair: str, max: int):
"""
return latest orderbook data
"""
# TODO: Implement me
pass
@property
def runmode(self) -> RunMode:
"""
Get runmode of the bot
can be "live", "dry-run", "backtest", "edgecli", "hyperopt" or "other".
"""
return RunMode(self._config.get('runmode', RunMode.OTHER))

View File

@ -5,15 +5,12 @@ includes:
* download data from exchange and store to disk * download data from exchange and store to disk
""" """
import gzip
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional, List, Dict, Tuple, Any from typing import Optional, List, Dict, Tuple, Any
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
import ujson
from freqtrade import misc, constants, OperationalException from freqtrade import misc, constants, OperationalException
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
@ -23,15 +20,6 @@ from freqtrade.arguments import TimeRange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def json_load(data):
"""
load data with ujson
Use this to have a consistent experience,
otherwise "precise_float" needs to be passed to all load operations
"""
return ujson.load(data, precise_float=True)
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]: def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
""" """
Trim tickerlist based on given timerange Trim tickerlist based on given timerange
@ -77,18 +65,10 @@ def load_tickerdata_file(
path = make_testdata_path(datadir) path = make_testdata_path(datadir)
pair_s = pair.replace('/', '_') pair_s = pair.replace('/', '_')
file = path.joinpath(f'{pair_s}-{ticker_interval}.json') file = path.joinpath(f'{pair_s}-{ticker_interval}.json')
gzipfile = file.with_suffix(file.suffix + '.gz')
# Try gzip file first, otherwise regular json file. pairdata = misc.file_load_json(file)
if gzipfile.is_file():
logger.debug('Loading ticker data from file %s', gzipfile) if not pairdata:
with gzip.open(gzipfile) as tickerdata:
pairdata = json_load(tickerdata)
elif file.is_file():
logger.debug('Loading ticker data from file %s', file)
with open(file) as tickerdata:
pairdata = json_load(tickerdata)
else:
return None return None
if timerange: if timerange:
@ -102,26 +82,28 @@ def load_pair_history(pair: str,
timerange: TimeRange = TimeRange(None, None, 0, 0), timerange: TimeRange = TimeRange(None, None, 0, 0),
refresh_pairs: bool = False, refresh_pairs: bool = False,
exchange: Optional[Exchange] = None, exchange: Optional[Exchange] = None,
fill_up_missing: bool = True
) -> DataFrame: ) -> DataFrame:
""" """
Loads cached ticker history for the given pair. Loads cached ticker history for the given pair.
:return: DataFrame with ohlcv data :return: DataFrame with ohlcv data
""" """
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
# If the user force the refresh of pairs # If the user force the refresh of pairs
if refresh_pairs: if refresh_pairs:
if not exchange: if not exchange:
raise OperationalException("Exchange needs to be initialized when " raise OperationalException("Exchange needs to be initialized when "
"calling load_data with refresh_pairs=True") "calling load_data with refresh_pairs=True")
logger.info('Download data for all pairs and store them in %s', datadir) logger.info('Download data for pair and store them in %s', datadir)
download_pair_history(datadir=datadir, download_pair_history(datadir=datadir,
exchange=exchange, exchange=exchange,
pair=pair, pair=pair,
tick_interval=ticker_interval, tick_interval=ticker_interval,
timerange=timerange) timerange=timerange)
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
if pairdata: if pairdata:
if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000: if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000:
logger.warning('Missing data at start for pair %s, data starts at %s', logger.warning('Missing data at start for pair %s, data starts at %s',
@ -130,7 +112,7 @@ def load_pair_history(pair: str,
logger.warning('Missing data at end for pair %s, data ends at %s', logger.warning('Missing data at end for pair %s, data ends at %s',
pair, pair,
arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S')) arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
return parse_ticker_dataframe(pairdata) return parse_ticker_dataframe(pairdata, ticker_interval, fill_up_missing)
else: else:
logger.warning('No data for pair: "%s", Interval: %s. ' logger.warning('No data for pair: "%s", Interval: %s. '
'Use --refresh-pairs-cached to download the data', 'Use --refresh-pairs-cached to download the data',
@ -143,7 +125,8 @@ def load_data(datadir: Optional[Path],
pairs: List[str], pairs: List[str],
refresh_pairs: bool = False, refresh_pairs: bool = False,
exchange: Optional[Exchange] = None, exchange: Optional[Exchange] = None,
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, DataFrame]: timerange: TimeRange = TimeRange(None, None, 0, 0),
fill_up_missing: bool = True) -> Dict[str, DataFrame]:
""" """
Loads ticker history data for a list of pairs the given parameters Loads ticker history data for a list of pairs the given parameters
:return: dict(<pair>:<tickerlist>) :return: dict(<pair>:<tickerlist>)
@ -154,7 +137,8 @@ def load_data(datadir: Optional[Path],
hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, hist = load_pair_history(pair=pair, ticker_interval=ticker_interval,
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)
if hist is not None: if hist is not None:
result[pair] = hist result[pair] = hist
return result return result
@ -185,7 +169,7 @@ def load_cached_data_for_updating(filename: Path, tick_interval: str,
# read the cached file # read the cached file
if filename.is_file(): if filename.is_file():
with open(filename, "rt") as file: with open(filename, "rt") as file:
data = json_load(file) data = misc.json_load(file)
# remove the last item, could be incomplete candle # remove the last item, could be incomplete candle
if data: if data:
data.pop() data.pop()

View File

@ -59,7 +59,7 @@ class Edge():
# checking max_open_trades. it should be -1 as with Edge # checking max_open_trades. it should be -1 as with Edge
# the number of trades is determined by position size # the number of trades is determined by position size
if self.config['max_open_trades'] != -1: if self.config['max_open_trades'] != float('inf'):
logger.critical('max_open_trades should be -1 in config !') logger.critical('max_open_trades should be -1 in config !')
if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT: if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT:
@ -190,9 +190,16 @@ class Edge():
if self._final_pairs != final: if self._final_pairs != final:
self._final_pairs = final self._final_pairs = final
if self._final_pairs: if self._final_pairs:
logger.info('Edge validated only %s', self._final_pairs) logger.info(
'Minimum expectancy and minimum winrate are met only for %s,'
' so other pairs are filtered out.',
self._final_pairs
)
else: else:
logger.info('Edge removed all pairs as no pair with minimum expectancy was found !') logger.info(
'Edge removed all pairs as no pair with minimum expectancy '
'and minimum winrate was found !'
)
return self._final_pairs return self._final_pairs

View File

@ -80,10 +80,10 @@ class Exchange(object):
self._cached_ticker: Dict[str, Any] = {} self._cached_ticker: Dict[str, Any] = {}
# Holds last candle refreshed time of each pair # Holds last candle refreshed time of each pair
self._pairs_last_refresh_time: Dict[str, int] = {} self._pairs_last_refresh_time: Dict[Tuple[str, str], int] = {}
# Holds candles # Holds candles
self._klines: Dict[str, DataFrame] = {} self._klines: Dict[Tuple[str, str], DataFrame] = {}
# Holds all open sell orders for dry_run # Holds all open sell orders for dry_run
self._dry_run_open_orders: Dict[str, Any] = {} self._dry_run_open_orders: Dict[str, Any] = {}
@ -158,11 +158,12 @@ class Exchange(object):
"""exchange ccxt id""" """exchange ccxt id"""
return self._api.id return self._api.id
def klines(self, pair: str, copy=True) -> DataFrame: def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame:
if pair in self._klines: # create key tuple
return self._klines[pair].copy() if copy else self._klines[pair] if pair_interval in self._klines:
return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
else: else:
return None return DataFrame()
def set_sandbox(self, api, exchange_config: dict, name: str): def set_sandbox(self, api, exchange_config: dict, name: str):
if exchange_config.get('sandbox'): if exchange_config.get('sandbox'):
@ -236,7 +237,7 @@ class Exchange(object):
f'Exchange {self.name} does not support market orders.') f'Exchange {self.name} does not support market orders.')
if order_types.get('stoploss_on_exchange'): if order_types.get('stoploss_on_exchange'):
if self.name is not 'Binance': if self.name != 'Binance':
raise OperationalException( raise OperationalException(
'On exchange stoploss is not supported for %s.' % self.name 'On exchange stoploss is not supported for %s.' % self.name
) )
@ -246,7 +247,7 @@ class Exchange(object):
Checks if order time in force configured in strategy/config are supported Checks if order time in force configured in strategy/config are supported
""" """
if any(v != 'gtc' for k, v in order_time_in_force.items()): if any(v != 'gtc' for k, v in order_time_in_force.items()):
if self.name is not 'Binance': if self.name != 'Binance':
raise OperationalException( raise OperationalException(
f'Time in force policies are not supporetd for {self.name} yet.') f'Time in force policies are not supporetd for {self.name} yet.')
@ -402,8 +403,11 @@ class Exchange(object):
return self._dry_run_open_orders[order_id] return self._dry_run_open_orders[order_id]
try: try:
return self._api.create_order(pair, 'stop_loss_limit', 'sell', order = self._api.create_order(pair, 'stop_loss_limit', 'sell',
amount, rate, {'stopPrice': stop_price}) amount, rate, {'stopPrice': stop_price})
logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s' % (pair, stop_price, rate))
return order
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise DependencyException(
@ -515,58 +519,68 @@ class Exchange(object):
input_coroutines = [self._async_get_candle_history( input_coroutines = [self._async_get_candle_history(
pair, tick_interval, since) for since in pair, tick_interval, 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 in tickers: for p, ticker_interval, 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" order # Sort data again after extending the result - above calls return in "async order"
data = sorted(data, key=lambda x: x[0]) data = sorted(data, key=lambda x: x[0])
logger.info("downloaded %s with length %s.", pair, len(data)) logger.info("downloaded %s with length %s.", pair, len(data))
return data return data
def refresh_tickers(self, pair_list: List[str], ticker_interval: str) -> None: def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]:
""" """
Refresh tickers asyncronously and set `_klines` of this object with the result Refresh in-memory ohlcv asyncronously and set `_klines` with the result
""" """
logger.debug("Refreshing klines for %d pairs", len(pair_list)) logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list))
asyncio.get_event_loop().run_until_complete(
self.async_get_candles_history(pair_list, ticker_interval))
async def async_get_candles_history(self, pairs: List[str],
tick_interval: str) -> List[Tuple[str, List]]:
"""Download ohlcv history for pair-list asyncronously """
# Calculating ticker interval in second
interval_in_sec = constants.TICKER_INTERVAL_MINUTES[tick_interval] * 60
input_coroutines = [] input_coroutines = []
# Gather corotines to run # Gather corotines to run
for pair in pairs: for pair, ticker_interval in set(pair_list):
if not (self._pairs_last_refresh_time.get(pair, 0) + interval_in_sec >= # Calculating ticker interval in second
arrow.utcnow().timestamp and pair in self._klines): interval_in_sec = constants.TICKER_INTERVAL_MINUTES[ticker_interval] * 60
input_coroutines.append(self._async_get_candle_history(pair, tick_interval))
else:
logger.debug("Using cached klines data for %s ...", pair)
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True) if not ((self._pairs_last_refresh_time.get((pair, ticker_interval), 0)
+ interval_in_sec) >= arrow.utcnow().timestamp
and (pair, ticker_interval) in self._klines):
input_coroutines.append(self._async_get_candle_history(pair, ticker_interval))
else:
logger.debug("Using cached ohlcv data for %s, %s ...", pair, ticker_interval)
tickers = asyncio.get_event_loop().run_until_complete(
asyncio.gather(*input_coroutines, return_exceptions=True))
# handle caching # handle caching
for pair, ticks in tickers: for res in tickers:
if isinstance(res, Exception):
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
continue
pair = res[0]
tick_interval = res[1]
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] = ticks[-1][0] // 1000 self._pairs_last_refresh_time[(pair, tick_interval)] = ticks[-1][0] // 1000
# keeping parsed dataframe in cache # keeping parsed dataframe in cache
self._klines[pair] = parse_ticker_dataframe(ticks) self._klines[(pair, tick_interval)] = parse_ticker_dataframe(
ticks, tick_interval, fill_missing=True)
return tickers return tickers
@retrier_async @retrier_async
async def _async_get_candle_history(self, pair: str, tick_interval: str, async def _async_get_candle_history(self, pair: str, tick_interval: str,
since_ms: Optional[int] = None) -> Tuple[str, List]: since_ms: Optional[int] = None) -> Tuple[str, str, List]:
"""
Asyncronously gets candle histories using fetch_ohlcv
returns tuple: (pair, tick_interval, ohlcv_list)
"""
try: try:
# fetch ohlcv asynchronously # fetch ohlcv asynchronously
logger.debug("fetching %s since %s ...", pair, since_ms) logger.debug("fetching %s, %s since %s ...", pair, tick_interval, since_ms)
data = await self._api_async.fetch_ohlcv(pair, timeframe=tick_interval, data = await self._api_async.fetch_ohlcv(pair, timeframe=tick_interval,
since=since_ms) since=since_ms)
@ -575,11 +589,14 @@ class Exchange(object):
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last) # Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
# when GDAX returns a list of tickers DESC (newest first, oldest last) # when GDAX returns a list of tickers DESC (newest first, oldest last)
# Only sort if necessary to save computing time # Only sort if necessary to save computing time
try:
if data and data[0][0] > data[-1][0]: if data and data[0][0] > data[-1][0]:
data = sorted(data, key=lambda x: x[0]) data = sorted(data, key=lambda x: x[0])
except IndexError:
logger.debug("done fetching %s ...", pair) logger.exception("Error loading %s. Result was %s.", pair, data)
return pair, data return pair, tick_interval, []
logger.debug("done fetching %s, %s ...", pair, tick_interval)
return pair, tick_interval, data
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(

View File

@ -15,6 +15,7 @@ from requests.exceptions import RequestException
from freqtrade import (DependencyException, OperationalException, from freqtrade import (DependencyException, OperationalException,
TemporaryError, __version__, constants, persistence) TemporaryError, __version__, constants, persistence)
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -36,9 +37,9 @@ class FreqtradeBot(object):
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
""" """
Init all variables and object the bot need to work Init all variables and objects the bot needs to work
:param config: configuration dict, you can use the Configuration.get_config() :param config: configuration dict, you can use Configuration.get_config()
method to get the config dict. to get the config dict.
""" """
logger.info( logger.info(
@ -54,9 +55,15 @@ class FreqtradeBot(object):
self.strategy: IStrategy = StrategyResolver(self.config).strategy self.strategy: IStrategy = StrategyResolver(self.config).strategy
self.rpc: RPCManager = RPCManager(self) self.rpc: RPCManager = RPCManager(self)
self.persistence = None
self.exchange = Exchange(self.config) self.exchange = Exchange(self.config)
self.wallets = Wallets(self.exchange) self.wallets = Wallets(self.exchange)
self.dataprovider = DataProvider(self.config, self.exchange)
# Attach Dataprovider to Strategy baseclass
IStrategy.dp = self.dataprovider
# Attach Wallets to Strategy baseclass
IStrategy.wallets = self.wallets
pairlistname = self.config.get('pairlist', {}).get('method', 'StaticPairList') pairlistname = self.config.get('pairlist', {}).get('method', 'StaticPairList')
self.pairlists = PairListResolver(pairlistname, self, self.config).pairlist self.pairlists = PairListResolver(pairlistname, self, self.config).pairlist
@ -151,9 +158,6 @@ class FreqtradeBot(object):
self.active_pair_whitelist = self.pairlists.whitelist self.active_pair_whitelist = self.pairlists.whitelist
# Calculating Edge positiong # Calculating Edge positiong
# Should be called before refresh_tickers
# Otherwise it will override cached klines in exchange
# with delta value (klines only from last refresh_pairs)
if self.edge: if self.edge:
self.edge.calculate() self.edge.calculate()
self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist) self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist)
@ -166,8 +170,12 @@ class FreqtradeBot(object):
self.active_pair_whitelist.extend([trade.pair for trade in trades self.active_pair_whitelist.extend([trade.pair for trade in trades
if trade.pair not in self.active_pair_whitelist]) if trade.pair not in self.active_pair_whitelist])
# Create pair-whitelist tuple with (pair, ticker_interval)
pair_whitelist_tuple = [(pair, self.config['ticker_interval'])
for pair in self.active_pair_whitelist]
# Refreshing candles # Refreshing candles
self.exchange.refresh_tickers(self.active_pair_whitelist, self.strategy.ticker_interval) self.dataprovider.refresh(pair_whitelist_tuple,
self.strategy.informative_pairs())
# First process current opened trades # First process current opened trades
for trade in trades: for trade in trades:
@ -183,7 +191,7 @@ class FreqtradeBot(object):
Trade.session.flush() Trade.session.flush()
except TemporaryError as error: except TemporaryError as error:
logger.warning('%s, retrying in 30 seconds...', error) logger.warning(f"Error: {error}, retrying in {constants.RETRY_TIMEOUT} seconds...")
time.sleep(constants.RETRY_TIMEOUT) time.sleep(constants.RETRY_TIMEOUT)
except OperationalException: except OperationalException:
tb = traceback.format_exc() tb = traceback.format_exc()
@ -196,19 +204,11 @@ class FreqtradeBot(object):
self.state = State.STOPPED self.state = State.STOPPED
return state_changed return state_changed
def get_target_bid(self, pair: str, ticker: Dict[str, float]) -> float: def get_target_bid(self, pair: str) -> float:
""" """
Calculates bid target between current ask price and last price Calculates bid target between current ask price and last price
:param ticker: Ticker to use for getting Ask and Last Price
:return: float: Price :return: float: Price
""" """
if ticker['ask'] < ticker['last']:
ticker_rate = ticker['ask']
else:
balance = self.config['bid_strategy']['ask_last_balance']
ticker_rate = ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
used_rate = ticker_rate
config_bid_strategy = self.config.get('bid_strategy', {}) config_bid_strategy = self.config.get('bid_strategy', {})
if 'use_order_book' in config_bid_strategy and\ if 'use_order_book' in config_bid_strategy and\
config_bid_strategy.get('use_order_book', False): config_bid_strategy.get('use_order_book', False):
@ -218,15 +218,16 @@ class FreqtradeBot(object):
logger.debug('order_book %s', order_book) logger.debug('order_book %s', order_book)
# top 1 = index 0 # top 1 = index 0
order_book_rate = order_book['bids'][order_book_top - 1][0] order_book_rate = order_book['bids'][order_book_top - 1][0]
# if ticker has lower rate, then use ticker ( usefull if down trending )
logger.info('...top %s order book buy rate %0.8f', order_book_top, order_book_rate) logger.info('...top %s order book buy rate %0.8f', order_book_top, order_book_rate)
if ticker_rate < order_book_rate:
logger.info('...using ticker rate instead %0.8f', ticker_rate)
used_rate = ticker_rate
else:
used_rate = order_book_rate used_rate = order_book_rate
else: else:
logger.info('Using Last Ask / Last Price') logger.info('Using Last Ask / Last Price')
ticker = self.exchange.get_ticker(pair)
if ticker['ask'] < ticker['last']:
ticker_rate = ticker['ask']
else:
balance = self.config['bid_strategy']['ask_last_balance']
ticker_rate = ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
used_rate = ticker_rate used_rate = ticker_rate
return used_rate return used_rate
@ -259,9 +260,8 @@ class FreqtradeBot(object):
# Check if stake_amount is fulfilled # Check if stake_amount is fulfilled
if avaliable_amount < stake_amount: if avaliable_amount < stake_amount:
raise DependencyException( raise DependencyException(
'Available balance(%f %s) is lower than stake amount(%f %s)' % ( f"Available balance({avaliable_amount} {self.config['stake_currency']}) is "
avaliable_amount, self.config['stake_currency'], f"lower than stake amount({stake_amount} {self.config['stake_currency']})"
stake_amount, self.config['stake_currency'])
) )
return stake_amount return stake_amount
@ -290,7 +290,9 @@ class FreqtradeBot(object):
if not min_stake_amounts: if not min_stake_amounts:
return None return None
amount_reserve_percent = 1 - 0.05 # reserve 5% + stoploss # reserve some percent defined in config (5% default) + stoploss
amount_reserve_percent = 1.0 - self.config.get('amount_reserve_percent',
constants.DEFAULT_AMOUNT_RESERVE_PERCENT)
if self.strategy.stoploss is not None: if self.strategy.stoploss is not None:
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%
@ -317,16 +319,16 @@ class FreqtradeBot(object):
# running get_signal on historical data fetched # running get_signal on historical data fetched
for _pair in whitelist: for _pair in whitelist:
(buy, sell) = self.strategy.get_signal(_pair, interval, self.exchange.klines(_pair)) (buy, sell) = self.strategy.get_signal(
_pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval))
if buy and not sell: if buy and not sell:
stake_amount = self._get_trade_stake_amount(_pair) stake_amount = self._get_trade_stake_amount(_pair)
if not stake_amount: if not stake_amount:
return False return False
logger.info( logger.info(f"Buy signal found: about create a new trade with stake_amount: "
'Buy signal found: about create a new trade with stake_amount: %f ...', f"{stake_amount} ...")
stake_amount
)
bidstrat_check_depth_of_market = self.config.get('bid_strategy', {}).\ bidstrat_check_depth_of_market = self.config.get('bid_strategy', {}).\
get('check_depth_of_market', {}) get('check_depth_of_market', {})
@ -373,7 +375,7 @@ class FreqtradeBot(object):
buy_limit_requested = price buy_limit_requested = price
else: else:
# Calculate amount # Calculate amount
buy_limit_requested = self.get_target_bid(pair, self.exchange.get_ticker(pair)) buy_limit_requested = self.get_target_bid(pair)
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit_requested) min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit_requested)
if min_stake_amount is not None and min_stake_amount > stake_amount: if min_stake_amount is not None and min_stake_amount > stake_amount:
@ -497,7 +499,7 @@ class FreqtradeBot(object):
trade.fee_open = 0 trade.fee_open = 0
except OperationalException as exception: except OperationalException as exception:
logger.warning("could not update trade amount: %s", exception) logger.warning("Could not update trade amount: %s", exception)
trade.update(order) trade.update(order)
@ -556,12 +558,12 @@ class FreqtradeBot(object):
fee_abs += exectrade['fee']['cost'] fee_abs += exectrade['fee']['cost']
if amount != order_amount: if amount != order_amount:
logger.warning(f"amount {amount} does not match amount {trade.amount}") logger.warning(f"Amount {amount} does not match amount {trade.amount}")
raise OperationalException("Half bought? Amounts don't match") raise OperationalException("Half bought? Amounts don't match")
real_amount = amount - fee_abs real_amount = amount - fee_abs
if fee_abs != 0: if fee_abs != 0:
logger.info(f"""Applying fee on amount for {trade} \ logger.info(f"Applying fee on amount for {trade} "
(from {order_amount} to {real_amount}) from Trades""") f"(from {order_amount} to {real_amount}) from Trades")
return real_amount return real_amount
def handle_trade(self, trade: Trade) -> bool: def handle_trade(self, trade: Trade) -> bool:
@ -570,16 +572,16 @@ class FreqtradeBot(object):
:return: True if trade has been sold, False otherwise :return: True if trade has been sold, False otherwise
""" """
if not trade.is_open: if not trade.is_open:
raise ValueError(f'attempt to handle closed trade: {trade}') raise ValueError(f'Attempt to handle closed trade: {trade}')
logger.debug('Handling %s ...', trade) logger.debug('Handling %s ...', trade)
sell_rate = self.exchange.get_ticker(trade.pair)['bid']
(buy, sell) = (False, False) (buy, sell) = (False, False)
experimental = self.config.get('experimental', {}) experimental = self.config.get('experimental', {})
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'): if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval, (buy, sell) = self.strategy.get_signal(
self.exchange.klines(trade.pair)) trade.pair, self.strategy.ticker_interval,
self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval))
config_ask_strategy = self.config.get('ask_strategy', {}) config_ask_strategy = self.config.get('ask_strategy', {})
if config_ask_strategy.get('use_order_book', False): if config_ask_strategy.get('use_order_book', False):
@ -592,18 +594,15 @@ class FreqtradeBot(object):
for i in range(order_book_min, order_book_max + 1): for i in range(order_book_min, order_book_max + 1):
order_book_rate = order_book['asks'][i - 1][0] order_book_rate = order_book['asks'][i - 1][0]
# if orderbook has higher rate (high profit),
# use orderbook, otherwise just use bids rate
logger.info(' order book asks top %s: %0.8f', i, order_book_rate) logger.info(' order book asks top %s: %0.8f', i, order_book_rate)
if sell_rate < order_book_rate:
sell_rate = order_book_rate sell_rate = order_book_rate
if self.check_sell(trade, sell_rate, buy, sell): if self.check_sell(trade, sell_rate, buy, sell):
return True return True
break
else: else:
logger.debug('checking sell') logger.debug('checking sell')
sell_rate = self.exchange.get_ticker(trade.pair)['bid']
if self.check_sell(trade, sell_rate, buy, sell): if self.check_sell(trade, sell_rate, buy, sell):
return True return True
@ -613,7 +612,7 @@ class FreqtradeBot(object):
def handle_stoploss_on_exchange(self, trade: Trade) -> bool: def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
""" """
Check if trade is fulfilled in which case the stoploss Check if trade is fulfilled in which case the stoploss
on exchange should be added immediately if stoploss on exchnage on exchange should be added immediately if stoploss on exchange
is enabled. is enabled.
""" """
@ -630,13 +629,14 @@ class FreqtradeBot(object):
stop_price = trade.open_rate * (1 + stoploss) stop_price = trade.open_rate * (1 + stoploss)
# limit price should be less than stop price. # limit price should be less than stop price.
# 0.98 is arbitrary here. # 0.99 is arbitrary here.
limit_price = stop_price * 0.98 limit_price = stop_price * 0.99
stoploss_order_id = self.exchange.stoploss_limit( stoploss_order_id = self.exchange.stoploss_limit(
pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price
)['id'] )['id']
trade.stoploss_order_id = str(stoploss_order_id) trade.stoploss_order_id = str(stoploss_order_id)
trade.stoploss_last_update = datetime.now()
# Or the trade open and there is already a stoploss on exchange. # Or the trade open and there is already a stoploss on exchange.
# so we check if it is hit ... # so we check if it is hit ...
@ -647,10 +647,38 @@ class FreqtradeBot(object):
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
trade.update(order) trade.update(order)
result = True result = True
else: elif self.config.get('trailing_stop', False):
result = False # if trailing stoploss is enabled we check if stoploss value has changed
# in which case we cancel stoploss order and put another one with new
# value immediately
self.handle_trailing_stoploss_on_exchange(trade, order)
return result return result
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order):
"""
Check to see if stoploss on exchange should be updated
in case of trailing stoploss on exchange
:param Trade: Corresponding Trade
:param order: Current on exchange stoploss order
:return: None
"""
if trade.stop_loss > float(order['info']['stopPrice']):
# we check if the update is neccesary
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() > update_beat:
# cancelling the current stoploss on exchange first
logger.info('Trailing stoploss: cancelling current stoploss on exchange '
'in order to add another one ...')
if self.exchange.cancel_order(order['id'], trade.pair):
# creating the new one
stoploss_order_id = self.exchange.stoploss_limit(
pair=trade.pair, amount=trade.amount,
stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99
)['id']
trade.stoploss_order_id = str(stoploss_order_id)
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
if self.edge: if self.edge:
stoploss = self.edge.stoploss(trade.pair) stoploss = self.edge.stoploss(trade.pair)
@ -698,8 +726,15 @@ class FreqtradeBot(object):
self.wallets.update() self.wallets.update()
continue continue
# Check if trade is still actually open # Handle cancelled on exchange
if order['status'] == 'open': if order['status'] == 'canceled':
if order['side'] == 'buy':
self.handle_buy_order_full_cancel(trade, "canceled on Exchange")
elif order['side'] == 'sell':
self.handle_timedout_limit_sell(trade, order)
self.wallets.update()
# Check if order is still actually open
elif order['status'] == 'open':
if order['side'] == 'buy' and ordertime < buy_timeoutthreashold: if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
self.handle_timedout_limit_buy(trade, order) self.handle_timedout_limit_buy(trade, order)
self.wallets.update() self.wallets.update()
@ -707,24 +742,24 @@ class FreqtradeBot(object):
self.handle_timedout_limit_sell(trade, order) self.handle_timedout_limit_sell(trade, order)
self.wallets.update() self.wallets.update()
# FIX: 20180110, why is cancel.order unconditionally here, whereas def handle_buy_order_full_cancel(self, trade: Trade, reason: str) -> None:
# it is conditionally called in the """Close trade in database and send message"""
# handle_timedout_limit_sell()? Trade.session.delete(trade)
Trade.session.flush()
logger.info('Buy order %s for %s.', reason, trade)
self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Unfilled buy order for {trade.pair} {reason}'
})
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
""" """
pair_s = trade.pair.replace('_', '/')
self.exchange.cancel_order(trade.open_order_id, trade.pair) self.exchange.cancel_order(trade.open_order_id, trade.pair)
if order['remaining'] == order['amount']: if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade # if trade is not partially completed, just delete the trade
Trade.session.delete(trade) self.handle_buy_order_full_cancel(trade, "cancelled due to timeout")
Trade.session.flush()
logger.info('Buy order timeout for %s.', trade)
self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Unfilled buy order for {pair_s} cancelled due to timeout'
})
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
@ -735,20 +770,24 @@ class FreqtradeBot(object):
logger.info('Partial buy order timeout for %s.', trade) logger.info('Partial buy order timeout for %s.', trade)
self.rpc.send_msg({ self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION, 'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Remaining buy order for {pair_s} cancelled due to timeout' 'status': f'Remaining buy order for {trade.pair} cancelled due to timeout'
}) })
return False return False
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool: def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool:
""" """
Sell timeout - cancel order and update trade Sell timeout - cancel order and update trade
:return: True if order was fully cancelled :return: True if order was fully cancelled
""" """
pair_s = trade.pair.replace('_', '/')
if order['remaining'] == order['amount']: if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade # if trade is not partially completed, just cancel the trade
if order["status"] != "canceled":
reason = "due to timeout"
self.exchange.cancel_order(trade.open_order_id, trade.pair) self.exchange.cancel_order(trade.open_order_id, trade.pair)
logger.info('Sell order timeout for %s.', trade)
else:
reason = "on exchange"
logger.info('Sell order canceled on exchange for %s.', trade)
trade.close_rate = None trade.close_rate = None
trade.close_profit = None trade.close_profit = None
trade.close_date = None trade.close_date = None
@ -756,9 +795,9 @@ class FreqtradeBot(object):
trade.open_order_id = None trade.open_order_id = None
self.rpc.send_msg({ self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION, 'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Unfilled sell order for {pair_s} cancelled due to timeout' 'status': f'Unfilled sell order for {trade.pair} cancelled {reason}'
}) })
logger.info('Sell order timeout for %s.', trade)
return True return True
# TODO: figure out how to handle partially complete sell orders # TODO: figure out how to handle partially complete sell orders

View File

@ -25,7 +25,7 @@ def main(sysargv: List[str]) -> None:
""" """
arguments = Arguments( arguments = Arguments(
sysargv, sysargv,
'Simple High Frequency Trading Bot for crypto currencies' 'Free, open source crypto trading bot'
) )
args = arguments.get_parsed_arg() args = arguments.get_parsed_arg()
@ -39,13 +39,13 @@ def main(sysargv: List[str]) -> None:
return_code = 1 return_code = 1
try: try:
# Load and validate configuration # Load and validate configuration
config = Configuration(args).get_config() config = Configuration(args, None).get_config()
# Init the bot # Init the bot
freqtrade = FreqtradeBot(config) freqtrade = FreqtradeBot(config)
state = None state = None
while 1: while True:
state = freqtrade.worker(old_state=state) state = freqtrade.worker(old_state=state)
if state == State.RELOAD_CONF: if state == State.RELOAD_CONF:
freqtrade = reconfigure(freqtrade, args) freqtrade = reconfigure(freqtrade, args)
@ -76,7 +76,7 @@ def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot:
freqtrade.cleanup() freqtrade.cleanup()
# Create new instance # Create new instance
freqtrade = FreqtradeBot(Configuration(args).get_config()) freqtrade = FreqtradeBot(Configuration(args, None).get_config())
freqtrade.rpc.send_msg({ freqtrade.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION, 'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'config reloaded' 'status': 'config reloaded'

View File

@ -3,7 +3,6 @@ Various tool function for Freqtrade and scripts
""" """
import gzip import gzip
import json
import logging import logging
import re import re
from datetime import datetime from datetime import datetime
@ -11,6 +10,7 @@ from typing import Dict
import numpy as np import numpy as np
from pandas import DataFrame from pandas import DataFrame
import rapidjson
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -38,12 +38,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
An numpy-array of datetimes An numpy-array of datetimes
:return: numpy-array of datetime :return: numpy-array of datetime
""" """
times = [] return dates.dt.to_pydatetime()
dates = dates.astype(datetime)
for index in range(0, dates.size):
date = dates[index].to_pydatetime()
times.append(date)
return np.array(times)
def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray: def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray:
@ -71,16 +66,45 @@ def file_dump_json(filename, data, is_zip=False) -> None:
:param data: JSON Data to save :param data: JSON Data to save
:return: :return:
""" """
print(f'dumping json to "{filename}"') logger.info(f'dumping json to "{filename}"')
if is_zip: if is_zip:
if not filename.endswith('.gz'): if not filename.endswith('.gz'):
filename = filename + '.gz' filename = filename + '.gz'
with gzip.open(filename, 'w') as fp: with gzip.open(filename, 'w') as fp:
json.dump(data, fp, default=str) rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE)
else: else:
with open(filename, 'w') as fp: with open(filename, 'w') as fp:
json.dump(data, fp, default=str) rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE)
logger.debug(f'done json to "{filename}"')
def json_load(datafile):
"""
load data with rapidjson
Use this to have a consistent experience,
sete number_mode to "NM_NATIVE" for greatest speed
"""
return rapidjson.load(datafile, number_mode=rapidjson.NM_NATIVE)
def file_load_json(file):
gzipfile = file.with_suffix(file.suffix + '.gz')
# Try gzip file first, otherwise regular json file.
if gzipfile.is_file():
logger.debug('Loading ticker data from file %s', gzipfile)
with gzip.open(gzipfile) as tickerdata:
pairdata = json_load(tickerdata)
elif file.is_file():
logger.debug('Loading ticker data from file %s', file)
with open(file) as tickerdata:
pairdata = json_load(tickerdata)
else:
return None
return pairdata
def format_ms_time(date: int) -> str: def format_ms_time(date: int) -> str:

View File

@ -13,7 +13,7 @@ from typing import Any, Dict, List, NamedTuple, Optional
from pandas import DataFrame from pandas import DataFrame
from tabulate import tabulate from tabulate import tabulate
import freqtrade.optimize as optimize from freqtrade import optimize
from freqtrade import DependencyException, constants from freqtrade import DependencyException, constants
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
@ -22,6 +22,7 @@ from freqtrade.data import history
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 StrategyResolver from freqtrade.resolvers import StrategyResolver
from freqtrade.state import RunMode
from freqtrade.strategy.interface import SellType, IStrategy from freqtrade.strategy.interface import SellType, IStrategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -100,11 +101,13 @@ class Backtesting(object):
:return: pretty printed table with tabulate as str :return: pretty printed table with tabulate as str
""" """
stake_currency = str(self.config.get('stake_currency')) stake_currency = str(self.config.get('stake_currency'))
max_open_trades = self.config.get('max_open_trades')
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f') floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
tabular_data = [] tabular_data = []
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %', headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss'] 'tot profit ' + stake_currency, 'tot profit %', 'avg duration',
'profit', 'loss']
for pair in data: for pair in data:
result = results[results.pair == pair] result = results[results.pair == pair]
if skip_nan and result.profit_abs.isnull().all(): if skip_nan and result.profit_abs.isnull().all():
@ -116,6 +119,7 @@ class Backtesting(object):
result.profit_percent.mean() * 100.0, result.profit_percent.mean() * 100.0,
result.profit_percent.sum() * 100.0, result.profit_percent.sum() * 100.0,
result.profit_abs.sum(), result.profit_abs.sum(),
result.profit_percent.sum() * 100.0 / max_open_trades,
str(timedelta( str(timedelta(
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00', minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
len(result[result.profit_abs > 0]), len(result[result.profit_abs > 0]),
@ -129,12 +133,15 @@ class Backtesting(object):
results.profit_percent.mean() * 100.0, results.profit_percent.mean() * 100.0,
results.profit_percent.sum() * 100.0, results.profit_percent.sum() * 100.0,
results.profit_abs.sum(), results.profit_abs.sum(),
results.profit_percent.sum() * 100.0 / max_open_trades,
str(timedelta( str(timedelta(
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
len(results[results.profit_abs > 0]), len(results[results.profit_abs > 0]),
len(results[results.profit_abs < 0]) len(results[results.profit_abs < 0])
]) ])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe") # Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(tabular_data, headers=headers, # type: ignore
floatfmt=floatfmt, tablefmt="pipe")
def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str: def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
""" """
@ -151,11 +158,13 @@ class Backtesting(object):
Generate summary table per strategy Generate summary table per strategy
""" """
stake_currency = str(self.config.get('stake_currency')) stake_currency = str(self.config.get('stake_currency'))
max_open_trades = self.config.get('max_open_trades')
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f') floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
tabular_data = [] tabular_data = []
headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %', headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss'] 'tot profit ' + stake_currency, 'tot profit %', 'avg duration',
'profit', 'loss']
for strategy, results in all_results.items(): for strategy, results in all_results.items():
tabular_data.append([ tabular_data.append([
strategy, strategy,
@ -163,12 +172,15 @@ class Backtesting(object):
results.profit_percent.mean() * 100.0, results.profit_percent.mean() * 100.0,
results.profit_percent.sum() * 100.0, results.profit_percent.sum() * 100.0,
results.profit_abs.sum(), results.profit_abs.sum(),
results.profit_percent.sum() * 100.0 / max_open_trades,
str(timedelta( str(timedelta(
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
len(results[results.profit_abs > 0]), len(results[results.profit_abs > 0]),
len(results[results.profit_abs < 0]) len(results[results.profit_abs < 0])
]) ])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe") # Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(tabular_data, headers=headers, # type: ignore
floatfmt=floatfmt, tablefmt="pipe")
def _store_backtest_result(self, recordfilename: str, results: DataFrame, def _store_backtest_result(self, recordfilename: str, results: DataFrame,
strategyname: Optional[str] = None) -> None: strategyname: Optional[str] = None) -> None:
@ -219,7 +231,8 @@ class Backtesting(object):
# Set close_rate to stoploss # Set close_rate to stoploss
closerate = trade.stop_loss closerate = trade.stop_loss
elif sell.sell_type == (SellType.ROI): elif sell.sell_type == (SellType.ROI):
# get entry in min_roi >= to trade duration # get next entry in min_roi > to trade duration
# Interface.py skips on trade_duration <= duration
roi_entry = max(list(filter(lambda x: trade_dur >= x, roi_entry = max(list(filter(lambda x: trade_dur >= x,
self.strategy.minimal_roi.keys()))) self.strategy.minimal_roi.keys())))
roi = self.strategy.minimal_roi[roi_entry] roi = self.strategy.minimal_roi[roi_entry]
@ -362,8 +375,9 @@ class Backtesting(object):
if self.config.get('live'): if self.config.get('live'):
logger.info('Downloading data for all pairs in whitelist ...') logger.info('Downloading data for all pairs in whitelist ...')
self.exchange.refresh_tickers(pairs, self.ticker_interval) self.exchange.refresh_latest_ohlcv([(pair, self.ticker_interval) for pair in pairs])
data = self.exchange._klines data = {key[0]: value for key, value in self.exchange._klines.items()}
else: else:
logger.info('Using local backtesting data (using whitelist in given config) ...') logger.info('Using local backtesting data (using whitelist in given config) ...')
@ -393,12 +407,9 @@ class Backtesting(object):
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)
# need to reprocess data every time to populate signals min_date, max_date = optimize.get_timeframe(data)
preprocessed = self.strategy.tickerdata_to_dataframe(data) # Validate dataframe for missing values (mainly at start and end, as fillup is called)
optimize.validate_backtest_data(data, min_date, max_date,
min_date, max_date = optimize.get_timeframe(preprocessed)
# Validate dataframe for missing values
optimize.validate_backtest_data(preprocessed, min_date, max_date,
constants.TICKER_INTERVAL_MINUTES[self.ticker_interval]) constants.TICKER_INTERVAL_MINUTES[self.ticker_interval])
logger.info( logger.info(
'Measuring data from %s up to %s (%s days)..', 'Measuring data from %s up to %s (%s days)..',
@ -406,6 +417,8 @@ class Backtesting(object):
max_date.isoformat(), max_date.isoformat(),
(max_date - min_date).days (max_date - min_date).days
) )
# need to reprocess data every time to populate signals
preprocessed = self.strategy.tickerdata_to_dataframe(data)
# 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(
@ -426,18 +439,18 @@ class Backtesting(object):
strategy if len(self.strategylist) > 1 else None) strategy if len(self.strategylist) > 1 else None)
print(f"Result for strategy {strategy}") print(f"Result for strategy {strategy}")
print(' BACKTESTING REPORT '.center(119, '=')) print(' BACKTESTING REPORT '.center(133, '='))
print(self._generate_text_table(data, results)) print(self._generate_text_table(data, results))
print(' SELL REASON STATS '.center(119, '=')) print(' SELL REASON STATS '.center(133, '='))
print(self._generate_text_table_sell_reason(data, results)) print(self._generate_text_table_sell_reason(data, results))
print(' LEFT OPEN TRADES REPORT '.center(119, '=')) print(' LEFT OPEN TRADES REPORT '.center(133, '='))
print(self._generate_text_table(data, results.loc[results.open_at_end], True)) print(self._generate_text_table(data, results.loc[results.open_at_end], True))
print() print()
if len(all_results) > 1: if len(all_results) > 1:
# Print Strategy summary table # Print Strategy summary table
print(' Strategy Summary '.center(119, '=')) print(' Strategy Summary '.center(133, '='))
print(self._generate_text_table_strategy(all_results)) print(self._generate_text_table_strategy(all_results))
print('\nFor more details, please look at the detail tables above') print('\nFor more details, please look at the detail tables above')
@ -448,7 +461,7 @@ def setup_configuration(args: Namespace) -> Dict[str, Any]:
:param args: Cli args from Arguments() :param args: Cli args from Arguments()
:return: Configuration :return: Configuration
""" """
configuration = Configuration(args) configuration = Configuration(args, RunMode.BACKTEST)
config = configuration.get_config() config = configuration.get_config()
# Ensure we do not use Exchange credentials # Ensure we do not use Exchange credentials

View File

@ -33,6 +33,7 @@ class DefaultHyperOpts(IHyperOpt):
# Bollinger bands # Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower'] dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_upperband'] = bollinger['upper']
dataframe['sar'] = ta.SAR(dataframe) dataframe['sar'] = ta.SAR(dataframe)
return dataframe return dataframe
@ -57,6 +58,7 @@ class DefaultHyperOpts(IHyperOpt):
conditions.append(dataframe['rsi'] < params['rsi-value']) conditions.append(dataframe['rsi'] < params['rsi-value'])
# TRIGGERS # TRIGGERS
if 'trigger' in params:
if params['trigger'] == 'bb_lower': if params['trigger'] == 'bb_lower':
conditions.append(dataframe['close'] < dataframe['bb_lowerband']) conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if params['trigger'] == 'macd_cross_signal': if params['trigger'] == 'macd_cross_signal':
@ -93,6 +95,67 @@ class DefaultHyperOpts(IHyperOpt):
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
] ]
@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
"""
# print(params)
conditions = []
# GUARDS AND TRENDS
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']:
conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
if 'sell-adx-enabled' in params and params['sell-adx-enabled']:
conditions.append(dataframe['adx'] < params['sell-adx-value'])
if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']:
conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
# 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']
))
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 [
Integer(75, 100, name='sell-mfi-value'),
Integer(50, 100, name='sell-fastd-value'),
Integer(50, 100, name='sell-adx-value'),
Integer(60, 100, name='sell-rsi-value'),
Categorical([True, False], name='sell-mfi-enabled'),
Categorical([True, False], name='sell-fastd-enabled'),
Categorical([True, False], name='sell-adx-enabled'),
Categorical([True, False], name='sell-rsi-enabled'),
Categorical(['sell-bb_upper',
'sell-macd_cross_signal',
'sell-sar_reversal'], name='sell-trigger')
]
@staticmethod @staticmethod
def generate_roi_table(params: Dict) -> Dict[int, float]: def generate_roi_table(params: Dict) -> Dict[int, float]:
""" """
@ -128,3 +191,36 @@ class DefaultHyperOpts(IHyperOpt):
Real(0.01, 0.07, name='roi_p2'), Real(0.01, 0.07, name='roi_p2'),
Real(0.01, 0.20, name='roi_p3'), Real(0.01, 0.20, name='roi_p3'),
] ]
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators. Should be a copy of from strategy
must align to populate_indicators in this file
Only used when --spaces does not include buy
"""
dataframe.loc[
(
(dataframe['close'] < dataframe['bb_lowerband']) &
(dataframe['mfi'] < 16) &
(dataframe['adx'] > 25) &
(dataframe['rsi'] < 21)
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators. Should be a copy of from strategy
must align to populate_indicators in this file
Only used when --spaces does not include sell
"""
dataframe.loc[
(
(qtpylib.crossed_above(
dataframe['macdsignal'], dataframe['macd']
)) &
(dataframe['fastd'] > 54)
),
'sell'] = 1
return dataframe

View File

@ -9,10 +9,11 @@ from typing import Dict, Any
from tabulate import tabulate from tabulate import tabulate
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.configuration import Configuration
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -67,7 +68,9 @@ class EdgeCli(object):
round(result[1].avg_trade_duration) round(result[1].avg_trade_duration)
]) ])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe") # Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(tabular_data, headers=headers, # type: ignore
floatfmt=floatfmt, tablefmt="pipe")
def start(self) -> None: def start(self) -> None:
self.edge.calculate() self.edge.calculate()
@ -81,7 +84,7 @@ def setup_configuration(args: Namespace) -> Dict[str, Any]:
:param args: Cli args from Arguments() :param args: Cli args from Arguments()
:return: Configuration :return: Configuration
""" """
configuration = Configuration(args) configuration = Configuration(args, RunMode.EDGECLI)
config = configuration.get_config() config = configuration.get_config()
# Ensure we do not use Exchange credentials # Ensure we do not use Exchange credentials

View File

@ -5,17 +5,18 @@ This module contains the hyperopt logic
""" """
import logging import logging
from argparse import Namespace import multiprocessing
import os import os
import sys import sys
from pathlib import Path from argparse import Namespace
from math import exp from math import exp
import multiprocessing
from operator import itemgetter from operator import itemgetter
from pathlib import Path
from pprint import pprint
from typing import Any, Dict, List from typing import Any, Dict, List
from pandas import DataFrame
from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects
from pandas import DataFrame
from skopt import Optimizer from skopt import Optimizer
from skopt.space import Dimension from skopt.space import Dimension
@ -24,9 +25,9 @@ from freqtrade.configuration import Configuration
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.optimize import get_timeframe from freqtrade.optimize import get_timeframe
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from freqtrade.state import RunMode
from freqtrade.resolvers import HyperOptResolver from freqtrade.resolvers import HyperOptResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
@ -102,13 +103,13 @@ class Hyperopt(Backtesting):
results = sorted(self.trials, key=itemgetter('loss')) results = sorted(self.trials, key=itemgetter('loss'))
best_result = results[0] best_result = results[0]
logger.info( logger.info(
'Best result:\n%s\nwith values:\n%s', 'Best result:\n%s\nwith values:\n',
best_result['result'], best_result['result']
best_result['params']
) )
pprint(best_result['params'], indent=4)
if 'roi_t1' in best_result['params']: if 'roi_t1' in best_result['params']:
logger.info('ROI table:\n%s', logger.info('ROI table:')
self.custom_hyperopt.generate_roi_table(best_result['params'])) pprint(self.custom_hyperopt.generate_roi_table(best_result['params']), indent=4)
def log_results(self, results) -> None: def log_results(self, results) -> None:
""" """
@ -151,6 +152,12 @@ class Hyperopt(Backtesting):
spaces: List[Dimension] = [] spaces: List[Dimension] = []
if self.has_space('buy'): if self.has_space('buy'):
spaces += self.custom_hyperopt.indicator_space() spaces += self.custom_hyperopt.indicator_space()
if self.has_space('sell'):
spaces += self.custom_hyperopt.sell_indicator_space()
# Make sure experimental is enabled
if 'experimental' not in self.config:
self.config['experimental'] = {}
self.config['experimental']['use_sell_signal'] = True
if self.has_space('roi'): if self.has_space('roi'):
spaces += self.custom_hyperopt.roi_space() spaces += self.custom_hyperopt.roi_space()
if self.has_space('stoploss'): if self.has_space('stoploss'):
@ -164,6 +171,13 @@ class Hyperopt(Backtesting):
if self.has_space('buy'): if self.has_space('buy'):
self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params) self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params)
elif hasattr(self.custom_hyperopt, 'populate_buy_trend'):
self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore
if self.has_space('sell'):
self.advise_sell = self.custom_hyperopt.sell_strategy_generator(params)
elif hasattr(self.custom_hyperopt, 'populate_sell_trend'):
self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore
if self.has_space('stoploss'): if self.has_space('stoploss'):
self.strategy.stoploss = params['stoploss'] self.strategy.stoploss = params['stoploss']
@ -247,7 +261,7 @@ class Hyperopt(Backtesting):
timerange=timerange timerange=timerange
) )
if self.has_space('buy'): if self.has_space('buy') or self.has_space('sell'):
self.strategy.advise_indicators = \ self.strategy.advise_indicators = \
self.custom_hyperopt.populate_indicators # type: ignore self.custom_hyperopt.populate_indicators # type: ignore
dump(self.strategy.tickerdata_to_dataframe(data), TICKERDATA_PICKLE) dump(self.strategy.tickerdata_to_dataframe(data), TICKERDATA_PICKLE)
@ -293,7 +307,7 @@ def start(args: Namespace) -> None:
# Initialize configuration # Initialize configuration
# Monkey patch the configuration with hyperopt_conf.py # Monkey patch the configuration with hyperopt_conf.py
configuration = Configuration(args) configuration = Configuration(args, RunMode.HYPEROPT)
logger.info('Starting freqtrade in Hyperopt mode') logger.info('Starting freqtrade in Hyperopt mode')
config = configuration.load_config() config = configuration.load_config()

View File

@ -37,6 +37,13 @@ class IHyperOpt(ABC):
Create a buy strategy generator Create a buy strategy generator
""" """
@staticmethod
@abstractmethod
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Create a sell strategy generator
"""
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def indicator_space() -> List[Dimension]: def indicator_space() -> List[Dimension]:
@ -44,6 +51,13 @@ class IHyperOpt(ABC):
Create an indicator space Create an indicator space
""" """
@staticmethod
@abstractmethod
def sell_indicator_space() -> List[Dimension]:
"""
Create a sell indicator space
"""
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def generate_roi_table(params: Dict) -> Dict[int, float]: def generate_roi_table(params: Dict) -> Dict[int, float]:

View File

@ -83,7 +83,7 @@ def check_migrate(engine) -> None:
logger.debug(f'trying {table_back_name}') logger.debug(f'trying {table_back_name}')
# Check for latest column # Check for latest column
if not has_column(cols, 'stoploss_order_id'): if not has_column(cols, 'stoploss_last_update'):
logger.info(f'Running database migration - backup available as {table_back_name}') logger.info(f'Running database migration - backup available as {table_back_name}')
fee_open = get_column_def(cols, 'fee_open', 'fee') fee_open = get_column_def(cols, 'fee_open', 'fee')
@ -93,6 +93,7 @@ def check_migrate(engine) -> None:
stop_loss = get_column_def(cols, 'stop_loss', '0.0') stop_loss = get_column_def(cols, 'stop_loss', '0.0')
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
max_rate = get_column_def(cols, 'max_rate', '0.0') max_rate = get_column_def(cols, 'max_rate', '0.0')
sell_reason = get_column_def(cols, 'sell_reason', 'null') sell_reason = get_column_def(cols, 'sell_reason', 'null')
strategy = get_column_def(cols, 'strategy', 'null') strategy = get_column_def(cols, 'strategy', 'null')
@ -111,7 +112,8 @@ def check_migrate(engine) -> None:
(id, exchange, pair, is_open, fee_open, fee_close, open_rate, (id, exchange, pair, is_open, fee_open, fee_close, open_rate,
open_rate_requested, close_rate, close_rate_requested, close_profit, open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id, stake_amount, amount, open_date, close_date, open_order_id,
stop_loss, initial_stop_loss, stoploss_order_id, max_rate, sell_reason, strategy, stop_loss, initial_stop_loss, stoploss_order_id, stoploss_last_update,
max_rate, sell_reason, strategy,
ticker_interval ticker_interval
) )
select id, lower(exchange), select id, lower(exchange),
@ -127,9 +129,9 @@ def check_migrate(engine) -> None:
{close_rate_requested} close_rate_requested, close_profit, {close_rate_requested} close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id, stake_amount, amount, open_date, close_date, open_order_id,
{stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss, {stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
{stoploss_order_id} stoploss_order_id, {max_rate} max_rate, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{sell_reason} sell_reason, {strategy} strategy, {max_rate} max_rate, {sell_reason} sell_reason,
{ticker_interval} ticker_interval {strategy} strategy, {ticker_interval} ticker_interval
from {table_back_name} from {table_back_name}
""") """)
@ -185,6 +187,8 @@ class Trade(_DECL_BASE):
initial_stop_loss = Column(Float, nullable=True, default=0.0) initial_stop_loss = Column(Float, nullable=True, default=0.0)
# stoploss order id which is on exchange # stoploss order id which is on exchange
stoploss_order_id = Column(String, nullable=True, index=True) stoploss_order_id = Column(String, nullable=True, index=True)
# last update time of the stoploss order on exchange
stoploss_last_update = Column(DateTime, nullable=True)
# absolute value of the highest reached price # absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0) max_rate = Column(Float, nullable=True, default=0.0)
sell_reason = Column(String, nullable=True) sell_reason = Column(String, nullable=True)
@ -218,11 +222,13 @@ class Trade(_DECL_BASE):
logger.debug("assigning new stop loss") logger.debug("assigning new stop loss")
self.stop_loss = new_loss self.stop_loss = new_loss
self.initial_stop_loss = new_loss self.initial_stop_loss = new_loss
self.stoploss_last_update = datetime.utcnow()
# evaluate if the stop loss needs to be updated # evaluate if the stop loss needs to be updated
else: else:
if new_loss > self.stop_loss: # stop losses only walk up, never down! if new_loss > self.stop_loss: # stop losses only walk up, never down!
self.stop_loss = new_loss self.stop_loss = new_loss
self.stoploss_last_update = datetime.utcnow()
logger.debug("adjusted stop loss") logger.debug("adjusted stop loss")
else: else:
logger.debug("keeping current stop loss") logger.debug("keeping current stop loss")

View File

@ -32,6 +32,13 @@ class HyperOptResolver(IResolver):
hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT
self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path')) self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path'))
if not hasattr(self.hyperopt, 'populate_buy_trend'):
logger.warning("Custom Hyperopt does not provide populate_buy_trend. "
"Using populate_buy_trend from DefaultStrategy.")
if not hasattr(self.hyperopt, 'populate_sell_trend'):
logger.warning("Custom Hyperopt does not provide populate_sell_trend. "
"Using populate_sell_trend from DefaultStrategy.")
def _load_hyperopt( def _load_hyperopt(
self, hyperopt_name: str, extra_dir: Optional[str] = None) -> IHyperOpt: self, hyperopt_name: str, extra_dir: Optional[str] = None) -> IHyperOpt:
""" """

View File

@ -3,11 +3,11 @@
""" """
This module load custom strategies This module load custom strategies
""" """
import inspect
import logging import logging
import tempfile import tempfile
from base64 import urlsafe_b64decode from base64 import urlsafe_b64decode
from collections import OrderedDict from collections import OrderedDict
from inspect import getfullargspec
from pathlib import Path from pathlib import Path
from typing import Dict, Optional from typing import Dict, Optional
@ -39,59 +39,67 @@ class StrategyResolver(IResolver):
config=config, config=config,
extra_dir=config.get('strategy_path')) extra_dir=config.get('strategy_path'))
# make sure experimental dict is available
if 'experimental' not in config:
config['experimental'] = {}
# Set attributes # Set attributes
# Check if we need to override configuration # Check if we need to override configuration
if 'minimal_roi' in config: # (Attribute name, default, experimental)
self.strategy.minimal_roi = config['minimal_roi'] attributes = [("minimal_roi", None, False),
logger.info("Override strategy 'minimal_roi' with value in config file: %s.", ("ticker_interval", None, False),
config['minimal_roi']) ("stoploss", None, False),
("trailing_stop", None, False),
("trailing_stop_positive", None, False),
("trailing_stop_positive_offset", 0.0, False),
("process_only_new_candles", None, False),
("order_types", None, False),
("order_time_in_force", None, False),
("use_sell_signal", False, True),
("sell_profit_only", False, True),
("ignore_roi_if_buy_signal", False, True),
]
for attribute, default, experimental in attributes:
if experimental:
self._override_attribute_helper(config['experimental'], attribute, default)
else: else:
config['minimal_roi'] = self.strategy.minimal_roi self._override_attribute_helper(config, attribute, default)
if 'stoploss' in config: # Loop this list again to have output combined
self.strategy.stoploss = config['stoploss'] for attribute, _, exp in attributes:
logger.info( if exp and attribute in config['experimental']:
"Override strategy 'stoploss' with value in config file: %s.", config['stoploss'] logger.info("Strategy using %s: %s", attribute, config['experimental'][attribute])
) elif attribute in config:
else: logger.info("Strategy using %s: %s", attribute, config[attribute])
config['stoploss'] = self.strategy.stoploss
if 'ticker_interval' in config: # Sort and apply type conversions
self.strategy.ticker_interval = config['ticker_interval'] self.strategy.minimal_roi = OrderedDict(sorted(
logger.info( {int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
"Override strategy 'ticker_interval' with value in config file: %s.", key=lambda t: t[0]))
config['ticker_interval'] self.strategy.stoploss = float(self.strategy.stoploss)
)
else:
config['ticker_interval'] = self.strategy.ticker_interval
if 'process_only_new_candles' in config: self._strategy_sanity_validations()
self.strategy.process_only_new_candles = config['process_only_new_candles']
logger.info(
"Override process_only_new_candles 'process_only_new_candles' "
"with value in config file: %s.", config['process_only_new_candles']
)
else:
config['process_only_new_candles'] = self.strategy.process_only_new_candles
if 'order_types' in config: def _override_attribute_helper(self, config, attribute: str, default):
self.strategy.order_types = config['order_types'] """
logger.info( Override attributes in the strategy.
"Override strategy 'order_types' with value in config file: %s.", Prevalence:
config['order_types'] - Configuration
) - Strategy
else: - default (if not None)
config['order_types'] = self.strategy.order_types """
if attribute in config:
if 'order_time_in_force' in config: setattr(self.strategy, attribute, config[attribute])
self.strategy.order_time_in_force = config['order_time_in_force'] logger.info("Override strategy '%s' with value in config file: %s.",
logger.info( attribute, config[attribute])
"Override strategy 'order_time_in_force' with value in config file: %s.", elif hasattr(self.strategy, attribute):
config['order_time_in_force'] config[attribute] = getattr(self.strategy, attribute)
) # Explicitly check for None here as other "falsy" values are possible
else: elif default is not None:
config['order_time_in_force'] = self.strategy.order_time_in_force setattr(self.strategy, attribute, default)
config[attribute] = default
def _strategy_sanity_validations(self):
if not all(k in self.strategy.order_types for k in constants.REQUIRED_ORDERTYPES): if not all(k in self.strategy.order_types for k in constants.REQUIRED_ORDERTYPES):
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. " raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
f"Order-types mapping is incomplete.") f"Order-types mapping is incomplete.")
@ -100,12 +108,6 @@ class StrategyResolver(IResolver):
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. " raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
f"Order-time-in-force mapping is incomplete.") f"Order-time-in-force mapping is incomplete.")
# Sort and apply type conversions
self.strategy.minimal_roi = OrderedDict(sorted(
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
key=lambda t: t[0]))
self.strategy.stoploss = float(self.strategy.stoploss)
def _load_strategy( def _load_strategy(
self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy: self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy:
""" """
@ -149,11 +151,9 @@ class StrategyResolver(IResolver):
if strategy: if strategy:
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, _path) logger.info('Using resolved strategy %s from \'%s\'', strategy_name, _path)
strategy._populate_fun_len = len( strategy._populate_fun_len = len(
inspect.getfullargspec(strategy.populate_indicators).args) getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len( strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
inspect.getfullargspec(strategy.populate_buy_trend).args) strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
strategy._sell_fun_len = len(
inspect.getfullargspec(strategy.populate_sell_trend).args)
return import_strategy(strategy, config=config) return import_strategy(strategy, config=config)
except FileNotFoundError: except FileNotFoundError:

View File

@ -246,14 +246,14 @@ class Telegram(RPC):
stake_cur, stake_cur,
fiat_disp_cur fiat_disp_cur
) )
stats = tabulate(stats, stats_tab = tabulate(stats,
headers=[ headers=[
'Day', 'Day',
f'Profit {stake_cur}', f'Profit {stake_cur}',
f'Profit {fiat_disp_cur}' f'Profit {fiat_disp_cur}'
], ],
tablefmt='simple') tablefmt='simple')
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats}</pre>' message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML) self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e), bot=bot)

View File

@ -3,13 +3,26 @@
""" """
Bot state constant Bot state constant
""" """
import enum from enum import Enum
class State(enum.Enum): class State(Enum):
""" """
Bot application states Bot application states
""" """
RUNNING = 0 RUNNING = 1
STOPPED = 1 STOPPED = 2
RELOAD_CONF = 2 RELOAD_CONF = 3
class RunMode(Enum):
"""
Bot running mode (backtest, hyperopt, ...)
can be "live", "dry-run", "backtest", "edgecli", "hyperopt".
"""
LIVE = "live"
DRY_RUN = "dry_run"
BACKTEST = "backtest"
EDGECLI = "edgecli"
HYPEROPT = "hyperopt"
OTHER = "other" # Used for plotting scripts and test

View File

@ -42,6 +42,19 @@ class DefaultStrategy(IStrategy):
'sell': '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: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Adds several different TA indicators to the given DataFrame Adds several different TA indicators to the given DataFrame

View File

@ -13,7 +13,9 @@ import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade import constants from freqtrade import constants
from freqtrade.data.dataprovider import DataProvider
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -67,6 +69,11 @@ class IStrategy(ABC):
# associated stoploss # associated stoploss
stoploss: float stoploss: float
# trailing stoploss
trailing_stop: bool = False
trailing_stop_positive: float
trailing_stop_positive_offset: float
# associated ticker interval # associated ticker interval
ticker_interval: str ticker_interval: str
@ -75,7 +82,8 @@ class IStrategy(ABC):
'buy': 'limit', 'buy': 'limit',
'sell': 'limit', 'sell': 'limit',
'stoploss': 'limit', 'stoploss': 'limit',
'stoploss_on_exchange': False 'stoploss_on_exchange': False,
'stoploss_on_exchange_interval': 60,
} }
# Optional time in force # Optional time in force
@ -87,12 +95,16 @@ 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
# Dict to determine if analysis is necessary # Class level variables (intentional) containing
_last_candle_seen_per_pair: Dict[str, datetime] = {} # the dataprovider (dp) (access to other candles, historic data, ...)
# and wallets - access to the current balance.
dp: DataProvider
wallets: Wallets
def __init__(self, config: dict) -> None: def __init__(self, config: dict) -> None:
self.config = config self.config = config
self._last_candle_seen_per_pair = {} # Dict to determine if analysis is necessary
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
@abstractmethod @abstractmethod
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@ -121,6 +133,19 @@ class IStrategy(ABC):
:return: DataFrame with sell column :return: DataFrame with sell column
""" """
def informative_pairs(self) -> List[Tuple[str, str]]:
"""
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 get_strategy_name(self) -> str: def get_strategy_name(self) -> str:
""" """
Returns strategy class name Returns strategy class name
@ -141,19 +166,19 @@ class IStrategy(ABC):
if (not self.process_only_new_candles or if (not self.process_only_new_candles or
self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']): self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']):
# Defs that only make change on new candle data. # Defs that only make change on new candle data.
logging.debug("TA Analysis Launched") logger.debug("TA Analysis Launched")
dataframe = self.advise_indicators(dataframe, metadata) dataframe = self.advise_indicators(dataframe, metadata)
dataframe = self.advise_buy(dataframe, metadata) dataframe = self.advise_buy(dataframe, metadata)
dataframe = self.advise_sell(dataframe, metadata) dataframe = self.advise_sell(dataframe, metadata)
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date'] self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
else: else:
logging.debug("Skippinig TA Analysis for already analyzed candle") logger.debug("Skipping TA Analysis for already analyzed candle")
dataframe['buy'] = 0 dataframe['buy'] = 0
dataframe['sell'] = 0 dataframe['sell'] = 0
# Other Defs in strategy that want to be called every loop here # Other Defs in strategy that want to be called every loop here
# twitter_sell = self.watch_twitter_feed(dataframe, metadata) # twitter_sell = self.watch_twitter_feed(dataframe, metadata)
logging.debug("Loop Analysis Launched") logger.debug("Loop Analysis Launched")
return dataframe return dataframe
@ -228,9 +253,6 @@ class IStrategy(ABC):
current_rate = low or rate current_rate = low or rate
current_profit = trade.calc_profit_percent(current_rate) current_profit = trade.calc_profit_percent(current_rate)
if self.order_types.get('stoploss_on_exchange'):
stoplossflag = SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
else:
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
current_time=date, current_profit=current_profit, current_time=date, current_profit=current_profit,
force_stoploss=force_stoploss) force_stoploss=force_stoploss)
@ -271,14 +293,16 @@ class IStrategy(ABC):
""" """
trailing_stop = self.config.get('trailing_stop', False) trailing_stop = self.config.get('trailing_stop', False)
trade.adjust_stop_loss(trade.open_rate, force_stoploss if force_stoploss trade.adjust_stop_loss(trade.open_rate, force_stoploss if force_stoploss
else self.stoploss, initial=True) else self.stoploss, initial=True)
# evaluate if the stoploss was hit # evaluate if the stoploss was hit if stoploss is not on exchange
if self.stoploss is not None and trade.stop_loss >= current_rate: if ((self.stoploss is not None) and
(trade.stop_loss >= current_rate) and
(not self.order_types.get('stoploss_on_exchange'))):
selltype = SellType.STOP_LOSS selltype = SellType.STOP_LOSS
if trailing_stop: # If Trailing stop (and max-rate did move above open rate)
if trailing_stop and trade.open_rate != trade.max_rate:
selltype = SellType.TRAILING_STOP_LOSS selltype = SellType.TRAILING_STOP_LOSS
logger.debug( logger.debug(
f"HIT STOP: current price at {current_rate:.6f}, " f"HIT STOP: current price at {current_rate:.6f}, "
@ -295,8 +319,9 @@ class IStrategy(ABC):
# check if we have a special stop loss for positive condition # check if we have a special stop loss for positive condition
# and if profit is positive # and if profit is positive
stop_loss_value = self.stoploss stop_loss_value = force_stoploss if force_stoploss else self.stoploss
sl_offset = self.config.get('trailing_stop_positive_offset', 0.0)
sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0
if 'trailing_stop_positive' in self.config and current_profit > sl_offset: if 'trailing_stop_positive' in self.config and current_profit > sl_offset:
@ -313,15 +338,16 @@ class IStrategy(ABC):
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
""" """
Based an earlier trade and current price and ROI configuration, decides whether bot should Based an earlier trade and current price and ROI configuration, decides whether bot should
sell sell. Requires current_profit to be in percent!!
:return True if bot should sell at current rate :return True if bot should sell at current rate
""" """
# Check if time matches and current rate is above threshold # Check if time matches and current rate is above threshold
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60 trade_dur = (current_time.timestamp() - trade.open_date.timestamp()) / 60
for duration, threshold in self.minimal_roi.items():
if time_diff <= duration: # Get highest entry in ROI dict where key >= trade-duration
return False roi_entry = max(list(filter(lambda x: trade_dur >= x, self.minimal_roi.keys())))
threshold = self.minimal_roi[roi_entry]
if current_profit > threshold: if current_profit > threshold:
return True return True

View File

@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
import json import json
import logging import logging
import re
from datetime import datetime from datetime import datetime
from functools import reduce from functools import reduce
from typing import Dict, Optional from typing import Dict, Optional
@ -27,6 +28,12 @@ def log_has(line, logs):
False) False)
def log_has_re(line, logs):
return reduce(lambda a, b: a or b,
filter(lambda x: re.match(line, x[2]), logs),
False)
def patch_exchange(mocker, api_mock=None, id='bittrex') -> None: def patch_exchange(mocker, api_mock=None, id='bittrex') -> None:
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
@ -542,7 +549,7 @@ def ticker_history_list():
@pytest.fixture @pytest.fixture
def ticker_history(ticker_history_list): def ticker_history(ticker_history_list):
return parse_ticker_dataframe(ticker_history_list) return parse_ticker_dataframe(ticker_history_list, "5m", True)
@pytest.fixture @pytest.fixture
@ -724,7 +731,7 @@ def tickers():
@pytest.fixture @pytest.fixture
def result(): def result():
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file: with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
return parse_ticker_dataframe(json.load(data_file)) return parse_ticker_dataframe(json.load(data_file), '1m', True)
# FIX: # FIX:
# Create an fixture/function # Create an fixture/function

View File

@ -1,25 +1,99 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
import logging import logging
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe, ohlcv_fill_up_missing_data
from freqtrade.data.history import load_pair_history
from freqtrade.optimize import validate_backtest_data, get_timeframe
from freqtrade.tests.conftest import log_has from freqtrade.tests.conftest import log_has
def test_dataframe_correct_length(result):
dataframe = parse_ticker_dataframe(result)
assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed
def test_dataframe_correct_columns(result): def test_dataframe_correct_columns(result):
assert result.columns.tolist() == \ assert result.columns.tolist() == ['date', 'open', 'high', 'low', 'close', 'volume']
['date', 'open', 'high', 'low', 'close', 'volume']
def test_parse_ticker_dataframe(ticker_history, caplog): def test_parse_ticker_dataframe(ticker_history_list, caplog):
columns = ['date', 'open', 'high', 'low', 'close', 'volume'] columns = ['date', 'open', 'high', 'low', 'close', 'volume']
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
# Test file with BV data # Test file with BV data
dataframe = parse_ticker_dataframe(ticker_history) dataframe = parse_ticker_dataframe(ticker_history_list, '5m', fill_missing=True)
assert dataframe.columns.tolist() == columns assert dataframe.columns.tolist() == columns
assert log_has('Parsing tickerlist to dataframe', caplog.record_tuples) assert log_has('Parsing tickerlist to dataframe', caplog.record_tuples)
def test_ohlcv_fill_up_missing_data(caplog):
data = load_pair_history(datadir=None,
ticker_interval='1m',
refresh_pairs=False,
pair='UNITTEST/BTC',
fill_up_missing=False)
caplog.set_level(logging.DEBUG)
data2 = ohlcv_fill_up_missing_data(data, '1m')
assert len(data2) > len(data)
# Column names should not change
assert (data.columns == data2.columns).all()
assert log_has(f"Missing data fillup: before: {len(data)} - after: {len(data2)}",
caplog.record_tuples)
# Test fillup actually fixes invalid backtest data
min_date, max_date = get_timeframe({'UNITTEST/BTC': data})
assert validate_backtest_data({'UNITTEST/BTC': data}, min_date, max_date, 1)
assert not validate_backtest_data({'UNITTEST/BTC': data2}, min_date, max_date, 1)
def test_ohlcv_fill_up_missing_data2(caplog):
ticker_interval = '5m'
ticks = [[
1511686200000, # 8:50:00
8.794e-05, # open
8.948e-05, # high
8.794e-05, # low
8.88e-05, # close
2255, # volume (in quote currency)
],
[
1511686500000, # 8:55:00
8.88e-05,
8.942e-05,
8.88e-05,
8.893e-05,
9911,
],
[
1511687100000, # 9:05:00
8.891e-05,
8.893e-05,
8.875e-05,
8.877e-05,
2251
],
[
1511687400000, # 9:10:00
8.877e-05,
8.883e-05,
8.895e-05,
8.817e-05,
123551
]
]
# Generate test-data without filling missing
data = parse_ticker_dataframe(ticks, ticker_interval, fill_missing=False)
assert len(data) == 3
caplog.set_level(logging.DEBUG)
data2 = ohlcv_fill_up_missing_data(data, ticker_interval)
assert len(data2) == 4
# 3rd candle has been filled
row = data2.loc[2, :]
assert row['volume'] == 0
# close shoult match close of previous candle
assert row['close'] == data.loc[1, 'close']
assert row['open'] == row['close']
assert row['high'] == row['close']
assert row['low'] == row['close']
# Column names should not change
assert (data.columns == data2.columns).all()
assert log_has(f"Missing data fillup: before: {len(data)} - after: {len(data2)}",
caplog.record_tuples)

View File

@ -0,0 +1,92 @@
from unittest.mock import MagicMock
from pandas import DataFrame
from freqtrade.data.dataprovider import DataProvider
from freqtrade.state import RunMode
from freqtrade.tests.conftest import get_patched_exchange
def test_ohlcv(mocker, default_conf, ticker_history):
default_conf["runmode"] = RunMode.DRY_RUN
tick_interval = default_conf["ticker_interval"]
exchange = get_patched_exchange(mocker, default_conf)
exchange._klines[("XRP/BTC", tick_interval)] = ticker_history
exchange._klines[("UNITTEST/BTC", tick_interval)] = ticker_history
dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.DRY_RUN
assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", tick_interval))
assert isinstance(dp.ohlcv("UNITTEST/BTC", tick_interval), DataFrame)
assert dp.ohlcv("UNITTEST/BTC", tick_interval) is not ticker_history
assert dp.ohlcv("UNITTEST/BTC", tick_interval, copy=False) is ticker_history
assert not dp.ohlcv("UNITTEST/BTC", tick_interval).empty
assert dp.ohlcv("NONESENSE/AAA", tick_interval).empty
# Test with and without parameter
assert dp.ohlcv("UNITTEST/BTC", tick_interval).equals(dp.ohlcv("UNITTEST/BTC"))
default_conf["runmode"] = RunMode.LIVE
dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.LIVE
assert isinstance(dp.ohlcv("UNITTEST/BTC", tick_interval), DataFrame)
default_conf["runmode"] = RunMode.BACKTEST
dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.BACKTEST
assert dp.ohlcv("UNITTEST/BTC", tick_interval).empty
def test_historic_ohlcv(mocker, default_conf, ticker_history):
historymock = MagicMock(return_value=ticker_history)
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
# exchange = get_patched_exchange(mocker, default_conf)
dp = DataProvider(default_conf, None)
data = dp.historic_ohlcv("UNITTEST/BTC", "5m")
assert isinstance(data, DataFrame)
assert historymock.call_count == 1
assert historymock.call_args_list[0][1]["datadir"] is None
assert historymock.call_args_list[0][1]["refresh_pairs"] is False
assert historymock.call_args_list[0][1]["ticker_interval"] == "5m"
def test_available_pairs(mocker, default_conf, ticker_history):
exchange = get_patched_exchange(mocker, default_conf)
tick_interval = default_conf["ticker_interval"]
exchange._klines[("XRP/BTC", tick_interval)] = ticker_history
exchange._klines[("UNITTEST/BTC", tick_interval)] = ticker_history
dp = DataProvider(default_conf, exchange)
assert len(dp.available_pairs) == 2
assert dp.available_pairs == [
("XRP/BTC", tick_interval),
("UNITTEST/BTC", tick_interval),
]
def test_refresh(mocker, default_conf, ticker_history):
refresh_mock = MagicMock()
mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock)
exchange = get_patched_exchange(mocker, default_conf, id="binance")
tick_interval = default_conf["ticker_interval"]
pairs = [("XRP/BTC", tick_interval), ("UNITTEST/BTC", tick_interval)]
pairs_non_trad = [("ETH/USDT", tick_interval), ("BTC/TUSD", "1h")]
dp = DataProvider(default_conf, exchange)
dp.refresh(pairs)
assert refresh_mock.call_count == 1
assert len(refresh_mock.call_args[0]) == 1
assert len(refresh_mock.call_args[0][0]) == len(pairs)
assert refresh_mock.call_args[0][0] == pairs
refresh_mock.reset_mock()
dp.refresh(pairs, pairs_non_trad)
assert refresh_mock.call_count == 1
assert len(refresh_mock.call_args[0]) == 1
assert len(refresh_mock.call_args[0][0]) == len(pairs) + len(pairs_non_trad)
assert refresh_mock.call_args[0][0] == pairs + pairs_non_trad

View File

@ -450,7 +450,7 @@ def test_trim_tickerlist() -> None:
assert not ticker assert not ticker
def test_file_dump_json() -> None: def test_file_dump_json_tofile() -> None:
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', file = os.path.join(os.path.dirname(__file__), '..', 'testdata',
'test_{id}.json'.format(id=str(uuid.uuid4()))) 'test_{id}.json'.format(id=str(uuid.uuid4())))
data = {'bar': 'foo'} data = {'bar': 'foo'}

View File

@ -281,8 +281,8 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals
123.45 123.45
] for x in range(0, 500)] ] for x in range(0, 500)]
pairdata = {'NEO/BTC': parse_ticker_dataframe(ETHBTC), pairdata = {'NEO/BTC': parse_ticker_dataframe(ETHBTC, '1h', fill_missing=True),
'LTC/BTC': parse_ticker_dataframe(LTCBTC)} 'LTC/BTC': parse_ticker_dataframe(LTCBTC, '1h', fill_missing=True)}
return pairdata return pairdata

View File

@ -765,7 +765,7 @@ def test_get_history(default_conf, mocker, caplog):
pair = 'ETH/BTC' pair = 'ETH/BTC'
async def mock_candle_hist(pair, tick_interval, since_ms): async def mock_candle_hist(pair, tick_interval, since_ms):
return pair, tick return pair, tick_interval, tick
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls # one_call calculation * 1.8 should do 2 calls
@ -778,7 +778,7 @@ def test_get_history(default_conf, mocker, caplog):
assert len(ret) == 2 assert len(ret) == 2
def test_refresh_tickers(mocker, default_conf, caplog) -> None: def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
tick = [ tick = [
[ [
(arrow.utcnow().timestamp - 1) * 1000, # unix timestamp ms (arrow.utcnow().timestamp - 1) * 1000, # unix timestamp ms
@ -802,12 +802,12 @@ def test_refresh_tickers(mocker, default_conf, caplog) -> None:
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
exchange._api_async.fetch_ohlcv = get_mock_coro(tick) exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
pairs = ['IOTA/ETH', 'XRP/ETH'] pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]
# empty dicts # empty dicts
assert not exchange._klines assert not exchange._klines
exchange.refresh_tickers(['IOTA/ETH', 'XRP/ETH'], '5m') exchange.refresh_latest_ohlcv(pairs)
assert log_has(f'Refreshing klines for {len(pairs)} pairs', caplog.record_tuples) assert log_has(f'Refreshing ohlcv data for {len(pairs)} pairs', caplog.record_tuples)
assert exchange._klines assert exchange._klines
assert exchange._api_async.fetch_ohlcv.call_count == 2 assert exchange._api_async.fetch_ohlcv.call_count == 2
for pair in pairs: for pair in pairs:
@ -822,10 +822,11 @@ def test_refresh_tickers(mocker, default_conf, caplog) -> None:
assert exchange.klines(pair, copy=False) is exchange.klines(pair, copy=False) assert exchange.klines(pair, copy=False) is exchange.klines(pair, copy=False)
# test caching # test caching
exchange.refresh_tickers(['IOTA/ETH', 'XRP/ETH'], '5m') exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')])
assert exchange._api_async.fetch_ohlcv.call_count == 2 assert exchange._api_async.fetch_ohlcv.call_count == 2
assert log_has(f"Using cached klines data for {pairs[0]} ...", caplog.record_tuples) assert log_has(f"Using cached ohlcv data for {pairs[0][0]}, {pairs[0][1]} ...",
caplog.record_tuples)
@pytest.mark.asyncio @pytest.mark.asyncio
@ -850,11 +851,12 @@ async def test__async_get_candle_history(default_conf, mocker, caplog):
pair = 'ETH/BTC' pair = 'ETH/BTC'
res = await exchange._async_get_candle_history(pair, "5m") res = await exchange._async_get_candle_history(pair, "5m")
assert type(res) is tuple assert type(res) is tuple
assert len(res) == 2 assert len(res) == 3
assert res[0] == pair assert res[0] == pair
assert res[1] == tick assert res[1] == "5m"
assert res[2] == tick
assert exchange._api_async.fetch_ohlcv.call_count == 1 assert exchange._api_async.fetch_ohlcv.call_count == 1
assert not log_has(f"Using cached klines data for {pair} ...", caplog.record_tuples) assert not log_has(f"Using cached ohlcv data for {pair} ...", caplog.record_tuples)
# exchange = Exchange(default_conf) # exchange = Exchange(default_conf)
await async_ccxt_exception(mocker, default_conf, MagicMock(), await async_ccxt_exception(mocker, default_conf, MagicMock(),
@ -883,44 +885,38 @@ async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
pair = 'ETH/BTC' pair = 'ETH/BTC'
res = await exchange._async_get_candle_history(pair, "5m") res = await exchange._async_get_candle_history(pair, "5m")
assert type(res) is tuple assert type(res) is tuple
assert len(res) == 2 assert len(res) == 3
assert res[0] == pair assert res[0] == pair
assert res[1] == tick assert res[1] == "5m"
assert res[2] == tick
assert exchange._api_async.fetch_ohlcv.call_count == 1 assert exchange._api_async.fetch_ohlcv.call_count == 1
@pytest.mark.asyncio def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
async def test_async_get_candles_history(default_conf, mocker):
tick = [
[
1511686200000, # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
]
]
async def mock_get_candle_hist(pair, tick_interval, since_ms=None): async def mock_get_candle_hist(pair, *args, **kwargs):
return (pair, tick) if pair == 'ETH/BTC':
return [[]]
else:
raise TypeError()
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
# Monkey-patch async function
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
exchange._async_get_candle_history = Mock(wraps=mock_get_candle_hist) # Monkey-patch async function with empty result
exchange._api_async.fetch_ohlcv = MagicMock(side_effect=mock_get_candle_hist)
pairs = [("ETH/BTC", "5m"), ("XRP/BTC", "5m")]
res = exchange.refresh_latest_ohlcv(pairs)
assert exchange._klines
assert exchange._api_async.fetch_ohlcv.call_count == 2
pairs = ['ETH/BTC', 'XRP/BTC']
res = await exchange.async_get_candles_history(pairs, "5m")
assert type(res) is list assert type(res) is list
assert len(res) == 2 assert len(res) == 2
assert type(res[0]) is tuple # Test that each is in list at least once as order is not guaranteed
assert res[0][0] == pairs[0] assert type(res[0]) is tuple or type(res[1]) is tuple
assert res[0][1] == tick assert type(res[0]) is TypeError or type(res[1]) is TypeError
assert res[1][0] == pairs[1] assert log_has("Error loading ETH/BTC. Result was [[]].", caplog.record_tuples)
assert res[1][1] == tick assert log_has("Async code raised an exception: TypeError", caplog.record_tuples)
assert exchange._async_get_candle_history.call_count == 2
def test_get_order_book(default_conf, mocker, order_book_l2): def test_get_order_book(default_conf, mocker, order_book_l2):
@ -986,7 +982,7 @@ async def test___async_get_candle_history_sort(default_conf, mocker):
# Test the ticker history sort # Test the ticker history sort
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert res[0] == 'ETH/BTC' assert res[0] == 'ETH/BTC'
ticks = res[1] ticks = res[2]
assert sort_mock.call_count == 1 assert sort_mock.call_count == 1
assert ticks[0][0] == 1527830400000 assert ticks[0][0] == 1527830400000
@ -1023,7 +1019,8 @@ async def test___async_get_candle_history_sort(default_conf, mocker):
# Test the ticker history sort # Test the ticker history sort
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert res[0] == 'ETH/BTC' assert res[0] == 'ETH/BTC'
ticks = res[1] assert res[1] == default_conf['ticker_interval']
ticks = res[2]
# Sorted not called again - data is already in order # Sorted not called again - data is already in order
assert sort_mock.call_count == 0 assert sort_mock.call_count == 0
assert ticks[0][0] == 1527827700000 assert ticks[0][0] == 1527827700000

View File

@ -18,6 +18,7 @@ from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.optimize import get_timeframe from freqtrade.optimize import get_timeframe
from freqtrade.optimize.backtesting import (Backtesting, setup_configuration, from freqtrade.optimize.backtesting import (Backtesting, setup_configuration,
start) start)
from freqtrade.state import RunMode
from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.default_strategy import DefaultStrategy
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.conftest import log_has, patch_exchange
@ -75,7 +76,7 @@ def load_data_test(what):
pair[x][5] # Keep old volume pair[x][5] # Keep old volume
] for x in range(0, datalen) ] for x in range(0, datalen)
] ]
return {'UNITTEST/BTC': parse_ticker_dataframe(data)} return {'UNITTEST/BTC': parse_ticker_dataframe(data, '1m', fill_missing=True)}
def simple_backtest(config, contour, num_results, mocker) -> None: def simple_backtest(config, contour, num_results, mocker) -> None:
@ -104,7 +105,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None:
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False,
timerange=None, exchange=None): timerange=None, exchange=None):
tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange) tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata)} pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', fill_missing=True)}
return pairdata return pairdata
@ -200,12 +201,15 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'timerange' not in config assert 'timerange' not in config
assert 'export' not in config assert 'export' not in config
assert 'runmode' in config
assert config['runmode'] == RunMode.BACKTEST
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x)
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@ -229,6 +233,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'exchange' in config assert 'exchange' in config
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert config['runmode'] == RunMode.BACKTEST
assert log_has( assert log_has(
'Using data folder: {} ...'.format(config['datadir']), 'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples
@ -322,15 +328,15 @@ def test_backtesting_init(mocker, default_conf) -> None:
assert backtesting.fee == 0.5 assert backtesting.fee == 0.5
def test_tickerdata_to_dataframe(default_conf, mocker) -> None: def test_tickerdata_to_dataframe_bt(default_conf, mocker) -> None:
patch_exchange(mocker) patch_exchange(mocker)
timerange = TimeRange(None, 'line', 0, -100) timerange = TimeRange(None, 'line', 0, -100)
tick = history.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange) tick = history.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)} tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', fill_missing=True)}
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
data = backtesting.strategy.tickerdata_to_dataframe(tickerlist) data = backtesting.strategy.tickerdata_to_dataframe(tickerlist)
assert len(data['UNITTEST/BTC']) == 99 assert len(data['UNITTEST/BTC']) == 102
# Load strategy to compare the result between Backtesting function and strategy are the same # Load strategy to compare the result between Backtesting function and strategy are the same
strategy = DefaultStrategy(default_conf) strategy = DefaultStrategy(default_conf)
@ -340,6 +346,7 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
def test_generate_text_table(default_conf, mocker): def test_generate_text_table(default_conf, mocker):
patch_exchange(mocker) patch_exchange(mocker)
default_conf['max_open_trades'] = 2
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
results = pd.DataFrame( results = pd.DataFrame(
@ -355,13 +362,13 @@ def test_generate_text_table(default_conf, mocker):
result_str = ( result_str = (
'| pair | buy count | avg profit % | cum profit % | ' '| pair | buy count | avg profit % | cum profit % | '
'total profit BTC | avg duration | profit | loss |\n' 'tot profit BTC | tot profit % | avg duration | profit | loss |\n'
'|:--------|------------:|---------------:|---------------:|' '|:--------|------------:|---------------:|---------------:|'
'-------------------:|:---------------|---------:|-------:|\n' '-----------------:|---------------:|:---------------|---------:|-------:|\n'
'| ETH/BTC | 2 | 15.00 | 30.00 | ' '| ETH/BTC | 2 | 15.00 | 30.00 | '
'0.60000000 | 0:20:00 | 2 | 0 |\n' '0.60000000 | 15.00 | 0:20:00 | 2 | 0 |\n'
'| TOTAL | 2 | 15.00 | 30.00 | ' '| TOTAL | 2 | 15.00 | 30.00 | '
'0.60000000 | 0:20:00 | 2 | 0 |' '0.60000000 | 15.00 | 0:20:00 | 2 | 0 |'
) )
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
@ -397,6 +404,7 @@ def test_generate_text_table_strategyn(default_conf, mocker):
Test Backtesting.generate_text_table_sell_reason() method Test Backtesting.generate_text_table_sell_reason() method
""" """
patch_exchange(mocker) patch_exchange(mocker)
default_conf['max_open_trades'] = 2
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
results = {} results = {}
results['ETH/BTC'] = pd.DataFrame( results['ETH/BTC'] = pd.DataFrame(
@ -424,13 +432,13 @@ def test_generate_text_table_strategyn(default_conf, mocker):
result_str = ( result_str = (
'| Strategy | buy count | avg profit % | cum profit % ' '| Strategy | buy count | avg profit % | cum profit % '
'| total profit BTC | avg duration | profit | loss |\n' '| tot profit BTC | tot profit % | avg duration | profit | loss |\n'
'|:-----------|------------:|---------------:|---------------:' '|:-----------|------------:|---------------:|---------------:'
'|-------------------:|:---------------|---------:|-------:|\n' '|-----------------:|---------------:|:---------------|---------:|-------:|\n'
'| ETH/BTC | 3 | 20.00 | 60.00 ' '| ETH/BTC | 3 | 20.00 | 60.00 '
'| 1.10000000 | 0:17:00 | 3 | 0 |\n' '| 1.10000000 | 30.00 | 0:17:00 | 3 | 0 |\n'
'| LTC/BTC | 3 | 30.00 | 90.00 ' '| LTC/BTC | 3 | 30.00 | 90.00 '
'| 1.30000000 | 0:20:00 | 3 | 0 |' '| 1.30000000 | 45.00 | 0:20:00 | 3 | 0 |'
) )
print(backtesting._generate_text_table_strategy(all_results=results)) print(backtesting._generate_text_table_strategy(all_results=results))
assert backtesting._generate_text_table_strategy(all_results=results) == result_str assert backtesting._generate_text_table_strategy(all_results=results) == result_str
@ -442,7 +450,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
mocker.patch('freqtrade.data.history.load_data', mocked_load_data) mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe) mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe)
mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting', 'freqtrade.optimize.backtesting.Backtesting',
@ -477,7 +485,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={})) mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={}))
mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe) mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe)
mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting', 'freqtrade.optimize.backtesting.Backtesting',
@ -526,13 +534,14 @@ def test_backtest(default_conf, fee, mocker) -> None:
{'pair': [pair, pair], {'pair': [pair, pair],
'profit_percent': [0.0, 0.0], 'profit_percent': [0.0, 0.0],
'profit_abs': [0.0, 0.0], 'profit_abs': [0.0, 0.0],
'open_time': [Arrow(2018, 1, 29, 18, 40, 0).datetime, 'open_time': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
Arrow(2018, 1, 30, 3, 30, 0).datetime], Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True
'close_time': [Arrow(2018, 1, 29, 22, 35, 0).datetime, ),
Arrow(2018, 1, 30, 4, 15, 0).datetime], 'close_time': pd.to_datetime([Arrow(2018, 1, 29, 22, 35, 0).datetime,
Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True),
'open_index': [78, 184], 'open_index': [78, 184],
'close_index': [125, 193], 'close_index': [125, 192],
'trade_duration': [235, 45], 'trade_duration': [235, 40],
'open_at_end': [False, False], 'open_at_end': [False, False],
'open_rate': [0.104445, 0.10302485], 'open_rate': [0.104445, 0.10302485],
'close_rate': [0.104969, 0.103541], 'close_rate': [0.104969, 0.103541],
@ -593,7 +602,7 @@ def test_processed(default_conf, mocker) -> None:
def test_backtest_pricecontours(default_conf, fee, mocker) -> None: def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
tests = [['raise', 18], ['lower', 0], ['sine', 19]] tests = [['raise', 19], ['lower', 0], ['sine', 18]]
# We need to enable sell-signal - otherwise it sells on ROI!! # We need to enable sell-signal - otherwise it sells on ROI!!
default_conf['experimental'] = {"use_sell_signal": True} default_conf['experimental'] = {"use_sell_signal": True}

View File

@ -7,6 +7,7 @@ from typing import List
from freqtrade.edge import PairInfo from freqtrade.edge import PairInfo
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.optimize.edge_cli import (EdgeCli, setup_configuration, start) from freqtrade.optimize.edge_cli import (EdgeCli, setup_configuration, start)
from freqtrade.state import RunMode
from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.conftest import log_has, patch_exchange
@ -26,6 +27,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
] ]
config = setup_configuration(get_args(args)) config = setup_configuration(get_args(args))
assert config['runmode'] == RunMode.EDGECLI
assert 'max_open_trades' in config assert 'max_open_trades' in config
assert 'stake_currency' in config assert 'stake_currency' in config
assert 'stake_amount' in config assert 'stake_amount' in config
@ -46,10 +49,11 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'stoploss_range' not in config assert 'stoploss_range' not in config
def test_setup_configuration_with_arguments(mocker, edge_conf, caplog) -> None: def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(edge_conf) read_data=json.dumps(edge_conf)
)) ))
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x)
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@ -69,6 +73,7 @@ def test_setup_configuration_with_arguments(mocker, edge_conf, caplog) -> None:
assert 'exchange' in config assert 'exchange' in config
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert config['runmode'] == RunMode.EDGECLI
assert log_has( assert log_has(
'Using data folder: {} ...'.format(config['datadir']), 'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples

View File

@ -9,7 +9,8 @@ import pytest
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.data.history import load_tickerdata_file from freqtrade.data.history import load_tickerdata_file
from freqtrade.optimize.hyperopt import Hyperopt, start from freqtrade.optimize.hyperopt import Hyperopt, start
from freqtrade.resolvers import StrategyResolver from freqtrade.optimize.default_hyperopt import DefaultHyperOpts
from freqtrade.resolvers import StrategyResolver, HyperOptResolver
from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.conftest import log_has, patch_exchange
from freqtrade.tests.optimize.test_backtesting import get_args from freqtrade.tests.optimize.test_backtesting import get_args
@ -38,6 +39,28 @@ def create_trials(mocker, hyperopt) -> None:
return [{'loss': 1, 'result': 'foo', 'params': {}}] return [{'loss': 1, 'result': 'foo', 'params': {}}]
def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
hyperopts = DefaultHyperOpts
delattr(hyperopts, 'populate_buy_trend')
delattr(hyperopts, 'populate_sell_trend')
mocker.patch(
'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt',
MagicMock(return_value=hyperopts)
)
x = HyperOptResolver(default_conf, ).hyperopt
assert not hasattr(x, 'populate_buy_trend')
assert not hasattr(x, 'populate_sell_trend')
assert log_has("Custom Hyperopt does not provide populate_sell_trend. "
"Using populate_sell_trend from DefaultStrategy.", caplog.record_tuples)
assert log_has("Custom Hyperopt does not provide populate_buy_trend. "
"Using populate_buy_trend from DefaultStrategy.", caplog.record_tuples)
def test_start(mocker, default_conf, caplog) -> None: def test_start(mocker, default_conf, caplog) -> None:
start_mock = MagicMock() start_mock = MagicMock()
mocker.patch( mocker.patch(
@ -201,7 +224,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
hyperopt.start() hyperopt.start()
parallel.assert_called_once() parallel.assert_called_once()
assert 'Best result:\nfoo result\nwith values:\n{}' in caplog.text assert 'Best result:\nfoo result\nwith values:\n\n' in caplog.text
assert dumper.called assert dumper.called
@ -243,7 +266,7 @@ def test_has_space(hyperopt):
def test_populate_indicators(hyperopt) -> None: def test_populate_indicators(hyperopt) -> None:
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)} tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', fill_missing=True)}
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist) dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'}) {'pair': 'UNITTEST/BTC'})
@ -256,7 +279,7 @@ def test_populate_indicators(hyperopt) -> None:
def test_buy_strategy_generator(hyperopt) -> None: def test_buy_strategy_generator(hyperopt) -> None:
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)} tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', fill_missing=True)}
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist) dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'}) {'pair': 'UNITTEST/BTC'})
@ -312,6 +335,15 @@ def test_generate_optimizer(mocker, default_conf) -> None:
'mfi-enabled': False, 'mfi-enabled': False,
'rsi-enabled': False, 'rsi-enabled': False,
'trigger': 'macd_cross_signal', 'trigger': 'macd_cross_signal',
'sell-adx-value': 0,
'sell-fastd-value': 75,
'sell-mfi-value': 0,
'sell-rsi-value': 0,
'sell-adx-enabled': False,
'sell-fastd-enabled': True,
'sell-mfi-enabled': False,
'sell-rsi-enabled': False,
'sell-trigger': 'macd_cross_signal',
'roi_t1': 60.0, 'roi_t1': 60.0,
'roi_t2': 30.0, 'roi_t2': 30.0,
'roi_t3': 20.0, 'roi_t3': 20.0,

View File

@ -30,7 +30,8 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None:
history.load_data( history.load_data(
datadir=None, datadir=None,
ticker_interval='1m', ticker_interval='1m',
pairs=['UNITTEST/BTC'] pairs=['UNITTEST/BTC'],
fill_up_missing=False
) )
) )
min_date, max_date = optimize.get_timeframe(data) min_date, max_date = optimize.get_timeframe(data)

View File

@ -117,7 +117,7 @@ def test_fiat_convert_get_price(mocker):
assert fiat_convert._pairs[0].crypto_symbol == 'BTC' assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
assert fiat_convert._pairs[0].fiat_symbol == 'USD' assert fiat_convert._pairs[0].fiat_symbol == 'USD'
assert fiat_convert._pairs[0].price == 28000.0 assert fiat_convert._pairs[0].price == 28000.0
assert fiat_convert._pairs[0]._expiration is not 0 assert fiat_convert._pairs[0]._expiration != 0
assert len(fiat_convert._pairs) == 1 assert len(fiat_convert._pairs) == 1
# Verify the cached is used # Verify the cached is used

View File

@ -10,7 +10,7 @@ from freqtrade.strategy.default_strategy import DefaultStrategy
@pytest.fixture @pytest.fixture
def result(): def result():
with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file: with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file:
return parse_ticker_dataframe(json.load(data_file)) return parse_ticker_dataframe(json.load(data_file), '1m', fill_missing=True)
def test_default_strategy_structure(): def test_default_strategy_structure():

View File

@ -111,14 +111,19 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
timerange = TimeRange(None, 'line', 0, -100) timerange = TimeRange(None, 'line', 0, -100)
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange) tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)} tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', True)}
data = strategy.tickerdata_to_dataframe(tickerlist) data = strategy.tickerdata_to_dataframe(tickerlist)
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed assert len(data['UNITTEST/BTC']) == 102 # partial candle was removed
def test_min_roi_reached(default_conf, fee) -> None: def test_min_roi_reached(default_conf, fee) -> None:
# Use list to confirm sequence does not matter
min_roi_list = [{20: 0.05, 55: 0.01, 0: 0.1},
{0: 0.1, 20: 0.05, 55: 0.01}]
for roi in min_roi_list:
strategy = DefaultStrategy(default_conf) strategy = DefaultStrategy(default_conf)
strategy.minimal_roi = {0: 0.1, 20: 0.05, 55: 0.01} strategy.minimal_roi = roi
trade = Trade( trade = Trade(
pair='ETH/BTC', pair='ETH/BTC',
stake_amount=0.001, stake_amount=0.001,
@ -129,8 +134,8 @@ def test_min_roi_reached(default_conf, fee) -> None:
open_rate=1, open_rate=1,
) )
assert not strategy.min_roi_reached(trade, 0.01, arrow.utcnow().shift(minutes=-55).datetime) assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime)
assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-55).datetime) assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime)
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime) assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-39).datetime) assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-39).datetime)
@ -139,6 +144,47 @@ def test_min_roi_reached(default_conf, fee) -> None:
assert strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-1).datetime) assert strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-1).datetime)
def test_min_roi_reached2(default_conf, fee) -> None:
# test with ROI raising after last interval
min_roi_list = [{20: 0.07,
30: 0.05,
55: 0.30,
0: 0.1
},
{0: 0.1,
20: 0.07,
30: 0.05,
55: 0.30
},
]
for roi in min_roi_list:
strategy = DefaultStrategy(default_conf)
strategy.minimal_roi = roi
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
open_date=arrow.utcnow().shift(hours=-1).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
open_rate=1,
)
assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime)
assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime)
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
assert strategy.min_roi_reached(trade, 0.071, arrow.utcnow().shift(minutes=-39).datetime)
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-26).datetime)
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-26).datetime)
# Should not trigger with 20% profit since after 55 minutes only 30% is active.
assert not strategy.min_roi_reached(trade, 0.20, arrow.utcnow().shift(minutes=-2).datetime)
assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x) ind_mock = MagicMock(side_effect=lambda x, meta: x)
@ -158,7 +204,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
assert buy_mock.call_count == 1 assert buy_mock.call_count == 1
assert log_has('TA Analysis Launched', caplog.record_tuples) assert log_has('TA Analysis Launched', caplog.record_tuples)
assert not log_has('Skippinig TA Analysis for already analyzed candle', assert not log_has('Skipping TA Analysis for already analyzed candle',
caplog.record_tuples) caplog.record_tuples)
caplog.clear() caplog.clear()
@ -168,7 +214,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
assert buy_mock.call_count == 2 assert buy_mock.call_count == 2
assert buy_mock.call_count == 2 assert buy_mock.call_count == 2
assert log_has('TA Analysis Launched', caplog.record_tuples) assert log_has('TA Analysis Launched', caplog.record_tuples)
assert not log_has('Skippinig TA Analysis for already analyzed candle', assert not log_has('Skipping TA Analysis for already analyzed candle',
caplog.record_tuples) caplog.record_tuples)
@ -196,7 +242,7 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
assert buy_mock.call_count == 1 assert buy_mock.call_count == 1
assert buy_mock.call_count == 1 assert buy_mock.call_count == 1
assert log_has('TA Analysis Launched', caplog.record_tuples) assert log_has('TA Analysis Launched', caplog.record_tuples)
assert not log_has('Skippinig TA Analysis for already analyzed candle', assert not log_has('Skipping TA Analysis for already analyzed candle',
caplog.record_tuples) caplog.record_tuples)
caplog.clear() caplog.clear()
@ -211,5 +257,5 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
assert ret['buy'].sum() == 0 assert ret['buy'].sum() == 0
assert ret['sell'].sum() == 0 assert ret['sell'].sum() == 0
assert not log_has('TA Analysis Launched', caplog.record_tuples) assert not log_has('TA Analysis Launched', caplog.record_tuples)
assert log_has('Skippinig TA Analysis for already analyzed candle', assert log_has('Skipping TA Analysis for already analyzed candle',
caplog.record_tuples) caplog.record_tuples)

View File

@ -150,6 +150,45 @@ def test_strategy_override_stoploss(caplog):
) in caplog.record_tuples ) in caplog.record_tuples
def test_strategy_override_trailing_stop(caplog):
caplog.set_level(logging.INFO)
config = {
'strategy': 'DefaultStrategy',
'trailing_stop': True
}
resolver = StrategyResolver(config)
assert resolver.strategy.trailing_stop
assert isinstance(resolver.strategy.trailing_stop, bool)
assert ('freqtrade.resolvers.strategy_resolver',
logging.INFO,
"Override strategy 'trailing_stop' with value in config file: True."
) in caplog.record_tuples
def test_strategy_override_trailing_stop_positive(caplog):
caplog.set_level(logging.INFO)
config = {
'strategy': 'DefaultStrategy',
'trailing_stop_positive': -0.1,
'trailing_stop_positive_offset': -0.2
}
resolver = StrategyResolver(config)
assert resolver.strategy.trailing_stop_positive == -0.1
assert ('freqtrade.resolvers.strategy_resolver',
logging.INFO,
"Override strategy 'trailing_stop_positive' with value in config file: -0.1."
) in caplog.record_tuples
assert resolver.strategy.trailing_stop_positive_offset == -0.2
assert ('freqtrade.resolvers.strategy_resolver',
logging.INFO,
"Override strategy 'trailing_stop_positive' with value in config file: -0.1."
) in caplog.record_tuples
def test_strategy_override_ticker_interval(caplog): def test_strategy_override_ticker_interval(caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
@ -178,8 +217,7 @@ def test_strategy_override_process_only_new_candles(caplog):
assert resolver.strategy.process_only_new_candles assert resolver.strategy.process_only_new_candles
assert ('freqtrade.resolvers.strategy_resolver', assert ('freqtrade.resolvers.strategy_resolver',
logging.INFO, logging.INFO,
"Override process_only_new_candles 'process_only_new_candles' " "Override strategy 'process_only_new_candles' with value in config file: True."
"with value in config file: True."
) in caplog.record_tuples ) in caplog.record_tuples
@ -256,6 +294,62 @@ def test_strategy_override_order_tif(caplog):
StrategyResolver(config) StrategyResolver(config)
def test_strategy_override_use_sell_signal(caplog):
caplog.set_level(logging.INFO)
config = {
'strategy': 'DefaultStrategy',
}
resolver = StrategyResolver(config)
assert not resolver.strategy.use_sell_signal
assert isinstance(resolver.strategy.use_sell_signal, bool)
# must be inserted to configuration
assert 'use_sell_signal' in config['experimental']
assert not config['experimental']['use_sell_signal']
config = {
'strategy': 'DefaultStrategy',
'experimental': {
'use_sell_signal': True,
},
}
resolver = StrategyResolver(config)
assert resolver.strategy.use_sell_signal
assert isinstance(resolver.strategy.use_sell_signal, bool)
assert ('freqtrade.resolvers.strategy_resolver',
logging.INFO,
"Override strategy 'use_sell_signal' with value in config file: True."
) in caplog.record_tuples
def test_strategy_override_use_sell_profit_only(caplog):
caplog.set_level(logging.INFO)
config = {
'strategy': 'DefaultStrategy',
}
resolver = StrategyResolver(config)
assert not resolver.strategy.sell_profit_only
assert isinstance(resolver.strategy.sell_profit_only, bool)
# must be inserted to configuration
assert 'sell_profit_only' in config['experimental']
assert not config['experimental']['sell_profit_only']
config = {
'strategy': 'DefaultStrategy',
'experimental': {
'sell_profit_only': True,
},
}
resolver = StrategyResolver(config)
assert resolver.strategy.sell_profit_only
assert isinstance(resolver.strategy.sell_profit_only, bool)
assert ('freqtrade.resolvers.strategy_resolver',
logging.INFO,
"Override strategy 'sell_profit_only' with value in config file: True."
) in caplog.record_tuples
def test_deprecate_populate_indicators(result): def test_deprecate_populate_indicators(result):
default_location = path.join(path.dirname(path.realpath(__file__))) default_location = path.join(path.dirname(path.realpath(__file__)))
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',

View File

@ -47,7 +47,7 @@ def test_scripts_options() -> None:
arguments = Arguments(['-p', 'ETH/BTC'], '') arguments = Arguments(['-p', 'ETH/BTC'], '')
arguments.scripts_options() arguments.scripts_options()
args = arguments.get_parsed_arg() args = arguments.get_parsed_arg()
assert args.pair == 'ETH/BTC' assert args.pairs == 'ETH/BTC'
def test_parse_args_version() -> None: def test_parse_args_version() -> None:

View File

@ -13,6 +13,7 @@ from freqtrade import OperationalException
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration, set_loggers from freqtrade.configuration import Configuration, set_loggers
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
from freqtrade.state import RunMode
from freqtrade.tests.conftest import log_has from freqtrade.tests.conftest import log_has
@ -73,11 +74,12 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) ->
args = Arguments([], '').get_parsed_arg() args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
print(validated_conf)
assert validated_conf['max_open_trades'] > 999999999 assert validated_conf['max_open_trades'] > 999999999
assert validated_conf['max_open_trades'] == float('inf') assert validated_conf['max_open_trades'] == float('inf')
assert log_has('Validating configuration ...', caplog.record_tuples) assert log_has('Validating configuration ...', caplog.record_tuples)
assert "runmode" in validated_conf
assert validated_conf['runmode'] == RunMode.DRY_RUN
def test_load_config_file_exception(mocker) -> None: def test_load_config_file_exception(mocker) -> None:
@ -125,6 +127,43 @@ def test_load_config_with_params(default_conf, mocker) -> None:
assert validated_conf.get('strategy_path') == '/some/path' assert validated_conf.get('strategy_path') == '/some/path'
assert validated_conf.get('db_url') == 'sqlite:///someurl' assert validated_conf.get('db_url') == 'sqlite:///someurl'
# Test conf provided db_url prod
conf = default_conf.copy()
conf["dry_run"] = False
conf["db_url"] = "sqlite:///path/to/db.sqlite"
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf)
))
arglist = [
'--strategy', 'TestStrategy',
'--strategy-path', '/some/path'
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
assert validated_conf.get('db_url') == "sqlite:///path/to/db.sqlite"
# Test conf provided db_url dry_run
conf = default_conf.copy()
conf["dry_run"] = True
conf["db_url"] = "sqlite:///path/to/db.sqlite"
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf)
))
arglist = [
'--strategy', 'TestStrategy',
'--strategy-path', '/some/path'
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
assert validated_conf.get('db_url') == "sqlite:///path/to/db.sqlite"
# Test args provided db_url prod
conf = default_conf.copy() conf = default_conf.copy()
conf["dry_run"] = False conf["dry_run"] = False
del conf["db_url"] del conf["db_url"]
@ -141,8 +180,10 @@ def test_load_config_with_params(default_conf, mocker) -> None:
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
assert validated_conf.get('db_url') == DEFAULT_DB_PROD_URL assert validated_conf.get('db_url') == DEFAULT_DB_PROD_URL
assert "runmode" in validated_conf
assert validated_conf['runmode'] == RunMode.LIVE
# Test dry=run with ProdURL # Test args provided db_url dry_run
conf = default_conf.copy() conf = default_conf.copy()
conf["dry_run"] = True conf["dry_run"] = True
conf["db_url"] = DEFAULT_DB_PROD_URL conf["db_url"] = DEFAULT_DB_PROD_URL
@ -247,6 +288,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x)
arglist = [ arglist = [
'--config', 'config.json', '--config', 'config.json',
@ -328,8 +370,9 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args, RunMode.BACKTEST)
config = configuration.get_config() config = configuration.get_config()
assert config['runmode'] == RunMode.BACKTEST
assert 'max_open_trades' in config assert 'max_open_trades' in config
assert 'stake_currency' in config assert 'stake_currency' in config
assert 'stake_amount' in config assert 'stake_amount' in config
@ -374,7 +417,7 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args, RunMode.HYPEROPT)
config = configuration.get_config() config = configuration.get_config()
assert 'epochs' in config assert 'epochs' in config
@ -385,6 +428,8 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
assert 'spaces' in config assert 'spaces' in config
assert config['spaces'] == ['all'] assert config['spaces'] == ['all']
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples) assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
assert "runmode" in config
assert config['runmode'] == RunMode.HYPEROPT
def test_check_exchange(default_conf, caplog) -> None: def test_check_exchange(default_conf, caplog) -> None:
@ -487,3 +532,13 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
def test_validate_default_conf(default_conf) -> None: def test_validate_default_conf(default_conf) -> None:
validate(default_conf, constants.CONF_SCHEMA, Draft4Validator) validate(default_conf, constants.CONF_SCHEMA, Draft4Validator)
def test__create_datadir(mocker, default_conf, caplog) -> None:
mocker.patch('os.path.isdir', MagicMock(return_value=False))
md = MagicMock()
mocker.patch('os.makedirs', md)
cfg = Configuration(Namespace())
cfg._create_datadir(default_conf, '/foo/bar')
assert md.call_args[0][0] == "/foo/bar"
assert log_has('Created data directory: /foo/bar', caplog.record_tuples)

View File

@ -18,7 +18,7 @@ from freqtrade.persistence import Trade
from freqtrade.rpc import RPCMessageType from freqtrade.rpc import RPCMessageType
from freqtrade.state import State from freqtrade.state import State
from freqtrade.strategy.interface import SellType, SellCheckTuple from freqtrade.strategy.interface import SellType, SellCheckTuple
from freqtrade.tests.conftest import log_has, patch_exchange, patch_edge, patch_wallet from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange, patch_edge, patch_wallet
# Functions for recurrent object patching # Functions for recurrent object patching
@ -43,7 +43,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None:
:return: None :return: None
""" """
freqtrade.strategy.get_signal = lambda e, s, t: value freqtrade.strategy.get_signal = lambda e, s, t: value
freqtrade.exchange.refresh_tickers = lambda p, i: None freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
def patch_RPCManager(mocker) -> MagicMock: def patch_RPCManager(mocker) -> MagicMock:
@ -691,7 +691,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order,
assert trade.amount == 90.99181073703367 assert trade.amount == 90.99181073703367
assert log_has( assert log_has(
'Buy signal found: about create a new trade with stake_amount: 0.001000 ...', 'Buy signal found: about create a new trade with stake_amount: 0.001 ...',
caplog.record_tuples caplog.record_tuples
) )
@ -807,25 +807,61 @@ def test_process_trade_no_whitelist_pair(
assert result is True assert result is True
def test_process_informative_pairs_added(default_conf, ticker, markets, mocker) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
def _refresh_whitelist(list):
return ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC']
refresh_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_ticker=ticker,
get_markets=markets,
buy=MagicMock(side_effect=TemporaryError),
refresh_latest_ohlcv=refresh_mock,
)
inf_pairs = MagicMock(return_value=[("BTC/ETH", '1m'), ("ETH/USDT", "1h")])
mocker.patch('time.sleep', return_value=None)
freqtrade = FreqtradeBot(default_conf)
freqtrade.pairlists._validate_whitelist = _refresh_whitelist
freqtrade.strategy.informative_pairs = inf_pairs
# patch_get_signal(freqtrade)
freqtrade._process()
assert inf_pairs.call_count == 1
assert refresh_mock.call_count == 1
assert ("BTC/ETH", "1m") in refresh_mock.call_args[0][0]
assert ("ETH/USDT", "1h") in refresh_mock.call_args[0][0]
assert ("ETH/BTC", default_conf["ticker_interval"]) in refresh_mock.call_args[0][0]
def test_balance_fully_ask_side(mocker, default_conf) -> None: def test_balance_fully_ask_side(mocker, default_conf) -> None:
default_conf['bid_strategy']['ask_last_balance'] = 0.0 default_conf['bid_strategy']['ask_last_balance'] = 0.0
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
MagicMock(return_value={'ask': 20, 'last': 10}))
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 20, 'last': 10}) == 20 assert freqtrade.get_target_bid('ETH/BTC') == 20
def test_balance_fully_last_side(mocker, default_conf) -> None: def test_balance_fully_last_side(mocker, default_conf) -> None:
default_conf['bid_strategy']['ask_last_balance'] = 1.0 default_conf['bid_strategy']['ask_last_balance'] = 1.0
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
MagicMock(return_value={'ask': 20, 'last': 10}))
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 20, 'last': 10}) == 10 assert freqtrade.get_target_bid('ETH/BTC') == 10
def test_balance_bigger_last_ask(mocker, default_conf) -> None: def test_balance_bigger_last_ask(mocker, default_conf) -> None:
default_conf['bid_strategy']['ask_last_balance'] = 1.0 default_conf['bid_strategy']['ask_last_balance'] = 1.0
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 5, 'last': 10}) == 5 MagicMock(return_value={'ask': 5, 'last': 10}))
assert freqtrade.get_target_bid('ETH/BTC') == 5
def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> None: def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> None:
@ -1014,6 +1050,211 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
assert trade.is_open is False assert trade.is_open is False
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
markets, limit_buy_order, limit_sell_order) -> None:
# When trailing stoploss is set
stoploss_limit = MagicMock(return_value={'id': 13434334})
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_ticker=MagicMock(return_value={
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee,
get_markets=markets,
stoploss_limit=stoploss_limit
)
# enabling TSL
default_conf['trailing_stop'] = True
# disabling ROI
default_conf['minimal_roi']['0'] = 999999999
freqtrade = FreqtradeBot(default_conf)
# enabling stoploss on exchange
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
# setting stoploss
freqtrade.strategy.stoploss = -0.05
# setting stoploss_on_exchange_interval to 60 seconds
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
patch_get_signal(freqtrade)
freqtrade.create_trade()
trade = Trade.query.first()
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = 100
stoploss_order_hanging = MagicMock(return_value={
'id': 100,
'status': 'open',
'type': 'stop_loss_limit',
'price': 3,
'average': 2,
'info': {
'stopPrice': '0.000011134'
}
})
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
# stoploss initially at 5%
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False
# price jumped 2x
mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={
'bid': 0.00002344,
'ask': 0.00002346,
'last': 0.00002344
}))
cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock)
# stoploss should not be updated as the interval is 60 seconds
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False
cancel_order_mock.assert_not_called()
stoploss_order_mock.assert_not_called()
assert freqtrade.handle_trade(trade) is False
assert trade.stop_loss == 0.00002344 * 0.95
# setting stoploss_on_exchange_interval to 0 seconds
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
assert freqtrade.handle_stoploss_on_exchange(trade) is False
cancel_order_mock.assert_called_once_with(100, 'ETH/BTC')
stoploss_order_mock.assert_called_once_with(amount=85.25149190110828,
pair='ETH/BTC',
rate=0.00002344 * 0.95 * 0.99,
stop_price=0.00002344 * 0.95)
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
markets, limit_buy_order, limit_sell_order) -> None:
# When trailing stoploss is set
stoploss_limit = MagicMock(return_value={'id': 13434334})
patch_RPCManager(mocker)
patch_exchange(mocker)
patch_edge(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_ticker=MagicMock(return_value={
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee,
get_markets=markets,
stoploss_limit=stoploss_limit
)
# enabling TSL
edge_conf['trailing_stop'] = True
edge_conf['trailing_stop_positive'] = 0.01
edge_conf['trailing_stop_positive_offset'] = 0.011
# disabling ROI
edge_conf['minimal_roi']['0'] = 999999999
freqtrade = FreqtradeBot(edge_conf)
# enabling stoploss on exchange
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
# setting stoploss
freqtrade.strategy.stoploss = -0.02
# setting stoploss_on_exchange_interval to 0 second
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
patch_get_signal(freqtrade)
freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist)
freqtrade.create_trade()
trade = Trade.query.first()
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = 100
stoploss_order_hanging = MagicMock(return_value={
'id': 100,
'status': 'open',
'type': 'stop_loss_limit',
'price': 3,
'average': 2,
'info': {
'stopPrice': '0.000009384'
}
})
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
# stoploss initially at 20% as edge dictated it.
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.stop_loss == 0.000009384
cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock)
# price goes down 5%
mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={
'bid': 0.00001172 * 0.95,
'ask': 0.00001173 * 0.95,
'last': 0.00001172 * 0.95
}))
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False
# stoploss should remain the same
assert trade.stop_loss == 0.000009384
# stoploss on exchange should not be canceled
cancel_order_mock.assert_not_called()
# price jumped 2x
mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={
'bid': 0.00002344,
'ask': 0.00002346,
'last': 0.00002344
}))
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False
# stoploss should be set to 1% as trailing is on
assert trade.stop_loss == 0.00002344 * 0.99
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
stoploss_order_mock.assert_called_once_with(amount=2131074.168797954,
pair='NEO/BTC',
rate=0.00002344 * 0.99 * 0.99,
stop_price=0.00002344 * 0.99)
def test_process_maybe_execute_buy(mocker, default_conf) -> None: def test_process_maybe_execute_buy(mocker, default_conf) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
@ -1082,7 +1323,7 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf,
side_effect=OperationalException() side_effect=OperationalException()
) )
freqtrade.process_maybe_execute_sell(trade) freqtrade.process_maybe_execute_sell(trade)
assert log_has('could not update trade amount: ', caplog.record_tuples) assert log_has('Could not update trade amount: ', caplog.record_tuples)
# Test raise of DependencyException exception # Test raise of DependencyException exception
mocker.patch( mocker.patch(
@ -1318,6 +1559,47 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe
assert nb_trades == 0 assert nb_trades == 0
def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old,
fee, mocker, caplog) -> None:
""" Handle Buy order cancelled on exchange"""
rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock()
patch_exchange(mocker)
limit_buy_order_old.update({"status": "canceled"})
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old),
cancel_order=cancel_order_mock,
get_fee=fee
)
freqtrade = FreqtradeBot(default_conf)
trade_buy = Trade(
pair='ETH/BTC',
open_rate=0.00001099,
exchange='bittrex',
open_order_id='123456789',
amount=90.99181073,
fee_open=0.0,
fee_close=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
)
Trade.session.add(trade_buy)
# check it does cancel buy orders over the time limit
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0
assert rpc_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
nb_trades = len(trades)
assert nb_trades == 0
assert log_has_re("Buy order canceled on Exchange for Trade.*", caplog.record_tuples)
def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_order_old, def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_order_old,
fee, mocker) -> None: fee, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
@ -1392,6 +1674,45 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
assert trade_sell.is_open is True assert trade_sell.is_open is True
def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old,
mocker, caplog) -> None:
""" Handle sell order cancelled on exchange"""
rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock()
limit_sell_order_old.update({"status": "canceled"})
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_ticker=ticker,
get_order=MagicMock(return_value=limit_sell_order_old),
cancel_order=cancel_order_mock
)
freqtrade = FreqtradeBot(default_conf)
trade_sell = Trade(
pair='ETH/BTC',
open_rate=0.00001099,
exchange='bittrex',
open_order_id='123456789',
amount=90.99181073,
fee_open=0.0,
fee_close=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(hours=-5).datetime,
close_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=False
)
Trade.session.add(trade_sell)
# check it does cancel sell orders over the time limit
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0
assert rpc_mock.call_count == 1
assert trade_sell.is_open is True
assert log_has_re("Sell order canceled on exchange for Trade.*", caplog.record_tuples)
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial, def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
mocker) -> None: mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
@ -1508,7 +1829,8 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None:
trade = MagicMock() trade = MagicMock()
order = {'remaining': 1, order = {'remaining': 1,
'amount': 1} 'amount': 1,
'status': "open"}
assert freqtrade.handle_timedout_limit_sell(trade, order) assert freqtrade.handle_timedout_limit_sell(trade, order)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
order['amount'] = 2 order['amount'] = 2
@ -2066,6 +2388,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog,
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order) trade.update(limit_buy_order)
trade.max_rate = trade.open_rate * 1.003
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
# Sell as trailing-stop is reached # Sell as trailing-stop is reached
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
@ -2495,10 +2818,13 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets)
instead of the ask rate instead of the ask rate
""" """
patch_exchange(mocker) patch_exchange(mocker)
ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046})
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
get_markets=markets, get_markets=markets,
get_order_book=order_book_l2 get_order_book=order_book_l2,
get_ticker=ticker_mock,
) )
default_conf['exchange']['name'] = 'binance' default_conf['exchange']['name'] = 'binance'
default_conf['bid_strategy']['use_order_book'] = True default_conf['bid_strategy']['use_order_book'] = True
@ -2507,7 +2833,8 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets)
default_conf['telegram']['enabled'] = False default_conf['telegram']['enabled'] = False
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 0.045, 'last': 0.046}) == 0.043935 assert freqtrade.get_target_bid('ETH/BTC') == 0.043935
assert ticker_mock.call_count == 0
def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets) -> None: def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets) -> None:
@ -2516,10 +2843,13 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets)
instead of the order book rate (even if enabled) instead of the order book rate (even if enabled)
""" """
patch_exchange(mocker) patch_exchange(mocker)
ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046})
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
get_markets=markets, get_markets=markets,
get_order_book=order_book_l2 get_order_book=order_book_l2,
get_ticker=ticker_mock,
) )
default_conf['exchange']['name'] = 'binance' default_conf['exchange']['name'] = 'binance'
default_conf['bid_strategy']['use_order_book'] = True default_conf['bid_strategy']['use_order_book'] = True
@ -2528,29 +2858,9 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets)
default_conf['telegram']['enabled'] = False default_conf['telegram']['enabled'] = False
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 0.042, 'last': 0.046}) == 0.042 # ordrebook shall be used even if tickers would be lower.
assert freqtrade.get_target_bid('ETH/BTC', ) != 0.042
assert ticker_mock.call_count == 0
def test_order_book_bid_strategy3(default_conf, mocker, order_book_l2, markets) -> None:
"""
test if function get_target_bid will return ask rate instead
of the order book rate
"""
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_markets=markets,
get_order_book=order_book_l2
)
default_conf['exchange']['name'] = 'binance'
default_conf['bid_strategy']['use_order_book'] = True
default_conf['bid_strategy']['order_book_top'] = 1
default_conf['bid_strategy']['ask_last_balance'] = 0
default_conf['telegram']['enabled'] = False
freqtrade = FreqtradeBot(default_conf)
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 0.03, 'last': 0.029}) == 0.03
def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2, markets) -> None: def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2, markets) -> None:

View File

@ -5,8 +5,8 @@ from unittest.mock import MagicMock
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.misc import (common_datearray, datesarray_to_datetimearray, from freqtrade.misc import (common_datearray, datesarray_to_datetimearray,
file_dump_json, format_ms_time, shorten_date) file_dump_json, file_load_json, format_ms_time, shorten_date)
from freqtrade.data.history import load_tickerdata_file from freqtrade.data.history import load_tickerdata_file, make_testdata_path
from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.default_strategy import DefaultStrategy
@ -17,7 +17,7 @@ def test_shorten_date() -> None:
def test_datesarray_to_datetimearray(ticker_history_list): def test_datesarray_to_datetimearray(ticker_history_list):
dataframes = parse_ticker_dataframe(ticker_history_list) dataframes = parse_ticker_dataframe(ticker_history_list, "5m", fill_missing=True)
dates = datesarray_to_datetimearray(dataframes['date']) dates = datesarray_to_datetimearray(dataframes['date'])
assert isinstance(dates[0], datetime.datetime) assert isinstance(dates[0], datetime.datetime)
@ -34,29 +34,42 @@ def test_datesarray_to_datetimearray(ticker_history_list):
def test_common_datearray(default_conf) -> None: def test_common_datearray(default_conf) -> None:
strategy = DefaultStrategy(default_conf) strategy = DefaultStrategy(default_conf)
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)} tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, "1m", fill_missing=True)}
dataframes = strategy.tickerdata_to_dataframe(tickerlist) dataframes = strategy.tickerdata_to_dataframe(tickerlist)
dates = common_datearray(dataframes) dates = common_datearray(dataframes)
assert dates.size == dataframes['UNITTEST/BTC']['date'].size assert dates.size == dataframes['UNITTEST/BTC']['date'].size
assert dates[0] == dataframes['UNITTEST/BTC']['date'][0] assert dates[0] == dataframes['UNITTEST/BTC']['date'][0]
assert dates[-1] == dataframes['UNITTEST/BTC']['date'][-1] assert dates[-1] == dataframes['UNITTEST/BTC']['date'].iloc[-1]
def test_file_dump_json(mocker) -> None: def test_file_dump_json(mocker) -> None:
file_open = mocker.patch('freqtrade.misc.open', MagicMock()) file_open = mocker.patch('freqtrade.misc.open', MagicMock())
json_dump = mocker.patch('json.dump', MagicMock()) json_dump = mocker.patch('rapidjson.dump', MagicMock())
file_dump_json('somefile', [1, 2, 3]) file_dump_json('somefile', [1, 2, 3])
assert file_open.call_count == 1 assert file_open.call_count == 1
assert json_dump.call_count == 1 assert json_dump.call_count == 1
file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock()) file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock())
json_dump = mocker.patch('json.dump', MagicMock()) json_dump = mocker.patch('rapidjson.dump', MagicMock())
file_dump_json('somefile', [1, 2, 3], True) file_dump_json('somefile', [1, 2, 3], True)
assert file_open.call_count == 1 assert file_open.call_count == 1
assert json_dump.call_count == 1 assert json_dump.call_count == 1
def test_file_load_json(mocker) -> None:
# 7m .json does not exist
ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-7m.json'))
assert not ret
# 1m json exists (but no .gz exists)
ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-1m.json'))
assert ret
# 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json
ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-8m.json'))
assert ret
def test_format_ms_time() -> None: def test_format_ms_time() -> None:
# Date 2018-04-10 18:02:01 # Date 2018-04-10 18:02:01
date_in_epoch_ms = 1523383321000 date_in_epoch_ms = 1523383321000

View File

@ -516,6 +516,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.strategy is None assert trade.strategy is None
assert trade.ticker_interval is None assert trade.ticker_interval is None
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
assert trade.stoploss_last_update is None
assert log_has("trying trades_bak1", caplog.record_tuples) assert log_has("trying trades_bak1", caplog.record_tuples)
assert log_has("trying trades_bak2", caplog.record_tuples) assert log_has("trying trades_bak2", caplog.record_tuples)
assert log_has("Running database migration - backup available as trades_bak2", assert log_has("Running database migration - backup available as trades_bak2",

View File

@ -236,7 +236,7 @@ def crossed(series1, series2, direction=None):
if direction is None: if direction is None:
return above or below return above or below
return above if direction is "above" else below return above if direction == "above" else below
def crossed_above(series1, series2): def crossed_above(series1, series2):

View File

@ -2,12 +2,12 @@
""" Wallet """ """ Wallet """
import logging import logging
from typing import Dict, Any, NamedTuple from typing import Dict, Any, NamedTuple
from collections import namedtuple
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# wallet data structure
class Wallet(NamedTuple): class Wallet(NamedTuple):
exchange: str exchange: str
currency: str currency: str
@ -18,12 +18,6 @@ class Wallet(NamedTuple):
class Wallets(object): class Wallets(object):
# wallet data structure
wallet = namedtuple(
'wallet',
['exchange', 'currency', 'free', 'used', 'total']
)
def __init__(self, exchange: Exchange) -> None: def __init__(self, exchange: Exchange) -> None:
self.exchange = exchange self.exchange = exchange
self.wallets: Dict[str, Any] = {} self.wallets: Dict[str, Any] = {}

43
mkdocs.yml Normal file
View File

@ -0,0 +1,43 @@
site_name: Freqtrade
nav:
- About: index.md
- Installation: installation.md
- Configuration: configuration.md
- Start the bot: bot-usage.md
- Stoploss: stoploss.md
- Custom Strategy: bot-optimization.md
- Telegram: telegram-usage.md
- Web Hook: webhook-config.md
- Backtesting: backtesting.md
- Hyperopt: hyperopt.md
- Edge positioning: edge.md
- Plotting: plotting.md
- FAQ: faq.md
- SQL Cheatsheet: sql_cheatsheet.md
- Sanbox testing: sandbox-testing.md
- Contributors guide: developer.md
theme:
name: material
logo: 'images/logo.png'
custom_dir: 'docs'
palette:
primary: 'blue grey'
accent: 'tear'
markdown_extensions:
- admonition
- codehilite:
guess_lang: false
- toc:
permalink: true
- pymdownx.arithmatex
- pymdownx.caret
- pymdownx.critic
- pymdownx.details
- pymdownx.inlinehilite
- pymdownx.magiclink
- pymdownx.mark
- pymdownx.smartsymbols
- pymdownx.superfences
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.tilde

View File

@ -1,8 +1,12 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
flake8==3.6.0 flake8==3.7.6
pytest==4.0.2 flake8-type-annotations==0.1.0
pytest-mock==1.10.0 flake8-tidy-imports==2.0.0
pytest-asyncio==0.9.0 pytest==4.3.0
pytest-cov==2.6.0 pytest-mock==1.10.1
pytest-asyncio==0.10.0
pytest-cov==2.6.1
coveralls==1.6.0
mypy==0.670

5
requirements-plot.txt Normal file
View File

@ -0,0 +1,5 @@
# Include all requirements to run the bot.
-r requirements.txt
plotly==3.6.1

View File

@ -1,19 +1,19 @@
ccxt==1.18.71 ccxt==1.18.270
SQLAlchemy==1.2.15 SQLAlchemy==1.2.18
python-telegram-bot==11.1.0 python-telegram-bot==11.1.0
arrow==0.12.1 arrow==0.13.1
cachetools==3.0.0 cachetools==3.1.0
requests==2.21.0 requests==2.21.0
urllib3==1.24.1 urllib3==1.24.1
wrapt==1.10.11 wrapt==1.11.1
pandas==0.23.4 numpy==1.16.1
pandas==0.24.1
scikit-learn==0.20.2 scikit-learn==0.20.2
joblib==0.13.0 joblib==0.13.2
scipy==1.2.0 scipy==1.2.1
jsonschema==2.6.0 jsonschema==2.6.0
numpy==1.15.4
TA-Lib==0.4.17 TA-Lib==0.4.17
tabulate==0.8.2 tabulate==0.8.3
coinmarketcap==5.0.3 coinmarketcap==5.0.3
# Required for hyperopt # Required for hyperopt
@ -23,4 +23,4 @@ scikit-optimize==0.5.2
py_find_1st==1.1.3 py_find_1st==1.1.3
#Load ticker files 30% faster #Load ticker files 30% faster
ujson==1.35 python-rapidjson==0.7.0

View File

@ -31,6 +31,7 @@ if args.config:
configuration = Configuration(args) configuration = Configuration(args)
config = configuration._load_config_file(args.config) config = configuration._load_config_file(args.config)
config['stake_currency'] = ''
# Ensure we do not use Exchange credentials # Ensure we do not use Exchange credentials
config['exchange']['key'] = '' config['exchange']['key'] = ''
config['exchange']['secret'] = '' config['exchange']['secret'] = ''

View File

@ -1,18 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Script to display when the bot will buy a specific pair Script to display when the bot will buy on specific pair(s)
Mandatory Cli parameters: Mandatory Cli parameters:
-p / --pair: pair to examine -p / --pairs: pair(s) to examine
Option but recommended Option but recommended
-s / --strategy: strategy to use -s / --strategy: strategy to use
Optional Cli parameters Optional Cli parameters
-d / --datadir: path to pair backtest data -d / --datadir: path to pair(s) backtest data
--timerange: specify what timerange of data to use. --timerange: specify what timerange of data to use.
-l / --live: Live, to download the latest ticker for the pair -l / --live: Live, to download the latest ticker for the pair(s)
-db / --db-url: Show trades stored in database -db / --db-url: Show trades stored in database
@ -21,8 +21,8 @@ Row 1: sma, ema3, ema5, ema10, ema50
Row 3: macd, rsi, fisher_rsi, mfi, slowd, slowk, fastd, fastk Row 3: macd, rsi, fisher_rsi, mfi, slowd, slowk, fastd, fastk
Example of usage: Example of usage:
> python3 scripts/plot_dataframe.py --pair BTC/EUR -d user_data/data/ --indicators1 sma,ema3 > python3 scripts/plot_dataframe.py --pairs BTC/EUR,XRP/BTC -d user_data/data/
--indicators2 fastk,fastd --indicators1 sma,ema3 --indicators2 fastk,fastd
""" """
import json import json
import logging import logging
@ -65,7 +65,8 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram
t.open_date.replace(tzinfo=timeZone), t.open_date.replace(tzinfo=timeZone),
t.close_date.replace(tzinfo=timeZone) if t.close_date else None, t.close_date.replace(tzinfo=timeZone) if t.close_date else None,
t.open_rate, t.close_rate, t.open_rate, t.close_rate,
t.close_date.timestamp() - t.open_date.timestamp() if t.close_date else None) t.close_date.timestamp() - t.open_date.timestamp()
if t.close_date else None)
for t in Trade.query.filter(Trade.pair.is_(pair)).all()], for t in Trade.query.filter(Trade.pair.is_(pair)).all()],
columns=columns) columns=columns)
@ -74,6 +75,7 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram
# must align with columns in backtest.py # must align with columns in backtest.py
columns = ["pair", "profit", "opents", "closets", "index", "duration", columns = ["pair", "profit", "opents", "closets", "index", "duration",
"open_rate", "close_rate", "open_at_end", "sell_reason"] "open_rate", "close_rate", "open_at_end", "sell_reason"]
if file.exists():
with file.open() as f: with file.open() as f:
data = json.load(f) data = json.load(f)
trades = pd.DataFrame(data, columns=columns) trades = pd.DataFrame(data, columns=columns)
@ -84,42 +86,55 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram
if timerange.stoptype == 'date': if timerange.stoptype == 'date':
trades = trades.loc[trades["opents"] <= timerange.stopts] trades = trades.loc[trades["opents"] <= timerange.stopts]
trades['opents'] = pd.to_datetime(trades['opents'], trades['opents'] = pd.to_datetime(
trades['opents'],
unit='s', unit='s',
utc=True, utc=True,
infer_datetime_format=True) infer_datetime_format=True)
trades['closets'] = pd.to_datetime(trades['closets'], trades['closets'] = pd.to_datetime(
trades['closets'],
unit='s', unit='s',
utc=True, utc=True,
infer_datetime_format=True) infer_datetime_format=True)
else:
trades = pd.DataFrame([], columns=columns)
return trades return trades
def plot_analyzed_dataframe(args: Namespace) -> None: def generate_plot_file(fig, pair, tick_interval, is_last) -> None:
""" """
Calls analyze() and plots the returned dataframe Generate a plot html file from pre populated fig plotly object
:return: None :return: None
""" """
logger.info('Generate plot file for %s', pair)
pair_name = pair.replace("/", "_")
file_name = 'freqtrade-plot-' + pair_name + '-' + tick_interval + '.html'
Path("user_data/plots").mkdir(parents=True, exist_ok=True)
plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), auto_open=False)
if is_last:
plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')), auto_open=False)
def get_trading_env(args: Namespace):
"""
Initalize freqtrade Exchange and Strategy, split pairs recieved in parameter
:return: Strategy
"""
global _CONF global _CONF
# Load the configuration # Load the configuration
_CONF.update(setup_configuration(args)) _CONF.update(setup_configuration(args))
print(_CONF) print(_CONF)
# Set the pair to audit
pair = args.pair
if pair is None: pairs = args.pairs.split(',')
logger.critical('Parameter --pair mandatory;. E.g --pair ETH/BTC') if pairs is None:
logger.critical('Parameter --pairs mandatory;. E.g --pairs ETH/BTC,XRP/BTC')
exit() exit()
if '/' not in pair:
logger.critical('--pair format must be XXX/YYY')
exit()
# Set timerange to use
timerange = Arguments.parse_timerange(args.timerange)
# Load the strategy # Load the strategy
try: try:
strategy = StrategyResolver(_CONF).strategy strategy = StrategyResolver(_CONF).strategy
@ -131,61 +146,84 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
) )
exit() exit()
# Set the ticker to use return [strategy, exchange, pairs]
tick_interval = strategy.ticker_interval
def get_tickers_data(strategy, exchange, pairs: List[str], args):
"""
Get tickers data for each pairs on live or local, option defined in args
:return: dictinnary of tickers. output format: {'pair': tickersdata}
"""
tick_interval = strategy.ticker_interval
timerange = Arguments.parse_timerange(args.timerange)
# Load pair tickers
tickers = {} tickers = {}
if args.live: if args.live:
logger.info('Downloading pair.') logger.info('Downloading pairs.')
exchange.refresh_tickers([pair], tick_interval) exchange.refresh_latest_ohlcv([(pair, tick_interval) for pair in pairs])
tickers[pair] = exchange.klines(pair) for pair in pairs:
tickers[pair] = exchange.klines((pair, tick_interval))
else: else:
tickers = history.load_data( tickers = history.load_data(
datadir=Path(_CONF.get("datadir")), datadir=Path(_CONF.get("datadir")),
pairs=[pair], pairs=pairs,
ticker_interval=tick_interval, ticker_interval=tick_interval,
refresh_pairs=_CONF.get('refresh_pairs', False), refresh_pairs=_CONF.get('refresh_pairs', False),
timerange=timerange, timerange=timerange,
exchange=Exchange(_CONF) exchange=Exchange(_CONF)
) )
# No ticker found, or impossible to download # No ticker found, impossible to download, len mismatch
if tickers == {}: for pair, data in tickers.copy().items():
exit() logger.debug("checking tickers data of pair: %s", pair)
logger.debug("data.empty: %s", data.empty)
logger.debug("len(data): %s", len(data))
if data.empty:
del tickers[pair]
logger.info(
'An issue occured while retreiving datas of %s pair, please retry '
'using -l option for live or --refresh-pairs-cached', pair)
return tickers
# Get trades already made from the DB
trades = load_trades(args, pair, timerange) def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame:
"""
Get tickers then Populate strategy indicators and signals, then return the full dataframe
:return: the DataFrame of a pair
"""
dataframes = strategy.tickerdata_to_dataframe(tickers) dataframes = strategy.tickerdata_to_dataframe(tickers)
dataframe = dataframes[pair] dataframe = dataframes[pair]
dataframe = strategy.advise_buy(dataframe, {'pair': pair}) dataframe = strategy.advise_buy(dataframe, {'pair': pair})
dataframe = strategy.advise_sell(dataframe, {'pair': pair}) dataframe = strategy.advise_sell(dataframe, {'pair': pair})
if len(dataframe.index) > args.plot_limit: return dataframe
logger.warning('Ticker contained more than %s candles as defined '
'with --plot-limit, clipping.', args.plot_limit)
dataframe = dataframe.tail(args.plot_limit)
def extract_trades_of_period(dataframe, trades) -> pd.DataFrame:
"""
Compare trades and backtested pair DataFrames to get trades performed on backtested period
:return: the DataFrame of a trades of period
"""
trades = trades.loc[trades['opents'] >= dataframe.iloc[0]['date']] trades = trades.loc[trades['opents'] >= dataframe.iloc[0]['date']]
fig = generate_graph( return trades
pair=pair,
trades=trades,
data=dataframe,
args=args
)
plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')))
def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tools.make_subplots: def generate_graph(
pair: str,
trades: pd.DataFrame,
data: pd.DataFrame,
indicators1: str,
indicators2: str
) -> tools.make_subplots:
""" """
Generate the graph from the data generated by Backtesting or from DB Generate the graph from the data generated by Backtesting or from DB
:param pair: Pair to Display on the graph :param pair: Pair to Display on the graph
:param trades: All trades created :param trades: All trades created
:param data: Dataframe :param data: Dataframe
:param args: sys.argv that contrains the two params indicators1, and indicators2 :indicators1: String Main plot indicators
:indicators2: String Sub plot indicators
:return: None :return: None
""" """
@ -201,6 +239,7 @@ def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tool
fig['layout']['yaxis1'].update(title='Price') fig['layout']['yaxis1'].update(title='Price')
fig['layout']['yaxis2'].update(title='Volume') fig['layout']['yaxis2'].update(title='Volume')
fig['layout']['yaxis3'].update(title='Other') fig['layout']['yaxis3'].update(title='Other')
fig['layout']['xaxis']['rangeslider'].update(visible=False)
# Common information # Common information
candles = go.Candlestick( candles = go.Candlestick(
@ -285,7 +324,7 @@ def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tool
fig.append_trace(bb_lower, 1, 1) fig.append_trace(bb_lower, 1, 1)
fig.append_trace(bb_upper, 1, 1) fig.append_trace(bb_upper, 1, 1)
fig = generate_row(fig=fig, row=1, raw_indicators=args.indicators1, data=data) fig = generate_row(fig=fig, row=1, raw_indicators=indicators1, data=data)
fig.append_trace(buys, 1, 1) fig.append_trace(buys, 1, 1)
fig.append_trace(sells, 1, 1) fig.append_trace(sells, 1, 1)
fig.append_trace(trade_buys, 1, 1) fig.append_trace(trade_buys, 1, 1)
@ -300,7 +339,7 @@ def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tool
fig.append_trace(volume, 2, 1) fig.append_trace(volume, 2, 1)
# Row 3 # Row 3
fig = generate_row(fig=fig, row=3, raw_indicators=args.indicators2, data=data) fig = generate_row(fig=fig, row=3, raw_indicators=indicators2, data=data)
return fig return fig
@ -349,7 +388,7 @@ def plot_parse_args(args: List[str]) -> Namespace:
help='Set indicators from your strategy you want in the third row of the graph. Separate ' help='Set indicators from your strategy you want in the third row of the graph. Separate '
'them with a coma. E.g: fastd,fastk (default: %(default)s)', 'them with a coma. E.g: fastd,fastk (default: %(default)s)',
type=str, type=str,
default='macd', default='macd,macdsignal',
dest='indicators2', dest='indicators2',
) )
arguments.parser.add_argument( arguments.parser.add_argument(
@ -366,15 +405,58 @@ def plot_parse_args(args: List[str]) -> Namespace:
return arguments.parse_args() return arguments.parse_args()
def analyse_and_plot_pairs(args: Namespace):
"""
From arguments provided in cli:
-Initialise backtest env
-Get tickers data
-Generate Dafaframes populated with indicators and signals
-Load trades excecuted on same periods
-Generate Plotly plot objects
-Generate plot files
:return: None
"""
strategy, exchange, pairs = get_trading_env(args)
# Set timerange to use
timerange = Arguments.parse_timerange(args.timerange)
tick_interval = strategy.ticker_interval
tickers = get_tickers_data(strategy, exchange, pairs, args)
pair_counter = 0
for pair, data in tickers.items():
pair_counter += 1
logger.info("analyse pair %s", pair)
tickers = {}
tickers[pair] = data
dataframe = generate_dataframe(strategy, tickers, pair)
trades = load_trades(args, pair, timerange)
trades = extract_trades_of_period(dataframe, trades)
fig = generate_graph(
pair=pair,
trades=trades,
data=dataframe,
indicators1=args.indicators1,
indicators2=args.indicators2
)
is_last = (False, True)[pair_counter == len(tickers)]
generate_plot_file(fig, pair, tick_interval, is_last)
logger.info('End of ploting process %s plots generated', pair_counter)
def main(sysargv: List[str]) -> None: def main(sysargv: List[str]) -> None:
""" """
This function will initiate the bot and start the trading loop. This function will initiate the bot and start the trading loop.
:return: None :return: None
""" """
logger.info('Starting Plot Dataframe') logger.info('Starting Plot Dataframe')
plot_analyzed_dataframe( analyse_and_plot_pairs(
plot_parse_args(sysargv) plot_parse_args(sysargv)
) )
exit()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -29,6 +29,7 @@ from freqtrade.configuration import Configuration
from freqtrade import constants from freqtrade import constants
from freqtrade.data import history from freqtrade.data import history
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from freqtrade.state import RunMode
import freqtrade.misc as misc import freqtrade.misc as misc
@ -82,7 +83,7 @@ def plot_profit(args: Namespace) -> None:
# to match the tickerdata against the profits-results # to match the tickerdata against the profits-results
timerange = Arguments.parse_timerange(args.timerange) timerange = Arguments.parse_timerange(args.timerange)
config = Configuration(args).get_config() config = Configuration(args, RunMode.OTHER).get_config()
# Init strategy # Init strategy
try: try:
@ -107,8 +108,8 @@ def plot_profit(args: Namespace) -> None:
exit(1) exit(1)
# Take pairs from the cli otherwise switch to the pair in the config file # Take pairs from the cli otherwise switch to the pair in the config file
if args.pair: if args.pairs:
filter_pairs = args.pair filter_pairs = args.pairs
filter_pairs = filter_pairs.split(',') filter_pairs = filter_pairs.split(',')
else: else:
filter_pairs = config['exchange']['pair_whitelist'] filter_pairs = config['exchange']['pair_whitelist']

View File

@ -11,7 +11,7 @@ from freqtrade import __version__
setup(name='freqtrade', setup(name='freqtrade',
version=__version__, version=__version__,
description='Simple High Frequency Trading Bot for crypto currencies', description='Crypto Trading Bot',
url='https://github.com/freqtrade/freqtrade', url='https://github.com/freqtrade/freqtrade',
author='gcarq and contributors', author='gcarq and contributors',
author_email='michael.egger@tsn.at', author_email='michael.egger@tsn.at',
@ -38,7 +38,7 @@ setup(name='freqtrade',
'cachetools', 'cachetools',
'coinmarketcap', 'coinmarketcap',
'scikit-optimize', 'scikit-optimize',
'ujson', 'python-rapidjson',
'py_find_1st' 'py_find_1st'
], ],
include_package_data=True, include_package_data=True,

View File

@ -17,23 +17,27 @@ function check_installed_python() {
return return
fi fi
if [ -z ${PYTHON} ]; then
echo "No usable python found. Please make sure to have python3.6 or python3.7 installed"
exit 1
fi
} }
function updateenv() { function updateenv() {
echo "-------------------------" echo "-------------------------"
echo "Update your virtual env" echo "Updating your virtual env"
echo "-------------------------" echo "-------------------------"
source .env/bin/activate source .env/bin/activate
echo "pip3 install in-progress. Please wait..." echo "pip3 install in-progress. Please wait..."
pip3 install --quiet --upgrade pip # Install numpy first to have py_find_1st install clean
pip3 install --quiet -r requirements.txt --upgrade pip3 install --upgrade pip numpy
pip3 install --quiet -r requirements.txt pip3 install --upgrade -r requirements.txt
read -p "Do you want to install dependencies for dev [Y/N]? " read -p "Do you want to install dependencies for dev [y/N]? "
if [[ $REPLY =~ ^[Yy]$ ]] if [[ $REPLY =~ ^[Yy]$ ]]
then then
pip3 install --quiet -r requirements-dev.txt --upgrade pip3 install --upgrade -r requirements-dev.txt
pip3 install --quiet -r requirements-dev.txt
else else
echo "Dev dependencies ignored." echo "Dev dependencies ignored."
fi fi
@ -45,6 +49,12 @@ function updateenv () {
# Install tab lib # Install tab lib
function install_talib() { function install_talib() {
if [ -f /usr/local/lib/libta_lib.a ]; then
echo "ta-lib already installed, skipping"
return
fi
cd build_helpers
tar zxvf ta-lib-0.4.0-src.tar.gz tar zxvf ta-lib-0.4.0-src.tar.gz
cd ta-lib cd ta-lib
sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
@ -52,6 +62,7 @@ function install_talib () {
make make
sudo make install sudo make install
cd .. && rm -rf ./ta-lib/ cd .. && rm -rf ./ta-lib/
cd ..
} }
# Install bot MacOS # Install bot MacOS
@ -59,7 +70,7 @@ function install_macos () {
if [ ! -x "$(command -v brew)" ] if [ ! -x "$(command -v brew)" ]
then then
echo "-------------------------" echo "-------------------------"
echo "Install Brew" echo "Installing Brew"
echo "-------------------------" echo "-------------------------"
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
fi fi
@ -85,7 +96,7 @@ function update () {
# Reset Develop or Master branch # Reset Develop or Master branch
function reset() { function reset() {
echo "----------------------------" echo "----------------------------"
echo "Reset branch and virtual env" echo "Reseting branch and virtual env"
echo "----------------------------" echo "----------------------------"
if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ] if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ]
then then
@ -131,7 +142,7 @@ function config_generator () {
echo "Starting to generate config.json" echo "Starting to generate config.json"
echo echo
echo "General configuration" echo "Generating General configuration"
echo "-------------------------" echo "-------------------------"
default_max_trades=3 default_max_trades=3
read -p "Max open trades: (Default: $default_max_trades) " max_trades read -p "Max open trades: (Default: $default_max_trades) " max_trades
@ -150,13 +161,13 @@ function config_generator () {
fiat_currency=${fiat_currency:-$default_fiat_currency} fiat_currency=${fiat_currency:-$default_fiat_currency}
echo echo
echo "Exchange config generator" echo "Generating exchange config "
echo "------------------------" echo "------------------------"
read -p "Exchange API key: " api_key read -p "Exchange API key: " api_key
read -p "Exchange API Secret: " api_secret read -p "Exchange API Secret: " api_secret
echo echo
echo "Telegram config generator" echo "Generating Telegram config"
echo "-------------------------" echo "-------------------------"
read -p "Telegram Token: " token read -p "Telegram Token: " token
read -p "Telegram Chat_id: " chat_id read -p "Telegram Chat_id: " chat_id
@ -176,11 +187,11 @@ function config_generator () {
function config() { function config() {
echo "-------------------------" echo "-------------------------"
echo "Config file generator" echo "Generating config file"
echo "-------------------------" echo "-------------------------"
if [ -f config.json ] if [ -f config.json ]
then then
read -p "A config file already exist, do you want to override it [Y/N]? " read -p "A config file already exist, do you want to override it [y/N]? "
if [[ $REPLY =~ ^[Yy]$ ]] if [[ $REPLY =~ ^[Yy]$ ]]
then then
config_generator config_generator
@ -201,7 +212,7 @@ function config () {
function install() { function install() {
echo "-------------------------" echo "-------------------------"
echo "Install mandatory dependencies" echo "Installing mandatory dependencies"
echo "-------------------------" echo "-------------------------"
if [ "$(uname -s)" == "Darwin" ] if [ "$(uname -s)" == "Darwin" ]
@ -222,7 +233,7 @@ function install () {
reset reset
config config
echo "-------------------------" echo "-------------------------"
echo "Run the bot" echo "Run the bot !"
echo "-------------------------" echo "-------------------------"
echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade/main.py'." echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade/main.py'."
} }
@ -230,7 +241,7 @@ function install () {
function plot() { function plot() {
echo " echo "
----------------------------------------- -----------------------------------------
Install dependencies for Plotting scripts Installing dependencies for Plotting scripts
----------------------------------------- -----------------------------------------
" "
pip install plotly --upgrade pip install plotly --upgrade

View File

@ -42,6 +42,7 @@ class SampleHyperOpts(IHyperOpt):
# Bollinger bands # Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower'] dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_upperband'] = bollinger['upper']
dataframe['sar'] = ta.SAR(dataframe) dataframe['sar'] = ta.SAR(dataframe)
return dataframe return dataframe
@ -66,6 +67,7 @@ class SampleHyperOpts(IHyperOpt):
conditions.append(dataframe['rsi'] < params['rsi-value']) conditions.append(dataframe['rsi'] < params['rsi-value'])
# TRIGGERS # TRIGGERS
if 'trigger' in params:
if params['trigger'] == 'bb_lower': if params['trigger'] == 'bb_lower':
conditions.append(dataframe['close'] < dataframe['bb_lowerband']) conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if params['trigger'] == 'macd_cross_signal': if params['trigger'] == 'macd_cross_signal':
@ -102,6 +104,67 @@ class SampleHyperOpts(IHyperOpt):
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
] ]
@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
"""
# print(params)
conditions = []
# GUARDS AND TRENDS
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']:
conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
if 'sell-adx-enabled' in params and params['sell-adx-enabled']:
conditions.append(dataframe['adx'] < params['sell-adx-value'])
if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']:
conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
# 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']
))
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 [
Integer(75, 100, name='sell-mfi-value'),
Integer(50, 100, name='sell-fastd-value'),
Integer(50, 100, name='sell-adx-value'),
Integer(60, 100, name='sell-rsi-value'),
Categorical([True, False], name='sell-mfi-enabled'),
Categorical([True, False], name='sell-fastd-enabled'),
Categorical([True, False], name='sell-adx-enabled'),
Categorical([True, False], name='sell-rsi-enabled'),
Categorical(['sell-bb_upper',
'sell-macd_cross_signal',
'sell-sar_reversal'], name='sell-trigger')
]
@staticmethod @staticmethod
def generate_roi_table(params: Dict) -> Dict[int, float]: def generate_roi_table(params: Dict) -> Dict[int, float]:
""" """
@ -137,3 +200,36 @@ class SampleHyperOpts(IHyperOpt):
Real(0.01, 0.07, name='roi_p2'), Real(0.01, 0.07, name='roi_p2'),
Real(0.01, 0.20, name='roi_p3'), Real(0.01, 0.20, name='roi_p3'),
] ]
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators. Should be a copy of from strategy
must align to populate_indicators in this file
Only used when --spaces does not include buy
"""
dataframe.loc[
(
(dataframe['close'] < dataframe['bb_lowerband']) &
(dataframe['mfi'] < 16) &
(dataframe['adx'] > 25) &
(dataframe['rsi'] < 21)
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators. Should be a copy of from strategy
must align to populate_indicators in this file
Only used when --spaces does not include sell
"""
dataframe.loc[
(
(qtpylib.crossed_above(
dataframe['macdsignal'], dataframe['macd']
)) &
(dataframe['fastd'] > 54)
),
'sell'] = 1
return dataframe

View File

@ -42,12 +42,22 @@ class TestStrategy(IStrategy):
# This attribute will be overridden if the config file contains "stoploss" # This attribute will be overridden if the config file contains "stoploss"
stoploss = -0.10 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 # Optimal ticker interval for the strategy
ticker_interval = '5m' ticker_interval = '5m'
# run "populate_indicators" only for new candle # run "populate_indicators" only for new candle
ta_on_candle = False ta_on_candle = False
# Experimental settings (configuration will overide these if set)
use_sell_signal = False
sell_profit_only = False
ignore_roi_if_buy_signal = False
# Optional order type mapping # Optional order type mapping
order_types = { order_types = {
'buy': 'limit', 'buy': 'limit',
@ -62,6 +72,19 @@ class TestStrategy(IStrategy):
'sell': '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: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Adds several different TA indicators to the given DataFrame Adds several different TA indicators to the given DataFrame
@ -243,7 +266,8 @@ class TestStrategy(IStrategy):
( (
(dataframe['adx'] > 30) & (dataframe['adx'] > 30) &
(dataframe['tema'] <= dataframe['bb_middleband']) & (dataframe['tema'] <= dataframe['bb_middleband']) &
(dataframe['tema'] > dataframe['tema'].shift(1)) (dataframe['tema'] > dataframe['tema'].shift(1)) &
(dataframe['volume'] > 0) # Make sure Volume is not 0
), ),
'buy'] = 1 'buy'] = 1
@ -260,7 +284,8 @@ class TestStrategy(IStrategy):
( (
(dataframe['adx'] > 70) & (dataframe['adx'] > 70) &
(dataframe['tema'] > dataframe['bb_middleband']) & (dataframe['tema'] > dataframe['bb_middleband']) &
(dataframe['tema'] < dataframe['tema'].shift(1)) (dataframe['tema'] < dataframe['tema'].shift(1)) &
(dataframe['volume'] > 0) # Make sure Volume is not 0
), ),
'sell'] = 1 'sell'] = 1
return dataframe return dataframe